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 it to clight::cancel:

// Nobody can break diamond ore.
clight::on<clight::ev::BlockBreak>([](cl_ev_block_break& e) {
    if (clight::registry::block_state_is(e.state, "minecraft:diamond_ore")) {
        clight::cancel(e.header);
        clight::Entity(e.player).send_action_bar("This ore is protected.");
    }
}).detach();

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).

clight::events::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:

clight::on<clight::ev::BlockBreak>([](cl_ev_block_break& e) {
    if (clight::registry::block_state_is(e.state, "minecraft:glass")) {
        e.drop_items = false;
    }
}).detach();

On entity.damage, amount is the damage:

// Players take half damage.
clight::on<clight::ev::EntityDamage>([](cl_ev_entity_damage& e) {
    if (clight::Entity(e.entity).is_player()) {
        e.amount = e.amount / 2.0f;
    }
}).detach();

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 that text must still exist after your handler returns, when the game reads it. A std::string member of your mod is a good home:

class ShoutMod final : public clight::Mod {
public:
    void on_construct() override {
        // Shout everything.
        clight::on<clight::ev::PlayerChat>([this](cl_ev_player_chat& e) {
            shouted_.assign(clight::sv(e.message));
            for (char& c : shouted_) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
            e.message = clight::s(shouted_);
        }).detach();
    }

private:
    std::string shouted_;
};

shouted_ lives as long as the mod, so it still exists when the game reads it. The game copies the text right away, so the next chat message can reuse the same string. std::toupper is from <cctype>; include it yourself. (It only changes the letters A to Z, so accented letters stay as they are.)

Never point a changed field at a local std::string: it is destroyed when the lambda returns, and the game would read freed memory.

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.
clight::on<clight::ev::PlayerInteractBlock>([](cl_ev_player_interact_block& e) {
    if (e.hand != CL_HAND_MAIN) return;   // the event comes once per hand

    auto state = clight::Level(e.level).get_block(e.pos);
    if (state && clight::registry::block_state_is(*state, "minecraft:crafting_table")) {
        clight::cancel(e.header);
        clight::Entity(e.player).send_message("Crafting is closed today.");
    }
}).detach();

The world explains 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:

clight::on<clight::ev::BlockBreak>([](cl_ev_block_break& e) {
    if (clight::registry::block_state_is(e.state, "minecraft:bedrock")) clight::cancel(e.header);
}, {.priority = CL_PRIORITY_FIRST}).detach();

Any number works, for something in between: {.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:

clight::on<clight::ev::BlockBreak>([](cl_ev_block_break& e) {
    if (clight::events::is_cancelled(&e.header)) clight::log::info("a break was stopped at {} {} {}", e.pos.x, e.pos.y, e.pos.z);
}, {.priority = CL_PRIORITY_MONITOR, .flags = CL_SUB_RECEIVE_CANCELLED}).detach();

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.
clight::on<clight::ev::BlockBreak>([](cl_ev_block_break& e) {
    e.xp = e.xp * 2;   // double experience from diamond ore
}, {.filter = "minecraft:diamond_ore"}).detach();

What the filter means depends on the event:

Events Filtered on
ev::BlockBreak, ev::BlockPlace, ev::PlayerInteractBlock the block id
ev::PlayerInteractItem the item id
ev::EntityDamage, ev::EntityDeath the entity type, like minecraft:skeleton
ev::ClientScreenOpen 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.

SubOpts also has a level field. Set it to a clight::Level, 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 clight::events::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.
clight::on<clight::ev::PlayerInteractBlock>([](cl_ev_player_interact_block& e) {
    if (e.hand != CL_HAND_MAIN) return;
    clight::Entity player = e.player;
    clight::ItemStack held = player.inventory_get(player.selected_slot());
    if (held.is("minecraft:stick")) {
        player.send_message("Ding! (with a stick)");
        clight::events::set_result(&e.header, CL_RESULT_SUCCESS);
    }
}, {.filter = "minecraft:bell"}).detach();

held is an owned copy of the stack, and it is released by itself at the end of the lambda.

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, 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.hpp>
#include <cctype>

bool is_word(std::string_view s, std::string_view word) {
    if (s.size() != word.size()) return false;
    for (size_t i = 0; i < s.size(); ++i) {
        if (std::tolower(static_cast<unsigned char>(s[i])) != word[i]) return false;
    }
    return true;
}

class NiceChatMod final : public clight::Mod {
public:
    void on_construct() override {
        clight::on<clight::ev::PlayerChat>([](cl_ev_player_chat& e) {
            if (is_word(clight::sv(e.message), "creeper")) {
                clight::cancel(e.header);
                clight::Entity(e.player).send_action_bar("Please be nice.");
            }
        }, {.priority = CL_PRIORITY_EARLY}).detach();
    }
};

CLIGHT_MOD(NiceChatMod)

clight::sv turns the message into a std::string_view, which has a size() and [] like a string. is_word compares letter by letter in lower case, so “Creeper” and “CREEPER” match too.

Recap: clight::cancel(e.header) stops cancellable events, fields marked /* mut */ can be changed (strings point at text that outlives the handler), clight::events::set_result reports a handled click, and .priority, .flags and .filter in the options decide who goes first, who sees cancelled events and which ids a handler hears about.