5
Reference Events
bryanthaboi edited this page 2026-08-15 23:44:59 -04:00

Event reference

Every event the engine emits, with its payload fields and emit site. 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.

Lifecycle and mods

Event Payload Emit site
mods.loaded { loader, data } src/mods/Loader.lua (end of load)
game.ready { game } src/core/Game.lua; re-emitted on dev hot reload (src/dev/HotReload.lua)
mod.options_changed { mod, key, value } src/mods/ManagerState.lua
assets.transformed { modId, count } 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 Emit site
save.created { save } src/core/Game.lua (fresh skeleton: boot without a save, New Game)
save.writing { save, meta } src/core/Game.lua (before the file write)
save.loading { raw } src/core/Game.lua (parsed, before migrations/validation)
save.loaded { save, meta, modsDiff } src/core/Game.lua (after validation)
checkpoint.restored { game, kind } 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 Emit site
battle.started { battle, kind, trainerId, species, level } src/battle/BattleState.lua
battle.ended { battle, result } src/battle/BattleState.lua (result: "caught", "run", "skipped", ...)
battle.turn_started { battle, turn, playerAction, enemyAction } src/battle/BattleState.lua; also src/link/LinkBattle.lua
battle.turn_ended { battle, turn } src/battle/BattleState.lua
battle.move_used { battle, user, target, move, isCalled } src/battle/BattleState.lua (isCalled: Metronome / Mirror Move re-entry)
battle.damage_dealt { battle, user, target, move, damage, crit, typeMult } src/battle/EffectRegistry.lua (typeMult base 10: 10 = neutral, 20 = super)
battle.battler_switched { battle, side, battler, previous } src/battle/BattleState.lua (both sides, all switch paths)
battle.fainted { battle, battler } src/battle/BattleState.lua
battle.exp_gained { battle, mon, gained, levels } src/battle/BattleState.lua
battle.ball_thrown { battle, ball, caught, shakes } src/battle/BattleState.lua (Safari and bag throws)
battle.status_inflicted { battle, target, status, source } src/battle/StatusRegistry.lua

Pokemon

Event Payload Emit site
pokemon.before_give { ctx, species, level, nickname? } — mutable src/script/Commands.lua (give_pokemon)
pokemon.caught { battle, mon, species, isNew, ball, destination, game } src/battle/BattleState.lua
pokemon.level_up { mon, level, prevLevel, learnable } src/battle/Experience.lua
pokemon.move_learned { mon, moveId } src/battle/BattleState.lua
pokemon.evolved { mon, fromSpecies, toSpecies, via } src/pokemon/Evolution.lua
pokemon.received { mon, from = "link", peerName } src/link/Protocol.lua, src/link/LinkBattle.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 Emit site
map.entered { mapId, map, fromMapId, via } src/world/OverworldController.lua (via: "boot", "warp", "connection", "reload", ...)
map.exited { mapId, toMapId } src/world/OverworldController.lua
map.reloaded { mapId, reason } src/world/OverworldController.lua, src/world/WorldAPI.lua
player.warped { fromMap, toMap, x, y, warp } src/world/OverworldController.lua
world.stepped { mapId, x, y, tile } src/world/OverworldController.lua (hot path — keep listeners cheap)
world.interacted { mapId, x, y, kind, target } src/world/OverworldController.lua
world.npc_spawned { mapId, npcId, runtime } src/world/OverworldController.lua
world.trainer_engaged { npc, trainerClass, partyIndex } src/world/OverworldController.lua
world.boulder_moved { mapId, npcId, x, y } src/world/OverworldController.lua
world.blacked_out { save, healTarget } src/world/OverworldController.lua
world.block_replaced { mapId, bx, by, block } src/world/OverworldController.lua
world.object_toggled { mapId, objName, visible } src/world/WorldAPI.lua
world.tod_changed { tod, previous, mapId } src/world/OverworldController.lua (timeOfDay — when world.tod returns a new period)
flag.changed { name, value } src/script/Flags.lua (set and clear)

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

Scripts and screens

Event Payload Emit site
script.started { ctx } src/script/ScriptRunner.lua
script.ended { ctx, completed } src/script/ScriptRunner.lua (completed = false on abort)
screen.pushed { state } src/core/StateStack.lua
screen.popped { state } src/core/StateStack.lua

Intro (Oak speech)

Fired from src/ui/OakSpeech.lua during NEW GAME. The step list itself is reshaped through intro.oak_speech.build.

Event Payload Emit site
intro.oak_speech.started { speech, steps } OakSpeech:enter (after the build hook)
intro.oak_speech.step { speech, step, index } each advance into a step
intro.oak_speech.answered { speech, step, index, label, value, saveKey } after a name / choice / yesno
intro.oak_speech.finished { speech, answers } 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 Emit site
music.started { song, previous, chip, reason } src/core/Music.lua
music.stopped { song } src/core/Music.lua
sound.played { kind, name, species } src/core/Sound.lua (kind: "sfx" or "cry")
Event Payload Emit site
link.connected { role, remote = { name, mode, mods, fingerprint } } src/link/LinkState.lua
link.ended { reason } src/link/LinkState.lua
link.desync { turn, component, localHash, remoteHash } src/link/LinkBattle.lua
trade.completed { sent, received, evolveTo } src/link/Protocol.lua
discord.join_requested { code, kind } src/core/DiscordPresence.lua (a Discord Ask-to-Join was accepted)

Gen 2 (Gold) events

These fire only on Gold, because the mechanic behind each one does. They are ordinary events: same mod.events:on, same isolation rules; a listener registered on a Red boot simply never fires.

The boot intro emits one event per screen as it advances:

Event Payload Emit site
intro.boot.copyright { screen, game } src/ui/gen2/CopyrightSplash.lua
intro.boot.gamefreak { screen, game } src/ui/gen2/GameFreakPresents.lua
intro.boot.movie { screen, game } src/ui/gen2/GoldSilverIntro.lua
intro.boot.movie_ended { screen, game, skipped, frames } src/ui/gen2/GoldSilverIntro.lua
intro.boot.title { screen, game } src/ui/gen2/TitleState.lua

Gold's own systems:

Event Payload Emit site
clock.day_changed { day, previous, reason } src/core/gen2/Clock.lua
happiness.changed { mon, event, reason, delta, from, to } src/core/gen2/Happiness.lua
pokerus.infected { party, slot, mon, strain, days, source } src/core/gen2/Pokerus.lua
breeding.egg_created { egg, mother, father, compatibility, stepsToEgg } src/core/gen2/Breeding.lua
egg.hatched { mon, egg, slot, species, nickname } src/core/gen2/Breeding.lua
apricorn.converted { apricorn, ball, event } src/core/gen2/Apricorns.lua (Kurt finishes a ball)
roamer.moved { index, slot, species, from, to, reason } src/core/gen2/Roamers.lua
roamer.encountered { index, slot, species, level, mapId } src/core/gen2/Roamers.lua
bug_contest.scored { mon, score, place, results } src/core/gen2/BugContest.lua
unown.unlocked { letter, name, word, count } src/core/gen2/Unown.lua
mail.written { entry, slot, mon, message, author, source } src/core/gen2/Mail.lua
mail.read { entry, message, author, top, bottom } src/core/gen2/Mail.lua
phone.call_received { call, contact, name, className, special, scriptKey } src/core/gen2/PhoneRing.lua
radio.channel { station, channel, name, source } src/ui/gen2/MapRadio.lua

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.