Skip to content

Concepts Lifecycle

bryanthaboi edited this page Jul 19, 2026 · 1 revision

Mod lifecycle

How a mod moves through the engine's boot sequence, and what it may legally do in each phase.

Phases

boot -> import -> discover -> load -> merge -> play -> save
  1. Boot. conf.lua sets the window; the engine version and mod API number come from src/core/Version.lua, the single home of every compatibility-relevant number (engine semver, mod API major, link protocol, save format, cache generation).
  2. Import. Game:load runs the ROM importer's cached data through Data:load(). Mods never participate in extraction; they see the world only after it exists.
  3. Discover. The loader scans the virtual mods/ root (source tree and save directory fused) and validates each manifest.json. A bad manifest skips that mod with a logged reason; boot continues. The engine then registers its own vanilla records into the behavioral registries under the owner engine (src/mods/Builtins.lua) — before any mod runs, so a mod's register on a vanilla id collides and must say override.
  4. Load. Manifest enforcement runs first: engine game_version ranges, hard dependencies (present, enabled, in range), conflict declarations, and dependency cycles, each failing only the mods involved. Entry chunks then run in dependency order (dependencies first, then priority ascending, ties by id). Each chunk runs under a rollback journal: if it errors, every registration it made and every subscription it took are undone, so a broken mod leaves zero residue.
  5. Merge. Registered content folds over the imported base data, one registry at a time (Loader:load, merge loop). A cross-reference pass then resolves id-typed fields against the merged world and reports dangling references. After the merge the content registries freeze: register/override/patch/remove raise from then on, including from inside a mods.loaded listener. Register content at entry-chunk time. Asset transforms run here too, and the merged mod set becomes the asset search path.
  6. Play. game.ready fires with { game = Game } once every service is up: reading ev.game is the sanctioned way to reach the live Game object. From here the game runs; mods observe it through events and hooks.
  7. Save. Saving and loading pass through the save model: meta stamping, per-mod namespaces, migrations, and the validation/quarantine pass.

Unsealed buses

The event and hook buses are never sealed. A mod may subscribe during its entry chunk, inside a mods.loaded or game.ready listener, or at any point later — from inside a battle callback if it wants. Only content freezes at the merge boundary; behavior stays open for the life of the process.

Every listener and hook link runs under pcall. A callback that raises is logged against the mod that subscribed it and skipped; the engine path that emitted the event or called the hook always completes, and a failing hook link degrades to "not installed for this call" (Events and Hooks).

Lifecycle events

Event Fires Payload
mods.loaded end of the merge phase { loader, data }
game.ready after services init, before the first screen { game }

Both fire exactly once per boot, with or without mods installed. Under POKEPORT_DEV=1, a hot reload re-emits game.ready after re-merging (src/dev/HotReload.lua).

What runs when — quick table

You want to Do it
Register/override/patch/remove content entry chunk only
Subscribe to events, wrap hooks any time
Reach the live Game game.ready payload
Touch the overworld (mod.world) after game.ready
Read another mod's exports mod.find(id) — after that mod loaded

Clone this wiki locally