Files
Sylpheed/docs/re/stage-drift-is-navigation-not-save.md
Sylpheed RE agent f2c54eeec2 re: pin the GHAD field base -- it is tag+4, not the tag+8 I guessed
Closes the caveat I flagged in 4ae63cb.  savegame.rs documents the chunk stream
(read off the title's own serializer at 0x822C00E8) as GDAA / phase string /
'GHAD' + 122 bytes / SHAB table, and FieldSpec.offset is "offset within the
122-byte GHAD block" -- so the base is the byte after the tag, tag+4.

Reading there yields three fields, two corroborated independently elsewhere in the
corpus: Stage +52 = 2, Points +24 = 4101 (weapon-datasheet-runtime.md's "4101 P"),
FlightTime +4 = 324773 (the 05:24.77 Stage-01 best time).  tag+8 reproduced only
the stage.  The payload's phase string reads GP_BUNK.

Left open: the .header mirror does not reproduce -- savegame.rs lists Stage at
header offset 0x14 but a BE u32 there reads 2097200 (00 20 00 30), which looks
like UTF-16 text.  Not chased; the payload reading does not depend on it.
2026-08-26 22:39:02 +00:00

5.3 KiB
Raw Permalink Blame History

✅ The stage drift is my NAVIGATION, not the save — slot 01 is deterministically Stage 02

Settled 2026-08-26, statically — no emulator needed.

❌ Refuted: "slot 01 is the auto-save, so the restored mission moves between runs"

The previous note blamed save drift for one run loading the S01 training area (with UN_S01_Asteroid_cmesh_*) and another loading a Stage-02-style escort roster. The file says otherwise:

2026-08-23 16:37    278  .../535107D4/00000001/game01/savedata
2026-08-26 22:18  26596  .../FFFE07D1/.../535107D4.gpd

savedata has not been written since 23 August. Every run today left it untouched; only the .gpd profile/achievement files moved. And there is exactly one save (game01), so a wandering save-list cursor cannot explain it either.

✅ What the save actually holds: Stage 02

The save is GDHA + a zlib payload at 0x92 (545 bytes decompressed), carrying GDAA, GHAD, NETA, 2 × BUNK and the 16-record SHAB table.

rec  0 (stage  1): (2, 4101, 324773, ...)
rec  1 (stage  2): (0, 0, 0, ...)
...
rec 15 (stage 16): (0, 0, 0, ...)

Only record 0 carries clear data; records 1–15 have zeros in all three result fields. Three independent readings agree on what that means:

  1. 324773 ms = 05:24.77, which SESSION-2026-08-11.md documents as exactly what MISSION SELECT displays as the Stage 01 best time.
  2. 4101 matches structures/weapon-datasheet-runtime.md, whose capture session is recorded as "Stage 02, 'At Standby', 5 % clear, 4101 P" — the same save, already described in the corpus.
  3. Reading the GHAD field block at tag+4 gives +52 = 2, the documented 1-based Stage field.

So LOAD GAME → slot 01 restores Stage 02 every time. It is deterministic.

✅ The base is pinned — and it was not tag+8

The caveat here originally said brute force gave seven candidate offsets and that I had picked tag+8 because it agreed with the other readings rather than because the search isolated it. The authoritative parser settles it: crates/sylpheed-formats/src/savegame.rs documents the chunk stream, read off the title's own serializer at 0x822C00E8, as

'GDAA'                            payload magic
u32 len, char[len]                current game phase, e.g. "GP_BUNK"
'GHAD' + 122 bytes                the progress block
u32 16, 16 x ('SHAB' + 5 x u32)   the per-stage record table

and FieldSpec.offset is "Offset within the 122-byte GHAD block" — so the base is tag+4, where the block begins. Reading there gives three fields at once, two of which match values documented independently elsewhere in the corpus:

field GHAD offset value corroboration
Stage +52 2 Stage 02 = the Acropolis escort mission
Points +24 4101 weapon-datasheet-runtime.md: "4101 P"
FlightTime +4 324773 the 05:24.77 Stage-01 best time

Two independently-documented values landing at once is what a correct base looks like; tag+8 reproduced only the stage. The phase string in the same payload reads GP_BUNK — the save sits in the bunk between missions.

❔ The .header mirror did not reproduce: savegame.rs lists Stage at header offset 0x14, but reading a big-endian u32 there gives 2097200 (00 20 00 30), which looks like UTF-16 text rather than a mirrored word. Not chased — the payload reading is confirmed and does not depend on it.

❌ And "stage = filled SHAB count + 1" must not be used: all 16 records are present, and the corpus already lists that rule as refuted. Presence is not filledness.

✅ So the S01 asteroid run was never the save

Stage 02 is the Acropolis escort mission — which is precisely the roster the most recent run produced (UN_f101_TCAF_Acropolis, UN_bf001_TCAF_SchlosBase, cruisers, frigates). The odd run out is the earlier one, whose briefing read "Glasner Training Area" — a tutorial, not a story stage, and structures/unit-struct-runtime.md confirms tutorials are separately reachable ("all six tutorials and Stage 02").

The cause is in my own driving: that run's navigation log shows the menu probe reading other repeatedly and the dpad down + Ⓐ being issued from a state that was not confirmed to be the main menu, so the selection landed on a tutorial entry instead of LOAD GAME. The nav taps a fixed sequence and never verifies what it selected.

Consequence — the cheap fix

Runtime rosters are reproducible after all, provided the route is verified. Two one-line guards, neither of which needs new tooling:

  • Confirm screen_id.py reads menu immediately before the dpad down, not merely at some point earlier in the loop.
  • After reaching flight, assert the definition table contains UN_f101_TCAF_Acropolis (Stage 02's signature) before trusting any comparison against an earlier run.

❔ Not settled: I did not re-run the emulator to demonstrate the tutorial mis-selection directly. The evidence here is the save file plus the earlier run's own screen log and briefing title, which is strong but circumstantial about which menu entry was hit.