Skip to content

Boot, Saves & Mods

The boot and load sequence, the save/version model, and how mods extend a game.

Reference: Config, Save, Keybinds & Passage Utilities.

The Boot & Load Sequence

The engine does not run GameInit on startup. It boots to the title screen (@system(title)). GameInit runs in exactly two situations, in this order:

New game ((game-new:)):

  1. All systems reset.
  2. @system(init) (GameInit) runs: declarations, definitions, config.
  3. The engine navigates to the (start:) passage.

Loading a save ((load: slot)):

  1. Systems reset.
  2. @system(init) (GameInit) runs again, to re-establish the variable schema and re-register definitions.
  3. The saved values are deserialized over that fresh schema.
  4. @system(init_save) (GameInitSave) runs, for any state-dependent re-initialization.
  5. @on(gameLoad) handlers fire.
  6. The engine navigates to the saved passage. (start:) is ignored on load.

The decision rule this implies: GameInit should contain only declarations, definitions, and configuration: things that are safe to run on every load because the saved values immediately overwrite them. Put one-time side effects (giving the player a starting item, a welcome (notify:), a scripted opening beat) in the (start:) passage or an @on(gameStart) handler, not in GameInit. If you (add: $inv, "starter_knife", 1) inside GameInit, the count is set during step 2 and then replaced by the saved count in step 3, so it won't duplicate on load, but it also won't behave like a real one-time event. Keep side effects out of GameInit.

Back to the title screen ((game-end:)): the mirror of (game-new:). It unloads the current game (resets state and systems, clears the session, restores the default layout) and returns to @system(title), so the title renders clean, with no stale clock or location lingering from the session you just left. It does not confirm on its own; wrap it in (confirm:) for the usual "unsaved progress will be lost" prompt. See (game-end:).

Saves, Versions & Mods

Save format versioning. Saves are stamped with a schema version. When the engine's save format changes between releases, GameSystems.migrate() upgrades older saves step by step on load; if a save is too corrupt or incompatible to migrate, the player is offered a "reset progress" dialog rather than a crash.

Your game's own versioning. Engine migration covers the engine's structures, not your content. For changes to your own data across game updates, version it yourself: keep a $game.version variable and reconcile in an @on(gameLoad) handler (e.g. grant a newly-added variable a default if the loaded save predates it). Because GameInit re-runs before deserialization, newly declared variables already have their defaults when an old save loads; the save simply doesn't override them. That makes adding variables safe by default; renaming or removing them is what needs an @on(gameLoad) fix-up.

Orphaned data (mods). If a save references an item template that no longer exists (say, a mod that defined it was uninstalled), deserialization does not break. The orphaned instance is kept, but (get: $item, uid, "...") returns undefined for it (since the template is gone). Guard mod-dependent content with (passage-exists:) and treat missing item properties as absent rather than assuming they're present.

Performance & lifecycle. Reactive bindings clean themselves up: every (watch:), (after:), and (every:) is torn down automatically when its zone is rebuilt or you navigate away, so they cannot leak across passages or fire after a scene change. There's no hard cap on passage count or watch density, but keep per-screen (watch:) bindings to what's actually visible, and remember that large arrays in $variables are serialized into every save, so prefer event-flag arrays of short strings over accumulating big structures.

Modding and Extension

Ana is designed from the ground up for mod support. See the Modding Guide for the complete reference. Key patterns:

Mod directory layout: place mod files under mods/my-mod/ with a mod.meta config file:

mods/
  my-mod/
    mod.meta            ← name, version, requires, conflicts
    passages/
      MyMod_Stuff.ana   ← any .ana files here are loaded after base game

Adding content to scenes: use @into(hookName). No base game files need editing.

Adding new NPCs: define them in a mod's @system(init) passage and inject their presence into scene hooks.

Adding new passages: passages in any .ana file in passages/ are available globally by name. A mod passage named MyMod_SpecialItem is reachable via (goto:), (update:), or (action:) from any base game passage.

Conditionally referencing mod content: use (passage-exists: "ModPassageName") to gracefully degrade when the mod isn't loaded, or (mod: "Mod Name") to branch on whether a whole mod is present:

ana
(if: (passage-exists: "DartsMod_Game"))[
    (link: "Play darts")[(goto: DartsMod_Game)]
]
(if: (mod: "Extra Outfits"))[An extra rack of clothes stands against the wall.]

Load order is derived from each mod's requires (a required mod loads first; otherwise alphabetical by name); there is no load_order field. The engine populates a $mods namespace and (mod-list:) so games can introspect what's loaded.

Mod init passages: mods can run initialization code using their own @system(init) passage:

ana
:: MyMod_Init @system(init)

// Register mod NPC data
(npc-define: "blacksmith", name, "Vera", location, "forge", events, [])

// Register mod items
(item-define: "custom_blade", unique, name, "Vera's Blade", type, "weapon", damage, 18)

// Register mod containers
(container-define: "forge_storage")

Custom logic, widgets & minigames: when a mod needs behavior the macro language can't express (a card game's deck logic, a canvas minigame, custom animated UI), drop to JavaScript with (script:) / (html:) / (css:). See Extending with JavaScript for the full walkthrough, including the ctx state bridge and the timing/cleanup rules for animated DOM.