using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; using JXHelper.Platform; namespace JXHelper.Core; /// /// Mô hình lưu trữ chỉ số sinh tồn và trạng thái thực tế của nhân vật đọc trực tiếp từ RAM game. /// Khớp đúng 100% chuẩn: Sinh lực (HP), Thể lực (Stamina), Nội lực (Mana), Cấp độ (Level). /// public record CharacterLiveStats( string CharacterName, int Level, int CurrentHp, int MaxHp, int CurrentStamina, int MaxStamina, int CurrentMana, int MaxMana, long StructAddress, int MapId = 0, string MapName = "", int CoordX = 0, int CoordY = 0) { public double HpPercent => MaxHp > 0 ? (double)CurrentHp / MaxHp * 100.0 : 100.0; public double StaminaPercent => MaxStamina > 0 ? (double)CurrentStamina / MaxStamina * 100.0 : 100.0; public double ManaPercent => MaxMana > 0 ? (double)CurrentMana / MaxMana * 100.0 : 100.0; public int HudX => CoordX > 0 ? (int)Math.Round(CoordX / 256.0) : 0; public int HudY => CoordY > 0 ? (int)Math.Round(CoordY / 512.0) : 0; public override string ToString() => $"[{CharacterName} Lv.{Level} | {MapName} ({HudX}/{HudY})] HP: {CurrentHp}/{MaxHp} | Stamina: {CurrentStamina}/{MaxStamina} | Mana: {CurrentMana}/{MaxMana}"; } /// /// Động cơ đọc chỉ số nhân vật thời gian thực từ bộ nhớ RAM tiến trình vggame.exe. /// Hỗ trợ Fast-Path với O(1) Cache Address và Auto-Scan chữ ký cấu trúc [43, 100, 0, Mana, 39, Stamina, MaxStamina]. /// public static class PlayerStatsReader { // Cache địa chỉ struct theo từng Process ID để đọc siêu tốc 0.001ms private static readonly ConcurrentDictionary CachedStaminaAddrs = new(); /// /// Xóa cache địa chỉ struct của một tiến trình khi thoát hoặc chuyển map. /// public static void InvalidateCache(int pid) { CachedStaminaAddrs.TryRemove(pid, out _); } /// /// Đọc trực tiếp toàn bộ chỉ số thực địa của nhân vật từ bộ nhớ RAM của tiến trình game. /// public static CharacterLiveStats? ReadLiveStats(int pid) { if (pid <= 0) return null; CharacterNameResolver.EnableDebugPrivilege(); IntPtr hProcess = NativeMethods.OpenProcess( NativeMethods.PROCESS_QUERY_INFORMATION | NativeMethods.PROCESS_VM_READ, false, pid); if (hProcess == IntPtr.Zero) return null; try { // 1. Đọc tên nhân vật, Cấp độ và Map ID từ địa chỉ tĩnh (INV-STATIC-RAM-LOCAL-PLAYER) string charName = ReadStaticPlayerName(hProcess); int level = ReadStaticPlayerLevel(hProcess); int mapId = ReadStaticPlayerMapId(hProcess); string mapName = MapDatabase.GetMapName(mapId); // 2. Fast Path: Thử đọc từ cached stamina address if (CachedStaminaAddrs.TryGetValue(pid, out long cachedAddr) && cachedAddr > 0) { var stats = TryReadStructAtAddress(hProcess, cachedAddr, charName, level, mapId, mapName); if (stats != null) { return stats; } // Địa chỉ cũ không còn hợp lệ, xóa cache để scan lại CachedStaminaAddrs.TryRemove(pid, out _); } // 2.5 Seed Candidates: Thử các địa chỉ struct đã được định vị qua telemetry thực địa long[] seedAddrs = new[] { 0x10A2C43CL, 0x10CA5E44L, 0x10D3EFBCL, 0x10DE8BACL, 0x10B786D4L, 0x1092343CL, 0x10B7C6D4L, 0x10BC65B4L }; foreach (var sAddr in seedAddrs) { var seedStats = TryReadStructAtAddress(hProcess, sAddr, charName, level, mapId, mapName); if (seedStats != null) { CachedStaminaAddrs[pid] = sAddr; return seedStats; } } // 3. Slow Path: Quét tìm chữ ký cấu trúc [43, 100, 0, Mana, 39, Stamina, MaxStamina] long foundAddr = ScanForPlayerStruct(hProcess); if (foundAddr > 0) { CachedStaminaAddrs[pid] = foundAddr; return TryReadStructAtAddress(hProcess, foundAddr, charName, level, mapId, mapName); } // Nếu không tìm thấy struct động, vẫn trả về tên, cấp và map từ RAM tĩnh if (!string.IsNullOrEmpty(charName)) { return new CharacterLiveStats( charName, level, CurrentHp: 0, MaxHp: 0, CurrentStamina: 0, MaxStamina: 0, CurrentMana: 0, MaxMana: 0, StructAddress: 0, MapId: mapId, MapName: mapName); } } catch { // Bỏ qua lỗi đọc bộ nhớ an toàn } finally { NativeMethods.CloseHandle(hProcess); } return null; } /// /// Tìm kiếm và đọc chỉ số thời gian thực của nhân vật theo tên (phục vụ tự tìm và bám sát Acc Chính / Leader). /// public static CharacterLiveStats? FindCharacterLiveStats(string characterName, IEnumerable? candidatePids = null) { if (string.IsNullOrWhiteSpace(characterName)) return null; var pids = candidatePids?.ToList(); if (pids == null || pids.Count == 0) { var windows = WindowManager.FindGameWindows(); var targetWin = windows.FirstOrDefault(w => string.Equals(w.CharacterName, characterName, StringComparison.OrdinalIgnoreCase)); if (targetWin != null) { return ReadLiveStats(targetWin.ProcessId); } pids = windows.Select(w => w.ProcessId).Distinct().ToList(); } foreach (var pid in pids) { var stats = ReadLiveStats(pid); if (stats != null && string.Equals(stats.CharacterName, characterName, StringComparison.OrdinalIgnoreCase)) { return stats; } } return null; } private static string ReadStaticPlayerName(IntPtr hProcess) { byte[] buf = new byte[32]; if (NativeMethods.ReadProcessMemory(hProcess, (IntPtr)CharacterNameResolver.StaticPlayerNameAddress, buf, (IntPtr)buf.Length, out var read) && read.ToInt64() > 0) { return Encoding.ASCII.GetString(buf).TrimEnd('\0').Trim(); } return string.Empty; } private static int ReadStaticPlayerLevel(IntPtr hProcess) { byte[] buf = new byte[2]; if (NativeMethods.ReadProcessMemory(hProcess, (IntPtr)CharacterNameResolver.StaticPlayerLevelAddress, buf, (IntPtr)2, out var read) && read.ToInt64() >= 2) { return BitConverter.ToInt16(buf, 0); } return 0; } private static int ReadStaticPlayerMapId(IntPtr hProcess) { long[] candidateAddrs = new[] { CharacterNameResolver.StaticPlayerMapIdAddress, // 0x006966CC 0x006966D0, 0x0068C274, 0x0068CA74 }; byte[] buf = new byte[4]; foreach (var addr in candidateAddrs) { if (NativeMethods.ReadProcessMemory(hProcess, (IntPtr)addr, buf, (IntPtr)4, out var read) && read.ToInt64() >= 4) { int id = BitConverter.ToInt32(buf, 0); if (id > 0 && id < 2000) { return id; } } } return 0; } /// /// Thử đọc và xác thực cấu trúc chỉ số tại địa chỉ Stamina Base đã biết. /// Cấu trúc: /// [Base - 0x18]: int32 CurHP /// [Base - 0x14]: int32 Const_43 (Signature) /// [Base - 0x10]: int32 Const_100 (Signature) /// [Base - 0x0C]: int32 Const_0 (Signature) /// [Base - 0x08]: int32 CurMana /// [Base - 0x04]: int32 Const_39 (Signature) /// [Base + 0x00]: int32 CurStamina /// [Base + 0x04]: int32 MaxStamina /// [Base + 0x270]: int32 CoordX (Engine sub-world pixel) /// [Base + 0x274]: int32 CoordY (Engine sub-world pixel) /// private static CharacterLiveStats? TryReadStructAtAddress(IntPtr hProcess, long staminaAddr, string charName, int level, int mapId, string mapName) { long readBase = staminaAddr - 24; byte[] buffer = new byte[36]; // từ -24 đến +12 if (NativeMethods.ReadProcessMemory(hProcess, (IntPtr)readBase, buffer, (IntPtr)buffer.Length, out var readBytes) && readBytes.ToInt64() >= 32) { int sig43 = BitConverter.ToInt32(buffer, 4); int sig100 = BitConverter.ToInt32(buffer, 8); int sig0 = BitConverter.ToInt32(buffer, 12); int sig39 = BitConverter.ToInt32(buffer, 20); try { System.IO.File.AppendAllText(@"c:\Projects\JX\logs\player_stats_debug.log", $"TryRead {staminaAddr:X8}: bytesRead={readBytes}, sig43={sig43}, sig100={sig100}, sig0={sig0}, sig39={sig39}\n"); } catch {} // Kiểm tra chữ ký hợp lệ: sig100 == 100 && sig0 == 0, và sig43/sig39 là các mã thuộc tính nội bộ (0..200) if (sig100 == 100 && sig0 == 0 && sig43 >= 0 && sig43 < 200 && sig39 >= 0 && sig39 < 200) { int hp = BitConverter.ToInt32(buffer, 0); int mana = BitConverter.ToInt32(buffer, 16); int stamina = BitConverter.ToInt32(buffer, 24); int maxStamina = BitConverter.ToInt32(buffer, 28); // Ước tính MaxHP và MaxMana dựa trên chỉ số thực tế int maxHp = hp > 0 ? hp : 1; int maxMana = mana > 0 ? mana : 1; // Đọc tọa độ thế giới (X, Y) tại [Base + 0x0270] và [Base + 0x0274] int coordX = 0; int coordY = 0; byte[] coordBuf = new byte[8]; if (NativeMethods.ReadProcessMemory(hProcess, (IntPtr)(staminaAddr + 0x0270), coordBuf, (IntPtr)8, out var cRead) && cRead.ToInt64() == 8) { coordX = BitConverter.ToInt32(coordBuf, 0); coordY = BitConverter.ToInt32(coordBuf, 4); } return new CharacterLiveStats( charName, level, CurrentHp: hp, MaxHp: maxHp, CurrentStamina: stamina, MaxStamina: maxStamina, CurrentMana: mana, MaxMana: maxMana, StructAddress: staminaAddr, MapId: mapId, MapName: mapName, CoordX: coordX, CoordY: coordY); } } return null; } /// /// Quét các vùng bộ nhớ Committed Memory tìm kiếm cấu trúc chứa chữ ký [43, 100, 0, Mana, 39, Stamina, MaxStamina]. /// private static long ScanForPlayerStruct(IntPtr hProcess) { // 1. FAST PATH: Quét dải địa chỉ người chơi 0x10000000 - 0x12000000 trước (tốc độ < 10ms) long fastAddr = ScanMemoryRange(hProcess, 0x10000000, 0x12000000); if (fastAddr > 0) return fastAddr; // 2. SLOW PATH: Quét toàn bộ không gian bộ nhớ nếu chưa tìm thấy return ScanMemoryRange(hProcess, 0x00400000, 0x7FFFFFFF); } private static long ScanMemoryRange(IntPtr hProcess, long startAddress, long maxAddress) { long address = startAddress; var mbi = new NativeMethods.MEMORY_BASIC_INFORMATION(); int mbiSize = Marshal.SizeOf(typeof(NativeMethods.MEMORY_BASIC_INFORMATION)); byte[] buffer = new byte[1024 * 1024 * 8]; // 8MB chunk while (address < maxAddress) { if (NativeMethods.VirtualQueryEx(hProcess, (IntPtr)address, out mbi, (IntPtr)mbiSize) == IntPtr.Zero) break; long baseAddr = mbi.BaseAddress.ToInt64(); long regionSize = mbi.RegionSize.ToInt64(); if (regionSize <= 0) break; bool isReadable = mbi.State == 0x1000 && (mbi.Protect & 0xEE) != 0 && (mbi.Protect & 0x01) == 0; if (isReadable) { int toRead = (int)Math.Min(buffer.Length, regionSize); if (NativeMethods.ReadProcessMemory(hProcess, (IntPtr)baseAddr, buffer, (IntPtr)toRead, out var bytesRead) && bytesRead.ToInt64() >= 32) { int readLen = (int)bytesRead.ToInt64(); int maxIdx = readLen - 28; for (int i = 4; i <= maxIdx; i += 4) { int sigVal = BitConverter.ToInt32(buffer, i); // Kiểm tra 12 bytes chữ ký: sigVal trong [0..200], 100, 0 if (sigVal >= 0 && sigVal < 200 && buffer[i + 4] == 100 && buffer[i + 5] == 0 && buffer[i + 6] == 0 && buffer[i + 7] == 0 && buffer[i + 8] == 0 && buffer[i + 9] == 0 && buffer[i + 10] == 0 && buffer[i + 11] == 0) { // Kiểm tra chữ ký property ID thứ hai trong [0..200] tại i + 16 int sigA = BitConverter.ToInt32(buffer, i + 16); if (sigA >= 0 && sigA < 200) { int hp = BitConverter.ToInt32(buffer, i - 4); int stamina = BitConverter.ToInt32(buffer, i + 20); int maxStamina = BitConverter.ToInt32(buffer, i + 24); // Bỏ qua struct mẫu / rác (HP=0 hoặc Stamina=0) để tìm đúng struct người chơi thật if (hp > 0 && stamina > 0 && maxStamina > 0 && maxStamina < 10000) { // Stamina nằm ở offset i + 20 return baseAddr + i + 20; } } } } } } address = baseAddr + regionSize; } return 0; } }