Skip to content

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.