work on memory
This commit is contained in:
parent
2f652aa1b7
commit
6e9d89b045
28 changed files with 3590 additions and 111 deletions
428
src/Automata.Ui/ViewModels/MemoryViewModel.cs
Normal file
428
src/Automata.Ui/ViewModels/MemoryViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue