re(savegame): stage-unlock probes are negative, and the Details panel is not a payload oracle

Four probes against slot 01 (the save the title-menu screens read), each backed up
and restored byte-identically:

- SHAB[1..7] filled -> MISSION SELECT still locks Stage02-08, cursor will not
  leave Stage01. The per-stage record table is not the unlock gate; the earlier
  SHAB[1] attempt used a throwaway slot and could not show this.
- GHAD +36/+52/+56 = 5 -> still locked, panel still STAGE 02 / EASY.
- GHAD +16 = 4 -> panel still STAGE 02, so it is not a 0-based stage index.
- live-RAM writes into the loaded GDHA header -> panel unchanged.

The useful part is why the panel cannot answer this: guest RAM at that screen holds
NO payload bytes (neither the 4101/4101/79 triple nor the 54-byte develop blob),
but it does hold the save's GDHA container header with a summary copy of Points,
flight time, clear ratio and a FILETIME. savegame_edit.py copies the donor header
verbatim, so a payload edit leaves that summary stale — "the panel did not change"
therefore cannot separate "not the stage field" from "the panel never reads the
payload". Future stage/difficulty probes must patch the header too, or be judged by
what the game does on load.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NptfmpjdpNCKEez6d2xvA9
This commit is contained in:
2026-08-13 09:18:46 +00:00
parent 0776beb6f4
commit 0cf9220d24

View File

@@ -466,3 +466,57 @@ strings are reached through a config lookup, not an immediate, so there is no
disassembling the save-screen's populate path, not another grep.
Editing beyond a throwaway slot is still the user's call.
## Stage unlock: four probes, all negative — and a method correction (2026-08-13)
The motivating question ("can stage progress be reached without winning
missions?") got its first tests **against slot 01**, the save the title-menu
screens actually read. Slot 01 was backed up, edited, and restored
byte-identically after each run (md5 `142b4f43…`, verified twice).
| probe (in slot 01) | oracle | result |
|---|---|---|
| `SHAB[1..7]` filled with record 0's shape | `EXTRAS → MISSION SELECT` | **Stage0208 stay greyed out**; the cursor will not leave Stage01 (two d-pad steps, no movement) |
| GHAD `+36 = +52 = +56 = 5` | MISSION SELECT + LOAD GAME Details for slot 01 | **still locked**, panel **still `STAGE 02`/`EASY`** |
| GHAD `+16 = 4` (a 0-based stage index would read `STAGE 05`) | LOAD GAME Details for slot 01 | **still `STAGE 02`** |
| live-RAM writes into the loaded container header (Points, and its `+0x14`) | LOAD GAME Details, re-rendered by moving the cursor off and back | **no change** |
So the per-stage record table is **not** the unlock gate — that is now tested on
the save the screens read, where the earlier `SHAB[1]` attempt was written to a
throwaway slot and could prove nothing. And `+16` joins `+36/+52/+56` as **not**
the displayed stage; the earlier refutation of those three stands, now confirmed
against slot 01 itself.
**The method correction, which is the useful part.** Searching guest RAM at the
LOAD GAME screen finds **no payload bytes at all** — neither the `4101/4101/79`
triple nor the 54-byte develop blob is anywhere in the guest address space — but
it does find the save's **`GDHA` container header**, loaded verbatim, carrying a
summary copy:
```
+0x00 'GDHA' +0x04 136 +0x08 FILETIME (2026-07-23 20:07:23)
+0x14 2 +0x1c 4101 (Points) +0x20 324773 (flight ms) +0x24 5 (clear ratio)
```
Two consequences:
1. **The Details panel is not a payload oracle.** The numbers it shows exist in
the *header* too, and `savegame_edit.py` copies the donor header verbatim — so a
payload edit leaves a stale summary beside it, and "the panel did not change"
cannot distinguish *"that field is not the stage"* from *"the panel never reads
the payload"*. Any future stage/difficulty probe must patch the header copy as
well, or be judged by what the game **does** on load rather than by the panel.
(This does not disturb the Points/flight-time/clear-ratio namings: those values
sit at both places and the two agree.)
2. The panel is **not** live-editable either: writing the header copy in
`/dev/shm` and forcing a re-render changed nothing, so the strings are formatted
before that copy is consulted again.
**Where this leaves the question.** Field guessing is now measurably the wrong
tool: three of the four candidate fields are eliminated and the remaining ones
(`+0`, `+12`, `+20`, `+32 = 79`, `+40 = 2014400`, `+48`, `+60`, `+64 = 09 15 00 00`)
have no oracle that a probe can read. The two routes that do have one are
**static analysis of the loader** — find what the title reads when it decides the
current mission and the selectable set, the same way the serializer at
`0x822C00E8` was found — or **clearing a stage in game**, which also settles
`Times Cleared` and whether `SHAB[1]` fills.