This repository has been archived on 2026-09-16. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Syplheed-Reborn/tools/re-capture/challenge_probe.sh
Sylpheed RE agent 01580d294e tools: two dead flags that made every emulator boot script fail silently
Trying to read a third paint order off the running game turned up two bugs in
the capture harness, both of which fail in ways that look like the game
misbehaving rather than the script being wrong.

1. `--audio` is not a cvar in this tree, and eight boot scripts passed it.
   Xenia calls ShowSimpleMessageBox from ParseLaunchArguments, BEFORE logging is
   initialised, so the symptom is a 10x10 window, no log, no guest memory and a
   dialog that blocks on XIfEvent forever - i.e. a hang deep in the emulator.
   run-canary`s own header documents this exact trap; the scripts predate it.
   Removed from all eight.

2. `vgamepad` no longer exists - the uinput pad was replaced by the --hid=file
   driver and pad.py - but skip_intro.sh still called it. The script runs
   without `set -e`, so the call failed silently and the title branch pressed
   nothing while still exiting 0. A caller was told "TITLE -> A" with the game
   sitting on the title screen. It now presses through pad.py and exits 6 if
   that fails.

The first bug is fixed and verified: the boot now reaches the title screen with
PRESS (A) BUTTON. The second is fixed but does NOT unblock the title - see the
next commit.
2026-08-19 07:48:16 +00:00

138 lines
5.9 KiB
Bash
Executable File

#!/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.
#
# POKE=1 (default) sets both words; POKE=0 runs the identical navigation without
# touching them. Run it BOTH ways: the first poked run hit a Xenia heap failure on
# the way into MISSION SELECT, and only the control says whether that failure has
# anything to do with the poke.
#
# 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: [POKE=0|1] [TAG=name] 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:-400}"
POKE="${POKE:-1}"
POKE_A="${POKE_A:-0x0001FFFE}" # stages 1-16 cleared
POKE_B="${POKE_B:-0x0000003F}" # challenge stages 24-29 cleared
TAG="${TAG:-$([ "$POKE" = 1 ] && echo poked || echo control)}"
mkdir -p "$SHOTS"
say() { echo "[$(date +%H:%M:%S)] $*"; }
shot() { screenshot "$SHOTS/chal-$TAG-$1.png" >/dev/null 2>&1; }
# --- clean slate -------------------------------------------------------------
pkill -9 -x xenia_canary 2>/dev/null
sleep 1
rm -f /dev/shm/xenia_* 2>/dev/null
: > "$XENIA_PAD_FILE"
LOG="$HOME/canary.stdout"
# --- launch ------------------------------------------------------------------
say "launching canary (lavapipe, file pad) — POKE=$POKE tag=$TAG"
run-canary --apu=sdl --log_mask=13 \
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 \
--hid=file --pad_file="$XENIA_PAD_FILE" &
# Xvfb keeps the LAST instance's framebuffer until the new one draws, so a
# screenshot taken seconds after launch shows the PREVIOUS run's screen. That is
# how a control run once reported "MAIN MENU reached after 1s" against a menu
# belonging to a process that no longer existed. Blank the root, and refuse to
# believe any screen oracle until the emulator has had time to draw its own.
xsetroot -solid black 2>/dev/null || true
LAUNCH_GRACE=40
# --- reach the MAIN MENU, verifying instead of pressing blind ----------------
# Two oracles, both sampled from real screenshots:
# title screen : green "PRESS (A) BUTTON" glyph at (625,618)
# main menu : the "NEW GAME" text at (648,221) is pure white (254,254,254),
# where the title has the yellow planet (208,189,88)
# The first control run pressed A while the ATTRACT MOVIE happened to show a
# greenish pixel at the title-glyph spot, then navigated a menu that was never
# open and reported "0 failures" for a screen it never reached. Verify the menu.
px() { convert /tmp/nav-probe.png -format \
"%[fx:int(255*p{$1}.r)] %[fx:int(255*p{$1}.g)] %[fx:int(255*p{$1}.b)]" info: 2>/dev/null; }
# A screen is identified by a PATTERN of sampled points, never by one pixel. A
# single "is (648,221) white?" test matched a white loading flash, and the run
# then navigated a menu that was not on screen -- the same class of mistake as
# trusting the stale framebuffer. Require the menu's contrast: white "NEW GAME"
# text AND the dark blue panel behind it.
at_menu() {
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
read -r r g b < <(px "648,221") # NEW GAME text: white
[ -n "${r:-}" ] || return 1
[ "$r" -gt 230 ] && [ "$g" -gt 230 ] && [ "$b" -gt 230 ] || return 1
read -r r2 g2 b2 < <(px "560,300") # panel left of LOAD GAME: dark blue
[ -n "${r2:-}" ] || return 1
[ "$r2" -lt 120 ] && [ "$b2" -gt "$r2" ]
}
at_title() {
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
read -r r g b < <(px "625,618")
[ -n "${g:-}" ] && [ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]
}
say "waiting for the main menu (up to ${BOOT_TIMEOUT}s)"
MENU=0
for i in $(seq 1 "$BOOT_TIMEOUT"); do
if [ "$i" -lt "$LAUNCH_GRACE" ]; then sleep 1; continue; fi
if at_menu && sleep 1 && at_menu; then say "MAIN MENU reached after ${i}s"; MENU=1; break; fi
if at_title; then say " title visible — tapping A"; pad tap A 0.25; sleep 2; fi
sleep 1
done
[ "$MENU" = 1 ] || { say "TIMEOUT: never reached the main menu"; shot 00-timeout; exit 1; }
shot 01-mainmenu
say "gate words:"
poke r32 0x828F40C0 1
poke r32 0x828F4814 1
if [ "$POKE" = 1 ]; then
# Default to REAL stage ids only. 0xFFFFFFFF claims stages that do not exist
# (0, 17, and 24-31 in word A), and that run blew the guest heap:
# MmAllocatePhysicalMemoryEx could not satisfy 128 MB and the guest threw.
# Word A bits 1..16 = the story campaign; 18..23 would be the tutorials.
say "poking word A = $POKE_A, word B = $POKE_B"
poke w32 0x828F40C0 "$POKE_A"
poke w32 0x828F4814 "$POKE_B"
else
say "control run — leaving the words untouched"
fi
# --- main menu -> EXTRAS -----------------------------------------------------
# NEW GAME / LOAD GAME / TUTORIAL / OPTIONS / EXTRAS
for _ in 1 2 3 4; do pad dpad down 0.06; sleep 0.35; done
sleep 0.5
pad tap A 0.15
sleep 3
shot 02-extras
# --- EXTRAS -> MISSION SELECT ------------------------------------------------
ALLOC_BEFORE=$(grep -c 'MmAllocatePhysicalMemoryEx: Allocation failed' "$LOG" 2>/dev/null | head -1)
pad tap A 0.15
sleep 4
shot 03-missionselect
ALLOC_AFTER=$(grep -c 'MmAllocatePhysicalMemoryEx: Allocation failed' "$LOG" 2>/dev/null | head -1)
say "MmAllocatePhysicalMemoryEx failures: before=$ALLOC_BEFORE after=$ALLOC_AFTER"
say "guest C++ exceptions: $(grep -c 'Guest attempted to throw a C++ exception' "$LOG" 2>/dev/null | head -1)"
say "shots: $SHOTS/chal-$TAG-*.png"
say "done — emulator left running"