Handles and strings
Lesson 4.2
Your mod runs in a sandbox, so it can’t hold pointers to the game’s objects. Instead, the API gives you small numbers that stand for them: handles. And because C strings and the game’s strings work differently, the API has its own string type. This lesson explains both, plus the one pattern every “give me a string” function uses.
Handles
A player, a world, an item stack, a text: each one is a handle, a tiny struct with one number inside:
cl_entity player; // a player or another entity
cl_level level; // a world (overworld, Nether, End)
cl_item_stack stack; // an item stack
cl_text text; // a chat text with colours
You never look inside a handle. You pass it to API functions, which know what it means:
cl_vec3 pos;
if (cl_entity_position(player, &pos) == CL_OK) {
CL_LOG("at %.1f %.1f %.1f", pos.x, pos.y, pos.z);
}
A handle can be null, which means “nothing”. CL_IS_NULL(handle) checks it:
cl_entity attacker = e->attacker;
if (CL_IS_NULL(attacker)) {
return; // nobody did it: a fall, lava, ...
}
You may also see if (!player.h) in code. It means the same thing.
Handles are different types (cl_entity, cl_level, …), so the compiler stops you from passing a world where a player is expected.
Handles can go stale
A handle stays valid as long as the thing it stands for exists. When a player leaves or an entity dies, its handle goes stale: functions called with it fail with CL_E_STALE_HANDLE or return an empty value, but nothing crashes. Before using a handle you kept for a while, check it:
if (cl_entity_is_valid(saved_player)) {
cl_easy_send_message(saved_player, "Still here!");
}
Owned handles: release what you get
Some functions create something and hand it to you. You own it, and you must give it back with the matching release function when you are done, like free after malloc:
cl_text t = cl_text_literal(CL_STR("Hello!")); // created: you own it
cl_player_send_message(player, t);
cl_text_release(t); // done: give it back
The owned kinds are:
| Created by | Released with |
|---|---|
cl_text_literal, cl_text_translatable |
cl_text_release |
cl_stack_create, cl_stack_create_by_name, cl_player_inventory_get, cl_menu_get_item |
cl_stack_release |
cl_buf_create |
cl_buf_release |
The header says (owned) in the comment of such functions. Everything else (players, levels, handles in event payloads) is borrowed: you use it, and you don’t release it.
Forgetting a release doesn’t crash, but your mod’s memory slowly grows, so make it a habit. The easy helpers release for you; that is half of what they do.
cl_str: the API’s strings
C marks the end of a string with a \0 byte. The API instead passes a pointer and a length, in a small struct:
typedef struct cl_str {
const char* ptr;
size_t len;
} cl_str;
Two macros make one:
cl_str a = CL_STR("minecraft:stone"); // from a string literal (length known at compile time)
char name[32] = "Alex";
cl_str b = CL_STR_C(name); // from any C string (counts up to the \0)
- Use
CL_STR("...")for text written in quotes in your code. It is the fastest. - Use
CL_STR_C(x)for everything else: arrays, pointers, strings you built withsnprintf.
CL_STR only works with literals. CL_STR(name) on an array compiles, but gives the array’s full size, not the text’s length, so use CL_STR_C there.
A cl_str you get from the game (for example a chat message in an event) is not \0-terminated. Print it with %.*s, which takes the length first:
CL_LOG("said: %.*s", (int)e->message.len, e->message.ptr);
To keep it as a C string, copy it into your own array:
char msg[256];
size_t n = e->message.len < sizeof msg - 1 ? e->message.len : sizeof msg - 1;
memcpy(msg, e->message.ptr, n);
msg[n] = '\0';
memcpy is from <string.h>; include it yourself.
Strings out: the two-call pattern
Functions that give you a string, like a player’s name or a world’s dimension, all have the same three parameters at the end: your buffer, its size, and where to write the length.
cl_result cl_player_name(cl_entity player, char* buf, size_t cap, size_t* out_len);
Usually you know a good size, and one call is enough:
char name[64];
size_t len = 0;
if (cl_player_name(player, name, sizeof name, &len) == CL_OK) {
CL_LOG("name: %s (%zu letters)", name, len);
}
The function writes the text and a \0 into buf, and the length (without the \0) into out_len.
If the buffer is too small, it returns CL_E_BUFFER_TOO_SMALL. It still writes as much as fits, with a \0 at the end, and out_len still tells you the full length. So when you don’t know the size, call twice: once to ask, once to get it:
size_t need = 0;
cl_level_dimension(level, NULL, 0, &need); // 1st call: how long is it?
char* dim = malloc(need + 1);
if (dim && cl_level_dimension(level, dim, need + 1, &need) == CL_OK) { // 2nd call: get it
CL_LOG("dimension: %s", dim);
}
free(dim);
That is the two-call pattern. For short things like names and ids, a fixed array of 64 or 256 bytes is fine and simpler.
Lists work the same way. cl_server_players fills your array of players and tells you how many there are in total:
cl_entity players[64];
size_t count = 0;
cl_server_players(players, 64, &count);
if (count > 64) count = 64; // more were online than fit
for (size_t i = 0; i < count; ++i) {
cl_easy_send_message(players[i], "Server restart in 5 minutes.");
}
Ids and names
Items, blocks and many other things have an id like "minecraft:stone". Some functions take the id directly as a cl_str. Others want a number the game looks up once:
cl_block_id stone = 0;
if (cl_block_lookup(CL_STR("minecraft:stone"), &stone) == CL_OK) {
cl_block_state s = cl_block_default_state(stone);
// use s with cl_level_set_block ...
}
You can keep such numbers in static variables and look them up only once. An id without a namespace means minecraft:.
Try it
Write a function void log_player(cl_entity player) that logs a line like Alex at 12.5 64.0 -30.2 in minecraft:overworld. Use the full API, not the easy helpers. You need cl_player_name, cl_entity_position, cl_entity_level and cl_level_dimension.
Show a solution
static void log_player(cl_entity player) {
char name[64];
size_t len = 0;
if (cl_player_name(player, name, sizeof name, &len) != CL_OK) return;
cl_vec3 pos;
if (cl_entity_position(player, &pos) != CL_OK) return;
char dim[128];
cl_level level = cl_entity_level(player);
if (cl_level_dimension(level, dim, sizeof dim, &len) != CL_OK) return;
CL_LOG("%s at %.1f %.1f %.1f in %s", name, pos.x, pos.y, pos.z, dim);
}
Every call that can fail is checked, and the function stops at the first failure. len is reused for both strings, because this code doesn’t need the lengths. 128 bytes is plenty for a dimension id.
Recap: handles stand for game objects (check them with CL_IS_NULL and cl_entity_is_valid, release the owned ones), CL_STR and CL_STR_C make API strings, and string results come through buf, cap, &len with the two-call pattern for unknown sizes.