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