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:
87
tools/re-capture/challenge_probe.sh
Executable file
87
tools/re-capture/challenge_probe.sh
Executable file
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env bash
|
||||
# Probe the challenge-mission gate on the running game.
|
||||
#
|
||||
# The gate (docs/re/challenge-mission-gate.md): GamePart_ChallengeMission tests a
|
||||
# CLEARED-STAGE bitmask on a static singleton at guest 0x828F4070 —
|
||||
# word A 0x828F40C0 bit = stage id, for ids < 24 (story 1-16, tutorial 18-23)
|
||||
# word B 0x828F4814 bit = stage id - 24 (challenge 24-29)
|
||||
# so setting every bit should make all six challenge missions available without
|
||||
# playing the campaign. This boots, reaches the title, pokes both words, and
|
||||
# screenshots the menus so the result can be seen.
|
||||
#
|
||||
# Runs as ONE blocking foreground call on purpose: setsid'd processes are reaped
|
||||
# at turn boundaries, so a session split across calls loses its emulator.
|
||||
#
|
||||
# Usage: challenge_probe.sh [boot_timeout_s]
|
||||
set -u
|
||||
export HOME=/sylph-home/re
|
||||
export DISPLAY=:99
|
||||
export SDL_AUDIODRIVER=dummy
|
||||
export XENIA_PAD_FILE=/tmp/xenia_pad.txt
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
pad() { python3 "$HERE/pad.py" "$@"; }
|
||||
poke() { python3 "$HERE/gpoke.py" "$@"; }
|
||||
SHOTS="$HOME/shots"
|
||||
BOOT_TIMEOUT="${1:-420}"
|
||||
mkdir -p "$SHOTS"
|
||||
|
||||
say() { echo "[$(date +%H:%M:%S)] $*"; }
|
||||
|
||||
# --- clean slate -------------------------------------------------------------
|
||||
pkill -9 -x xenia_canary 2>/dev/null
|
||||
sleep 1
|
||||
rm -f /dev/shm/xenia_* 2>/dev/null
|
||||
: > "$XENIA_PAD_FILE"
|
||||
|
||||
# --- launch ------------------------------------------------------------------
|
||||
say "launching canary (lavapipe, file pad)"
|
||||
run-canary --audio --apu=sdl --log_mask=13 \
|
||||
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 \
|
||||
--hid=file --pad_file="$XENIA_PAD_FILE" &
|
||||
CANARY_PID=$!
|
||||
trap 'pkill -9 -x xenia_canary 2>/dev/null' EXIT
|
||||
|
||||
# --- wait for the title ------------------------------------------------------
|
||||
# Oracle: the green "PRESS (A) BUTTON" glyph at (625,618).
|
||||
say "waiting for the title (up to ${BOOT_TIMEOUT}s)"
|
||||
TITLE=0
|
||||
for _ in $(seq 1 "$BOOT_TIMEOUT"); do
|
||||
if screenshot /tmp/title-probe.png >/dev/null 2>&1; then
|
||||
read -r r g b < <(convert /tmp/title-probe.png -format \
|
||||
"%[fx:int(255*p{625,618}.r)] %[fx:int(255*p{625,618}.g)] %[fx:int(255*p{625,618}.b)]" info: 2>/dev/null)
|
||||
if [ -n "${g:-}" ] && [ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]; then
|
||||
say "TITLE detected (rgb $r,$g,$b)"
|
||||
TITLE=1
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
[ "$TITLE" = 1 ] || { say "TIMEOUT: no title"; screenshot "$SHOTS/chal-00-timeout.png"; exit 1; }
|
||||
|
||||
# --- prove the file pad works before trusting anything else -------------------
|
||||
say "pad check: tapping A at the title"
|
||||
pad tap A 0.25
|
||||
sleep 3
|
||||
screenshot "$SHOTS/chal-01-after-A.png" >/dev/null
|
||||
say "file-pad log lines so far:"
|
||||
grep -c 'file-pad' "$HOME/canary.stdout" 2>/dev/null || true
|
||||
grep 'file-pad' "$HOME/canary.stdout" 2>/dev/null | tail -3
|
||||
|
||||
# --- read the gate words BEFORE poking ---------------------------------------
|
||||
say "gate words before poke:"
|
||||
poke r32 0x828F40C0 1
|
||||
poke r32 0x828F4814 1
|
||||
|
||||
# --- poke --------------------------------------------------------------------
|
||||
say "poking word A = 0xFFFFFFFF, word B = 0x3F"
|
||||
poke w32 0x828F40C0 0xFFFFFFFF
|
||||
poke w32 0x828F4814 0x0000003F
|
||||
|
||||
# --- look at the menu --------------------------------------------------------
|
||||
sleep 2
|
||||
screenshot "$SHOTS/chal-02-mainmenu.png" >/dev/null
|
||||
say "screenshots in $SHOTS: chal-01-after-A.png chal-02-mainmenu.png"
|
||||
say "done — leaving the emulator running for follow-up"
|
||||
trap - EXIT
|
||||
Reference in New Issue
Block a user