using System.Globalization; using System.Text; using Serilog; namespace Automata.Memory; public class GameStateSnapshot { // Process public bool Attached; public int ProcessId; public nint ModuleBase; public int ModuleSize; public string? Error; // GameState public nint GameStateBase; public bool OffsetsConfigured; public int StatesCount; // Pointers public nint ControllerPtr; public nint InGameStatePtr; public nint AreaInstancePtr; public nint ServerDataPtr; public nint LocalPlayerPtr; // Area public int AreaLevel; public uint AreaHash; // Player position (Render component) public bool HasPosition; public float PlayerX, PlayerY, PlayerZ; // Player vitals (Life component) public bool HasVitals; public int LifeCurrent, LifeTotal; public int ManaCurrent, ManaTotal; public int EsCurrent, EsTotal; // Entities public int EntityCount; // Terrain public int TerrainWidth, TerrainHeight; public int TerrainCols, TerrainRows; } public class GameMemoryReader : IDisposable { private ProcessMemory? _memory; private PatternScanner? _scanner; private readonly TerrainOffsets _offsets; private nint _moduleBase; private int _moduleSize; private nint _gameStateBase; private bool _disposed; private int _cachedLifeIndex = -1; private int _cachedRenderIndex = -1; private nint _lastLocalPlayer; public GameMemoryReader() { _offsets = TerrainOffsets.Load("offsets.json"); } public bool IsAttached => _memory != null; public bool Attach() { Detach(); _memory = ProcessMemory.Attach(_offsets.ProcessName); if (_memory is null) return false; var module = _memory.GetMainModule(); if (module is not null) (_moduleBase, _moduleSize) = module.Value; // Try pattern scan first if (!string.IsNullOrWhiteSpace(_offsets.GameStatePattern)) { _scanner = new PatternScanner(_memory); _gameStateBase = _scanner.FindPatternRip(_offsets.GameStatePattern); if (_gameStateBase != 0) { _gameStateBase += _offsets.PatternResultAdjust; Log.Information("GameState base (pattern+adjust): 0x{Address:X}", _gameStateBase); } } // Fallback: manual offset from module base if (_gameStateBase == 0 && _offsets.GameStateGlobalOffset > 0) { _gameStateBase = _moduleBase + _offsets.GameStateGlobalOffset; Log.Information("GameState base (manual): 0x{Address:X}", _gameStateBase); } return true; } public void Detach() { _memory?.Dispose(); _memory = null; _scanner = null; _gameStateBase = 0; _moduleBase = 0; _moduleSize = 0; } public GameStateSnapshot ReadSnapshot() { var snap = new GameStateSnapshot(); if (_memory is null) { snap.Error = "Not attached"; return snap; } snap.Attached = true; snap.ProcessId = _memory.ProcessId; snap.ModuleBase = _moduleBase; snap.ModuleSize = _moduleSize; snap.OffsetsConfigured = _gameStateBase != 0; snap.GameStateBase = _gameStateBase; if (_gameStateBase == 0) return snap; // Static area level — direct module offset, always reliable (CE: exe+3E84B78) if (_offsets.AreaLevelStaticOffset > 0 && _moduleBase != 0) { var level = _memory.Read(_moduleBase + _offsets.AreaLevelStaticOffset); if (level > 0 && level < 200) snap.AreaLevel = level; } try { // Resolve InGameState from controller var inGameState = ResolveInGameState(snap); if (inGameState == 0) return snap; snap.InGameStatePtr = inGameState; // InGameState → AreaInstance (dump: InGameStateOffset.AreaInstanceData at +0x298) var ingameData = _memory.ReadPointer(inGameState + _offsets.IngameDataFromStateOffset); snap.AreaInstancePtr = ingameData; if (ingameData != 0) { // Area level (dump: byte at +0xAC) if (_offsets.AreaLevelIsByte) { var level = _memory.Read(ingameData + _offsets.AreaLevelOffset); if (level > 0 && level < 200) snap.AreaLevel = level; } else { var level = _memory.Read(ingameData + _offsets.AreaLevelOffset); if (level > 0 && level < 200) snap.AreaLevel = level; } // Area hash (dump: 0xEC) snap.AreaHash = _memory.Read(ingameData + _offsets.AreaHashOffset); // ServerData pointer (dump: via LocalPlayerStruct at +0x9F0) var serverData = _memory.ReadPointer(ingameData + _offsets.ServerDataOffset); snap.ServerDataPtr = serverData; // LocalPlayer — try direct offset first (dump: +0xA10), fallback to ServerData chain if (_offsets.LocalPlayerDirectOffset > 0) snap.LocalPlayerPtr = _memory.ReadPointer(ingameData + _offsets.LocalPlayerDirectOffset); if (snap.LocalPlayerPtr == 0 && serverData != 0) snap.LocalPlayerPtr = _memory.ReadPointer(serverData + _offsets.LocalPlayerOffset); // Entity count — dump: EntityListStruct contains StdMap, count at StdMap+0x08 (_Mysize) var entityCount = (int)_memory.Read(ingameData + _offsets.EntityListOffset + _offsets.EntityCountInternalOffset); if (entityCount > 0 && entityCount < 50000) snap.EntityCount = entityCount; // Player vitals & position — ECS: LocalPlayer → ComponentList → Life/Render components if (snap.LocalPlayerPtr != 0) { // Invalidate caches if LocalPlayer entity changed (zone change, new character) if (snap.LocalPlayerPtr != _lastLocalPlayer) { _cachedLifeIndex = -1; _cachedRenderIndex = -1; _lastLocalPlayer = snap.LocalPlayerPtr; } ReadVitalsEcs(snap); ReadPlayerPosition(snap); } // Terrain ReadTerrain(snap, ingameData); } } catch (Exception ex) { Log.Debug(ex, "Error reading snapshot"); } return snap; } private nint ResolveInGameState(GameStateSnapshot snap) { // Global points to the GameStateMachine controller object var controller = _memory!.ReadPointer(_gameStateBase); if (controller == 0) return 0; snap.ControllerPtr = controller; // Direct offset mode: read InGameState straight from controller (CE confirmed: +0x210) if (_offsets.InGameStateDirectOffset > 0) { var igs = _memory.ReadPointer(controller + _offsets.InGameStateDirectOffset); if (igs != 0) { // Count states for display (scan inline slots) for (var i = 0; i < 20; i++) { var slotOffset = _offsets.StatesBeginOffset + i * _offsets.StateStride + _offsets.StatePointerOffset; var ptr = _memory.ReadPointer(controller + slotOffset); if (ptr == 0) break; snap.StatesCount++; } return igs; } } if (_offsets.StatesInline) { // POE1-style: states stored INLINE at fixed offsets in the controller struct // InGameState = controller + StatesBeginOffset + InGameStateIndex * StateStride + StatePointerOffset // POE1: controller + 0x48 + 4*0x10 + 0 = controller + 0x88 var inlineOffset = _offsets.StatesBeginOffset + _offsets.InGameStateIndex * _offsets.StateStride + _offsets.StatePointerOffset; // Count states by scanning inline slots for (var i = 0; i < 20; i++) { var slotOffset = _offsets.StatesBeginOffset + i * _offsets.StateStride + _offsets.StatePointerOffset; var ptr = _memory.ReadPointer(controller + slotOffset); if (ptr == 0) break; snap.StatesCount++; } return _memory.ReadPointer(controller + inlineOffset); } else { // Vector-style: StatesBeginOffset points to a std::vector of state entries var statesBegin = _memory.ReadPointer(controller + _offsets.StatesBeginOffset); if (statesBegin == 0) return 0; var statesEnd = _memory.ReadPointer(controller + _offsets.StatesBeginOffset + 8); if (statesEnd > statesBegin && statesEnd - statesBegin < 0x1000 && _offsets.StateStride > 0) { snap.StatesCount = (int)((statesEnd - statesBegin) / _offsets.StateStride); } else { for (var i = 0; i < 20; i++) { if (_memory.ReadPointer(statesBegin + i * _offsets.StateStride + _offsets.StatePointerOffset) == 0) break; snap.StatesCount++; } } if (_offsets.InGameStateIndex < 0 || _offsets.InGameStateIndex >= snap.StatesCount) return 0; return _memory.ReadPointer(statesBegin + _offsets.InGameStateIndex * _offsets.StateStride + _offsets.StatePointerOffset); } } /// /// Reads vitals via Entity Component System: LocalPlayer → ComponentList StdVector → Life component. /// Auto-discovers the Life component index by scanning for VitalStruct pattern, caches the index. /// Stable across zone changes because the ECS pointers are maintained by the game engine. /// private void ReadVitalsEcs(GameStateSnapshot snap) { var entity = snap.LocalPlayerPtr; if (entity == 0) return; // Find component array — try multiple strategies var (compFirst, count) = FindComponentList(entity); if (count <= 0) return; // Try cached index first if (_cachedLifeIndex >= 0 && _cachedLifeIndex < count) { var lifeComp = _memory!.ReadPointer(compFirst + _cachedLifeIndex * 8); if (lifeComp != 0 && TryReadVitals(snap, lifeComp)) return; // Cache miss — index shifted, re-scan _cachedLifeIndex = -1; } // Scan all component pointers for VitalStruct pattern (HP + Mana at known offsets) for (var i = 0; i < count; i++) { var compPtr = _memory!.ReadPointer(compFirst + i * 8); if (compPtr == 0) continue; // Quick heap pointer validation var high = (ulong)compPtr >> 32; if (high == 0 || high >= 0x7FFF) continue; if ((compPtr & 0x3) != 0) continue; // Check VitalStruct pattern: HP.Total must be reasonable player HP (not 1/1 false positives) var hpTotal = _memory.Read(compPtr + _offsets.LifeHealthOffset + _offsets.VitalTotalOffset); if (hpTotal < 20 || hpTotal > 200000) continue; var hpCurrent = _memory.Read(compPtr + _offsets.LifeHealthOffset + _offsets.VitalCurrentOffset); if (hpCurrent < 0 || hpCurrent > hpTotal + 1000) continue; var manaTotal = _memory.Read(compPtr + _offsets.LifeManaOffset + _offsets.VitalTotalOffset); if (manaTotal < 0 || manaTotal > 200000) continue; var manaCurrent = _memory.Read(compPtr + _offsets.LifeManaOffset + _offsets.VitalCurrentOffset); if (manaCurrent < 0 || manaCurrent > manaTotal + 1000) continue; // Require at least some mana or ES (all player characters have base mana) var esTotal = _memory.Read(compPtr + _offsets.LifeEsOffset + _offsets.VitalTotalOffset); if (manaTotal == 0 && esTotal == 0) continue; // Found Life component — cache index and read vitals _cachedLifeIndex = i; Log.Information("ECS: Life component at index {Index} (0x{Addr:X}) — HP: {Hp}/{HpMax}, Mana: {Mana}/{ManaMax}", i, compPtr, hpCurrent, hpTotal, manaCurrent, manaTotal); TryReadVitals(snap, compPtr); return; } } /// /// Finds the best component list from the entity, trying multiple strategies: /// 1. StdVector at entity+ComponentListOffset (ExileCore standard) /// 2. Inner entity: entity+0x000 → deref → StdVector at +ComponentListOffset (POE2 wrapper) /// 3. Scan entity memory for any StdVector with many pointer-sized elements /// private (nint First, int Count) FindComponentList(nint entity) { // Strategy 1: direct StdVector at entity+ComponentListOffset var compFirst = _memory!.ReadPointer(entity + _offsets.ComponentListOffset); var compLast = _memory.ReadPointer(entity + _offsets.ComponentListOffset + 8); var count = 0; if (compFirst != 0 && compLast > compFirst && (compLast - compFirst) < 0x2000) count = (int)((compLast - compFirst) / 8); if (count > 1) return (compFirst, count); // Strategy 2: POE2 may wrap entities — entity+0x000 is a pointer to the real entity var innerEntity = _memory.ReadPointer(entity); if (innerEntity != 0 && innerEntity != entity && !IsModuleAddress(innerEntity)) { var high = (ulong)innerEntity >> 32; if (high > 0 && high < 0x7FFF && (innerEntity & 0x3) == 0) { var innerFirst = _memory.ReadPointer(innerEntity + _offsets.ComponentListOffset); var innerLast = _memory.ReadPointer(innerEntity + _offsets.ComponentListOffset + 8); if (innerFirst != 0 && innerLast > innerFirst && (innerLast - innerFirst) < 0x2000) { var innerCount = (int)((innerLast - innerFirst) / 8); if (innerCount > count) { Log.Debug("ECS: Using inner entity 0x{Addr:X} component list ({Count} entries)", innerEntity, innerCount); return (innerFirst, innerCount); } } } } // Strategy 3: scan entity memory for StdVector patterns with ≥3 pointer-sized elements var entityData = _memory.ReadBytes(entity, 0x300); if (entityData is not null) { for (var off = 0; off + 24 <= entityData.Length; off += 8) { var f = (nint)BitConverter.ToInt64(entityData, off); var l = (nint)BitConverter.ToInt64(entityData, off + 8); if (f == 0 || l <= f) continue; var sz = l - f; if (sz < 24 || sz > 0x2000 || sz % 8 != 0) continue; var n = (int)(sz / 8); if (n <= count) continue; // Verify: first element should be a valid heap pointer var firstEl = _memory.ReadPointer(f); var h = (ulong)firstEl >> 32; if (h == 0 || h >= 0x7FFF || (firstEl & 0x3) != 0) continue; Log.Debug("ECS: Found StdVector at entity+0x{Off:X} with {Count} elements", off, n); if (n > count) { compFirst = f; count = n; } } } return (compFirst, count); } /// /// Attempts to read all vitals from a Life component pointer. Returns true if values pass sanity checks. /// private bool TryReadVitals(GameStateSnapshot snap, nint lifeComp) { var hp = _memory!.Read(lifeComp + _offsets.LifeHealthOffset + _offsets.VitalCurrentOffset); var hpMax = _memory.Read(lifeComp + _offsets.LifeHealthOffset + _offsets.VitalTotalOffset); var mana = _memory.Read(lifeComp + _offsets.LifeManaOffset + _offsets.VitalCurrentOffset); var manaMax = _memory.Read(lifeComp + _offsets.LifeManaOffset + _offsets.VitalTotalOffset); var es = _memory.Read(lifeComp + _offsets.LifeEsOffset + _offsets.VitalCurrentOffset); var esMax = _memory.Read(lifeComp + _offsets.LifeEsOffset + _offsets.VitalTotalOffset); // Sanity check: values must be non-negative and within reasonable range if (hpMax <= 0 || hpMax > 200000 || hp < 0 || hp > hpMax + 1000) return false; if (manaMax < 0 || manaMax > 200000 || mana < 0) return false; snap.HasVitals = true; snap.LifeCurrent = hp; snap.LifeTotal = hpMax; snap.ManaCurrent = mana; snap.ManaTotal = manaMax; snap.EsCurrent = es; snap.EsTotal = esMax; return true; } /// /// Reads player position from the Render component via ECS. /// Auto-discovers the Render component index by scanning for world-coordinate float triplets, caches the index. /// private void ReadPlayerPosition(GameStateSnapshot snap) { var entity = snap.LocalPlayerPtr; if (entity == 0) return; var (compFirst, count) = FindComponentList(entity); if (count <= 0) return; // Try configured index first if (_offsets.RenderComponentIndex >= 0) { var idx = _offsets.RenderComponentIndex; if (idx < count) { var renderComp = _memory!.ReadPointer(compFirst + idx * 8); if (renderComp != 0 && TryReadPosition(snap, renderComp)) return; } } // Try cached index if (_cachedRenderIndex >= 0 && _cachedRenderIndex < count) { var renderComp = _memory!.ReadPointer(compFirst + _cachedRenderIndex * 8); if (renderComp != 0 && TryReadPosition(snap, renderComp)) return; _cachedRenderIndex = -1; } // Auto-discover: scan components for float triplet that looks like world coordinates for (var i = 0; i < count; i++) { if (i == _cachedLifeIndex) continue; var compPtr = _memory!.ReadPointer(compFirst + i * 8); if (compPtr == 0) continue; var high = (ulong)compPtr >> 32; if (high == 0 || high >= 0x7FFF) continue; if ((compPtr & 0x3) != 0) continue; if (TryReadPosition(snap, compPtr)) { _cachedRenderIndex = i; Log.Information("ECS: Render component at index {Index} (0x{Addr:X}) — Pos: ({X:F1}, {Y:F1}, {Z:F1})", i, compPtr, snap.PlayerX, snap.PlayerY, snap.PlayerZ); return; } } } /// /// Attempts to read position from a Render component pointer. Returns true if values look like valid world coordinates. /// private bool TryReadPosition(GameStateSnapshot snap, nint renderComp) { var x = _memory!.Read(renderComp + _offsets.PositionXOffset); var y = _memory.Read(renderComp + _offsets.PositionYOffset); var z = _memory.Read(renderComp + _offsets.PositionZOffset); if (float.IsNaN(x) || float.IsNaN(y) || float.IsNaN(z)) return false; if (float.IsInfinity(x) || float.IsInfinity(y) || float.IsInfinity(z)) return false; // POE2 world coordinates: typically 50-50000 range for X/Y if (x < 50 || x > 50000 || y < 50 || y > 50000) return false; if (MathF.Abs(z) > 5000) return false; snap.HasPosition = true; snap.PlayerX = x; snap.PlayerY = y; snap.PlayerZ = z; return true; } private void ReadTerrain(GameStateSnapshot snap, nint areaInstance) { if (_offsets.TerrainInline) { // Dump: TerrainStruct is inline at AreaInstance + 0xCC0 // TotalTiles (StdTuple2D) at TerrainStruct + 0x18 var terrainBase = areaInstance + _offsets.TerrainListOffset; var cols = (int)_memory!.Read(terrainBase + _offsets.TerrainDimensionsOffset); var rows = (int)_memory.Read(terrainBase + _offsets.TerrainDimensionsOffset + 8); if (cols > 0 && cols < 1000 && rows > 0 && rows < 1000) { snap.TerrainCols = cols; snap.TerrainRows = rows; snap.TerrainWidth = cols * _offsets.SubTilesPerCell; snap.TerrainHeight = rows * _offsets.SubTilesPerCell; } } else { // Pointer-based: AreaInstance → TerrainList → first terrain → dimensions var terrainListPtr = _memory!.ReadPointer(areaInstance + _offsets.TerrainListOffset); if (terrainListPtr == 0) return; var terrainPtr = _memory.ReadPointer(terrainListPtr); if (terrainPtr == 0) return; var dimsPtr = _memory.ReadPointer(terrainPtr + _offsets.TerrainDimensionsOffset); if (dimsPtr == 0) return; snap.TerrainCols = _memory.Read(dimsPtr); snap.TerrainRows = _memory.Read(dimsPtr + 4); if (snap.TerrainCols > 0 && snap.TerrainCols < 1000 && snap.TerrainRows > 0 && snap.TerrainRows < 1000) { snap.TerrainWidth = snap.TerrainCols * _offsets.SubTilesPerCell; snap.TerrainHeight = snap.TerrainRows * _offsets.SubTilesPerCell; } else { snap.TerrainCols = 0; snap.TerrainRows = 0; } } } /// /// Raw explorer: parse hex address, follow offset chain, read as specified type. /// public string ReadAddress(string hexAddr, string offsetsCsv, string type) { if (_memory is null) return "Error: not attached"; if (!nint.TryParse(hexAddr, NumberStyles.HexNumber, null, out var addr)) return $"Error: invalid address '{hexAddr}'"; // Parse offsets var offsets = Array.Empty(); if (!string.IsNullOrWhiteSpace(offsetsCsv)) { var parts = offsetsCsv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); offsets = new int[parts.Length]; for (var i = 0; i < parts.Length; i++) { if (!int.TryParse(parts[i], NumberStyles.HexNumber, null, out offsets[i])) return $"Error: invalid offset '{parts[i]}'"; } } // Follow chain if offsets provided if (offsets.Length > 0) { addr = _memory.FollowChain(addr, offsets); if (addr == 0) return "Error: pointer chain broken (null)"; } return type.ToLowerInvariant() switch { "int32" => _memory.Read(addr).ToString(), "int64" => _memory.Read(addr).ToString(), "float" => _memory.Read(addr).ToString("F4"), "double" => _memory.Read(addr).ToString("F4"), "pointer" => $"0x{_memory.ReadPointer(addr):X}", "bytes16" => FormatBytes(_memory.ReadBytes(addr, 16)), "string" => ReadNullTermString(addr), _ => $"Error: unknown type '{type}'" }; } /// /// Scans a memory region and returns all pointer-like values with their offsets. /// public string ScanRegion(string hexAddr, string offsetsCsv, int size) { if (_memory is null) return "Error: not attached"; if (!nint.TryParse(hexAddr, NumberStyles.HexNumber, null, out var addr)) return $"Error: invalid address '{hexAddr}'"; // Parse and follow offset chain if (!string.IsNullOrWhiteSpace(offsetsCsv)) { var parts = offsetsCsv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var offsets = new int[parts.Length]; for (var i = 0; i < parts.Length; i++) { if (!int.TryParse(parts[i], NumberStyles.HexNumber, null, out offsets[i])) return $"Error: invalid offset '{parts[i]}'"; } addr = _memory.FollowChain(addr, offsets); if (addr == 0) return "Error: pointer chain broken (null)"; } // Read the full block size = Math.Clamp(size, 0x10, 0x10000); var data = _memory.ReadBytes(addr, size); if (data is null) return "Error: read failed"; var sb = new StringBuilder(); sb.AppendLine($"Scan: 0x{addr:X} size: 0x{size:X}"); sb.AppendLine(new string('─', 60)); for (var offset = 0; offset + 8 <= data.Length; offset += 8) { var value = BitConverter.ToUInt64(data, offset); if (value == 0) continue; var nVal = (nint)(long)value; var tag = ClassifyPointer(nVal); if (tag is null) { // Show non-zero non-pointer values only if they look like small ints or floats if (value <= 0xFFFF) { sb.AppendLine($"+0x{offset:X3}: {value,-20} [int: {value}]"); } else { var f1 = BitConverter.ToSingle(data, offset); var f2 = BitConverter.ToSingle(data, offset + 4); if (IsReasonableFloat(f1) || IsReasonableFloat(f2)) { sb.AppendLine($"+0x{offset:X3}: {f1,12:F2} {f2,12:F2} [float pair]"); } } continue; } sb.AppendLine($"+0x{offset:X3}: 0x{value:X} [{tag}]"); } return sb.ToString(); } /// /// Resolves a vtable address to its RTTI class name using MSVC x64 RTTI layout. /// vtable[-1] → RTTICompleteObjectLocator → TypeDescriptor → mangled name /// public string? ResolveRttiName(nint vtableAddr) { if (_memory is null || _moduleBase == 0) return null; try { // vtable[-1] = pointer to RTTICompleteObjectLocator var colPtr = _memory.ReadPointer(vtableAddr - 8); if (colPtr == 0) return null; // COL signature check: x64 = 1 var signature = _memory.Read(colPtr); if (signature != 1) return null; // COL+0x0C = typeDescriptorOffset (image-relative, 4 bytes) var typeDescOffset = _memory.Read(colPtr + 0x0C); if (typeDescOffset <= 0) return null; // TypeDescriptor = moduleBase + typeDescOffset var typeDesc = _moduleBase + typeDescOffset; // TypeDescriptor+0x10 = mangled name string (e.g. ".?AVInGameState@@") var nameBytes = _memory.ReadBytes(typeDesc + 0x10, 128); if (nameBytes is null) return null; var end = Array.IndexOf(nameBytes, (byte)0); if (end <= 0) return null; var mangled = Encoding.ASCII.GetString(nameBytes, 0, end); // Demangle: ".?AVClassName@@" → "ClassName" if (mangled.StartsWith(".?AV") && mangled.EndsWith("@@")) return mangled[4..^2]; if (mangled.StartsWith(".?AU") && mangled.EndsWith("@@")) return mangled[4..^2]; return mangled; } catch { return null; } } private bool IsModuleAddress(nint value) { return _moduleBase != 0 && _moduleSize > 0 && value >= _moduleBase && value < _moduleBase + _moduleSize; } private string? ClassifyPointer(nint value) { if (value == 0) return null; // Module range — try RTTI resolution if (IsModuleAddress(value)) { var name = ResolveRttiName(value); return name ?? "module (vtable?)"; } // Heap heuristic: user-mode addresses in typical 64-bit range if (value > 0x10000 && value < (nint)0x7FFFFFFFFFFF && (value & 0x3) == 0) { var high = (ulong)value >> 32; if (high > 0 && high < 0x7FFF) return "heap ptr"; } return null; } /// /// Scans all game states and returns their info. /// Supports both inline (POE1-style) and vector modes. /// public string ScanAllStates() { if (_memory is null) return "Error: not attached"; if (_gameStateBase == 0) return "Error: GameState base not resolved"; var controller = _memory.ReadPointer(_gameStateBase); if (controller == 0) return "Error: controller is null"; var sb = new StringBuilder(); sb.AppendLine($"Controller: 0x{controller:X}"); sb.AppendLine($"Mode: {(_offsets.StatesInline ? "inline" : "vector")}"); int count; if (_offsets.StatesInline) { // Inline: count by scanning slots until null count = 0; for (var i = 0; i < 30; i++) { var slotOffset = _offsets.StatesBeginOffset + i * _offsets.StateStride + _offsets.StatePointerOffset; var ptr = _memory.ReadPointer(controller + slotOffset); if (ptr == 0) break; count++; } sb.AppendLine($"States: {count} (inline at controller+0x{_offsets.StatesBeginOffset:X})"); sb.AppendLine(new string('─', 70)); for (var i = 0; i < count; i++) { var slotOffset = _offsets.StatesBeginOffset + i * _offsets.StateStride + _offsets.StatePointerOffset; var statePtr = _memory.ReadPointer(controller + slotOffset); string? stateName = null; if (statePtr != 0) { var stateVtable = _memory.ReadPointer(statePtr); if (stateVtable != 0 && IsModuleAddress(stateVtable)) stateName = ResolveRttiName(stateVtable); } var marker = i == _offsets.InGameStateIndex ? " ◄◄◄" : ""; var tag = IsModuleAddress(statePtr) ? " [module!]" : ""; sb.AppendLine($"[{i}] +0x{slotOffset:X}: 0x{statePtr:X} {stateName ?? "?"}{tag}{marker}"); } return sb.ToString(); } // Vector mode (fallback) var statesBegin = _memory.ReadPointer(controller + _offsets.StatesBeginOffset); if (statesBegin == 0) return "Error: states begin is null"; var statesEnd = _memory.ReadPointer(controller + _offsets.StatesBeginOffset + 8); count = 0; if (statesEnd > statesBegin && statesEnd - statesBegin < 0x1000 && _offsets.StateStride > 0) count = (int)((statesEnd - statesBegin) / _offsets.StateStride); else { for (var i = 0; i < 30; i++) { if (_memory.ReadPointer(statesBegin + i * _offsets.StateStride + _offsets.StatePointerOffset) == 0) break; count++; } } sb.AppendLine($"States: {count} (vector begin: 0x{statesBegin:X})"); sb.AppendLine(new string('─', 70)); for (var i = 0; i < count; i++) { var entryBase = statesBegin + i * _offsets.StateStride; var vtable = _memory.ReadPointer(entryBase); var statePtr = _memory.ReadPointer(entryBase + _offsets.StatePointerOffset); var vtableName = vtable != 0 && IsModuleAddress(vtable) ? ResolveRttiName(vtable) : null; string? stateName = null; if (statePtr != 0) { var stateVtable = _memory.ReadPointer(statePtr); if (stateVtable != 0 && IsModuleAddress(stateVtable)) stateName = ResolveRttiName(stateVtable); } var marker = i == _offsets.InGameStateIndex ? " ◄◄◄" : ""; sb.AppendLine($"[{i}] 0x{statePtr:X} {stateName ?? "?"}{marker}"); } return sb.ToString(); } /// /// Scans an object's memory and identifies all RTTI-typed sub-elements. /// Groups by vtable to show the structure with class names. /// public string ScanStructure(string hexAddr, string offsetsCsv, int size) { if (_memory is null) return "Error: not attached"; if (!nint.TryParse(hexAddr, NumberStyles.HexNumber, null, out var addr)) return $"Error: invalid address '{hexAddr}'"; if (!string.IsNullOrWhiteSpace(offsetsCsv)) { var parts = offsetsCsv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var offsets = new int[parts.Length]; for (var i = 0; i < parts.Length; i++) { if (!int.TryParse(parts[i], NumberStyles.HexNumber, null, out offsets[i])) return $"Error: invalid offset '{parts[i]}'"; } addr = _memory.FollowChain(addr, offsets); if (addr == 0) return "Error: pointer chain broken (null)"; } size = Math.Clamp(size, 0x10, 0x10000); var data = _memory.ReadBytes(addr, size); if (data is null) return "Error: read failed"; // First pass: get RTTI name for the object itself var objVtable = BitConverter.ToInt64(data, 0); var objName = objVtable != 0 && IsModuleAddress((nint)objVtable) ? ResolveRttiName((nint)objVtable) : null; var sb = new StringBuilder(); sb.AppendLine($"Structure: 0x{addr:X} size: 0x{size:X} type: {objName ?? "?"}"); sb.AppendLine(new string('─', 80)); // Scan for all vtable pointers and resolve their RTTI names for (var offset = 0; offset + 8 <= data.Length; offset += 8) { var value = (nint)BitConverter.ToInt64(data, offset); if (value == 0) continue; if (IsModuleAddress(value)) { var name = ResolveRttiName(value); if (name is not null) { // Check if next qword is a heap pointer (data for this element) nint dataPtr = 0; if (offset + 16 <= data.Length) dataPtr = (nint)BitConverter.ToInt64(data, offset + 8); sb.AppendLine($"+0x{offset:X4}: [{name}] data: 0x{dataPtr:X}"); } else { sb.AppendLine($"+0x{offset:X4}: 0x{value:X} [module ?]"); } } } return sb.ToString(); } /// /// Scans the LocalPlayer's component list, resolves RTTI names, and searches each component /// for known vital values (HP, Mana, ES) to determine component indices and offsets. /// public string ScanComponents(int hpValue, int manaValue, int esValue) { if (_memory is null) return "Error: not attached"; if (_gameStateBase == 0) return "Error: GameState base not resolved"; var snap = new GameStateSnapshot(); var inGameState = ResolveInGameState(snap); if (inGameState == 0) return "Error: InGameState not resolved"; var ingameData = _memory.ReadPointer(inGameState + _offsets.IngameDataFromStateOffset); if (ingameData == 0) return "Error: AreaInstance not resolved"; // Try direct LocalPlayer offset first, then ServerData chain var localPlayer = nint.Zero; if (_offsets.LocalPlayerDirectOffset > 0) localPlayer = _memory.ReadPointer(ingameData + _offsets.LocalPlayerDirectOffset); if (localPlayer == 0) { var serverData = _memory.ReadPointer(ingameData + _offsets.ServerDataOffset); if (serverData != 0) localPlayer = _memory.ReadPointer(serverData + _offsets.LocalPlayerOffset); } if (localPlayer == 0) return "Error: LocalPlayer not resolved"; var sb = new StringBuilder(); sb.AppendLine($"LocalPlayer: 0x{localPlayer:X}"); // Find component list (tries direct, inner entity, and StdVector scan) var (compBegin, vectorCount) = FindComponentList(localPlayer); if (compBegin == 0) return "Error: ComponentList not found (tried direct, inner entity, and scan)"; sb.AppendLine($"ComponentList: 0x{compBegin:X} count: {vectorCount}"); sb.AppendLine(new string('═', 90)); var searchValues = new (string name, int value)[] { ("HP", hpValue), ("Mana", manaValue), ("ES", esValue) }; // Determine scan limit: use vector bounds if valid, otherwise scan up to 64 slots var maxSlots = vectorCount > 0 ? vectorCount : 64; var realCount = 0; for (var i = 0; i < maxSlots; i++) { var compPtr = _memory.ReadPointer(compBegin + i * 8); if (compPtr == 0) continue; // Null slot // Filter bogus pointers (pointing back into entity/list region) var distFromEntity = Math.Abs((long)(compPtr - localPlayer)); if (distFromEntity < 0x200) { sb.AppendLine($"[{i,2}] 0x{compPtr:X} (skip: within entity struct)"); continue; } var high = (ulong)compPtr >> 32; if (high == 0 || high >= 0x7FFF) { if (vectorCount == 0) break; // No vector bounds, stop on invalid continue; } realCount++; // Get RTTI name var vtable = _memory.ReadPointer(compPtr); string? rtti = null; if (vtable != 0 && IsModuleAddress(vtable)) rtti = ResolveRttiName(vtable); var line = $"[{i,2}] 0x{compPtr:X} {rtti ?? "?"}"; // Scan this component's memory for vital values (deeper: 0x2000 = 8KB) var compSize = 0x2000; var compData = _memory.ReadBytes(compPtr, compSize); if (compData is not null) { var hits = new List(); foreach (var (vName, vValue) in searchValues) { if (vValue == 0) continue; for (var off = 0; off + 4 <= compData.Length; off += 4) { var val = BitConverter.ToInt32(compData, off); if (val == vValue) hits.Add($"{vName}={vValue}@+0x{off:X}"); } } if (hits.Count > 0) line += $"\n ◄ {string.Join(", ", hits)}"; } sb.AppendLine(line); } sb.AppendLine($"\nReal components: {realCount}"); // Also scan the entity struct itself (first 0x200 bytes) for vitals sb.AppendLine($"\n── Entity scan (0x{localPlayer:X}, 0x200 bytes) ──"); var entityData = _memory.ReadBytes(localPlayer, 0x200); if (entityData is not null) { foreach (var (vName, vValue) in searchValues) { if (vValue == 0) continue; for (var off = 0; off + 4 <= entityData.Length; off += 4) { var val = BitConverter.ToInt32(entityData, off); if (val == vValue) sb.AppendLine($" Entity+0x{off:X}: {vName}={vValue}"); } } } return sb.ToString(); } /// /// Scans all components for float triplets (X,Y,Z) that look like world coordinates. /// Finds the Render component and the correct position offsets within it. /// public string ScanPosition() { if (_memory is null) return "Error: not attached"; if (_gameStateBase == 0) return "Error: GameState base not resolved"; var snap = new GameStateSnapshot(); var inGameState = ResolveInGameState(snap); if (inGameState == 0) return "Error: InGameState not resolved"; var ingameData = _memory.ReadPointer(inGameState + _offsets.IngameDataFromStateOffset); if (ingameData == 0) return "Error: AreaInstance not resolved"; var localPlayer = nint.Zero; if (_offsets.LocalPlayerDirectOffset > 0) localPlayer = _memory.ReadPointer(ingameData + _offsets.LocalPlayerDirectOffset); if (localPlayer == 0) { var serverData = _memory.ReadPointer(ingameData + _offsets.ServerDataOffset); if (serverData != 0) localPlayer = _memory.ReadPointer(serverData + _offsets.LocalPlayerOffset); } if (localPlayer == 0) return "Error: LocalPlayer not resolved"; var sb = new StringBuilder(); sb.AppendLine($"LocalPlayer: 0x{localPlayer:X}"); var (compBegin, vectorCount) = FindComponentList(localPlayer); if (compBegin == 0) return "Error: ComponentList not found"; sb.AppendLine($"ComponentList: 0x{compBegin:X} count: {vectorCount}"); sb.AppendLine(new string('═', 90)); sb.AppendLine("Scanning each component (8KB) for float triplets that look like world coordinates..."); sb.AppendLine("(X,Y in 50-50000, |Z| < 5000, consecutive floats at 4-byte alignment)"); sb.AppendLine(); var maxSlots = vectorCount > 0 ? vectorCount : 64; for (var i = 0; i < maxSlots; i++) { var compPtr = _memory.ReadPointer(compBegin + i * 8); if (compPtr == 0) continue; var high = (ulong)compPtr >> 32; if (high == 0 || high >= 0x7FFF) continue; if ((compPtr & 0x3) != 0) continue; // Skip pointers that are too close to entity struct var dist = Math.Abs((long)(compPtr - localPlayer)); if (dist < 0x200) continue; // RTTI var vtable = _memory.ReadPointer(compPtr); string? rtti = null; if (vtable != 0 && IsModuleAddress(vtable)) rtti = ResolveRttiName(vtable); // Read 8KB of component data var compData = _memory.ReadBytes(compPtr, 0x2000); if (compData is null) continue; var hits = new List(); for (var off = 0; off + 12 <= compData.Length; off += 4) { var x = BitConverter.ToSingle(compData, off); var y = BitConverter.ToSingle(compData, off + 4); var z = BitConverter.ToSingle(compData, off + 8); if (float.IsNaN(x) || float.IsNaN(y) || float.IsNaN(z)) continue; if (float.IsInfinity(x) || float.IsInfinity(y) || float.IsInfinity(z)) continue; if (x < 50 || x > 50000 || y < 50 || y > 50000) continue; if (MathF.Abs(z) > 5000) continue; hits.Add($"+0x{off:X}: ({x:F1}, {y:F1}, {z:F1})"); } if (hits.Count > 0) { sb.AppendLine($"[{i,2}] 0x{compPtr:X} {rtti ?? "?"}"); foreach (var hit in hits.Take(10)) sb.AppendLine($" ◄ {hit}"); if (hits.Count > 10) sb.AppendLine($" ... and {hits.Count - 10} more"); sb.AppendLine(); } } sb.AppendLine("Done."); sb.AppendLine(); sb.AppendLine("If a component shows position hits, update offsets.json:"); sb.AppendLine(" PositionXOffset = the offset shown (e.g. 0xB0)"); sb.AppendLine(" PositionYOffset = PositionXOffset + 4"); sb.AppendLine(" PositionZOffset = PositionXOffset + 8"); return sb.ToString(); } /// /// Diagnostic: shows the ECS vitals reading state — LocalPlayer, component list, cached Life index, current values. /// public string DiagnoseVitals() { if (_memory is null) return "Error: not attached"; if (_gameStateBase == 0) return "Error: GameState base not resolved"; var snap = new GameStateSnapshot(); var inGameState = ResolveInGameState(snap); if (inGameState == 0) return "Error: InGameState not resolved"; var ingameData = _memory.ReadPointer(inGameState + _offsets.IngameDataFromStateOffset); if (ingameData == 0) return "Error: IngameData not resolved"; // Resolve LocalPlayer var localPlayer = nint.Zero; if (_offsets.LocalPlayerDirectOffset > 0) localPlayer = _memory.ReadPointer(ingameData + _offsets.LocalPlayerDirectOffset); if (localPlayer == 0) { var serverData = _memory.ReadPointer(ingameData + _offsets.ServerDataOffset); if (serverData != 0) localPlayer = _memory.ReadPointer(serverData + _offsets.LocalPlayerOffset); } var sb = new StringBuilder(); sb.AppendLine("── ECS Vitals Diagnostics ──"); sb.AppendLine($"IngameData: 0x{ingameData:X}"); sb.AppendLine($"LocalPlayer: 0x{localPlayer:X}"); sb.AppendLine($"Cached Life index: {_cachedLifeIndex}"); sb.AppendLine($"Last LocalPlayer: 0x{_lastLocalPlayer:X}"); sb.AppendLine(); if (localPlayer == 0) { sb.AppendLine("FAIL: LocalPlayer is null"); return sb.ToString(); } // Read StdVector at entity+ComponentListOffset var compFirst = _memory.ReadPointer(localPlayer + _offsets.ComponentListOffset); var compLast = _memory.ReadPointer(localPlayer + _offsets.ComponentListOffset + 8); var compEnd = _memory.ReadPointer(localPlayer + _offsets.ComponentListOffset + 16); var count = 0; if (compFirst != 0 && compLast > compFirst && (compLast - compFirst) < 0x2000) count = (int)((compLast - compFirst) / 8); sb.AppendLine($"ComponentList StdVector (entity+0x{_offsets.ComponentListOffset:X}):"); sb.AppendLine($" First: 0x{compFirst:X}"); sb.AppendLine($" Last: 0x{compLast:X}"); sb.AppendLine($" End: 0x{compEnd:X}"); sb.AppendLine($" Count: {count}"); sb.AppendLine(); if (count <= 0) { sb.AppendLine("FAIL: Component list empty or invalid"); return sb.ToString(); } // Scan components for Life pattern sb.AppendLine($"── Component scan ({count} entries) ──"); for (var i = 0; i < count && i < 40; i++) { var compPtr = _memory.ReadPointer(compFirst + i * 8); if (compPtr == 0) { sb.AppendLine($"[{i,2}] (null)"); continue; } var high = (ulong)compPtr >> 32; if (high == 0 || high >= 0x7FFF) { sb.AppendLine($"[{i,2}] 0x{compPtr:X} (invalid pointer)"); continue; } // RTTI var vtable = _memory.ReadPointer(compPtr); string? rtti = null; if (vtable != 0 && IsModuleAddress(vtable)) rtti = ResolveRttiName(vtable); // Check VitalStruct pattern var hpTotal = _memory.Read(compPtr + _offsets.LifeHealthOffset + _offsets.VitalTotalOffset); var hpCurr = _memory.Read(compPtr + _offsets.LifeHealthOffset + _offsets.VitalCurrentOffset); var manaTotal = _memory.Read(compPtr + _offsets.LifeManaOffset + _offsets.VitalTotalOffset); var manaCurr = _memory.Read(compPtr + _offsets.LifeManaOffset + _offsets.VitalCurrentOffset); var esTotal = _memory.Read(compPtr + _offsets.LifeEsOffset + _offsets.VitalTotalOffset); var esCurr = _memory.Read(compPtr + _offsets.LifeEsOffset + _offsets.VitalCurrentOffset); var hpOk = hpTotal > 0 && hpTotal < 200000 && hpCurr >= 0 && hpCurr <= hpTotal + 1000; var manaOk = manaTotal >= 0 && manaTotal < 200000 && manaCurr >= 0 && manaCurr <= manaTotal + 1000; var lifeTag = (hpOk && manaOk) ? $" ◄ LIFE HP={hpCurr}/{hpTotal} Mana={manaCurr}/{manaTotal} ES={esCurr}/{esTotal}" : ""; var cached = (i == _cachedLifeIndex) ? " [CACHED]" : ""; sb.AppendLine($"[{i,2}] 0x{compPtr:X} {rtti ?? "?"}{lifeTag}{cached}"); } return sb.ToString(); } /// /// Diagnostic: dumps LocalPlayer entity structure, component list with RTTI names, /// VitalStruct pattern matches, and ObjectHeader alternative path. /// public string DiagnoseEntity() { if (_memory is null) return "Error: not attached"; if (_gameStateBase == 0) return "Error: GameState base not resolved"; var snap = new GameStateSnapshot(); var inGameState = ResolveInGameState(snap); if (inGameState == 0) return "Error: InGameState not resolved"; var ingameData = _memory.ReadPointer(inGameState + _offsets.IngameDataFromStateOffset); if (ingameData == 0) return "Error: IngameData not resolved"; // Resolve LocalPlayer var localPlayer = nint.Zero; if (_offsets.LocalPlayerDirectOffset > 0) localPlayer = _memory.ReadPointer(ingameData + _offsets.LocalPlayerDirectOffset); if (localPlayer == 0) { var serverData = _memory.ReadPointer(ingameData + _offsets.ServerDataOffset); if (serverData != 0) localPlayer = _memory.ReadPointer(serverData + _offsets.LocalPlayerOffset); } if (localPlayer == 0) return "Error: LocalPlayer not resolved"; var sb = new StringBuilder(); sb.AppendLine($"LocalPlayer: 0x{localPlayer:X}"); // RTTI of entity var entityVtable = _memory.ReadPointer(localPlayer); string? entityRtti = null; if (entityVtable != 0 && IsModuleAddress(entityVtable)) entityRtti = ResolveRttiName(entityVtable); sb.AppendLine($"Entity RTTI: {entityRtti ?? "?"} (vtable: 0x{entityVtable:X})"); sb.AppendLine(); // Hex dump of first 0x100 bytes with pointer classification sb.AppendLine("── Entity hex dump (first 0x100 bytes) ──"); var entityData = _memory.ReadBytes(localPlayer, 0x100); if (entityData is not null) { for (var row = 0; row < entityData.Length; row += 16) { sb.Append($"+0x{row:X3}: "); for (var col = 0; col < 16 && row + col < entityData.Length; col++) { sb.Append($"{entityData[row + col]:X2} "); if (col == 7) sb.Append(' '); } // Pointer interpretation for each 8-byte slot sb.Append(" | "); for (var slot = 0; slot < 16 && row + slot + 8 <= entityData.Length; slot += 8) { var val = (nint)BitConverter.ToInt64(entityData, row + slot); if (val == 0) continue; if (IsModuleAddress(val)) { var name = ResolveRttiName(val); sb.Append(name is not null ? $"[{name}] " : "[module] "); } else { var h = (ulong)val >> 32; if (h > 0 && h < 0x7FFF && (val & 0x3) == 0) sb.Append("[heap] "); } } sb.AppendLine(); } } sb.AppendLine(); // StdVector at entity+ComponentListOffset sb.AppendLine($"── StdVector ComponentList at entity+0x{_offsets.ComponentListOffset:X} ──"); var compFirst = _memory.ReadPointer(localPlayer + _offsets.ComponentListOffset); var compLast = _memory.ReadPointer(localPlayer + _offsets.ComponentListOffset + 8); var compEnd = _memory.ReadPointer(localPlayer + _offsets.ComponentListOffset + 16); sb.AppendLine($"First: 0x{compFirst:X}"); sb.AppendLine($"Last: 0x{compLast:X}"); sb.AppendLine($"End: 0x{compEnd:X}"); var vectorCount = 0; if (compFirst != 0 && compLast > compFirst && (compLast - compFirst) < 0x2000) vectorCount = (int)((compLast - compFirst) / 8); sb.AppendLine($"Element count: {vectorCount}"); sb.AppendLine(); // List components with RTTI names and VitalStruct pattern check if (vectorCount > 0) { var maxShow = Math.Min(vectorCount, 30); sb.AppendLine($"── Components ({maxShow} of {vectorCount}) ──"); for (var i = 0; i < maxShow; i++) { var compPtr = _memory.ReadPointer(compFirst + i * 8); if (compPtr == 0) { sb.AppendLine($"[{i,2}] (null)"); continue; } var high = (ulong)compPtr >> 32; if (high == 0 || high >= 0x7FFF) { sb.AppendLine($"[{i,2}] 0x{compPtr:X} (bad pointer)"); continue; } // RTTI var compVtable = _memory.ReadPointer(compPtr); string? compRtti = null; if (compVtable != 0 && IsModuleAddress(compVtable)) compRtti = ResolveRttiName(compVtable); // VitalStruct pattern check var hpTotal = _memory.Read(compPtr + _offsets.LifeHealthOffset + _offsets.VitalTotalOffset); var hpCurr = _memory.Read(compPtr + _offsets.LifeHealthOffset + _offsets.VitalCurrentOffset); var manaTotal = _memory.Read(compPtr + _offsets.LifeManaOffset + _offsets.VitalTotalOffset); var manaCurr = _memory.Read(compPtr + _offsets.LifeManaOffset + _offsets.VitalCurrentOffset); var esTotal = _memory.Read(compPtr + _offsets.LifeEsOffset + _offsets.VitalTotalOffset); var esCurr = _memory.Read(compPtr + _offsets.LifeEsOffset + _offsets.VitalCurrentOffset); var hpOk = hpTotal > 0 && hpTotal < 200000 && hpCurr >= 0 && hpCurr <= hpTotal + 1000; var manaOk = manaTotal >= 0 && manaTotal < 200000 && manaCurr >= 0 && manaCurr <= manaTotal + 1000; var lifeTag = (hpOk && manaOk) ? $"\n ◄ LIFE: HP={hpCurr}/{hpTotal}, Mana={manaCurr}/{manaTotal}, ES={esCurr}/{esTotal}" : ""; // Position float triplet check var px = _memory.Read(compPtr + _offsets.PositionXOffset); var py = _memory.Read(compPtr + _offsets.PositionYOffset); var pz = _memory.Read(compPtr + _offsets.PositionZOffset); var posTag = (!float.IsNaN(px) && !float.IsNaN(py) && !float.IsNaN(pz) && px > 50 && px < 50000 && py > 50 && py < 50000 && MathF.Abs(pz) < 5000) ? $"\n ◄ RENDER: Pos=({px:F1}, {py:F1}, {pz:F1})" : ""; sb.AppendLine($"[{i,2}] 0x{compPtr:X} {compRtti ?? "?"}{lifeTag}{posTag}"); } } sb.AppendLine(); // Follow entity+0x000 as potential "inner entity" (POE2 wrapper pattern) sb.AppendLine($"── Inner Entity (entity+0x000 deref) ──"); var innerEntity = _memory.ReadPointer(localPlayer); sb.AppendLine($"Ptr: 0x{innerEntity:X}"); if (innerEntity != 0 && !IsModuleAddress(innerEntity)) { var innerHigh = (ulong)innerEntity >> 32; if (innerHigh > 0 && innerHigh < 0x7FFF && (innerEntity & 0x3) == 0) { // Read inner entity vtable and RTTI var innerVtable = _memory.ReadPointer(innerEntity); string? innerRtti = null; if (innerVtable != 0 && IsModuleAddress(innerVtable)) innerRtti = ResolveRttiName(innerVtable); sb.AppendLine($"Inner vtable: 0x{innerVtable:X} RTTI: {innerRtti ?? "?"}"); // Inner entity hex dump (first 0x80 bytes) var innerData = _memory.ReadBytes(innerEntity, 0x80); if (innerData is not null) { for (var row = 0; row < innerData.Length; row += 16) { sb.Append($" +0x{row:X3}: "); for (var col = 0; col < 16 && row + col < innerData.Length; col++) { sb.Append($"{innerData[row + col]:X2} "); if (col == 7) sb.Append(' '); } sb.AppendLine(); } } // Check StdVector at inner+ComponentListOffset var innerFirst = _memory.ReadPointer(innerEntity + _offsets.ComponentListOffset); var innerLast = _memory.ReadPointer(innerEntity + _offsets.ComponentListOffset + 8); var innerCount = 0; if (innerFirst != 0 && innerLast > innerFirst && (innerLast - innerFirst) < 0x2000) innerCount = (int)((innerLast - innerFirst) / 8); sb.AppendLine($"Inner ComponentList (inner+0x{_offsets.ComponentListOffset:X}): First=0x{innerFirst:X}, count={innerCount}"); // List inner components if (innerCount > 1) { var maxInner = Math.Min(innerCount, 20); for (var i = 0; i < maxInner; i++) { var cp = _memory.ReadPointer(innerFirst + i * 8); if (cp == 0) { sb.AppendLine($" [{i,2}] (null)"); continue; } var cv = _memory.ReadPointer(cp); string? cr = null; if (cv != 0 && IsModuleAddress(cv)) cr = ResolveRttiName(cv); var ht = _memory.Read(cp + _offsets.LifeHealthOffset + _offsets.VitalTotalOffset); var hc = _memory.Read(cp + _offsets.LifeHealthOffset + _offsets.VitalCurrentOffset); var mt = _memory.Read(cp + _offsets.LifeManaOffset + _offsets.VitalTotalOffset); var mc = _memory.Read(cp + _offsets.LifeManaOffset + _offsets.VitalCurrentOffset); var hpOk2 = ht > 0 && ht < 200000 && hc >= 0 && hc <= ht + 1000; var mOk2 = mt >= 0 && mt < 200000 && mc >= 0 && mc <= mt + 1000; var lt = (hpOk2 && mOk2) ? $" ◄ LIFE HP={hc}/{ht} Mana={mc}/{mt}" : ""; sb.AppendLine($" [{i,2}] 0x{cp:X} {cr ?? "?"}{lt}"); } } } } sb.AppendLine(); // Scan entity memory for all StdVector-like patterns with >1 pointer-sized elements sb.AppendLine("── StdVector scan (entity 0x000-0x300) ──"); var scanData = _memory.ReadBytes(localPlayer, 0x300); if (scanData is not null) { for (var off = 0; off + 24 <= scanData.Length; off += 8) { var f = (nint)BitConverter.ToInt64(scanData, off); var l = (nint)BitConverter.ToInt64(scanData, off + 8); if (f == 0 || l <= f) continue; var sz = l - f; if (sz < 16 || sz > 0x2000 || sz % 8 != 0) continue; var n = (int)(sz / 8); // Quick validate: first element should be non-zero var firstEl = _memory.ReadPointer(f); if (firstEl == 0) continue; var tag = ""; var elHigh = (ulong)firstEl >> 32; if (elHigh > 0 && elHigh < 0x7FFF && (firstEl & 0x3) == 0) tag = " [ptr elements]"; else if (firstEl < 0x100000) tag = " [small int elements]"; sb.AppendLine($"entity+0x{off:X3}: StdVector count={n}{tag} First=0x{f:X}"); } } sb.AppendLine(); // Extended entity dump (+0x100 to +0x200) for finding component-related offsets sb.AppendLine("── Entity extended dump (+0x100 to +0x200) ──"); var extData = _memory.ReadBytes(localPlayer + 0x100, 0x100); if (extData is not null) { for (var row = 0; row < extData.Length; row += 16) { var absOff = row + 0x100; sb.Append($"+0x{absOff:X3}: "); for (var col = 0; col < 16 && row + col < extData.Length; col++) { sb.Append($"{extData[row + col]:X2} "); if (col == 7) sb.Append(' '); } sb.Append(" | "); for (var slot = 0; slot < 16 && row + slot + 8 <= extData.Length; slot += 8) { var val = (nint)BitConverter.ToInt64(extData, row + slot); if (val == 0) continue; if (IsModuleAddress(val)) { var name = ResolveRttiName(val); sb.Append(name is not null ? $"[{name}] " : "[module] "); } else { var h = (ulong)val >> 32; if (h > 0 && h < 0x7FFF && (val & 0x3) == 0) sb.Append("[heap] "); } } sb.AppendLine(); } } // ECS cache state sb.AppendLine(); sb.AppendLine($"── ECS Cache ──"); sb.AppendLine($"Cached Life index: {_cachedLifeIndex}"); sb.AppendLine($"Last LocalPlayer: 0x{_lastLocalPlayer:X}"); // FindComponentList result var (bestFirst, bestCount) = FindComponentList(localPlayer); sb.AppendLine($"FindComponentList result: First=0x{bestFirst:X}, count={bestCount}"); return sb.ToString(); } /// /// Deep scan: follows pointers from AreaInstance and LocalPlayer, scanning each target for vital values. /// Uses two-level search to find the correct pointer chain to vitals. /// public string DeepScanVitals(int hpValue, int manaValue, int esValue) { if (_memory is null) return "Error: not attached"; if (_gameStateBase == 0) return "Error: GameState base not resolved"; var snap = new GameStateSnapshot(); var inGameState = ResolveInGameState(snap); if (inGameState == 0) return "Error: InGameState not resolved"; var ingameData = _memory.ReadPointer(inGameState + _offsets.IngameDataFromStateOffset); if (ingameData == 0) return "Error: AreaInstance not resolved"; var sb = new StringBuilder(); sb.AppendLine($"Deep vital scan — HP={hpValue}, Mana={manaValue}, ES={esValue}"); sb.AppendLine($"AreaInstance: 0x{ingameData:X}"); var searchValues = new List<(string name, int value)>(); if (hpValue > 0) searchValues.Add(("HP", hpValue)); if (manaValue > 0) searchValues.Add(("Mana", manaValue)); if (esValue > 0) searchValues.Add(("ES", esValue)); if (searchValues.Count == 0) return "Error: enter at least one vital value"; // Level 1: scan AreaInstance pointers (first 0x1200 bytes covers all known offsets) var areaData = _memory.ReadBytes(ingameData, 0x1200); if (areaData is null) return "Error: failed to read AreaInstance"; var visited = new HashSet(); var results = new List(); for (var off1 = 0; off1 + 8 <= areaData.Length; off1 += 8) { var ptr1 = (nint)BitConverter.ToInt64(areaData, off1); if (ptr1 == 0 || ptr1 == ingameData) continue; var h = (ulong)ptr1 >> 32; if (h == 0 || h >= 0x7FFF) continue; if ((ptr1 & 0x3) != 0) continue; if (IsModuleAddress(ptr1)) continue; if (!visited.Add(ptr1)) continue; // Scan this target for vital values (first 0xC00 bytes) var targetData = _memory.ReadBytes(ptr1, 0xC00); if (targetData is null) continue; var hits = FindVitalHits(targetData, searchValues); if (hits.Count > 0) { var vtable = _memory.ReadPointer(ptr1); var rtti = vtable != 0 && IsModuleAddress(vtable) ? ResolveRttiName(vtable) : null; results.Add($"Area+0x{off1:X} → 0x{ptr1:X} [{rtti ?? "?"}]\n {string.Join(", ", hits)}"); } // Level 2: follow pointers FROM this target (first 0x200 bytes) for (var off2 = 0; off2 + 8 <= Math.Min(targetData.Length, 0x200); off2 += 8) { var ptr2 = (nint)BitConverter.ToInt64(targetData, off2); if (ptr2 == 0 || ptr2 == ptr1 || ptr2 == ingameData) continue; var h2 = (ulong)ptr2 >> 32; if (h2 == 0 || h2 >= 0x7FFF) continue; if ((ptr2 & 0x3) != 0) continue; if (IsModuleAddress(ptr2)) continue; if (!visited.Add(ptr2)) continue; var deepData = _memory.ReadBytes(ptr2, 0xC00); if (deepData is null) continue; var deepHits = FindVitalHits(deepData, searchValues); if (deepHits.Count > 0) { results.Add($"Area+0x{off1:X} → +0x{off2:X} → 0x{ptr2:X}\n {string.Join(", ", deepHits)}"); } } } sb.AppendLine($"\nFound {results.Count} objects with vital matches:"); sb.AppendLine(new string('─', 80)); foreach (var r in results) sb.AppendLine(r); return sb.ToString(); } private static List FindVitalHits(byte[] data, List<(string name, int value)> searchValues) { // Find hits and check for CLUSTERS (multiple vitals within 0x40 bytes of each other) var allHits = new List<(string name, int value, int offset)>(); foreach (var (vName, vValue) in searchValues) { for (var off = 0; off + 4 <= data.Length; off += 4) { if (BitConverter.ToInt32(data, off) == vValue) allHits.Add((vName, vValue, off)); } } // Only return if we have at least 2 different vitals, or a single vital at a reasonable offset var distinctVitals = allHits.Select(h => h.name).Distinct().Count(); if (distinctVitals >= 2) { // Find clusters where 2+ vitals are within 0x80 bytes var clusters = new List(); foreach (var h1 in allHits) { foreach (var h2 in allHits) { if (h1.name == h2.name) continue; if (Math.Abs(h1.offset - h2.offset) <= 0x80) { clusters.Add($"{h1.name}@+0x{h1.offset:X}, {h2.name}@+0x{h2.offset:X}"); } } } if (clusters.Count > 0) return clusters.Distinct().Take(10).ToList(); } return []; } /// /// Probes InGameState to find sub-structure offsets by looking for recognizable data patterns. /// Scans the object for heap pointers and checks each for area level, entity counts, terrain, etc. /// public string ProbeInGameState() { if (_memory is null) return "Error: not attached"; if (_gameStateBase == 0) return "Error: GameState base not resolved"; // Resolve InGameState var snap = new GameStateSnapshot(); var inGameState = ResolveInGameState(snap); if (inGameState == 0) return $"Error: InGameState not resolved (states: {snap.StatesCount})"; var sb = new StringBuilder(); sb.AppendLine($"InGameState: 0x{inGameState:X} (State[{_offsets.InGameStateIndex}] of {snap.StatesCount})"); // Read InGameState vtable for identification var igsVtable = _memory.ReadPointer(inGameState); sb.AppendLine($"Vtable: 0x{igsVtable:X}"); sb.AppendLine(new string('═', 80)); // Scan InGameState for all heap pointers (potential sub-object data pointers) var igsSize = 0x8000; var igsData = _memory.ReadBytes(inGameState, igsSize); if (igsData is null) return "Error: failed to read InGameState"; // Collect unique data pointers to probe var candidates = new List<(int offset, nint ptr)>(); for (var off = 0; off + 8 <= igsData.Length; off += 8) { var val = (nint)BitConverter.ToInt64(igsData, off); if (val == 0) continue; // Only probe heap pointers (not module addresses, not self-references) if (IsModuleAddress(val)) continue; if (val == inGameState) continue; var high = (ulong)val >> 32; if (high == 0 || high >= 0x7FFF) continue; if ((val & 0x3) != 0) continue; // Skip self-referencing pointers (InGameState + small offset) if (val >= inGameState && val < inGameState + igsSize) continue; candidates.Add((off, val)); } sb.AppendLine($"Found {candidates.Count} heap pointer candidates"); sb.AppendLine(); // Probe patterns for AreaInstance identification // Dump offsets + POE1 reference + nearby values for version drift var probeOffsets = new (string name, int offset, string type)[] { // Area level — byte at dump 0xAC, strong signal (value 1-100) ("AreaLevel byte (0xAC)", 0xAC, "byte_level"), // Dump ("AreaLevel byte (0xBC)", 0xBC, "byte_level"), // Near dump (version drift) ("AreaLevel int (0xAC)", 0xAC, "int_level"), // Dump as int ("AreaLevel int (0xBC)", 0xBC, "int_level"), // Near dump as int ("AreaLevel int (0xD4)", 0xD4, "int_level"), // POE1 // Area hash (dump: 0xEC) ("AreaHash (0xEC)", 0xEC, "nonzero32"), // Dump // Server data pointer (dump: 0x9F0 via LocalPlayerStruct) ("ServerData (0x9F0)", 0x9F0, "ptr"), // Dump ("ServerData (0xA00)", 0xA00, "ptr"), // Near dump // Local player pointer (dump: 0xA10) ("LocalPlayer (0xA10)", 0xA10, "ptr"), // Dump ("LocalPlayer (0xA20)", 0xA20, "ptr"), // Near dump // Entity list — StdMap at dump 0xAF8. StdMap.head at +0, .size at +8 ("Entities.head (0xAF8)", 0xAF8, "ptr"), // Dump ("Entities.size (0xB00)", 0xB00, "int_count"), // Dump StdMap._Mysize ("Entities.head (0xB58)", 0xB58, "ptr"), // Near dump ("Entities.size (0xB60)", 0xB60, "int_count"), // Near dump // Terrain inline (dump: 0xCC0) ("Terrain (0xCC0)", 0xCC0, "nonzero64"), // Dump (inline, check for data) }; // Track best matches var matches = new List<(int igsOffset, nint ptr, string desc, int score)>(); foreach (var (off, ptr) in candidates) { var score = 0; var details = new List(); // Also try RTTI on the candidate object itself var candidateVtable = _memory.ReadPointer(ptr); string? candidateRtti = null; if (candidateVtable != 0 && IsModuleAddress(candidateVtable)) candidateRtti = ResolveRttiName(candidateVtable); // Try AreaInstance-like probes foreach (var (name, probeOff, probeType) in probeOffsets) { if (probeType == "ptr") { var val = _memory.ReadPointer(ptr + probeOff); if (val != 0 && !IsModuleAddress(val)) { var h = (ulong)val >> 32; if (h > 0 && h < 0x7FFF) { score++; details.Add($" {name} = 0x{val:X} ✓"); } } } else if (probeType == "byte_level") { var val = _memory.Read(ptr + probeOff); if (val > 0 && val <= 100) { score += 3; // Strong signal details.Add($" {name} = {val} ✓✓✓"); } } else if (probeType == "int_level") { var val = _memory.Read(ptr + probeOff); if (val > 0 && val <= 100) { score += 3; details.Add($" {name} = {val} ✓✓✓"); } } else if (probeType == "int_count") { var val = _memory.Read(ptr + probeOff); if (val > 0 && val < 10000) { score += 2; details.Add($" {name} = {val} ✓✓"); } } else if (probeType == "nonzero32") { var val = _memory.Read(ptr + probeOff); if (val != 0) { score++; details.Add($" {name} = 0x{val:X8} ✓"); } } else if (probeType == "nonzero64") { var val = _memory.Read(ptr + probeOff); if (val != 0) { score++; details.Add($" {name} = 0x{val:X} ✓"); } } } // RTTI bonus: if object has a known class name, boost score significantly if (candidateRtti is not null) { details.Insert(0, $" RTTI: {candidateRtti}"); if (candidateRtti.Contains("AreaInstance") || candidateRtti.Contains("IngameData") || candidateRtti.Contains("WorldInstance")) score += 10; // Very strong signal } if (score >= 3) { matches.Add((off, ptr, string.Join("\n", details), score)); } } // Sort by score descending matches.Sort((a, b) => b.score.CompareTo(a.score)); if (matches.Count == 0) { sb.AppendLine("No matches found with known offset patterns."); sb.AppendLine("Try scanning InGameState with the raw Scan tool."); } else { sb.AppendLine($"── Top matches (by score) ──"); sb.AppendLine(); foreach (var (off, ptr, desc, score) in matches.Take(15)) { sb.AppendLine($"IGS+0x{off:X3} → 0x{ptr:X} (score: {score})"); sb.AppendLine(desc); sb.AppendLine(); } } // Check InGameState at dump-predicted offsets directly sb.AppendLine("── Dump-predicted InGameState fields ──"); var dumpFields = new (string name, int offset)[] { ("AreaInstanceData", 0x290), ("WorldData", 0x2F8), ("UiRootPtr", 0x648), ("IngameUi", 0xC40), }; foreach (var (fname, foff) in dumpFields) { if (foff + 8 <= igsSize) { var val = (nint)BitConverter.ToInt64(igsData, foff); if (val != 0) { var tag = ClassifyPointer(val); string extra = ""; // If it's a heap pointer, try RTTI if (tag == "heap ptr") { var vt = _memory.ReadPointer(val); if (vt != 0 && IsModuleAddress(vt)) { var rtti = ResolveRttiName(vt); if (rtti != null) extra = $" RTTI={rtti}"; } // Quick check: does it look like an AreaInstance? var lvl = _memory.Read(val + 0xAC); var lvl2 = _memory.Read(val + 0xBC); if (lvl > 0 && lvl <= 100) extra += $" lvl@0xAC={lvl}"; if (lvl2 > 0 && lvl2 <= 100) extra += $" lvl@0xBC={lvl2}"; } sb.AppendLine($" IGS+0x{foff:X}: {fname} = 0x{val:X} [{tag ?? "?"}]{extra}"); } else { sb.AppendLine($" IGS+0x{foff:X}: {fname} = (null)"); } } } sb.AppendLine(); // Also scan InGameState directly for position-like floats sb.AppendLine("── Position-like floats in InGameState ──"); for (var off = 0; off + 12 <= igsData.Length; off += 4) { var x = BitConverter.ToSingle(igsData, off); var y = BitConverter.ToSingle(igsData, off + 4); var z = BitConverter.ToSingle(igsData, off + 8); // Look for reasonable world coordinates (POE2: typically 100-10000 range) if (x > 100 && x < 50000 && y > 100 && y < 50000 && !float.IsNaN(x) && !float.IsNaN(y) && !float.IsNaN(z) && MathF.Abs(z) < 5000) { sb.AppendLine($" IGS+0x{off:X4}: ({x:F1}, {y:F1}, {z:F1})"); } } return sb.ToString(); } private static bool IsReasonableFloat(float f) { if (float.IsNaN(f) || float.IsInfinity(f)) return false; var abs = MathF.Abs(f); return abs > 0.001f && abs < 100000f; } private static string FormatBytes(byte[]? data) { if (data is null) return "Error: read failed"; return BitConverter.ToString(data).Replace('-', ' '); } private string ReadNullTermString(nint addr) { var data = _memory!.ReadBytes(addr, 256); if (data is null) return "Error: read failed"; var end = Array.IndexOf(data, (byte)0); if (end < 0) end = data.Length; return Encoding.UTF8.GetString(data, 0, end); } public void Dispose() { if (_disposed) return; _disposed = true; Detach(); } }