Changing and cancelling events
Lesson 5.2
Some events don’t just tell you what happened. They ask: should this happen at all? Your handler can stop it, or change how it happens.
Cancelling
A cancellable event happens after the handlers run, and only if none of them cancelled it. Every payload starts with a header; pass its address to cl_event_cancel:
// Nobody can break diamond ore.
static void on_block_break(void* payload, void* user) {
(void)user;
cl_ev_block_break* e = CL_EVENT(cl_ev_block_break, payload);
if (cl_block_state_is(e->state, CL_STR("minecraft:diamond_ore"))) {
cl_event_cancel(&e->header);
cl_easy_send_action_bar(e->player, "This ore is protected.");
}
}
The block stays, and the player’s client is told so. It looks exactly like breaking a block in spawn protection.
Only some events can be cancelled. The event list marks them. Cancelling one that can’t be cancelled does nothing (in development mode the log says event is not cancellable).
cl_event_is_cancelled(&e->header) tells you whether a handler before yours already cancelled it.
Changing fields
Some fields can be changed. The game reads them again after the handlers ran. In clight.h they are marked with /* mut */. On block.break, drop_items decides whether the block drops anything:
static void on_block_break(void* payload, void* user) {
(void)user;
cl_ev_block_break* e = CL_EVENT(cl_ev_block_break, payload);
if (cl_block_state_is(e->state, CL_STR("minecraft:glass"))) {
e->drop_items = false;
}
}
On entity.damage, amount is the damage:
// Players take half damage.
static void on_damage(void* payload, void* user) {
(void)user;
cl_ev_entity_damage* e = CL_EVENT(cl_ev_entity_damage, payload);
if (cl_entity_is_player(e->entity)) {
e->amount = e->amount / 2.0f;
}
}
Fields without /* mut */ are copies for you to read. Changing them has no effect.
Changing a string
Strings are a little special. player.chat has a changeable message, a cl_str. To change it, point it at text you own, and set the length:
static char shouted[512];
// Shout everything.
static void on_chat(void* payload, void* user) {
(void)user;
cl_ev_player_chat* e = CL_EVENT(cl_ev_player_chat, payload);
size_t n = e->message.len < sizeof shouted ? e->message.len : sizeof shouted;
for (size_t i = 0; i < n; ++i) {
shouted[i] = (char)toupper((unsigned char)e->message.ptr[i]);
}
e->message.ptr = shouted;
e->message.len = n;
}
The buffer is static, so it still exists when the game reads it after your handler returns. The game copies the text right away, so the next chat message can reuse the same buffer. toupper is from <ctype.h>; include it yourself. (It only changes the letters A to Z, so accented letters stay as they are.)
Stopping a click
player.interact_block happens when a player right-clicks a block. Cancel it and the click does nothing: no door opens, no block is placed.
// Crafting tables can't be opened; players get a tip instead.
static void on_interact_block(void* payload, void* user) {
(void)user;
cl_ev_player_interact_block* e = CL_EVENT(cl_ev_player_interact_block, payload);
if (e->hand != CL_HAND_MAIN) return; // the event comes once per hand
cl_block_state state = 0;
if (cl_level_get_block(e->level, e->pos, &state) != CL_OK) return;
if (cl_block_state_is(state, CL_STR("minecraft:crafting_table"))) {
cl_event_cancel(&e->header);
cl_easy_send_message(e->player, "Crafting is closed today.");
}
}
The world explains cl_level_get_block.
Priorities
When several mods listen to the same event, who goes first? Every handler has a priority. Lower runs earlier:
| Priority | Number | Use it for |
|---|---|---|
CL_PRIORITY_FIRST |
-2000 | Rules that must win, like protection. |
CL_PRIORITY_EARLY |
-1000 | |
CL_PRIORITY_NORMAL |
0 | The default. |
CL_PRIORITY_LATE |
1000 | |
CL_PRIORITY_LAST |
2000 | Last-minute changes after everybody else. |
CL_PRIORITY_MONITOR |
10000 | Only watching, never changing. |
Set it in the options:
cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
o.priority = CL_PRIORITY_FIRST;
cl_event_subscribe(CL_EV_BLOCK_BREAK, &o, on_protect_bedrock, NULL, NULL);
Any number works, for something in between: o.priority = -500;. Handlers with the same priority run in the order they subscribed.
Once an event is cancelled, the handlers after it are skipped. If you need to see cancelled events as well, for example to log them, ask for them with a flag:
cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
o.priority = CL_PRIORITY_MONITOR;
o.flags = CL_SUB_RECEIVE_CANCELLED;
cl_event_subscribe(CL_EV_BLOCK_BREAK, &o, on_log_break, NULL, NULL);
Flags can be combined with |: CL_SUB_RECEIVE_CANCELLED | CL_SUB_ONCE. A MONITOR handler should only look. If it changed things, the handlers that ran earlier would never know.
Only some blocks: filters
Many handlers only care about one block, item or mob. Instead of checking at the top of the handler, set a filter in the options. The handler then only runs for that id:
// Only called for diamond ore: no work at all for other blocks.
static void on_diamond_break(void* payload, void* user) {
(void)user;
cl_ev_block_break* e = CL_EVENT(cl_ev_block_break, payload);
e->xp = e->xp * 2; // double experience from diamond ore
}
static cl_result subscribe_diamonds(void) {
cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
o.filter = cl_ident_intern(CL_STR("minecraft:diamond_ore"));
return cl_event_subscribe(CL_EV_BLOCK_BREAK, &o, on_diamond_break, NULL, NULL);
}
cl_ident_intern turns an id into the number the game compares with. What the filter means depends on the event:
| Events | Filtered on |
|---|---|
CL_EV_BLOCK_BREAK, CL_EV_BLOCK_PLACE, CL_EV_PLAYER_INTERACT_BLOCK |
the block id |
CL_EV_PLAYER_INTERACT_ITEM |
the item id |
CL_EV_ENTITY_DAMAGE, CL_EV_ENTITY_DEATH |
the entity type, like minecraft:skeleton |
CL_EV_CLIENT_SCREEN_OPEN |
the screen id, like minecraft:inventory |
On other events the filter is ignored. A filter is also faster: the game skips your handler without calling into your mod at all.
cl_subscribe_opts also has a level field. Set it to a level handle, and the handler only gets events from that world (the block, interaction and entity events know their level).
Results
player.interact_block and player.interact_item have a result as well. When your code handled the click itself, say so with cl_event_set_result, and the game skips its own handling, just like after a cancel:
// Right-clicking a bell with a stick only prints a message: the bell stays quiet.
static void on_bell(void* payload, void* user) {
(void)user;
cl_ev_player_interact_block* e = CL_EVENT(cl_ev_player_interact_block, payload);
if (e->hand != CL_HAND_MAIN) return;
cl_item_stack held = cl_player_inventory_get(e->player, cl_player_selected_slot(e->player));
bool stick = cl_easy_stack_is(held, "minecraft:stick");
cl_stack_release(held);
if (stick) {
cl_easy_send_message(e->player, "Ding! (with a stick)");
cl_event_set_result(&e->header, CL_RESULT_SUCCESS);
}
}
static cl_result subscribe_bell(void) {
cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
o.filter = cl_ident_intern(CL_STR("minecraft:bell"));
return cl_event_subscribe(CL_EV_PLAYER_INTERACT_BLOCK, &o, on_bell, NULL, NULL);
}
| Result | Means |
|---|---|
CL_RESULT_PASS |
You did nothing (the default): the game goes on as normal. |
CL_RESULT_SUCCESS |
You handled it. |
CL_RESULT_CONSUME |
You handled it, quietly. |
CL_RESULT_FAIL |
The click is refused. |
On an event without a result, cl_event_set_result does nothing (in development mode the log says event has no result).
Try it
Make a “no swearing” filter: when a chat message is exactly the word “creeper” (any case), cancel it and tell the player in the action bar “Please be nice.” Run early, so other chat mods never see it.
Show a solution
#include <clight_easy.h>
#include <ctype.h>
#include <string.h>
static bool is_word(cl_str s, const char* word) {
size_t n = strlen(word);
if (s.len != n) return false;
for (size_t i = 0; i < n; ++i) {
if (tolower((unsigned char)s.ptr[i]) != word[i]) return false;
}
return true;
}
static void on_chat(void* payload, void* user) {
(void)user;
cl_ev_player_chat* e = CL_EVENT(cl_ev_player_chat, payload);
if (is_word(e->message, "creeper")) {
cl_event_cancel(&e->header);
cl_easy_send_action_bar(e->player, "Please be nice.");
}
}
static cl_result mod_main(void) {
cl_subscribe_opts o = CL_SUBSCRIBE_OPTS_INIT;
o.priority = CL_PRIORITY_EARLY;
return cl_event_subscribe(CL_EV_PLAYER_CHAT, &o, on_chat, NULL, NULL);
}
CLIGHT_MOD_ENTRY(mod_main)
is_word compares the cl_str letter by letter, because it has no \0 at the end and strcmp can’t be used on it. strlen comes from <string.h> and tolower from <ctype.h>.
Recap: cl_event_cancel(&e->header) stops cancellable events, fields marked /* mut */ can be changed (strings point at your own static buffer), cl_event_set_result reports a handled click, and priority, flags and filter in cl_subscribe_opts decide who goes first, who sees cancelled events and which ids a handler hears about.