re(savegame): GHAD +52 is the stage number — a hand-edited save plays Stage 05

Static analysis of the header builder (0x822870b4..0x82287128) shows the GDHA
container header carries a SUMMARY of the progress block, and the screens read
that: hdr+0x14 <- GHAD+52, hdr+0x18 <- GHAD+48, hdr+0x1c <- +24 (Points),
hdr+0x20 <- +4 (flight), hdr+0x24 <- the computed clear ratio, hdr+0x28 <- +12.
savegame_edit.py copies the donor header verbatim, so payload-only probes left a
stale summary — which is why +52 looked refuted.

Patching both (hdr[0x14] = 5, ghad[+52] = 5) makes the title read
"STAGE 05 - Star System Escape", load it, brief the Gallia Asteroid Group, and fly
an asteroid-field mission with a different objective and roster. So +52 is the
stage number, 1-based, and one u32 chooses the mission.

Also corrected in the GHAD table: +48 is the Game Status enum (0 = At Standby,
matching the screen's STATE_STAND_BY/STAGE_CLEAR/GAME_CLEAR list) and +12 is Times
Cleared, both via their header mirrors.

This unlocks runtime capture in containers other than Stage_S02 (the 85 XBG7
misses, box identity, ship-placement generalisation) and per-stage unit
definitions. Slot 01 was backed up and restored byte-identically.

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:46:08 +00:00
parent 0cf9220d24
commit 9fd5dcd37d
3 changed files with 56 additions and 3 deletions

View File

@@ -76,7 +76,7 @@ photographed against this exact save (see "Naming the fields", further down).
| +0 | 0 | ❔ |
| +4 | **324773** | ✅ **flight time, milliseconds** — the screen shows `Flight Time 000:05:24` and 324773 ms = 5 m 24.773 s |
| +8 | 5 | ✅ **clear ratio, percent** — the screen shows `Clear Ratio 5 %`. **It is not a stage counter**: developing one Arsenal weapon stepped it to 6 (see [the develop differential](#the-develop-differential-one-weapon-three-fields)) |
| +12 | 0 | |
| +12 | 0 | 🟡 **Times Cleared** — mirrored to header `+0x28`, and the panel prints `Times Cleared: 0` |
| +16 | 1 | ❔ |
| +20 | 0 | ❔ |
| +24 | **4101** (`0x1005`) | ✅ **Points — the spendable balance.** Named from the Details panel, and **separated from +28** by the develop differential: spending 4000 P moved *only* this field (4101 → 101) and the panel then read `Points 101 P` |
@@ -84,8 +84,8 @@ photographed against this exact save (see "Naming the fields", further down).
| +32 | 79 | ❔ |
| +36 | 2 | ❌ **not** difficulty and **not** stage — refuted by probe saves (see below) |
| +40 (u64) | 2014400 | ❔ |
| +48 | 0 | ❔ (`Times Cleared: 0` is on screen, so it is one of the zero fields) |
| +52 | 2 | ❌ see +36 — refuted |
| +48 | 0 | 🟡 **Game Status enum** — mirrored to header `+0x18`; `0` renders `At Standby`, matching the screen's own `STATE_STAND_BY / STATE_STAGE_CLEAR / STATE_GAME_CLEAR` list |
| +52 | 2 | **STAGE NUMBER, 1-based** — set it to 5 (with the header mirror, below) and the game reads `STAGE 05 — Star System Escape`, loads it, and **flies Stage 05** |
| +56 | 2 | ❌ see +36 — refuted |
| +60 | 0 | ❔ |
| +64 (raw 4) | `09 15 00 00` | ❔ the trailer's u32 is the **same** value |
@@ -520,3 +520,56 @@ have no oracle that a probe can read. The two routes that do have one are
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.
## ✅ SOLVED: `+52` is the stage, and the save picks the mission (2026-08-13)
**One `u32` chooses which of the 16 story stages the game plays.** Set GHAD `+52`
(and its header mirror, below) to 5 and the title reads
`STAGE 05 — Star System Escape`, loads it, briefs the **Gallia Asteroid Group**,
and takes off into an asteroid field with a different objective, roster and pilot
chatter — from a save whose only edited bytes are that field and its mirror
([details panel](../captures/savegame-stage05-probe-details.png) ·
[in flight](../captures/savegame-stage05-probe-inflight.png)).
### Why the earlier probes said "not the stage"
The header builder at `0x822870b4…0x82287128` copies a **summary of the progress
block into the GDHA container header**, and the screens read *that*:
| header | ← progress block (GHAD offset) | in this save |
|---|---|---|
| `+0x10` | `+0` | 0 |
| `+0x14` | **`+52`** | 2 → the panel's `STAGE 02` |
| `+0x18` | `+48` | 0 → `Game Status: At Standby` |
| `+0x1c` | `+24` | 4101 → `Points` |
| `+0x20` | `+4` | 324773 → `Flight Time` |
| `+0x24` | *computed* (`0x822842B0`, also cached to `+8`) | 5 → `Clear Ratio` |
| `+0x28` | `+12` | 0 → `Times Cleared` |
`savegame_edit.py` copies the donor header verbatim, so a payload-only edit leaves
that summary stale and the panel keeps showing the **old** stage — which is exactly
what made `+52` look refuted. **Patch both** (`hdr[0x14] = stage` and
`ghad[+52] = stage`) and the whole chain follows. The payload writer
`0x822BF678` walks the block field-by-field from `save+8`, so payload offset ==
progress-block offset; the block itself sits at **game-state `+312`** (Points at
`+336` = `312+24`, flight at `+316`), which is how the header mapping was read off
the disassembly rather than guessed.
The content header's **display string** (`Game01 07/23/2026 21:08 STAGE02 EASY`)
is separate and was *not* patched, so the slot row still reads `STAGE02` while the
Details panel reads `STAGE 05` — harmless, and a useful reminder that the row and
the panel have different sources.
### What this unlocks
Any story stage is now reachable without playing to it: write the probe into
slot 01 (back it up first — restore verified byte-identical here) and the existing
`tools/re-capture/launch_mission.sh` route loads it and takes off. That opens
**runtime captures in containers other than `Stage_S02`** — the remaining 85 XBG7
misses, the 24-vertex box identity, ship-placement generalisation to other classes
— and **per-stage unit definitions** for the Route-B coverage that grows by
visiting missions.
Still open: `+36` and `+56` (both 2, one of which is likely `Difficulty`, since the
header carries no difficulty field and the display string does), and the
`+32 = 79` / `+40 = 2014400` / `+64 = 09 15 00 00` fields.