Event bus reference
Plugins subscribe to gameplay events with the register builtin at load time:
register(event_name, fn) takes the event name string and any callable (a def, lambda, or
builtin). It validates the event name against the closed known-event set at load — a typo’d
name is a load error (register: unknown event "..."), never a hook that silently never fires.
Events are discrete occurrences: each fires once per real happening (a block breaks, a player
joins, a mob dies) — never per-entity-per-tick. The one per-tick event is on_tick, and it is
guarded by a zero-subscriber fast path: on a server where no plugin subscribes to an event,
dispatching it costs a single map read and zero allocation.
The events
Hook arguments are positional, in the exact order listed. All payloads are plain frozen scalars (ints, strings, floats) — never live entity or world handles (those exist only in the capability-gated goal-callback API).
Dispatch semantics
- Register-once. Hooks are captured while the module body runs at load. There is no runtime subscribe/unsubscribe; a reload rebuilds the whole hook table.
- Tick-goroutine dispatch (Starlark). The server emits events at the discrete seams on the tick goroutine and calls Starlark hooks inline, in registration order.
- Off-tick dispatch (Python). For
runtime = "python"plugins the event is submitted to a bounded off-tick pool and the tick continues immediately — see Python runtime. - Per-hook isolation. Every hook call runs on a fresh, step-budgeted Starlark thread inside a
panic recover. A hook that errors, panics, or exceeds the 10M-step budget is logged
(
plugin "name" hook on_x error: ...) and skipped; the remaining hooks and the tick continue.
Example: reacting to events with chat
The bundled gate_events plugin (used by Sulfur’s own end-to-end gate test) subscribes to two
events and reacts via chat() so the reaction lands on every player’s screen:
Adding your own state across events works through module-level mutable containers created before
the module freezes — but remember the payloads are value copies; there is no way to hold a live
reference to an entity from an event hook. For stateful per-mob behavior, use
declare_mob goals and the get_state/set_state scratch API instead.

