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:
BIN
docs/re/captures/savegame-stage05-probe-details.png
Normal file
BIN
docs/re/captures/savegame-stage05-probe-details.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 906 KiB |
BIN
docs/re/captures/savegame-stage05-probe-inflight.png
Normal file
BIN
docs/re/captures/savegame-stage05-probe-inflight.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user