#!/usr/bin/env python3 """Identify which moving object in RAM is the *player* craft, by input correlation. Position triples are easy to find (findplayer.py); saying which one is us is the hard part, and no static property answers it. This does: hold hard-left yaw for a few seconds, then hard-right, and look for the object whose turn axis reverses in phase with the stick. Every other craft is AI-flown and uncorrelated with our input, so the discriminator is causal rather than circumstantial. Phase 1 finds candidate triples from two whole-RAM samples; after that only the candidates' 12 bytes are re-read, so the sampling loop is cheap. Usage: findself.py [phase_seconds] [hz] """ import math import os import struct import sys import time import numpy as np sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import gmem # noqa: E402 FIFO = "/tmp/sylph-vgamepad.fifo" def pad(line): with open(FIFO, "w") as f: f.write(line + "\n") def snapshot(fd, size): return [(a, np.frombuffer(os.pread(fd, (b - a) // 4 * 4, a), dtype=">f4")) for a, b in gmem.extents(fd, size) if (b - a) >= 64] def candidates(fd, size, dt=0.35): s0 = snapshot(fd, size) t0 = time.time() time.sleep(dt) s1 = snapshot(fd, size) t1 = time.time() out = [] for (o, a), (o2, b) in zip(s0, s1): if o != o2 or len(a) != len(b): continue with np.errstate(invalid="ignore"): af = np.nan_to_num(a.astype(np.float64), nan=0, posinf=0, neginf=0) bf = np.nan_to_num(b.astype(np.float64), nan=0, posinf=0, neginf=0) d = bf - af idx = np.flatnonzero(np.abs(d) > 1e-3) cand = idx[np.isin(idx + 1, idx) & np.isin(idx + 2, idx)] for i in cand: v = np.array([d[i], d[i + 1], d[i + 2]]) sp = float(np.linalg.norm(v)) / (t1 - t0) if 80.0 < sp < 2000.0: out.append(o + int(i) * 4) return out def sample(fd, offs): t = time.time() pos = np.empty((len(offs), 3)) for k, o in enumerate(offs): b = os.pread(fd, 12, o) if len(b) < 12: pos[k] = np.nan continue pos[k] = struct.unpack(">3f", b) return t, pos def turn_axis(series): """Mean normalised cross(v_k, v_k+1) over a phase, per candidate.""" ts = [t for t, _ in series] ps = [p for _, p in series] vs = [] for k in range(len(ps) - 1): dt = ts[k + 1] - ts[k] vs.append((ps[k + 1] - ps[k]) / max(dt, 1e-3)) ax = np.zeros((ps[0].shape[0], 3)) n = 0 for k in range(len(vs) - 1): c = np.cross(vs[k], vs[k + 1]) nrm = np.linalg.norm(c, axis=1, keepdims=True) with np.errstate(invalid="ignore", divide="ignore"): ax += np.where(nrm > 1e-6, c / nrm, 0.0) n += 1 return ax / max(n, 1) def main(): secs = float(sys.argv[1]) if len(sys.argv) > 1 else 3.0 hz = float(sys.argv[2]) if len(sys.argv) > 2 else 6.0 path = gmem.mem_path() fd = os.open(path, os.O_RDONLY) size = os.path.getsize(path) pad("reset") time.sleep(0.5) offs = candidates(fd, size) print(f"# {len(offs)} moving-triple candidates", flush=True) if not offs: sys.exit("nothing is moving — in flight?") phases = {} for name, lx in (("left", -0.9), ("right", 0.9)): pad(f"axis LX {lx}") time.sleep(0.6) # let the turn establish series = [] n = int(secs * hz) for _ in range(n): t0 = time.time() series.append(sample(fd, offs)) time.sleep(max(0, 1.0 / hz - (time.time() - t0))) phases[name] = turn_axis(series) pad("axis LX 0") time.sleep(1.2) print(f"# phase {name} done", flush=True) pad("reset") aL, aR = phases["left"], phases["right"] nL = np.linalg.norm(aL, axis=1) nR = np.linalg.norm(aR, axis=1) with np.errstate(invalid="ignore", divide="ignore"): cos = np.sum(aL * aR, axis=1) / (nL * nR) good = np.isfinite(cos) & (nL > 0.5) & (nR > 0.5) order = np.argsort(np.where(good, cos, 9e9)) print("\n# objects whose turn axis REVERSES with the stick (most negative first)") shown = 0 for k in order: if not good[k]: break print(f" va {gmem.primary_va(offs[k]):#010x} cos(axisL,axisR) = {cos[k]:+.3f}" f" |L|={nL[k]:.2f} |R|={nR[k]:.2f}") shown += 1 if shown >= 12: break if not shown: print(" (none — try longer phases)") if __name__ == "__main__": main()