506 lines
19 KiB
C#
506 lines
19 KiB
C#
|
|
|
|
namespace MemoryViewer
|
|
{
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using System.Text;
|
|
using GameHelper;
|
|
using GameHelper.Plugin;
|
|
using GameHelper.Utils;
|
|
using GameOffsets;
|
|
using ImGuiNET;
|
|
using Newtonsoft.Json;
|
|
|
|
|
|
public sealed class MemoryViewer : PCore<MemoryViewerSettings>
|
|
{
|
|
private bool isWindowVisible = false;
|
|
private DateTime lastRefreshTime = DateTime.MinValue;
|
|
private MemoryValidationData validationData = new MemoryValidationData();
|
|
|
|
private struct MemoryValidationData
|
|
{
|
|
public bool GameStatesValid;
|
|
public IntPtr GameStatesAddress;
|
|
public IntPtr InGameStateAddress;
|
|
public IntPtr AreaInstanceAddress;
|
|
public IntPtr IngameDataAddress;
|
|
public IntPtr EntityListAddress;
|
|
public IntPtr LocalPlayerAddress;
|
|
public Dictionary<string, AddressInfo> Addresses;
|
|
}
|
|
|
|
private struct AddressInfo
|
|
{
|
|
public string Name;
|
|
public IntPtr Address;
|
|
public bool IsValid;
|
|
public bool IsReadable;
|
|
public string ValueHex;
|
|
public string Description;
|
|
}
|
|
|
|
|
|
public override void OnEnable(bool isGameOpened)
|
|
{
|
|
LoadSettings();
|
|
}
|
|
|
|
|
|
public override void OnDisable()
|
|
{
|
|
SaveSettings();
|
|
}
|
|
|
|
|
|
public override void SaveSettings()
|
|
{
|
|
try
|
|
{
|
|
var settingsPath = Path.Combine(this.DllDirectory, "settings.json");
|
|
var json = JsonConvert.SerializeObject(this.Settings, Formatting.Indented);
|
|
File.WriteAllText(settingsPath, json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CategorizedLogger.LogError($"[MemoryViewer] Failed to save settings: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void LoadSettings()
|
|
{
|
|
try
|
|
{
|
|
var settingsPath = Path.Combine(this.DllDirectory, "settings.json");
|
|
if (File.Exists(settingsPath))
|
|
{
|
|
var json = File.ReadAllText(settingsPath);
|
|
this.Settings = JsonConvert.DeserializeObject<MemoryViewerSettings>(json) ?? new MemoryViewerSettings();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CategorizedLogger.LogError($"[MemoryViewer] Failed to load settings: {ex.Message}");
|
|
this.Settings = new MemoryViewerSettings();
|
|
}
|
|
}
|
|
|
|
|
|
public override void DrawSettings()
|
|
{
|
|
if (ImGui.CollapsingHeader("Memory Viewer Settings"))
|
|
{
|
|
var hexBytes = this.Settings.HexViewBytes;
|
|
if (ImGui.InputInt("Hex View Bytes", ref hexBytes, 64, 256))
|
|
{
|
|
this.Settings.HexViewBytes = Math.Max(64, Math.Min(4096, hexBytes));
|
|
}
|
|
|
|
ImGui.Checkbox("Auto Refresh", ref this.Settings.AutoRefresh);
|
|
|
|
if (this.Settings.AutoRefresh)
|
|
{
|
|
var interval = this.Settings.RefreshIntervalMs;
|
|
if (ImGui.InputInt("Refresh Interval (ms)", ref interval, 100, 500))
|
|
{
|
|
this.Settings.RefreshIntervalMs = Math.Max(100, Math.Min(10000, interval));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public override void DrawUI()
|
|
{
|
|
if (Core.Process.Handle == null || Core.Process.Handle.IsInvalid || Core.Process.Handle.IsClosed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
|
|
if (this.Settings.AutoRefresh &&
|
|
(DateTime.Now - lastRefreshTime).TotalMilliseconds >= this.Settings.RefreshIntervalMs)
|
|
{
|
|
RefreshMemoryData();
|
|
lastRefreshTime = DateTime.Now;
|
|
}
|
|
|
|
ImGui.SetNextWindowSize(new Vector2(900, 700), ImGuiCond.FirstUseEver);
|
|
if (ImGui.Begin("Memory Viewer", ref this.isWindowVisible))
|
|
{
|
|
DrawMemoryViewer();
|
|
ImGui.End();
|
|
}
|
|
}
|
|
|
|
private void DrawMemoryViewer()
|
|
{
|
|
ImGui.TextWrapped("Memory Address Validator - View and validate game memory addresses");
|
|
ImGui.Separator();
|
|
|
|
if (ImGui.Button("Refresh Data"))
|
|
{
|
|
RefreshMemoryData();
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
if (ImGui.Button("Dump to Log"))
|
|
{
|
|
DumpToLog();
|
|
}
|
|
|
|
ImGui.Separator();
|
|
|
|
|
|
if (ImGui.CollapsingHeader("GameStates Structure"))
|
|
{
|
|
DrawAddressInfo("GameStates", validationData.GameStatesAddress, validationData.GameStatesValid,
|
|
"Main GameStates pointer - Base offset for all game state access");
|
|
|
|
if (validationData.GameStatesValid && validationData.GameStatesAddress != IntPtr.Zero)
|
|
{
|
|
DrawOffsetInfo("InGameState Index", 4, "Index in GameStates array");
|
|
}
|
|
}
|
|
|
|
|
|
if (ImGui.CollapsingHeader("InGameState Structure"))
|
|
{
|
|
DrawAddressInfo("InGameState", validationData.InGameStateAddress, validationData.InGameStateAddress != IntPtr.Zero,
|
|
"Current InGameState pointer");
|
|
|
|
if (validationData.InGameStateAddress != IntPtr.Zero)
|
|
{
|
|
DrawOffsetInfo("AreaInstanceData", 0x948, "AreaInstanceData offset");
|
|
DrawOffsetInfo("IngameData Offset", 0x370, "IngameData structure offset");
|
|
}
|
|
}
|
|
|
|
|
|
if (ImGui.CollapsingHeader("IngameData Structure"))
|
|
{
|
|
DrawAddressInfo("IngameData", validationData.IngameDataAddress, validationData.IngameDataAddress != IntPtr.Zero,
|
|
"IngameData structure pointer");
|
|
|
|
if (validationData.IngameDataAddress != IntPtr.Zero)
|
|
{
|
|
DrawOffsetInfo("Entity List", 0x490, "EntityList pointer offset");
|
|
DrawOffsetInfo("Entities Count", 0x498, "Entity count offset");
|
|
DrawOffsetInfo("Local Player", 0x408, "Local player pointer offset");
|
|
}
|
|
}
|
|
|
|
|
|
if (ImGui.CollapsingHeader("AreaInstance Structure"))
|
|
{
|
|
DrawAddressInfo("AreaInstance", validationData.AreaInstanceAddress, validationData.AreaInstanceAddress != IntPtr.Zero,
|
|
"AreaInstance structure pointer");
|
|
|
|
if (validationData.AreaInstanceAddress != IntPtr.Zero)
|
|
{
|
|
DrawOffsetInfo("Local Player Ptr", 0xA10, "Local player pointer");
|
|
DrawOffsetInfo("Entity List Ptr", 0xB50, "Entity list pointer (was 0x13F8)");
|
|
DrawOffsetInfo("Terrain List Ptr", 0x12C8, "Terrain/Exits list pointer");
|
|
DrawOffsetInfo("Terrain Grid Ptr", 0x08, "Terrain grid pointer (was 0x30)");
|
|
DrawOffsetInfo("Terrain Dimensions", 0x28, "Terrain dimensions pointer");
|
|
}
|
|
}
|
|
|
|
|
|
if (ImGui.CollapsingHeader("Entity Structure"))
|
|
{
|
|
DrawOffsetInfo("Component List Ptr", 0x10, "ComponentList pointer (inside ItemBase)");
|
|
}
|
|
|
|
|
|
if (ImGui.CollapsingHeader("Component Offsets"))
|
|
{
|
|
DrawOffsetInfo("Component Index Debuffs", 3, "Debuff component index");
|
|
DrawOffsetInfo("Component Owner Entity", 0x08, "Entity pointer in ComponentHeader");
|
|
}
|
|
|
|
|
|
if (ImGui.CollapsingHeader("Life Component Offsets"))
|
|
{
|
|
DrawOffsetInfo("Health", 0x1A8, "Health VitalStruct offset");
|
|
DrawOffsetInfo("Mana", 0x1F8, "Mana VitalStruct offset");
|
|
DrawOffsetInfo("Energy Shield", 0x230, "Energy Shield VitalStruct offset");
|
|
DrawOffsetInfo("Buffs", 0x58, "Buffs offset");
|
|
}
|
|
|
|
|
|
if (ImGui.CollapsingHeader("VitalStruct Offsets"))
|
|
{
|
|
DrawOffsetInfo("Reserved Flat", 0x10, "Reserved flat value");
|
|
DrawOffsetInfo("Reserved Percent", 0x14, "Reserved percentage value");
|
|
DrawOffsetInfo("Total (Max)", 0x2C, "Maximum value");
|
|
DrawOffsetInfo("Current", 0x30, "Current value");
|
|
}
|
|
|
|
|
|
if (ImGui.CollapsingHeader("Render/Position Component"))
|
|
{
|
|
DrawOffsetInfo("Position X", 0x138, "X coordinate");
|
|
DrawOffsetInfo("Position Y", 0x13C, "Y coordinate");
|
|
DrawOffsetInfo("Position Z", 0x140, "Z coordinate");
|
|
}
|
|
|
|
|
|
if (ImGui.CollapsingHeader("Memory Hex View"))
|
|
{
|
|
if (validationData.Addresses != null && validationData.Addresses.Count > 0)
|
|
{
|
|
var selectedAddr = validationData.Addresses.First().Key;
|
|
if (ImGui.BeginCombo("Select Address", selectedAddr))
|
|
{
|
|
foreach (var addr in validationData.Addresses.Keys)
|
|
{
|
|
bool isSelected = addr == selectedAddr;
|
|
if (ImGui.Selectable(addr, isSelected))
|
|
{
|
|
selectedAddr = addr;
|
|
}
|
|
if (isSelected)
|
|
{
|
|
ImGui.SetItemDefaultFocus();
|
|
}
|
|
}
|
|
ImGui.EndCombo();
|
|
}
|
|
|
|
if (validationData.Addresses.ContainsKey(selectedAddr))
|
|
{
|
|
var addrInfo = validationData.Addresses[selectedAddr];
|
|
DrawHexView(addrInfo.Address, this.Settings.HexViewBytes);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DrawAddressInfo(string name, IntPtr address, bool isValid, string description)
|
|
{
|
|
var color = isValid && address != IntPtr.Zero ? new Vector4(0, 1, 0, 1) : new Vector4(1, 0, 0, 1);
|
|
var status = isValid && address != IntPtr.Zero ? "✓ VALID" : "✗ INVALID";
|
|
|
|
ImGui.TextColored(color, $"{status} {name}");
|
|
ImGui.SameLine();
|
|
ImGui.Text($"0x{address.ToInt64():X}");
|
|
if (!string.IsNullOrEmpty(description))
|
|
{
|
|
ImGui.TextWrapped($" {description}");
|
|
}
|
|
}
|
|
|
|
private void DrawOffsetInfo(string name, long offset, string description)
|
|
{
|
|
ImGui.Text($"{name}: 0x{offset:X}");
|
|
if (!string.IsNullOrEmpty(description))
|
|
{
|
|
ImGui.SameLine();
|
|
ImGui.TextDisabled($"({description})");
|
|
}
|
|
}
|
|
|
|
private void DrawHexView(IntPtr address, int byteCount)
|
|
{
|
|
if (address == IntPtr.Zero)
|
|
{
|
|
ImGui.Text("Invalid address");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var reader = Core.Process.Handle;
|
|
var bytes = reader.ReadMemoryArray<byte>(address, byteCount);
|
|
|
|
if (bytes != null && bytes.Length > 0)
|
|
{
|
|
ImGui.Text($"Address: 0x{address.ToInt64():X} ({bytes.Length} bytes)");
|
|
ImGui.Separator();
|
|
|
|
ImGui.PushFont(ImGui.GetIO().Fonts.Fonts[1]);
|
|
var sb = new StringBuilder();
|
|
|
|
for (int i = 0; i < bytes.Length; i++)
|
|
{
|
|
if (i > 0 && i % 16 == 0)
|
|
{
|
|
ImGui.Text(sb.ToString());
|
|
sb.Clear();
|
|
}
|
|
sb.Append($"{bytes[i]:X2} ");
|
|
}
|
|
if (sb.Length > 0)
|
|
{
|
|
ImGui.Text(sb.ToString());
|
|
}
|
|
|
|
ImGui.PopFont();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ImGui.TextColored(new Vector4(1, 0, 0, 1), $"Error reading memory: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void RefreshMemoryData()
|
|
{
|
|
validationData = new MemoryValidationData
|
|
{
|
|
Addresses = new Dictionary<string, AddressInfo>()
|
|
};
|
|
|
|
if (Core.Process.Handle == null || Core.Process.Handle.IsInvalid || Core.Process.Handle.IsClosed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var reader = Core.Process.Handle;
|
|
|
|
|
|
if (Core.Process.StaticAddresses.ContainsKey("Game States"))
|
|
{
|
|
validationData.GameStatesAddress = Core.Process.StaticAddresses["Game States"];
|
|
validationData.GameStatesValid = ValidateAddress(validationData.GameStatesAddress);
|
|
|
|
if (validationData.GameStatesValid)
|
|
{
|
|
|
|
try
|
|
{
|
|
var gameStatesPtr = reader.ReadMemory<IntPtr>(validationData.GameStatesAddress);
|
|
var inGameStatePtr = reader.ReadMemory<IntPtr>(new IntPtr(gameStatesPtr.ToInt64() + (4 * 8)));
|
|
|
|
if (ValidateAddress(inGameStatePtr))
|
|
{
|
|
validationData.InGameStateAddress = inGameStatePtr;
|
|
|
|
|
|
try
|
|
{
|
|
var ingameDataPtr = reader.ReadMemory<IntPtr>(new IntPtr(inGameStatePtr.ToInt64() + 0x370));
|
|
if (ValidateAddress(ingameDataPtr))
|
|
{
|
|
validationData.IngameDataAddress = ingameDataPtr;
|
|
|
|
|
|
try
|
|
{
|
|
var localPlayerPtr = reader.ReadMemory<IntPtr>(new IntPtr(ingameDataPtr.ToInt64() + 0x408));
|
|
if (ValidateAddress(localPlayerPtr))
|
|
{
|
|
validationData.LocalPlayerAddress = localPlayerPtr;
|
|
validationData.Addresses["Local Player"] = new AddressInfo
|
|
{
|
|
Name = "Local Player",
|
|
Address = localPlayerPtr,
|
|
IsValid = true,
|
|
IsReadable = true,
|
|
ValueHex = $"0x{localPlayerPtr.ToInt64():X}",
|
|
Description = "Local player entity pointer"
|
|
};
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
catch { }
|
|
}
|
|
}
|
|
|
|
|
|
if (validationData.GameStatesAddress != IntPtr.Zero)
|
|
{
|
|
validationData.Addresses["GameStates"] = new AddressInfo
|
|
{
|
|
Name = "GameStates",
|
|
Address = validationData.GameStatesAddress,
|
|
IsValid = validationData.GameStatesValid,
|
|
IsReadable = validationData.GameStatesValid,
|
|
ValueHex = $"0x{validationData.GameStatesAddress.ToInt64():X}",
|
|
Description = "GameStates pointer"
|
|
};
|
|
}
|
|
|
|
if (validationData.InGameStateAddress != IntPtr.Zero)
|
|
{
|
|
validationData.Addresses["InGameState"] = new AddressInfo
|
|
{
|
|
Name = "InGameState",
|
|
Address = validationData.InGameStateAddress,
|
|
IsValid = true,
|
|
IsReadable = true,
|
|
ValueHex = $"0x{validationData.InGameStateAddress.ToInt64():X}",
|
|
Description = "InGameState pointer"
|
|
};
|
|
}
|
|
|
|
if (validationData.IngameDataAddress != IntPtr.Zero)
|
|
{
|
|
validationData.Addresses["IngameData"] = new AddressInfo
|
|
{
|
|
Name = "IngameData",
|
|
Address = validationData.IngameDataAddress,
|
|
IsValid = true,
|
|
IsReadable = true,
|
|
ValueHex = $"0x{validationData.IngameDataAddress.ToInt64():X}",
|
|
Description = "IngameData pointer"
|
|
};
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
CategorizedLogger.LogError($"[MemoryViewer] Error refreshing memory data: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private bool ValidateAddress(IntPtr address)
|
|
{
|
|
if (address == IntPtr.Zero || address.ToInt64() < 0x10000 || address.ToInt64() > 0x7FFFFFFFFFFF)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var reader = Core.Process.Handle;
|
|
|
|
reader.ReadMemory<byte>(address);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private void DumpToLog()
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine("=== Memory Viewer Dump ===");
|
|
sb.AppendLine($"GameStates: 0x{validationData.GameStatesAddress.ToInt64():X} (Valid: {validationData.GameStatesValid})");
|
|
sb.AppendLine($"InGameState: 0x{validationData.InGameStateAddress.ToInt64():X}");
|
|
sb.AppendLine($"IngameData: 0x{validationData.IngameDataAddress.ToInt64():X}");
|
|
sb.AppendLine($"Local Player: 0x{validationData.LocalPlayerAddress.ToInt64():X}");
|
|
sb.AppendLine();
|
|
sb.AppendLine("All Addresses:");
|
|
foreach (var addr in validationData.Addresses.Values)
|
|
{
|
|
sb.AppendLine($" {addr.Name}: 0x{addr.Address.ToInt64():X} (Valid: {addr.IsValid}, Readable: {addr.IsReadable})");
|
|
}
|
|
|
|
CategorizedLogger.Log(CategorizedLogger.LogCategory.AddressOffsets, sb.ToString());
|
|
}
|
|
}
|
|
}
|