diff --git a/docs/re/challenge-mission-gate.md b/docs/re/challenge-mission-gate.md index cfa6f7d..d20c3b3 100644 --- a/docs/re/challenge-mission-gate.md +++ b/docs/re/challenge-mission-gate.md @@ -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 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` / `sub_822C8748` looked promising because they sit in the save serializer's code region