re: saves can be written back, which settles what the develop blob's 4s mean and refutes the tail

The container's derived fields turned out to be reproducible -- length+10 at
+0x30, payload length at +0x8c, adler32(payload) at +0x8e, everything else
copied -- and savegame_edit.py re-wraps a real save BYTE-IDENTICALLY, which is
the check that those three are the only ones. A hand-written save then loaded.

That replaced a blocked experiment (the tail question needed a mission payout,
and none of the currently developable items even sit in the disputed range) with
a direct one: write the blob, read the Arsenal.

  - controls: 4 at index 9 -> STILETTO BG1 Developed, 21 -> FALCON 9AM
    Developed. A hand-written 4 reaches the screen.
  - tail: 4 at 33 and 45 left their rows dashed (both on screen, not below the
    fold), and 38 left TOMAHAWK ALPHA RAIL GUN at "0 P" -- not owned. So the
    tail is not the weapon.tbl order continued.
  - clearing the real save's {22,26,39,46,47} cost the Tomahawk its Developed
    status, which puts its flag in that set (39 positionally) -- but a uniform
    +1 fails for SPECIAL, so no shift is asserted. Indices >=32 stay marked.

Two behaviours fell out. The title RE-DERIVES developable state on load and
announces it ("You can now develop Broad Sword ..."), so only the 4s are stored
state and a written 2 is pointless. And a no-cost item is bought for 0 P rather
than granted -- TOMAHAWK at "0 P" is what unowned looks like -- which is the
actual reason items read Developed in a save where nothing was spent.

Slot 03 was restored from its archived original (md5 verified); slots 01/02 were
never touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-11 18:10:28 +00:00
parent f88f4f6847
commit f5f95be0a1
4 changed files with 166 additions and 11 deletions

View File

@@ -268,8 +268,32 @@ it would settle:
is what maps blob indices to items — but it needs Points, and the balance is
now 101 P, so it waits on a mission payout.
**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.
## Writing a save back — the container's derived fields
**A hand-written save loads.** Done 2026-08-11 in the sandbox, into our own
throwaway slot 03 (the user's slots 01/02 were not touched and the original of 03
is archived in `captures/`), to settle a question that otherwise needed story
progress — see [the Arsenal develop economy](../arsenal-develop-economy.md).
`savegame.py` round-trips the *payload*; the container needs three more fields,
all functions of the payload, found by diffing two saves whose payloads differed:
| header off | value |
|---|---|
| `+0x30` u32 | `len(deflate stream) + 10` |
| `+0x8c` u16 | `len(payload)` — 545 in every save seen |
| `+0x8e` u32 | `adler32(payload)` — the zlib trailer, duplicated into the header |
Everything else is copied from the donor. That is safe precisely because of the
earlier same-state result: the words that move between two saves of one state are
the FILETIME and uninitialised guest-pointer padding, so nothing else is
validated. [`tools/re-capture/savegame_edit.py`](../../../tools/re-capture/savegame_edit.py)
implements it and **re-wraps a real save byte-identically**, which is the check
that the three fields above are the only derived ones.
What the title does with an edited save is itself a finding: on load it
**re-derives** which items are *developable* and announces the difference ("You
can now develop …"). So the blob's `4`s are authoritative state and its `2`s are
not — see the economy note.
Editing beyond a throwaway slot is still the user's call.