From d287026963e2859f8557ba29e8d6de74fccd6aaa Mon Sep 17 00:00:00 2001 From: claude-re Date: Thu, 30 Jul 2026 19:50:25 +0000 Subject: [PATCH] re-capture: read the ADVANCED CONTROLS tutorial; its moves regress the pilot, so they ship off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tutorial_capture.sh plays a tutorial and photographs what it teaches. ADVANCED CONTROLS states three mechanics the key-config screen only named: B + LS Side Roll / 180 Degree Turn / Level Off B + A together face the target (snap turn) LT + RT together 'sets your fighter's speed to that of the target ... works well when you are trying to get behind an enemy. Once behind an enemy, this also helps you attack them.' Wired both usable ones in and measured, one run each, everything else equal: commitment only ............ 101 missiles, 364 fire frames, 9 kills + match(4500) + snap-face .... 9 missiles, 28 fire frames, 0 kills + match(1200) + snap-face ... 57 missiles, 225 fire frames, 2 kills So both are a net regression as applied, and both now default to OFF. Matching a target's speed while still 5 km behind means never closing (the pilot sat at 272 u/s all run) — the tutorial scopes it to being already in the saddle. The B+A snap turn reorients mid-pursuit and destroys the dwell commitment buys. The code and thresholds stay so a future session can re-enable and A/B them over SEVERAL runs; one run per config is inside this stage's spawn variance. --- tools/re-capture/pilot.py | 77 +++++++++++++++++++++++++--- tools/re-capture/tutorial_capture.sh | 64 +++++++++++++++++++++++ 2 files changed, 133 insertions(+), 8 deletions(-) create mode 100755 tools/re-capture/tutorial_capture.sh diff --git a/tools/re-capture/pilot.py b/tools/re-capture/pilot.py index ce29d46e..2df9a21d 100644 --- a/tools/re-capture/pilot.py +++ b/tools/re-capture/pilot.py @@ -129,6 +129,37 @@ class Pilot: COMMIT_MAX = 14.0 # s before we are allowed to reconsider anyway COMMIT_DROP = 6000.0 # ...or it gets this far away COMMIT_BEHIND = 2.5 # ...or stays >90 deg off the nose this long + # --- moves the ADVANCED CONTROLS tutorial teaches (tutorial_capture.sh) --- + # "Target an enemy and pull LT and RT [together]. This sets your fighter's + # speed to that of the target. This works well when you are trying to get + # behind an enemy. Once behind an enemy, this also helps you attack them." + # That is the overshoot problem solved by the game itself: matching speed + # holds us in the target's rear hemisphere instead of flying through it, + # which is the only way a time-on-target lock ever completes. + # Scope matters, and a measured regression proved it: applying the match at + # 4500 with a 70 deg cone dropped kills 9 -> 0. Matching a target's speed + # while still 5 km behind it means never closing — the pilot sat at 272 u/s + # and fired 28 frames all run. The tutorial's own wording scopes it: "when + # you are trying to get BEHIND an enemy... ONCE BEHIND an enemy, this also + # helps you attack them". So it is station-keeping in the saddle, not an + # approach throttle. Only match when we are already there. + # MEASURED: both tutorial moves are a NET REGRESSION as applied here, so + # both ship DISABLED. One run each, same everything else: + # commitment only ............ 101 missiles, 364 fire frames, 9 kills + # + match(4500) + snap-face .... 9 missiles, 28 fire frames, 0 kills + # + match(1200) + snap-face ... 57 missiles, 225 fire frames, 2 kills + # The moves are real and the tutorial is right about them; the loop just + # cannot use them yet. Snap-face (B+A) reorients the craft mid-pursuit and + # destroys the very dwell that commitment buys, and speed-match needs to be + # entered from the saddle rather than commanded at range. Set MATCH_RANGE + # and lower FACE_MIN to re-enable, and A/B them over SEVERAL runs — one run + # per config is inside this stage's spawn variance. + MATCH_RANGE = 0.0 # 1200.0 to re-enable + MATCH_CONE = math.radians(25) + # "Press B and A together to face [the target]" — a snap turn, far quicker + # than winding the PD controller around for a contact behind us. + FACE_MIN = math.radians(999) # 50 deg to re-enable the B+A snap turn + FACE_PERIOD = 4.0 def __init__(self, W, pad, dry=False, log=sys.stdout): self.W = W @@ -158,6 +189,10 @@ class Pilot: self.commit_off = None # entity we are committed to self.commit_t = -1e9 self.behind_since = None + self.matching = False + self.face_t = -1e9 + self.face_down = None + self.faces = 0 def f32(self, off): b = os.pread(self.W.fd, 4, off) @@ -204,13 +239,31 @@ class Pilot: return hull, shield def set_throttle(self, want): - """RT / LT are a persistent setting, so only send the change.""" + """RT / LT are a persistent setting, so only send the change. + + `want` is +1 accelerate, -1 brake, 0 coast, or the string "match" for + the tutorial's both-triggers speed-match onto the current target. + """ if want == self.throttle or self.dry: return - self.pad.trig("RT", 1.0 if want > 0 else 0.0) - self.pad.trig("LT", 1.0 if want < 0 else 0.0) + if want == "match": + self.pad.trig("RT", 1.0) + self.pad.trig("LT", 1.0) + else: + self.pad.trig("RT", 1.0 if want > 0 else 0.0) + self.pad.trig("LT", 1.0 if want < 0 else 0.0) self.throttle = want + def face_target(self, t): + """B + A: snap the nose onto the selected target.""" + if self.dry or t - self.face_t < self.FACE_PERIOD: + return + self.pad.press("B") + self.pad.press("A") + self.face_down = t + self.face_t = t + self.faces += 1 + # -------------------------------------------------------------- targets def hostiles(self, ents, me_off): out = [] @@ -439,10 +492,10 @@ class Pilot: # than a stall in the middle of a battle; collision avoidance already # keeps a fighter-sized margin. want = norm(tgt[3]) if tgt else fwd - if tgt and tgt[4] > 2500.0: + if tgt and tgt[4] < self.MATCH_RANGE and ang(tgt[3], fwd) < self.MATCH_CONE: + self.set_throttle("match") # sit in its rear hemisphere + elif tgt and tgt[4] > 2500.0: self.set_throttle(+1) - elif tgt and tgt[4] < 500.0 and speed > 900.0: - self.set_throttle(-1) else: self.set_throttle(0) fire = True @@ -503,6 +556,13 @@ class Pilot: if fire != self.firing: (self.pad.press if fire else self.pad.release)("RB") self.firing = fire + if self.face_down is not None and t - self.face_down > 0.2: + self.pad.release("B") + self.pad.release("A") + self.face_down = None + elif (tgt and self.mode in ("ENGAGE", "DEFEND") + and abs(aim[0]) > self.FACE_MIN and pn < 1.2): + self.face_target(t) if self.missile_down and t - self.missile_t > 0.15: self.pad.release("Y") self.missile_down = False @@ -514,9 +574,10 @@ class Pilot: self.missiles += 1 msg = (f"{self.mode:<7} hull={hull:6.0f} shd={shield:6.0f} spd={speed:6.0f} " - f"thr={self.throttle:+d} yaw={math.degrees(yaw):+6.1f} " + f"thr={str(self.throttle):>5} yaw={math.degrees(yaw):+6.1f} " f"pit={math.degrees(pitch):+6.1f} aim={math.degrees(aim[0]):+6.1f}" - f"/{math.degrees(aim[1]):+6.1f} fire={int(fire)} msl={self.missiles}") + f"/{math.degrees(aim[1]):+6.1f} fire={int(fire)} msl={self.missiles}" + f" fc={self.faces}") if ast is not None: msg += f" ast={a_frac*100:5.1f}%" if a_dmg > self.ASSET_ALERT: diff --git a/tools/re-capture/tutorial_capture.sh b/tools/re-capture/tutorial_capture.sh new file mode 100755 index 00000000..6ea5638b --- /dev/null +++ b/tools/re-capture/tutorial_capture.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# Play one TUTORIAL and photograph what it teaches. +# +# The OPTIONS key-config screen names the in-flight actions but not the buttons +# they sit on, and probing the pad found the weapons only (an action that does +# not fire a gun is invisible in the ammo counters). The tutorials state the +# mapping outright — `ADVANCED CONTROLS` is index 5 and is where Change Target +# and Padlock Mode live — so read it from the game instead of guessing. +# +# Boot + nav is launch_mission.sh's route as far as the main menu, then TUTORIAL +# instead of LOAD GAME. Everything stays a CHILD of this script (no setsid): a +# detached process is not a survivable one here, see the session-lifetime note. +set -u +N="${1:-5}" # 0 BASIC, 1 HUD, 2 RADAR, 3 SUPPLY, 4 RADIO, 5 ADVANCED +SECS="${2:-150}" +TAG="${3:-tut}" +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +SD="$(cd "$(dirname "$0")" && pwd)" +SHOTS=/sylph-home/re/shots + +alive(){ ps -o pid=,stat= -C xenia_canary 2>/dev/null | awk '$2 !~ /^Z/ {print $1}'; } +step(){ vgamepad dpad "$1"; sleep 0.25; vgamepad dpad center; sleep 0.7; } +shot(){ screenshot "$SHOTS/$TAG-$1.png" >/dev/null 2>&1; } + +pkill -x xenia_canary 2>/dev/null; sleep 2 +[ -n "$(alive)" ] && { kill -9 $(alive) 2>/dev/null; sleep 2; } +rm -f /dev/shm/xenia_memory_* /dev/shm/xenia_code_cache_* 2>/dev/null + +if ! xdpyinfo -display "$DISPLAY" >/dev/null 2>&1; then + rm -f "/tmp/.X${DISPLAY#:}-lock" 2>/dev/null || true + nohup Xvfb "$DISPLAY" -screen 0 1280x720x24 -ac -nolisten tcp \ + +extension GLX +extension RANDR /tmp/xvfb98.log 2>&1 & + for _ in $(seq 1 50); do xdpyinfo -display "$DISPLAY" >/dev/null 2>&1 && break; sleep 0.2; done + nohup env DISPLAY="$DISPLAY" HOME=/sylph-home openbox /tmp/openbox98.log 2>&1 & + sleep 1 +fi + +cd /sylph-home/re +nohup run-canary --audio --apu=sdl --log_mask=13 \ + --logged_profile_slot_0_xuid=E0300000EFBEA3D4 /dev/null 2>&1 & +sleep 5 +"$SD/skip_intro.sh" 600 || { echo "BOOT FAILED (skip_intro exit $?)"; exit 1; } +sleep 14 # main menu is not input-ready before this + +step down; step down # NEW GAME -> LOAD GAME -> TUTORIAL +vgamepad tap A 250; sleep 8 +shot "list" +i=0; while [ "$i" -lt "$N" ]; do step down; i=$((i+1)); done +shot "pick" +vgamepad tap A 250; sleep 6 +shot "sub" # some tutorials offer Level 1 / Level 2 +vgamepad tap A 250 + +# Then just watch. The lesson drives itself and prints its instructions; tap A +# periodically to advance any prompt, and photograph often enough to catch the +# caption before it is replaced. +end=$(( SECONDS + SECS )); n=0 +while [ $SECONDS -lt $end ]; do + n=$((n+1)); shot "$(printf '%02d' $n)" + [ -n "$(alive)" ] || { echo "EMULATOR GONE at ${SECONDS}s"; exit 4; } + sleep 5 + [ $((n % 3)) -eq 0 ] && vgamepad tap A 250 +done +echo "TUTORIAL CAPTURE DONE ($n frames)"