#!/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 [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()