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) <noreply@anthropic.com>
This commit is contained in:
2026-08-11 20:24:47 +00:00
parent bc42c0970e
commit 98b3cf56ac

View File

@@ -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, is to find the code that formats `STAGE %02d` and read which offset it loads,
which the static DB can answer directly. 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 308340, 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`) ### What the screen itself is built from (static, `.rdata`)
The LOAD/SAVE screen's config **key list is compiled into the executable**, as a The LOAD/SAVE screen's config **key list is compiled into the executable**, as a