7.3 KiB
RFC 0005 — Persistent battle safe-point checkpoints
Status
Proposed. Extends RFC 0004. Engine: BattleCheckpoint.lua, Checkpoint.lua,
Game.lua, BattleState.lua, and OverworldController.lua. Tests:
battle_checkpoint_*.lua, checkpoints.lua, and the existing no-mod suites.
Motivation
RFC 0004 lets a tool capture and reconstruct settled overworld progress without
private engine access. A battle is a different runtime: its queue can hold Lua
functions and UI factories, its controller contains renderer objects and live
references, completion is currently an onFinish closure, and scripted battles
resume a suspended ScriptRunner coroutine. Copying the controller would create
a record that is neither data-only nor process-independent.
The engine can instead expose a narrow semantic safe point. Ordinary encounters use fixed engine-owned completion descriptors. A scripted story encounter may also participate when its active command row and remaining row-list are detached data, its NPC can be rebound by stable object id, and its completion is one of the engine-declared semantic forms. The suspended coroutine itself is never captured.
API delta
No new facade is added. The existing additive mod.checkpoints API gains a
second format-1 runtime kind.
Capability
mod.checkpoints:inspect(game) returns this only when a supported single-player
wild or trainer battle is settled at the player command menu:
{ canCapture = true, canRestore = true, kind = "battle" }
The action/message queue, waits, UI, animations, HP/status presentation, and faint processing must be settled. The player must actually control the menu. The battle must carry an engine-owned semantic continuation descriptor. An ordinary encounter requires an idle overworld. A scripted story encounter may have exactly its originating foreground runner suspended at the battle command; queued/parallel scripts and scripted movement remain unsafe.
Additional refusal codes are battle_phase_busy, battle_origin_unsupported,
battle_variant_unsupported, and link_battle_unsupported. Link, Safari,
ghost, old-man/demo, fishing, opaque callback continuations, non-data-only
scripts, and unsupported concurrent script work remain rejected.
Capture
A battle checkpoint remains detached and data-only:
{
format = 1,
kind = "battle",
identity = { engineVersion = "...", gameVersion = "red",
playthroughId = "..." },
save = { -- canonical dynamic progress, excluding global options },
runtime = {
overworld = { map = "ROUTE_1", x = 7, y = 8,
facing = "left", surfing = false },
battle = { -- normalized semantic model and continuation },
},
rng = { love = "..." },
}
The model carries player/enemy roster indices, dynamic enemy Pokémon, turn and escape state, HP/PP/status/stages/volatiles, participants, level-up tracking, trainer AI state, battle ruleset identity, side/field extension data, and normalized pointer relationships such as multi-turn move slots and Mimic restoration entries. Definitions, sprites, canvases, queues, callbacks, and controller objects are reconstructed or excluded.
Callback-bearing battle extension tokens fail with battle_extension_unsafe;
invalid live reference relationships fail with battle_state_invalid. Nothing
is silently stripped.
New overworld checkpoints also carry the LÖVE gameplay RNG state. Legacy
format-1 overworld checkpoints without rng remain loadable and leave the
current stream untouched.
Restore
Battle restore validates the detached save, map, content references, ruleset, roster indices, move references, continuation identity, and RNG before live mutation. The engine then:
- reconstructs the saved overworld return point without entry side effects;
- creates a fresh
BattleStatefrom current content registries; - applies the normalized battle model and rebuilds object-reference relations;
- binds an engine-owned wild/trainer completion continuation;
- installs the battle directly at the settled menu without replaying its intro;
- restores the RNG after reconstruction has finished; and
- recaptures and compares the complete checkpoint; and
- emits
checkpoint.restoredwith{ game = game, kind = "battle" }after the comparison succeeds.
The pre-operation checkpoint is the transaction rollback. A failed post-install RNG restore is covered: both battle runtime and RNG are reconstructed back to their original values. Validation failure, failed reconstruction, and successful rollback emit no checkpoint lifecycle event.
Continuation decision
Ordinary random wild battles resume through OverworldState:afterBattle.
Ordinary trainer battles use a descriptor containing map id, stable NPC id,
trainer class/party, and optional header event; a win reapplies the same defeated
flag, event, reward, and afterBattle path. Reconstructed overworld input and
NPC freeze state are normalized instead of reviving the old closure.
For start_battle, rival_battle, and static_battle, the runner records the
detached row list and current command program counter plus stable source/NPC
identity where present. On restore, battle completion starts a fresh runner at
that command with a one-use semantic battle result. Replaying the current
command (rather than skipping to the next row) preserves wrapper behavior such
as rival-party consequences, static-object removal, lastCheck, and deferred
afterBattle evolution ordering. The reconstructed overworld starts with
normalized input/NPC freeze state, so an engine-marked release_npc callback
needs no closure revival.
Arbitrary onDone callbacks, function-bearing rows, unknown commands, missing
NPC identities, old-man/demo flows, and concurrent scripts fail closed. This is
not a general ScriptRunner snapshot: no coroutine, Lua stack, local variable,
function, or runtime object enters the checkpoint.
Migration note
Existing mods require no changes. The facade and format number are unchanged;
the new kind, RNG field, and success-only lifecycle event are additive.
Overworld-only callers may continue to filter capability.kind. Mods with derived
runtime caches may rebuild them from restored public state when the event fires.
No-mod behavior is unchanged when checkpoints are unused.
Verification
- settled/unsafe boundary and every variant refusal;
- data-only wild and trainer capture, including callback-bearing extension rejection;
- process-independent controller and continuation reconstruction;
- exact differential recapture for wild and trainer states;
- HP, PP, status/stages/volatiles, AI layer, participants, enemy roster, multi-turn move references, and Mimic restore pointers;
- exact damage, critical, accuracy, random AI, escape, next encounter, and next raw RNG result after reload;
- corrupt content/continuation rejection before mutation;
- injected post-install failure with full runtime and RNG rollback;
- mod-added Pokémon metadata and
mod.saverewind while independentmod.storageand options remain current; - exactly one post-verification
checkpoint.restoredevent and none on failure; - legacy overworld checkpoint compatibility;
- complete ROM-free engine and public mod-API suites.
- scripted trainer and static/wild story continuation, including wrapper-row replay, cold reconstruction, malformed row rejection, and opaque callback refusal.