4 Concepts Lifecycle
bryanthaboi edited this page 2026-10-20 17:01:40 -04:00

Mod lifecycle

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

When your file runs

Your mod entry file runs during load, before the engine combines mod content with the game's imported data. Put registry calls such as mod.content.moves:patch(...) there. They describe the content the game will use after the merge; they do not change a running game immediately.

The live game exists later. Use the game.ready event when an action needs the live Game object, and touch mod.world only after that event. Events and hooks are different: you may subscribe or wrap them in the entry file and they run when their documented game moment occurs. Do not put every runtime action inside game.ready; use the event or hook that describes the moment you need.

For a first content change, see Tutorial 00. For the exact capabilities on mod, see The mod object.

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). The launcher then boots one game: Game for Gen 1, Game2 for Gold, Silver and Crystal, Game3 for the Game Boy Advance games; each builds its own loader with the booted generation and version, which routes every registry (see Data Model).
  2. Import. The game's load runs the ROM importer's cached data through Data:load() (Gen 3 binds its generated tables with Schemas.bindGen3). 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: the manifest api against the engine's modApi, engine game_version ranges, the games list against the booted game (a mod that does not claim it is skipped, not failed), 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 and optional dependencies first; then a custom cart's load_order, the player's launcher load order, priority ascending, and finally 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, shallowest target first (Loader:load, merge loop). Each registry lands in the table its generation routes it to. 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 (Game, Game2 or Game3; mod.game resolves the same one). 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