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 { private string _dumpDirectory; private string _customPatternInput = string.Empty; private string _lastScanResult = string.Empty; private readonly List _scanResults = new(); private readonly Dictionary _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(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(baseAddress, scanSize); if (buffer == null || buffer.Length < pattern.Data.Length) { _lastScanResult = "ERROR: Cannot read process memory"; return; } var matches = new List(); 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(); 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"; } } } }