"""Fast AOB scan of live PathOfExile.exe module (RPM). 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)

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


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 find_pid():
    snap = kernel32.CreateToolhelp32Snapshot(0x2, 0)
    class PE(ctypes.Structure):
        _fields_ = [
            ("dwSize", w.DWORD),
            ("cntUsage", w.DWORD),
            ("th32ProcessID", w.DWORD),
            ("th32DefaultHeapID", ctypes.POINTER(ctypes.c_ulong)),
            ("th32ModuleID", w.DWORD),
            ("cntThreads", w.DWORD),
            ("th32ParentProcessID", w.DWORD),
            ("pcPriClassBase", ctypes.c_long),
            ("dwFlags", w.DWORD),
            ("szExeFile", ctypes.c_wchar * 260),
        ]
    pe = PE()
    pe.dwSize = ctypes.sizeof(PE)
    kernel32.Process32FirstW.argtypes = [w.HANDLE, ctypes.c_void_p]
    kernel32.Process32NextW.argtypes = [w.HANDLE, ctypes.c_void_p]
    ok = kernel32.Process32FirstW(snap, ctypes.byref(pe))
    names = {"pathofexile.exe", "pathofexilesteam.exe", "pathofexile2.exe"}
    pid = 0
    while ok:
        if pe.szExeFile.lower() in names:
            pid = pe.th32ProcessID
            break
        ok = kernel32.Process32NextW(snap, ctypes.byref(pe))
    kernel32.CloseHandle(snap)
    return pid


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() 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 parse_pat(s):
    parts = s.split()
    raw = bytearray()
    mask = bytearray()
    for p in parts:
        if p == "?":
            raw.append(0)
            mask.append(0)
        else:
            raw.append(int(p, 16))
            mask.append(1)
    return bytes(raw), bytes(mask)


def findall_masked(blob, raw, mask, limit=40):
    hits = []
    n = len(blob)
    m = len(raw)
    firsts = [i for i, b in enumerate(mask) if b]
    first = firsts[0] if firsts else 0
    start = 0
    needle = raw[first]
    while len(hits) < limit:
        idx = blob.find(needle, start + first)
        if idx < 0:
            break
        i = idx - first
        start = idx + 1
        if i < 0 or i + m > n:
            continue
        ok = True
        for j in range(m):
            if mask[j] and blob[i + j] != raw[j]:
                ok = False
                break
        if ok:
            hits.append(i)
    return hits


def rip_target(insn_addr, disp_off, blob, blob_base):
    off = insn_addr - blob_base + disp_off
    if off < 0 or off + 4 > len(blob):
        return 0
    disp = struct.unpack_from("<i", blob, off)[0]
    return insn_addr + disp_off + 4 + disp


def looks_like_camera(h, addr):
    b = rpm(h, addr, 0x50)
    if not b or len(b) < 0x50:
        return None
    cur, mn, mx, fov, zn, zf = struct.unpack_from("<ffffff", b, 0x2C)
    vt = struct.unpack_from("<Q", b, 0)[0]
    return {
        "vt": vt,
        "cur": cur,
        "min": mn,
        "max": mx,
        "fov": fov,
        "zn": zn,
        "zf": zf,
        "ok": (3.0 <= mn <= 80.0 and 15.0 <= mx <= 800.0 and mn < mx
               and mn * 0.4 <= cur <= mx * 4.0),
    }


def main():
    t0 = time.time()
    pid = int(sys.argv[1]) if len(sys.argv) > 1 else find_pid()
    print(f"pid={pid}", flush=True)
    h = kernel32.OpenProcess(0x10 | 0x400, False, pid)
    print(f"OpenProcess={bool(h)} err={ctypes.get_last_error()}", flush=True)
    mod = main_module(h)
    print("mod", mod, flush=True)
    name, base, size = mod
    print(f"reading {size/1024/1024:.1f} MB ...", flush=True)
    chunks = []
    off = 0
    failed = 0
    while off < size:
        n = min(2 * 1024 * 1024, size - off)
        b = rpm(h, base + off, n)
        if b is None:
            failed += 1
            chunks.append(b"\x00" * n)
        else:
            if len(b) < n:
                b = b + b"\x00" * (n - len(b))
            chunks.append(b)
        off += n
        if off % (16 * 1024 * 1024) == 0:
            print(f"  {off/1024/1024:.0f} MB", flush=True)
    blob = b"".join(chunks)
    print(f"blob={len(blob)} failed_chunks={failed} t={time.time()-t0:.1f}s", flush=True)

    patterns = {
        "old_cam": "48 8D 0D ? ? ? ? F3 0F 10 41 2C F3 0F 10 49 34 0F 2F C1",
        "lea_rcx_movss_2c": "48 8D 0D ? ? ? ? F3 0F 10 41 2C",
        "mov_rcx_rip_movss_2c": "48 8B 0D ? ? ? ? F3 0F 10 41 2C",
        "mov_rax_rip_movss_2c": "48 8B 05 ? ? ? ? F3 0F 10 40 2C",
        "old_fog": "48 8B 0D ? ? ? ? F3 0F 11 05 ? ? ? ? 48 85 C9 74 ? E8",
        "movss_store_rip": "F3 0F 11 05",
        "cmp_rax_2f": "80 78 2F 00",
        "movss_rax_34": "F3 0F 10 40 34",
        "movss_rcx_34": "F3 0F 10 41 34",
        "movss_rax_2c": "F3 0F 10 40 2C",
        "movss_rcx_2c": "F3 0F 10 41 2C",
        "lea_rcx": "48 8D 0D",
    }

    for name, spec in patterns.items():
        raw, mask = parse_pat(spec)
        hits = findall_masked(blob, raw, mask, limit=15 if name not in ("lea_rcx", "movss_store_rip") else 8)
        print(f"\n[{name}] hits_shown={len(hits)} (cap)", flush=True)
        for i in hits[:8]:
            print(f"  {base+i:#x}", flush=True)

    # Follow pointer-style camera patterns
    print("\n=== resolve pointer camera candidates ===", flush=True)
    for pname, spec, disp, deref in [
        ("lea_rcx_2c", "48 8D 0D ? ? ? ? F3 0F 10 41 2C", 3, False),
        ("mov_rcx_2c", "48 8B 0D ? ? ? ? F3 0F 10 41 2C", 3, True),
        ("mov_rax_2c", "48 8B 05 ? ? ? ? F3 0F 10 40 2C", 3, True),
        ("lea_rcx_34", "48 8D 0D ? ? ? ? F3 0F 10 41 34", 3, False),
        ("mov_rcx_34", "48 8B 0D ? ? ? ? F3 0F 10 41 34", 3, True),
        ("mov_rax_34", "48 8B 05 ? ? ? ? F3 0F 10 40 34", 3, True),
    ]:
        raw, mask = parse_pat(spec)
        hits = findall_masked(blob, raw, mask, limit=20)
        print(f"{pname}: {len(hits)}", flush=True)
        for i in hits[:12]:
            insn = base + i
            tgt = rip_target(insn, disp, blob, base)
            obj = tgt
            note = ""
            if deref:
                pb = rpm(h, tgt, 8)
                if pb:
                    obj = struct.unpack_from("<Q", pb, 0)[0]
                    note = f" ptr={tgt:#x}"
            cam = looks_like_camera(h, obj) if 0x10000 < obj < 0x7fffffffffff else None
            print(f"  insn={insn:#x} obj={obj:#x}{note} cam={cam}", flush=True)

    # Near cmp [rax+2F],0 look at following 16 bytes
    print("\n=== cmp [reg+2F],0 nearby movss +34 ===", flush=True)
    raw, mask = parse_pat("80 78 2F 00")
    hits = findall_masked(blob, raw, mask, limit=30)
    for i in hits[:20]:
        window = blob[i:i+24]
        hx = window.hex()
        print(f"  {base+i:#x} {hx}", flush=True)

    print(f"\ndone t={time.time()-t0:.1f}s", flush=True)


if __name__ == "__main__":
    main()
