"""Probe live PathOfExile.exe for InGameState, Camera at +0x378, Zoom at +0x528.
11/09/2026.
"""
from __future__ import annotations
import ctypes
import ctypes.wintypes as w
import struct
import sys

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

class MODULEENTRY32W(ctypes.Structure):
    _fields_ = [
        ("dwSize", w.DWORD),
        ("th32ModuleID", w.DWORD),
        ("th32ProcessID", w.DWORD),
        ("GlblcntUsage", w.DWORD),
        ("ProccntUsage", w.DWORD),
        ("modBaseAddr", ctypes.c_void_p),
        ("modBaseSize", w.DWORD),
        ("hModule", w.HMODULE),
        ("szModule", ctypes.c_wchar * 256),
        ("szExePath", ctypes.c_wchar * 260),
    ]

def get_module(pid: int, mod_name: str):
    snap = k32.CreateToolhelp32Snapshot(0x00000008, pid)
    if snap == -1:
        return None, 0
    me = MODULEENTRY32W()
    me.dwSize = ctypes.sizeof(MODULEENTRY32W)
    if k32.Module32FirstW(snap, ctypes.byref(me)):
        while True:
            if me.szModule.lower() == mod_name.lower():
                base = me.modBaseAddr
                size = me.modBaseSize
                k32.CloseHandle(snap)
                return base, size
            if not k32.Module32NextW(snap, ctypes.byref(me)):
                break
    k32.CloseHandle(snap)
    return None, 0

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

def rpm_u64(h, addr):
    b = rpm(h, addr, 8)
    return struct.unpack("<Q", b)[0] if b and len(b) == 8 else 0

def rpm_f32(h, addr):
    b = rpm(h, addr, 4)
    return struct.unpack("<f", b)[0] if b and len(b) == 4 else 0.0

def main():
    import subprocess
    # Find PID
    out = subprocess.check_output(['powershell', '-Command', '(Get-Process -Name *PathOfExile*).Id'])
    pids = [int(x.strip()) for x in out.decode().split() if x.strip().isdigit()]
    if not pids:
        print("No PathOfExile.exe found")
        return
    pid = pids[0]
    print(f"Target PID: {pid}")
    
    h = k32.OpenProcess(0x1F0FFF, False, pid)
    if not h:
        print(f"OpenProcess failed: {ctypes.get_last_error()}")
        return

    base, size = get_module(pid, "PathOfExile.exe")
    print(f"Module base: {base:#x}, size: {size:#x}")
    if not base:
        return

    # Let's scan .text (up to 48MB) for IngameState pattern
    # Pattern: 8B C7 48 83 C4 40 5F C3 48 8B 05 ?? ?? ?? ?? 48 89 07 48
    # Prefix: 8B C7 48 83 C4 40 5F C3 48 8B 05
    pat_prefix = bytes([0x8B, 0xC7, 0x48, 0x83, 0xC4, 0x40, 0x5F, 0xC3, 0x48, 0x8B, 0x05])
    
    print("Scanning for IngameState pattern...")
    chunk_sz = 4 * 1024 * 1024
    found_slots = []
    for off in range(0, min(size, 48 * 1024 * 1024), chunk_sz - 64):
        data = rpm(h, base + off, chunk_sz)
        if not data:
            continue
        idx = 0
        while True:
            idx = data.find(pat_prefix, idx)
            if idx == -1 or idx + 19 > len(data):
                break
            # Check trailing: 48 89 07 48
            if data[idx+15:idx+19] == bytes([0x48, 0x89, 0x07, 0x48]):
                rel_disp = struct.unpack("<i", data[idx+11:idx+15])[0]
                ins_addr = base + off + idx + 8 # start of 48 8B 05
                slot = ins_addr + 7 + rel_disp
                found_slots.append((ins_addr, slot))
                print(f"Found hit at {ins_addr:#x} -> Slot: {slot:#x}")
            idx += 1

    # Also try the AutoPOE2 PointerChainResolver known static roots
    known_roots = [
        0x45CFEF8, 0x4715740, 0x45CCA20, 0x45D3280,
        0x45D3288, 0x45CCA28, 0x45CCA30, 0x45D3290
    ]
    for r in known_roots:
        found_slots.append((0, base + r))

    # Also try AOB from PointerChainResolver: 48 8B 05 ? ? ? ? 48 8B 40 38 48 8B 88 ? ? ? ? 48 85 C9 74
    pat_pc = bytes([0x48, 0x8B, 0x05])
    for off in range(0, min(size, 48 * 1024 * 1024), chunk_sz - 64):
        data = rpm(h, base + off, chunk_sz)
        if not data:
            continue
        idx = 0
        while True:
            idx = data.find(pat_pc, idx)
            if idx == -1 or idx + 21 > len(data):
                break
            if data[idx+7:idx+11] == bytes([0x48, 0x8B, 0x40, 0x38]) and data[idx+11:idx+14] == bytes([0x48, 0x8B, 0x88]):
                rel_disp = struct.unpack("<i", data[idx+3:idx+7])[0]
                ins_addr = base + off + idx
                slot = ins_addr + 7 + rel_disp
                found_slots.append((ins_addr, slot))
                print(f"Found PC hit at {ins_addr:#x} -> Slot: {slot:#x}")
            idx += 1

    print(f"\nTotal candidate slots: {len(found_slots)}")
    seen_slots = set()
    for _, slot in found_slots:
        if slot in seen_slots:
            continue
        seen_slots.add(slot)
        
        # Read slot
        root_val = rpm_u64(h, slot)
        if root_val < 0x10000:
            continue
        
        # InGameState might be directly root_val or root_val is GameState
        # Let's test both
        candidates = [root_val]
        # If GameState: CurrentStatePtr is at +0x08 (StdVector: first/last/end)
        vec_first = rpm_u64(h, root_val + 0x08)
        if vec_first >= 0x10000:
            active_st = rpm_u64(h, vec_first)
            if active_st >= 0x10000:
                candidates.append(active_st)
        # Also check States[12] at +0x48
        for si in range(12):
            st = rpm_u64(h, root_val + 0x48 + si * 0x10)
            if st >= 0x10000:
                candidates.append(st)

        for igs in candidates:
            # Let's inspect Camera at igs + 0x378 and 0x368
            for cam_off in [0x378, 0x368, 0x388, 0x358]:
                cam_ptr = rpm_u64(h, igs + cam_off)
                if cam_ptr >= 0x10000:
                    zoom_val = rpm_f32(h, cam_ptr + 0x528)
                    m11 = rpm_f32(h, cam_ptr + 0x1A0)
                    m44 = rpm_f32(h, cam_ptr + 0x1A0 + 0x3C)
                    # If zoom_val is around 1.0 (e.g. 0.5 to 4.0) and matrix looks like projection
                    if 0.2 < zoom_val < 10.0:
                        print(f"!!! MATCH FOUND !!!")
                        print(f"Slot: {slot:#x} -> InGameState: {igs:#x}")
                        print(f"  Camera (igs+{cam_off:#x}): {cam_ptr:#x}")
                        print(f"  Zoom @ +0x528: {zoom_val}")
                        print(f"  Matrix @ +0x1A0: M11={m11}, M44={m44}")
                        
                        # Inspect other offsets on Camera
                        print("  Dumping camera floats:")
                        for fo in range(0, 0x600, 4):
                            val = rpm_f32(h, cam_ptr + fo)
                            if 0.5 <= val <= 2000.0:
                                print(f"    +{fo:#04x}: {val:.4f}")
                        
                        # Also check AreaInstance at igs + 0x2A0, 0x290
                        for aio in [0x2A0, 0x290, 0x2B0]:
                            ai = rpm_u64(h, igs + aio)
                            if ai >= 0x10000:
                                name_row = rpm_u64(h, ai + 0x098)
                                print(f"  AreaInstance (igs+{aio:#x}): {ai:#x}")
                        return

if __name__ == "__main__":
    main()
