lots working good, minimap / rotation / follow / entities
This commit is contained in:
parent
69a8eaea62
commit
1ba7c39c30
11 changed files with 2496 additions and 99 deletions
|
|
@ -1,4 +1,8 @@
|
|||
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;
|
||||
|
|
@ -32,11 +36,21 @@ 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<MemoryNodeViewModel> 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 = "";
|
||||
|
|
@ -75,10 +89,15 @@ public partial class MemoryViewModel : ObservableObject
|
|||
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)
|
||||
{
|
||||
|
|
@ -115,6 +134,13 @@ public partial class MemoryViewModel : ObservableObject
|
|||
_reader = null;
|
||||
RootNodes.Clear();
|
||||
StatusText = "Not attached";
|
||||
|
||||
_terrainBasePixels = null;
|
||||
_terrainImageAreaHash = 0;
|
||||
_terrainGridRef = null;
|
||||
var old = TerrainImage;
|
||||
TerrainImage = null;
|
||||
old?.Dispose();
|
||||
}
|
||||
|
||||
private void BuildTree()
|
||||
|
|
@ -137,11 +163,17 @@ public partial class MemoryViewModel : ObservableObject
|
|||
_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");
|
||||
|
|
@ -176,15 +208,19 @@ public partial class MemoryViewModel : ObservableObject
|
|||
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);
|
||||
|
|
@ -198,7 +234,7 @@ public partial class MemoryViewModel : ObservableObject
|
|||
|
||||
private async Task ReadLoop(CancellationToken ct)
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(500));
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(30));
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
|
|
@ -240,6 +276,54 @@ public partial class MemoryViewModel : ObservableObject
|
|||
_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)
|
||||
|
|
@ -292,18 +376,15 @@ public partial class MemoryViewModel : ObservableObject
|
|||
// Entities
|
||||
if (snap.Entities is { Count: > 0 })
|
||||
{
|
||||
var withPath = snap.Entities.Count(e => e.Path is not null);
|
||||
var withPos = snap.Entities.Count(e => e.HasPosition);
|
||||
_entitySummary!.Set($"{snap.Entities.Count} entities, {withPath} with path, {withPos} with pos");
|
||||
var withComps = snap.Entities.Count(e => e.Components is not null);
|
||||
var monsters = snap.Entities.Count(e => e.Type == Automata.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 path category: "Metadata/Monsters/..." → "Monsters"
|
||||
// Group by EntityType
|
||||
var typeCounts = snap.Entities
|
||||
.GroupBy(e =>
|
||||
{
|
||||
if (e.Path is null) return "?";
|
||||
var parts = e.Path.Split('/');
|
||||
return parts.Length >= 2 ? parts[1] : "?";
|
||||
})
|
||||
.GroupBy(e => e.Type)
|
||||
.OrderByDescending(g => g.Count())
|
||||
.Take(20);
|
||||
|
||||
|
|
@ -314,11 +395,15 @@ public partial class MemoryViewModel : ObservableObject
|
|||
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
|
||||
|
|
@ -326,11 +411,372 @@ public partial class MemoryViewModel : ObservableObject
|
|||
{
|
||||
_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<Entity> 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<string, MemoryNodeViewModel>();
|
||||
foreach (var child in _entityListNode.Children)
|
||||
existingGroups[child.Name] = child;
|
||||
|
||||
var usedGroups = new HashSet<string>();
|
||||
|
||||
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<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -465,6 +911,66 @@ public partial class MemoryViewModel : ObservableObject
|
|||
ScanResult = _reader.ScanEntities();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanCompLookupExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.ScanComponentLookup();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanAreaLoadingExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.ScanAreaLoadingState();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanDiffExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.ScanMemoryDiff();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanActiveVecExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.ScanActiveStatesVector();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanTerrainExecute()
|
||||
{
|
||||
if (_reader is null || !_reader.IsAttached)
|
||||
{
|
||||
ScanResult = "Error: not attached";
|
||||
return;
|
||||
}
|
||||
|
||||
ScanResult = _reader.ScanTerrain();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ScanStructureExecute()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue