diff --git a/docs/re/structures/savegame-format.md b/docs/re/structures/savegame-format.md index f1757f5d..9c0c4345 100644 --- a/docs/re/structures/savegame-format.md +++ b/docs/re/structures/savegame-format.md @@ -573,3 +573,61 @@ visiting missions. Still open: `+36` and `+56` (both 2, one of which is likely `Difficulty`, since the header carries no difficulty field and the display string does), and the `+32 = 79` / `+40 = 2014400` / `+64 = 09 15 00 00` fields. + +## ✅ The save embeds live guest pointers — which is why diffing saves misleads + +**2026-08-26.** Comparing the two committed saves that represent the **same game +state** (`savedata-stage02-5pct.bin` and `savedata-game02-samestate.bin`, both +276 bytes), only **12 of 69 `u32` words differ** — and most of those are not +data. + +**Nine carry guest addresses**: values with a `0xBC…`, `0xBD…` or `0x70…` +prefix, the same regions the runtime work uses (`0xbdb59668` for the objective +counter, `0x820af030` for entity records). Three of them differ by **exactly +`0x101080`** and two by exactly `0x300000` — a shared constant offset is what a +relocated heap does to a pointer, and it is not what data does. + + +0x04C 0xBD4B6200 -> 0xBD3B5180 delta 0x101080 + +0x064 0xBD4B6330 -> 0xBD3B52B0 delta 0x101080 + +0x06C 0xBD4B6338 -> 0xBD3B52B8 delta 0x101080 + +0x070 0x70D8FD50 -> 0x70A8FD50 delta 0x300000 + +0x080 0x70D8FD60 -> 0x70A8FD60 delta 0x300000 + +The split is sharp across the whole file: + +| | words | differ between same-state saves | +|---|---|---| +| pointer-shaped (`0x70–0x8F`, `0xB0–0xBF`) | 18 of 69 (26 %) | **8 (44 %)** | +| everything else | 51 | **4 (8 %)** | + +So **a quarter of this structure is captured heap addresses**, and they change +run to run regardless of what the player did. This is the concrete form of the +page's existing "much of the rest is uninitialised memory" — now demonstrated by +the constant deltas rather than inferred from the values looking odd. + +**Practical consequence**: a byte diff between two saves is dominated by pointer +churn. Only the 51 non-pointer words carry state worth attributing, and of those +just four move between identical states: + + +0x008 0x01DD1ADF -> 0x01DD2956 high half `0x01DD` stable, low half moves + +0x00C 0x144F6CDA -> 0x120EAB37 + +0x038 0xDD0FF734 -> 0xAF0C96D7 + +0x084 0x006800C0 -> 0x006E3A40 + +🟡 `+0x008` looks like a counter or timestamp — a stable high half with a moving +low half is that shape. The other three look unstructured; a checksum would fit +but nothing here tests it. + +### ⚠️ A three-way diff of these files is invalid + +My first pass diffed all three saves byte-wise and reported **135 of 276 bytes +(48.9 %) differing**. That number is wrong. `savedata-game03-developed-mg1.bin` +is **280 bytes**, not 276, and the longest common suffix with the others is +**zero bytes** — so the files are not a common layout with an insertion, and +comparing offset *i* across them is meaningless past the first divergence at +`0x00A`. The valid comparison is the equal-length pair, and it gives **30 bytes +in 12 words**, not 135. + +Worth stating because the inflated figure looked plausible: half the file +changing between saves is exactly what one would expect if saves were dense +state, and nothing about it invites a second look.