Hooks & the snapshot
Stability: Stable.
Hooks are optional functions you add to your descriptor to extend or override engine behaviour. Data-only modes (like the shipped 1v1 Duel) use none. Add them only when you need custom logic.
How hooks run (cross-VM)
A mode is a separate resource, so a hook is a real function reference that runs in your mode's VM, not the engine's. Two consequences:
- A hook receives a scene id (and, for
tick, a plain snapshot) — never the engine's internal Scene object. It reads from the snapshot and acts back through the write-exports. - Your resource must stay started for hooks to fire. When your resource stops, the engine unregisters the mode, so a dead hook is never called.
Hooks are invoked defensively (pcall): an error in a hook is caught and printed
as [engine] mode <id> hook <name>: <err> rather than crashing the engine.
The hooks
| Hook | Signature | Fires when |
|---|---|---|
setup | (sceneId) | A new scene of your mode is created. Good for initialising mst via setVar. |
onJoin | (sceneId, src) | A player has joined the scene (after they're placed in the bucket and spawned frozen). |
tick | (sceneId, snapshot) | Every 500 ms. Replaces the driver entirely — you drive the whole match. |
onKill | (sceneId, killerSrc, victimSrc) | A PvP kill. killerSrc is nil if the killer is unknown (e.g. self/world). |
onDeath | (sceneId, victimSrc) | A non-kill death (suicide / environment). |
onKill and onDeath are mutually exclusive for a single death: a player-kill
fires onKill; a suicide or environmental death fires onDeath.
local MODE = {
id = 'mygame', label = 'My Game',
teamCount = 2, perTeam = 2,
setup = function(sceneId)
exports.engine:setVar(sceneId, 'objectives', 0)
end,
onKill = function(sceneId, killerSrc, victimSrc)
if killerSrc then
exports.engine:notify(sceneId, 'First blood!')
end
end,
}tick replaces the driver
If your descriptor has a tick, the engine calls it every 500 ms and runs no
driver for that scene. You are responsible for the whole state machine — spawning,
state transitions, win detection — using the snapshot to read and the
write-exports to act:
tick = function(sceneId, snap)
if snap.state == 'waiting' and snap.bothPresent then
exports.engine:spawnAll(sceneId, { alive = true })
exports.engine:setState(sceneId, 'live', 300)
exports.engine:announce(sceneId, 'engine:live')
elseif snap.state == 'live' and snap.now >= snap.phaseEnd then
exports.engine:setState(sceneId, 'ended', 6)
end
endIf your custom rules fit a reusable shape, consider a shared
registerDriver instead — it uses the exact same
(sceneId, snapshot) convention.
The snapshot
tick (and external drivers) receive a read-only plain-data view of the
scene. You never mutate it — write back through the
write-exports.
| Field | Type | Meaning |
|---|---|---|
id | number | Scene id (pass it to the write-exports). |
modeId | string | Your mode's id. |
state | string | 'waiting' | 'live' | 'ended'. |
round | number | Current round number (0 before the first). |
phaseEnd | number | GetGameTimer() ms at which the current phase ends (0 if untimed). |
now | number | GetGameTimer() ms at snapshot time (compare with phaseEnd). |
mst | table | The mode state bag — whatever you've written via setVar. |
map | { label, areas, spawns } | The scene's map: label, plus the arena's per-team areas/spawns placement arrays (indexed by team number; a system reads them to derive the play area). areas/spawns are absent if the arena declares none. |
counts | array | Players per team, indexed by team number (counts[1], counts[2]). |
alive | array | Alive players per team, same indexing. |
wins | array | Each team's score (round wins / continuous score), same indexing. |
players | list | { { src, name, team, kind, alive, kills, deaths }, ... } — team is the index (nil for a spectator). |
bothPresent | boolean | true when every team has ≥ minToStart players (or, for a teamless mode, when the scene has any combatants). |
{
id = 3, modeId = 'mygame', state = 'live', round = 2,
phaseEnd = 184213, now = 152877, mst = {},
map = { label = 'The Yard' }, -- + areas/spawns when the arena declares them
counts = { 2, 2 }, alive = { 1, 2 }, wins = { 1, 0 },
players = {
{ src = 5, name = 'Alice', team = 1, kind = 'player', alive = true, kills = 3, deaths = 1 },
-- ...
},
bothPresent = true,
}See also
- Write-exports — how a hook acts on the scene.
- Drivers — built-in shapes and
registerDriver. - Client events — what
announcecan trigger. - Mode descriptor — where hooks live in
def.