Changing and cancelling events
Lesson 10.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. Call event:cancel() to stop it:
-- Nobody can break diamond ore.
cl.on("block.break", function(event)
if event.block == "minecraft:diamond_ore" then
event:cancel()
event.player:send_action_bar("This ore is protected.")
end
end)
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. Calling cancel on one that can’t be cancelled raises an error: this event can't be cancelled.
event.cancelled (or event:is_cancelled()) 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. On player.chat the message is one of them:
-- Shout everything.
cl.on("player.chat", function(event)
event.message = event.message:upper()
end)
On block.break, drop_items decides whether the block drops anything:
cl.on("block.break", function(event)
if event.block == "minecraft:glass" then
event.drop_items = false
end
end)
And on entity.damage, amount is the damage:
-- Players take half damage.
cl.on("entity.damage", function(event)
if event.player then
event.amount = event.amount / 2
end
end)
Fields that can’t be changed raise an error when you assign them: event field 'pos' is read-only or has the wrong type.
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.
cl.on("player.interact_block", function(event)
if event.hand ~= "main" then return end -- the event comes once per hand
local state = event.world:get_block(event.pos)
if cl.registry.block_state_is(state, "minecraft:crafting_table") then
event:cancel()
event.player:send_message("Crafting is closed today.")
end
end)
event.world:get_block(pos) reads the block at a position. The world explains it.
Results
player.interact_block and player.interact_item have a result as well. When your code handled the click itself, say so with event:set_result(...), and the game skips its own handling, just like after cancel:
-- Right-clicking a bell with a stick only prints a message: the bell stays quiet.
cl.on("player.interact_block", function(event)
if event.hand ~= "main" then return end
local state = event.world:get_block(event.pos)
if cl.registry.block_state_is(state, "minecraft:bell") and event.player:held_item().id == "minecraft:stick" then
event.player:send_message("Ding! (with a stick)")
event:set_result("success")
end
end)
| Result | Means |
|---|---|
"pass" |
You did nothing (the default): the game goes on as normal. |
"success" |
You handled it. |
"consume" |
You handled it, quietly. |
"fail" |
The click is refused. |
Calling set_result on an event without a result logs a warning and does nothing.
Filters
Many handlers only care about one block, item or mob. Instead of checking at the top of the handler, pass a filter. The handler then only runs for that id:
-- Only called for diamond ore: no work at all for other blocks.
cl.on("block.break", function(event)
event.xp = event.xp * 2 -- double experience from diamond ore
end, { filter = "minecraft:diamond_ore" })
cl.on("entity.death", function(event)
event.world:set_block(cl.pos(event.entity.pos), "minecraft:skeleton_skull")
end, { filter = "minecraft:skeleton" })
| Events | Filtered on |
|---|---|
block.break, block.place, player.interact_block |
the block id |
player.interact_item |
the item id |
entity.damage, entity.death |
the entity type |
client.screen_open |
the screen id ("minecraft:inventory") |
A filter is also faster: the game skips your handler without running any Lua.
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 |
|---|---|---|
"first" |
-2000 | Rules that must win, like protection. |
"early" |
-1000 | |
"normal" |
0 | The default. |
"late" |
1000 | |
"last" |
2000 | Last-minute changes after everybody else. |
"monitor" |
10000 | Only watching, never changing. |
Pass it as the third argument of cl.on:
cl.on("block.break", function(event)
if event.block == "minecraft:bedrock" then event:cancel() end
end, { priority = "first" })
A number works too, for something in between: { priority = -500 }.
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:
cl.on("block.break", function(event)
cl.log("break at", event.pos, "cancelled:", event.cancelled)
end, { priority = "monitor", receive_cancelled = true })
A "monitor" handler should only look. If it changed things, the handlers that ran earlier would never know.
Try it
Make a “no swearing” filter: when a chat message contains the word “creeper” (any case), replace that word with *******. If the message is only that word, cancel it completely and tell the player in the action bar.
Show a solution
cl.on("player.chat", function(event)
local text = event.message
if text:lower():match("^%s*creeper%s*$") then
event:cancel()
event.player:send_action_bar("Please be nice.")
return
end
local cleaned = text:gsub("[Cc][Rr][Ee][Ee][Pp][Ee][Rr]", "*******")
event.message = cleaned
end, { priority = "early" })
gsub returns two values, the new string and the number of changes. Storing it in cleaned first keeps only the string. The character classes like [Cc] match both cases, which String patterns explained.
Recap: event:cancel() stops cancellable events, some fields can be changed, and priority decides who goes first.