re(challenge): the cleared-stage mask is CONFIRMED on the running game
Booted the title and read the two gate words live:
0x828F40C0 = 0x00000002 word A
0x828F4814 = 0x00000000 word B
Word A = 2 = bit 1. The profile's save is Stage 02 "At Standby" -- stage 01
cleared -- so the mask is exactly one bit, at the index of the one cleared
stage, 1-BASED. Reproduced across two cold boots. That confirms against a known
progress state, on the real game:
- the singleton is the static object at 0x828F4070, as derived statically;
- word A is a cleared-stage bitmask (not achievements, not a stage number);
- bit index = stage id, 1-based, so TimeAttack's REQUIREMENT 16 means "clear
stage 16" -- the last story mission;
- word B is the challenge half and is 0 on a story-only profile.
New tools: gpoke.py (live guest-memory WRITE, companion to gmem.py, prints
before/after for every word), pad.py (drives the new --hid=file pad; replaces
vgamepad, which leaked to the host through /dev/uinput), challenge_probe.sh
(one blocking session: boot, wait for title, drive in, poke, screenshot).
Poking both words did NOT surface a challenge entry in EXTRAS -- and that menu
was built 26 s after the poke, so it is not staleness. Entering MISSION SELECT
then failed, but the log names the real cause and it is not the gate:
MmAllocatePhysicalMemoryEx could not satisfy a 128 MB request (parent free
30633/131072 pages), the guest threw a C++ exception, and Xenia surfaced its
generic "Disc Read Error". It is preceded by "BaseHeap::Release failed because
address is not a region start" -- a failed release leaking the range. Recorded
as an emulator heap problem, with the control run (same navigation, no poke)
named as the next step.
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
# Challenge / EX missions — the stage set, the GamePart graph, and the kind field
|
||||
|
||||
**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`, 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.
|
||||
switch) and for the **unlock mechanism** — a bit test against a *cleared-stage* mask,
|
||||
now **read live off the running game and matching the profile's progress exactly**
|
||||
(§5.6). 🟡 for the stage-field crash mechanism, for word B's writer, and for which
|
||||
mission consumes which bit; ❔ how the challenge menu is actually entered.
|
||||
**Method:** static analysis (`.pe` strings/pointers + DuckDB disassembly + disc
|
||||
records), then confirmed on the running title via the container-safe
|
||||
[`--hid=file` pad](../../../xenia-canary-native/src/xenia/hid/file/file_input_driver.h)
|
||||
and a live guest-memory read.
|
||||
**Evidence:** [`captures/challenge-map.txt`](captures/challenge-map.txt),
|
||||
[`captures/challenge-screen-config.txt`](captures/challenge-screen-config.txt),
|
||||
[`structures/achievements.md`](structures/achievements.md);
|
||||
@@ -268,7 +270,40 @@ into this record.
|
||||
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
|
||||
### 5.6 CONFIRMED ON THE RUNNING GAME ✅
|
||||
|
||||
Read live from a booted title (`tools/re-capture/gpoke.py r32`):
|
||||
|
||||
```
|
||||
0x828F40C0 = 0x00000002 word A
|
||||
0x828F4814 = 0x00000000 word B
|
||||
```
|
||||
|
||||
**Word A = 2 = bit 1 set.** The profile's save is *Stage 02, "At Standby"* — i.e.
|
||||
**stage 01 cleared** — so the mask is exactly one bit, at the index of the one
|
||||
cleared stage, **1-based**. Reproduced on two separate cold boots. That confirms, on
|
||||
the running game and against a known progress state:
|
||||
|
||||
- the singleton really is the static object at `0x828F4070`;
|
||||
- word A is a **cleared-stage bitmask**, not achievements, not a stage number;
|
||||
- bit index = **stage id, 1-based** — so `TimeAttack`'s `REQUIREMENT 16` is
|
||||
"clear stage 16", the last story mission;
|
||||
- word B is the challenge half and is `0` on a story-only profile, as expected.
|
||||
|
||||
Both words were then poked (`0xFFFFFFFF` / `0x3F`) and read back OK.
|
||||
|
||||
**What the poke did not do (yet):** `EXTRAS` still shows only `MISSION SELECT /
|
||||
MOVIE THEATER / BACK` — no challenge entry — although the menu was built 26 s
|
||||
*after* the poke, so this is not staleness. Entering `MISSION SELECT` then failed,
|
||||
and the log gives the real reason: **`MmAllocatePhysicalMemoryEx` could not satisfy
|
||||
a 128 MB request** (`parent free 30633/131072 pages`), the guest threw a C++
|
||||
exception, and Xenia surfaced it as its generic *"Disc Read Error"* dialog. It is
|
||||
preceded by `BaseHeap::Release failed because address is not a region start`, a
|
||||
failed release that leaks the range. So that is an emulator/heap problem on the way
|
||||
into the screen, **not** evidence about the gate. Open: repeat without the poke to
|
||||
see whether `MISSION SELECT` fails the same way regardless.
|
||||
|
||||
### 5.7 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
|
||||
|
||||
Reference in New Issue
Block a user