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;
}
}