621 lines
22 KiB
C#
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";
|
|
}
|
|
}
|
|
}
|
|
}
|