diff --git a/docs/re/flight-speed-law.md b/docs/re/flight-speed-law.md index 54ac8f8..284dc21 100644 --- a/docs/re/flight-speed-law.md +++ b/docs/re/flight-speed-law.md @@ -100,3 +100,34 @@ pitching — but the yaw fields may equally belong to the AI or to an input this has not driven. Raw samples: [`captures/turn-law-pitch-phases.csv`](captures/turn-law-pitch-phases.csv). + +--- + +# The afterburner is not on the obvious buttons — a bounded negative + +The definition describes the burner without naming its input: `AB_ConsumeShield_Begin` +**50**, `AB_ConsumeShield` **10**, and a set of `AB_AV_*` turn caps far below the +normal ones (roll 40 vs 125–200 °/s, pitch-up 30 vs 70–150). Notably there is **no +`AB_*Velocity`**, so the burner may not raise the speed target at all. + +Two independent probes, both negative for `A`, `B`, `X`, `LB` (and `LS`/`RS` on the +first): + +* [`tools/re-capture/ab_probe.py`](../../tools/re-capture/ab_probe.py) — hold `RT` + for a max-speed baseline, then hold each candidate: speed stayed inside the + baseline's own noise band (1 200–1 580 units/s) for every one. +* [`tools/re-capture/ab_state_probe.py`](../../tools/re-capture/ab_state_probe.py) — + sample a window of the player object while each candidate is held and report any + float that falls during the hold: **nothing fell**. +* And the cheapest oracle of the three, needing no offsets at all: **count the green + pixels of the HUD's SHIELD bar** before and after each hold, on a freshly spawned + craft with a full shield. `AB_ConsumeShield_Begin 50` should take a visible bite; + the bar read **156, 156, 156, 157, 157** across baseline and all four buttons. + +So the burner is **not a simple hold on `A`/`B`/`X`/`LB`/`LS`/`RS`**. What remains: +a chord (something plus `RT`), an input this pad cannot reach, a craft variant that +has it, or fields that belong to the AI rather than the player. Worth knowing before +anyone re-probes the obvious buttons. + +(`RB` is the nose gun, `Y` the missile mount and the d-pad the tactical map — see +[flight controls](flight-controls-runtime.md) — so those were not held here.) diff --git a/tools/re-capture/ab_probe.py b/tools/re-capture/ab_probe.py new file mode 100644 index 0000000..32ce5b6 --- /dev/null +++ b/tools/re-capture/ab_probe.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Which input is the AFTERBURNER, and what does it do? + +The definition carries `AB_ConsumeShield_Begin 50`, `AB_ConsumeShield 10` and a set +of `AB_AV_*` turn caps far below the normal ones (roll 40 vs 125-200 °/s, pitch-up +30 vs 70-150) — so the burner should cost shield and cut agility. It carries no +`AB_*Velocity`, so whether it raises the speed target at all is an open question. + +Method: hold `RT` (max-speed baseline), then hold each candidate button in turn and +measure speed over 1-second windows. A button that pushes speed past the RT plateau +is the burner. Buttons known to do something else are skipped: `RB` is the nose gun, +`Y` the missile mount, the d-pad the tactical map (see flight-controls-runtime.md). + +Usage: ab_probe.py [hold_s] +""" +import math, os, subprocess, sys, time +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import speed_law + +def pad(*a): + subprocess.run(["vgamepad", *a], capture_output=True) + +def windowed(seq, lo, hi): + a = [s for s in seq if s[0] >= lo][0] + b = [s for s in seq if s[0] <= hi][-1] + return math.dist(a[1:], b[1:]) / (b[0] - a[0]) + +def main(): + out_csv = sys.argv[1] + hold = float(sys.argv[2]) if len(sys.argv) > 2 else 5.0 + w, off, nm = speed_law.find_player() + if not w: + sys.exit("player entity not found after retries") + print(f"# locked on {nm}") + rows = [] + pad("trig", "RT", "1.0") # max-speed baseline for every phase + time.sleep(2.0) + for label in ("baseline", "A", "B", "X", "LB", "baseline2"): + if label.startswith("baseline"): + pass + else: + pad("press", label) + seq, t0 = [], time.time() + while time.time() - t0 < hold: + p = speed_law.pos_at(w.fd, off) + seq.append((round(time.time() - t0, 3), *p)) + time.sleep(0.05) + if not label.startswith("baseline"): + pad("release", label) + for s in seq: + rows.append((label, *s)) + try: + v = [windowed(seq, t, t + 1.0) for t in range(int(hold) - 1)] + print(f"# {label:<10} speed per 1 s window: " + " ".join(f"{x:6.0f}" for x in v)) + except Exception as e: + print(f"# {label:<10} failed: {e}") + pad("trig", "RT", "0.0"); pad("reset") + with open(out_csv, "w") as f: + f.write("phase,t,x,y,z\n") + for r in rows: + f.write(",".join(str(x) for x in r) + "\n") + print(f"# wrote {out_csv}") + +if __name__ == "__main__": + main() diff --git a/tools/re-capture/ab_state_probe.py b/tools/re-capture/ab_state_probe.py new file mode 100644 index 0000000..8185539 --- /dev/null +++ b/tools/re-capture/ab_state_probe.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Find the afterburner by what it COSTS, not by what it speeds up. + +`ab_probe.py` held A/B/X/LB at full throttle and none of them moved the speed — +so either the burner is a different input or it does not raise the speed target +(the definition has no `AB_*Velocity`, only `AB_AV_*` turn caps and +`AB_ConsumeShield{,_Begin}` 10 / 50). This looks for the cost instead: sample a +window of the player object while each candidate is held, and report any float +that FALLS during the hold and not before it — `AB_ConsumeShield_Begin 50` should +be a visible step. + +Usage: ab_state_probe.py [hold_s] +""" +import os, struct, subprocess, sys, time +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import speed_law + +WIN_LO, WIN_HI = 0x100, 0x220 # object window, relative to the position triple + +def pad(*a): + subprocess.run(["vgamepad", *a], capture_output=True) + +def snap(fd, off): + b = os.pread(fd, WIN_HI - WIN_LO, off + WIN_LO) + return [struct.unpack_from(">f", b, i)[0] for i in range(0, len(b) - 3, 4)] + +def main(): + hold = float(sys.argv[1]) if len(sys.argv) > 1 else 4.0 + w, off, nm = speed_law.find_player() + if not w: + sys.exit("player entity not found after retries") + print(f"# locked on {nm}; window {WIN_LO:#x}..{WIN_HI:#x}") + pad("trig", "RT", "1.0") + time.sleep(1.5) + for btn in ("LS", "RS", "A", "B", "X", "LB"): + before = snap(w.fd, off) + pad("press", btn) + time.sleep(hold) + during = snap(w.fd, off) + pad("release", btn) + time.sleep(1.0) + after = snap(w.fd, off) + drops = [] + for i, (b, d, a) in enumerate(zip(before, during, after)): + # a float that fell while held and did not fall further after release + if b - d > 1.0 and (a - d) > -1.0 and abs(b) < 1e6: + drops.append((WIN_LO + i * 4, round(b, 1), round(d, 1), round(a, 1))) + tag = ", ".join(f"{o:#05x}: {b}->{d}->{a}" for o, b, d, a in drops[:4]) or "nothing fell" + print(f"# {btn:<3} {tag}") + pad("trig", "RT", "0.0"); pad("reset") + +if __name__ == "__main__": + main()