poe2-bot/tools/MemoryViewer/MemoryViewer.cs
2026-03-04 15:36:20 -05:00

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