work on memory

This commit is contained in:
Boki 2026-03-01 13:26:47 -05:00
parent 2f652aa1b7
commit 6e9d89b045
28 changed files with 3590 additions and 111 deletions

View file

@ -0,0 +1,428 @@
using System.Collections.ObjectModel;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Automata.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<MemoryNodeViewModel> 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;
[ObservableProperty] private bool _isEnabled;
[ObservableProperty] private string _statusText = "Not attached";
public ObservableCollection<MemoryNodeViewModel> RootNodes { get; } = [];
// 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? _terrainCells;
private MemoryNodeViewModel? _terrainGrid;
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";
}
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:");
gameState.Children.Add(_gsPattern);
gameState.Children.Add(_gsBase);
gameState.Children.Add(_gsController);
gameState.Children.Add(_gsStates);
gameState.Children.Add(_inGameState);
// 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);
// Terrain
var terrain = new MemoryNodeViewModel("Terrain");
_terrainCells = new MemoryNodeViewModel("Cells:");
_terrainGrid = new MemoryNodeViewModel("Grid:");
terrain.Children.Add(_terrainCells);
terrain.Children.Add(_terrainGrid);
inGameStateGroup.Children.Add(areaInstanceGroup);
inGameStateGroup.Children.Add(player);
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(500));
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);
// 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);
}
// Terrain
if (snap.TerrainCols > 0 && snap.TerrainRows > 0)
{
_terrainCells!.Set($"{snap.TerrainCols}x{snap.TerrainRows}");
_terrainGrid!.Set($"{snap.TerrainWidth}x{snap.TerrainHeight}");
}
else
{
_terrainCells!.Set("?", false);
_terrainGrid!.Set("?", false);
}
}
[RelayCommand]
private void ReadAddressExecute()
{
if (_reader is null || !_reader.IsAttached)
{
RawResult = "Error: not attached";
return;
}
RawResult = _reader.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.ScanRegion(ScanAddress, ScanOffsets, size);
}
[RelayCommand]
private void ScanStatesExecute()
{
if (_reader is null || !_reader.IsAttached)
{
ScanResult = "Error: not attached";
return;
}
ScanResult = _reader.ScanAllStates();
}
[RelayCommand]
private void ProbeExecute()
{
if (_reader is null || !_reader.IsAttached)
{
ScanResult = "Error: not attached";
return;
}
ScanResult = _reader.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.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.DeepScanVitals(hp, mana, es);
}
[RelayCommand]
private void DiagnoseVitalsExecute()
{
if (_reader is null || !_reader.IsAttached)
{
ScanResult = "Error: not attached";
return;
}
ScanResult = _reader.DiagnoseVitals();
}
[RelayCommand]
private void ScanPositionExecute()
{
if (_reader is null || !_reader.IsAttached)
{
ScanResult = "Error: not attached";
return;
}
ScanResult = _reader.ScanPosition();
}
[RelayCommand]
private void DiagnoseEntityExecute()
{
if (_reader is null || !_reader.IsAttached)
{
ScanResult = "Error: not attached";
return;
}
ScanResult = _reader.DiagnoseEntity();
}
[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.ScanStructure(ScanAddress, ScanOffsets, size);
}
}