-
Notifications
You must be signed in to change notification settings - Fork 2
Concepts Events And Hooks
Two buses, two contracts. Events notify: past-tense names
(battle.started, pokemon.caught), one payload table, listeners observe
and may mutate only documented fields. Hooks intercept: noun names
(battle.damage, music.select), middleware over the vanilla function,
each link may rewrite arguments or results. Nothing hands mods a way to
bypass engine invariants — observed, then intercepted, never bypassed.
The complete catalogs, with payloads and emit/call sites read from the engine source, are Reference: Events and Reference: Hooks.
mod.events:on(name, callback, priority) -- subscribe; returns unsubscribe()
mod.events:once(name, callback, priority) -- retire after the first fire-
priority(default 0): higher runs first within one event. - The callback receives the payload table. Payloads are shared between
listeners; treat fields as read-only unless the event is documented
mutable (
pokemon.before_giveis the canonical mutable event — rewritegift.species,gift.level,gift.nickname). - Dispatch walks a snapshot of the listener list, so a listener may
unsubscribe itself (or a sibling) mid-emit;
oncerelies on this. - A listener that raises is logged against its mod and skipped
(
src/mods/Events.lua:emit); the engine path that emitted always completes.
A mod may also broadcast to other mods — only under its own prefix, so no mod can forge an engine event:
mod.events:emit("mod.my_mod_id.season_changed", { season = "winter" })mod.hooks:wrap(name, function(next, ...)
-- before: inspect or rewrite the arguments
local result = next(...) -- call downstream (ends at vanilla)
-- after: inspect or rewrite the result
return result
end, priority)- Links form a chain sorted by
priority(higher first, default 0); the innermost call is the vanilla function. With nothing wrapped,Runtime.call(name, vanilla, ...)returnsvanilla(...)unchanged — that is the parity guarantee. -
next(...)with no arguments forwards the current arguments unchanged; passing arguments rewrites them for everything downstream. - Return what the site expects. A hook that returns a wrong type at a UI
site (
ui.start_menu.itemsand friends) is discarded with a logged error and the vanilla value is kept. - Error isolation (
src/mods/Hooks.lua:call): a link that throws before callingnextis skipped and the chain continues; a link that throws after itsnextreturned keeps the downstream result (its post-processing is discarded). Vanilla runs at most once per call — it has side effects — so a failure never re-walks the chain. -
wrapreturns an unsubscribe function.
Prefer data over code, and the mildest mechanism that works:
- A registry record if the thing you want to change is content (stats, text, a song, a party). See Registries.
- A hook if you need to change a decision the engine makes (damage, catch rate, which song plays).
- An event if you only need to know something happened.
Engine call sites guard payload construction: Runtime.wants(name) /
Runtime.wantsHook(name) are one table lookup, so an event nobody
subscribed to and a hook nobody wrapped cost nothing on hot paths
(src/mods/Runtime.lua). Subscribing to a hot event (world.stepped,
battle.damage_dealt) makes the engine start paying for that payload —
keep such listeners cheap.
Events are past-tense domain.subject_verbed (battle.move_used,
map.entered); hooks are noun domain.operation (battle.accuracy,
warp.destination). pokemon.before_give predates the rule and is
grandfathered forever.
Engine 1.0.0 · mod API 2 · repository · Discord
This project ships no ROM data. All game content is decoded on the player's machine from their own verified Pokemon Red ROM; mods ship recipes and original assets, never extracted content.
Start here
Concepts
Tutorials
- The ladder
- 01 Sprite and Text Tweak
- 02 Balance Patch
- 03 New Species
- 04 New Item and Ball
- 05 New Move
- 06 NPC and Dialogue
- 07 New Map
- 08 Quest
- 09 Custom Music
- 10 New Mechanic
- 11 Custom UI Screen
- 12 Mini Total Conversion
Reference
Guides
Playing