Files
Syplheed-Reborn/tools/re-capture/flight_analyze.py
Claude (auto-RE) 72365b217a re: memory-driven autopilot -- infrastructure, and an honest account of what is not solved
Reads the live world out of guest RAM and drives the pad from it. Working:
loop-rate memory reads, whole-RAM float scanning with numpy (1270 orthonormal
3x3 blocks in 6.2 s), entity enumeration by unit type (116 live instances in
Stage 02), pad control written straight into the vgamepad FIFO (the CLI spawns
a process per command and its tap/hold sleep inside the server, so neither is
usable in a control loop), unattended mission entry, and the Hangar loadout --
the "Recommended" control is AUTO SELECT, which at 5 % progress is a no-op
because only two weapons are developed and both are already mounted.

Not working, and the reason the craft is not yet flown: the class 0x820af030
is NOT the live entity. It has one object per spawned thing and carries the
unit-ID string, which is why it looked like the entity list, but every one of
its 384 words is constant across a 29 s in-flight capture. No transform lives
in it or one pointer hop from it. Input correlation (hard left yaw vs hard
right, looking for a turn axis that reverses) does find self-like objects at
cos = -0.99, but they cluster in what looks like a camera volume rather than
the craft, and with no definition pointer near them the trick of learning one
entity's layout and applying it to the rest has nothing to anchor on -- so the
33418 moving triples in a firefight cannot be split into enemies, friendlies
and bullets, and there is nothing to aim at.

Two dead ends are recorded so they are not repeated: RT is not the throttle
(the two-state speed scan therefore found nothing), and comparing orientation
matrices 2 s apart is outside the small-angle regime, which is what produced
"angular velocities" of 30000.

Also corrects the claim in unit-struct-runtime.md that 0x820af030 holds live
state. The definition class 0x820af844 and every value derived from it are
unaffected.

autopilot2.py (a PD controller using body angular velocity from consecutive
rotation matrices) is committed but has never had a valid config to run
against, and is marked as untested.
2026-07-29 19:04:33 +00:00

158 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""Derive the player craft's live transform offsets from a flight_probe capture.
Nothing here is guessed from plausible-looking numbers. Two independent
structural facts do the work:
1. a rotation matrix is 9 floats that are orthonormal with det +1 — a
property essentially no other data has;
2. a position triple's frame-to-frame delta must point along the craft's
own forward axis when it is flying straight, and its magnitude must be the
same for every frame at constant speed.
Test 2 validates the position candidate *and* the rotation candidate against
each other, so a coincidence has to satisfy both at once.
Usage: flight_analyze.py <probe.bin> [name-substring]
"""
import math
import os
import struct
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import gworld # noqa: E402
def load(path):
f = open(path, "rb")
assert f.read(8) == b"SYLPHPRB"
n_inst, window, n_in = struct.unpack("<III", f.read(12))
insts = []
for _ in range(n_inst):
va, off = struct.unpack("<IQ", f.read(12))
nm = f.read(64).split(b"\0")[0].decode()
insts.append((va, off, nm))
frames = []
rec = 8 + 4 * n_in + 32 + n_inst * window
while True:
b = f.read(rec)
if len(b) < rec:
break
t = struct.unpack_from("<d", b, 0)[0]
inp = struct.unpack_from("<%df" % n_in, b, 8)
label = struct.unpack_from("<32s", b, 8 + 4 * n_in)[0].split(b"\0")[0].decode()
base = 8 + 4 * n_in + 32
bufs = [b[base + i * window: base + (i + 1) * window] for i in range(n_inst)]
frames.append((t, inp, label, bufs))
return insts, window, frames
def f32(buf, o):
return struct.unpack_from(">f", buf, o)[0]
def vec(buf, o):
return (f32(buf, o), f32(buf, o + 4), f32(buf, o + 8))
def sub(a, b):
return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
def norm(a):
return math.sqrt(sum(c * c for c in a))
def dot(a, b):
return sum(x * y for x, y in zip(a, b))
def analyse(insts, window, frames, want):
idx = [i for i, (_, _, nm) in enumerate(insts) if want in nm]
if not idx:
sys.exit(f"no instance matching {want!r}; have: "
+ ", ".join(sorted({nm for _, _, nm in insts})))
i = idx[0]
va, off, nm = insts[i]
print(f"# player instance {va:#010x} {nm} ({len(frames)} frames)")
# ---- rotation blocks that hold up across every frame ------------------
per_frame = []
for t, inp, lab, bufs in frames:
per_frame.append({o for o, kind, m in gworld.find_rotations(bufs[i]) if kind == "3x3"})
stable = set.intersection(*per_frame) if per_frame else set()
print(f"# 3x3 orthonormal blocks valid in ALL frames: "
+ (", ".join(f"{o:#05x}" for o in sorted(stable)) or "none"))
# ---- position candidates ---------------------------------------------
# constant-speed straight flight: |delta| equal every frame, direction fixed
idle = [k for k, (t, inp, lab, b) in enumerate(frames) if lab.startswith("idle")]
if len(idle) < 6:
idle = list(range(min(20, len(frames))))
seg = idle[:20]
cands = []
for o in range(0, window - 12, 4):
ds, ok = [], True
for a, b in zip(seg, seg[1:]):
if b != a + 1:
continue
dt = frames[b][0] - frames[a][0]
p0, p1 = vec(frames[a][3][i], o), vec(frames[b][3][i], o)
if not all(map(math.isfinite, p0 + p1)):
ok = False
break
d = sub(p1, p0)
if dt <= 0:
ok = False
break
ds.append((norm(d) / dt, d))
if not ok or len(ds) < 4:
continue
sp = [s for s, _ in ds]
mean = sum(sp) / len(sp)
if mean < 1.0 or mean > 5000.0: # a craft, not a counter
continue
spread = max(sp) - min(sp)
if spread > 0.15 * mean: # constant speed while coasting
continue
# direction must be steady too
dirs = [(d[0] / (norm(d) or 1), d[1] / (norm(d) or 1), d[2] / (norm(d) or 1))
for _, d in ds]
if min(dot(dirs[0], d) for d in dirs) < 0.99:
continue
cands.append((o, mean, dirs[0]))
print(f"# position candidates (steady speed + steady heading while coasting): "
f"{len(cands)}")
for o, sp, d in cands[:12]:
print(f" +{o:#05x} speed {sp:8.2f}/s dir ({d[0]:+.3f},{d[1]:+.3f},{d[2]:+.3f})")
# ---- cross-validate: velocity must lie along a rotation row -----------
print("\n# cross-check: does the motion direction match a rotation-matrix axis?")
best = []
for o, sp, d in cands:
for ro in sorted(stable):
m = struct.unpack_from(">9f", frames[seg[0]][3][i], ro)
for r, label in ((m[0:3], "row0/right"), (m[3:6], "row1/up"), (m[6:9], "row2/fwd")):
c = abs(dot(d, r))
if c > 0.98:
best.append((c, o, ro, label, sp))
best.sort(reverse=True)
if not best:
print(" none — position and orientation candidates do not corroborate")
for c, o, ro, label, sp in best[:10]:
print(f" pos +{o:#05x} ∥ rot +{ro:#05x} {label} |cos|={c:.5f} speed {sp:.1f}")
return stable, cands
def main():
path = sys.argv[1]
want = sys.argv[2] if len(sys.argv) > 2 else "Player"
insts, window, frames = load(path)
analyse(insts, window, frames, want)
if __name__ == "__main__":
main()