"""Find callers of camera-copy fn and heap objects with module vtable. 11/09/2026."""
from __future__ import annotations

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

k32 = ctypes.WinDLL("kernel32", use_last_error=True)
psapi = ctypes.WinDLL("psapi", 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),
    ]


class MI(ctypes.Structure):
    _fields_ = [
        ("lpBaseOfDll", ctypes.c_void_p),
        ("SizeOfImage", w.DWORD),
        ("EntryPoint", ctypes.c_void_p),
    ]


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 main_module(h):
    mods = (ctypes.c_uint64 * 1024)()
    needed = w.DWORD()
    psapi.EnumProcessModulesEx(h, ctypes.byref(mods), ctypes.sizeof(mods), ctypes.byref(needed), 3)
    name = ctypes.create_unicode_buffer(260)
    psapi.GetModuleBaseNameW.argtypes = [w.HANDLE, ctypes.c_void_p, w.LPWSTR, w.DWORD]
    psapi.GetModuleInformation.argtypes = [w.HANDLE, ctypes.c_void_p, ctypes.c_void_p, w.DWORD]
    for i in range(needed.value // 8):
        psapi.GetModuleBaseNameW(h, mods[i], name, 260)
        if name.value.lower().startswith("pathofexile"):
            mi = MI()
            psapi.GetModuleInformation(h, ctypes.c_void_p(mods[i]), ctypes.byref(mi), ctypes.sizeof(mi))
            return int(mi.lpBaseOfDll or 0), int(mi.SizeOfImage)
    return 0, 0


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


def main():
    pid = int(sys.argv[1]) if len(sys.argv) > 1 else 3132
    t0 = time.time()
    h = k32.OpenProcess(0x410, False, pid)
    base, size = main_module(h)
    print(f"base={base:#x} size={size:#x}", flush=True)
    text_n = min(size, 0x2E86000)
    blob = rpm(h, base, text_n)
    print(f"text={len(blob) if blob else 0}", flush=True)

    target = 0x7FF6AFE823E0
    print(f"\n=== calls to camera-copy {target:#x} ===", flush=True)
    if blob:
        for i in range(0, len(blob) - 5):
            if blob[i] != 0xE8:
                continue
            disp = struct.unpack_from("<i", blob, i + 1)[0]
            dest = base + i + 5 + disp
            if dest == target:
                print(f"  call @ {base+i:#x}", flush=True)

    print("\n=== heap scan vtable-in-module + camera floats ===", flush=True)
    mbi = MBI()
    addr = 0x10000
    found = []
    scanned = 0
    MEM_COMMIT = 0x1000
    PAGE_NOACCESS = 1
    PAGE_GUARD = 0x100
    MEM_PRIVATE = 0x20000
    while addr < 0x7FFFFFFFFFFF and len(found) < 20:
        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 mbi.Type == MEM_PRIVATE and 0x1000 <= rsz <= (64 << 20):
            off = 0
            while off + 0x50 < rsz and len(found) < 20:
                n = min(2 << 20, rsz - off)
                chunk = rpm(h, baddr + off, n)
                if chunk:
                    scanned += len(chunk)
                    for i in range(0, len(chunk) - 0x50, 8):
                        vt = struct.unpack_from("<Q", chunk, i)[0]
                        if not canonical_user(vt):
                            continue
                        if not (base <= vt < base + size):
                            continue
                        cur, mn, mx, fov, zn, zf = struct.unpack_from("<ffffff", chunk, i + 0x2C)
                        vals = (cur, mn, mx, fov, zn, zf)
                        if not all(x == x and abs(x) < 1e6 for x in vals):
                            continue
                        if not (1.0 <= mn <= 80 and 8 <= mx <= 400 and mn < mx):
                            continue
                        if not ((0.30 <= fov <= 1.80) or (25 <= fov <= 90)):
                            continue
                        if not (0.01 <= zn <= 5.0 and 20 <= zf <= 20000 and zn < zf):
                            continue
                        if not (3 <= cur <= 800):
                            continue
                        found.append((baddr + off + i, vt, cur, mn, mx, fov, zn, zf))
                        if len(found) >= 20:
                            break
                off += n
        if nxt <= addr:
            break
        addr = nxt

    print(f"found={len(found)} scanned_mb={scanned/1048576:.1f} t={time.time()-t0:.1f}s", flush=True)
    for item in found:
        a, vt, cur, mn, mx, fov, zn, zf = item
        print(
            f"  {a:#x} vt={vt:#x} cur={cur:.2f} min={mn:.2f} max={mx:.2f} "
            f"fov={fov:.4f} zn={zn:.3f} zf={zf:.1f}",
            flush=True,
        )


if __name__ == "__main__":
    main()
