"""Strict live camera scan: vtable in PathOfExile.exe, FOV+minDistance present. 11/09/2026."""
from __future__ import annotations

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

kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
psapi = ctypes.WinDLL("psapi", use_last_error=True)

PROCESS_VM_READ = 0x0010
PROCESS_QUERY_INFORMATION = 0x0400
MEM_COMMIT = 0x1000
PAGE_NOACCESS = 0x01
PAGE_GUARD = 0x100

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


class MEMORY_BASIC_INFORMATION(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 MODULEINFO(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 kernel32.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), 0x03)
    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() in ("pathofexile.exe", "pathofexilesteam.exe", "pathofexile2.exe"):
            mi = MODULEINFO()
            psapi.GetModuleInformation(h, ctypes.c_void_p(mods[i]), ctypes.byref(mi), ctypes.sizeof(mi))
            return name.value, int(mi.lpBaseOfDll or 0), int(mi.SizeOfImage)
    return None


def isfinite_f(v):
    return v == v and abs(v) < 1e12


def main():
    pid = int(sys.argv[1]) if len(sys.argv) > 1 else 3132
    locked = int(sys.argv[2], 16) if len(sys.argv) > 2 else 0
    t0 = time.time()
    h = kernel32.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
    if not h:
        print("OpenProcess fail", ctypes.get_last_error())
        return
    mod = main_module(h)
    print("module", mod)
    name, base, size = mod
    rdata0 = base + 0x2E87000
    rdata1 = base + 0x3A04000
    print(f"vtable window .rdata {rdata0:#x}-{rdata1:#x}")

    if locked:
        b = rpm(h, locked, 0x50)
        print("LOCKED dump", locked, None if not b else b[:8].hex(),
              None if not b else struct.unpack_from("<Qffffff", b, 0))

    mbi = MEMORY_BASIC_INFORMATION()
    addr = 0x10000
    found = []
    scanned = 0
    regions = 0
    while addr < 0x7FFFFFFFFFFF and len(found) < 40:
        q = kernel32.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
        # heap-like: private/mapped, not the main image
        if readable and 0x1000 <= rsz <= (32 << 20) and not (base <= baddr < base + size):
            regions += 1
            off = 0
            chunk = 1 << 20
            while off + 0x50 < rsz and len(found) < 40:
                n = min(chunk, rsz - off)
                blob = rpm(h, baddr + off, n)
                if blob:
                    scanned += len(blob)
                    for i in range(0, len(blob) - 0x50, 8):
                        vt = struct.unpack_from("<Q", blob, i)[0]
                        if not (rdata0 <= vt < rdata1):
                            continue
                        cur, mn, mx, fov, zn, zf = struct.unpack_from("<ffffff", blob, i + 0x2C)
                        if not all(map(isfinite_f, (cur, mn, mx, fov, zn, zf))):
                            continue
                        min_ok = 1.0 <= mn <= 80.0 and mn < mx
                        fov_ok = (0.30 <= fov <= 1.80) or (5.0 <= fov <= 90.0)
                        zn_ok = 0.001 <= zn <= 20.0
                        zf_ok = 20.0 <= zf <= 20000.0 and zn < zf
                        mx_ok = 8.0 <= mx <= 400.0
                        cur_ok = 3.0 <= cur <= 800.0
                        if min_ok and fov_ok and zn_ok and zf_ok and mx_ok and cur_ok:
                            found.append((baddr + off + i, vt, cur, mn, mx, fov, zn, zf))
                            if len(found) >= 40:
                                break
                off += n
        if nxt <= addr:
            break
        addr = nxt

    print(f"strict cameras {len(found)} regions={regions} scanned={scanned} t={time.time()-t0:.1f}s")
    for item in found[:25]:
        addr, vt, cur, mn, mx, fov, zn, zf = item
        print(
            f"  {addr:#x} vt={vt:#x} cur={cur:.2f} min={mn:.2f} max={mx:.2f} "
            f"fov={fov:.4f} zNear={zn:.3f} zFar={zf:.1f}"
        )


if __name__ == "__main__":
    main()
