Files
Syplheed-Reborn/tools/re-capture/mission_state.py
claude-re 91fb022caf re-capture: a dead display must not look like a playing movie
skip_intro.sh compared two screenshots to detect the intro movie. When the X
server died 3 min into a run, `screenshot` failed silently and left both PNGs
at their previous contents — two stale files, whose RMSE is a constant non-zero
number, i.e. exactly the signature of a changing screen. The loop then reported
"movie -> skip A" every 5 s for the whole 600 s timeout with no emulator and no
display alive, and the session wasted 10 minutes before saying BOOT FAILED.

Both waiters now verify, every iteration, that the screenshot was actually
written, that the display answers xdpyinfo, and that a non-zombie xenia_canary
exists — with distinct exit codes (3 display, 4 emulator, 5 capture) so the
session log names the cause instead of timing out.

Also adds mission_state.py + escort_session.sh: read pos+0x154 against each
definition's HP for EVERY entity, to test whether the hull anchor is a property
of the entity class rather than of the player object (the escort question).
2026-07-30 17:06:08 +00:00

168 lines
6.0 KiB
Python
Executable File

#!/usr/bin/env python3
"""Mission state from guest RAM: every entity's hull, not just the player's.
`own_state.py` found the player's hull by anchoring on a solved definition
field: an undamaged craft carries its definition's own `HP` (+0x054), so the
live counter is the copy of that number that *falls*. The result was
`hull = position + 0x154`.
The escort question needs the same number for **someone else's** ship. Stage 02
is lost when the ACROPOLIS sinks, not when the player dies, and the 240 s run in
autopilot-memory-driven.md hit GAME OVER with our own hull untouched. So the
claim to test here is that `+0x154` is a property of the *entity class*, not of
the player object: every entity, hostile or friendly, fighter or capital ship,
should hold its own definition's `HP` there at spawn and lose it when hit.
That is falsifiable in one run: read `pos+0x154` and the definition `HP` for
every entity in the scene and compare. If the anchor is class-wide, the ratio
is 1.0 for everything undamaged and nothing else lines up by accident; if it is
player-specific, most entities hold something unrelated.
Sub-commands:
scan [cfg] one table of every entity: hull vs definition HP
watch <cfg> <secs> [hz] [out] sample over time; report what took damage
"""
import json
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
import navigator # noqa: E402
HULL_OFF = 0x154 # own_state.py, player craft
DEF_HP = 0x054 # unit-struct-runtime.md, ✅ CONFIRMED
DEF_SIZE_R = 0x50
def f32(fd, off):
b = os.pread(fd, 4, off)
if len(b) < 4:
return float("nan")
(v,) = struct.unpack(">f", b)
return v
class Mission:
def __init__(self, cfg):
self.W = navigator.World(cfg)
self.def_hp = {}
for va in self.W.defs:
self.def_hp[va] = f32(self.W.fd, gmem.va_to_off(va) + DEF_HP)
def snapshot(self):
"""[(off, name, faction, radius, pos, hull, hp_max)] for every entity."""
out = []
for off, va in self.W.ents:
p = self.W.pos(off)
if p is None:
continue
hull = f32(self.W.fd, off + HULL_OFF)
nm = self.W.defs[va]
out.append((off, nm, navigator.faction(nm), self.W.radius[va],
p, hull, self.def_hp.get(va, float("nan"))))
return out
def fmt(e):
off, nm, fac, r, p, hull, hp0 = e
frac = hull / hp0 if hp0 and math.isfinite(hp0) and hp0 > 0 else float("nan")
return (f"{gmem.primary_va(off):#010x} {nm[:34]:<34} {fac:<4} r={r:6.0f} "
f"({p[0]:+8.0f},{p[1]:+8.0f},{p[2]:+8.0f}) hull={hull:9.1f} "
f"HP={hp0:9.1f} frac={frac:6.3f}")
def cmd_scan(cfg):
m = Mission(cfg)
m.W.scan()
ents = m.snapshot()
print(f"# {len(ents)} entities")
ok = sum(1 for e in ents
if math.isfinite(e[6]) and e[6] > 0 and abs(e[5] / e[6] - 1.0) < 1e-3)
fin = sum(1 for e in ents if math.isfinite(e[6]) and e[6] > 0)
print(f"# hull(+{HULL_OFF:#x}) == definition HP for {ok}/{fin} entities "
f"whose definition has an HP")
for e in sorted(ents, key=lambda e: -e[3]):
print(" " + fmt(e))
def cmd_watch(cfg, secs, hz, out):
m = Mission(cfg)
m.W.scan()
base = {e[0]: e for e in m.snapshot()}
print(f"# watching {len(base)} entities for {secs:g}s at {hz:g}Hz", flush=True)
fh = open(out, "w") if out else None
t0 = time.time()
last_scan = t0
last_seen = {}
while time.time() - t0 < secs:
t = time.time()
if t - last_scan > 15.0: # new waves spawn; re-enumerate rarely
m.W.scan()
last_scan = t
for e in m.snapshot():
base.setdefault(e[0], e)
ents = m.snapshot()
rec = {"t": round(t - t0, 2),
"n": len(ents),
"adan": sum(1 for e in ents if e[2] == "ADAN"),
"tcaf": sum(1 for e in ents if e[2] == "TCAF"),
"ents": []}
for e in ents:
off, nm, fac, r, p, hull, hp0 = e
b = base.get(off)
drop = (b[5] - hull) if b and math.isfinite(b[5]) else 0.0
big = r >= navigator.Navigator.BIG_RADIUS
if big or drop > 0.5:
rec["ents"].append({"va": gmem.primary_va(off), "nm": nm,
"fac": fac, "r": round(r, 1),
"hull": round(hull, 1), "hp0": round(hp0, 1),
"drop": round(drop, 1),
"pos": [round(float(c), 1) for c in p]})
if drop > 0.5 and abs(last_seen.get(off, 0.0) - hull) > 0.5:
last_seen[off] = hull
print(f"[{t-t0:6.1f}] HIT {nm[:30]:<30} {fac} "
f"hull {hull:9.1f}/{hp0:9.1f} (-{drop:.0f})", flush=True)
if fh:
fh.write(json.dumps(rec) + "\n")
fh.flush()
time.sleep(max(0.0, 1.0 / hz - (time.time() - t)))
if fh:
fh.close()
print("\n# damage summary")
ents = {e[0]: e for e in m.snapshot()}
for off, b in sorted(base.items(), key=lambda kv: -kv[1][3]):
cur = ents.get(off)
gone = cur is None
hull = cur[5] if cur else float("nan")
if not gone and abs(hull - b[5]) < 0.5 and b[3] < navigator.Navigator.BIG_RADIUS:
continue
print(f" {b[1][:34]:<34} {b[2]:<4} r={b[3]:6.0f} "
f"{b[5]:9.1f} -> {hull:9.1f}" + (" GONE" if gone else ""))
def main():
if len(sys.argv) < 3:
sys.exit(__doc__)
cmd = sys.argv[1]
cfg = json.load(open(sys.argv[2]))
if cmd == "scan":
cmd_scan(cfg)
elif cmd == "watch":
cmd_watch(cfg,
float(sys.argv[3]) if len(sys.argv) > 3 else 120.0,
float(sys.argv[4]) if len(sys.argv) > 4 else 1.0,
sys.argv[5] if len(sys.argv) > 5 else None)
else:
sys.exit(__doc__)
if __name__ == "__main__":
main()