Merge remote-tracking branch 'origin/dev' into feat/battle-menu-auxiliary

# Conflicts:
#	src/core/Checkpoint.lua
This commit is contained in:
MaxTomahawk
2026-08-12 09:46:00 +02:00
542 changed files with 231061 additions and 1905 deletions
-96
View File
@@ -1,96 +0,0 @@
# RFC 0001 — Port Yellow's `IsSurfingPikachuInParty` surf sprite
## Status
Proposed. Engine: `Player.lua`, `FieldDefaults.lua`,
`OverworldController.lua`, `RomExtractor.lua`, `PaletteFX.lua`. Tools:
`build_rom_data.py`, `extract/sprites.py`, `make_rom_manifest.py`,
`make_yellow_manifest.py`. Tests: `parity_surfing_pikachu_sprite.lua`,
`mod_world_tests.lua`.
**Regeneration required.** The manifest and sprite sheet update by
re-running `make_yellow_manifest.py` against a `pret/pokeyellow`
checkout, then re-importing the Yellow ROM.
## Motivation
Yellow's `IsSurfingPikachuInParty` + `LoadSurfingPlayerSpriteGraphics2`
(`home/map_objects.asm`, `home/overworld.asm`) swap the player's
overworld sheet to `SurfingPikachuSprite` (`gfx/sprites/
surfing_pikachu.2bpp`, a 16×96 walk sheet — not the minigame sheets)
when the party mon that knows SURF is a Pikachu. The recomp misses this
in two places:
1. **Extraction.** `SurfingPikachuSprite` is not in
`SpriteSheetPointerTable` — loaded by its own `ld de,` like
`RedBikeSprite`. The extractor never sees it, and the symbol is not
in the Yellow manifest.
2. **Engine rule.** `field.playerSprites.surf` is one static
(`SPRITE_SEEL`), cached at boot. No seam for "swap when the SURF-mon
is a Pikachu."
## The decision it extends
No prior D-number. Extends the surf-field-move port in
`docs/behavior-porting-notes.md` (the `IsSurfingAllowed` exact port)
with the player-sprite swap vanilla runs alongside it.
## The exact API delta
Backward-compatible, additive-only.
### `field.playerSprites.surfPikachu`
New optional key alongside `walk`/`surf`/`bike`/`fly`, defaults to
`SPRITE_SURFING_PIKACHU`. Guarded in `Player.new` so before extraction
lands the ride keeps the Seel — no plain on-water Pikachu.
### `Player.surfPikachuSprite`
`Player.new` builds a second `SpriteRenderer` when the field resolves.
`pose()` picks it when `surfing and surfingPikachu`.
### `Player.surfingPikachu` (runtime)
Runtime-only boolean (not persisted); re-derived so a party change
between save and load is honored.
### `OverworldState:syncSurfingPikachu()`
Sets `player.surfingPikachu` from `partyKnows("SURF")`. Called at every
surf-state toggle: trySurf, dismount, flyTo, beginTeleportOut,
warpToHealPoint, forced-surf tile, setMap boot-restore.
### Importer — `SPRITE_SURFING_PIKACHU`
`make_yellow_manifest.py` adds `SurfingPikachuSprite` to
`YELLOW_EXTRA_SYMBOLS`. `make_rom_manifest.py`'s `sprite_metadata()`
gains a `surfPikachu` entry (guarded, so Red/Blue unchanged).
`RomExtractor.extractSprites` + `build_rom_data.py` + `extract/sprites.py`
each gain a parallel extract mirroring `RedBikeSprite`.
### `PaletteFX.spriteObp`
`SurfingPikachuSprite` joins `RedBikeSprite` in the no-bracket-index
special case, wearing the player's OBP palette so it colors in GBC mode.
## Migration note for existing mods
**Nothing.** `surf` still defaults to `SPRITE_SEEL`; `surfPikachu`
only resolves on a Yellow import after regeneration. No manifest or
`mod.save` shape changes. An eligibility hook that swaps a rental
SURF-mon still drives the sprite pick via `partyKnows`.
## Parity tests
- **No-mod** (`mod_world_tests.lua`): `surf == "SPRITE_SEEL"`,
`surfPikachu == "SPRITE_SURFING_PIKACHU"` seeded at boot. The 19229-check
`world & maps v2` suite stays green.
- **Mod-API** (`parity_surfing_pikachu_sprite.lua`): `syncSurfingPikachu`
+ `Player:pose` across four party shapes (12/12). The existing
`parity_cinnabar_east_surf.lua` (24/24) stays green.
## Deprecation etiquette
Nothing deprecated. Additive: a new `field.playerSprites` key, a new
runtime flag, a new engine method, a new sprite id.
-54
View File
@@ -1,54 +0,0 @@
# RFC 0002 — Let mods hide an active screen state from the main render
## Status
Proposed. Engine: `StateStack.lua`, `Game.lua`. Tests:
`screen_render_visible.lua`.
## Motivation
A mod can render a native menu on a companion display through
`render.compose`, but it cannot remove that menu from the main display without
also popping it. Popping transfers update and input ownership and forces the
mod to reimplement native menu behavior.
## The decision it extends
No prior D-number. Extends the render-hook plan in `docs/modding.md` and the
state-stack rendering contract in `docs/architecture.md`.
## The exact API delta
Backward-compatible, additive-only.
### `screen.render_visible`
New hook called with `(state) -> boolean` through the public wrapper signature
`(next, state)`. Its vanilla result is `true`.
Returning `false` excludes the state from the main draw, from opaque-base
selection and from palette-zone ownership. It does not remove the state or
change update, input, push or pop behavior. The call sites are
`StateStack:visibleBase`, `StateStack:draw` and the equivalent draw and palette
walks in `Game:draw`.
The hook is guarded by `Runtime.wantsHook`, so the no-subscriber path allocates
nothing. It is a pure render predicate and may be evaluated more than once per
frame.
## Migration note for existing mods
**Nothing.** With no subscriber every state remains visible, and the existing
state-stack, event and hook behavior is unchanged.
## Parity tests
- **No-mod:** the topmost opaque state still owns drawing and palette zones,
and `Runtime.wantsHook("screen.render_visible")` stays false.
- **Mod-API:** a fixture mod registers through `mod.hooks:wrap`, hides one
opaque state and proves the state beneath draws and owns the palette while
the hidden state remains topmost and continues updating.
## Deprecation etiquette
Nothing deprecated. This is one additive hook with a `true` vanilla default.
+30 -12
View File
@@ -15,9 +15,12 @@ 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. This gives all mods
the strongest persistent battle checkpoint the current architecture can prove,
without claiming mid-animation or suspended-script support.
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
@@ -26,7 +29,7 @@ second format-1 runtime kind.
### Capability
`mod.checkpoints:inspect(game)` returns this only when an ordinary single-player
`mod.checkpoints:inspect(game)` returns this only when a supported single-player
wild or trainer battle is settled at the player command menu:
```lua
@@ -35,13 +38,15 @@ wild or trainer battle is settled at the player command menu:
The action/message queue, waits, UI, animations, HP/status presentation, and
faint processing must be settled. The player must actually control the menu.
The underlying overworld must have no running/queued script or scripted move,
and the battle must carry an engine-owned semantic continuation descriptor.
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, static-object, script-suspended, and mod-created
closure continuations remain rejected.
ghost, old-man/demo, fishing, opaque callback continuations, non-data-only
scripts, and unsupported concurrent script work remain rejected.
### Capture
@@ -107,10 +112,20 @@ trainer class/party, and optional header event; a win reapplies the same defeate
flag, event, reward, and `afterBattle` path. Reconstructed overworld input and
NPC freeze state are normalized instead of reviving the old closure.
`Commands.start_battle` is deliberately unsupported: its completion closure
mutates script context and resumes a coroutine whose program counter and Lua
stack cannot be serialized. Existing script rejection remains the correct safe
contract until a separate semantic ScriptRunner checkpoint RFC exists.
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
@@ -138,3 +153,6 @@ No-mod behavior is unchanged when checkpoints are unused.
- exactly one post-verification `checkpoint.restored` event 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.
@@ -0,0 +1,92 @@
# RFC 0006 — Selected title playthrough storage and checkpoint resume
## Status
Proposed. Engine: `SaveData.lua`, `Storage.lua`, `Checkpoint.lua`, and
`Loader.lua`. Tests: `title_playthrough_context.lua`, existing storage,
checkpoint, title, save-slot, and no-mod parity suites.
## Motivation
A tool checkpoint may be the first durable record of a new playthrough. The
engine intentionally keeps normal Pokémon SAVE independent: before the first
normal write, identity is retained by the engine-owned selected-slot mapping,
while title starts with a fresh New Game skeleton. A durable checkpoint tool may
explicitly create one ordinary progress anchor after its first checkpoint has
committed; later tool writes must remain independent. Calling ordinary active
`mod.storage` there would allocate/adopt an identity, and live
`mod.checkpoints:restore` correctly refuses title because it has no gameplay
rollback state. Generic public capabilities are required; a tool must not use
private storage paths, slot ids, or simulate the player's SAVE menu flow.
## Additive public API
### `mod.storage:selected(game)`
Available only while the engine is in a title session. Returns an opaque bound
facade or `nil, code, message`:
```lua
local selected = mod.storage:selected(game)
local context = selected:context()
local history = selected:read("history/index")
```
The facade exposes `context()`, `read(key)`, `write(key, value)`,
`list(prefix)`, and `delete(key)`. It is bound internally to the launcher-
selected existing game-version/playthrough and the calling mod id. It neither
accepts an arbitrary playthrough id nor reveals a slot id, filesystem path, or
another mod namespace. Resolution is read-only; no selected mapping means
`no_selected_playthrough`, and opening a title browser never mints an identity.
Its detached context can include only `normalSavedAt` from a matching ordinary
save, so title tools can apply their own resume policy without receiving the
canonical normal-save record.
### `mod.checkpoints:ensureNormalSave(game, checkpoint)`
Available only at a live checkpoint-safe boundary. After a tool has durably
committed the supplied current checkpoint, it may request an ordinary progress
anchor for a playthrough that has never had one. The engine validates the
checkpoint, proves it exactly matches a fresh capture of the live runtime, and
uses the normal atomic save path including `save.write` lifecycle/veto hooks.
The operation is idempotent. It returns `true, "already_exists"` without writing
when matching normal progress already exists, so later checkpoints never move
the vanilla CONTINUE target. A stale/non-current checkpoint, unsafe runtime,
write veto/failure, or failed readback returns a structured failure. A tool
should call it only after its own checkpoint and index are durable and must not
report that first checkpoint as successful if the required anchor fails.
### `mod.checkpoints:resume(game, checkpoint)`
Available only from title. It validates format, data-only structure, selected
game/playthrough identity, canonical save/content, overworld/battle runtime, and
RNG exactly as `restore` does. It then reconstructs semantic overworld or a
supported battle continuation, preserves current options, and differentially
recaptures before committing. On success it emits `checkpoint.restored` once.
Title has no live runtime rollback. A reconstruction or verification failure
therefore rebuilds a clean title session from the pre-operation title save and
RNG; it emits no success event and never rewrites normal progress. Validation
failure leaves the existing title session untouched. Stable errors include
`not_at_title`, `no_selected_playthrough`, normal checkpoint validation codes,
`resume_failed`, and `title_recovery_failed`.
## Isolation and migration
Explicit NEW GAME retains its existing fresh-identity rule. It does not reuse a
previous selected mapping and cannot see old tool history. Existing mods change
nothing: no identity, storage, title reconstruction, or event is created unless
the new methods are called. `mod.storage` remains independent durable data and
does not rewind with a checkpoint; canonical `game.save` / `mod.save` does.
## Verification
The public SDK test starts a fresh playthrough, stores tool history, creates and
readback-verifies exactly one normal anchor, proves subsequent calls do not
rewrite it, simulates title/restart, reads the selected binding without
allocating title identity, resumes an overworld checkpoint, preserves options,
differentially recaptures, and confirms a later explicit NEW GAME receives
another identity. A separate two-process disk test proves cold-start routing and
reconstruction. Existing no-mod, storage, checkpoint, battle, and title suites
prove additive parity.