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:
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
**Status:** ✅ for the static structure (stage set, GamePart ids, the config-section
|
**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,
|
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
|
whose sole writer is `GamePart_StageClear`, in a ~1 880-byte progress record that is
|
||||||
writer, and for which mission consumes which bit.
|
**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
|
**Method:** static only — `.pe` string/pointer analysis + DuckDB disassembly + disc
|
||||||
records. No emulator run, no gamepad input.
|
records. No emulator run, no gamepad input.
|
||||||
**Evidence:** [`captures/challenge-map.txt`](captures/challenge-map.txt),
|
**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
|
### 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`
|
### 5.5 Both gate words are one record — and it is **not** the savegame ✅
|
||||||
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.
|
|
||||||
|
|
||||||
So a **hand-written progress record with the stage bits set would unlock the challenge
|
`0x82175110` copies the record at singleton `+80` in full: two words, an 8-byte pair, a
|
||||||
missions**, and the last 42 units of the Route-B harvest become one run instead of an
|
184-byte `memcpy`, 8 words from `+200`, then **816 bytes at `+232`, 816 more at
|
||||||
unreachable menu — *if* this struct is what the savegame carries. That is the open
|
`+1048`**, a sub-object at `+1864`, and a final word at **`+1876`**. So the record runs
|
||||||
question.
|
`+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 24–29) |
|
||||||
|
|
||||||
|
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` /
|
**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
|
`sub_822C8748` looked promising because they sit in the save serializer's code region
|
||||||
|
|||||||
Reference in New Issue
Block a user