using System.Collections.ObjectModel; using System.Runtime.InteropServices; using Avalonia; using Avalonia.Media.Imaging; using Avalonia.Platform; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Roboto.Memory; namespace Automata.Ui.ViewModels; public partial class MemoryNodeViewModel : ObservableObject { [ObservableProperty] private string _name; [ObservableProperty] private string _value = ""; [ObservableProperty] private string _valueColor = "#484f58"; [ObservableProperty] private bool _isExpanded = true; public ObservableCollection Children { get; } = []; public MemoryNodeViewModel(string name) { _name = name; } public void Set(string value, bool valid = true) { Value = value; ValueColor = valid ? "#3fb950" : "#484f58"; } } public partial class MemoryViewModel : ObservableObject { private GameMemoryReader? _reader; private CancellationTokenSource? _cts; private const int ViewportRadius = 300; // grid pixels visible in each direction from player [ObservableProperty] private bool _isEnabled; [ObservableProperty] private string _statusText = "Not attached"; public ObservableCollection RootNodes { get; } = []; // Minimap [ObservableProperty] private Bitmap? _terrainImage; private byte[]? _terrainBasePixels; private byte[]? _minimapBuffer; // reused each frame private int _terrainImageWidth, _terrainImageHeight; private uint _terrainImageAreaHash; private WalkabilityGrid? _terrainGridRef; // Raw explorer [ObservableProperty] private string _rawAddress = ""; [ObservableProperty] private string _rawOffsets = ""; [ObservableProperty] private string _rawType = "pointer"; [ObservableProperty] private string _rawResult = ""; // Scan [ObservableProperty] private string _scanAddress = ""; [ObservableProperty] private string _scanOffsets = ""; [ObservableProperty] private string _scanSize = "400"; [ObservableProperty] private string _scanResult = ""; // Component scan vitals [ObservableProperty] private string _vitalHp = ""; [ObservableProperty] private string _vitalMana = ""; [ObservableProperty] private string _vitalEs = ""; public static string[] RawTypes { get; } = ["int32", "int64", "float", "double", "pointer", "bytes16", "string"]; // Tree node references private MemoryNodeViewModel? _processStatus; private MemoryNodeViewModel? _processPid; private MemoryNodeViewModel? _processModule; private MemoryNodeViewModel? _gsPattern; private MemoryNodeViewModel? _gsBase; private MemoryNodeViewModel? _gsController; private MemoryNodeViewModel? _gsStates; private MemoryNodeViewModel? _inGameState; private MemoryNodeViewModel? _areaInstance; private MemoryNodeViewModel? _areaLevel; private MemoryNodeViewModel? _areaHash; private MemoryNodeViewModel? _serverData; private MemoryNodeViewModel? _localPlayer; private MemoryNodeViewModel? _entityCount; private MemoryNodeViewModel? _playerPos; private MemoryNodeViewModel? _playerLife; private MemoryNodeViewModel? _playerMana; private MemoryNodeViewModel? _playerEs; private MemoryNodeViewModel? _isLoadingNode; private MemoryNodeViewModel? _escapeStateNode; private MemoryNodeViewModel? _statesNode; private MemoryNodeViewModel? _terrainCells; private MemoryNodeViewModel? _terrainGrid; private MemoryNodeViewModel? _terrainWalkable; private MemoryNodeViewModel? _entitySummary; private MemoryNodeViewModel? _entityTypesNode; private MemoryNodeViewModel? _entityListNode; partial void OnIsEnabledChanged(bool value) { if (value) Enable(); else Disable(); } private void Enable() { _reader = new GameMemoryReader(); var attached = _reader.Attach(); if (attached) { var snap = _reader.ReadSnapshot(); StatusText = $"Attached (PID {snap.ProcessId})"; } else { StatusText = "Process not found"; } BuildTree(); _cts = new CancellationTokenSource(); _ = ReadLoop(_cts.Token); } private void Disable() { _cts?.Cancel(); _cts = null; _reader?.Dispose(); _reader = null; RootNodes.Clear(); StatusText = "Not attached"; _terrainBasePixels = null; _terrainImageAreaHash = 0; _terrainGridRef = null; var old = TerrainImage; TerrainImage = null; old?.Dispose(); } private void BuildTree() { RootNodes.Clear(); // Process var process = new MemoryNodeViewModel("Process"); _processStatus = new MemoryNodeViewModel("Status:"); _processPid = new MemoryNodeViewModel("PID:"); _processModule = new MemoryNodeViewModel("Module:"); process.Children.Add(_processStatus); process.Children.Add(_processPid); process.Children.Add(_processModule); // GameState var gameState = new MemoryNodeViewModel("GameState"); _gsPattern = new MemoryNodeViewModel("Pattern:"); _gsBase = new MemoryNodeViewModel("Base:"); _gsController = new MemoryNodeViewModel("Controller:"); _gsStates = new MemoryNodeViewModel("States:"); _inGameState = new MemoryNodeViewModel("InGameState:"); _isLoadingNode = new MemoryNodeViewModel("Loading:"); _escapeStateNode = new MemoryNodeViewModel("Escape:"); _statesNode = new MemoryNodeViewModel("State Slots") { IsExpanded = true }; gameState.Children.Add(_gsPattern); gameState.Children.Add(_gsBase); gameState.Children.Add(_gsController); gameState.Children.Add(_gsStates); gameState.Children.Add(_inGameState); gameState.Children.Add(_isLoadingNode); gameState.Children.Add(_escapeStateNode); gameState.Children.Add(_statesNode); // InGameState children var inGameStateGroup = new MemoryNodeViewModel("InGameState"); // AreaInstance (primary data source in POE2) var areaInstanceGroup = new MemoryNodeViewModel("AreaInstance"); _areaInstance = new MemoryNodeViewModel("Ptr:"); _areaLevel = new MemoryNodeViewModel("AreaLevel:"); _areaHash = new MemoryNodeViewModel("AreaHash:"); _serverData = new MemoryNodeViewModel("ServerData:"); _localPlayer = new MemoryNodeViewModel("LocalPlayer:"); _entityCount = new MemoryNodeViewModel("Entities:"); areaInstanceGroup.Children.Add(_areaInstance); areaInstanceGroup.Children.Add(_areaLevel); areaInstanceGroup.Children.Add(_areaHash); areaInstanceGroup.Children.Add(_serverData); areaInstanceGroup.Children.Add(_localPlayer); areaInstanceGroup.Children.Add(_entityCount); // Player var player = new MemoryNodeViewModel("Player"); _playerPos = new MemoryNodeViewModel("Position:") { Value = "?", ValueColor = "#484f58" }; _playerLife = new MemoryNodeViewModel("Life:") { Value = "?", ValueColor = "#484f58" }; _playerMana = new MemoryNodeViewModel("Mana:") { Value = "?", ValueColor = "#484f58" }; _playerEs = new MemoryNodeViewModel("ES:") { Value = "?", ValueColor = "#484f58" }; player.Children.Add(_playerPos); player.Children.Add(_playerLife); player.Children.Add(_playerMana); player.Children.Add(_playerEs); // Entities var entitiesGroup = new MemoryNodeViewModel("Entities"); _entitySummary = new MemoryNodeViewModel("Summary:"); _entityTypesNode = new MemoryNodeViewModel("Types:") { IsExpanded = false }; _entityListNode = new MemoryNodeViewModel("List:") { IsExpanded = false }; entitiesGroup.Children.Add(_entitySummary); entitiesGroup.Children.Add(_entityTypesNode); entitiesGroup.Children.Add(_entityListNode); // Terrain var terrain = new MemoryNodeViewModel("Terrain"); _terrainCells = new MemoryNodeViewModel("Cells:"); _terrainGrid = new MemoryNodeViewModel("Grid:"); _terrainWalkable = new MemoryNodeViewModel("Walkable:"); terrain.Children.Add(_terrainCells); terrain.Children.Add(_terrainGrid); terrain.Children.Add(_terrainWalkable); inGameStateGroup.Children.Add(areaInstanceGroup); inGameStateGroup.Children.Add(player); inGameStateGroup.Children.Add(entitiesGroup); inGameStateGroup.Children.Add(terrain); RootNodes.Add(process); RootNodes.Add(gameState); RootNodes.Add(inGameStateGroup); } private async Task ReadLoop(CancellationToken ct) { using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(30)); while (!ct.IsCancellationRequested) { try { if (!await timer.WaitForNextTickAsync(ct)) break; } catch (OperationCanceledException) { break; } if (_reader is null) break; var snap = _reader.ReadSnapshot(); Dispatcher.UIThread.Post(() => UpdateTree(snap)); } } private void UpdateTree(GameStateSnapshot snap) { if (_processStatus is null) return; // Process _processStatus.Set(snap.Attached ? "Attached" : "Not found", snap.Attached); _processPid!.Set(snap.Attached ? snap.ProcessId.ToString() : "—", snap.Attached); _processModule!.Set( snap.Attached ? $"0x{snap.ModuleBase:X} ({snap.ModuleSize:N0} bytes)" : "—", snap.Attached); // GameState _gsPattern!.Set(snap.OffsetsConfigured ? "configured" : "not configured", snap.OffsetsConfigured); _gsBase!.Set( snap.GameStateBase != 0 ? $"0x{snap.GameStateBase:X}" : "not resolved", snap.GameStateBase != 0); _gsController!.Set( snap.ControllerPtr != 0 ? $"0x{snap.ControllerPtr:X}" : "—", snap.ControllerPtr != 0); _gsStates!.Set( snap.StatesCount > 0 ? snap.StatesCount.ToString() : "—", snap.StatesCount > 0); _inGameState!.Set( snap.InGameStatePtr != 0 ? $"0x{snap.InGameStatePtr:X}" : "not found", snap.InGameStatePtr != 0); _isLoadingNode!.Set(snap.IsLoading ? "Loading..." : "Ready", !snap.IsLoading); _escapeStateNode!.Set(snap.IsEscapeOpen ? "Open" : "Closed", !snap.IsEscapeOpen); // State Slots — show pointer + int32 at +0x08 for each state slot if (_statesNode is not null && snap.StateSlots.Length > 0) { var slots = snap.StateSlots; var needed = slots.Length; while (_statesNode.Children.Count > needed) _statesNode.Children.RemoveAt(_statesNode.Children.Count - 1); for (var i = 0; i < needed; i++) { var ptr = slots[i]; var stateName = i < GameMemoryReader.StateNames.Length ? GameMemoryReader.StateNames[i] : $"State{i}"; var label = $"[{i}] {stateName}:"; string val; string color; if (ptr == 0) { val = "null"; color = "#484f58"; } else { // Read int32 at state+0x08 (the value CE found) var int32Val = snap.StateSlotValues?.Length > i ? snap.StateSlotValues[i] : 0; val = $"0x{ptr:X} [+0x08]={int32Val}"; color = ptr == snap.InGameStatePtr ? "#3fb950" : "#8b949e"; } if (i < _statesNode.Children.Count) { _statesNode.Children[i].Name = label; _statesNode.Children[i].Set(val, true); _statesNode.Children[i].ValueColor = color; } else { var node = new MemoryNodeViewModel(label); node.Set(val, true); node.ValueColor = color; _statesNode.Children.Add(node); } } } // Status text if (snap.Attached) StatusText = snap.InGameStatePtr != 0 ? $"Attached (PID {snap.ProcessId}) — InGame" : $"Attached (PID {snap.ProcessId})"; else if (snap.Error is not null) StatusText = $"Error: {snap.Error}"; // AreaInstance _areaInstance!.Set( snap.AreaInstancePtr != 0 ? $"0x{snap.AreaInstancePtr:X}" : "—", snap.AreaInstancePtr != 0); _areaLevel!.Set( snap.AreaLevel > 0 ? snap.AreaLevel.ToString() : "—", snap.AreaLevel > 0); _areaHash!.Set( snap.AreaHash != 0 ? $"0x{snap.AreaHash:X8}" : "—", snap.AreaHash != 0); _serverData!.Set( snap.ServerDataPtr != 0 ? $"0x{snap.ServerDataPtr:X}" : "—", snap.ServerDataPtr != 0); _localPlayer!.Set( snap.LocalPlayerPtr != 0 ? $"0x{snap.LocalPlayerPtr:X}" : "—", snap.LocalPlayerPtr != 0); _entityCount!.Set( snap.EntityCount > 0 ? snap.EntityCount.ToString() : "—", snap.EntityCount > 0); // Player position if (snap.HasPosition) _playerPos!.Set($"({snap.PlayerX:F1}, {snap.PlayerY:F1}, {snap.PlayerZ:F1})"); else _playerPos!.Set("? (no Render component found)", false); // Player vitals if (snap.HasVitals) { _playerLife!.Set($"{snap.LifeCurrent} / {snap.LifeTotal}"); _playerMana!.Set($"{snap.ManaCurrent} / {snap.ManaTotal}"); _playerEs!.Set($"{snap.EsCurrent} / {snap.EsTotal}"); } else { _playerLife!.Set("? (set LifeComponentIndex)", false); _playerMana!.Set("? (set LifeComponentIndex)", false); _playerEs!.Set("? (set LifeComponentIndex)", false); } // Entities if (snap.Entities is { Count: > 0 }) { var withPos = snap.Entities.Count(e => e.HasPosition); var withComps = snap.Entities.Count(e => e.Components is not null); var monsters = snap.Entities.Count(e => e.Type == Roboto.Memory.EntityType.Monster); var knownComps = _reader?.Registry["components"].Count ?? 0; _entitySummary!.Set($"{snap.Entities.Count} total, {withComps} with comps, {knownComps} known, {monsters} monsters"); // Group by EntityType var typeCounts = snap.Entities .GroupBy(e => e.Type) .OrderByDescending(g => g.Count()) .Take(20); _entityTypesNode!.Children.Clear(); foreach (var group in typeCounts) { var node = new MemoryNodeViewModel($"{group.Key}:"); node.Set(group.Count().ToString()); _entityTypesNode.Children.Add(node); } // Entity list grouped by type UpdateEntityList(snap.Entities); } else { _entitySummary!.Set("—", false); _entityTypesNode!.Children.Clear(); _entityListNode!.Children.Clear(); } // Terrain if (snap.TerrainCols > 0 && snap.TerrainRows > 0) { _terrainCells!.Set($"{snap.TerrainCols}x{snap.TerrainRows}"); _terrainGrid!.Set($"{snap.TerrainWidth}x{snap.TerrainHeight}"); if (snap.Terrain != null) _terrainWalkable!.Set($"{snap.TerrainWalkablePercent}%"); else _terrainWalkable!.Set("no grid data", false); } else { _terrainCells!.Set("?", false); _terrainGrid!.Set("?", false); _terrainWalkable!.Set("?", false); } UpdateMinimap(snap); } private void UpdateMinimap(GameStateSnapshot snap) { // Skip rendering entirely during loading — terrain data is stale/invalid if (snap.IsLoading) { _terrainBasePixels = null; _terrainImageAreaHash = 0; _terrainGridRef = null; var oldImg = TerrainImage; TerrainImage = null; oldImg?.Dispose(); return; } // Invalidate cache when area changes or terrain grid object changes var terrainChanged = snap.Terrain is not null && !ReferenceEquals(snap.Terrain, _terrainGridRef); if (terrainChanged || (snap.AreaHash != 0 && snap.AreaHash != _terrainImageAreaHash)) { _terrainBasePixels = null; _terrainImageAreaHash = 0; _terrainGridRef = null; var old = TerrainImage; TerrainImage = null; old?.Dispose(); } // Rebuild base pixels from new terrain data if (snap.Terrain is { } grid && _terrainBasePixels is null) { var w = grid.Width; var h = grid.Height; var pixels = new byte[w * h * 4]; for (var y = 0; y < h; y++) { var srcY = h - 1 - y; // flip Y: game Y-up → bitmap Y-down for (var x = 0; x < w; x++) { var i = (y * w + x) * 4; if (grid.Data[srcY * w + x] == 0) // walkable — transparent { pixels[i] = 0x00; pixels[i + 1] = 0x00; pixels[i + 2] = 0x00; pixels[i + 3] = 0x00; } else // blocked { pixels[i] = 0x50; // B pixels[i + 1] = 0x50; // G pixels[i + 2] = 0x50; // R pixels[i + 3] = 0xFF; // A } } } _terrainBasePixels = pixels; _terrainImageWidth = w; _terrainImageHeight = h; _terrainImageAreaHash = snap.AreaHash; _terrainGridRef = grid; } var basePixels = _terrainBasePixels; if (basePixels is null) return; var tw = _terrainImageWidth; var th = _terrainImageHeight; if (tw < 2 || th < 2) return; // World-to-grid conversion: Render component gives world coords, // terrain bitmap is in grid/subtile coords (NumCols*23 x NumRows*23). // Each tile = 250 world units = 23 subtiles, so grid = world * 23/250. const float worldToGrid = 23.0f / 250.0f; const float cos45 = 0.70710678f; const float sin45 = 0.70710678f; var viewSize = ViewportRadius * 2; // Player grid position — center of the output float pgx, pgy; if (snap.HasPosition) { pgx = snap.PlayerX * worldToGrid; pgy = th - 1 - snap.PlayerY * worldToGrid; } else { pgx = tw * 0.5f; pgy = th * 0.5f; } var bufSize = viewSize * viewSize * 4; if (_minimapBuffer is null || _minimapBuffer.Length != bufSize) _minimapBuffer = new byte[bufSize]; var buf = _minimapBuffer; Array.Clear(buf, 0, bufSize); var outStride = viewSize * 4; var cx = viewSize * 0.5f; var cy = viewSize * 0.5f; // Sample terrain with -45° rotation baked in (nearest-neighbor, unsafe). unsafe { fixed (byte* srcPtr = basePixels, dstPtr = buf) { var srcInt = (int*)srcPtr; var dstInt = (int*)dstPtr; for (var ry = 0; ry < viewSize; ry++) { var dy = ry - cy; var baseX = -dy * sin45 + pgx; var baseY = dy * cos45 + pgy; for (var rx = 0; rx < viewSize; rx++) { var dx = rx - cx; var sx = (int)(dx * cos45 + baseX); var sy = (int)(dx * sin45 + baseY); if ((uint)sx >= (uint)tw || (uint)sy >= (uint)th) continue; dstInt[ry * viewSize + rx] = srcInt[sy * tw + sx]; } } } } // Draw entity dots — transform grid coords into rotated output space if (snap.Entities is { Count: > 0 }) { foreach (var e in snap.Entities) { if (!e.HasPosition) continue; if (e.Type is not (EntityType.Player or EntityType.Monster or EntityType.Npc)) continue; if (e.Address == snap.LocalPlayerPtr) continue; if (e.Type == EntityType.Monster && e.IsDead) continue; // Entity position relative to player in grid coords var dx = e.X * worldToGrid - pgx; var dy = (th - 1 - e.Y * worldToGrid) - pgy; // Apply -45° rotation into output space var ex = (int)(dx * cos45 + dy * sin45 + cx); var ey = (int)(-dx * sin45 + dy * cos45 + cy); if (ex < 0 || ex >= viewSize || ey < 0 || ey >= viewSize) continue; byte b, g, r; switch (e.Type) { case EntityType.Player: // other players — green #3FB950 b = 0x50; g = 0xB9; r = 0x3F; break; case EntityType.Npc: // orange #FF8C00 b = 0x00; g = 0x8C; r = 0xFF; break; case EntityType.Monster: // red #FF4444 b = 0x44; g = 0x44; r = 0xFF; break; default: continue; } DrawDot(buf, outStride, viewSize, viewSize, ex, ey, 4, b, g, r); } } // Draw player dot at center (white, on top) if (snap.HasPosition) DrawDot(buf, outStride, viewSize, viewSize, (int)cx, (int)cy, 5, 0xFF, 0xFF, 0xFF); // Create WriteableBitmap var bmp = new WriteableBitmap( new PixelSize(viewSize, viewSize), new Vector(96, 96), Avalonia.Platform.PixelFormat.Bgra8888, Avalonia.Platform.AlphaFormat.Premul); using (var fb = bmp.Lock()) { Marshal.Copy(buf, 0, fb.Address, buf.Length); } var old2 = TerrainImage; TerrainImage = bmp; old2?.Dispose(); } private static void DrawDot(byte[] buf, int stride, int bw, int bh, int cx, int cy, int radius, byte b, byte g, byte r) { var outer = radius + 1; for (var dy = -outer; dy <= outer; dy++) { for (var dx = -outer; dx <= outer; dx++) { var sx = cx + dx; var sy = cy + dy; if (sx < 0 || sx >= bw || sy < 0 || sy >= bh) continue; var dist = MathF.Sqrt(dx * dx + dy * dy); if (dist > radius + 0.5f) continue; var alpha = Math.Clamp(radius + 0.5f - dist, 0f, 1f); var i = sy * stride + sx * 4; buf[i] = (byte)(b * alpha + buf[i] * (1 - alpha)); buf[i + 1] = (byte)(g * alpha + buf[i + 1] * (1 - alpha)); buf[i + 2] = (byte)(r * alpha + buf[i + 2] * (1 - alpha)); buf[i + 3] = (byte)(Math.Max(alpha, buf[i + 3] / 255f) * 255); } } } private void UpdateEntityList(List entities) { if (_entityListNode is null) return; // Group by type, sorted by type name var groups = entities .GroupBy(e => e.Type) .OrderBy(g => g.Key.ToString()); // Build a lookup of existing type group nodes by name for reuse var existingGroups = new Dictionary(); foreach (var child in _entityListNode.Children) existingGroups[child.Name] = child; var usedGroups = new HashSet(); foreach (var group in groups) { var groupName = $"{group.Key} ({group.Count()})"; usedGroups.Add(groupName); if (!existingGroups.TryGetValue(groupName, out var groupNode)) { groupNode = new MemoryNodeViewModel(groupName) { IsExpanded = false }; _entityListNode.Children.Add(groupNode); existingGroups[groupName] = groupNode; } // Sort: players first, then by distance to 0,0 (or by id) var sorted = group.OrderBy(e => e.Id).ToList(); // Rebuild children — reuse by index to reduce churn while (groupNode.Children.Count > sorted.Count) groupNode.Children.RemoveAt(groupNode.Children.Count - 1); for (var i = 0; i < sorted.Count; i++) { var e = sorted[i]; var label = FormatEntityName(e); var value = FormatEntityValue(e); if (i < groupNode.Children.Count) { var existing = groupNode.Children[i]; existing.Name = label; existing.Set(value, e.HasPosition); UpdateEntityChildren(existing, e); } else { var node = new MemoryNodeViewModel(label) { IsExpanded = false }; node.Set(value, e.HasPosition); UpdateEntityChildren(node, e); groupNode.Children.Add(node); } } } // Remove stale type groups for (var i = _entityListNode.Children.Count - 1; i >= 0; i--) { if (!usedGroups.Contains(_entityListNode.Children[i].Name)) _entityListNode.Children.RemoveAt(i); } } private static string FormatEntityName(Entity e) { // Short name: last path segment without @instance if (e.Path is not null) { var lastSlash = e.Path.LastIndexOf('/'); var name = lastSlash >= 0 ? e.Path[(lastSlash + 1)..] : e.Path; var at = name.IndexOf('@'); if (at > 0) name = name[..at]; return $"[{e.Id}] {name}"; } return $"[{e.Id}] ?"; } private static string FormatEntityValue(Entity e) { var parts = new List(); if (e.HasVitals) parts.Add(e.IsAlive ? "Alive" : "Dead"); if (e.HasPosition) parts.Add($"({e.X:F0},{e.Y:F0})"); if (e.HasVitals) parts.Add($"HP:{e.LifeCurrent}/{e.LifeTotal}"); if (e.Components is { Count: > 0 }) parts.Add($"{e.Components.Count} comps"); return parts.Count > 0 ? string.Join(" ", parts) : "—"; } private static void UpdateEntityChildren(MemoryNodeViewModel node, Entity e) { // Build children: address, position, vitals, components var needed = new List<(string name, string value, bool valid)>(); needed.Add(("Addr:", $"0x{e.Address:X}", true)); if (e.Path is not null) needed.Add(("Path:", e.Path, true)); if (e.HasPosition) needed.Add(("Pos:", $"({e.X:F1}, {e.Y:F1}, {e.Z:F1})", true)); if (e.HasVitals) { needed.Add(("Life:", $"{e.LifeCurrent} / {e.LifeTotal}", e.LifeCurrent > 0)); if (e.ManaTotal > 0) needed.Add(("Mana:", $"{e.ManaCurrent} / {e.ManaTotal}", true)); if (e.EsTotal > 0) needed.Add(("ES:", $"{e.EsCurrent} / {e.EsTotal}", true)); } if (e.Components is { Count: > 0 }) { var compList = string.Join(", ", e.Components.OrderBy(c => c)); needed.Add(("Components:", compList, true)); } // Reuse existing children by index while (node.Children.Count > needed.Count) node.Children.RemoveAt(node.Children.Count - 1); for (var i = 0; i < needed.Count; i++) { var (name, value, valid) = needed[i]; if (i < node.Children.Count) { node.Children[i].Name = name; node.Children[i].Set(value, valid); } else { var child = new MemoryNodeViewModel(name); child.Set(value, valid); node.Children.Add(child); } } } [RelayCommand] private void ReadAddressExecute() { if (_reader is null || !_reader.IsAttached) { RawResult = "Error: not attached"; return; } RawResult = _reader.Diagnostics!.ReadAddress(RawAddress, RawOffsets, RawType); } [RelayCommand] private void ScanExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } if (!int.TryParse(ScanSize, System.Globalization.NumberStyles.HexNumber, null, out var size)) size = 0x400; ScanResult = _reader.Diagnostics!.ScanRegion(ScanAddress, ScanOffsets, size); } [RelayCommand] private void ScanStatesExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ScanAllStates(); } [RelayCommand] private void ProbeExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ProbeInGameState(); } [RelayCommand] private void ScanComponentsExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } int.TryParse(VitalHp, out var hp); int.TryParse(VitalMana, out var mana); int.TryParse(VitalEs, out var es); ScanResult = _reader.Diagnostics!.ScanComponents(hp, mana, es); } [RelayCommand] private void DeepScanExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } int.TryParse(VitalHp, out var hp); int.TryParse(VitalMana, out var mana); int.TryParse(VitalEs, out var es); ScanResult = _reader.Diagnostics!.DeepScanVitals(hp, mana, es); } [RelayCommand] private void DiagnoseVitalsExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.DiagnoseVitals(); } [RelayCommand] private void ScanPositionExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ScanPosition(); } [RelayCommand] private void DiagnoseEntityExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.DiagnoseEntity(); } [RelayCommand] private void ScanEntitiesExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ScanEntities(); } [RelayCommand] private void ScanCompLookupExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ScanComponentLookup(); } [RelayCommand] private void ScanAreaLoadingExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ScanAreaLoadingState(); } [RelayCommand] private void ScanDiffExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ScanMemoryDiff(); } [RelayCommand] private void ScanActiveVecExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ScanActiveStatesVector(); } [RelayCommand] private void ScanTerrainExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ScanTerrain(); } [RelayCommand] private void ScanStructureExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } if (!int.TryParse(ScanSize, System.Globalization.NumberStyles.HexNumber, null, out var size)) size = 0x2000; ScanResult = _reader.Diagnostics!.ScanStructure(ScanAddress, ScanOffsets, size); } [RelayCommand] private void ScanCameraExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.ScanCamera(); } [RelayCommand] private void CameraDiffExecute() { if (_reader is null || !_reader.IsAttached) { ScanResult = "Error: not attached"; return; } ScanResult = _reader.Diagnostics!.CameraDiff(); } }