Skip to content

Listening to events

Lesson 10.1

Almost everything a mod does starts with an event: a player joins, a block breaks, a tick passes. You tell the game which events you care about, and it calls your function each time one happens.

cl.on

cl.on("player.join", function(event)
  cl.log(event.player.name, "joined")
end)

cl.on(name, handler) takes the event’s name and a function. The game calls the function with one argument, the event, which holds the details of what happened.

Names are written family.what: player.join, block.break, tick.server_post. A wrong name doesn’t raise an error, but the log warns you:

[my_mod] cl.on: unknown event 'player.joined' (see 99_Appendix/02)

The event list has every event with its fields.

Event fields

An event is like a table with named fields. Which fields there are depends on the event. block.break has these:

cl.on("block.break", function(event)
  cl.log(event.player.name, "broke", event.block, "at", event.pos)
end)
[my_mod] Alex broke minecraft:oak_log at (12, 64, -30)

A few friendly names work on many events:

Field Means
event.player The player involved, or nil if it wasn’t a player. On entity.damage it is the hurt entity if that is a player.
event.world The world (level) it happened in. The same as event.level.
event.block The block’s id, like "minecraft:stone", on events with a block state.
event.item The item’s id on events with an item stack.
event.pos A block position with x, y and z.

Fields that don’t exist on an event are nil, like missing keys in a table. So check before you use something that may be missing:

cl.on("entity.death", function(event)
  if event.attacker == nil then return end  -- died from a fall, lava, ...
  if not event.attacker:is_player() then return end
  event.attacker:send_message("Nice hit!")
end)

Events only live during the call

The event object is valid while your function runs. When it returns, the game reuses the event’s memory. If you need a value later, copy it into a local variable first:

local last_breaker = nil

cl.on("block.break", function(event)
  last_breaker = event.player.name   -- a copy of the string: fine
  -- saving `event` itself would not work later
end)

Using an old event raises a clear error: event used after its callback returned (copy the fields you need).

Stopping a handler

cl.on returns a subscription. Call cancel on it to stop listening:

local sub = cl.on("tick.server_post", function(event)
  cl.log("tick", event.tick_count)
end)

-- later
sub:cancel()

If you only want the first time, use cl.once. It cancels itself after one call:

cl.once("player.join", function(event)
  cl.log("The first player of this session is", event.player.name)
end)

Many handlers, many files

You can listen to the same event as often as you like, also from different files. Each handler runs on its own. This keeps code tidy: one file for chat features, another for blocks.

-- scripts/main.lua
require "my_mod.chat"
require "my_mod.blocks"
-- scripts/chat.lua
cl.on("player.chat", function(event)
  cl.log(event.player.name, "says", event.message)
end)

require "my_mod.chat" loads scripts/chat.lua. The my_mod. at the front is your mod’s id, and dots become folders: my_mod.util.text is scripts/util/text.lua. A mod can only require its own files.

Errors in handlers

If your handler raises an error, the game logs it with the file and line, skips the rest of your function, and goes on with the game. Other mods and other handlers still run. If your mod keeps failing (10 errors within one minute), it is turned off for the rest of the session, and the Mods screen says why. Debugging has more.

Try it

Log a line every time a player places a block, with the player’s name, the block id and the position. Then make it stop by itself after 5 placements.

Show a solution
local placed = 0
local sub

sub = cl.on("block.place", function(event)
  placed = placed + 1
  cl.log("%s placed %s at %s", event.player.name, event.block, tostring(event.pos))
  if placed >= 5 then
    sub:cancel()
    cl.log("stopped logging")
  end
end)

sub is declared before cl.on so the handler can see it. That is the closure rule from part 1: the function remembers the variable, and by the time it runs, sub holds the subscription.

Recap: cl.on(name, fn) calls fn(event) each time the event happens; read its fields during the call, and cancel the subscription to stop.