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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
can now develop …"). So the blob's `4`s are authoritative state and its `2`s are
|
||||||
not — see the economy note.
|
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
|
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
|
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
|
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
|
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
|
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
|
row (date, `Difficulty`, flight time, clear ratio). Extra slots can be fabricated
|
||||||
payload. Extra slots can be fabricated — copy the save directory and its
|
— copy the save directory and its `Headers/…/gameNN.header`, patching the
|
||||||
`Headers/…/gameNN.header`, patching the UTF-16BE display string and the ASCII
|
UTF-16BE display string and the ASCII `gameNN` inside it — so **five probes fit
|
||||||
`gameNN` inside it — so **four probes fit in one boot**, read-only, nothing
|
in one boot**, read-only, nothing loaded.
|
||||||
loaded.
|
|
||||||
|
|
||||||
Eleven candidate fields were written and read back: `+36` at 1, 3 and 9; `+52`
|
**Probed, all with no effect on `STAGE 02` or `Difficulty EASY`:** every scalar in
|
||||||
and `+56` at 1 and 9; `+0`, `+16`, `+32`, `+48`, `+28`, and `SHAB[0].a` — every
|
the GHAD block — `+0`, `+12`, `+16`, `+20`, `+28`, `+32`, `+36` (at 1, 3 and 9),
|
||||||
one of them left the panel at `STAGE 02 / Declaration of War`, `Difficulty EASY`,
|
`+40` (u64), `+48`, `+52`, `+56`, `+60`, `+64` (raw) — plus `SHAB[0].a`, plus the
|
||||||
`At Standby`, `Times Cleared 0`.
|
`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
|
The panel genuinely re-reads each slot — the control is slot 02, which holds
|
||||||
payload: slot 02 shows `Clear Ratio 5 %` against the others' `6 %`, and `Points`
|
`Points 4101` and `Clear Ratio 5 %` and displays exactly that while its
|
||||||
tracked `+24` exactly. Two further controls: patching a slot's **header** string
|
neighbours show `101` and `6 %`. A further control: patching a slot's **header**
|
||||||
to `STAGE09 HARD` changed nothing, so the display is payload-driven, not header
|
string to `STAGE09 HARD` changed nothing, and the row's date follows the
|
||||||
text; and the row's date follows the **container FILETIME**, which is why every
|
**container FILETIME**, so the row is payload-formatted, not header text.
|
||||||
fabricated slot showed 18:04.
|
|
||||||
|
|
||||||
So stage and difficulty live in one of the remaining unprobed fields — `+12`,
|
**What is left**, and it is a much narrower set than when this started: the
|
||||||
`+20`, `+40` (u64), `+60`, `+64` (raw 4) — or in the phase string. That is the
|
**phase string** (`GP_BUNK`), the trailer (`"BUNK"` + `0x09150000`), or the blob.
|
||||||
next probe round, and it is now cheap.
|
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.
|
Editing beyond a throwaway slot is still the user's call.
|
||||||
|
|||||||
@@ -77,7 +77,11 @@ def main():
|
|||||||
blob[int(k)] = int(v)
|
blob[int(k)] = int(v)
|
||||||
elif a == "--set":
|
elif a == "--set":
|
||||||
k, v = args[i + 1].split("=")
|
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":
|
elif a == "--slot":
|
||||||
# --slot <record>,<field>=<value>, e.g. --slot 0,0=3
|
# --slot <record>,<field>=<value>, e.g. --slot 0,0=3
|
||||||
where, v = args[i + 1].split("=")
|
where, v = args[i + 1].split("=")
|
||||||
|
|||||||
Reference in New Issue
Block a user