8 Reference Events
bryanthaboi edited this page 2026-10-20 17:01:40 -04:00

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")

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.