re: the Stage-02 capture drew no capital ship at all — invert the match, then control range

Inverting the capture↔part question (invert_capture over one container,
vcount_index over all 166) identifies every large draw in the 2026-07-31
capture: the player's own DeltaSaber (10891 verts), its weapon packs, the
backdrop and particles. Of f101/e105/e106 only 1-3 of 15-37 resources have a
drawn vcount, each a 44-225-vertex far-LOD/effect piece whose count collides
with dozens of unrelated resources.

So the zero-correlation was not an LOD-list gap, not over-strict position
validation and not a different draw path: the ships were too far away to be
drawn. approach_capture.py flies at a locked capital ship and presses F10 per
range band, stamping each capture with its distance.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-10 18:04:58 +00:00
parent bbfeb1c387
commit 1d4b35df0f
5 changed files with 499 additions and 3 deletions

View File

@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Fly TO a capital ship and dump a draw capture at several ranges.
Why this exists: the 2026-07-31 Stage-02 capture correlated **zero** parts, and
inverting the match (`cargo run --example invert_capture`) showed why — at the
captured frames no capital-ship hull was drawn at all. The only large draw was
the player's own craft (`DeltaSaber_T:f001`, 10891 verts); of `f101`/`e105`/
`e106` only a handful of tiny far-LOD/effect pieces appeared. The ships were
simply too far away. Pressing F10 wherever the craft happens to be is therefore
not a capture strategy.
So: pick a capital ship, fly at it, and press F10 as each distance band is
crossed. That gives (a) frames where the full-detail hull is actually drawn —
what the correlator needs — and (b) as a by-product, the game's own **LOD
ladder**, because each capture is stamped with the range it was taken at.
Firing is disabled (the target is usually a friendly), and navigator.py's
closest-point-of-approach avoidance is inherited unchanged, so closing on a hull
does not end in a collision.
Usage: approach_capture.py <config.json> [seconds] [--target REGEX] [--dry]
Env: SYLPH_CAPTURE_WIN xdotool window id to send F10 to (unset = no capture)
SYLPH_CAPTURE_OUT where to write the band log and screenshots
"""
import json
import math
import os
import re
import subprocess
import sys
import time
from collections import Counter
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import navigator # noqa: E402
from navigator import Navigator, ang, norm # noqa: E402
from flight_probe import Pad # noqa: E402
# Ranges (guest units) at which to dump a capture, largest first. Chosen to
# straddle the plausible LOD switches: the far-LOD pieces seen in the 2026-07-31
# capture were drawn at whatever range the craft sat at, and the one validated
# capture (e106, Stage_S01) had the ship close.
BANDS = [8000.0, 6000.0, 4500.0, 3000.0, 2000.0, 1400.0, 900.0]
# A capital ship, not a fighter: the definition's own size radius says which.
CAPITAL_RADIUS = 150.0
class Approach(Navigator):
# Never shoot: the approach target is usually the escorted asset, and a
# negative cone makes the inherited fire gate unsatisfiable.
FIRE_CONE = -1.0
HOLD = 700.0 # stop closing inside this; the capture is already made
def __init__(self, W, pad, target_re=None, dry=False, log=sys.stdout,
win=None, out=None):
super().__init__(W, pad, dry=dry, log=log)
self.target_re = re.compile(target_re, re.I) if target_re else None
self.win = win
self.out = out or "/sylph-home/re/shipcap"
self.locked = None # (off, name) — stay on one ship
self.pending = list(BANDS)
self.captures = []
self.throttle = None
# -------------------------------------------------------------- target
def pick(self, me_p, me_v, fwd, ents, me_off):
"""The chosen capital ship — locked once, so the run is one approach."""
cands = [e for e in ents
if e[0] != me_off and "Player" not in e[1] and e[4] >= CAPITAL_RADIUS
and (self.target_re is None or self.target_re.search(e[1]))]
if not cands:
return None
if self.locked is not None:
same = [e for e in cands if e[0] == self.locked]
if same:
e = same[0]
return (e[0], e[1], e[2], e[2] - me_p, float(np.linalg.norm(e[2] - me_p)))
# First lock: the biggest ship that is not absurdly far.
cands.sort(key=lambda e: (-e[4], float(np.linalg.norm(e[2] - me_p))))
e = cands[0]
self.locked = e[0]
print(f"LOCK {e[1]} radius={e[4]:.0f} d={np.linalg.norm(e[2]-me_p):.0f}",
file=self.log, flush=True)
return (e[0], e[1], e[2], e[2] - me_p, float(np.linalg.norm(e[2] - me_p)))
# ------------------------------------------------------------- capture
def capture(self, band, dist, name):
idx = len(self.captures) + 1
shot = f"{self.out}/approach-{idx:02d}.png"
if self.win:
subprocess.run(["screenshot", shot], capture_output=True)
subprocess.run(["xdotool", "key", "--window", self.win, "F10"],
capture_output=True)
rec = {"index": idx, "band": band, "distance": round(dist, 1),
"target": name, "shot": shot, "t": round(time.time(), 3)}
self.captures.append(rec)
print(f"CAPTURE {idx:02d} band={band:.0f} d={dist:.0f} {name}",
file=self.log, flush=True)
with open(f"{self.out}/approach-bands.jsonl", "a") as f:
f.write(json.dumps(rec) + "\n")
# ---------------------------------------------------------------- loop
def step(self, t, dt, prev_vhat):
msg, vhat = super().step(t, dt, prev_vhat)
# Distance to the locked ship drives both the throttle and the captures.
ents = self.W.sample(t)
me = next((e for e in ents if "Player" in e[1]), None)
tgt = next((e for e in ents if e[0] == self.locked), None) if self.locked else None
if me is None or tgt is None:
return msg, vhat
d = float(np.linalg.norm(tgt[2] - me[2]))
# Throttle: RT to close, LT to hold off once we are as near as we want.
want = 1 if d > self.HOLD * 2 else (-1 if d < self.HOLD else 0)
if want != self.throttle and not self.dry:
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
while self.pending and d <= self.pending[0]:
band = self.pending.pop(0)
self.capture(band, d, tgt[1])
return f"{msg} | d={d:7.0f} thr={want:+d} left={len(self.pending)}", vhat
def main():
cfg = json.load(open(sys.argv[1]))
secs = float(sys.argv[2]) if len(sys.argv) > 2 and not sys.argv[2].startswith("-") else 240.0
target = None
if "--target" in sys.argv:
target = sys.argv[sys.argv.index("--target") + 1]
W = navigator.World(cfg)
a = Approach(W, Pad(), target_re=target, dry="--dry" in sys.argv,
win=os.environ.get("SYLPH_CAPTURE_WIN"),
out=os.environ.get("SYLPH_CAPTURE_OUT"))
a.run(secs)
print(f"CAPTURES {json.dumps(a.captures)}", flush=True)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# ONE blocking session: boot -> Stage 02 in flight -> fly AT a capital ship and
# dump an F10 draw capture at each distance band (approach_capture.py).
#
# Supersedes ship_capture_session.sh for correlation work: that one pressed F10
# wherever the craft happened to be, and the 2026-07-31 run proved that captures
# nothing — inverting the match showed no capital-ship hull was drawn in any of
# those frames, only the player's own craft and particles. Range is the variable
# that matters, so range is what this controls.
#
# Usage: ship_capture_close.sh [seconds] [out_dir] [target_regex]
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)"
SECS="${1:-240}"
OUT="${2:-/sylph-home/re/shipcap-close}"
TARGET="${3:-}"
BINDIR="/home/fabi/RE - Project Sylpheed/xenia-canary-native/build/bin/Linux/Release"
CFG=/tmp/nav-close.json
mkdir -p "$OUT"
rm -f "$BINDIR"/xenia_ship_capture_*.log "$OUT"/approach-bands.jsonl
"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; }
python3 "$SD/entities2.py" self 0x130 "$CFG" || { echo "BIND FAILED"; exit 1; }
echo "--- config: $(cat "$CFG")"
# F10 goes to the emulator window through XTEST; the window must be focused.
win="$(xdotool search --class -- xenia | tail -1)"
[ -z "$win" ] && win="$(xdotool search --name -- Xenia | tail -1)"
echo "WINDOW=$win"
[ -n "$win" ] && { xdotool windowactivate "$win" 2>/dev/null; xdotool windowfocus "$win" 2>/dev/null; }
export SYLPH_CAPTURE_WIN="$win" SYLPH_CAPTURE_OUT="$OUT"
if [ -n "$TARGET" ]; then
python3 "$SD/approach_capture.py" "$CFG" "$SECS" --target "$TARGET" 2>&1 | tail -80
else
python3 "$SD/approach_capture.py" "$CFG" "$SECS" 2>&1 | tail -80
fi
sleep 2
cp -v "$BINDIR"/xenia_ship_capture_*.log "$OUT"/ 2>/dev/null
echo "--- bands ---"; cat "$OUT/approach-bands.jsonl" 2>/dev/null
grep -c '^DRAW' "$OUT"/xenia_ship_capture_*.log 2>/dev/null
echo "CLOSE CAPTURE SESSION DONE"