Files
Sylpheed/tools/re-capture/wait_screen.sh
Sylpheed RE agent 3cdf2b24b2 re: locate both guest hash routines; IXUD solved; two corrections
Found the routines in the disassembly DB rather than guessing from data:
  sub_82447DF0  IDXD tag hash  (lbz+extsb, modulus 0x00FFFFDF, magic 0x2101)
  sub_82447E70  IXUD tag hash  (lhz, 64-bit, modulus 0xFFFFFF67 then 0x00FFFFDF)
Both transcribed instruction-for-instruction into Python and Rust.

IXUD SOLVED. It defeated every single-modulus search because it chains TWO
exact moduli -- the loop reduces mod 2^32-153 in 64-bit arithmetic and only the
result is folded mod 2^24-33. A polynomial mod M1 folded through M2 is not a
polynomial mod anything, which is exactly why the gcd test returned 1. Verified
independently: 86/86 record keys and 108,261/108,261 field tags in
GP_MAIN_GAME_E.pak, and NoRecord -> 0x1c6d9c96.

CORRECTION 1: tag_hash must SIGN-EXTEND each byte (extsb). My reconstruction
used unsigned bytes and matched all 1.27M disc names -- every one is ASCII --
while disagreeing on ~90% of random inputs with a byte >= 0x80 (verified:
18096/20000). The disc could never have caught this; only the disassembly did.

CORRECTION 2: name_hash's reduction is EXACT, not lossy. The module doc claimed
the missing conditional subtract made it something other than %. rlwinm r6,r6,
9,23,31 is just hi>>23, and with RECIP = floor(2^55/M)+1 that is Granlund-
Montgomery magic division -- 0 wrong at every quotient boundary across the full
32-bit domain. Retracted.

cargo test -p sylpheed-formats --lib hash: 10/10.
2026-08-25 11:28:35 +00:00

76 lines
3.2 KiB
Bash
Executable File

#!/usr/bin/env bash
# Wait until screen_id.py reports <class>, instead of sleeping a guessed number
# of seconds.
#
# Written because `launch_mission.sh` allowed a fixed 28 s for LOAD -> READY
# ROOM and neither run on 2026-08-23 was ready in time: the next press was
# swallowed by the transition, which left one run in OPTIONS and the next in
# BRIEFINGS, and `wait_flight.sh` then tapped A into the wrong screen for five
# minutes. See docs/re/dynamic-re-state-restore.md.
#
# `--tap BTN` presses BTN every 6 s *until the class is first seen* — that is
# what clears a dialog the caller cannot know about, such as the
# "Auto-Save is active. OK?" that a freshly restored profile puts up. Tapping
# STOPS at the first match, because on most screens the button that dismisses a
# dialog also leaves the screen you are waiting for.
#
# ⚠️ `--tap` is BLIND and that is dangerous on a YES/NO dialog: this game starts
# those with the cursor on **NO**, so on the LOAD GAME screen a blind A answers
# NO, falls back to the save list, and the next tap reopens the dialog — a
# stable oscillation that burned three consecutive 300 s boots as
# "NO readyroom" while the pad was working perfectly.
#
# Prefer **`--tap-if-dialog BTN`**: same thing, but only presses while a modal
# is actually up (dialog_up.py, which detects the dim the game draws behind a
# modal). With a dialog up, A is the OK/affirmative button and pressing it is
# safe; with no dialog up it does nothing, so it cannot oscillate.
#
# Two consecutive matches are required, so a single frame caught mid-fade does
# not count as arrival.
#
# Usage: wait_screen.sh <class> [timeout_s] [--tap BTN]
set -u
CLASS="${1:?usage: wait_screen.sh <class> [timeout_s] [--tap BTN]}"
TIMEOUT="${2:-180}"
TAP=""; TAP_ONLY_IF_DIALOG=0
case "${3:-}" in
--tap) TAP="${4:-A}" ;;
--tap-if-dialog) TAP="${4:-A}"; TAP_ONLY_IF_DIALOG=1 ;;
esac
export HOME=/sylph-home/re
DISP="${DISPLAY:-:98}"
SD="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Same liveness rule as skip_intro.sh/wait_flight.sh: a failed screenshot must
# not be indistinguishable from "the screen is not there yet".
alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; }
DEADLINE=$(( SECONDS + TIMEOUT ))
hits=0; seen=0; last_tap=0
while [ $SECONDS -lt $DEADLINE ]; do
rm -f /tmp/ws.png
if ! screenshot /tmp/ws.png >/dev/null 2>&1 || [ ! -s /tmp/ws.png ]; then
xdpyinfo -display "$DISP" >/dev/null 2>&1 \
|| { echo "DISPLAY LOST at ${SECONDS}s"; exit 3; }
sleep 2; continue
fi
[ -n "$(alive)" ] || { echo "EMULATOR GONE at ${SECONDS}s"; exit 4; }
got="$(python3 "$SD/screen_id.py" /tmp/ws.png | awk '{print $1}')"
if [ "$got" = "$CLASS" ]; then
seen=1; hits=$(( hits + 1 ))
[ $hits -ge 2 ] && { echo "$CLASS at ${SECONDS}s"; exit 0; }
else
hits=0
if [ -n "$TAP" ] && [ $seen -eq 0 ] && [ $(( SECONDS - last_tap )) -ge 6 ]; then
if [ "$TAP_ONLY_IF_DIALOG" = 1 ] && ! python3 "$SD/dialog_up.py" /tmp/ws.png >/dev/null 2>&1; then
: # no modal up -- pressing would be blind
else
python3 "$SD/pad.py" tap "$TAP" 0.3
last_tap=$SECONDS
fi
fi
fi
sleep 2
done
echo "NO $CLASS within ${TIMEOUT}s (last seen: ${got:-none})"; exit 1