Skip to content

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.