From 2da9b8a9b823af1c5b4694f038d6f245f566bd6c Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Tue, 11 Aug 2026 20:01:26 +0000 Subject: [PATCH] re: the whole GHAD block is neither stage nor difficulty -- sixteen probe saves Extends the previous refutation to a sweep. Probed with no effect on STAGE 02 or Difficulty EASY: every scalar in the GHAD block (+0, +12, +16, +20, +28, +32, +36 at 1/3/9, +40 u64, +48, +52, +56, +60, +64 raw), SHAB[0].a, and the SHAB FILL COUNT in both directions -- record 1 filled with a copy of record 0, and record 0 cleared. The "stage = filled-record count + 1" idea dies with it, and so does the reading that made SHAB a per-stage result table by that route. The panel does re-read each slot: slot 02 holds Points 4101 / Clear Ratio 5 % and displays exactly that while its neighbours show 101 / 6 %. Left: the phase string, the trailer, or the blob. Recorded caveat -- every save on disc is genuinely Stage 02 EASY, so "field not found" and "panel does not vary those two labels per slot" are not yet separated, and another probe round cannot separate them. The next move is static: find the code that formats STAGE %02d and read which offset it loads. savegame_edit.py --set now packs an int into raw_* byte fields. Co-Authored-By: Claude Opus 5 (1M context) --- docs/re/structures/savegame-format.md | 43 ++++++++++++++------------- tools/re-capture/savegame_edit.py | 6 +++- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/docs/re/structures/savegame-format.md b/docs/re/structures/savegame-format.md index 8a0382d..9b40dbc 100644 --- a/docs/re/structures/savegame-format.md +++ b/docs/re/structures/savegame-format.md @@ -298,35 +298,38 @@ What the title does with an edited save is itself a finding: on load it can now develop …"). So the blob's `4`s are authoritative state and its `2`s are not — see the economy note. -### What the probes then refuted: +36 / +52 / +56 are **not** stage or difficulty +### What the probes then refuted: the whole GHAD block is **not** stage or difficulty The three fields that all hold `2` were read as "difficulty or stage, undecidable from one save" from the day the format was parsed. Writing saves makes it -decidable, and the answer is **neither**. +decidable, and the answer is **neither** — and the sweep did not stop there. Method — [`boot_menu.sh`](../../../tools/re-capture/boot_menu.sh) boots to the title menu **without loading anything**, and LOAD GAME's slot list renders each slot's Details panel (`STAGE`, `Game Status`, `Points`, `Times Cleared`) plus a -row (date, `Difficulty`, flight time, clear ratio) straight out of that slot's -payload. Extra slots can be fabricated — copy the save directory and its -`Headers/…/gameNN.header`, patching the UTF-16BE display string and the ASCII -`gameNN` inside it — so **four probes fit in one boot**, read-only, nothing -loaded. +row (date, `Difficulty`, flight time, clear ratio). Extra slots can be fabricated +— copy the save directory and its `Headers/…/gameNN.header`, patching the +UTF-16BE display string and the ASCII `gameNN` inside it — so **five probes fit +in one boot**, read-only, nothing loaded. -Eleven candidate fields were written and read back: `+36` at 1, 3 and 9; `+52` -and `+56` at 1 and 9; `+0`, `+16`, `+32`, `+48`, `+28`, and `SHAB[0].a` — every -one of them left the panel at `STAGE 02 / Declaration of War`, `Difficulty EASY`, -`At Standby`, `Times Cleared 0`. +**Probed, all with no effect on `STAGE 02` or `Difficulty EASY`:** every scalar in +the GHAD block — `+0`, `+12`, `+16`, `+20`, `+28`, `+32`, `+36` (at 1, 3 and 9), +`+40` (u64), `+48`, `+52`, `+56`, `+60`, `+64` (raw) — plus `SHAB[0].a`, plus the +`SHAB` **fill count** in both directions (record 1 filled with a copy of record 0; +record 0 cleared). Sixteen elements. -The negative is meaningful because the panel demonstrably *does* read each -payload: slot 02 shows `Clear Ratio 5 %` against the others' `6 %`, and `Points` -tracked `+24` exactly. Two further controls: patching a slot's **header** string -to `STAGE09 HARD` changed nothing, so the display is payload-driven, not header -text; and the row's date follows the **container FILETIME**, which is why every -fabricated slot showed 18:04. +The panel genuinely re-reads each slot — the control is slot 02, which holds +`Points 4101` and `Clear Ratio 5 %` and displays exactly that while its +neighbours show `101` and `6 %`. A further control: patching a slot's **header** +string to `STAGE09 HARD` changed nothing, and the row's date follows the +**container FILETIME**, so the row is payload-formatted, not header text. -So stage and difficulty live in one of the remaining unprobed fields — `+12`, -`+20`, `+40` (u64), `+60`, `+64` (raw 4) — or in the phase string. That is the -next probe round, and it is now cheap. +**What is left**, and it is a much narrower set than when this started: the +**phase string** (`GP_BUNK`), the trailer (`"BUNK"` + `0x09150000`), or the blob. +One honest caveat on the negative: **every save on disc is genuinely Stage 02 / +EASY**, so "the field was not found" and "the panel does not vary those two +labels per slot" are not yet separated. The next move is not another probe — it +is to find the code that formats `STAGE %02d` and read which offset it loads, +which the static DB can answer directly. Editing beyond a throwaway slot is still the user's call. diff --git a/tools/re-capture/savegame_edit.py b/tools/re-capture/savegame_edit.py index 0e77236..caf458b 100755 --- a/tools/re-capture/savegame_edit.py +++ b/tools/re-capture/savegame_edit.py @@ -77,7 +77,11 @@ def main(): blob[int(k)] = int(v) elif a == "--set": k, v = args[i + 1].split("=") - parsed["ghad"][k] = int(v, 0) + # `raw_*` fields are stored as bytes; accept an int and pack it BE. + old = parsed["ghad"][k] + parsed["ghad"][k] = ( + int(v, 0).to_bytes(len(old), "big") if isinstance(old, bytes) else int(v, 0) + ) elif a == "--slot": # --slot ,=, e.g. --slot 0,0=3 where, v = args[i + 1].split("=")