2
Concepts Lifecycle
bryanthaboi edited this page 2026-08-15 23:44:59 -04:00

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, the games list against the booted game, hard dependencies (present, enabled, in range, and not scoped to another game), conflict declarations, missing required_imports, 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 inside its own sandbox (src/mods/Sandbox.lua): a private _G, compat stand-ins for the removed globals, and no reach into the engine's namespace. 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
Poll mod.fetch / mod.job handles any time; poll never blocks, and handles are released on unload