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.