#!/usr/bin/env python3 """Type the live entities: link each moving position back to a unit definition. findplayer.py locates positions by motion alone, but a bare (x,y,z) says nothing about *what* is moving. Every live entity should reference its parsed `.tbl` definition (vtable 0x820af844, one object per unit type, addresses known), so scanning the neighbourhood of each position for a word equal to a definition address both types the entity and reveals the live object's own layout — the delta from that pointer to the position triple is the position offset. Usage: liveents.py [radius_hex] """ import os import struct import sys from collections import Counter import numpy as np sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import gmem # noqa: E402 import gworld # noqa: E402 def definitions(w): """{definition_va: unit_id}""" out = {} for off in w.scan_vtable(gworld.DEF_VTABLE): nm = w.name_of(off) if nm and nm.startswith("UN_"): va = gmem.primary_va(off) if va is not None: out[va] = nm return out def main(): radius = int(sys.argv[1], 16) if len(sys.argv) > 1 else 0x600 w = gworld.World() defs = definitions(w) print(f"# {len(defs)} unit definitions") by_word = {struct.pack(">I", va): nm for va, nm in defs.items()} # sample twice to locate movers, exactly as findplayer.py does import time fd, size = w.fd, w.size def snap(): return [(a, np.frombuffer(os.pread(fd, (b - a) // 4 * 4, a), dtype=">f4")) for a, b in gmem.extents(fd, size) if (b - a) >= 64] s0 = snap(); t0 = time.time() time.sleep(0.4) s1 = snap(); t1 = time.time() shapes0 = {(o, len(a)) for o, a in s0} pairs = [(o, a, b) for (o, a), (o2, b) in zip(s0, s1) if o == o2 and len(a) == len(b) and (o, len(a)) in shapes0] movers = [] for base, a, b in pairs: af = np.nan_to_num(a.astype(np.float64), nan=0, posinf=0, neginf=0) bf = np.nan_to_num(b.astype(np.float64), nan=0, posinf=0, neginf=0) d = bf - af m = np.abs(d) > 1e-3 idx = np.flatnonzero(m) cand = idx[np.isin(idx + 1, idx) & np.isin(idx + 2, idx)] for i in cand: v = np.array([d[i], d[i + 1], d[i + 2]]) sp = float(np.linalg.norm(v)) / (t1 - t0) if 20.0 < sp < 3000.0: movers.append((base + int(i) * 4, tuple(float(af[i + j]) for j in range(3)), sp)) print(f"# {len(movers)} moving triples; typing them...") typed, untyped = [], 0 seen = set() for off, pos, sp in movers: lo = max(0, off - radius) blob = os.pread(fd, radius * 2, lo) hit = None for k in range(0, len(blob) - 3, 4): nm = by_word.get(blob[k:k + 4]) if nm: hit = (lo + k, nm) break if hit: ptr_off, nm = hit key = (ptr_off, nm) if key in seen: continue seen.add(key) typed.append((ptr_off, off - ptr_off, nm, pos, sp)) else: untyped += 1 print(f"# typed {len(typed)}, untyped {untyped}") deltas = Counter(d for _, d, _, _, _ in typed) print(f"# most common (def-pointer -> position) deltas: {deltas.most_common(6)}") for ptr_off, d, nm, pos, sp in sorted(typed, key=lambda x: x[2])[:40]: va = gmem.primary_va(ptr_off) print(f" obj~{va:#010x} defptr+{d:#06x} {nm:<40} " f"({pos[0]:+10.1f},{pos[1]:+10.1f},{pos[2]:+10.1f}) {sp:7.1f}/s") if __name__ == "__main__": main()