import ctypes
from ctypes import wintypes
import struct
import psutil

pids = [p.pid for p in psutil.process_iter() if 'PathOfExile' in p.name()]
if not pids:
    print('No PathOfExile.exe found!')
    exit(1)
pid = pids[0]
print(f'Scanning PID: {pid}')

kernel32 = ctypes.windll.kernel32
hProcess = kernel32.OpenProcess(0x0010 | 0x0400, False, pid)

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

mbi = MEMORY_BASIC_INFORMATION()
mbi_size = ctypes.sizeof(mbi)
addr = 0x2AE00000000
max_addr = 0x2B020000000

ci_hits = []
es_exact_hits = []
ward_hits = []

target_es = 4713
target_ward = 240
target_mana = 876

while addr < max_addr:
    if not kernel32.VirtualQueryEx(hProcess, ctypes.c_void_p(addr), ctypes.byref(mbi), mbi_size):
        break
    base = mbi.BaseAddress or 0
    size = mbi.RegionSize
    protect = mbi.Protect
    state = mbi.State
    addr = base + size
    if state != 0x1000 or protect != 0x04:
        continue

    buf = (ctypes.c_char * size)()
    bytesRead = ctypes.c_size_t()
    if not kernel32.ReadProcessMemory(hProcess, ctypes.c_void_p(base), buf, size, ctypes.byref(bytesRead)):
        continue
    raw = bytes(buf[:bytesRead.value])

    # 1. Search for CI LifeComponent: [+0]=1, [+4]=1, [+8]=1, [+12]=curES, [+16]=maxES
    for i in range(8, len(raw) - 64, 4):
        h1, h2, h3 = struct.unpack_from('<III', raw, i)
        if h1 == 1 and h2 == 1 and h3 == 1:
            es_c, es_m = struct.unpack_from('<II', raw, i + 12)
            if 500 <= es_m <= 30000 and 0 < es_c <= es_m * 2:
                prev8, prev4 = struct.unpack_from('<II', raw, i - 8)
                ward_c, ward_m = struct.unpack_from('<II', raw, i + 32)
                ci_hits.append({
                    'addr': base + i,
                    'hp': (h1, h2),
                    'es': (es_c, es_m),
                    'ward': (ward_c, ward_m),
                    'prev': (prev8, prev4),
                    'extra': [struct.unpack_from('<I', raw, i + o)[0] for o in range(20, 64, 4)]
                })

    # 2. Search for exact target_es anywhere
    idx = 0
    es_bytes = struct.pack('<I', target_es)
    while True:
        pos = raw.find(es_bytes, idx)
        if pos == -1: break
        # check surrounding
        s_start = max(0, pos - 16)
        s_end = min(len(raw), pos + 32)
        vals = [struct.unpack_from('<I', raw, o)[0] for o in range(s_start, s_end-3, 4)]
        es_exact_hits.append((base + pos, vals, pos - s_start))
        idx = pos + 4
        if len(es_exact_hits) > 200: break

kernel32.CloseHandle(hProcess)

print(f'=== FOUND {len(ci_hits)} CI LifeCandidates in heap ===', flush=True)
for h in ci_hits:
    print(f"  Addr 0x{h['addr']:X}: HP={h['hp']}, ES={h['es']}, Ward={h['ward']}, prev={h['prev']}", flush=True)
    print(f"    extra: {[hex(x) for x in h['extra']]}", flush=True)

print(f'\n=== FOUND {len(es_exact_hits)} exact ES={target_es} hits in heap ===', flush=True)
for addr, vals, rel in es_exact_hits[:30]:
    print(f"  Addr 0x{addr:X} (rel {rel}): {[hex(x) for x in vals]} | {vals}", flush=True)
