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:
144
tools/re-capture/approach_capture.py
Executable file
144
tools/re-capture/approach_capture.py
Executable 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()
|
||||
Reference in New Issue
Block a user