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") |
Link
| 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.
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
- Art Pipeline
- Audio Authoring
- Preparing Your Mod for Gen 2
- Translations
- Total Conversions
- Link Compatibility
- Publishing a Mod
- Custom Carts
- Docs Style Guide
Playing
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.