#!/usr/bin/env python3 """Ask questions of a ctrl_probe.py capture: which words answer to which input? The capture is a window of the player entity object sampled every tick, tagged with the pad state that produced it. That makes the interesting question mechanical: for each 4-byte offset, does its value during phase X differ from its value during the rest phases either side? A word that only moves while `RT` is held is that input's state — a throttle setting, an afterburner tank, a heat gauge — and one that moves in *every* phase is just live physics. Sub-commands phases per-phase mean of every offset that moves at all respond offsets that move during and not at rest near [tol] offsets whose first sample is ~= value (HUD anchor) trace [off...] full time series of specific offsets (pos-relative hex) """ import os import struct import sys import numpy as np HDR = b"SYLPHCTR" REC = struct.Struct(" 1e-3)) order = idx[np.argsort(-mv[idx])][:int(args[0]) if args else 25] print("offset " + "".join(f"{p[:8]:>10}" for p in phases)) for i in order: print(f"{label(d, i):<10}" + "".join(f"{means[p][i]:10.2f}" for p in phases)) def cmd_respond(d, args): want = args[0] F, ok = d["F"], finite(d) inp = np.array([p == want for p in d["phase"]]) rest = np.array([p.startswith("rest") or p == "base" for p in d["phase"]]) if not inp.any(): sys.exit(f"no phase {want!r}") # A word answering to this input must move *while it is held* and be quiet # at rest; a word that also moves at rest is live physics, not the input. a = F[inp] r = F[rest] d_in = a.max(axis=0) - a.min(axis=0) d_rest = r.max(axis=0) - r.min(axis=0) score = d_in - 2.0 * d_rest idx = np.flatnonzero(ok & (d_in > 1e-3) & (score > 0)) for i in idx[np.argsort(-score[idx])][:20]: print(f"{label(d, i):<10} in-phase {a[:, i].min():12.3f}..{a[:, i].max():12.3f}" f" at-rest {r[:, i].min():12.3f}..{r[:, i].max():12.3f}") if not len(idx): print("(nothing moves under this input that is quiet at rest)") def cmd_near(d, args): v = float(args[0]) tol = float(args[1]) if len(args) > 1 else max(1e-3, abs(v) * 1e-3) F, U = d["F"], d["U"] for i in np.flatnonzero(np.abs(F[0] - v) <= tol): print(f"{label(d, i):<10} f32 {F[0, i]:.4f} -> {F[-1, i]:.4f}") for i in np.flatnonzero(np.abs(U[0].astype(np.float64) - v) <= tol): print(f"{label(d, i):<10} u32 {U[0, i]} -> {U[-1, i]}") def cmd_trace(d, args): offs = [int(a, 0) for a in args] idx = [(o + d["back"]) // 4 for o in offs] print("t phase speed " + " ".join(f"{o:+#07x}" for o in offs)) for k in range(d["n"]): print(f"{d['t'][k]:6.2f} {d['phase'][k]:<12} {d['speed'][k]:6.0f} " + " ".join(f"{d['F'][k, i]:8.2f}" for i in idx)) def main(): d = load(sys.argv[1]) print(f"# {d['n']} ticks, window {d['win']:#x} bytes, back {d['back']:#x}") cmd = sys.argv[2] if len(sys.argv) > 2 else "phases" {"phases": cmd_phases, "respond": cmd_respond, "near": cmd_near, "trace": cmd_trace}[cmd](d, sys.argv[3:]) if __name__ == "__main__": main()