diff --git a/docs/re/structures/savegame-format.md b/docs/re/structures/savegame-format.md index e792fdf..7a9ad1d 100644 --- a/docs/re/structures/savegame-format.md +++ b/docs/re/structures/savegame-format.md @@ -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` | **Stage02–08 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.