# Save file (`savedata`) — container ✅ exact, field meanings ❔ (2026-08-11) **Status: ✅ CONFIRMED for the container and the chunk layout** — parsed off the title's own serializer, not guessed, and verified by a byte-identical round-trip. **❔ for what most of the numbers mean**; those are marked below and none are asserted. Parser + round-trip check: [`tools/re-capture/savegame.py`](../../../tools/re-capture/savegame.py) (`savegame.py --verify` re-serializes the parse and asserts equality). ## Where it lives ``` //535107D4/00000001/game01/savedata 545 B payload //535107D4/Headers/00000001/game01.header 328 B ``` The **header** is the Xbox content header, not game data: a UTF-16BE display string (`Game01 07/23/2026 21:08 STAGE02 EASY`), the internal name `game01`, and the title id `53 51 07 D4`. It is what the save-select screen lists. `savedata` is the whole save. **545 bytes** — there is no second file, so everything the game remembers between sessions is in here. ## Container ``` 'GDHA' <146-byte header> ``` The deflated payload is 545 bytes. Two of the GDHA header words are a FILETIME and a copy of the slot-0 fields; **much of the rest is uninitialised memory** — words like `0x828F3DA8`, `0xBD4B6200`, `0x70D8FD50` are guest virtual addresses that leaked out of the struct's padding, so the header is not byte-reproducible and must not be treated as meaningful. The last four bytes of the file are the zlib stream's own Adler-32, and the same value also appears at header offset `0x8E`. ## Payload — a chunk stream written by `0x822C00E8` Every element below is read off the serializer, whose writer primitive is `0x821885A8(stream, buf, len)`: | bytes | what | source | |---|---|---| | `'GDAA'` | payload magic | `addis 0x4744 / ori 0x4141` at `0x822C00F8` | | `u32 len`, `char[len]` | **current game phase**, here `GP_BUNK` | `lwz r11,0(save)`; if `< 29`, index the GP_* name table at `0x820A5680` | | `'GHAD'` + 122 B | the progress block | `0x822BF678`, called with `save+8` | | `u32 16`, 16 × (`'SHAB'` + 5×u32) | the save-slot table | `addi r28,r0,16` / stride `addi r29,r29,20` | | `u32 4`, `"BUNK"`, `'NETA'`, `u32` | trailer | — | **The in-memory struct is written field-by-field with no packing changes, so a payload offset is also the offset in the live save object**: the GHAD block is `save+8`, the slot table is `save+136`, and the serializer's next access after the table is `lwz r11,456(save)` — exactly `136 + 16*20`. That makes this doc a map of the runtime object too, which is what a live-RAM read would need. The phase name is ✅ real: `GP_BUNK` is one of the title's screen ids, and the .pe carries the neighbouring list `GP_HANGAR`, `GP_READY_ROOM`, `GP_BUNK`, `GP_MOVIE`, `GP_OPTIONS`, `GP_MISSION_*`. (Note the `SHAB` string that also appears in the .pe is a **false hit** — it is inside the RTTI name `.?AVSCRIPT_COMMAND_PUSHABLE@SilpheedSCS@@`. The four-character tags are built as `lis`/`ori` immediate pairs, so they are not in the string pool at all.) ### The GHAD block (122 bytes, `0x822BF678`) Ten u32, one u64 (`ld r11,40(r30)`), four u32, a raw 4-byte field and a raw 54-byte blob. 10·4 + 8 + 4·4 + 4 + 54 = **122**, which is exactly what the file carries — the layout is closed, with nothing unaccounted for. | off | value in this save | reading | |---|---|---| | +0 | 0 | ❔ | | +4 | **324773** | ❔ a large per-save counter — credits/score is the obvious guess, untested | | +8 | 5 | ❔ the save-select screen shows this save at **5 %** | | +12 | 0 | ❔ | | +16 | 1 | ❔ | | +20 | 0 | ❔ | | +24 | **4101** (`0x1005`) | ❔ same value as +28 and as slot 0's field b | | +28 | **4101** (`0x1005`) | ❔ | | +32 | 79 | ❔ | | +36 | 2 | ❔ equals slot 0's field a; the header calls this save `EASY` | | +40 (u64) | 2014400 | ❔ | | +48 | 0 | ❔ | | +52 | 2 | ❔ | | +56 | 2 | ❔ | | +60 | 0 | ❔ | | +64 (raw 4) | `09 15 00 00` | ❔ the trailer's u32 is the **same** value | | +68 (raw 54) | see below | ❔ 54 one-byte entries, values only ∈ {0, 2, 4} | The 54-byte blob is the interesting one — a fixed-length array of small enumerated states, 20 of the 54 non-zero: ``` 04 02 00 00 00 02 00 00 00 04 02 00 02 00 00 00 00 00 00 00 00 04 04 00 00 00 04 02 00 00 00 02 00 04 00 00 00 00 00 04 00 00 00 00 00 04 04 04 00 00 00 00 00 00 ``` 54 matches no count we have already established (22 stages, 110 units, 126 weapons, ~61 Arsenal entries), so **do not** assume which list it indexes. ### The slot table (16 × 20 bytes) | slot | a | b | c | FILETIME | |---|---|---|---|---| | 0 | 2 | 4101 | 324773 | **2026-07-23 20:07:23** | | 1–15 | 0 | 0 | 0 | 2006-01-01 00:00:00 | **The FILETIME reading is ✅ confirmed independently**: the last two u32 of slot 0 decode as a Windows FILETIME to 2026-07-23 20:07:23 UTC, and the content header's own display string — written by the game, parsed by nobody here — says `07/23/2026 21:08` (UTC+1). The 15 unused slots all hold 2006-01-01, a plausible epoch default rather than zero, which is a second check on the same reading. Fields a/b/c of slot 0 are copies of GHAD +36, +24/+28 and +4. So the slot record is a **summary** of a save, which is what a slot-select screen needs. ## What this does and does not unlock It settles the format. It does **not** yet settle which field is stage-unlock state, so it does not yet answer the question that motivated it — whether stage progress can be reached without winning missions ([mission outcome](../mission-outcome-stage02.md)). **The naming oracle is a differential, and only one save exists** (all four copies on disk — the live one, both `content-backup-*` snapshots and the `prof2` profile — are byte-identical, md5 `142b4f432e0e…`). So the next step is to *make* a second save: change one thing in-game that the player can change cheaply, save, and diff. Developing or buying a single Arsenal item should move +4 (if it is currency) and one entry of the 54-byte blob, which would name two fields at once. **Nothing here has been written back to a save.** Editing is a separate decision and needs the user's go-ahead; the round-trip in `savegame.py` exists so that an edit *could* be made safely (parse → change one field → re-serialize → re-deflate → re-wrap), and so that a diff of two saves can be read structurally instead of as raw bytes.