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