Skip to content

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:ruby item and a ruby_c:ruby_block block (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 /rubyc command 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 the item_model component and assets/<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).