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(thelifecycle.registerevent), once, while the game starts. After that the list of items is frozen, and registering fails withCL_E_WRONG_PHASE. cl_item_descis a descriptor: a C struct you fill in and pass to the game. It always starts from its_INITmacro, 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_INITis refused withCL_E_INVALID_ARGUMENT.- The fields are
cl_str:CL_STR("...")for literals,clight::s(x)for astd::string_viewconstant. idmust be in your own namespace:my_mod:.... Always write the namespace: an id without one meansminecraft:, and like another mod’s namespace that givesCL_E_PERMISSION_DENIED.creative_tabputs it into a creative inventory tab.vanilla_fallbackis the vanilla item that players without your mod see instead.clight::registry::item_registerreturns anExpected<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}).