re(challenge): both gate words are one ~1880-byte record, and it is not the save

Two results, one of which kills an operational hope I had been carrying.

THE RECORD IS MUCH BIGGER THAN I MEASURED. Reading 0x82175110 to the end: after
the two words, the 8-byte pair, the 184-byte memcpy and the 8 words at +200, it
copies 816 bytes at +232, 816 more at +1048, a sub-object at +1864 and a final
word at +1876. So the record spans +0..~+1880 = singleton +80..+1960, which means
word A is record +0 AND WORD B IS RECORD +1876. That retires last commit's "word
B has no known writer": there is no separate store because the whole record is
copied out, modified and assigned back as a unit (0x8216FF70 -> compare
0x822C3708, assign 0x82170650, then a worker 0x821700A8 -- note 168 decimal, not
hex, which I misread first time -- retrying a commit 0x822C33B8 up to five times).

IT IS NOT THE SAVEGAME, by two independent checks:
 - size: every real save on disk is a 276-byte container deflating to 545 bytes;
   the record is ~1880. It does not fit.
 - files: after many sessions the content tree holds only game0N/savedata and
   game0N/__thumbnail.png per slot plus three Headers/*.header. No second data
   file exists anywhere under the title id.
Also negative: the savegame object is *(*(this+4)) + 304, and the singleton's
holder address 0x828F48B0 is referenced NOWHERE outside the accessor, so this+4
is a different holder -- the save block is not a window into this record.

=> Hand-editing a save cannot unlock the challenge missions. The earlier
savegame-editing win does not extend here.

WHAT WOULD WORK. The singleton is a static object at 0x828F4070 (0x8216F650:
addis 0x828F + addi 16496), so the gate words sit at fixed guest addresses with
no scanning: word A = 0x828F40C0, word B = 0x828F4814. Canary maps guest RAM into
/dev/shm and the project already reads it live, so writing 0xFFFF / 0x3F there
while the title sits on a menu should open all six challenge missions without
playing the campaign -- the route to the last 42 EX units. Untested; needs a run.
This commit is contained in:
2026-08-13 20:06:11 +00:00
parent 20299c05a1
commit f490fecefb

View File

@@ -2,8 +2,9 @@
**Status:** ✅ for the static structure (stage set, GamePart ids, the config-section
switch) and for the **unlock mechanism** (a bit test against a *cleared-stage* mask,
whose sole writer is `GamePart_StageClear`); 🟡 for the crash mechanism, for word B's
writer, and for which mission consumes which bit.
whose sole writer is `GamePart_StageClear`, in a ~1 880-byte progress record that is
**not** the savegame); 🟡 for the crash mechanism, for word B's writer, and for which
mission consumes which bit.
**Method:** static only — `.pe` string/pointer analysis + DuckDB disassembly + disc
records. No emulator run, no gamepad input.
**Evidence:** [`captures/challenge-map.txt`](captures/challenge-map.txt),
@@ -232,18 +233,58 @@ via `hash::TOC_NAME_SCHEMES`.
### 5.4 Where the mask lives, and one lead refuted
Word A is **word 0 of a 232-byte progress struct at singleton `+80`** (`0x82175110`
copies it out: two words, an 8-byte pair, a 184-byte `memcpy`, then 8 words from
`+200`). `0x8216FF70` compares, assigns, and then spawns a worker (`0x82170168`) under
a lock — i.e. the struct is **persisted asynchronously**. `GamePart_Debriefing` uses
the same get/modify/set on it to accumulate saturating career counters
(`+200``+224`, the last a 64-bit total), which are exactly the quantities the
achievement requirements test.
### 5.5 Both gate words are one record — and it is **not** the savegame ✅
So a **hand-written progress record with the stage bits set would unlock the challenge
missions**, and the last 42 units of the Route-B harvest become one run instead of an
unreachable menu — *if* this struct is what the savegame carries. That is the open
question.
`0x82175110` copies the record at singleton `+80` in full: two words, an 8-byte pair, a
184-byte `memcpy`, 8 words from `+200`, then **816 bytes at `+232`, 816 more at
`+1048`**, a sub-object at `+1864`, and a final word at **`+1876`**. So the record runs
`+0 … ~+1880`, i.e. singleton `+80 … +1960` — which means
- **word A** is record `+0` (singleton `+80`), and
- **word B is record `+1876`** (singleton `+1956`).
That answers §5.3's "word B has no known writer": there is no separate `stw` because
the whole record is copied out, modified and assigned back as a unit
(`0x8216FF70` → compare `0x822C3708`, assign `0x82170650`, then spawn a worker
`0x821700A8` under a lock which retries a commit `0x822C33B8` up to five times).
`GamePart_Debriefing` uses the same get/modify/set to accumulate saturating career
counters at `+200``+224` (the last 64-bit) — the quantities the achievement
requirements test.
**And the record is not what `savedata` holds.** Two independent checks:
- **Size.** Every real save on disk is a **276-byte** container deflating to a
**545-byte** payload. The progress record is **~1 880 bytes**. It does not fit.
- **Files.** After many sessions the game's content tree holds only
`game0N/savedata` + `game0N/__thumbnail.png` per slot and the three
`Headers/*.header` — no second data file anywhere.
Also checked and negative: the savegame object is `*(*(this+4)) + 304`, and the
singleton's holder address (`0x828F48B0`) is referenced **nowhere but inside the
accessor itself**, so `this+4` is a different holder — the save block is not a window
into this record.
⚠️ **So hand-editing a save cannot unlock the challenge missions.** That kills the
operational hope this section previously carried; the earlier savegame-editing win
does not extend here.
### 5.6 What *would* work — static addresses for a live write
The singleton is a **static object at `0x828F4070`** (`0x8216F650`:
`addis 0x828F` + `addi …, 16496`), with the holder at `0x828F48B0` pointing at it. So
the two gate words are at fixed guest addresses, no scanning required:
| word | guest VA | meaning |
|---|---|---|
| A | **`0x828F40C0`** | cleared stages, bit = stage id (`< 24`) |
| B | **`0x828F4814`** | cleared stages, bit = stage id 24 (challenge 2429) |
Canary maps guest RAM into `/dev/shm`, which the project already reads live
([`tools/re-capture/gmem.py`](../../tools/re-capture/gmem.py)). Writing
`0xFFFF` into word A and `0x3F` into word B while the title sits on a menu should
open all six challenge missions **without playing the campaign** — and that is the
route to the last 42 `*_EX4`/`*_EX5` units. Untested: it needs a run, and the run
needs gamepad input to reach the menu.
**The obvious lead is dead.** The three `stw`s to `+1956` in `0x822AF278` /
`sub_822C8748` looked promising because they sit in the save serializer's code region