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).
168 lines
6.0 KiB
Python
Executable File
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()
|