Files
gen1recomp/docs/rfcs/0004-runtime-checkpoints.md
T
2026-08-07 15:05:43 +02:00

125 lines
5.3 KiB
Markdown

# RFC 0004 — Stable runtime checkpoints for mods
## Status
Proposed. Engine: `Checkpoint.lua`, `Game.lua`, `OverworldController.lua`,
`Loader.lua`. Tests: `checkpoints.lua`, existing world and engine suites.
## Motivation
Mods can observe world events and request semantic actions, but no supported API
can capture canonical progress at a proven-safe runtime boundary or reconstruct
the overworld without replaying map-entry scripts. Reaching into the state stack,
controller, ScriptRunner, or save restore internals would bind distributable mods
to private objects and can duplicate story side effects.
The engine is the only component that can authoritatively decide whether the
runtime is settled and rebuild its controller objects. A generic checkpoint seam
lets tools store data-only records while keeping those responsibilities private.
## The decision it extends
Extends the public world/tool surfaces in `docs/modding.md`. It does not change
`mod.world`, normal CONTINUE, vanilla SAVE, or save lifecycle hooks/events.
## The exact API delta
Backward-compatible, additive-only. `Loader:_api` binds `mod.checkpoints`; mods
never receive `Game`, StateStack, controller, coroutine, renderer, or filesystem
internals inside a checkpoint.
### `mod.checkpoints:inspect(game)`
Returns a capability record. Stable overworld control returns:
```lua
{ canCapture = true, canRestore = true, kind = "overworld" }
```
A refusal returns the same booleans as `false` plus `kind`, `reason`, and a
player-readable `message`. Format-1 supports only an overworld whose controller
is topmost, player movement has settled on a tile, and no transition, foreground
or parallel ScriptRunner, queued script, scripted move, engagement, emote,
teleport, field animation, or similar partial controller mutation is active.
Refusal reasons are `not_in_playthrough`, `not_overworld`, `screen_busy`,
`transition_busy`, `script_busy`, `animation_busy`, and `movement_busy`.
Identity allocation is lazy and happens only after an active topmost overworld
has been established.
### `mod.checkpoints:capture(game)`
Returns a detached data-only format-1 checkpoint, or
`nil, code, message`:
```lua
{
format = 1,
kind = "overworld",
identity = { gameVersion = "red", playthroughId = "..." },
save = { -- canonical dynamic progress, excluding global options },
runtime = { overworld = {
map = "PALLET_TOWN", x = 5, y = 6,
facing = "down", surfing = false,
} },
}
```
Capture deep-copies through the restricted serializer before and after
`OverworldController:captureSave` synchronizes live map, tile, facing, and surf
state. It excludes `save.options`, functions, userdata, threads, metatables as
behavior, controller instances, and static content registries. Failure code
`capture_failed` covers non-data progress and synchronization errors.
### `mod.checkpoints:restore(game, checkpoint)`
Returns `true`, or `false, code, message`. Before mutation it requires the current
runtime to be capturable and validates a detached copy of the complete record:
format, kind, internal identity consistency, current game/playthrough identity,
map availability, integral in-bounds tile, facing, surfing, and synchronized save
position.
Validation codes are `invalid_checkpoint`, `unsupported_format`,
`unsupported_runtime_kind`, `wrong_game`, `wrong_playthrough`, `invalid_map`, and
`invalid_position`, in addition to the capability refusal reasons.
The engine captures an in-memory rollback checkpoint, preserves current global
options, then reconstructs semantic overworld state through
`Game:restoreCheckpointSave`. Checkpoint entry suppresses normal map exit/entry
events, `onEnter` scripts, forced-movement/current checks, and last-map rewrites;
it does not emit normal `save.loading`/`save.loaded` lifecycle events. After
reconstruction, the engine recaptures and byte-compares normalized data. A failed
apply rolls back and returns `restore_failed`; failure of that rollback returns
`rollback_failed`.
Durable recovery remains a caller responsibility: in-memory rollback handles a
runtime exception, not process termination.
## Runtime boundary and future kinds
Format 1 intentionally rejects battles, menus, transitions, animations, and
suspended/queued scripts. Future battle or explicit script-checkpoint kinds must
have separate inventories, validation, reconstruction, deterministic RNG, and
differential tests; they are not implied by this RFC.
## Migration note for existing mods
**Nothing.** No existing hook, event, save, controller, or world action changes
when `mod.checkpoints` is unused. The reconstruction path is called only by a
successful public restore after validation.
## Parity tests
- **No-mod:** the complete ROM-free engine suite and existing world behavior stay
green; ordinary New Game/save/load allocates no checkpoint identity.
- **Public Mod API:** a real API-2 entry chunk proves stable inspection and every
unsafe refusal, detached data-only capture, exact map/tile/facing/surf sync,
`A -> mutate B -> restore A -> recapture A2` equality across representative
progress, settings preservation, compatibility rejection without mutation,
map-side-effect suppression, and injected reconstruction rollback.
## Deprecation etiquette
Nothing deprecated. This adds one public facade and a checkpoint-only semantic
reconstruction route.