From dfa769420de4be29f4cab99a2cb362fb9056fd2c Mon Sep 17 00:00:00 2001 From: "Claude (auto-RE)" Date: Thu, 13 Aug 2026 22:22:19 +0000 Subject: [PATCH] re(flight): axis probe pins the rows and checks liveness; fly_stage waits, not sleeps Both fixes the previous run's caveats asked for, plus one the run itself forced. axis_probe.py now: - PINS which non-forward row is up and which is right, by comparing world-Y across the rows in level flight, and says CONFIDENT or WEAK. entities2 measures row 2 = forward against velocity, but the other two were labelled by the D3D convention, and yaw/pitch SWAP if that is wrong -- so the previous run's last two columns were named on an assumption. - checks the craft is ALIVE between inputs, and ABORTS with a message instead of reporting the clean zeros a destroyed craft produces. The first run ended on GAME OVER and only said so afterwards. - measures the UNKNOWN inputs (rx, ry, LB, RB) first while the craft is healthy, keeping the established lx/ly as controls at the end. fly_stage.sh now WAITS for the stage load instead of sleeping a fixed guess. The fixed sleeps worked until they didn't: one load ran long, the script pressed START into a black screen, and every later step went to nothing while the screenshots recorded a plausible-looking sequence. It now polls for a non-black frame and aborts with a pointer to the log if the load hangs (PhysicalHeap::Release failures) rather than continuing blind. The probe itself did not run this iteration -- the stage load hung -- so there is no new axis data, and none is claimed. --- tools/re-capture/axis_probe.py | 55 ++++++++++++++++++++++++++++++++-- tools/re-capture/fly_stage.sh | 23 ++++++++++++++ 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/tools/re-capture/axis_probe.py b/tools/re-capture/axis_probe.py index a072723..083e053 100755 --- a/tools/re-capture/axis_probe.py +++ b/tools/re-capture/axis_probe.py @@ -52,13 +52,16 @@ PAD_FILE = os.environ.get("XENIA_PAD_FILE", "/tmp/xenia_pad.txt") # Candidates. Sticks at full deflection, shoulders as buttons. Triggers are the # throttle (already settled) and are left out. +# Order matters: the craft can be shot down mid-run, so the UNKNOWN inputs go +# first while it is healthy and the already-established ones (lx = roll) go last +# as controls. A row measured after death looks like a clean zero. CANDIDATES = [ - ("lx+", {"lx": 32767}), - ("ly+", {"ly": 32767}), ("rx+", {"rx": 32767}), ("ry+", {"ry": 32767}), ("LB", {"press": "LB"}), ("RB", {"press": "RB"}), + ("lx+", {"lx": 32767}), + ("ly+", {"ly": 32767}), ] @@ -84,6 +87,46 @@ def dot(a, b): return sum(a[i] * b[i] for i in range(3)) +def alive(w, off, cfg, secs=1.0): + """Is the craft still flying? A destroyed craft stops moving, and every + subsequent input then measures a clean, meaningless zero — the failure mode + that invalidated this probe's first run (it ended on GAME OVER and only said + so afterwards).""" + p0 = struct.unpack(">3f", os.pread(w.fd, 12, off)) + time.sleep(secs) + p1 = struct.unpack(">3f", os.pread(w.fd, 12, off)) + return math.dist(p0, p1) > 1.0 + + +def pin_rows(w, off, cfg): + """Which non-forward row is UP and which is RIGHT? + + `entities2` measures row 2 = forward against the velocity vector, but the other + two are assigned by the D3D convention rather than evidence — and yaw and pitch + SWAP if that is wrong, so naming them without this test is a guess. + + Discriminator: in level flight the craft's up-vector points along world +Y and + its right-vector lies near the horizontal plane. Sample at neutral and compare + |world-Y| across the rows.""" + f_i = cfg["fwd_row"] + acc = [0.0, 0.0, 0.0] + n = 0 + for _ in range(20): + time.sleep(0.05) + rs = rows_at(w.fd, off, cfg) + for r in range(3): + acc[r] += rs[r][1] # world Y component + n += 1 + ys = [a / n for a in acc] + cand = [r for r in (0, 1, 2) if r != f_i] + up_i = max(cand, key=lambda r: abs(ys[r])) + right_i = [r for r in cand if r != up_i][0] + print(f"# row world-Y means: {[round(y,3) for y in ys]} (forward = row {f_i})") + print(f"# -> up = row {up_i}, right = row {right_i}" + f" {'CONFIDENT' if abs(ys[up_i]) - abs(ys[right_i]) > 0.3 else 'WEAK — craft may not be level'}") + return up_i, right_i + + def main(): out_csv = sys.argv[1] dwell = float(sys.argv[2]) if len(sys.argv) > 2 else 5.0 @@ -93,7 +136,9 @@ def main(): sys.exit("player entity not found") print(f"# locked on {nm}") f_i = cfg["fwd_row"] - u_i, w_i = [r for r in (0, 1, 2) if r != f_i] + if not alive(w, off, cfg): + sys.exit("craft is not moving at the start — not in flight, or already dead") + u_i, w_i = pin_rows(w, off, cfg) rows = [] print(f"# {'input':<6} {'roll':>9} {'yaw':>9} {'pitch':>9} (deg over the dwell)") @@ -115,6 +160,10 @@ def main(): prev = cur pad_state() wall = time.time() - t0 + if not alive(w, off, cfg): + print(f"# CRAFT STOPPED MOVING after {label} — everything from here is " + f"meaningless, aborting rather than reporting zeros") + break rate = [v / wall for v in sw] rows.append((label, *[round(v, 2) for v in rate])) print(f"# {label:<6} {rate[0]:9.1f} {rate[1]:9.1f} {rate[2]:9.1f} deg/wall-s") diff --git a/tools/re-capture/fly_stage.sh b/tools/re-capture/fly_stage.sh index 40cc5e5..c39f110 100755 --- a/tools/re-capture/fly_stage.sh +++ b/tools/re-capture/fly_stage.sh @@ -76,6 +76,21 @@ 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 +# Wait for a screen instead of sleeping a guess. The fixed sleeps below used to be +# enough and then were not: one run's stage load ran long, the script pressed START +# into a black loading screen, and every later step went to nothing while the shots +# recorded a plausible-looking sequence. Same lesson as the menu oracles. +wait_not_black() { # $1 = timeout s + for _ in $(seq 1 "$1"); do + screenshot /tmp/nav-probe.png >/dev/null 2>&1 || { sleep 1; continue; } + read -r r g b < <(px "640,360") + read -r r2 g2 b2 < <(px "200,200") + if [ -n "${r:-}" ] && [ $((r + g + b + r2 + g2 + b2)) -gt 60 ]; then return 0; fi + sleep 1 + done + return 1 +} + # Selecting a stage lands on the mission BRIEFING (A: Continue), which leads to the # READY ROOM (TAKE OFF / BRIEFINGS / HANGAR / ...) with BRIEFINGS pre-highlighted. # A snapshot taken at the briefing yields ZERO unit-definition objects — they are @@ -83,6 +98,14 @@ pad tap A 0.20; sleep 3; shot 05-after-A2 # START (Skip) clears the briefing. `A` does NOT: it pages through the brief and # ten taps still left the run sitting on a briefing screen, twice. One START press # lands on the READY ROOM. +say "waiting for the stage to finish loading" +if ! wait_not_black 180; then + say "STILL BLACK after 180s — the load hung (this happens; check the log for" + say " 'PhysicalHeap::Release failed'), aborting instead of pressing into nothing" + shot 06-stuck-black + exit 2 +fi +sleep 3 say "skipping the briefing (START, not A)" pad tap START 0.30; sleep 6 shot 06-readyroom