The withdrawal last iteration was caused by `rm -f /tmp/xenia-canary.lock` -- the obvious way past a lock orphaned by kill -9, which also disables the guard for every later launch. Three instances ended up live at once, sharing /tmp/xenia_pad.txt and display :98, and silently confounded an input experiment. Care is not a fix, so this is tooling. ensure_single_emulator.sh counts live instances, stops them (plain kill, then -9, each with a bounded wait), verifies ZERO, and only then removes the lock -- refusing loudly if any remain. The lock is never removed before the condition it guards against is verified absent. FOUR scripts did the bare `rm -f`, and only two were mine from today: menu_loop_session.sh, title_draw_capture.sh, poke_control.sh and resume_reliability.sh. So the footgun was corpus-wide rather than introduced this session. All four now route through the guard. The guard is controlled rather than assumed: run against a deliberately started live instance it reports "1 instance(s) live -- stopping them", ends at 0 with the lock cleared, and exits 0. A guard that only ever passes on an already-clean slate would prove nothing. It also kills by process NAME. `pkill -f xenia_canary` matches the shell running it -- that has now cost this corpus three commands, one of them a cleanup that died halfway and left the very instances it was meant to remove. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wuu56cE8vJGTBtn1ppsk8v
85 lines
3.9 KiB
Bash
Executable File
85 lines
3.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# POSITIVE CONTROL for the poke method: does a write to /dev/shm reach the guest?
|
|
#
|
|
# Two poke experiments produced no observable effect, and without this control
|
|
# "the game ignored it" cannot be told from "the write never arrived"
|
|
# (script-runtime-probe.md). The stick test only proves the value persisted in
|
|
# the shared-memory FILE.
|
|
#
|
|
# The control has to be something the GAME visibly reacts to. Hull is ideal:
|
|
# it lives at `player position + 0x154`, and dropping it to 1 should destroy the
|
|
# player -- an unambiguous screen change, no OCR needed.
|
|
#
|
|
# A SINGLE write is not enough: measured 2026-08-25, the game rewrote hull from
|
|
# 1 back to 1000.0f within 12 s, so one poke lands between two of the game's own
|
|
# writes and is gone before anything samples it. This HAMMERS the value in a
|
|
# tight loop for HAMMER_S seconds so it is low whenever the game looks.
|
|
#
|
|
# ship dies / GAME OVER -> the guest reads our writes, and hull is
|
|
# authoritative. The poke method is validated.
|
|
# nothing happens -> hull is a readout and the real copy is elsewhere;
|
|
# the method is still unproven, but that is a fact
|
|
# about the field rather than about the plumbing.
|
|
#
|
|
# Self-retrying, because roughly two runs in three freeze and a freeze was
|
|
# costing a whole iteration. Each attempt: boot, check the guest is animating,
|
|
# locate the player, poke, look. A freeze at any step costs a retry.
|
|
set -u
|
|
export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98
|
|
export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages
|
|
SD="$(cd "$(dirname "$0")" && pwd)"; export SD
|
|
ATTEMPTS="${1:-3}"
|
|
|
|
alive_and_moving(){ python3 -c "
|
|
import sys; sys.path.insert(0,'$SD')
|
|
import frozen; d,_=frozen.frozen(5.0); sys.exit(1 if d else 0)"; }
|
|
|
|
for a in $(seq 1 "$ATTEMPTS"); do
|
|
echo "=== attempt $a/$ATTEMPTS ($(date +%T))"
|
|
pkill -9 -x xenia_canary 2>/dev/null; pkill -9 -f '[p]ilot.py' 2>/dev/null; sleep 2
|
|
. "$(dirname "${BASH_SOURCE[0]}")/ensure_single_emulator.sh"
|
|
ensure_single_emulator || exit 3
|
|
"$SD/launch_mission.sh" fly >/tmp/pc-boot.log 2>&1 || { echo " boot failed"; continue; }
|
|
if ! alive_and_moving; then echo " guest already frozen after boot"; continue; fi
|
|
|
|
pos=""
|
|
for t in 1 2 3; do
|
|
python3 "$SD/pad.py" set "rt=1" >/dev/null 2>&1; sleep 4
|
|
python3 "$SD/pad.py" clear >/dev/null 2>&1
|
|
pos=$(timeout 120 python3 "$SD/entities2.py" self 0x130 2>/dev/null \
|
|
| sed -n 's/.*pos va \(0x[0-9a-fA-F]*\).*/\1/p' | head -1)
|
|
[ -n "$pos" ] && break
|
|
echo " locate attempt $t: no player"
|
|
done
|
|
[ -n "$pos" ] || { echo " could not locate the player"; continue; }
|
|
if ! alive_and_moving; then echo " froze during locate"; continue; fi
|
|
|
|
hull=$((pos + 0x154))
|
|
echo " player pos $pos, hull $(printf '0x%X' $hull)"
|
|
before=$(python3 "$SD/gpoke.py" r32 "$hull" 1 2>/dev/null | tail -1)
|
|
echo " hull before: $before"
|
|
screenshot /tmp/pc-before.png >/dev/null 2>&1
|
|
HAMMER_S="${HAMMER_S:-15}"
|
|
echo " hammering hull=1 for ${HAMMER_S}s..."
|
|
python3 - "$hull" "$HAMMER_S" <<'PY'
|
|
import os, struct, sys, time
|
|
sys.path.insert(0, os.environ.get('SD', '.'))
|
|
import gmem
|
|
va = int(sys.argv[1]); secs = float(sys.argv[2])
|
|
off = gmem.va_to_off(va)
|
|
n = 0
|
|
with open(gmem.mem_path(), 'r+b', buffering=0) as f:
|
|
t0 = time.time()
|
|
while time.time() - t0 < secs:
|
|
f.seek(off); f.write(struct.pack('>I', 1)); n += 1
|
|
print(' wrote hull=1 %d times' % n)
|
|
PY
|
|
screenshot /tmp/pc-after.png >/dev/null 2>&1
|
|
echo " hull after : $(python3 "$SD/gpoke.py" r32 "$hull" 1 2>/dev/null | tail -1)"
|
|
echo " screen before: $(python3 "$SD/screen_id.py" /tmp/pc-before.png | head -1)"
|
|
echo " screen after : $(python3 "$SD/screen_id.py" /tmp/pc-after.png | head -1)"
|
|
python3 "$SD/frozen.py" --pair /tmp/pc-before.png /tmp/pc-after.png 2>&1 | tail -1
|
|
echo "CONTROL RAN"; exit 0
|
|
done
|
|
echo "CONTROL DID NOT RUN in $ATTEMPTS attempts"; exit 1
|