"""RE Phase 1 - Tim BANG DOI TUONG (NPC/nguoi choi/quai) trong RAM vggame.exe.

CHI DOC (PROCESS_VM_READ + QUERY) - tuyet doi khong WriteProcessMemory.
Can quyen Admin (VietGuard chan non-admin).
Ket qua: logs/re_npc_list/phase1_pid<PID>.txt

Quy trinh:
  0) Liet ke vggame PID, mo handle, doc ten nhan vat tinh (0x6DEC44).
  1) Tim struct nguoi choi bang chu ky property [sig1,100,0,0,sig2,Sta,MaxSta] (nhu PlayerStatsReader).
  2) Xac dinh delta "object base" bang cach quet con tro tro ve vung struct nguoi choi.
  3) Quet candidates (moi struct co toa do tai +0x270/0x274 hop le) = danh sach doi tuong.
  4) Tim MANG CON TRO (pointer array) tro den cac candidate -> day la "danh sach NPC".
  5) Truy vet nguoc len vung static (module) -> chain pointer base on dinh.
"""
from __future__ import annotations

import ctypes
import os
import subprocess
import time
from ctypes import wintypes

import numpy as np

LOG_DIR = r"C:\Projects\JX\logs\re_npc_list"
os.makedirs(LOG_DIR, exist_ok=True)

k32 = ctypes.WinDLL("kernel32", use_last_error=True)
PROCESS_VM_READ = 0x0010
PROCESS_QUERY_INFORMATION = 0x0400

k32.OpenProcess.restype = wintypes.HANDLE
k32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
k32.ReadProcessMemory.restype = wintypes.BOOL
k32.ReadProcessMemory.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID,
                                  ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
k32.VirtualQueryEx.restype = ctypes.c_size_t
k32.VirtualQueryEx.argtypes = [wintypes.HANDLE, wintypes.LPCVOID, ctypes.c_void_p, ctypes.c_size_t]
k32.CloseHandle.argtypes = [wintypes.HANDLE]

STATIC_NAME = 0x006DEC44
STATIC_LEVEL = 0x006DEC66
STATIC_MAP = 0x006966CC

MEM_COMMIT = 0x1000
MEM_IMAGE = 0x1000000
MEM_MAPPED = 0x40000
MEM_PRIVATE = 0x20000
PAGE_GUARD = 0x100
PAGE_NOACCESS = 0x01

CHUNK = 8 * 1024 * 1024


class MBI(ctypes.Structure):
    _fields_ = [
        ("BaseAddress", ctypes.c_void_p),
        ("AllocationBase", ctypes.c_void_p),
        ("AllocationProtect", wintypes.DWORD),
        ("RegionSize", ctypes.c_size_t),
        ("State", wintypes.DWORD),
        ("Protect", wintypes.DWORD),
        ("Type", wintypes.DWORD),
    ]


def vggame_pids():
    out = subprocess.check_output(["tasklist", "/FI", "IMAGENAME eq vggame.exe", "/FO", "CSV", "/NH"],
                                  text=True, errors="replace")
    pids = []
    for line in out.splitlines():
        parts = [p.strip('"') for p in line.split('","')]
        if len(parts) >= 2 and parts[0].lower() == "vggame.exe":
            try:
                pids.append(int(parts[1]))
            except ValueError:
                pass
    return pids


def read_mem(h, addr, size):
    buf = (ctypes.c_char * size)()
    br = ctypes.c_size_t(0)
    if not k32.ReadProcessMemory(h, ctypes.c_void_p(addr), buf, size, ctypes.byref(br)):
        return None
    return bytes(buf)[: br.value]


def enum_regions(h):
    regions = []
    addr = 0
    mbi = MBI()
    sz = ctypes.sizeof(mbi)
    maxa = 0x7FFF0000
    while addr < maxa:
        if k32.VirtualQueryEx(h, ctypes.c_void_p(addr), ctypes.byref(mbi), sz) == 0:
            break
        base = int(mbi.BaseAddress or 0)
        size = int(mbi.RegionSize)
        if size <= 0:
            size = 0x1000
        readable = (mbi.State == MEM_COMMIT and (mbi.Protect & 0xEE) != 0
                    and (mbi.Protect & PAGE_NOACCESS) == 0 and (mbi.Protect & PAGE_GUARD) == 0)
        # Bo qua vung mapped file lon (paks) de tiet kiem thoi gian
        if readable and not (mbi.Type == MEM_MAPPED and size > 16 * 1024 * 1024):
            regions.append((base, size, int(mbi.Type)))
        addr = base + size
    return regions


def iter_chunks(h, regions):
    for base, size, typ in regions:
        off = 0
        while off < size:
            n = min(CHUNK, size - off)
            data = read_mem(h, base + off, n)
            if data:
                yield base + off, data, typ
            off += n

def sig_scan_buffer(buf: bytes, base: int, strict=True):
    """Tim chu ky property [sig1,100,0,0,sig2,Sta,MaxSta] (+HP tai -1 slot). Tra ve list (S_addr, hp, sta, ms)."""
    a = np.frombuffer(buf, dtype=np.int32)
    n = a.size
    if n < 16:
        return []
    i = np.arange(1, n - 8)
    hp = a[i - 1]
    s1 = a[i]
    v100 = a[i + 1]
    z1 = a[i + 2]
    z2 = a[i + 3]
    s2 = a[i + 4]
    st = a[i + 5]
    ms = a[i + 6]
    if strict:
        m = ((s1 >= 0) & (s1 < 200) & (v100 == 100) & (z1 == 0) & (z2 == 0) &
             (s2 >= 0) & (s2 < 200) & (hp > 0) & (hp < 50_000_000) &
             (st > 0) & (st < 100_000) & (ms >= st) & (ms < 200_000))
    else:
        m = ((s1 >= -1000) & (s1 < 1000) & (v100 >= 0) & (v100 < 10_000_000) &
             (s2 >= -1000) & (s2 < 1000) & (hp > 0) & (hp < 50_000_000) &
             (st >= 0) & (st < 200_000) & (ms >= 0) & (ms < 500_000))
    idx = np.nonzero(m)[0] + 1
    out = []
    for k in idx:
        k = int(k)
        S = base + (k + 5) * 4  # k = slot cua sig1; S = slot sig1+5
        out.append((S, int(a[k - 1]), int(a[k + 5]), int(a[k + 6])))
    return out


def find_player_struct(h, pid, log, strict=True):
    ranges = [(0x0F000000, 0x14000000), (0x30000000, 0x38000000), (0x58000000, 0x68000000)]
    found = []
    for lo, hi in ranges:
        for base, size, typ in enum_regions(h):
            if base + size <= lo or base >= hi:
                continue
            # doc trong khoang giao
            s0 = max(base, lo)
            s1 = min(base + size, hi)
            off = 0
            total = s1 - s0
            while off < total:
                n = min(CHUNK, total - off)
                data = read_mem(h, s0 + off, n)
                if data:
                    for (S, hp, st, ms) in sig_scan_buffer(data, s0 + off, strict=strict):
                        found.append((S, hp, st, ms))
                off += n
        if found:
            break
    if not found:
        log(f"  [sig/{'strict' if strict else 'loose'}] khong thay trong cac range hep -> quet toan bo")
        for base, size, typ in enum_regions(h):
            off = 0
            while off < size:
                n = min(CHUNK, size - off)
                data = read_mem(h, base + off, n)
                if data:
                    for (S, hp, st, ms) in sig_scan_buffer(data, base + off, strict=strict):
                        found.append((S, hp, st, ms))
                off += n
    return found


def validate_player(h, S):
    """Doc cac truong quanh S de xac nhan la struct nguoi choi: toa do tai +0x270/+0x274."""
    raw = read_mem(h, S - 0x40, 0x40 + 0x280)
    if not raw or len(raw) < 0x2C0:
        return None

    def at(rel):
        return int(np.frombuffer(raw[rel:rel + 4], dtype=np.int32)[0])

    hp = at(0x40 - 0x18)
    mana = at(0x40 - 0x08)
    sta = at(0x40)
    ms = at(0x40 + 4)
    cx = at(0x40 + 0x270)
    cy = at(0x40 + 0x274)
    return {"hp": hp, "mana": mana, "sta": sta, "ms": ms, "x": cx, "y": cy}

def scan_candidates(h, log):
    """Quet moi S co property-block + toa do hop le tai S+0x270/S+0x274 => candidate object (NPC/quai)."""
    cands = []
    for base, size, typ in enum_regions(h):
        off = 0
        while off < size:
            n = min(CHUNK, size - off)
            m0 = max(0, off - 0x300)
            m1 = min(size, off + n + 0x300)
            data = read_mem(h, base + m0, m1 - m0)
            if data:
                a = np.frombuffer(data[: (len(data) // 4) * 4], dtype=np.int32)
                cnt = a.size
                off_in = (off - m0) // 4
                j0 = max(6, off_in)
                j1 = min(cnt - 158, off_in + n // 4)
                if j1 > j0:
                    j = np.arange(j0, j1)
                    hp = a[j - 6]
                    s1 = a[j - 5]
                    s2 = a[j - 1]
                    st = a[j]
                    ms = a[j + 1]
                    cx = a[j + 156]
                    cy = a[j + 157]
                    m = ((hp > 0) & (hp < 50_000_000) & (s1 > -1000) & (s1 < 1000) &
                         (s2 > -1000) & (s2 < 1000) & (st >= 0) & (st < 200_000) &
                         (ms >= st) & (ms < 1_000_000) &
                         (cx > 0) & (cx < 0x800000) & (cy > 0) & (cy < 0x800000))
                    for k0 in np.nonzero(m)[0]:
                        k = int(k0) + j0  # FIX: index theo mang j -> slot that
                        S = base + m0 + k * 4
                        cands.append((S, int(a[k - 6]), int(a[k - 5]), int(a[k - 1]),
                                      int(a[k]), int(a[k + 1]), int(a[k + 156]), int(a[k + 157])))
            off += n
    cands.sort()
    out = []
    for c in cands:
        if out and c[0] - out[-1][0] < 0x40:
            continue
        out.append(c)
    log(f"  [candidates] {len(out)} doi tuong (da dedup tu {len(cands)})")
    return out


def merged_intervals(targets, radius):
    ints = sorted((max(0, t - radius), t + radius) for t in targets)
    merged = []
    for s, e in ints:
        if merged and s <= merged[-1][1] + 4:
            merged[-1] = (merged[-1][0], max(merged[-1][1], e))
        else:
            merged.append((s, e))
    return merged


def pointer_hits(h, targets, radius, static_only=False, label=""):
    """Tra ve list (hit_addr, value) noi value nam trong [t-radius, t+radius] voi t thuoc targets."""
    merged = merged_intervals(targets, radius)
    if not merged:
        return []
    starts = np.array([s for s, _ in merged], dtype=np.uint32)
    ends = np.array([e for _, e in merged], dtype=np.uint32)
    hits = []
    for base, size, typ in enum_regions(h):
        if static_only and base >= 0x10000000:
            continue
        off = 0
        while off < size:
            n = min(CHUNK, size - off)
            data = read_mem(h, base + off, n)
            if data:
                a = np.frombuffer(data[: (len(data) // 4) * 4], dtype=np.uint32)
                pos = np.searchsorted(starts, a, side="right") - 1
                vmask = pos >= 0
                if vmask.any():
                    idx = np.nonzero(vmask)[0]
                    pv = pos[idx]
                    ok = a[idx] <= ends[pv]
                    for k in idx[ok]:
                        k = int(k)
                        hits.append((base + off + k * 4, int(a[k])))
            off += n
    hits.sort()
    return hits


def find_runs(hits, min_len=3):
    runs = []
    cur = []
    for hh in hits:
        if cur and hh[0] - cur[-1][0] == 4:
            cur.append(hh)
        else:
            if len(cur) >= min_len:
                runs.append(cur)
            cur = [hh]
    if len(cur) >= min_len:
        runs.append(cur)
    return runs

def dump_window(h, addr, size, log, label=""):
    raw = read_mem(h, addr, size)
    if not raw:
        log(f"   dump fail @0x{addr:X}")
        return
    log(f"   --- dump {label} @0x{addr:X} ({size} bytes) ---")
    for off in range(0, len(raw), 32):
        chunk = raw[off:off + 32]
        hexs = " ".join(f"{b:02X}" for b in chunk)
        asc = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
        log(f"   {addr + off:08X}: {hexs}  {asc}")


def main():
    import bisect
    from collections import Counter

    pids = vggame_pids()
    print(f"vggame PIDs: {pids}")
    for pid in pids:
        log_path = os.path.join(LOG_DIR, f"phase1_pid{pid}.txt")
        lines = []
        t0 = time.time()

        def log(msg, _lines=lines, _path=log_path):
            s = f"[{time.strftime('%H:%M:%S')}] {msg}"
            print(s)
            _lines.append(s)
            with open(_path, "w", encoding="utf-8") as f:
                f.write("\n".join(_lines))

        log(f"===== PID {pid} =====")
        h = k32.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid)
        if not h:
            log(f"OpenProcess FAILED err={ctypes.get_last_error()}")
            continue
        try:
            nmB = read_mem(h, STATIC_NAME, 32)
            lvB = read_mem(h, STATIC_LEVEL, 4)
            mpB = read_mem(h, STATIC_MAP, 8)
            name = nmB.split(b"\x00", 1)[0].decode("latin1", "replace") if nmB else "?"
            level = int.from_bytes(lvB, "little") if lvB else -1
            mapid = int.from_bytes(mpB[:4], "little") if mpB else -1
            log(f"name='{name}' level={level} mapId={mapid}")

            chosen = []
            matches = find_player_struct(h, pid, log, strict=True)
            log(f"  [sig/strict] matches={len(matches)}")
            for (S, hp, st, ms) in matches[:20]:
                v = validate_player(h, S)
                ok = bool(v and v["sta"] > 0 and v["x"] > 0 and v["y"] > 0)
                log(f"   S=0x{S:X} hp={hp} sta={st}/{ms} " + (f"coords=({v['x']},{v['y']}) HUD=({round(v['x']/256)},{round(v['y']/512)})" if v else "read-fail") + (" VALID" if ok else ""))
                if ok:
                    chosen.append((S, v))
            if not chosen:
                log("  strict khong co struct hop le -> thu loose")
                matches2 = find_player_struct(h, pid, log, strict=False)
                log(f"  [sig/loose] matches={len(matches2)}")
                for (S, hp, st, ms) in matches2[:40]:
                    v = validate_player(h, S)
                    ok = bool(v and v["sta"] > 0 and v["x"] > 0 and v["y"] > 0)
                    if ok:
                        chosen.append((S, v))
                        log(f"   LOOSE S=0x{S:X} hp={hp} sta={st}/{ms} coords=({v['x']},{v['y']}) VALID")
            if not chosen:
                log("  khong co struct hop le -> skip")
                continue
            S_p, vp = chosen[0]
            px, py = vp["x"], vp["y"]
            log(f"  => PlayerS=0x{S_p:X} coords=({px},{py}) HUD=({round(px/256)},{round(py/512)})")

            hitsP = [t for t in pointer_hits(h, [S_p], 0x400, static_only=False) if (t[1] & 3) == 0 and (t[0] & 3) == 0]
            log(f"  [passA] refs aligned quanh S_p (±0x400): {len(hitsP)}")
            for d, c in Counter(S_p - val for (_, val) in hitsP).most_common(12):
                log(f"     delta={d} (0x{d & 0xFFFFFFFF:X}) x{c}")

            cands = scan_candidates(h, log)
            if cands:
                cands.sort(key=lambda c: (c[6] - px) ** 2 + (c[7] - py) ** 2)
                log("  [passB] top 15 gan nhat:")
                for c in cands[:15]:
                    log(f"     S=0x{c[0]:X} hp={c[1]} sig=({c[2]},{c[3]}) sta={c[4]}/{c[5]} "
                        f"coords=({c[6]},{c[7]}) dHUD~({round((c[6]-px)/256)},{round((c[7]-py)/512)})")
                for c in cands[:2]:
                    dump_window(h, c[0] - 0x40, 0x340, log, f"cand0x{c[0]:X}")

                cand_targets = [c[0] for c in cands[:250]]
                hitsC = pointer_hits(h, cand_targets, 0x800, static_only=False)
                hitsC = [t for t in hitsC if (t[1] & 3) == 0]
                log(f"  [passC] refs (aligned) toi candidates: {len(hitsC)}")
                runs = find_runs(hitsC, min_len=3)
                log(f"  [passC] runs (>=3 slot lien tuc): {len(runs)}")
                cset = sorted(c[0] for c in cands)
                scored = []
                for run in runs:
                    matched = 0
                    for _, val in run:
                        i = bisect.bisect_left(cset, val - 0x800)
                        if i < len(cset) and cset[i] <= val + 0x800:
                            matched += 1
                    scored.append((matched, len(run), run[0][0], [v for _, v in run][:12]))
                scored.sort(reverse=True)
                for (m, ln, base_addr, sample) in scored[:20]:
                    log(f"     RUN base=0x{base_addr:X} len={ln} matched={m} sample_vals={[hex(v) for v in sample]}")

                top_bases = [s[2] for s in scored[:10] if s[0] > 0]
                if top_bases:
                    hitsD = pointer_hits(h, top_bases, 0x40, static_only=True)
                    log(f"  [passD] static/global refs toi run bases: {len(hitsD)}")
                    for (hit, val) in hitsD[:20]:
                        log(f"     STATIC? 0x{hit:X} -> 0x{val:X}")
                    if hitsD:
                        lvl2 = pointer_hits(h, [hit for hit, _ in hitsD[:20]], 0x8, static_only=True)
                        log(f"  [passD.2] refs level-2: {len(lvl2)}")
                        for (hit, val) in lvl2[:20]:
                            log(f"     STATIC2 0x{hit:X} -> 0x{val:X}")
        except Exception as ex:
            import traceback
            log(f"EXCEPTION: {ex}")
            log(traceback.format_exc())
        finally:
            k32.CloseHandle(h)
            log(f"===== PID {pid} done in {time.time() - t0:.1f}s =====")
    print("ALL DONE")


if __name__ == "__main__":
    main()



