Skip to content

Items

Lesson 7.1

Time to add something new to the game: an item of your own. An item needs two parts: a registration in your code, so the game knows it exists, and assets, so it has a name and a picture.

Registering an item

class RubyMod final : public clight::Mod {
public:
    void on_register() override {
        cl_item_desc d = CL_ITEM_DESC_INIT;
        d.id = CL_STR("my_mod:ruby");
        d.creative_tab = CL_STR("minecraft:ingredients");
        d.vanilla_fallback = CL_STR("minecraft:emerald");
        if (!clight::registry::item_register(&d)) {
            clight::log::error("my_mod:ruby failed to register");
        }
    }
};
  • Items are registered in on_register (the lifecycle.register event), once, while the game starts. After that the list of items is frozen, and registering fails with CL_E_WRONG_PHASE.
  • cl_item_desc is a descriptor: a C struct you fill in and pass to the game. It always starts from its _INIT macro, which sets every field to its default and fills in the struct’s size. The game uses that size to know which version of the struct your mod was built with, so a descriptor made without _INIT is refused with CL_E_INVALID_ARGUMENT.
  • The fields are cl_str: CL_STR("...") for literals, clight::s(x) for a std::string_view constant.
  • id must be in your own namespace: my_mod:.... Always write the namespace: an id without one means minecraft:, and like another mod’s namespace that gives CL_E_PERMISSION_DENIED.
  • creative_tab puts it into a creative inventory tab.
  • vanilla_fallback is the vanilla item that players without your mod see instead.
  • clight::registry::item_register returns an Expected<cl_holder>. The holder is the item’s registry entry; you rarely need it, because most functions take the id as a string.

A new or changed registration needs a game restart, like every code change in a C++ mod.

Item options

Field Default Means
id The item’s id. Required.
max_stack_size 64 How many fit in one slot, 1 to 99. Use 1 for tools.
creative_tab none "minecraft:ingredients", "minecraft:tools_and_utilities", "minecraft:combat", "minecraft:food_and_drinks", "minecraft:building_blocks", …
vanilla_fallback "minecraft:paper" What players without the mod see.
copy_from none A vanilla item to base this one on, when there is no fallback.
block none The block this item places (blocks usually set this up for you, see Blocks).
behavior none Your own code for using the item (below).
user nullptr Your pointer, handed back to the behaviour.

Assets: name and picture

The game finds an item’s look by its id. For my_mod:ruby it needs these files:

assets/my_mod/
  items/ruby.json            which model to use
  models/item/ruby.json      the model
  textures/item/ruby.png     the picture, 16 x 16 pixels
  lang/en_us.json            the name

items/ruby.json points at the model:

{
  "model": {
    "type": "minecraft:model",
    "model": "my_mod:item/ruby"
  }
}

models/item/ruby.json says “a flat item with this texture”, like most vanilla items:

{
  "parent": "minecraft:item/generated",
  "textures": {
    "layer0": "my_mod:item/ruby"
  }
}

The name goes into the language file. Item keys are item.<mod id>.<name>:

{
  "item.my_mod.ruby": "Ruby"
}

These are the same files a resource pack uses, so everything you know about resource packs works here. A missing texture shows the purple and black checkerboard; a missing name shows the key. Assets can be changed while the game runs: save the file and press F3 + T.

Using it in code

Your item is a normal item now. Everything from Players works with it:

player.give("my_mod:ruby", 5);

if (player.inventory_get(player.selected_slot()).is("my_mod:ruby")) {
    player.send_message("Shiny!");
}

Keep ids you use in several places in one constant, so a typo can’t creep in:

constexpr std::string_view kRuby = "my_mod:ruby";

Behaviour: when the item is used

behavior gives the item its own code. Right now an item can react to use: the player right-clicks with it in hand. The behaviour is a C struct with a function pointer, so you give it a plain function (or a lambda without captures):

cl_override heal_crystal_use(const cl_item_ctx* ctx, cl_interaction_result* out) {
    clight::Entity player = ctx->entity;
    player.set_health(20.0f);
    player.send_action_bar("You feel better.");
    player.consume_held(1);   // one crystal is used up
    *out = CL_RESULT_SUCCESS;
    return CL_OVERRIDE_HANDLED;
}

const cl_item_behavior kHealCrystalBehavior = {sizeof(cl_item_behavior), heal_crystal_use};

class CrystalMod final : public clight::Mod {
public:
    void on_register() override {
        cl_item_desc d = CL_ITEM_DESC_INIT;
        d.id = CL_STR("my_mod:heal_crystal");
        d.max_stack_size = 16;
        d.creative_tab = CL_STR("minecraft:tools_and_utilities");
        d.vanilla_fallback = CL_STR("minecraft:amethyst_shard");
        d.behavior = &kHealCrystalBehavior;
        clight::registry::item_register(&d);
    }
};

The game copies the behaviour struct when you register, so it could be a local variable too; a constant next to its function is just the tidy way to write it. Its first field is its size, like every descriptor; CL_ITEM_BEHAVIOR_INIT gives the same with no functions set.

The use function gets a cl_item_ctx with:

Field Means
ctx->entity The player using the item.
ctx->level The world it happens in.
ctx->hand CL_HAND_MAIN or CL_HAND_OFF.
ctx->stack A copy of the stack being used. Borrowed: don’t put it into a clight::ItemStack.
ctx->user The user pointer from the item’s descriptor.

consume_held removes one crystal from the stack in the player’s main hand, so each crystal heals once. (ctx->stack is only a copy, so changing its count would do nothing.) If your item can also be used from the offhand, check ctx->hand and shrink slot 40 with player.inventory_shrink(40, 1) instead.

The return value decides what happens next:

  • CL_OVERRIDE_HANDLED: your code handled the click. Vanilla does nothing more.
  • CL_OVERRIDE_CALL_ORIGINAL: your code didn’t handle it, so the game does what the fallback item would do.

Set *out to say how it went when you handle it: CL_RESULT_SUCCESS (the arm swings), CL_RESULT_CONSUME (it worked, no swing) or CL_RESULT_FAIL.

With a lambda

clight::registry::item(desc, behaviour) registers the item with a lambda instead of a C function. The lambda may capture what it needs, and it returns true when it handled the click (false lets the fallback item do its thing):

class CrystalMod final : public clight::Mod {
public:
    void on_register() override {
        cl_item_desc d = CL_ITEM_DESC_INIT;
        d.id = CL_STR("my_mod:heal_crystal");
        d.max_stack_size = 16;
        d.creative_tab = CL_STR("minecraft:tools_and_utilities");
        d.vanilla_fallback = CL_STR("minecraft:amethyst_shard");
        clight::registry::item(d, {.use = [this](const cl_item_ctx& ctx, cl_interaction_result& out) {
            clight::Entity player = ctx.entity;
            player.set_health(heal_to_);
            player.consume_held(1);
            out = CL_RESULT_SUCCESS;
            return true;
        }});
    }

private:
    float heal_to_ = 20.0f;
};

ctx and out are the same as above, as references. The helper uses the descriptor’s user field itself, so capture your state instead of reading ctx.user. The lambda lives as long as the mod.

Try it

Register an item my_mod:compass_stone with a stack size of 1 in the tools tab. When it is used, it tells the player their coordinates in the action bar, like x 12, y 64, z -30. Add the language entry too.

Show a solution
#include <clight.hpp>
#include <cmath>

cl_override compass_use(const cl_item_ctx* ctx, cl_interaction_result* out) {
    clight::Entity player = ctx->entity;
    auto p = player.position();
    if (!p) return CL_OVERRIDE_CALL_ORIGINAL;
    player.send_action_bar(std::format("x {}, y {}, z {}", std::floor(p->x), std::floor(p->y), std::floor(p->z)));
    *out = CL_RESULT_SUCCESS;
    return CL_OVERRIDE_HANDLED;
}

const cl_item_behavior kCompassBehavior = {sizeof(cl_item_behavior), compass_use};

class CompassStoneMod final : public clight::Mod {
public:
    void on_register() override {
        cl_item_desc d = CL_ITEM_DESC_INIT;
        d.id = CL_STR("my_mod:compass_stone");
        d.max_stack_size = 1;
        d.creative_tab = CL_STR("minecraft:tools_and_utilities");
        d.vanilla_fallback = CL_STR("minecraft:flint");
        d.behavior = &kCompassBehavior;
        if (!clight::registry::item_register(&d)) clight::log::error("compass_stone failed");
    }
};

CLIGHT_MOD(CompassStoneMod)

assets/my_mod/lang/en_us.json:

{
  "item.my_mod.compass_stone": "Compass Stone"
}

Without a texture it shows the checkerboard, which is fine for testing. Add items/, models/item/ and textures/item/ files like the ruby’s when you have a picture.

Recap: register items in on_register with a cl_item_desc from CL_ITEM_DESC_INIT and clight::registry::item_register, give them a model, texture and name under assets/<mod id>/, and add right-click code with a cl_item_behavior (a plain function) or clight::registry::item(d, {.use = lambda}).