skills working somewhat

This commit is contained in:
Boki 2026-03-04 15:36:20 -05:00
parent a8c43ba7e2
commit 8a0e4bb481
22 changed files with 4227 additions and 161 deletions

View file

@ -0,0 +1,506 @@
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());
}
}
}

View file

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ImGui.NET" Version="1.91.6.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\GameHelper\GameHelper.csproj">
<Private>false</Private>
<CopyLocalSatelliteAssemblies>false</CopyLocalSatelliteAssemblies>
</ProjectReference>
</ItemGroup>
<Target Name="CopyFiles" AfterTargets="Build">
<Copy SourceFiles="$(OutDir)$(TargetName)$(TargetExt); $(OutDir)$(TargetName).pdb;" DestinationFolder="..\..\GameHelper\$(OutDir)Plugins\$(ProjectName)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -0,0 +1,23 @@
namespace MemoryViewer
{
using GameHelper.Plugin;
public class MemoryViewerSettings : IPSettings
{
public bool Enable { get; set; } = true;
public int HexViewBytes { get; set; } = 256;
public bool AutoRefresh { get; set; } = true;
public int RefreshIntervalMs { get; set; } = 1000;
}
}

View file

@ -0,0 +1,621 @@
namespace PatternTool
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using GameHelper;
using GameHelper.Plugin;
using GameHelper.Utils;
using GameOffsets;
using ImGuiNET;
using Newtonsoft.Json;
public sealed class PatternTool : PCore<PatternToolSettings>
{
private string _dumpDirectory;
private string _customPatternInput = string.Empty;
private string _lastScanResult = string.Empty;
private readonly List<PatternScanResult> _scanResults = new();
private readonly Dictionary<string, OffsetInfo> _offsetInfo = new();
private struct PatternScanResult
{
public string PatternName;
public IntPtr Address;
public long Offset;
public string PatternString;
public bool Valid;
}
private struct OffsetInfo
{
public string Name;
public long Offset;
public string Description;
public IntPtr? Value;
}
public override void OnEnable(bool isGameOpened)
{
_dumpDirectory = Path.Combine(this.DllDirectory, this.Settings.OutputDirectory);
Directory.CreateDirectory(_dumpDirectory);
LoadSettings();
InitializeOffsetInfo();
}
public override void OnDisable()
{
SaveSettings();
}
public override void DrawSettings()
{
if (ImGui.CollapsingHeader("Pattern Tool Settings"))
{
var outputDir = this.Settings.OutputDirectory;
if (ImGui.InputText("Output Directory", ref outputDir, 256))
{
this.Settings.OutputDirectory = outputDir;
_dumpDirectory = Path.Combine(this.DllDirectory, this.Settings.OutputDirectory);
Directory.CreateDirectory(_dumpDirectory);
}
ImGui.Checkbox("Auto Dump on Area Change", ref this.Settings.AutoDumpOnAreaChange);
ImGui.Checkbox("Enable Custom Pattern Scan", ref this.Settings.EnableCustomPatternScan);
var maxScanMB = this.Settings.MaxScanSizeMB;
if (ImGui.InputInt("Max Scan Size (MB)", ref maxScanMB, 10, 100))
{
if (maxScanMB < 1) maxScanMB = 1;
if (maxScanMB > 1000) maxScanMB = 1000;
this.Settings.MaxScanSizeMB = maxScanMB;
}
ImGui.Separator();
if (ImGui.Button("Scan All Known Patterns"))
{
ScanAllKnownPatterns();
}
if (ImGui.Button("Dump Patterns & Offsets"))
{
DumpPatternsAndOffsets();
}
}
if (ImGui.CollapsingHeader("Custom Pattern Scanner"))
{
ImGui.TextWrapped("Enter pattern bytes (hex, space-separated, use ?? for wildcards):");
ImGui.InputTextMultiline("##PatternInput", ref _customPatternInput, 1024, new System.Numerics.Vector2(-1, 100));
if (ImGui.Button("Scan Custom Pattern"))
{
ScanCustomPattern(_customPatternInput);
}
if (!string.IsNullOrEmpty(_lastScanResult))
{
ImGui.Separator();
ImGui.TextWrapped(_lastScanResult);
}
}
if (ImGui.CollapsingHeader("Scan Results"))
{
if (_scanResults.Count > 0)
{
if (ImGui.BeginTable("ScanResults", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable))
{
ImGui.TableSetupColumn("Pattern");
ImGui.TableSetupColumn("Address");
ImGui.TableSetupColumn("Offset");
ImGui.TableSetupColumn("Valid");
ImGui.TableHeadersRow();
foreach (var result in _scanResults)
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(result.PatternName);
ImGui.TableNextColumn();
ImGui.Text($"0x{result.Address.ToInt64():X}");
ImGui.TableNextColumn();
ImGui.Text($"0x{result.Offset:X}");
ImGui.TableNextColumn();
ImGui.TextColored(result.Valid ? new System.Numerics.Vector4(0, 1, 0, 1) : new System.Numerics.Vector4(1, 0, 0, 1),
result.Valid ? "Yes" : "No");
}
ImGui.EndTable();
}
}
else
{
ImGui.Text("No scan results yet. Click 'Scan All Known Patterns' to start.");
}
}
if (ImGui.CollapsingHeader("Offset Reference"))
{
ImGui.TextWrapped("Known offsets within structures:");
if (ImGui.BeginTable("OffsetInfo", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable))
{
ImGui.TableSetupColumn("Name");
ImGui.TableSetupColumn("Offset");
ImGui.TableSetupColumn("Description");
ImGui.TableHeadersRow();
foreach (var offset in _offsetInfo.Values.OrderBy(o => o.Name))
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.Text(offset.Name);
ImGui.TableNextColumn();
ImGui.Text($"0x{offset.Offset:X}");
ImGui.TableNextColumn();
ImGui.TextWrapped(offset.Description);
}
ImGui.EndTable();
}
}
}
public override void DrawUI()
{
}
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($"[PatternTool] Failed to save settings: {ex.Message}", ex);
}
}
private void LoadSettings()
{
try
{
var settingsPath = Path.Combine(this.DllDirectory, "settings.json");
if (File.Exists(settingsPath))
{
var json = File.ReadAllText(settingsPath);
var loaded = JsonConvert.DeserializeObject<PatternToolSettings>(json);
if (loaded != null)
{
this.Settings = loaded;
_dumpDirectory = Path.Combine(this.DllDirectory, this.Settings.OutputDirectory);
Directory.CreateDirectory(_dumpDirectory);
}
}
}
catch (Exception ex)
{
CategorizedLogger.LogError($"[PatternTool] Failed to load settings: {ex.Message}", ex);
}
}
private void InitializeOffsetInfo()
{
_offsetInfo["IN_GAME_STATE_INDEX"] = new OffsetInfo
{
Name = "IN_GAME_STATE_INDEX",
Offset = 4,
Description = "Index in GameStates array for InGameState pointer"
};
_offsetInfo["AREA_INSTANCE_DATA"] = new OffsetInfo
{
Name = "AREA_INSTANCE_DATA",
Offset = 0x948,
Description = "AreaInstanceData pointer offset in InGameState"
};
_offsetInfo["INGAME_DATA_OFFSET"] = new OffsetInfo
{
Name = "INGAME_DATA_OFFSET",
Offset = 0x370,
Description = "Offset to IngameData in InGameState"
};
_offsetInfo["INGAME_DATA_ENTITY_LIST"] = new OffsetInfo
{
Name = "INGAME_DATA_ENTITY_LIST",
Offset = 0x490,
Description = "EntityList pointer offset in IngameData"
};
_offsetInfo["INGAME_DATA_ENTITIES_COUNT"] = new OffsetInfo
{
Name = "INGAME_DATA_ENTITIES_COUNT",
Offset = 0x498,
Description = "Entities count offset in IngameData"
};
_offsetInfo["INGAME_DATA_LOCAL_PLAYER"] = new OffsetInfo
{
Name = "INGAME_DATA_LOCAL_PLAYER",
Offset = 0x408,
Description = "Local player pointer offset in IngameData (0x408 in source, was 0xA10 in area? verify)"
};
_offsetInfo["LOCAL_PLAYER_PTR"] = new OffsetInfo
{
Name = "LOCAL_PLAYER_PTR",
Offset = 0xA10,
Description = "Local player pointer offset in AreaInstance"
};
_offsetInfo["ENTITY_LIST_PTR"] = new OffsetInfo
{
Name = "ENTITY_LIST_PTR",
Offset = 0xB50,
Description = "EntityList pointer offset in AreaInstance (Confirmed by Graph Fuzzer BFS Tree, was 0x13F8)"
};
_offsetInfo["TERRAIN_LIST_PTR"] = new OffsetInfo
{
Name = "TERRAIN_LIST_PTR",
Offset = 0x12C8,
Description = "Terrain/Exits list pointer offset in AreaInstance (Count ~260)"
};
_offsetInfo["TERRAIN_GRID_PTR"] = new OffsetInfo
{
Name = "TERRAIN_GRID_PTR",
Offset = 0x08,
Description = "Terrain grid pointer offset (was 0x30)"
};
_offsetInfo["TERRAIN_DIMENSIONS_PTR"] = new OffsetInfo
{
Name = "TERRAIN_DIMENSIONS_PTR",
Offset = 0x28,
Description = "Terrain dimensions pointer offset"
};
_offsetInfo["COMPONENT_LIST_PTR"] = new OffsetInfo
{
Name = "COMPONENT_LIST_PTR",
Offset = 0x10,
Description = "Component list pointer offset inside ItemBase/Entity"
};
_offsetInfo["COMPONENT_INDEX_DEBUFFS"] = new OffsetInfo
{
Name = "COMPONENT_INDEX_DEBUFFS",
Offset = 3,
Description = "Index in component list for Debuffs component"
};
_offsetInfo["LIFE_COMPONENT_HEALTH"] = new OffsetInfo
{
Name = "LIFE_COMPONENT_HEALTH",
Offset = 0x1A8,
Description = "Health VitalStruct offset in Life component"
};
_offsetInfo["LIFE_COMPONENT_MANA"] = new OffsetInfo
{
Name = "LIFE_COMPONENT_MANA",
Offset = 0x1F8,
Description = "Mana VitalStruct offset in Life component"
};
_offsetInfo["LIFE_COMPONENT_ES"] = new OffsetInfo
{
Name = "LIFE_COMPONENT_ES",
Offset = 0x230,
Description = "Energy Shield VitalStruct offset in Life component"
};
_offsetInfo["LIFE_COMPONENT_BUFFS"] = new OffsetInfo
{
Name = "LIFE_COMPONENT_BUFFS",
Offset = 0x58,
Description = "Buffs pointer offset in Life component"
};
_offsetInfo["DEBUFF_COMPONENT_LIST"] = new OffsetInfo
{
Name = "DEBUFF_COMPONENT_LIST",
Offset = 0x160,
Description = "Debuff list pointer offset in Debuff component"
};
_offsetInfo["VITAL_RESERVED_FLAT"] = new OffsetInfo
{
Name = "VITAL_RESERVED_FLAT",
Offset = 0x10,
Description = "Reserved flat value offset in VitalStruct"
};
_offsetInfo["VITAL_RESERVED_PERCENT"] = new OffsetInfo
{
Name = "VITAL_RESERVED_PERCENT",
Offset = 0x14,
Description = "Reserved percent value offset in VitalStruct"
};
_offsetInfo["VITAL_TOTAL"] = new OffsetInfo
{
Name = "VITAL_TOTAL",
Offset = 0x2C,
Description = "Max value offset in VitalStruct"
};
_offsetInfo["VITAL_CURRENT"] = new OffsetInfo
{
Name = "VITAL_CURRENT",
Offset = 0x30,
Description = "Current value offset in VitalStruct"
};
_offsetInfo["POSITION_X"] = new OffsetInfo
{
Name = "POSITION_X",
Offset = 0x138,
Description = "X position offset in Render/Position component"
};
_offsetInfo["POSITION_Y"] = new OffsetInfo
{
Name = "POSITION_Y",
Offset = 0x13C,
Description = "Y position offset in Render/Position component"
};
_offsetInfo["POSITION_Z"] = new OffsetInfo
{
Name = "POSITION_Z",
Offset = 0x140,
Description = "Z position offset in Render/Position component"
};
_offsetInfo["COMPONENT_OWNER_ENTITY"] = new OffsetInfo
{
Name = "COMPONENT_OWNER_ENTITY",
Offset = 0x08,
Description = "Owner entity pointer offset in Component Header (to check Owner)"
};
}
private void ScanAllKnownPatterns()
{
_scanResults.Clear();
if (Core.Process.Address == IntPtr.Zero)
{
_lastScanResult = "ERROR: Process not available";
return;
}
try
{
var baseAddress = Core.Process.Address;
var patterns = StaticOffsetsPatterns.Patterns;
var staticAddresses = Core.Process.StaticAddresses;
foreach (var pattern in patterns)
{
var result = new PatternScanResult
{
PatternName = pattern.Name,
PatternString = ReconstructPatternString(pattern),
Valid = false
};
if (staticAddresses.TryGetValue(pattern.Name, out var address) && address != IntPtr.Zero)
{
result.Address = address;
result.Offset = address.ToInt64() - baseAddress.ToInt64();
result.Valid = true;
}
_scanResults.Add(result);
}
_lastScanResult = $"Scanned {patterns.Length} patterns. Found {_scanResults.Count(r => r.Valid)} valid addresses.";
}
catch (Exception ex)
{
_lastScanResult = $"ERROR: {ex.Message}";
CategorizedLogger.LogError($"[PatternTool] Scan error: {ex.Message}", ex);
}
}
private void ScanCustomPattern(string patternString)
{
if (string.IsNullOrWhiteSpace(patternString))
{
_lastScanResult = "ERROR: Pattern string is empty";
return;
}
if (Core.Process.Address == IntPtr.Zero)
{
_lastScanResult = "ERROR: Process not available";
return;
}
try
{
var pattern = new Pattern("Custom", patternString);
var baseAddress = Core.Process.Address;
var reader = Core.Process.Handle;
var scanSize = this.Settings.MaxScanSizeMB * 1024 * 1024;
var buffer = reader.ReadMemoryArray<byte>(baseAddress, scanSize);
if (buffer == null || buffer.Length < pattern.Data.Length)
{
_lastScanResult = "ERROR: Cannot read process memory";
return;
}
var matches = new List<IntPtr>();
for (int i = 0; i <= buffer.Length - pattern.Data.Length; i++)
{
bool match = true;
for (int j = 0; j < pattern.Data.Length; j++)
{
if (pattern.Mask[j] && buffer[i + j] != pattern.Data[j])
{
match = false;
break;
}
}
if (match)
{
matches.Add(new IntPtr(baseAddress.ToInt64() + i));
}
}
_lastScanResult = $"Found {matches.Count} matches for pattern '{patternString}'";
if (matches.Count > 0 && matches.Count <= 10)
{
_lastScanResult += "\nMatches:\n";
foreach (var match in matches)
{
var offset = match.ToInt64() - baseAddress.ToInt64();
_lastScanResult += $" 0x{match.ToInt64():X} (offset: 0x{offset:X})\n";
}
}
}
catch (Exception ex)
{
_lastScanResult = $"ERROR: {ex.Message}";
CategorizedLogger.LogError($"[PatternTool] Custom scan error: {ex.Message}", ex);
}
}
private void DumpPatternsAndOffsets()
{
try
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
var filename = Path.Combine(_dumpDirectory, $"pattern_tool_dump_{timestamp}.txt");
var sb = new StringBuilder();
var baseAddress = Core.Process.Address;
var staticAddresses = Core.Process.StaticAddresses;
sb.AppendLine("=".PadRight(80, '='));
sb.AppendLine("PATTERN TOOL DUMP - Patterns & Offsets");
sb.AppendLine($"Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine($"Base Address: 0x{baseAddress.ToInt64():X}");
sb.AppendLine("=".PadRight(80, '='));
sb.AppendLine();
sb.AppendLine("KNOWN PATTERNS:");
sb.AppendLine("-".PadRight(80, '-'));
foreach (var pattern in StaticOffsetsPatterns.Patterns)
{
var patternString = ReconstructPatternString(pattern);
var found = staticAddresses.TryGetValue(pattern.Name, out var address);
sb.AppendLine($"Pattern: {pattern.Name}");
sb.AppendLine($" Pattern String: {patternString}");
sb.AppendLine($" Bytes To Skip: {pattern.BytesToSkip}");
if (found && address != IntPtr.Zero)
{
var offset = address.ToInt64() - baseAddress.ToInt64();
sb.AppendLine($" Address: 0x{address.ToInt64():X}");
sb.AppendLine($" Offset: 0x{offset:X} ({offset})");
sb.AppendLine($" IDA: Press G -> {offset:X}");
}
else
{
sb.AppendLine($" Address: NOT FOUND");
}
sb.AppendLine();
}
sb.AppendLine();
sb.AppendLine("=".PadRight(80, '='));
sb.AppendLine("KNOWN OFFSETS:");
sb.AppendLine("-".PadRight(80, '-'));
foreach (var offset in _offsetInfo.Values.OrderBy(o => o.Name))
{
sb.AppendLine($"{offset.Name}:");
sb.AppendLine($" Offset: 0x{offset.Offset:X} ({offset.Offset})");
sb.AppendLine($" Description: {offset.Description}");
sb.AppendLine();
}
File.WriteAllText(filename, sb.ToString());
_lastScanResult = $"SUCCESS: Dumped to {filename}";
}
catch (Exception ex)
{
_lastScanResult = $"ERROR: {ex.Message}";
CategorizedLogger.LogError($"[PatternTool] Dump error: {ex.Message}", ex);
}
}
private static string ReconstructPatternString(Pattern pattern)
{
try
{
var parts = new List<string>();
for (int i = 0; i < pattern.Data.Length && i < pattern.Mask.Length; i++)
{
if (pattern.BytesToSkip >= 0 && i == pattern.BytesToSkip)
{
parts.Add("^");
}
if (pattern.Mask[i])
{
parts.Add($"{pattern.Data[i]:X2}");
}
else
{
parts.Add("??");
}
}
return string.Join(" ", parts);
}
catch
{
return "N/A";
}
}
}
}

View file

@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ImGui.NET" Version="1.91.6.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\GameHelper\GameHelper.csproj">
<Private>false</Private>
<CopyLocalSatelliteAssemblies>false</CopyLocalSatelliteAssemblies>
</ProjectReference>
</ItemGroup>
<Target Name="CopyFiles" AfterTargets="Build">
<Copy SourceFiles="$(OutDir)$(TargetName)$(TargetExt); $(OutDir)$(TargetName).pdb;" DestinationFolder="..\..\GameHelper\$(OutDir)Plugins\$(ProjectName)" SkipUnchangedFiles="true" />
</Target>
</Project>

View file

@ -0,0 +1,24 @@
namespace PatternTool
{
using System;
using GameHelper.Plugin;
public class PatternToolSettings : IPSettings
{
public string OutputDirectory { get; set; } = "pattern_dumps";
public bool AutoDumpOnAreaChange { get; set; } = false;
public bool EnableCustomPatternScan { get; set; } = true;
public int MaxScanSizeMB { get; set; } = 100;
}
}

791
tools/skillbar_scanner.py Normal file
View file

@ -0,0 +1,791 @@
#!/usr/bin/env python3
"""
SkillBar Discovery Tool finds the SkillBarIds offset within PlayerServerData.
ExileCore uses ServerData.SkillBarIds: a Buffer13<(ushort Id, ushort Id2)>
13 skill bar slots, each a pair of ushort values (4 bytes per slot, 52 bytes total).
This script:
1. Attaches to the game process via pymem
2. Follows the pointer chain to read Actor component's ActiveSkills
3. Extracts (Id, Id2) ushort pairs from each skill's UnknownIdAndEquipmentInfo
4. Reads PSD memory and scans for a 52-byte region containing those IDs
5. Reports the PSD offset and dumps the skill bar mapping
Usage:
pip install pymem
python tools/skillbar_scanner.py
"""
import json
import struct
import sys
from pathlib import Path
try:
import pymem
import pymem.process
except ImportError:
print("ERROR: pymem not installed. Run: pip install pymem")
sys.exit(1)
def load_offsets() -> dict:
"""Load offsets.json from project root."""
p = Path(__file__).resolve().parent.parent / "offsets.json"
if not p.exists():
print(f"ERROR: offsets.json not found at {p}")
sys.exit(1)
with open(p) as f:
data = json.load(f)
# Convert hex strings to int
result = {}
for k, v in data.items():
if isinstance(v, str) and v.startswith("0x"):
result[k] = int(v, 16)
elif isinstance(v, (int, float)):
result[k] = int(v)
else:
result[k] = v
return result
def read_ptr(pm: pymem.Pymem, addr: int) -> int:
"""Read a 64-bit pointer. Returns 0 on failure."""
try:
return struct.unpack("<Q", pm.read_bytes(addr, 8))[0]
except Exception:
return 0
def read_i32(pm: pymem.Pymem, addr: int) -> int:
try:
return struct.unpack("<i", pm.read_bytes(addr, 4))[0]
except Exception:
return 0
def read_u32(pm: pymem.Pymem, addr: int) -> int:
try:
return struct.unpack("<I", pm.read_bytes(addr, 4))[0]
except Exception:
return 0
def is_valid_ptr(addr: int) -> bool:
"""Check if address looks like a valid 64-bit heap pointer."""
if addr == 0:
return False
high = (addr >> 32) & 0xFFFFFFFF
return 0 < high < 0x7FFF
def read_std_vector(pm: pymem.Pymem, vec_addr: int) -> tuple[int, int]:
"""Read begin/end pointers of an MSVC std::vector."""
begin = read_ptr(pm, vec_addr)
end = read_ptr(pm, vec_addr + 8)
return begin, end
def read_ascii_string(pm: pymem.Pymem, addr: int, max_len: int = 64) -> str | None:
"""Read a null-terminated ASCII string."""
try:
data = pm.read_bytes(addr, max_len)
null_idx = data.index(0)
return data[:null_idx].decode("ascii", errors="replace")
except Exception:
return None
def find_component_address(pm: pymem.Pymem, entity_ptr: int, component_name: str, offsets: dict,
verbose: bool = False) -> int:
"""
Find a named component's address via the ComponentLookup system.
Entity+0x08 EntityDetails +0x28 ComponentLookup +0x28 Vec2 (name entries)
Each entry: { char* name (8), int32 index (4), int32 flags (4) } = 16 bytes
Then: Entity+0x10 component pointer list [index] component address
"""
details_ptr = read_ptr(pm, entity_ptr + offsets["EntityDetailsOffset"])
if verbose:
print(f" Entity+0x{offsets['EntityDetailsOffset']:X} → EntityDetails: 0x{details_ptr:X}")
if not is_valid_ptr(details_ptr):
if verbose:
print(" ERROR: Invalid EntityDetails pointer")
return 0
lookup_ptr = read_ptr(pm, details_ptr + offsets["ComponentLookupOffset"])
if verbose:
print(f" EntityDetails+0x{offsets['ComponentLookupOffset']:X} → ComponentLookup: 0x{lookup_ptr:X}")
if not is_valid_ptr(lookup_ptr):
if verbose:
print(" ERROR: Invalid ComponentLookup pointer")
return 0
vec_begin, vec_end = read_std_vector(pm, lookup_ptr + offsets["ComponentLookupVec2Offset"])
if verbose:
print(f" ComponentLookup+0x{offsets['ComponentLookupVec2Offset']:X} → Vec2: 0x{vec_begin:X}..0x{vec_end:X}")
if not is_valid_ptr(vec_begin) or vec_end <= vec_begin:
if verbose:
print(" ERROR: Invalid Vec2 range")
return 0
entry_size = offsets["ComponentLookupEntrySize"] # 16
total = vec_end - vec_begin
count = total // entry_size
if verbose:
print(f" Entries: {count} (entry_size={entry_size})")
if count <= 0 or count > 128:
if verbose:
print(f" ERROR: Bad entry count: {count}")
return 0
# Read all entries at once
try:
data = pm.read_bytes(vec_begin, total)
except Exception:
return 0
target_index = -1
found_names = []
for i in range(count):
off = i * entry_size
name_ptr = struct.unpack("<Q", data[off:off + 8])[0]
if not is_valid_ptr(name_ptr):
continue
name = read_ascii_string(pm, name_ptr)
if name:
found_names.append(name)
if name == component_name:
target_index = struct.unpack("<i", data[off + 8:off + 12])[0]
break
if verbose:
print(f" Found components: {', '.join(found_names[:20])}")
if target_index < 0:
if verbose:
print(f" ERROR: '{component_name}' not found in lookup entries")
return 0
if verbose:
print(f" '{component_name}' → index {target_index}")
# Read component list: Entity+0x10 → begin pointer → [index * 8]
comp_list_begin = read_ptr(pm, entity_ptr + offsets["ComponentListOffset"])
if not is_valid_ptr(comp_list_begin):
if verbose:
print(" ERROR: Invalid component list pointer")
return 0
comp_addr = read_ptr(pm, comp_list_begin + target_index * 8)
return comp_addr if is_valid_ptr(comp_addr) else 0
def resolve_pointer_chain(pm: pymem.Pymem, offsets: dict) -> dict:
"""
Follow the full pointer chain from module base to all needed addresses.
Returns dict with AreaInstance, ServerData, PSD, LocalPlayer, etc.
"""
result = {}
# Find module base
module = pymem.process.module_from_name(pm.process_handle, offsets["ProcessName"] + ".exe")
if module is None:
print("ERROR: Could not find game module")
return result
module_base = module.lpBaseOfDll
module_size = module.SizeOfImage
print(f"Module base: 0x{module_base:X} size: 0x{module_size:X}")
# Pattern scan for GameState base
pattern = offsets.get("GameStatePattern", "")
game_state_base = 0
if pattern and "^" in pattern:
# Parse pattern: bytes before ^ are prefix, ^ marks the RIP displacement position,
# bytes after the 4 ?? wildcards are suffix (must also match)
parts = pattern.split()
caret_idx = parts.index("^")
# Build prefix (before ^)
prefix_bytes = []
prefix_mask = []
for p in parts[:caret_idx]:
if p == "??":
prefix_bytes.append(0)
prefix_mask.append(False)
else:
prefix_bytes.append(int(p, 16))
prefix_mask.append(True)
prefix_len = len(prefix_bytes)
# Count wildcards after ^ (the displacement bytes)
disp_wildcards = 0
suffix_start = caret_idx + 1
while suffix_start < len(parts) and parts[suffix_start] == "??":
disp_wildcards += 1
suffix_start += 1
if disp_wildcards == 0:
disp_wildcards = 4 # default: 4-byte RIP displacement
# Build suffix (after the wildcards)
suffix_bytes = []
suffix_mask = []
for p in parts[suffix_start:]:
if p == "??":
suffix_bytes.append(0)
suffix_mask.append(False)
else:
suffix_bytes.append(int(p, 16))
suffix_mask.append(True)
suffix_len = len(suffix_bytes)
total_pattern_len = prefix_len + disp_wildcards + suffix_len
# Scan module memory in chunks
CHUNK = 0x100000
for chunk_off in range(0, module_size, CHUNK):
try:
chunk_size = min(CHUNK + 256, module_size - chunk_off)
if chunk_size <= total_pattern_len:
continue
data = pm.read_bytes(module_base + chunk_off, chunk_size)
except Exception:
continue
for i in range(len(data) - total_pattern_len):
# Match prefix
match = True
for j in range(prefix_len):
if prefix_mask[j] and data[i + j] != prefix_bytes[j]:
match = False
break
if not match:
continue
# Match suffix (after displacement bytes)
suffix_off = i + prefix_len + disp_wildcards
for j in range(suffix_len):
if suffix_mask[j] and data[suffix_off + j] != suffix_bytes[j]:
match = False
break
if not match:
continue
# Read RIP-relative displacement at caret position
disp_offset = i + prefix_len
disp = struct.unpack("<i", data[disp_offset:disp_offset + 4])[0]
# RIP = module_base + chunk_off + disp_offset + 4 (after the 4-byte displacement)
rip = module_base + chunk_off + disp_offset + 4
game_state_base = rip + disp + offsets.get("PatternResultAdjust", 0)
print(f"Pattern matched at module+0x{chunk_off + i:X}, disp=0x{disp & 0xFFFFFFFF:X}")
break
if game_state_base:
break
if game_state_base == 0:
gso = offsets.get("GameStateGlobalOffset", 0)
if gso > 0:
game_state_base = module_base + gso
print(f"GameState base (manual): 0x{game_state_base:X}")
else:
print("ERROR: Could not find GameState base via pattern or manual offset")
return result
print(f"GameState base: 0x{game_state_base:X}")
result["GameStateBase"] = game_state_base
# GameState → ReadPointer → Controller
controller = read_ptr(pm, game_state_base)
if not is_valid_ptr(controller):
print("ERROR: Invalid controller pointer")
return result
print(f"Controller: 0x{controller:X}")
result["Controller"] = controller
# Controller → InGameState (direct offset)
igs_off = offsets.get("InGameStateDirectOffset", 0x210)
in_game_state = read_ptr(pm, controller + igs_off)
print(f" Controller+0x{igs_off:X} → raw=0x{in_game_state:X}")
if not is_valid_ptr(in_game_state):
# Try inline state array fallback: controller + StatesBeginOffset + InGameStateIndex * StateStride
states_begin = offsets.get("StatesBeginOffset", 0x48)
igs_index = offsets.get("InGameStateIndex", 4)
stride = offsets.get("StateStride", 0x10)
state_ptr_off = offsets.get("StatePointerOffset", 0)
inline_off = states_begin + igs_index * stride + state_ptr_off
in_game_state = read_ptr(pm, controller + inline_off)
print(f" Fallback: Controller+0x{inline_off:X} → raw=0x{in_game_state:X}")
if not is_valid_ptr(in_game_state):
print("ERROR: Invalid InGameState pointer (tried direct + inline fallback)")
print(" Make sure you are logged in and in-game (not at login screen)")
return result
print(f"InGameState: 0x{in_game_state:X}")
result["InGameState"] = in_game_state
# InGameState → AreaInstance
area_instance = read_ptr(pm, in_game_state + offsets["IngameDataFromStateOffset"])
if not is_valid_ptr(area_instance):
print("ERROR: Invalid AreaInstance pointer")
return result
print(f"AreaInstance: 0x{area_instance:X}")
result["AreaInstance"] = area_instance
# AreaInstance → ServerData
server_data = read_ptr(pm, area_instance + offsets["ServerDataOffset"])
if not is_valid_ptr(server_data):
print("ERROR: Invalid ServerData pointer")
return result
print(f"ServerData: 0x{server_data:X}")
result["ServerData"] = server_data
# ServerData → PlayerServerData vector → PSD[0]
psd_vec_begin, psd_vec_end = read_std_vector(pm, server_data + offsets["PlayerServerDataOffset"])
if not is_valid_ptr(psd_vec_begin):
print("ERROR: Invalid PSD vector")
return result
psd_ptr = read_ptr(pm, psd_vec_begin)
if not is_valid_ptr(psd_ptr):
print("ERROR: Invalid PSD[0] pointer")
return result
print(f"PSD[0]: 0x{psd_ptr:X}")
result["PSD"] = psd_ptr
# AreaInstance → LocalPlayer entity
# Try direct offset first, then scan nearby offsets to auto-discover
local_player = 0
lp_direct_off = offsets["LocalPlayerDirectOffset"]
lp_candidate = read_ptr(pm, area_instance + lp_direct_off)
print(f" AreaInstance+0x{lp_direct_off:X} → 0x{lp_candidate:X}")
# Validate: a real entity has vtable in module range at +0x00 and a heap pointer at +0x08
def is_entity(addr: int) -> bool:
if not is_valid_ptr(addr):
return False
vtable = read_ptr(pm, addr)
details = read_ptr(pm, addr + 8)
return module_base <= vtable < module_base + module_size and is_valid_ptr(details)
if is_entity(lp_candidate):
local_player = lp_candidate
else:
# Maybe it's an intermediate struct — try deref + small offsets
for sub_off in [0x00, 0x08, 0x10, 0x18, 0x20, 0x28, 0x30]:
candidate = read_ptr(pm, lp_candidate + sub_off) if is_valid_ptr(lp_candidate) else 0
if is_entity(candidate):
print(f" Found entity via deref+0x{sub_off:X}: 0x{candidate:X}")
local_player = candidate
break
if local_player == 0:
# Scan AreaInstance offsets near 0xA10 for any entity pointer
print(" Scanning AreaInstance offsets 0xA00..0xA30 for entity pointer...")
for scan_off in range(0xA00, 0xA30, 8):
candidate = read_ptr(pm, area_instance + scan_off)
if is_entity(candidate):
print(f" Found entity at AreaInstance+0x{scan_off:X}: 0x{candidate:X}")
local_player = candidate
break
if local_player == 0 and is_valid_ptr(server_data):
# Fallback: ServerData + LocalPlayerOffset
lp_sd = read_ptr(pm, server_data + offsets.get("LocalPlayerOffset", 0x20))
if is_entity(lp_sd):
local_player = lp_sd
print(f" Found entity via ServerData+0x{offsets.get('LocalPlayerOffset', 0x20):X}: 0x{local_player:X}")
if local_player == 0:
# Dump first 0x20 bytes at the direct candidate for debugging
if is_valid_ptr(lp_candidate):
try:
raw = pm.read_bytes(lp_candidate, 0x40)
print(f" DEBUG: Raw bytes at 0x{lp_candidate:X}:")
for row in range(0, 0x40, 16):
hex_str = " ".join(f"{b:02X}" for b in raw[row:row + 16])
ptrs = " ".join(f"0x{struct.unpack('<Q', raw[row + j:row + j + 8])[0]:X}" for j in range(0, 16, 8))
print(f" +0x{row:02X}: {hex_str} [{ptrs}]")
except Exception:
pass
print("ERROR: Could not find valid LocalPlayer entity")
return result
print(f"LocalPlayer: 0x{local_player:X}")
result["LocalPlayer"] = local_player
return result
def read_active_skills(pm: pymem.Pymem, actor_comp: int) -> list[tuple[int, int, int]]:
"""
Read ActiveSkills vector from Actor component.
Returns list of (raw_uint32, id_lo16, id_hi16) tuples.
"""
ACTIVE_SKILLS_OFFSET = 0xB00
vec_begin, vec_end = read_std_vector(pm, actor_comp + ACTIVE_SKILLS_OFFSET)
if not is_valid_ptr(vec_begin) or vec_end <= vec_begin:
print(" No active skills found")
return []
total = vec_end - vec_begin
entry_size = 0x10 # ActiveSkillEntry: 2 pointers
count = total // entry_size
if count <= 0 or count > 128:
print(f" Bad skill count: {count}")
return []
try:
data = pm.read_bytes(vec_begin, total)
except Exception:
return []
skills = []
for i in range(count):
# Follow ptr1 (ActiveSkillPtr)
active_skill_ptr = struct.unpack("<Q", data[i * entry_size:i * entry_size + 8])[0]
if not is_valid_ptr(active_skill_ptr):
continue
# Read UnknownIdAndEquipmentInfo at ActiveSkillDetails+0x10
uid = read_u32(pm, active_skill_ptr + 0x10)
if uid == 0:
continue
id_lo = uid & 0xFFFF
id_hi = (uid >> 16) & 0xFFFF
# Also try to read the skill name for display
skills.append((uid, id_lo, id_hi))
return skills
def read_wchar_string(pm: pymem.Pymem, addr: int, max_bytes: int = 128) -> str | None:
"""Read null-terminated wchar_t (UTF-16LE) string."""
if not is_valid_ptr(addr):
return None
try:
raw = pm.read_bytes(addr, max_bytes)
chars = []
for j in range(0, len(raw) - 1, 2):
c = struct.unpack("<H", raw[j:j + 2])[0]
if c == 0:
break
if c > 0xFFFF:
return None
chars.append(chr(c))
name = "".join(chars)
return name if name and len(name) > 1 and all(32 <= ord(c) < 127 for c in name) else None
except Exception:
return None
def resolve_skill_name(pm: pymem.Pymem, active_skill_ptr: int) -> str | None:
"""
Try to resolve a skill name via multiple paths:
1. ActiveSkillsDatPtr (+0x20) wchar* (direct dat row string)
2. ActiveSkillsDatPtr (+0x20) ptr wchar* (indirect via pointer in dat row)
3. GEPL FK chain: +0x18 GEPL row +0x00 FK GE row +0x00 wchar*
"""
# Path 1: Direct wchar* at +0x20
dat_ptr = read_ptr(pm, active_skill_ptr + 0x20)
if is_valid_ptr(dat_ptr):
name = read_wchar_string(pm, dat_ptr)
if name:
return name
# Path 2: Indirect — dat_ptr is a dat row, first field is a pointer to wchar*
str_ptr = read_ptr(pm, dat_ptr)
if is_valid_ptr(str_ptr):
name = read_wchar_string(pm, str_ptr)
if name:
return name
# Path 3: GEPL FK chain
gepl_ptr = read_ptr(pm, active_skill_ptr + 0x18)
if is_valid_ptr(gepl_ptr):
ge_fk = read_ptr(pm, gepl_ptr)
if is_valid_ptr(ge_fk):
# GE+0x00 → ActiveSkills.dat row → wchar*
as_dat = read_ptr(pm, ge_fk)
if is_valid_ptr(as_dat):
name = read_wchar_string(pm, as_dat)
if name:
return name
# Try indirect
str_ptr = read_ptr(pm, as_dat)
if is_valid_ptr(str_ptr):
name = read_wchar_string(pm, str_ptr)
if name:
return name
return None
def read_active_skills_with_names(pm: pymem.Pymem, actor_comp: int) -> list[dict]:
"""Read ActiveSkills with name resolution. Returns list of skill dicts."""
ACTIVE_SKILLS_OFFSET = 0xB00
vec_begin, vec_end = read_std_vector(pm, actor_comp + ACTIVE_SKILLS_OFFSET)
if not is_valid_ptr(vec_begin) or vec_end <= vec_begin:
return []
total = vec_end - vec_begin
entry_size = 0x10
count = total // entry_size
if count <= 0 or count > 128:
return []
try:
data = pm.read_bytes(vec_begin, total)
except Exception:
return []
skills = []
seen = set()
for i in range(count):
asp = struct.unpack("<Q", data[i * entry_size:i * entry_size + 8])[0]
if not is_valid_ptr(asp):
continue
uid = read_u32(pm, asp + 0x10)
if uid == 0 or uid in seen:
continue
seen.add(uid)
id_lo = uid & 0xFFFF
id_hi = (uid >> 16) & 0xFFFF
name = resolve_skill_name(pm, asp)
skills.append({
"raw": uid,
"id": id_lo,
"id2": id_hi,
"name": name,
"ptr": asp,
})
return skills
def find_uint32_occurrences(psd_data: bytes, target_values: set[int]) -> list[tuple[int, int]]:
"""Find all 4-byte-aligned occurrences of any target uint32 in PSD data.
Returns list of (offset, value)."""
hits = []
for off in range(0, len(psd_data) - 3, 4):
val = struct.unpack("<I", psd_data[off:off + 4])[0]
if val in target_values:
hits.append((off, val))
return hits
def scan_psd_for_skillbar(psd_data: bytes, known_ids: set[tuple[int, int]],
known_raw: set[int]) -> list[tuple[int, int, list]]:
"""
Scan PSD memory for a Buffer13<(ushort Id, ushort Id2)> pattern.
13 slots × 4 bytes = 52 bytes. We look for 4-byte-aligned offsets where
reading 13 × (ushort, ushort) produces overlap with known skill IDs.
Returns list of (offset, overlap_count, slot_data) sorted by overlap desc.
"""
SLOTS = 13
ENTRY_SIZE = 4 # 2 × ushort
BUFFER_SIZE = SLOTS * ENTRY_SIZE # 52 bytes
candidates = []
for off in range(0, len(psd_data) - BUFFER_SIZE, 4): # 4-byte aligned
slots = []
non_zero = 0
for s in range(SLOTS):
entry_off = off + s * ENTRY_SIZE
id_lo = struct.unpack("<H", psd_data[entry_off:entry_off + 2])[0]
id_hi = struct.unpack("<H", psd_data[entry_off + 2:entry_off + 4])[0]
slots.append((id_lo, id_hi))
if id_lo != 0 or id_hi != 0:
non_zero += 1
if non_zero < 2:
continue
# Count overlap with known skill IDs (as ushort pairs)
overlap = 0
for pair in slots:
if pair != (0, 0) and pair in known_ids:
overlap += 1
# Also try matching raw uint32 values
if overlap < 2:
raw_overlap = 0
for s in range(SLOTS):
entry_off = off + s * ENTRY_SIZE
raw_val = struct.unpack("<I", psd_data[entry_off:entry_off + 4])[0]
if raw_val != 0 and raw_val in known_raw:
raw_overlap += 1
if raw_overlap >= 2:
overlap = raw_overlap
if overlap >= 2:
candidates.append((off, overlap, slots))
# Sort by overlap descending
candidates.sort(key=lambda x: -x[1])
return candidates
def main():
offsets = load_offsets()
proc_name = offsets.get("ProcessName", "PathOfExileSteam")
print(f"Attaching to {proc_name}...")
try:
pm = pymem.Pymem(proc_name + ".exe")
except pymem.exception.ProcessNotFound:
# Try without .exe
try:
pm = pymem.Pymem(proc_name)
except Exception as e:
print(f"ERROR: Could not attach to process: {e}")
sys.exit(1)
print(f"Attached to PID {pm.process_id}")
print()
# Resolve the full pointer chain
print("=== Resolving pointer chain ===")
addrs = resolve_pointer_chain(pm, offsets)
if "PSD" not in addrs or "LocalPlayer" not in addrs:
print("\nFATAL: Could not resolve required addresses")
pm.close_process()
sys.exit(1)
print()
# Find Actor component via ComponentLookup
print("=== Finding Actor component ===")
actor_comp = find_component_address(pm, addrs["LocalPlayer"], "Actor", offsets, verbose=True)
if actor_comp == 0:
print("ERROR: Could not find Actor component on local player")
pm.close_process()
sys.exit(1)
print(f"Actor component: 0x{actor_comp:X}")
print()
# Read active skills
print("=== Reading ActiveSkills ===")
skills = read_active_skills_with_names(pm, actor_comp)
if not skills:
print("ERROR: No active skills found. Make sure you have skills equipped.")
pm.close_process()
sys.exit(1)
print(f"Found {len(skills)} unique skills:")
known_ids = set()
for s in skills:
known_ids.add((s["id"], s["id2"]))
name_str = s["name"] or "???"
print(f" Id={s['id']:5d} Id2={s['id2']:5d} Raw=0x{s['raw']:08X} {name_str}")
print()
# Scan PSD for SkillBarIds
print("=== Scanning PSD for SkillBarIds ===")
SCAN_SIZE = 0x10000 # 64KB — PSD can be very large
print(f"Reading 0x{SCAN_SIZE:X} bytes from PSD at 0x{addrs['PSD']:X}")
try:
psd_data = pm.read_bytes(addrs["PSD"], SCAN_SIZE)
except Exception as e:
print(f"ERROR reading PSD: {e}")
pm.close_process()
sys.exit(1)
print(f"Read {len(psd_data)} bytes OK")
# Build sets for matching
known_raw = set()
for s in skills:
known_raw.add(s["raw"])
# Step 1: Find individual uint32 occurrences in PSD (needle search)
print(f"\nStep 1: Searching for {len(known_raw)} known uint32 skill IDs in PSD...")
hits = find_uint32_occurrences(psd_data, known_raw)
if hits:
print(f" Found {len(hits)} individual hits:")
id_to_name_raw = {s["raw"]: s["name"] or f"0x{s['raw']:08X}" for s in skills}
for off, val in hits[:30]:
name = id_to_name_raw.get(val, "?")
print(f" PSD+0x{off:X}: 0x{val:08X} ({name})")
else:
print(" No individual raw uint32 hits found in PSD!")
# Also try just the lo16 values
known_lo16 = {s["id"] for s in skills if s["id"] > 0}
lo16_hits = []
for off in range(0, len(psd_data) - 1, 2):
val = struct.unpack("<H", psd_data[off:off + 2])[0]
if val in known_lo16:
lo16_hits.append((off, val))
if lo16_hits:
print(f" But found {len(lo16_hits)} lo16 (ushort) hits — showing first 20:")
for off, val in lo16_hits[:20]:
print(f" PSD+0x{off:X}: {val}")
else:
print(" No lo16 hits either — skill IDs may not be stored in PSD at this address")
print(" (Possibly the PSD vector element is different, or scan range too small)")
# Step 2: Buffer13 pattern scan
print(f"\nStep 2: Scanning for Buffer13<(ushort, ushort)> pattern (13 × 4 = 52 bytes)...")
candidates = scan_psd_for_skillbar(psd_data, known_ids, known_raw)
if not candidates:
print("No Buffer13 candidates found.")
# If individual hits exist, suggest they might use a different buffer layout
if hits:
print("\nIndividual hits exist — checking for clustered groups near those offsets...")
# Check for any 3+ hits within a 52-byte window
for i, (base_off, _) in enumerate(hits):
window_start = max(0, base_off - 52)
window_end = min(len(psd_data), base_off + 52)
nearby = [(o, v) for o, v in hits if window_start <= o < window_end]
if len(nearby) >= 3:
print(f" Cluster at PSD+0x{window_start:X}..0x{window_end:X}: {len(nearby)} hits")
for o, v in nearby:
print(f" +0x{o:X}: 0x{v:08X}")
pm.close_process()
sys.exit(1)
# Display top results
SLOT_LABELS = [
"LMB", "RMB", "MMB", "Q", "E", "R", "T", "F",
"Slot8", "Slot9", "Slot10", "Slot11", "Slot12"
]
print(f"Found {len(candidates)} candidate offsets (top 10):")
print()
# Build reverse lookup: (id, id2) → name
id_to_name = {}
for s in skills:
id_to_name[(s["id"], s["id2"])] = s["name"] or f"skill_{s['raw']:08X}"
for rank, (off, overlap, slots) in enumerate(candidates[:10]):
print(f" #{rank + 1} PSD+0x{off:X} (overlap: {overlap}/{len(known_ids)} known skills)")
for i, (id_lo, id_hi) in enumerate(slots):
label = SLOT_LABELS[i] if i < len(SLOT_LABELS) else f"Slot{i}"
if id_lo == 0 and id_hi == 0:
print(f" [{label:>6}] (empty)")
else:
match = (id_lo, id_hi) in known_ids
name = id_to_name.get((id_lo, id_hi), "")
marker = " ✓ MATCH" if match else ""
name_str = f" ({name})" if name else ""
print(f" [{label:>6}] Id={id_lo:5d} Id2={id_hi:5d}{name_str}{marker}")
print()
# Highlight the best result
best_off, best_overlap, best_slots = candidates[0]
print("=" * 60)
print(f"BEST MATCH: PSD+0x{best_off:X}")
print(f" Overlap: {best_overlap}/{len(known_ids)} known skills")
print(f" Add to offsets.json: \"SkillBarIdsOffset\": \"0x{best_off:X}\"")
print("=" * 60)
pm.close_process()
if __name__ == "__main__":
main()