Files
Sylpheed/docs/re/stage-drift-is-navigation-not-save.md
Sylpheed RE agent 67001f04fb re: pin the GHAD field base -- it is tag+4, not the tag+8 I guessed
Closes the caveat I flagged in c9dc8dd.  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 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 115 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.