re(units): S10 closes the story campaign for real -- 69 units, 7 204 values

roster_target had flagged a contradiction: S10, a STORY stage, still fielded an
unharvested unit, which the "story campaign complete at 68 units" claim did not
account for. S10 was simply never flown. Flying it settles it:
UN_e005_ADAN_ElanTypeQ_Margras is in the snapshot, 144 rows, and the merge takes
the file 68 -> 69 units, 9 393 -> 9 537 rows, 7 115 -> 7 204 defaulted-on-disc
values, with 154 disc cross-checks agreeing and 0 disagreeing. roster_target now
reports S10: 0 missing, and every remaining gap is a CHALLENGE stage
(S24/S25/S27/S28/S29).

A much simpler way to fly a story stage, replacing the save-editing route
(tools/re-capture/fly_stage.sh): poke ONE word -- 0x828F40C0 = 0x0001FFFE marks
stages 1-16 cleared -- and MISSION SELECT will launch any of them. Nothing is
written to disc, so there is no save to back up and restore, and TRAP 1 from the
old recipe (launch_mission.sh silently loading the last-used slot) cannot happen.

Route, learned by screenshotting each step: MISSION SELECT -> pick stage -> A ->
mission briefing (A: Continue) -> READY ROOM -> TAKE OFF -> flight. The READY
ROOM carries an "EXTRA" watermark, which is the static analysis's mission-KIND
= 3 ("EXTRA" config section, docs/re/challenge-mission-gate.md section 4) visible
on screen -- an independent confirmation of that field's meaning.

Also recorded: a snapshot taken at the BRIEFING yields 0 runtime objects. Unit
definitions are instantiated at stage load proper, so the snapshot has to wait
for flight; the briefing screen is too early.
This commit is contained in:
2026-08-13 21:14:21 +00:00
parent aac017dd0d
commit 4cbce6bd21
3 changed files with 234 additions and 0 deletions

90
tools/re-capture/fly_stage.sh Executable file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Launch any STORY stage from MISSION SELECT and snapshot guest RAM in flight.
#
# Supersedes the save-editing route (patch GHAD +52 in every slot, boot, LOAD
# GAME, TAKE OFF): poking ONE word marks every story stage cleared, and MISSION
# SELECT will then launch any of them —
# 0x828F40C0 = 0x0001FFFE cleared-stage mask, bits 1..16
# See docs/re/challenge-mission-gate.md. Nothing is written to disc, so there is
# no save to back up and restore afterwards.
#
# Usage: fly_stage.sh <stage 1..16> [boot_timeout_s]
set -u
STAGE="${1:?usage: fly_stage.sh <stage 1..16>}"
BOOT_TIMEOUT="${2:-400}"
export HOME=/sylph-home/re DISPLAY=:99 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"; mkdir -p "$SHOTS"
say() { echo "[$(date +%H:%M:%S)] $*"; }
shot() { screenshot "$SHOTS/fly$STAGE-$1.png" >/dev/null 2>&1; }
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 points, never one pixel — a single
# bright-pixel test once matched a white loading flash and the run navigated a
# menu that was not on screen.
at_menu() {
screenshot /tmp/nav-probe.png >/dev/null 2>&1 || return 1
read -r r g b < <(px "648,221"); [ -n "${r:-}" ] || return 1
[ "$r" -gt 230 ] && [ "$g" -gt 230 ] && [ "$b" -gt 230 ] || return 1
read -r r2 g2 b2 < <(px "560,300"); [ -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:-}" ] || return 1
[ "$g" -gt 130 ] && [ $((g - r)) -gt 45 ] && [ $((g - b)) -gt 45 ]
}
pkill -9 -x xenia_canary 2>/dev/null; sleep 1
rm -f /dev/shm/xenia_* 2>/dev/null
: > "$XENIA_PAD_FILE"
say "launching canary for stage $STAGE"
run-canary --audio --apu=sdl --log_mask=13 \
--logged_profile_slot_0_xuid=E0300000EFBEA3D4 \
--hid=file --pad_file="$XENIA_PAD_FILE" &
# Xvfb keeps the previous instance's framebuffer until the new one draws.
xsetroot -solid black 2>/dev/null || true
say "waiting for the main menu"
MENU=0
for i in $(seq 1 "$BOOT_TIMEOUT"); do
[ "$i" -lt 40 ] && { sleep 1; continue; }
if at_menu && sleep 1 && at_menu; then say "MAIN MENU after ${i}s"; MENU=1; break; fi
at_title && { say " title — tapping A"; pad tap A 0.25; sleep 2; }
sleep 1
done
[ "$MENU" = 1 ] || { say "TIMEOUT: no main menu"; shot 00-timeout; exit 1; }
say "poking the cleared-stage mask so every story stage is selectable"
poke w32 0x828F40C0 0x0001FFFE
# main menu -> EXTRAS -> MISSION SELECT
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 01-extras
pad tap A 0.15; sleep 4; shot 02-missionselect
# the list starts on Stage01
say "stepping to Stage$STAGE"
for _ in $(seq 2 "$STAGE"); do pad dpad down 0.06; sleep 0.30; done
sleep 1; shot 03-selected
pad tap A 0.20; sleep 3; shot 04-after-A
pad tap A 0.20; sleep 3; shot 05-after-A2
say "waiting for flight (up to 240s) — snapshotting when the guest settles"
for i in $(seq 1 240); do
sleep 1
[ $((i % 30)) -eq 0 ] && shot "06-wait-$i"
done
shot 07-final
SHM=$(ls /dev/shm/xenia_memory_* 2>/dev/null | head -1)
if [ -n "$SHM" ]; then
OUT="$HOME/snap-stage$STAGE.bin"
cp --sparse=always "$SHM" "$OUT" && say "snapshot -> $OUT ($(du -h "$OUT" | cut -f1) actual)"
fi
say "shots: $SHOTS/fly$STAGE-*.png"