Skip to content

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" } registers my_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 then in 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 with cl_entity_is_valid(e).
  • Handles are cheap to copy and need no release, except owned values: cl_item_stack, cl_text and cl_buf. Release them with cl_stack_release, cl_text_release and cl_buf_release. The C++ classes ItemStack, Text and Buf do this in their destructor; Lua releases them when they are collected (or at once with local 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()).