Event reference
Every event the engine emits, with its payload fields, emit site and the
games that raise it. Read from the Runtime.emit call sites in src/: the
site column is where to look when you need the exact surrounding behavior.
Subscribe with mod.events:on(name, fn); see
Concepts: Events and Hooks for dispatch and
error-isolation rules.
Payloads are shared between listeners. Treat fields as read-only unless the event is listed as mutable.
Which games raise it
Event names are shared across generations; nothing is prefixed per game. The
Games column uses 1 = Red, Blue, Yellow, 2 = Gold, Silver, Crystal and
3 = FireRed, LeafGreen, Ruby, Sapphire, Emerald. The emit site shown is the
Gen 1 or shared one; the Gen 2 sites sit in the gen2 folders and src/core/Game2.lua, the
Gen 3 sites in src/core/game3, src/world/game3 and src/link/Game3Link.lua.
all* marks events raised from shared engine code (module loading, audio, the
screen stack, asset transforms, checkpoints, Discord). A listener for an event
a game never raises is legal and simply never fires, so one source file loads
everywhere. On a mod with "api": 3, mod:supports("events", name) answers
per version, from the per-generation call sites (Portable API 3).
mod.options_changed is in that catalog and true on every game. The Gen 2 and Gen 3
lists are held to the source by tests/engine/gate_gen2_mod_api.lua and
tests/engine/gate_gen3_mod_api.lua.
Find an event by task
| You want to react when | Look in |
|---|---|
| Mods finish loading or the game becomes live | Lifecycle and mods |
| A save is created, loaded, or restored | Save |
| A battle, move, catch, or level-up happens | Battle or Pokemon |
| The player changes map or interacts in the world | World |
| A script or screen opens or closes | Scripts and screens |
| A Gen 2 (or Gen 2 and 3) system changes | Gen 2 and Gen 3 events |
Where code goes: subscribe in your mod entry file. Status: this is a
complete read-only listener. ev is a payload table; its named fields are the
ones shown in the event table.
mod.events:on("map.entered", function(ev)
mod.log:info("Entered %s", ev.mapId)
end)
Read payload fields unless the catalog explicitly calls an event mutable. A callback is simply the function the game calls when the named event happens.
Lifecycle and mods
| Event | Payload | Games | Emit site |
|---|---|---|---|
mods.loaded |
{ loader, data } |
all* | src/mods/Loader.lua (end of load) |
game.ready |
{ game } |
1 2 3 | src/core/Game.lua, Game2.lua, Game3.lua; re-emitted on dev hot reload (src/dev/HotReload.lua) |
mod.options_changed |
{ mod, key, value } |
all* | src/mods/ManagerState.lua |
assets.transformed |
{ modId, count } |
all* | src/mods/AssetTransform.lua |
game.ready's payload is the sanctioned way to reach the live Game
object: read ev.game. Content registries are frozen by the time
mods.loaded fires - registering from a listener raises.
Save
| Event | Payload | Games | Emit site |
|---|---|---|---|
save.created |
{ save } |
1 2 3 | src/core/Game.lua (fresh skeleton: boot without a save, New Game) |
save.writing |
{ save, meta } |
1 2 3 | src/core/Game.lua (before the file write) |
save.loading |
{ raw } |
1 2 3 | src/core/Game.lua (parsed, before migrations/validation) |
save.loaded |
{ save, meta, modsDiff } |
1 2 3 | src/core/Game.lua (after validation) |
checkpoint.restored |
{ game, kind } |
all* | src/core/Checkpoint.lua (kind: "overworld" or "battle") |
modsDiff is { added, removed, changed } between the save's recorded
mod set and the active one (Save Model).
checkpoint.restored fires only after a mod.checkpoints:restore /
resume has reconstructed the runtime and verified it by recapture, not
on validation failure, failed reconstruction, or a successful rollback.
It is the moment for a mod to re-read its own rewound state and rebuild
caches; the payload carries no checkpoint data and no other mod's private
state.
Battle
| Event | Payload | Games | Emit site |
|---|---|---|---|
battle.started |
{ battle, kind, trainerId, species, level } |
1 2 3 | src/battle/BattleState.lua |
battle.ended |
{ battle, result } |
1 2 3 | src/battle/BattleState.lua (result: "caught", "run", "skipped", ...) |
battle.turn_started |
{ battle, turn, playerAction, enemyAction } |
1 2 3 | src/battle/BattleState.lua; also src/link/LinkBattle.lua |
battle.turn_ended |
{ battle, turn } |
1 2 3 | src/battle/BattleState.lua |
battle.move_used |
{ battle, user, target, move, isCalled } |
1 2 3 | src/battle/BattleState.lua (isCalled: Metronome / Mirror Move re-entry) |
battle.damage_dealt |
{ battle, user, target, move, damage, crit, typeMult } |
1 2 3 | src/battle/EffectRegistry.lua (typeMult base 10: 10 = neutral, 20 = super) |
battle.battler_switched |
{ battle, side, battler, previous } |
1 2 3 | src/battle/BattleState.lua (both sides, all switch paths) |
battle.fainted |
{ battle, battler } |
1 2 3 | src/battle/BattleState.lua |
battle.exp_gained |
{ battle, mon, gained, levels } |
1 2 3 | src/battle/BattleState.lua |
battle.ball_thrown |
{ battle, ball, caught, shakes } |
1 2 3 | src/battle/BattleState.lua (Safari and bag throws) |
battle.status_inflicted |
{ battle, target, status, source } |
1 2 3 | src/battle/StatusRegistry.lua |
Every battle event has a Gen 2 site (src/battle/gen2/Battle.lua,
src/ui/gen2/BattleState.lua) and a Gen 3 site under src/core/game3/battle
with the same payload keys; Gen 3 battlers are native Gen 3 battle records,
and a Gen 3 double battle raises the same events per battler.
Pokemon
| Event | Payload | Games | Emit site |
|---|---|---|---|
pokemon.before_give |
{ ctx, species, level, nickname? }, mutable |
1 3 | src/script/Commands.lua (give_pokemon); src/core/game3/scripting/ops_a.lua; the portable give_pokemon row (src/mods/UnifiedScripts.lua) |
pokemon.caught |
{ battle, mon, species, isNew, ball, destination, game } |
1 2 3 | src/battle/BattleState.lua (destination = "mod" when catch.party_full took custody) |
pokemon.level_up |
{ mon, level, prevLevel, learnable } |
1 2 3 | src/battle/Experience.lua |
pokemon.move_learned |
{ mon, moveId } |
1 2 3 | src/battle/BattleState.lua, src/pokemon/Evolution.lua |
pokemon.evolved |
{ mon, fromSpecies, toSpecies, via } |
1 2 3 | src/pokemon/Evolution.lua |
pokemon.received |
{ mon, from = "link", peerName } |
1 2 3 | src/link/Protocol.lua, src/link/LinkBattle.lua; Gen 2 LinkBattle2.lua; Gen 3 src/core/game3/link/trade.lua |
pokemon.before_give is the grandfathered mutable event: every story
gift, fossil, and scripted give_pokemon passes through it, and listeners
may rewrite species, level, and nickname before the mon is built.
ctx is the running script context (ctx.save, ctx.overworld).
World
| Event | Payload | Games | Emit site |
|---|---|---|---|
map.entered |
{ mapId, map, fromMapId, via } |
1 2 3 | src/world/OverworldController.lua (via: "boot", "warp", "connection", "reload", ...) |
map.exited |
{ mapId, toMapId } |
1 2 3 | src/world/OverworldController.lua |
map.reloaded |
{ mapId, reason } |
1 2 3 | src/world/OverworldController.lua, src/world/WorldAPI.lua |
player.warped |
{ fromMap, toMap, x, y, warp } |
1 2 3 | src/world/OverworldController.lua |
world.stepped |
{ mapId, x, y, tile } |
1 2 3 | src/world/OverworldController.lua (hot path - keep listeners cheap) |
world.interacted |
{ mapId, x, y, kind, target } |
1 2 3 | src/world/OverworldController.lua |
world.npc_spawned |
{ mapId, npcId, runtime } |
1 2 3 | src/world/OverworldController.lua |
world.trainer_engaged |
{ npc, trainerClass, partyIndex } |
1 2 3 | src/world/OverworldController.lua |
world.boulder_moved |
{ mapId, npcId, x, y } |
1 2 3 | src/world/OverworldController.lua |
world.blacked_out |
{ save, healTarget } |
1 2 3 | src/world/OverworldController.lua |
world.block_replaced |
{ mapId, bx, by, block } |
1 2 3 | src/world/OverworldController.lua |
world.object_toggled |
{ mapId, objName, visible } |
1 2 3 | src/world/WorldAPI.lua |
world.tod_changed |
{ tod, previous, mapId } |
1 2 | src/world/OverworldController.lua (timeOfDay - when world.tod returns a new period) |
world.live_maps_preparing |
{ mapId, maps } |
1 2 | src/world/OverworldController.lua, src/world/gen2/World.lua: the neighborhood is known, connected-map ghosts are not built yet |
world.live_maps_updated |
{ mapId, maps } |
1 2 | same sites, once the ghosts are materialized |
flag.changed |
{ name, value } |
1 2 3 | src/script/Flags.lua (set and clear); Gold's numeric wEventFlags bits from src/world/gen2/Events.lua; Gen 3 src/core/game3/scripting/flags.lua |
flag.changed fires only on a real transition -- setting a flag that is
already set is silent -- and covers the story beats the overworld writes
itself as well as the ones scripts set: a card-key door opening, a defeated
trainer's event, a victory item or reward (and the rewards it deactivates),
the Route 22 and Route 23 gate guards, a Seafoam boulder dropped in a hole,
and a battle started by walking into a trainer's line of sight. name is
the pokered EVENT_* constant on Gen 1, the numeric wEventFlags bit on
Gold, and on Gen 3 the pret FLAG_* name when known (else the number), with
the numeric id beside it. A listener that wants every game writes one arm per
naming scheme.
npcId on world.npc_spawned and world.boulder_moved, ev.npc.id on
world.trainer_engaged, and ev.target.id on a world.interacted whose
kind is "npc" are all the same thing: the object id
"<mapId>_obj_<index>" the save keys defeated trainers and taken items
by. For a mapped object it is stable across sessions, so it is the key to
hang per-NPC mod state off; a runtime spawn (runtime = true on
world.npc_spawned) gets an id allocated at spawn time and is never
serialized. See
Map objects and object ids.
On Gold, world.trainer_engaged keeps those three keys and adds
trainerEvent and sight (src/world/gen2/World.lua); Gen 3 adds trainerId,
mapId and sight (src/core/game3/trainer_sight.lua).
world.live_maps_preparing and world.live_maps_updated bracket each rebuild
of the active map plus its connected neighbors (see mod.world:liveMaps() in
the Mod Object). Both fire on map load and
on survey-zoom resizes, and on Gold on the hourly object refresh, so handlers
must be idempotent. spawnNpc and removeNpc inside preparing land in the
same pass, so a dynamic actor exists before its ghost does. Gen 3 has no
neighbor-ghost pass and raises neither.
Scripts and screens
| Event | Payload | Games | Emit site |
|---|---|---|---|
script.started |
{ ctx } |
1 2 3 | src/script/ScriptRunner.lua; Gen 2 src/script/gen2/Vm.lua; Gen 3 src/core/game3/scripting/vm.lua |
script.ended |
{ ctx, completed } |
1 2 3 | same sites (completed = false on abort) |
screen.pushed |
{ state } |
all* | src/core/StateStack.lua; Gen 3 mod screens from src/ui/game3/mod_ui.lua |
screen.popped |
{ state } |
all* | same |
Intro (Oak speech)
Fired from src/ui/OakSpeech.lua during NEW GAME. Gold's Elm speech
(src/ui/gen2/OakSpeech.lua) and the Gen 3 new-game scene
(src/ui/game3/new_game_scene.lua) raise the same four names and payload keys
for the same moment. The step list itself is reshaped through
intro.oak_speech.build on Gen 1 and Gen 2; Gen 3 has no
build hook, only these events.
| Event | Payload | Games | Emit site |
|---|---|---|---|
intro.oak_speech.started |
{ speech, steps } |
1 2 3 | OakSpeech:enter (after the build hook) |
intro.oak_speech.step |
{ speech, step, index } |
1 2 3 | each advance into a step |
intro.oak_speech.answered |
{ speech, step, index, label, value, saveKey } |
1 2 3 | after a name / choice / yesno |
intro.oak_speech.finished |
{ speech, answers } |
1 2 3 | just before the screen pops |
answered.value keeps booleans (false is not collapsed to the label).
Write durable answers with mod.save:set(ev.saveKey, ev.value) - they
land in save.modData[<id>] once the slot is adopted
(Save Model).
Audio
| Event | Payload | Games | Emit site |
|---|---|---|---|
music.started |
{ song, previous, chip, reason } |
all* | src/core/Music.lua |
music.stopped |
{ song } |
all* | src/core/Music.lua |
sound.played |
{ kind, name, species } |
all* | src/core/Sound.lua (kind: "sfx" or "cry") |
Link (cart protocol telemetry)
Player link setup happens in the launcher with a vanilla cart or sealed custom cart. These events document the engine cart protocol; subscribing to them does not make a freely enabled loose mod eligible for player link play.
| Event | Payload | Games | Emit site |
|---|---|---|---|
link.connected |
{ role, remote = { name, mode, mods, fingerprint } } |
1 3 | src/link/LinkState.lua; Gen 3 src/link/Game3Link.lua (once the handshake has the peer's hello) |
link.ended |
{ reason } |
1 3 | src/link/LinkState.lua; Gen 3 src/link/Game3Link.lua |
link.desync |
{ turn, component, localHash, remoteHash, fatal, seat? } |
1 2 3 | src/link/LinkBattle.lua, src/link/LinkBattle2.lua, src/core/game3/link/battle.lua (seat is Gen 3 only) |
link.battle_ended |
{ result, myParty, theirParty, peerName, role } |
1 2 3 | src/ui/ArenaState.lua, src/link/LinkState.lua; Gen 2 src/ui/gen2/ArenaState.lua; Gen 3 src/core/game3/link/battle.lua |
trade.completed |
{ sent, received, evolveTo } |
1 3 | src/link/Protocol.lua; Gen 3 src/core/game3/link/trade.lua |
discord.join_requested |
{ code, kind, invite } |
all* | src/core/DiscordPresence.lua (a Discord Ask-to-Join was accepted) |
A Gold boot has no in-game link menu or trade room, so link.connected,
link.ended and trade.completed are not raised there; Gold's launcher arena
battles raise link.battle_ended and link.desync.
Gen 2 and Gen 3 events
These events belong to mechanics the Gen 1 games do not have. They are
ordinary events: same mod.events:on, same isolation rules. A listener on a
game that never raises one simply never fires. Gen 3 has no phone, radio,
apricorns, mail, Bug Contest or Unown puzzle, so those stay Gen 2 only; the
Gen 3 carts do share the breeding, happiness, Pokerus and roamer systems.
Gen 2 boot intro
The boot intro emits one event per screen as it advances. The Gen 2 games
(Gold, Silver, Crystal) raise all of them; the cards differ per version
(Crystal has its own splash and intro movie, CrystalSplash.lua and
CrystalIntro.lua).
| Event | Payload | Games | Emit site |
|---|---|---|---|
intro.boot.copyright |
{ screen, game } |
2 | src/ui/gen2/CopyrightSplash.lua |
intro.boot.gamefreak |
{ screen, game } |
2 | src/ui/gen2/GameFreakPresents.lua, CrystalSplash.lua |
intro.boot.movie |
{ screen, game } |
2 | src/ui/gen2/GoldSilverIntro.lua, CrystalIntro.lua |
intro.boot.movie_ended |
{ screen, game, skipped, frames } |
2 | same (skipped is true when the player pressed START) |
intro.boot.title |
{ screen, game } |
2 | src/ui/gen2/TitleState.lua |
Shared by Gen 2 and Gen 3
| Event | Payload | Games | Emit site |
|---|---|---|---|
happiness.changed |
{ mon, event, reason, delta, from, to } |
2 3 | src/core/gen2/Happiness.lua; Gen 3 src/core/game3/pokemon.lua |
pokerus.infected |
{ party, slot, mon, strain, days, source } |
2 3 | src/core/gen2/Pokerus.lua; Gen 3 pokemon.lua |
breeding.egg_created |
Gen 2 { egg, mother, father, compatibility, stepsToEgg }; Gen 3 { egg, stepsToEgg } |
2 3 | src/core/gen2/Breeding.lua; Gen 3 src/core/game3/breeding.lua |
egg.hatched |
{ mon, egg, slot, species, nickname } |
2 3 | Breeding.lua (both) |
roamer.moved |
{ index, slot, species, from, to, reason } |
2 3 | src/core/gen2/Roamers.lua; Gen 3 src/core/game3/roamer.lua |
roamer.encountered |
{ index, slot, species, level, mapId } |
2 3 | same sites |
Gen 2 only
| Event | Payload | Games | Emit site |
|---|---|---|---|
clock.day_changed |
{ day, previous, reason } |
2 | src/core/gen2/Clock.lua |
apricorn.converted |
{ apricorn, ball, event } |
2 | src/core/gen2/Apricorns.lua (Kurt finishes a ball) |
bug_contest.scored |
{ mon, score, place, results } |
2 | src/core/gen2/BugContest.lua |
unown.unlocked |
{ letter, name, word, count } |
2 | src/core/gen2/Unown.lua |
mail.written |
{ entry, slot, mon, message, author, source } |
2 | src/core/gen2/Mail.lua |
mail.read |
{ entry, message, author, top, bottom } |
2 | src/core/gen2/Mail.lua |
phone.call_received |
{ call, contact, name, className, special, scriptKey } |
2 | src/core/gen2/PhoneRing.lua |
radio.channel |
{ station, channel, name, source } |
2 | src/ui/gen2/MapRadio.lua |
delta on happiness.changed is to - from, not the table's column,
because the 0 and 255 clamps are part of what the cart applied. clock.day_changed
compares against a process-local latch, so the first read after a boot raises
nothing. unown.unlocked is raised when a form first enters the dex list.
mail.read re-arms per letter, so reopening the same letter raises a second
event.
Mod-emitted events
A mod may emit only under its own prefix, so no mod can forge an engine event:
mod.events:emit("mod.my_mod_id.season_changed", { season = "winter" })
Anyone (any mod) can subscribe to mod.<id>.* names with the ordinary
mod.events:on.
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.