Events and hooks
Two buses, two contracts. A callback is a function the game calls later. An event gives its callback a payload table that describes something that already happened; a hook lets its callback participate while the engine is deciding a result.
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.
One vocabulary, three generations
Event and hook names are shared by Gen 1, Gen 2 and Gen 3: nothing is prefixed
or renamed per generation. A name is raised only where the game has a call
site. Most gameplay names (battle.damage, map.entered, world.stepped)
fire on all three; the Pokegear, radio, apricorn and Gold/Silver boot-cinema
names exist on Gold, Silver and Crystal only; shininess, gender, held items,
breeding, happiness, Pokerus and roamer names fire on Gen 2 and Gen 3; link
and trade names follow the link play each game has. Subscribing to a name the booted
game never raises is legal and simply never fires, so one source file loads
everywhere. The per-generation tables are in the reference pages, and an
api: 3 mod can ask at runtime with mod:supports("hooks", name) or
mod:supports("events", name) (see Portable API 3).
Events
Code placement: subscribe in your mod entry file. Status: this is a complete read-only listener; it records information but does not alter play.
mod.events:on("map.entered", function(ev)
mod.log:info("Entered %s", ev.mapId)
end)
Here ev is the event payload: a Lua table whose named fields are listed in
Reference: Events. Read a payload unless that catalog marks
the event mutable.
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" })
Hooks
For an ordinary hook that returns a value, the usual path is:
engine asks for a result -> your wrapper -> next(...) -> vanilla code
<- result <-
return result to the engine
Call next and preserve its result unless the hook's own contract says you
are changing it. This is a useful default, not a universal rule:
trainer.before_battle deliberately supplies a continue function instead
of an ordinary result path. Read the named hook's entry in
Reference: Hooks before writing a wrapper.
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.items,intro.oak_speech.build, and 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.
One hook hands its wrapper a continuation instead of expecting a value
back: trainer.before_battle receives a one-shot continue so a mod can
defer a trainer engagement across its own UI. Its contract lives on the
hook reference. A few other
wrappers are not "call next, return its result" either: input.key,
input.gamepad, input.step and render.compose have documented rules for
when skipping next is allowed, so read the entry first.
API 3 callbacks
For an api: 3 mod, mod.events and mod.hooks keep the same on, once,
emit and wrap forms, priorities and unsubscribe closures. The difference is
at the boundary: record-valued payloads (species, moves, items, parties, mons)
are converted to the shared record shape on the way in and back on the way
out, including the mutable pokemon.before_give payload, while game, battle
and context handles stay native. Only catalogued names are accepted; an
unknown name raises. A mod may subscribe to another mod's events but emits
only under its own mod.<id>.* prefix, and engine events cannot be forged.
The same rule applies to api 1 and 2 mods.
Choosing between them
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.
Performance
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.
Naming
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.
Start here
Playing
Tutorials
- Choose a tutorial
- 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
Creative guides
Concepts
Recipes and reference
Author and developer tools
Home · Start Playing · Glossary · Troubleshooting · Repository · Community
Game data is imported locally from a supported ROM supplied by the player. Shared mods contain original work or transform recipes, never extracted game content. Check each guide's target game before using its examples.