#!/usr/bin/env python3 """Find the player craft's live position in guest RAM, from motion alone. The spawn records at vtable 0x820af030 turned out to hold no live state (every word is constant across a 29 s capture), so the flying entity is some other object. Rather than guess which class, this finds it by what it must *do*: a position triple moves in a straight line at constant speed while coasting. Method: sample all of RAM K times ~dt apart, keep word indices whose float value changes every time, then require three consecutive such words to satisfy * equal step length each interval (constant speed), and * a constant direction (straight flight), * with speed in a sane range for a craft. Only the wholly-mechanical properties of motion are used; no offset, class or encoding is assumed. Usage: findplayer.py [samples] [interval_s] """ 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 def snapshot(fd, size): """[(start_off, np.ndarray big-endian f32)] over allocated extents.""" out = [] for a, b in gmem.extents(fd, size): n = (b - a) // 4 * 4 if n < 64: continue buf = os.pread(fd, n, a) out.append((a, np.frombuffer(buf, dtype=">f4"))) return out def main(): K = int(sys.argv[1]) if len(sys.argv) > 1 else 5 dt = float(sys.argv[2]) if len(sys.argv) > 2 else 0.35 path = gmem.mem_path() fd = os.open(path, os.O_RDONLY) size = os.path.getsize(path) print(f"# sampling {K}x every {dt}s", flush=True) snaps, times = [], [] for k in range(K): t = time.time() snaps.append(snapshot(fd, size)) times.append(t) print(f"# sample {k} ({len(snaps[-1])} extents, " f"{sum(len(a) for _, a in snaps[-1])*4/1e6:.0f} MB) " f"in {time.time()-t:.2f}s", flush=True) time.sleep(max(0, dt - (time.time() - t))) # extents must line up across samples for a word-wise comparison shapes = [tuple((o, len(a)) for o, a in s) for s in snaps] if len(set(shapes)) != 1: common = set(shapes[0]) for sh in shapes[1:]: common &= set(sh) print(f"# extents shifted between samples; using {len(common)} common ones") keep = lambda s: [(o, a) for o, a in s if (o, len(a)) in common] snaps = [keep(s) for s in snaps] hits = [] for e in range(len(snaps[0])): base = snaps[0][e][0] arrs = [np.nan_to_num(s[e][1].astype(np.float64), nan=0, posinf=0, neginf=0) for s in snaps] # per-interval deltas d = [arrs[k + 1] - arrs[k] for k in range(K - 1)] moving = np.ones(len(arrs[0]), dtype=bool) for dk in d: moving &= np.abs(dk) > 1e-3 idx = np.flatnonzero(moving) if len(idx) == 0: continue # candidate triples: i, i+1, i+2 all moving cand = idx[np.isin(idx + 1, idx) & np.isin(idx + 2, idx)] for i in cand: steps = [] dirs = [] ok = True for k in range(K - 1): v = np.array([d[k][i], d[k][i + 1], d[k][i + 2]]) n = float(np.linalg.norm(v)) ddt = times[k + 1] - times[k] sp = n / ddt if not (20.0 < sp < 3000.0): ok = False break steps.append(sp) dirs.append(v / n) if not ok or len(steps) < 3: continue if max(steps) - min(steps) > 0.25 * (sum(steps) / len(steps)): continue if min(float(np.dot(dirs[0], dd)) for dd in dirs) < 0.985: continue va = gmem.primary_va(base + int(i) * 4) pos = tuple(float(arrs[0][i + j]) for j in range(3)) hits.append((va, base + int(i) * 4, sum(steps) / len(steps), pos)) print(f"\n# {len(hits)} position-like triples (straight, constant speed)") hits.sort(key=lambda h: -h[2]) for va, off, sp, pos in hits[:40]: print(f" va {va:#010x} speed {sp:8.1f}/s pos " f"({pos[0]:+11.1f},{pos[1]:+11.1f},{pos[2]:+11.1f})") if __name__ == "__main__": main()