Your first mod in C
Status: Implemented. The complete mod from this page is
clight-sdk/examples/ruby_c.
We build Ruby:
- a
ruby_c:rubyitem and aruby_c:ruby_blockblock (models, textures, a loot table and recipes) - a ruby given to every player on join
- a chime (the mod’s own sound) when a ruby block breaks
- a
/rubyccommand that tells you how many rubies you carry
The same mod appears in C++ and Lua, so you can compare the languages. C mods compile to WebAssembly and run sandboxed.
1. Create the project
clight setup # once: downloads the WebAssembly compiler (wasi-sdk)
clight new c ruby_c --name "Ruby (C example)"
cd ruby_c
2. clight.mod.json
{
"schema": 1,
"id": "ruby_c",
"version": "1.0.0",
"name": "Ruby (C example)",
"description": "A ruby block and item, a join gift, a /rubyc command and a custom chime sound.",
"authors": ["You"],
"license": "MIT",
"api": 1,
"game": ">=26.3 <26.4",
"side": "both",
"sync": "required",
"runtime": { "type": "wasm", "entry": "bin/ruby_c.wasm" }
}
3. src/main.c
The entry point subscribes to events. Content is registered in lifecycle.register. This page uses the plain C API so
you see every handle; clight_easy.h has one-line helpers for the common tasks (cl_easy_give(player, MOD ":ruby", 1),
cl_easy_send_messagef, CL_ON), see C (the C ABI):
#include <clight.h>
#include <stdio.h>
#define MOD "ruby_c"
static cl_holder g_ruby_item, g_ruby_block;
static void on_register(void* payload, void* user) {
(void)payload; (void)user;
cl_block_desc b = CL_BLOCK_DESC_INIT;
b.id = CL_STR(MOD ":ruby_block");
b.copy_properties_from = CL_STR("minecraft:emerald_block"); // hardness, sounds, tool
b.vanilla_fallback = CL_STR("minecraft:emerald_block"); // what vanilla clients see
b.item = CL_STR(MOD ":ruby_block"); // also register its block item
cl_block_register(&b, &g_ruby_block);
cl_item_desc i = CL_ITEM_DESC_INIT;
i.id = CL_STR(MOD ":ruby");
i.vanilla_fallback = CL_STR("minecraft:emerald");
i.creative_tab = CL_STR("minecraft:ingredients");
cl_item_register(&i, &g_ruby_item);
}
static void on_player_join(void* payload, void* user) {
(void)user;
cl_ev_player* e = (cl_ev_player*)payload;
cl_item_stack ruby = cl_stack_create_by_name(CL_STR(MOD ":ruby"), 1);
cl_player_give(e->player, ruby);
cl_stack_release(ruby); // owned values are released by hand in C
cl_text msg = cl_text_translatable(CL_STR(MOD ".welcome"));
cl_player_send_message(e->player, msg);
cl_text_release(msg);
}
static void on_block_break(void* payload, void* user) {
(void)user;
cl_ev_block_break* e = (cl_ev_block_break*)payload;
if (!cl_block_state_is(e->state, CL_STR(MOD ":ruby_block"))) return;
cl_vec3 at = {e->pos.x + 0.5, e->pos.y + 0.5, e->pos.z + 0.5};
cl_sound_play(e->level, at, CL_STR(MOD ":ruby_chime"), CL_SOUND_BLOCKS, 1.0f, 1.0f);
}
/* … cmd_rubyc and on_command_register: see the example … */
static cl_result mod_main(void) {
CL_LOGF(CL_LOG_INFO, "Ruby (C) loading");
cl_result r = cl_event_subscribe(CL_EV_LIFECYCLE_REGISTER, NULL, on_register, NULL, NULL);
if (r == CL_OK) r = cl_event_subscribe(CL_EV_PLAYER_JOIN, NULL, on_player_join, NULL, NULL);
if (r == CL_OK) r = cl_event_subscribe(CL_EV_BLOCK_BREAK, NULL, on_block_break, NULL, NULL);
if (r == CL_OK) r = cl_event_subscribe(CL_EV_SERVER_COMMAND_REGISTER, NULL, on_command_register, NULL, NULL);
return r;
}
CLIGHT_MOD_ENTRY(mod_main)
The command (/rubyc) is a cl_cmd_desc with a callback that counts the rubies in slots 0–40 with cl_player_inventory_get and replies with cl_cmd_send_success.
4. Assets and data
assets/ruby_c/
├── blockstates/ruby_block.json models/block/ruby_block.json models/item/ruby.json
├── items/ruby.json items/ruby_block.json the item model definitions
├── textures/block/ruby_block.png textures/item/ruby.png
├── sounds.json { "ruby_chime": { "sounds": ["ruby_c:ruby_chime"] } }
├── sounds/ruby_chime.ogg Ogg Vorbis, mono for a positional sound
└── lang/en_us.json block/item names, "ruby_c.welcome"
datapack/ruby_c/
├── loot_table/blocks/ruby_block.json
└── recipe/ruby_block.json recipe/ruby_from_block.json
assets/ becomes the resource pack mod/ruby_c and datapack/ the data pack mod/ruby_c when the mod loads.
Limit: mod blocks are vanilla-backed: in the chunk a ruby block is its
vanilla_fallback(emerald block), which gives collision, shape and what vanilla clients see, while CLight tracks which positions are ruby blocks for events, drops, behaviour callbacks and its own properties. Celestial clients draw the mod’s own block model there. Mod items use their own model and texture (through theitem_modelcomponent andassets/<ns>/items/). The recipe files work as they are: nine rubies make a ruby block and a ruby block gives nine rubies back. The ruby shows up in the creative Ingredients tab (creative_tab), after the vanilla items.
5. Build and run
clight build # → build/bin/ruby_c.wasm and build/ruby_c-1.0.0.clmod
clight run # starts Celestial with the mod (your mods/ folder is not touched)
To install it for real, copy build/ruby_c-1.0.0.clmod into the mods/ folder of your game (with the Celestial launcher: the instance’s folder, or drag the file onto the instance’s Mods tab).