-
Notifications
You must be signed in to change notification settings - Fork 2
Concepts Save Model
What a save records about mods, where a mod keeps its own state, how old
saves upgrade, and what happens when a save references content that no
longer exists. Implementation: src/core/SaveData.lua, src/core/Game.lua
(restoreSave).
Every save carries save.meta = { format, mods }:
-
format— the save-format number (Version.saveFormat, currently 2). Core migrations run any steps the save's format predates, then stamp the current number. A pre-meta vanilla save becomes a v2 vanilla save. -
mods— the{ id, version }set that was active when the save was written. On load,SaveData.modsDiffcompares it against the active set and produces{ added, removed, changed }; a non-empty diff surfaces a one-line notice ("This save was made with 2 mods; 1 is no longer active") and rides thesave.loadedpayload.
Saves are plain Lua tables serialized with deterministic key order and read back through a data-only parser — a save can never execute code. Every write makes a rolling backup first.
Mod state is namespaced, never mixed into engine tables:
mod.save:set("stage", 2) -- persists under save.modData[modId]
local stage = mod.save:get("stage", 0)
mod.options:define({
{ key = "hard_mode", type = "toggle", label = "HARD MODE", default = false },
{ key = "scale", type = "number", label = "SCALE", default = 1, min = 1, max = 5 },
{ key = "voice", type = "choice", label = "VOICE",
choices = { { "LOW", "low" }, { "HIGH", "high" } }, default = "low" },
})
local hard = mod.options:get("hard_mode")-
mod.savereads and writessave.modData[modId]— progress state, saved with the game. -
mod.optionsreadsoptions.modOptions[modId]— settings, persisted inoptions.luabeside the engine's own, surviving New Game. Defined rows render automatically in the mod manager; row types aretoggle,choice(choices = { { label, value }, ... }),number(min/max/step), andtext(src/mods/ManagerState.luabuildOptionRows). Changing a value emitsmod.options_changed({ mod, key, value }), so a mod can react without polling.
Use mod.save for quest and feature state instead of global flags —
flags are a shared namespace, modData is yours alone.
When a new mod version changes its own modData shape, register an
upgrade step:
mod.migrations:add("1.1.0", function(save)
local bucket = save.modData[mod.id]
if bucket and bucket.stage == nil then bucket.stage = 0 end
end)add(since, fn) records "upgrading from before since runs fn". On
load, each active mod's chain runs in semver order against the version the
save recorded for that mod, before validation — so a mod repairs its own
data instead of watching it get quarantined (SaveData.runMigrations,
driven from Game:restoreSave).
After migrations, SaveData.validate walks every content id the save
references against the merged data and quarantines unknowns instead of
letting them nil-index later:
-
Mons whose species no longer exists move to
save.orphaned— the LOST box — with a report row. - Items whose id no longer exists are removed with a report row.
- Locations that no longer exist fall back to the heal point.
- Reclaim first: quarantined content whose id reappeared (the mod was re-enabled) goes home again — mons through a PC deposit, items through the bag with the PC as overflow.
On a mod-free save every membership test passes and the save comes back untouched. The load report is logged and the diff notice shown; nothing is silently dropped.
| Name | Kind | Payload / signature | Fires |
|---|---|---|---|
save.created |
event | { save } |
a fresh save skeleton exists (boot without a save; New Game) |
save.writing |
event | { save, meta } |
before save.lua is written |
save.loading |
event | { raw } |
after parse, before migrations and validation |
save.loaded |
event | { save, meta, modsDiff } |
after validation, save restored |
save.new_game |
hook | wrap(next, save) -> save |
New Game skeleton construction — a total conversion reshapes spawn, party, and money here before anything reads them |
The new-game skeleton itself is data-driven: spawn, names, and money come
from field.boot (Data Model), so most mods patch
that instead of wrapping save.new_game.
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.
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
Playing