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

621 lines
22 KiB
C#

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