skills working somewhat
This commit is contained in:
parent
a8c43ba7e2
commit
8a0e4bb481
22 changed files with 4227 additions and 161 deletions
506
tools/MemoryViewer/MemoryViewer.cs
Normal file
506
tools/MemoryViewer/MemoryViewer.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
29
tools/MemoryViewer/MemoryViewer.csproj
Normal file
29
tools/MemoryViewer/MemoryViewer.csproj
Normal 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>
|
||||
23
tools/MemoryViewer/MemoryViewerSettings.cs
Normal file
23
tools/MemoryViewer/MemoryViewerSettings.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
621
tools/PatternTool/PatternTool.cs
Normal file
621
tools/PatternTool/PatternTool.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
tools/PatternTool/PatternTool.csproj
Normal file
32
tools/PatternTool/PatternTool.csproj
Normal 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>
|
||||
24
tools/PatternTool/PatternToolSettings.cs
Normal file
24
tools/PatternTool/PatternToolSettings.cs
Normal 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
791
tools/skillbar_scanner.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue