From 98b3cf56ac4f11631859a2b1738dd5a6a20bbbeb Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Tue, 11 Aug 2026 20:24:47 +0000 Subject: [PATCH] re: name the save read path, and locate the save state at game_object+304 Disassembling around the serializer names its mirror and the whole read side: deserializer 0x822C0380 (reads GDAA + phase string), GHAD reader 0x822BF858 (called with save+8, pairs with the writer 0x822BF678), SHAB reader 0x822BFD60 (save+136, count 16). The GHAD reader is a plain unrolled byte copy with no clamping or validation anywhere, which is why hand-written field values survive into the object exactly as written. The deserializer's only caller builds its argument as lwz r11,4(r30) / lwz r11,0(r11) / addi r4,r11,304, so the save state lives at +304 of a game object and every field now has a fixed absolute offset there -- Points at +336, clear ratio at +320, the develop blob at +380, the SHAB table at +440. That is what a live-RAM read of the save state needs. Recorded as a negative too: searching those absolute offsets does NOT find the panel's populate code. lwz rX,336(rY) has 193 matches, and the windows holding several right offsets at once are a vtable thunk table (slots 308-340) and a float-parameter block. The stage/difficulty fields need the screen-side path. Co-Authored-By: Claude Opus 5 (1M context) --- docs/re/structures/savegame-format.md | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/re/structures/savegame-format.md b/docs/re/structures/savegame-format.md index 18c6a7c..1d99da8 100644 --- a/docs/re/structures/savegame-format.md +++ b/docs/re/structures/savegame-format.md @@ -332,6 +332,41 @@ labels per slot" are not yet separated. The next move is not another probe — i is to find the code that formats `STAGE %02d` and read which offset it loads, which the static DB can answer directly. +### The read side, and where the save state lives in memory + +The serializer at `0x822C00E8` has a mirror, found by disassembling around it — +and the call graph names the whole read path: + +| function | role | pairs with | +|---|---|---| +| `0x822C0380` | **deserializer** — reads `GDAA`, the phase string, then calls the two below | `0x822C00E8` | +| `0x822BF858` | **GHAD reader**, called with `save+8` | `0x822BF678` | +| `0x822BFD60` | **`SHAB` table reader**, called with `save+136`, count 16 | — | + +The GHAD reader is a plain byte-for-byte copy — unrolled per field, no clamping +or validation anywhere in it, which is why a hand-written value survives into the +object exactly as written (and why the develop-blob probes worked). + +**The save state lives at offset `+304` of a game object.** The deserializer's +only caller sets it up as `lwz r11,4(r30)` → `lwz r11,0(r11)` → `addi r4,r11,304` +(`0x82290594`–`0x8229059c`). Combined with the layout above, every field has a +fixed absolute offset in that object, which is what a live-RAM read needs: + +``` +save base = obj + 304 GHAD block = obj + 312 +flight time = obj + 316 clear ratio = obj + 320 +Points = obj + 336 develop blob = obj + 380 +SHAB table = obj + 440 (obj + 304 + 136) +``` + +**What this did not do, and why:** finding the panel's populate code by searching +for those absolute offsets does not work — the address space is dense enough that +`lwz rX, 336(rY)` has 193 matches, and the windows that contain several of the +right offsets at once are false positives: one is a **vtable thunk table** whose +slots happen to sit at 308–340, another is a float-parameter block. Naming the +stage and difficulty fields needs the save-screen's populate path reached from +the screen side (its config record and sprite-key lookup), not from the offsets. + ### What the screen itself is built from (static, `.rdata`) The LOAD/SAVE screen's config **key list is compiled into the executable**, as a