re(challenge): the part id is never persisted -- differential search says stack only

No literal 26 exists anywhere, so the GamePart id is computed. That does not stop
it being found: the id is KNOWN at each screen from the GamePart table (EXTRAS = 5,
MISSION SELECT = 7), so snapshot both and intersect. New tool diff_words.py does
the classic differential search over the sparse guest image, and find_partslot.sh
drives the two screens and runs it.

Result: 171 MB scanned, exactly 4 addresses read 5 then 7 -- 0x708FFBEC,
0x708FFCBC, 0x708FFDAC, 0x708FFE20 -- and all four are guest STACK (the same run's
log puts thread stacks at 0x709...). So the requested part id exists only as a
stack argument in flight; there is no persistent field, which is consistent with
finding no literal store, and means there is nothing stable to poke.

That closes the last memory-and-menu route to the challenge missions. Reaching
them needs either the genuine in-game unlock (an in-mission attainment, per
AVSCRIPT_COMMAND_ATTAINMENT_CHALLENGE_MISSION_CARGO_SCORE) or an emulator-side
hook that forces the transition -- a code change, not a poke.

diff_words.py is worth keeping well beyond this question: it locates any field
whose address is unknown but whose value is known at two moments.
This commit is contained in:
2026-08-13 21:31:47 +00:00
parent a79a1da183
commit faa7b3d611
3 changed files with 167 additions and 0 deletions

View File

@@ -385,6 +385,30 @@ against the profile route. `+80` at least is handled as a small struct by addres
(`addi r4, obj, 80` → copy helper `0x82175110`, written back via `0x8216FF70`), which
is what a serialised value object looks like.
### 5.8 The part id is never persisted — differential search ✅ (negative)
`sub_821749C0` creates a part from `slot+12`, and no literal 26 exists anywhere, so
the id is computed. It can still be found by **differential search**, because the id
is *known* at each screen from §3: `GP_EXTRAS` = 5, `GP_MISSION_SELECT` = 7. Snapshot
both screens and intersect (`tools/re-capture/diff_words.py`):
```
scanned 171 MB of allocated guest memory
addresses reading 5 on EXTRAS and 7 on MISSION SELECT: 4
0x708FFBEC 0x708FFCBC 0x708FFDAC 0x708FFE20
```
**All four are guest stack** (thread stacks sit at `0x709…` in the same run's log).
So the requested part id exists only as a **stack argument in flight** — there is no
persistent field holding it, which is consistent with finding no literal store and
means **there is nothing stable to poke**. Forcing a transition to GamePart 26 needs
the caller's context, i.e. an emulator-side hook rather than a memory write.
**Every memory-and-menu route to the challenge missions is now closed** (§5.4, §5.6,
§5.7, this section). What remains is the genuine in-game unlock — an in-mission
attainment, per `AVSCRIPT_COMMAND_ATTAINMENT_CHALLENGE_MISSION_CARGO_SCORE` — or a
Canary patch that forces the transition in emulator code.
## 6. The unlock condition, from the strings ❔
Three strings say challenge missions are *announced*, not menu-browsed: