"""Search process RAM for vanilla camera float pairs. 11/09/2026."""
from __future__ import annotations

import ctypes
import ctypes.wintypes as w
import struct
import time

k32 = ctypes.WinDLL("kernel32", use_last_error=True)
k32.OpenProcess.restype = w.HANDLE
k32.ReadProcessMemory.argtypes = [
    w.HANDLE, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)
]
k32.ReadProcessMemory.restype = w.BOOL
k32.VirtualQueryEx.argtypes = [w.HANDLE, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
k32.VirtualQueryEx.restype = ctypes.c_size_t


class MBI(ctypes.Structure):
    _fields_ = [
        ("BaseAddress", ctypes.c_void_p),
        ("AllocationBase", ctypes.c_void_p),
        ("AllocationProtect", w.DWORD),
        ("PartitionId", w.WORD),
        ("RegionSize", ctypes.c_size_t),
        ("State", w.DWORD),
        ("Protect", w.DWORD),
        ("Type", w.DWORD),
    ]


def rpm(h, addr, n):
    buf = (ctypes.c_ubyte * n)()
    got = ctypes.c_size_t(0)
    if not k32.ReadProcessMemory(h, ctypes.c_void_p(addr), buf, n, ctypes.byref(got)):
        return None
    return bytes(buf[: got.value])


def canonical_user(p):
    return 0x10000 <= p < 0x0000800000000000


def main():
    pid = 3132
    h = k32.OpenProcess(0x410, False, pid)
    needles = {
        "min15_max45": struct.pack("<ff", 15.0, 45.0),
        "min15_max40": struct.pack("<ff", 15.0, 40.0),
        "min10_max45": struct.pack("<ff", 10.0, 45.0),
        "min20_max45": struct.pack("<ff", 20.0, 45.0),
        "zfar150": struct.pack("<f", 150.0),
        "fov_pi4": bytes.fromhex("db0f493f"),  # ~0.785398
    }
    mbi = MBI()
    addr = 0x10000
    MEM_COMMIT = 0x1000
    PAGE_NOACCESS = 1
    PAGE_GUARD = 0x100
    t0 = time.time()
    scanned = 0
    hits = {k: [] for k in needles}
    while addr < 0x7FFFFFFFFFFF and time.time() - t0 < 45:
        q = k32.VirtualQueryEx(h, ctypes.c_void_p(addr), ctypes.byref(mbi), ctypes.sizeof(mbi))
        if q == 0:
            break
        baddr = int(mbi.BaseAddress or 0)
        rsz = int(mbi.RegionSize)
        nxt = baddr + rsz
        prot = mbi.Protect
        readable = (mbi.State == MEM_COMMIT) and not (prot & (PAGE_NOACCESS | PAGE_GUARD)) and prot != 0
        if readable and 0x100 <= rsz <= (128 << 20):
            off = 0
            while off < rsz:
                n = min(4 << 20, rsz - off)
                chunk = rpm(h, baddr + off, n)
                if chunk:
                    scanned += len(chunk)
                    for name, needle in needles.items():
                        if len(hits[name]) >= 8:
                            continue
                        start = 0
                        while len(hits[name]) < 8:
                            i = chunk.find(needle, start)
                            if i < 0:
                                break
                            hits[name].append(baddr + off + i)
                            start = i + 4
                off += n
        if nxt <= addr:
            break
        addr = nxt

    print(f"scanned_mb={scanned/1048576:.1f} t={time.time()-t0:.1f}s", flush=True)
    for name, addrs in hits.items():
        print(f"\n[{name}] {len(addrs)}", flush=True)
        for a in addrs[:8]:
            # if this is min/max at +0x30, object base is a-0x30
            base_guess = a - 0x30 if name.startswith("min") else (a - 0x40 if name == "zfar150" else a - 0x38)
            blk = rpm(h, base_guess, 0x50)
            if not blk:
                print(f"  {a:#x} (obj {base_guess:#x} unread)", flush=True)
                continue
            vt = struct.unpack_from("<Q", blk, 0)[0]
            cur, mn, mx, fov, zn, zf = struct.unpack_from("<ffffff", blk, 0x2C)
            print(
                f"  needle={a:#x} obj={base_guess:#x} vt={vt:#x} canon={canonical_user(vt)} "
                f"cur={cur:.2f} min={mn:.2f} max={mx:.2f} fov={fov:.4f} zn={zn:.3f} zf={zf:.1f}",
                flush=True,
            )


if __name__ == "__main__":
    main()
