Identifiers and handles
Status: Implemented. Identifiers, holders, handles and the position types on this page are in
clight.h. Typed holders and block-state properties are Planned.
Identifiers
Everything with a name uses a namespaced identifier, namespace:path, exactly as in vanilla (minecraft:stone, ruby_tools:ruby_block).
- The namespace of your content is your mod id (
[a-z][a-z0-9_]{1,63}). - The path uses
[a-z0-9_./-]. - A missing namespace means
minecraft. (Lua registration fills in your own mod id instead:cl.items.register{ id = "ruby" }registersmy_mod:ruby.)
Most functions take the identifier as text. Where speed matters, the API passes interned 32-bit ids (cl_ident): interning happens once, and comparing two cl_idents is an integer compare.
cl_ident cl_ident_intern(cl_str id); // "ruby_tools:ruby" -> id
cl_result cl_ident_name(cl_ident id, char* buf, size_t cap, size_t* out_len); // id -> text
| C | C++ | Lua |
|---|---|---|
cl_ident_intern(CL_STR("a:b")) |
clight::core::ident_intern("a:b") |
plain strings everywhere |
cl_ident_name(id, buf, cap, &n) |
clight::core::ident_name(id) → Expected<std::string> |
n/a |
Interned ids are valid for the whole process, but they are not stable between runs. Never save a cl_ident to disk; save the text.
Raw ids
| Kind | Type | Stable? | Use |
|---|---|---|---|
| Identifier | text, or cl_ident in memory |
The text is stable | Saving, config, cross-mod references |
| Raw block / item id | cl_block_id, cl_item_id (uint32_t) |
Only within one session | Fast compares |
| Block state | cl_block_state (uint32_t) |
Only within one session | cl_level_get_block / cl_level_set_block |
Vanilla entries keep vanilla’s ids. Mod blocks and items get virtual ids (0x40000000 | index) that CLight maps to their vanilla base in the world (Registries). Look ids up by name with cl_block_lookup / cl_item_lookup, and turn them back into names with cl_block_name / cl_item_name. In Lua and the C++ easy helpers you pass the name directly (level:set_block(pos, "minecraft:stone")).
Holders
Registration happens before ids are final, so cl_block_register and cl_item_register give you a holder (cl_holder, a uint32_t).
cl_holder ruby_block; // filled in lifecycle.register
// after lifecycle.registries_frozen:
uint32_t raw = cl_holder_value(ruby_block); // 0xFFFFFFFF before the freeze
Lua returns a holder table ({ id = "my_mod:ruby", raw = ..., registry = "minecraft:item" }; tostring(holder) is the id), C++ returns Expected<cl_holder>.
Handles
Live game objects (levels, entities, item stacks, texts, buffers, subscriptions, screens and so on) are opaque 64-bit handles:
- Each handle type is a distinct C struct (
typedef struct cl_entity { uint64_t h; } cl_entity;), so passing an entity where a level is expected is a compile error. - 0 is the null handle. Test it with
CL_IS_NULL(e)in C,if (!e)in C++,if not e thenin Lua. - When the object goes away (a player leaves, an entity is removed), old handles become stale. A call with a stale handle returns
CL_E_STALE_HANDLE(or an empty value) instead of crashing. Check withcl_entity_is_valid(e). - Handles are cheap to copy and need no release, except owned values:
cl_item_stack,cl_textandcl_buf. Release them withcl_stack_release,cl_text_releaseandcl_buf_release. The C++ classesItemStack,TextandBufdo this in their destructor; Lua releases them when they are collected (or at once withlocal s <close> = ...).
Handle kinds
| Handle | Lifetime | Owned? |
|---|---|---|
cl_level |
Until the level unloads | no |
cl_entity (also players) |
Until the entity is removed. A player’s handle survives respawn. | no |
cl_item_stack |
Until released | yes (cl_stack_retain / cl_stack_release) |
cl_text |
Until released | yes (cl_text_retain / cl_text_release) |
cl_buf |
Until released | yes (cl_buf_release, no retain) |
cl_sub, cl_task, cl_mixin |
Until cancelled or removed, or until the mod unloads | no |
cl_hud_layer, cl_screen, cl_widget, cl_rml_doc, cl_menu, cl_sound, cl_keybind |
Until removed, closed or stopped, or until the mod unloads | no |
cl_cmd_ctx, cl_gui |
Only during the callback that received it | no |
cl_config, cl_cmd_node |
Config: the mod’s lifetime. A command node: until cl_cmd_register_tree takes the tree. |
no |
The full list with the C++ class names is in the generated types page.
Why handles and not pointers
- Safety: a WASM or Lua mod can never forge a pointer into engine memory.
- Stability: the engine can move objects without breaking mods.
- Speed: validation is a table lookup and a generation compare.
Positions and vectors
typedef struct cl_block_pos { int32_t x, y, z; } cl_block_pos;
typedef struct cl_vec3 { double x, y, z; } cl_vec3;
typedef enum cl_direction { CL_DIRECTION_DOWN, CL_DIRECTION_UP, CL_DIRECTION_NORTH,
CL_DIRECTION_SOUTH, CL_DIRECTION_WEST, CL_DIRECTION_EAST } cl_direction; // vanilla order
C++ uses the same structs (cl_block_pos{1, 64, 1}). Lua: cl.pos(x, y, z) and cl.vec(x, y, z) make value tables with operators and helpers (pos + cl.pos(1, 0, 0), pos:above(), pos:below(2), pos:offset("north", 3), pos:center(), vec:length(), vec:normalize()).