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

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -76,7 +76,7 @@ photographed against this exact save (see "Naming the fields", further down).
| +0 | 0 | ❔ | | +0 | 0 | ❔ |
| +4 | **324773** | ✅ **flight time, milliseconds** — the screen shows `Flight Time 000:05:24` and 324773 ms = 5 m 24.773 s | | +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)) | | +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 | ❔ | | +16 | 1 | ❔ |
| +20 | 0 | ❔ | | +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` | | +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 | ❔ | | +32 | 79 | ❔ |
| +36 | 2 | ❌ **not** difficulty and **not** stage — refuted by probe saves (see below) | | +36 | 2 | ❌ **not** difficulty and **not** stage — refuted by probe saves (see below) |
| +40 (u64) | 2014400 | ❔ | | +40 (u64) | 2014400 | ❔ |
| +48 | 0 | ❔ (`Times Cleared: 0` is on screen, so it is one of the zero fields) | | +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 | ❌ see +36 — refuted | | +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 | | +56 | 2 | ❌ see +36 — refuted |
| +60 | 0 | ❔ | | +60 | 0 | ❔ |
| +64 (raw 4) | `09 15 00 00` | ❔ the trailer's u32 is the **same** value | | +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 current mission and the selectable set, the same way the serializer at
`0x822C00E8` was found — or **clearing a stage in game**, which also settles `0x822C00E8` was found — or **clearing a stage in game**, which also settles
`Times Cleared` and whether `SHAB[1]` fills. `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.