import sys, struct, ctypes, json
from ctypes import wintypes
import psutil

output_file = r"C:\Projects\JX\logs\verified_4_chars_stats.txt"

def run():
    lines = []
    def log(msg):
        print(msg)
        lines.append(str(msg))

    kernel32 = ctypes.windll.kernel32
    advapi32 = ctypes.windll.advapi32

    # Enable SeDebugPrivilege
    hToken = wintypes.HANDLE()
    if advapi32.OpenProcessToken(kernel32.GetCurrentProcess(), 0x0020 | 0x0008, ctypes.byref(hToken)):
        luid = wintypes.LARGE_INTEGER()
        if advapi32.LookupPrivilegeValueW(None, "SeDebugPrivilege", ctypes.byref(luid)):
            class TP(ctypes.Structure):
                _fields_ = [('Count', wintypes.DWORD), ('Luid', wintypes.LARGE_INTEGER), ('Attr', wintypes.DWORD)]
            tp = TP(1, luid, 2)
            advapi32.AdjustTokenPrivileges(hToken, False, ctypes.byref(tp), ctypes.sizeof(tp), None, None)
        kernel32.CloseHandle(hToken)

    class MEMORY_BASIC_INFORMATION(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),
        ]

    # Map PIDs to character names
    procs = []
    for p in psutil.process_iter(['pid', 'name']):
        if (p.info['name'] or '').lower() in ('vggame.exe', 'game.exe'):
            procs.append(p.info['pid'])

    log(f"Found game PIDs: {procs}")

    final_results = []

    for pid in procs:
        hProc = kernel32.OpenProcess(0x0010 | 0x0400, False, pid)
        if not hProc:
            log(f"PID {pid}: Cannot OpenProcess")
            continue

        bytesRead = ctypes.c_size_t()

        # Read static name at 0x006DEC44
        nbuf = (ctypes.c_char * 32)()
        kernel32.ReadProcessMemory(hProc, ctypes.c_void_p(0x006DEC44), nbuf, 32, ctypes.byref(bytesRead))
        name = bytes(nbuf).split(b'\x00')[0].decode('latin1', errors='ignore').strip()

        # Read static level at 0x006DEC66
        lbuf = (ctypes.c_short * 1)()
        kernel32.ReadProcessMemory(hProc, ctypes.c_void_p(0x006DEC66), lbuf, 2, ctypes.byref(bytesRead))
        lvl = lbuf[0]

        log(f"\n=======================================================")
        log(f"PID {pid} | TÊN NHÂN VẬT: '{name}' | CẤP ĐỘ: {lvl}")
        log(f"=======================================================")

        # We know that the player struct contains:
        # - Level (lvl) repeated multiple times at offset around +0xD0 ~ +0xE0 from HP
        # - Stamina: CurStamina, MaxStamina as two consecutive identical/near-identical int32s (e.g. 500-1500)
        # - HP: CurHP at offset -0x18 from CurStamina!
        # Let's search all committed memory for the player struct!
        
        mbi = MEMORY_BASIC_INFORMATION()
        cur_addr = 0
        best_struct = None

        pat_lvl5 = struct.pack('<iiiii', lvl, lvl, lvl, lvl, lvl)

        while kernel32.VirtualQueryEx(hProc, ctypes.c_void_p(cur_addr), ctypes.byref(mbi), ctypes.sizeof(mbi)):
            if mbi.State == 0x1000 and (mbi.Protect & 0xEE) != 0 and (mbi.Protect & 0x01) == 0:
                rsize = min(mbi.RegionSize, 1024 * 1024 * 8)
                buf = (ctypes.c_char * rsize)()
                if kernel32.ReadProcessMemory(hProc, ctypes.c_void_p(cur_addr), buf, rsize, ctypes.byref(bytesRead)):
                    data = bytes(buf)[:bytesRead.value]
                    
                    # Pattern search for 5 consecutive level ints: [lvl, lvl, lvl, lvl, lvl]
                    pos = 0
                    while True:
                        pos = data.find(pat_lvl5, pos)
                        if pos == -1: break
                        
                        lvl_addr = cur_addr + pos
                        # If this is the player struct, HP is at lvl_addr - 0xF0 (240 bytes before)
                        # Stamina is at lvl_addr - 0xF0 + 0x18 = lvl_addr - 0xD8
                        hp_addr = lvl_addr - 0xD4 # wait, let's calculate relative:
                        # In BoCuaCacCon:
                        # HP was at 0x10B786BC
                        # Level 75 (5 ints) was at 0x10B78790: (0x10B78790 - 0x10B786BC = +0xD4 = 212 bytes)
                        # Stamina was at 0x10B786D4: (0x10B786D4 - 0x10B786BC = +0x18 = 24 bytes)
                        calc_hp_addr = lvl_addr - 212
                        calc_sta_addr = calc_hp_addr + 24
                        calc_mana_addr = calc_hp_addr + 172
                        
                        # Check if within bounds
                        rel_hp_offset = pos - 212
                        if 0 <= rel_hp_offset <= len(data) - 250:
                            hp_val = struct.unpack_from('<i', data, rel_hp_offset)[0]
                            sta_val = struct.unpack_from('<i', data, rel_hp_offset + 24)[0]
                            max_sta_val = struct.unpack_from('<i', data, rel_hp_offset + 28)[0]
                            mana_val = struct.unpack_from('<i', data, rel_hp_offset + 172)[0]
                            max_mana_val = struct.unpack_from('<i', data, rel_hp_offset + 180)[0]
                            
                            log(f"  [TÌM THẤY PLAYER STRUCT] tại 0x{calc_hp_addr:08X} (Lvl5 tại 0x{lvl_addr:08X}):")
                            log(f"    * Sinh lực (HP)       : {hp_val} (Addr: 0x{calc_hp_addr:08X})")
                            log(f"    * Thể lực (Stamina)   : {sta_val} / {max_sta_val} (Addr: 0x{calc_sta_addr:08X})")
                            log(f"    * Nội lực (Mana)      : {mana_val} / {max_mana_val} (Addr: 0x{calc_mana_addr:08X})")
                            log(f"    * Cấp độ              : {lvl} (Addr: 0x{lvl_addr:08X})")
                            
                            best_struct = {
                                "pid": pid,
                                "name": name,
                                "level": lvl,
                                "hp": hp_val,
                                "stamina_cur": sta_val,
                                "stamina_max": max_sta_val,
                                "mana_cur": mana_val,
                                "mana_max": max_mana_val,
                                "hp_addr": f"0x{calc_hp_addr:08X}",
                                "stamina_addr": f"0x{calc_sta_addr:08X}",
                                "mana_addr": f"0x{calc_mana_addr:08X}",
                                "level_addr": f"0x{lvl_addr:08X}"
                            }
                            break
                        pos += 4
            cur_addr += mbi.RegionSize
            if cur_addr >= 0x7FFFFFFF: break
            if best_struct: break

        if best_struct:
            final_results.append(best_struct)
        kernel32.CloseHandle(hProc)

    with open(output_file, 'w', encoding='utf-8') as f:
        f.write("\n".join(lines))
        f.write("\n\n" + json.dumps(final_results, ensure_ascii=False, indent=2))
    print("Done verify all 4 chars.")

if __name__ == '__main__':
    run()
