Reads the live world out of guest RAM and drives the pad from it. Working: loop-rate memory reads, whole-RAM float scanning with numpy (1270 orthonormal 3x3 blocks in 6.2 s), entity enumeration by unit type (116 live instances in Stage 02), pad control written straight into the vgamepad FIFO (the CLI spawns a process per command and its tap/hold sleep inside the server, so neither is usable in a control loop), unattended mission entry, and the Hangar loadout -- the "Recommended" control is AUTO SELECT, which at 5 % progress is a no-op because only two weapons are developed and both are already mounted. Not working, and the reason the craft is not yet flown: the class 0x820af030 is NOT the live entity. It has one object per spawned thing and carries the unit-ID string, which is why it looked like the entity list, but every one of its 384 words is constant across a 29 s in-flight capture. No transform lives in it or one pointer hop from it. Input correlation (hard left yaw vs hard right, looking for a turn axis that reverses) does find self-like objects at cos = -0.99, but they cluster in what looks like a camera volume rather than the craft, and with no definition pointer near them the trick of learning one entity's layout and applying it to the rest has nothing to anchor on -- so the 33418 moving triples in a firefight cannot be split into enemies, friendlies and bullets, and there is nothing to aim at. Two dead ends are recorded so they are not repeated: RT is not the throttle (the two-state speed scan therefore found nothing), and comparing orientation matrices 2 s apart is outside the small-angle regime, which is what produced "angular velocities" of 30000. Also corrects the claim in unit-struct-runtime.md that 0x820af030 holds live state. The definition class 0x820af844 and every value derived from it are unaffected. autopilot2.py (a PD controller using body angular velocity from consecutive rotation matrices) is committed but has never had a valid config to run against, and is marked as untested.
204 lines
6.7 KiB
Python
Executable File
204 lines
6.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Live world-state reader: the running game's entities, straight out of guest RAM.
|
|
|
|
Builds on gmem.py (Canary backs the guest address space with
|
|
/dev/shm/xenia_memory_*, so it is an ordinary file). The difference here is that
|
|
this is meant to run *in a loop* while the game plays, not against a snapshot:
|
|
the memory file is opened once and re-read with pread, and the expensive
|
|
whole-RAM vtable scan is done rarely and cached.
|
|
|
|
Two classes matter (see docs/re/structures/unit-struct-runtime.md):
|
|
0x820af844 the parsed `.tbl` definition — one per unit type
|
|
0x820af030 the spawned entity instance — one per thing in the scene
|
|
|
|
This module finds instances and reads their live transform. The transform
|
|
offsets are NOT guessed: `find_transform` scans an instance for a 3x3 block of
|
|
floats that is orthonormal to 1e-3 with det = +1, which a rotation matrix is and
|
|
essentially nothing else is.
|
|
|
|
Sub-commands:
|
|
entities list live instances (name, address)
|
|
probe <seconds> [hz] [out] record every instance's raw bytes over time
|
|
orient report orthonormal 3x3 blocks per instance
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import struct
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import gmem # noqa: E402
|
|
|
|
INST_VTABLE = 0x820AF030
|
|
DEF_VTABLE = 0x820AF844
|
|
WINDOW = 0x600 # bytes of an instance we read; its real size is unknown
|
|
NAME_STR_OFF = 0x10
|
|
|
|
|
|
class World:
|
|
def __init__(self, path=None):
|
|
self.path = path or gmem.mem_path()
|
|
self.fd = os.open(self.path, os.O_RDONLY)
|
|
self.size = os.path.getsize(self.path)
|
|
self.instances = [] # [(va, file_off, name)]
|
|
|
|
# ---------------------------------------------------------------- io
|
|
def read(self, va, n):
|
|
return os.pread(self.fd, n, gmem.va_to_off(va))
|
|
|
|
def read_off(self, off, n):
|
|
return os.pread(self.fd, n, off)
|
|
|
|
def u32(self, va):
|
|
return struct.unpack(">I", self.read(va, 4))[0]
|
|
|
|
def cstr(self, va, n=96):
|
|
b = os.pread(self.fd, n, gmem.va_to_off(va)).split(b"\0")[0]
|
|
try:
|
|
return b.decode("ascii")
|
|
except UnicodeDecodeError:
|
|
return None
|
|
|
|
# ------------------------------------------------------------- scan
|
|
def scan_vtable(self, vt):
|
|
pat = struct.pack(">I", vt)
|
|
hits = []
|
|
for a, b in gmem.extents(self.fd, self.size):
|
|
blob = os.pread(self.fd, b - a, a)
|
|
start = 0
|
|
while True:
|
|
i = blob.find(pat, start)
|
|
if i < 0:
|
|
break
|
|
off = a + i
|
|
if off % 4 == 0:
|
|
hits.append(off)
|
|
start = i + 4
|
|
return sorted(set(hits))
|
|
|
|
def name_of(self, off):
|
|
"""The unit ID an object at `off` names, via its name-record pointer."""
|
|
raw = self.read_off(off + 4, 4)
|
|
if len(raw) < 4:
|
|
return None
|
|
(p,) = struct.unpack(">I", raw)
|
|
try:
|
|
return self.cstr(p + NAME_STR_OFF)
|
|
except ValueError:
|
|
return None
|
|
|
|
def refresh(self, vt=INST_VTABLE):
|
|
"""Re-scan for live instances. Costs ~1 s; call it rarely."""
|
|
out = []
|
|
for off in self.scan_vtable(vt):
|
|
nm = self.name_of(off)
|
|
if nm and nm.startswith("UN_"):
|
|
out.append((gmem.primary_va(off), off, nm))
|
|
self.instances = out
|
|
return out
|
|
|
|
|
|
# --------------------------------------------------------------- geometry
|
|
|
|
|
|
def floats(buf, off, n):
|
|
return struct.unpack_from(">" + "f" * n, buf, off)
|
|
|
|
|
|
def is_rotation(m, tol=2e-3):
|
|
"""m = 9 floats, row-major. Orthonormal rows + det +1?"""
|
|
r = [m[0:3], m[3:6], m[6:9]]
|
|
for row in r:
|
|
n2 = sum(c * c for c in row)
|
|
if abs(n2 - 1.0) > tol:
|
|
return False
|
|
for i in range(3):
|
|
for j in range(i + 1, 3):
|
|
if abs(sum(r[i][k] * r[j][k] for k in range(3))) > tol:
|
|
return False
|
|
det = (r[0][0] * (r[1][1] * r[2][2] - r[1][2] * r[2][1])
|
|
- r[0][1] * (r[1][0] * r[2][2] - r[1][2] * r[2][0])
|
|
+ r[0][2] * (r[1][0] * r[2][1] - r[1][1] * r[2][0]))
|
|
return abs(det - 1.0) < 1e-2
|
|
|
|
|
|
def find_rotations(buf, stride=4):
|
|
"""Offsets where 9 consecutive floats form a rotation matrix.
|
|
|
|
Also tries the 4-float-per-row (3x4 / 4x4 matrix) stride, which is how a
|
|
transform with a translation column is normally stored.
|
|
"""
|
|
out = []
|
|
for off in range(0, len(buf) - 36, stride):
|
|
m = floats(buf, off, 9)
|
|
if all(abs(c) <= 1.001 for c in m) and is_rotation(m):
|
|
out.append((off, "3x3", m))
|
|
for off in range(0, len(buf) - 48, stride):
|
|
rows = [floats(buf, off + 16 * i, 3) for i in range(3)]
|
|
m = rows[0] + rows[1] + rows[2]
|
|
if all(abs(c) <= 1.001 for c in m) and is_rotation(m):
|
|
out.append((off, "4x4", m))
|
|
return out
|
|
|
|
|
|
# ------------------------------------------------------------------ cmds
|
|
|
|
|
|
def cmd_entities(w, args):
|
|
t0 = time.time()
|
|
inst = w.refresh()
|
|
print(f"# {len(inst)} live instances (scan {time.time()-t0:.2f}s)")
|
|
from collections import Counter
|
|
c = Counter(n for _, _, n in inst)
|
|
for nm, k in c.most_common():
|
|
print(f" {k:4d} {nm}")
|
|
|
|
|
|
def cmd_orient(w, args):
|
|
inst = w.refresh()
|
|
want = args[0] if args else None
|
|
for va, off, nm in inst:
|
|
if want and want not in nm:
|
|
continue
|
|
buf = w.read_off(off, WINDOW)
|
|
rots = find_rotations(buf)
|
|
print(f"\n{va:#010x} {nm} -> {len(rots)} rotation-like block(s)")
|
|
for o, kind, m in rots[:6]:
|
|
print(f" +{o:#05x} {kind} "
|
|
+ " ".join(f"{v:+.3f}" for v in m))
|
|
|
|
|
|
def cmd_probe(w, args):
|
|
secs = float(args[0]) if args else 6.0
|
|
hz = float(args[1]) if len(args) > 1 else 5.0
|
|
out = args[2] if len(args) > 2 else "probe.bin"
|
|
inst = w.refresh()
|
|
print(f"# probing {len(inst)} instances for {secs}s at {hz}Hz -> {out}")
|
|
with open(out, "wb") as f:
|
|
f.write(struct.pack("<II", len(inst), WINDOW))
|
|
for va, off, nm in inst:
|
|
nb = nm.encode()[:63].ljust(64, b"\0")
|
|
f.write(struct.pack("<II", va, off) + nb)
|
|
n = int(secs * hz)
|
|
for i in range(n):
|
|
t = time.time()
|
|
f.write(struct.pack("<d", t))
|
|
for va, off, nm in inst:
|
|
f.write(w.read_off(off, WINDOW).ljust(WINDOW, b"\0"))
|
|
time.sleep(max(0, 1.0 / hz - (time.time() - t)))
|
|
print(f"# wrote {n} frames")
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
sys.exit(__doc__)
|
|
w = World()
|
|
cmd, args = sys.argv[1], sys.argv[2:]
|
|
{"entities": cmd_entities, "orient": cmd_orient, "probe": cmd_probe}[cmd](w, args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|