Files
Syplheed-Reborn/tools/re-capture/own_state.py
Claude (auto-RE) 2e9903d0fc re: the autopilot now survives, and kills
Three measurements, then a pilot built on them.

* Hull is position+0x154. Found by anchoring on a field the definition
  already had solved (HP = 1500) rather than scanning for a value that
  falls: an undamaged craft must contain its own definition's number.
  Confirmed by the trace across a death -- 30/60/90 per hit, negative
  at 0, GAME OVER on screen.
* RT accelerates, LT brakes, and the throttle is a persistent setting
  (488 -> 1510 -> 174 units/s, measured as displacement per second of
  the craft's own position, so no speed field was needed). This
  overturns the earlier "RT is not the throttle", which came from
  assuming the control and hunting for a field.
* Shield is probably position+0x430 (== definition MaxValue 400), not
  yet confirmed live -- nothing had damaged it.

pilot.py is a state machine on damage (ENGAGE / EVADE / RETIRE) that
treats turrets as keep-out zones instead of targets. It flew Stage 02
for 300 s with the hull untouched at 1500/1500 and took the first
confirmed kill (WARPLANES 0001); every run the day before was dead
inside 35 s.

Two bugs the live run exposed and this fixes: gating the guns on the
commanded direction keeps them cold whenever avoidance is steering
(gate on the target instead), and an orbit-plus-brake rule made it
circle one attacker for 40 s outside its own firing cone.

Also: boot to in-flight is now ~100 s unattended, because
wait_flight.sh waits for the HUD's own shield bar instead of a fixed
75 s sleep that lavapipe does not honour; entities2.py picks the
attitude block by matching the measured flight path (taking the first
orthonormal block gave a bone/camera frame); and the entity-heap scan
is numpy instead of a per-word Python loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 05:38:00 +00:00

118 lines
4.2 KiB
Python

#!/usr/bin/env python3
"""Where does the craft keep its hull and shield? Anchor on the definition.
The parsed unit definition already has solved fields (unit-struct-runtime.md):
`HP` at `+0x054`, shield `MaxValue` at `+0x238`, shield `ChargeSpeed` at
`+0x244`. A craft that has taken no damage is at full hull and full shield, so
its live entity object must *contain those very numbers*. That turns "find the
HP field" from a value-scan over 4 GB into: read two floats from the definition,
then look for them inside the entity object.
Then watch the candidates while the craft is under fire. The live field is the
one that falls; a copy of the definition value that never moves is not it.
Usage: own_state.py <config.json> [watch_seconds] [out.json]
"""
import json
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
import navigator # noqa: E402
DEF_HP, DEF_SHIELD_MAX, DEF_SHIELD_CHG = 0x054, 0x238, 0x244
BACK, FWD = 0x800, 0x800
def near(a, v):
return np.abs(a - v) <= max(1e-3, abs(v) * 1e-4)
def main():
cfg = json.load(open(sys.argv[1]))
secs = float(sys.argv[2]) if len(sys.argv) > 2 else 40.0
out = sys.argv[3] if len(sys.argv) > 3 else None
W = navigator.World(cfg)
ents = W.scan()
me = [(off, va) for off, va in ents if "Player" in W.defs[va]]
if not me:
sys.exit("player entity not found — not in flight?")
me_off, def_va = me[0]
print(f"# player {W.defs[def_va]} pos off {me_off:#x} def {def_va:#010x}")
d = os.pread(W.fd, 0x300, gmem.va_to_off(def_va))
want = {}
for nm, o in (("HP", DEF_HP), ("Shield_MaxValue", DEF_SHIELD_MAX),
("Shield_ChargeSpeed", DEF_SHIELD_CHG)):
(v,) = struct.unpack_from(">f", d, o)
want[nm] = v
print(f"# definition {nm:<18} = {v:g}")
blob = os.pread(W.fd, BACK + FWD, me_off - BACK)
arr = np.frombuffer(blob, dtype=">f4").astype(np.float64)
cands = [] # (label, delta_from_position)
for nm, v in want.items():
if not np.isfinite(v) or v == 0.0:
continue
for i in np.flatnonzero(near(arr, v)):
cands.append((nm, int(i) * 4 - BACK))
print(f"# {len(cands)} candidate offsets inside the entity object:")
for nm, dlt in cands:
print(f" pos{dlt:+#07x} == definition {nm}")
if not cands:
print("# none — the object does not carry the definition's own numbers"
" at this offset window; widen BACK/FWD or the craft is damaged")
# ---- watch them; the live field is the one that moves
hist = {c: [] for c in cands}
t0 = time.time()
last_print = 0.0
while time.time() - t0 < secs:
p = W.pos(me_off)
if p is None:
print("# player object gone (death / stage change?)")
break
w = os.pread(W.fd, BACK + FWD, me_off - BACK)
a = np.frombuffer(w, dtype=">f4").astype(np.float64)
for c in cands:
i = (c[1] + BACK) // 4
hist[c].append(float(a[i]))
t = time.time() - t0
if t - last_print > 4.0:
last_print = t
cur = " ".join(f"{c[0][:4]}{c[1]:+#x}={hist[c][-1]:.1f}" for c in cands[:6])
print(f"[{t:6.1f}] {cur}", flush=True)
time.sleep(0.2)
print("\n# offset anchor first last min moved")
moving = []
for c in cands:
h = np.array(hist[c])
if not len(h):
continue
mv = float(h.max() - h.min())
print(f" pos{c[1]:+#07x} {c[0]:<18} {h[0]:8.1f} {h[-1]:8.1f} "
f"{h.min():8.1f} {mv:7.3f}")
if mv > 1e-3:
moving.append({"anchor": c[0], "delta": c[1],
"first": h[0], "last": float(h[-1]),
"min": float(h.min())})
if not moving:
print("# nothing moved — the craft took no damage during the window")
if out:
json.dump({"player": W.defs[def_va], "def_va": def_va,
"definition": want,
"candidates": [{"anchor": a, "delta": b} for a, b in cands],
"moved": moving}, open(out, "w"), indent=1)
print(f"# wrote {out}")
if __name__ == "__main__":
main()