refactoring

This commit is contained in:
Boki 2026-03-02 16:23:23 -05:00
parent fbd0ba445a
commit 18d8721dd5
68 changed files with 2187 additions and 36 deletions

View file

@ -25,7 +25,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Ui", "src\Automata
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Navigation", "src\Automata.Navigation\Automata.Navigation.csproj", "{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Memory", "src\Automata.Memory\Automata.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Memory", "src\Roboto.Memory\Roboto.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.GameOffsets", "src\Roboto.GameOffsets\Roboto.GameOffsets.csproj", "{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{652F700E-4F84-4E66-BD62-717D3A8D6FBC}"
EndProject
@ -112,6 +114,10 @@ Global
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.Build.0 = Release|Any CPU
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.Build.0 = Release|Any CPU
{B858F6F2-389F-475A-87FE-E4E01DA3E948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B858F6F2-389F-475A-87FE-E4E01DA3E948}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B858F6F2-389F-475A-87FE-E4E01DA3E948}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -180,7 +186,8 @@ Global
{F186DDC8-6843-43E9-8BD3-9F914C5E784E} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
{859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA}
{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890}
{B858F6F2-389F-475A-87FE-E4E01DA3E948} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
{6FEA655D-18E4-4DA1-839F-A41433B03FBB} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}
{74FD0F88-86BC-49AE-9A16-136D92A10090} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC}

View file

@ -84,11 +84,13 @@
"Metadata/NPC/Four_Act1/UnaAfterIronCount",
"Metadata/NPC/Four_Act1/UnaHoodedOneInjured",
"Metadata/NPC/League/Incursion/AlvaIncursionWild",
"Metadata/Pet/AzmeriStag/AzmeriStag",
"Metadata/Pet/BabyBossesHumans/BabyBrutus/BabyBrutus",
"Metadata/Pet/BabyChimera/BabyChimera",
"Metadata/Pet/BetaKiwis/BaronKiwi",
"Metadata/Pet/BetaKiwis/FaridunKiwi",
"Metadata/Pet/BookAndQuillPet/BookAndQuillPet_Abyss",
"Metadata/Pet/Cat/Sphynx/GiantSphynx/GiantSphynxBlack",
"Metadata/Pet/FledglingBellcrow/FledglingBellcrow",
"Metadata/Pet/LandSharkPet/LandSharkPet",
"Metadata/Pet/OctopusParasite/OctopusParasiteCelestial",

View file

@ -16,5 +16,8 @@
<conv:BoolToOverlayBrushConverter x:Key="OccupiedOverlayBrush" />
<conv:MapRequirementsConverter x:Key="MapRequirementsText" />
<conv:MatchedModBrushConverter x:Key="MatchedModBrush" />
<conv:ChangedRowBrushConverter x:Key="ChangedRowBrush" />
<conv:BoolToHandCursorConverter x:Key="PointerCursor" />
<conv:BoolToUnderlineConverter x:Key="PointerUnderline" />
</Application.Resources>
</Application>

View file

@ -71,6 +71,7 @@ public partial class App : Application
services.AddSingleton<CraftingViewModel>();
services.AddSingleton<MemoryViewModel>();
services.AddSingleton<RobotoViewModel>();
services.AddSingleton<ObjectBrowserViewModel>();
var provider = services.BuildServiceProvider();
@ -97,6 +98,7 @@ public partial class App : Application
mainVm.CraftingVm = provider.GetRequiredService<CraftingViewModel>();
mainVm.MemoryVm = provider.GetRequiredService<MemoryViewModel>();
mainVm.RobotoVm = provider.GetRequiredService<RobotoViewModel>();
mainVm.BrowserVm = provider.GetRequiredService<ObjectBrowserViewModel>();
var window = new MainWindow { DataContext = mainVm };
window.SetConfigStore(store);

View file

@ -23,7 +23,8 @@
<ProjectReference Include="..\Automata.Trade\Automata.Trade.csproj" />
<ProjectReference Include="..\Automata.Log\Automata.Log.csproj" />
<ProjectReference Include="..\Automata.Inventory\Automata.Inventory.csproj" />
<ProjectReference Include="..\Automata.Memory\Automata.Memory.csproj" />
<ProjectReference Include="..\Roboto.Memory\Roboto.Memory.csproj" />
<ProjectReference Include="..\Roboto.Data\Roboto.Data.csproj" />
<ProjectReference Include="..\Roboto.Engine\Roboto.Engine.csproj" />
</ItemGroup>
<!-- Sidekick data files (English only) -->

View file

@ -142,3 +142,33 @@ public class MatchedModBrushConverter : IValueConverter
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public class ChangedRowBrushConverter : IValueConverter
{
private static readonly SolidColorBrush ChangedBrush = new(Color.Parse("#30d29922"));
private static readonly SolidColorBrush NormalBrush = new(Colors.Transparent);
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is true ? ChangedBrush : NormalBrush;
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public class BoolToHandCursorConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is true ? new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand) : new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Arrow);
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
public class BoolToUnderlineConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is true ? Avalonia.Media.TextDecorations.Underline : null;
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotSupportedException();
}

View file

@ -1,6 +1,7 @@
using System.Drawing;
using System.Numerics;
using Automata.Ui.ViewModels;
using Roboto.Data;
using Vortice.DirectWrite;
namespace Automata.Ui.Overlay.Layers;
@ -24,16 +25,41 @@ internal sealed class D2dEntityLabelLayer : ID2dOverlayLayer, IDisposable
{
var data = RobotoViewModel.OverlayData;
if (data is null || data.Entries.Length == 0) return;
if (data.CameraMatrix is not { } matrix) return;
var cache = RobotoViewModel.SharedCache;
if (cache is null) return;
// Read camera and player position from centralized cache (updated at 60Hz)
var camData = cache.CameraMatrix;
if (camData is null) return;
var mat = camData.Matrix;
var rt = ctx.RenderTarget;
var playerZ = data.PlayerZ;
// Compute delta between fresh player position (60Hz) and snapshot position (10Hz)
// Entities near the snapshot player get shifted by this delta to compensate for movement
var freshPos = cache.PlayerPosition;
var playerZ = freshPos.HasPosition ? freshPos.Z : data.SnapshotPlayerZ;
var dx = freshPos.HasPosition ? freshPos.X - data.SnapshotPlayerPosition.X : 0f;
var dy = freshPos.HasPosition ? freshPos.Y - data.SnapshotPlayerPosition.Y : 0f;
var snapshotPx = data.SnapshotPlayerPosition.X;
var snapshotPy = data.SnapshotPlayerPosition.Y;
foreach (ref readonly var entry in data.Entries.AsSpan())
{
// WorldToScreen using camera's view-projection matrix (same as ExileCore)
var worldPos = new Vector4(entry.X, entry.Y, playerZ, 1f);
var clip = Vector4.Transform(worldPos, matrix);
// If entity is near the snapshot player position, apply the movement delta
var ex = entry.X;
var ey = entry.Y;
var distToPlayer = MathF.Abs(ex - snapshotPx) + MathF.Abs(ey - snapshotPy);
if (distToPlayer < 50f)
{
ex += dx;
ey += dy;
}
// WorldToScreen using camera's view-projection matrix
var worldPos = new Vector4(ex, ey, playerZ, 1f);
var clip = Vector4.Transform(worldPos, mat);
// Perspective divide
if (clip.W is 0f or float.NaN) continue;

View file

@ -184,6 +184,7 @@ public partial class MainWindowViewModel : ObservableObject
public CraftingViewModel? CraftingVm { get; set; }
public MemoryViewModel? MemoryVm { get; set; }
public RobotoViewModel? RobotoVm { get; set; }
public ObjectBrowserViewModel? BrowserVm { get; set; }
partial void OnBotModeChanged(BotMode value)
{

View file

@ -6,7 +6,7 @@ using Avalonia.Platform;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Automata.Memory;
using Roboto.Memory;
namespace Automata.Ui.ViewModels;
@ -378,7 +378,7 @@ public partial class MemoryViewModel : ObservableObject
{
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 == Automata.Memory.EntityType.Monster);
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");

View file

@ -0,0 +1,470 @@
using System.Collections.ObjectModel;
using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Roboto.Memory;
namespace Automata.Ui.ViewModels;
public partial class FieldRowViewModel : ObservableObject
{
[ObservableProperty] private string _offset = "";
[ObservableProperty] private string _typeTag = "";
[ObservableProperty] private string _typeColor = "#8b949e";
[ObservableProperty] private string _value = "";
[ObservableProperty] private string _rawHex = "";
[ObservableProperty] private bool _isPointer;
[ObservableProperty] private bool _isChanged;
[ObservableProperty] private string _offsetName = "";
public nint PointerTarget { get; set; }
/// <summary>Number of 8-byte rows this field spans (1 for most, 4 for String).</summary>
public int RowSpan { get; set; } = 1;
}
public partial class ObjectBrowserViewModel : ObservableObject
{
private GameMemoryReader? _reader;
private readonly Stack<NavEntry> _navStack = new();
private Dictionary<int, long> _previousValues = new();
[ObservableProperty] private string _statusText = "Not attached";
[ObservableProperty] private string _currentAddress = "";
[ObservableProperty] private string _currentLabel = "Object";
[ObservableProperty] private string _objectSize = "400";
[ObservableProperty] private bool _canGoBack;
[ObservableProperty] private string _breadcrumbText = "";
[ObservableProperty] private string _goToAddressText = "";
public ObservableCollection<FieldRowViewModel> Fields { get; } = [];
private record NavEntry(nint Address, string Label, int ScrollIndex);
[RelayCommand]
private void Attach()
{
if (_reader != null)
{
_reader.Dispose();
_reader = null;
}
_reader = new GameMemoryReader();
if (!_reader.Attach())
{
StatusText = "Process not found";
_reader.Dispose();
_reader = null;
return;
}
// Do an initial snapshot read to resolve GameState pointers
var snap = _reader.ReadSnapshot();
StatusText = $"Attached (PID {snap.ProcessId})";
if (snap.GameStateBase == 0)
{
StatusText = "Attached but GameState base not found";
return;
}
_navStack.Clear();
NavigateTo(snap.GameStateBase, "GameState");
}
[RelayCommand]
private void GoBack()
{
if (_navStack.Count == 0) return;
var entry = _navStack.Pop();
CanGoBack = _navStack.Count > 0;
ScanAndDisplay(entry.Address, entry.Label);
UpdateBreadcrumb();
}
[RelayCommand]
private void Refresh()
{
if (_reader?.Context == null) return;
if (!TryParseHex(CurrentAddress, out var addr)) return;
ScanAndDisplay(addr, CurrentLabel);
}
[RelayCommand]
private void GoToAddress()
{
if (_reader?.Context == null) return;
if (!TryParseHex(GoToAddressText, out var addr)) return;
NavigateTo(addr, $"0x{addr:X}");
}
public void NavigateToPointer(FieldRowViewModel row)
{
if (!row.IsPointer || _reader?.Context == null) return;
// Push current onto stack
if (TryParseHex(CurrentAddress, out var currentAddr))
_navStack.Push(new NavEntry(currentAddr, CurrentLabel, 0));
var label = row.Value.StartsWith("-> ") ? row.Value[3..] : $"+{row.Offset}";
NavigateTo(row.PointerTarget, label);
}
private void NavigateTo(nint address, string label)
{
CanGoBack = _navStack.Count > 0;
ScanAndDisplay(address, label);
UpdateBreadcrumb();
}
private void ScanAndDisplay(nint address, string label)
{
CurrentAddress = $"0x{address:X}";
CurrentLabel = label;
if (!int.TryParse(ObjectSize, System.Globalization.NumberStyles.HexNumber, null, out var size))
size = 0x400;
var fields = ScanObject(address, size);
// Detect changed values
var newValues = new Dictionary<int, long>();
foreach (var f in fields)
{
if (!int.TryParse(f.Offset.Replace("+0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var off))
continue;
var rawVal = ParseRawHexToLong(f.RawHex);
newValues[off] = rawVal;
if (_previousValues.TryGetValue(off, out var prev) && prev != rawVal)
f.IsChanged = true;
}
_previousValues = newValues;
Avalonia.Threading.Dispatcher.UIThread.Post(() =>
{
Fields.Clear();
foreach (var f in fields)
Fields.Add(f);
});
}
private List<FieldRowViewModel> ScanObject(nint address, int size)
{
var ctx = _reader!.Context!;
var mem = ctx.Memory;
var rtti = new RttiResolver(ctx);
var strings = new MsvcStringReader(ctx);
var offsetLabels = BuildOffsetLabels(ctx.Offsets, CurrentLabel);
var data = mem.ReadBytes(address, size);
if (data == null)
return [new FieldRowViewModel { Offset = "+0x000", TypeTag = "Error", Value = "Read failed", TypeColor = "#f85149" }];
var result = new List<FieldRowViewModel>();
var skip = new HashSet<int>(); // offsets to skip (consumed by multi-row fields)
for (var i = 0; i < data.Length; i += 8)
{
if (skip.Contains(i)) continue;
if (i + 8 > data.Length) break;
var qword = BitConverter.ToInt64(data, i);
var ptr = (nint)qword;
var offsetStr = $"+0x{i:X3}";
var rawHex = FormatHex(data, i, 8);
// Priority 1: Vtable — offset 0, points into .rdata
if (i == 0 && ptr != 0 && ctx.IsModuleAddress(ptr))
{
var className = rtti.ResolveRttiName(ptr);
if (className != null)
{
result.Add(new FieldRowViewModel
{
Offset = offsetStr, TypeTag = "Vtable", TypeColor = "#d2a8ff",
Value = className, RawHex = rawHex
});
CurrentLabel = className;
offsetLabels = BuildOffsetLabels(ctx.Offsets, className);
continue;
}
}
// Priority 4: MSVC std::string (32 bytes: _Bx[16] + size[8] + capacity[8])
if (i + 32 <= data.Length)
{
var strSize = BitConverter.ToInt64(data, i + 0x10);
var strCap = BitConverter.ToInt64(data, i + 0x18);
if (strSize > 0 && strSize <= 512 && strCap >= strSize && strCap < 1_000_000)
{
// Try reading as std::string
var strVal = strings.ReadMsvcString(address + i);
if (strVal != null)
{
var fullHex = FormatHex(data, i, 32);
result.Add(new FieldRowViewModel
{
Offset = offsetStr, TypeTag = "String", TypeColor = "#a5d6ff",
Value = $"\"{strVal}\"", RawHex = fullHex, RowSpan = 4
});
// Skip the next 3 rows (24 bytes) since string occupies 32 bytes total
for (var s = i + 8; s < i + 32 && s < data.Length; s += 8)
skip.Add(s);
continue;
}
// Try wstring
var wstrVal = strings.ReadMsvcWString(address + i);
if (wstrVal != null)
{
var fullHex = FormatHex(data, i, 32);
result.Add(new FieldRowViewModel
{
Offset = offsetStr, TypeTag = "WString", TypeColor = "#a5d6ff",
Value = $"\"{wstrVal}\"", RawHex = fullHex, RowSpan = 4
});
for (var s = i + 8; s < i + 32 && s < data.Length; s += 8)
skip.Add(s);
continue;
}
}
}
// Priority 2: Valid heap ptr whose first qword is in .rdata → Ptr with RTTI name
if (ptr != 0 && ctx.IsValidHeapPtr(ptr))
{
var targetFirst = mem.ReadPointer(ptr);
if (targetFirst != 0 && ctx.IsModuleAddress(targetFirst))
{
var className = rtti.ResolveRttiName(targetFirst);
if (className != null)
{
result.Add(new FieldRowViewModel
{
Offset = offsetStr, TypeTag = "Ptr->", TypeColor = "#58a6ff",
Value = $"-> {className}", RawHex = rawHex,
IsPointer = true, PointerTarget = ptr
});
continue;
}
}
// Priority 3: Valid heap ptr, dereferenceable
// Verify it's actually readable
var probeArr = new byte[8];
if (mem.ReadBytes(ptr, probeArr.AsSpan()))
{
result.Add(new FieldRowViewModel
{
Offset = offsetStr, TypeTag = "Ptr", TypeColor = "#58a6ff",
Value = $"0x{ptr:X}", RawHex = rawHex,
IsPointer = true, PointerTarget = ptr
});
continue;
}
}
// Module pointer (not heap but in module range)
if (ptr != 0 && ctx.IsModuleAddress(ptr))
{
var className = rtti.ResolveRttiName(ptr);
result.Add(new FieldRowViewModel
{
Offset = offsetStr, TypeTag = "Module", TypeColor = "#d2a8ff",
Value = className ?? $"0x{ptr:X}", RawHex = rawHex
});
continue;
}
// Priority 5 & 6: Float / Int guesses (look at low 4 bytes and high 4 bytes)
var lo32 = BitConverter.ToInt32(data, i);
var hi32 = BitConverter.ToInt32(data, i + 4);
var loFloat = BitConverter.ToSingle(data, i);
var hiFloat = BitConverter.ToSingle(data, i + 4);
var loIsFloat = IsReasonableFloat(loFloat);
var hiIsFloat = IsReasonableFloat(hiFloat);
var loIsInt = IsReasonableInt(lo32);
var hiIsInt = IsReasonableInt(hi32);
if (loIsFloat || hiIsFloat)
{
var parts = new List<string>();
if (loIsFloat) parts.Add($"{loFloat:F2}f");
else if (loIsInt) parts.Add($"{lo32}");
else parts.Add($"0x{(uint)lo32:X8}");
if (hiIsFloat) parts.Add($"{hiFloat:F2}f");
else if (hiIsInt) parts.Add($"{hi32}");
else parts.Add($"0x{(uint)hi32:X8}");
result.Add(new FieldRowViewModel
{
Offset = offsetStr, TypeTag = "Float?", TypeColor = "#f0883e",
Value = string.Join(" | ", parts), RawHex = rawHex
});
continue;
}
if (loIsInt || hiIsInt)
{
var parts = new List<string>();
parts.Add(loIsInt ? $"{lo32}" : $"0x{(uint)lo32:X8}");
parts.Add(hiIsInt ? $"{hi32}" : $"0x{(uint)hi32:X8}");
result.Add(new FieldRowViewModel
{
Offset = offsetStr, TypeTag = "Int?", TypeColor = "#3fb950",
Value = string.Join(" | ", parts), RawHex = rawHex
});
continue;
}
// Priority 7: Raw hex
result.Add(new FieldRowViewModel
{
Offset = offsetStr, TypeTag = "Hex", TypeColor = "#484f58",
Value = $"0x{(ulong)qword:X16}", RawHex = rawHex
});
}
// Apply offset labels
if (offsetLabels.Count > 0)
{
foreach (var row in result)
{
if (int.TryParse(row.Offset.Replace("+0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var off)
&& offsetLabels.TryGetValue(off, out var name))
{
row.OffsetName = name;
}
}
}
return result;
}
private void UpdateBreadcrumb()
{
var parts = new List<string>();
foreach (var entry in _navStack.Reverse())
parts.Add(entry.Label);
parts.Add(CurrentLabel);
BreadcrumbText = string.Join(" -> ", parts);
}
private static string FormatHex(byte[] data, int offset, int length)
{
var sb = new StringBuilder();
for (var j = 0; j < length && offset + j < data.Length; j++)
{
if (j > 0) sb.Append(' ');
sb.Append(data[offset + j].ToString("X2"));
}
return sb.ToString();
}
private static bool IsReasonableFloat(float f)
{
if (float.IsNaN(f) || float.IsInfinity(f)) return false;
var abs = MathF.Abs(f);
// Must be nonzero and within a reasonable range, and not look like an integer
return abs > 0.001f && abs < 1e8f;
}
private static bool IsReasonableInt(int v)
{
// Nonzero, not a likely pointer fragment, reasonable range
return v != 0 && v > -10_000_000 && v < 10_000_000;
}
private static bool TryParseHex(string text, out nint result)
{
result = 0;
if (string.IsNullOrWhiteSpace(text)) return false;
var clean = text.Trim();
if (clean.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
clean = clean[2..];
if (long.TryParse(clean, System.Globalization.NumberStyles.HexNumber, null, out var val))
{
result = (nint)val;
return true;
}
return false;
}
private static long ParseRawHexToLong(string hex)
{
if (string.IsNullOrEmpty(hex)) return 0;
var bytes = hex.Split(' ');
long val = 0;
for (var i = 0; i < Math.Min(bytes.Length, 8); i++)
{
if (byte.TryParse(bytes[i], System.Globalization.NumberStyles.HexNumber, null, out var b))
val |= (long)b << (i * 8);
}
return val;
}
/// <summary>
/// Builds a map of offset → label name for the given object type, based on known GameOffsets.
/// </summary>
private static Dictionary<int, string> BuildOffsetLabels(GameOffsets o, string objectType)
{
// Normalize: strip namespace prefixes (e.g. "GameStates@InGameState" → "InGameState")
var type = objectType;
var atIdx = type.LastIndexOf('@');
if (atIdx >= 0) type = type[(atIdx + 1)..];
return type switch
{
"GameState" => new()
{
[0x00] = "Controller",
},
"Controller" or "GameStateController" => new()
{
[o.StatesBeginOffset] = "StatesBegin",
[o.ActiveStatesOffset] = "ActiveStates",
[o.InGameStateDirectOffset] = "InGameState",
},
"InGameState" or "IngameState" => new()
{
[o.EscapeStateOffset] = "EscapeState",
[o.IngameDataFromStateOffset] = "IngameData",
[o.WorldDataFromStateOffset] = "WorldData",
[o.CameraOffset] = "Camera",
},
"AreaInstance" or "IngameData" or "WorldData" => new()
{
[o.AreaLevelOffset] = "AreaLevel",
[o.AreaHashOffset] = "AreaHash",
[o.ServerDataOffset] = "ServerData",
[o.LocalPlayerDirectOffset] = "LocalPlayer",
[o.EntityListOffset] = "EntityList",
[o.TerrainListOffset] = "Terrain",
[o.LifeComponentOffset1] = "LifeComp1",
},
"Entity" => new()
{
[o.EntityDetailsOffset] = "Details",
[o.ComponentListOffset] = "Components",
[o.EntityIdOffset] = "EntityId",
[o.EntityFlagsOffset] = "Flags",
},
"Life" => new()
{
[o.LifeHealthOffset] = "Health",
[o.LifeManaOffset] = "Mana",
[o.LifeEsOffset] = "ES",
},
"Render" or "Positioned" => new()
{
[o.PositionXOffset] = "X",
[o.PositionYOffset] = "Y",
[o.PositionZOffset] = "Z",
},
_ => []
};
}
}

View file

@ -1,8 +1,10 @@
using System.Collections.ObjectModel;
using System.Numerics;
using Roboto.Memory;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Roboto.Core;
using Roboto.Data;
using Roboto.Engine;
using Roboto.Input;
using Roboto.Navigation;
@ -14,9 +16,9 @@ namespace Automata.Ui.ViewModels;
/// </summary>
public sealed class EntityOverlayData
{
public Vector2 PlayerPosition;
public float PlayerZ;
public Matrix4x4? CameraMatrix;
/// <summary>Player position at time the entity list was built (cold tick, 10Hz).</summary>
public Vector2 SnapshotPlayerPosition;
public float SnapshotPlayerZ;
public EntityOverlayEntry[] Entries = [];
}
@ -96,14 +98,19 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
/// </summary>
public static volatile EntityOverlayData? OverlayData;
/// <summary>
/// Shared GameDataCache for the overlay layer to read camera/player data directly.
/// </summary>
public static volatile GameDataCache? SharedCache;
public RobotoViewModel()
{
var config = new BotConfig();
var memory = new MemoryAdapter();
var reader = new GameMemoryReader();
var humanizer = new Humanizer(config);
var input = new InterceptionInputController(humanizer);
_engine = new BotEngine(config, memory, input);
_engine = new BotEngine(config, reader, input);
_engine.StatusChanged += status =>
{
@ -119,7 +126,9 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
if (_engine.IsRunning) return;
var ok = _engine.Start();
IsRunning = _engine.IsRunning;
if (!ok)
if (ok)
SharedCache = _engine.Cache;
else
StatusText = _engine.Status;
}
@ -142,6 +151,7 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
NavStatus = "—";
Entities.Clear();
OverlayData = null;
SharedCache = null;
}
[RelayCommand]
@ -232,9 +242,8 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
{
OverlayData = new EntityOverlayData
{
PlayerPosition = state.Player.Position,
PlayerZ = state.Player.Z,
CameraMatrix = state.CameraMatrix,
SnapshotPlayerPosition = state.Player.Position,
SnapshotPlayerZ = state.Player.Z,
Entries = overlayEntries.ToArray(),
};
}

View file

@ -1034,6 +1034,112 @@
</ScrollViewer>
</TabItem>
<!-- ========== BROWSER TAB ========== -->
<TabItem Header="Browser">
<DockPanel DataContext="{Binding BrowserVm}" Margin="0,6,0,0"
x:DataType="vm:ObjectBrowserViewModel">
<!-- Top toolbar -->
<Border DockPanel.Dock="Top" Background="#161b22" BorderBrush="#30363d"
BorderThickness="1" CornerRadius="8" Padding="8" Margin="0,0,0,6">
<StackPanel Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Attach" Command="{Binding AttachCommand}"
Padding="12,4" FontWeight="Bold" />
<Button Content="&lt;-" Command="{Binding GoBackCommand}"
IsEnabled="{Binding CanGoBack}"
Padding="12,4" FontWeight="Bold" />
<Button Content="Refresh" Command="{Binding RefreshCommand}"
Padding="12,4" />
<TextBlock Text="|" Foreground="#30363d" VerticalAlignment="Center" />
<TextBox Text="{Binding GoToAddressText}" Watermark="Address (hex)"
Width="160" FontFamily="Consolas" FontSize="11" />
<Button Content="Go" Command="{Binding GoToAddressCommand}"
Padding="12,4" />
<TextBlock Text="|" Foreground="#30363d" VerticalAlignment="Center" />
<TextBlock Text="Size:" Foreground="#8b949e" FontSize="11"
VerticalAlignment="Center" />
<TextBox Text="{Binding ObjectSize}" Width="60"
FontFamily="Consolas" FontSize="11" />
<TextBlock Text="{Binding StatusText}" Foreground="#8b949e"
FontSize="11" VerticalAlignment="Center" Margin="12,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{Binding CurrentAddress}" FontFamily="Consolas"
FontSize="12" Foreground="#58a6ff" VerticalAlignment="Center"
FontWeight="Bold" />
<TextBlock Text="{Binding CurrentLabel}" FontSize="12"
Foreground="#d2a8ff" VerticalAlignment="Center"
FontWeight="SemiBold" />
<TextBlock Text="{Binding BreadcrumbText}" FontSize="11"
Foreground="#484f58" VerticalAlignment="Center"
Margin="12,0,0,0" />
</StackPanel>
</StackPanel>
</Border>
<!-- Column headers -->
<Border DockPanel.Dock="Top" Background="#21262d" Padding="8,4"
Margin="0,0,0,2">
<Grid ColumnDefinitions="70,70,110,*,220">
<TextBlock Grid.Column="0" Text="OFFSET" FontSize="10"
FontWeight="Bold" Foreground="#8b949e" />
<TextBlock Grid.Column="1" Text="TYPE" FontSize="10"
FontWeight="Bold" Foreground="#8b949e" />
<TextBlock Grid.Column="2" Text="NAME" FontSize="10"
FontWeight="Bold" Foreground="#8b949e" />
<TextBlock Grid.Column="3" Text="VALUE" FontSize="10"
FontWeight="Bold" Foreground="#8b949e" />
<TextBlock Grid.Column="4" Text="HEX" FontSize="10"
FontWeight="Bold" Foreground="#8b949e" />
</Grid>
</Border>
<!-- Field rows -->
<Border Background="#161b22" BorderBrush="#30363d"
BorderThickness="1" CornerRadius="8" Padding="4">
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Fields}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:FieldRowViewModel">
<Border Padding="4,2" Margin="0,0,0,1"
Background="{Binding IsChanged, Converter={StaticResource ChangedRowBrush}}"
CornerRadius="2"
PointerPressed="BrowserRow_PointerPressed"
Cursor="{Binding IsPointer, Converter={StaticResource PointerCursor}}">
<Grid ColumnDefinitions="70,70,110,*,220">
<TextBlock Grid.Column="0" Text="{Binding Offset}"
FontFamily="Consolas" FontSize="11"
Foreground="#8b949e" VerticalAlignment="Center" />
<Border Grid.Column="1" Background="{Binding TypeColor}"
CornerRadius="3" Padding="4,1" Margin="0,0,4,0"
HorizontalAlignment="Left" Opacity="0.2">
<TextBlock Text="{Binding TypeTag}" FontSize="10"
FontWeight="Bold"
Foreground="{Binding TypeColor}" />
</Border>
<TextBlock Grid.Column="2" Text="{Binding OffsetName}"
FontFamily="Consolas" FontSize="11"
Foreground="#d29922" FontWeight="SemiBold"
VerticalAlignment="Center" />
<TextBlock Grid.Column="3" Text="{Binding Value}"
FontFamily="Consolas" FontSize="11"
Foreground="{Binding TypeColor}"
VerticalAlignment="Center"
TextDecorations="{Binding IsPointer, Converter={StaticResource PointerUnderline}}" />
<TextBlock Grid.Column="4" Text="{Binding RawHex}"
FontFamily="Consolas" FontSize="10"
Foreground="#484f58" VerticalAlignment="Center" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
</DockPanel>
</TabItem>
<!-- ========== SETTINGS TAB ========== -->
<TabItem Header="Settings">
<TabControl DataContext="{Binding SettingsVm}" Margin="0,6,0,0"

View file

@ -146,6 +146,14 @@ public partial class MainWindow : Window
marker.IsVisible = true;
}
public void BrowserRow_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender is not Control { DataContext: FieldRowViewModel row }) return;
if (!row.IsPointer) return;
if (DataContext is not MainWindowViewModel mainVm) return;
mainVm.BrowserVm?.NavigateToPointer(row);
}
protected override void OnClosing(WindowClosingEventArgs e)
{
if (_store != null)

View file

@ -0,0 +1,108 @@
using System.Numerics;
using Roboto.Core;
namespace Roboto.Data;
/// <summary>
/// Immutable snapshot of player position for lock-free cross-thread reads.
/// Class (reference type) so volatile assignment is atomic.
/// </summary>
public sealed class PlayerPositionData
{
public readonly float X, Y, Z;
public readonly bool HasPosition;
private PlayerPositionData() { }
public PlayerPositionData(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
HasPosition = true;
}
public static readonly PlayerPositionData Empty = new();
}
/// <summary>
/// Immutable snapshot of player vitals for lock-free cross-thread reads.
/// Class (reference type) so volatile assignment is atomic.
/// </summary>
public sealed class PlayerVitalsData
{
public readonly int LifeCurrent, LifeTotal;
public readonly int ManaCurrent, ManaTotal;
public readonly int EsCurrent, EsTotal;
public readonly bool HasVitals;
private PlayerVitalsData() { }
public PlayerVitalsData(int lifeCur, int lifeMax, int manaCur, int manaMax, int esCur, int esMax)
{
LifeCurrent = lifeCur;
LifeTotal = lifeMax;
ManaCurrent = manaCur;
ManaTotal = manaMax;
EsCurrent = esCur;
EsTotal = esMax;
HasVitals = true;
}
public static readonly PlayerVitalsData Empty = new();
}
/// <summary>
/// Immutable wrapper for Matrix4x4 so volatile reference assignment works.
/// </summary>
public sealed class CameraMatrixData
{
public readonly Matrix4x4 Matrix;
public CameraMatrixData(Matrix4x4 matrix)
{
Matrix = matrix;
}
}
/// <summary>
/// Centralized memory cache — single source of truth for all game data.
/// Hot fields updated at 60Hz (4 RPM calls), cold fields at 10Hz (full snapshot).
/// All fields use volatile reference types or atomic primitives for lock-free cross-thread reads.
/// </summary>
public sealed class GameDataCache
{
// ── Hot data (updated at 60Hz) ──
public volatile CameraMatrixData? CameraMatrix;
public volatile PlayerPositionData PlayerPosition = PlayerPositionData.Empty;
public volatile PlayerVitalsData PlayerVitals = PlayerVitalsData.Empty;
public volatile bool IsLoading;
public volatile bool IsEscapeOpen;
// ── Cold data (updated at 10Hz) ──
public volatile IReadOnlyList<EntitySnapshot> Entities = [];
public volatile IReadOnlyList<EntitySnapshot> HostileMonsters = [];
public volatile IReadOnlyList<EntitySnapshot> NearbyLoot = [];
public volatile WalkabilitySnapshot? Terrain;
public volatile uint AreaHash;
public volatile int AreaLevel;
// ── Full GameState (updated at 10Hz) — for systems that need the complete object ──
public volatile GameState? LatestState;
// ── Timestamps ──
private long _hotTickTimestamp;
private long _coldTickTimestamp;
public long HotTickTimestamp
{
get => Interlocked.Read(ref _hotTickTimestamp);
set => Interlocked.Exchange(ref _hotTickTimestamp, value);
}
public long ColdTickTimestamp
{
get => Interlocked.Read(ref _coldTickTimestamp);
set => Interlocked.Exchange(ref _coldTickTimestamp, value);
}
}

View file

@ -0,0 +1,372 @@
using System.Diagnostics;
using System.Numerics;
using Roboto.Memory;
using Roboto.Core;
using Serilog;
using MemEntity = Roboto.Memory.Entity;
using MemEntityType = Roboto.Memory.EntityType;
namespace Roboto.Data;
/// <summary>
/// Owns the memory read thread. Runs a two-tier loop:
/// - Hot tick (60Hz): 4 RPM calls using cached addresses → updates GameDataCache hot fields
/// - Cold tick (10Hz): full snapshot via ReadSnapshot() → updates cold fields + re-resolves addresses
/// </summary>
public sealed class MemoryPoller : IDisposable
{
private readonly GameMemoryReader _reader;
private readonly GameDataCache _cache;
private readonly BotConfig _config;
private Thread? _thread;
private volatile bool _running;
private bool _disposed;
// Cached resolved addresses (re-resolved on each cold tick)
private nint _cameraMatrixAddr;
private nint _playerRenderAddr;
private nint _playerLifeAddr;
private nint _inGameStateAddr;
private nint _controllerAddr;
private Roboto.Memory.GameOffsets? _offsets;
private ProcessMemory? _mem;
private int _hotHz;
private int _coldHz;
private long _coldTickNumber;
public event Action? StateUpdated;
public MemoryPoller(GameMemoryReader reader, GameDataCache cache, BotConfig config)
{
_reader = reader;
_cache = cache;
_config = config;
}
public void Start(int hotHz = 60, int coldHz = 10)
{
if (_running) return;
_hotHz = hotHz;
_coldHz = coldHz;
_running = true;
_thread = new Thread(PollLoop)
{
Name = "Roboto.MemoryPoller",
IsBackground = true,
};
_thread.Start();
}
public void Stop()
{
if (!_running) return;
_running = false;
_thread?.Join(2000);
_thread = null;
}
private void PollLoop()
{
var hotIntervalMs = 1000.0 / _hotHz;
var coldEveryN = Math.Max(1, _hotHz / _coldHz); // e.g. 60/10 = every 6th hot tick
var sw = Stopwatch.StartNew();
var hotTickCount = 0;
GameState? previousState = null;
while (_running)
{
try
{
var isColdTick = hotTickCount % coldEveryN == 0;
if (isColdTick)
{
// ── Cold tick: full snapshot + re-resolve addresses ──
previousState = DoColdTick(previousState);
}
else
{
// ── Hot tick: minimal reads from cached addresses ──
DoHotTick();
}
hotTickCount++;
}
catch (Exception ex)
{
Log.Debug(ex, "MemoryPoller error");
}
var elapsed = sw.Elapsed.TotalMilliseconds;
var sleepMs = hotIntervalMs - (elapsed % hotIntervalMs);
if (sleepMs > 1)
Thread.Sleep((int)sleepMs);
}
}
/// <summary>
/// Full snapshot: read everything, build GameState, update cache cold + hot fields, re-resolve addresses.
/// </summary>
private GameState? DoColdTick(GameState? previous)
{
if (!_reader.IsAttached) return previous;
var ctx = _reader.Context;
if (ctx is null) return previous;
_mem = ctx.Memory;
_offsets = ctx.Offsets;
// Full snapshot
var snap = _reader.ReadSnapshot();
if (!snap.Attached) return previous;
// Re-resolve hot addresses
var hot = _reader.ResolveHotAddresses();
_cameraMatrixAddr = hot.CameraMatrixAddr;
_playerRenderAddr = hot.PlayerRenderAddr;
_playerLifeAddr = hot.PlayerLifeAddr;
_inGameStateAddr = hot.InGameStateAddr;
_controllerAddr = hot.ControllerAddr;
// Build full GameState
var state = BuildGameState(snap, previous);
_coldTickNumber++;
// Update cache — cold fields
_cache.Entities = state.Entities;
_cache.HostileMonsters = state.HostileMonsters;
_cache.NearbyLoot = state.NearbyLoot;
_cache.Terrain = state.Terrain;
_cache.AreaHash = state.AreaHash;
_cache.AreaLevel = state.AreaLevel;
_cache.LatestState = state;
_cache.ColdTickTimestamp = Environment.TickCount64;
// Also update hot fields from the snapshot (so they're never stale)
_cache.CameraMatrix = snap.CameraMatrix.HasValue ? new CameraMatrixData(snap.CameraMatrix.Value) : null;
_cache.PlayerPosition = snap.HasPosition
? new PlayerPositionData(snap.PlayerX, snap.PlayerY, snap.PlayerZ)
: PlayerPositionData.Empty;
_cache.PlayerVitals = snap.HasVitals
? new PlayerVitalsData(snap.LifeCurrent, snap.LifeTotal, snap.ManaCurrent, snap.ManaTotal, snap.EsCurrent, snap.EsTotal)
: PlayerVitalsData.Empty;
_cache.IsLoading = snap.IsLoading;
_cache.IsEscapeOpen = snap.IsEscapeOpen;
_cache.HotTickTimestamp = Environment.TickCount64;
StateUpdated?.Invoke();
return state;
}
/// <summary>
/// Hot tick: minimal reads (4 RPM calls) using pre-resolved addresses.
/// </summary>
private void DoHotTick()
{
if (_mem is null || _offsets is null) return;
// 1. Camera matrix (64 bytes, 1 RPM)
if (_cameraMatrixAddr != 0)
{
var bytes = _mem.ReadBytes(_cameraMatrixAddr, 64);
if (bytes is { Length: >= 64 })
{
var m = new Matrix4x4(
BitConverter.ToSingle(bytes, 0), BitConverter.ToSingle(bytes, 4), BitConverter.ToSingle(bytes, 8), BitConverter.ToSingle(bytes, 12),
BitConverter.ToSingle(bytes, 16), BitConverter.ToSingle(bytes, 20), BitConverter.ToSingle(bytes, 24), BitConverter.ToSingle(bytes, 28),
BitConverter.ToSingle(bytes, 32), BitConverter.ToSingle(bytes, 36), BitConverter.ToSingle(bytes, 40), BitConverter.ToSingle(bytes, 44),
BitConverter.ToSingle(bytes, 48), BitConverter.ToSingle(bytes, 52), BitConverter.ToSingle(bytes, 56), BitConverter.ToSingle(bytes, 60));
if (!float.IsNaN(m.M11) && !float.IsInfinity(m.M11))
_cache.CameraMatrix = new CameraMatrixData(m);
}
}
// 2. Player position (12 bytes, 1 RPM)
if (_playerRenderAddr != 0)
{
var bytes = _mem.ReadBytes(_playerRenderAddr + _offsets.PositionXOffset, 12);
if (bytes is { Length: >= 12 })
{
var x = BitConverter.ToSingle(bytes, 0);
var y = BitConverter.ToSingle(bytes, 4);
var z = BitConverter.ToSingle(bytes, 8);
if (!float.IsNaN(x) && !float.IsNaN(y) && x >= 50 && x <= 50000 && y >= 50 && y <= 50000)
_cache.PlayerPosition = new PlayerPositionData(x, y, z);
}
}
// 3. Player vitals (24 bytes across 3 VitalStruct regions, 1 RPM via bulk read)
if (_playerLifeAddr != 0)
{
// Read a contiguous block covering HP, Mana, ES structs
// HP at LifeHealthOffset, Mana at LifeManaOffset, ES at LifeEsOffset
// Each has VitalCurrentOffset (+0x30) and VitalTotalOffset (+0x2C)
var hpOff = _offsets.LifeHealthOffset;
var esOff = _offsets.LifeEsOffset;
var endOff = esOff + _offsets.VitalCurrentOffset + 4; // last byte we need
var regionSize = endOff - hpOff;
if (regionSize > 0 && regionSize < 0x200)
{
var bytes = _mem.ReadBytes(_playerLifeAddr + hpOff, regionSize);
if (bytes is not null && bytes.Length >= regionSize)
{
var hpCur = BitConverter.ToInt32(bytes, _offsets.VitalCurrentOffset);
var hpMax = BitConverter.ToInt32(bytes, _offsets.VitalTotalOffset);
var manaCur = BitConverter.ToInt32(bytes, _offsets.LifeManaOffset - hpOff + _offsets.VitalCurrentOffset);
var manaMax = BitConverter.ToInt32(bytes, _offsets.LifeManaOffset - hpOff + _offsets.VitalTotalOffset);
var esCur = BitConverter.ToInt32(bytes, _offsets.LifeEsOffset - hpOff + _offsets.VitalCurrentOffset);
var esMax = BitConverter.ToInt32(bytes, _offsets.LifeEsOffset - hpOff + _offsets.VitalTotalOffset);
if (hpMax > 0 && hpMax <= 200000 && hpCur >= 0)
{
_cache.PlayerVitals = new PlayerVitalsData(hpCur, hpMax, manaCur, manaMax, esCur, esMax);
}
}
}
}
// 4. Loading/escape state (~16 bytes, 1 RPM)
if (_controllerAddr != 0 && _offsets.IsLoadingOffset > 0 && _inGameStateAddr != 0)
{
var activePtr = _mem.ReadPointer(_controllerAddr + _offsets.IsLoadingOffset);
_cache.IsLoading = activePtr != 0 && activePtr != _inGameStateAddr;
}
if (_inGameStateAddr != 0 && _offsets.EscapeStateOffset > 0)
{
var escVal = _mem.Read<int>(_inGameStateAddr + _offsets.EscapeStateOffset);
_cache.IsEscapeOpen = escVal != 0;
}
_cache.HotTickTimestamp = Environment.TickCount64;
}
private GameState BuildGameState(GameStateSnapshot snap, GameState? previous)
{
var state = new GameState
{
TickNumber = (previous?.TickNumber ?? 0) + 1,
TimestampMs = Environment.TickCount64,
};
if (previous is not null)
state.DeltaTime = (state.TimestampMs - previous.TimestampMs) / 1000f;
state.AreaHash = snap.AreaHash;
state.AreaLevel = snap.AreaLevel;
state.IsLoading = snap.IsLoading;
state.IsEscapeOpen = snap.IsEscapeOpen;
state.CameraMatrix = snap.CameraMatrix;
state.Player = new PlayerState
{
HasPosition = snap.HasPosition,
Position = snap.HasPosition ? new Vector2(snap.PlayerX, snap.PlayerY) : Vector2.Zero,
Z = snap.PlayerZ,
LifeCurrent = snap.LifeCurrent,
LifeTotal = snap.LifeTotal,
ManaCurrent = snap.ManaCurrent,
ManaTotal = snap.ManaTotal,
EsCurrent = snap.EsCurrent,
EsTotal = snap.EsTotal,
};
if (snap.Entities is { Count: > 0 })
{
var playerPos = state.Player.Position;
var allEntities = new List<EntitySnapshot>(snap.Entities.Count);
var hostiles = new List<EntitySnapshot>();
var loot = new List<EntitySnapshot>();
foreach (var e in snap.Entities)
{
if (e.Address == snap.LocalPlayerPtr) continue;
var es = MapEntity(e, playerPos);
allEntities.Add(es);
if (es.Category == EntityCategory.Monster && es.IsAlive)
hostiles.Add(es);
else if (es.Category == EntityCategory.WorldItem)
loot.Add(es);
}
state.Entities = allEntities;
state.HostileMonsters = hostiles;
state.NearbyLoot = loot;
}
if (snap.Terrain is not null)
{
state.Terrain = new WalkabilitySnapshot
{
Width = snap.Terrain.Width,
Height = snap.Terrain.Height,
Data = snap.Terrain.Data,
};
}
return state;
}
private static EntitySnapshot MapEntity(MemEntity e, Vector2 playerPos)
{
var pos = e.HasPosition ? new Vector2(e.X, e.Y) : Vector2.Zero;
var dist = e.HasPosition ? Vector2.Distance(pos, playerPos) : float.MaxValue;
return new EntitySnapshot
{
Id = e.Id,
Path = e.Path,
Category = MapCategory(e.Type),
ThreatLevel = MapThreatLevel(e),
Position = pos,
DistanceToPlayer = dist,
IsAlive = e.IsAlive || !e.HasVitals,
LifeCurrent = e.LifeCurrent,
LifeTotal = e.LifeTotal,
IsTargetable = e.IsTargetable,
Components = e.Components,
};
}
private static EntityCategory MapCategory(MemEntityType type) => type switch
{
MemEntityType.Player => EntityCategory.Player,
MemEntityType.Monster => EntityCategory.Monster,
MemEntityType.Npc => EntityCategory.Npc,
MemEntityType.WorldItem => EntityCategory.WorldItem,
MemEntityType.Chest => EntityCategory.Chest,
MemEntityType.Portal or MemEntityType.TownPortal => EntityCategory.Portal,
MemEntityType.AreaTransition => EntityCategory.AreaTransition,
MemEntityType.Effect => EntityCategory.Effect,
MemEntityType.Terrain => EntityCategory.Terrain,
_ => EntityCategory.MiscObject,
};
private static MonsterThreatLevel MapThreatLevel(MemEntity e)
{
if (e.Type != MemEntityType.Monster) return MonsterThreatLevel.None;
return e.Rarity switch
{
MonsterRarity.White => MonsterThreatLevel.Normal,
MonsterRarity.Magic => MonsterThreatLevel.Magic,
MonsterRarity.Rare => MonsterThreatLevel.Rare,
MonsterRarity.Unique => MonsterThreatLevel.Unique,
_ => MonsterThreatLevel.Normal,
};
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Stop();
}
}

View file

@ -9,5 +9,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Roboto.Core\Roboto.Core.csproj" />
<ProjectReference Include="..\Roboto.Memory\Roboto.Memory.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,68 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Components;
/// <summary>Actor component — skills, animations, deployments.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x2E8)]
public struct Actor
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Pointer to animation controller.</summary>
[FieldOffset(0x1D8)] public nint AnimationControllerPtr;
/// <summary>Active skills StdVector (of ActiveSkillStructure).</summary>
[FieldOffset(0x2C0)] public StdVector ActiveSkills;
/// <summary>Deployed entities StdVector (of DeployedEntityStructure).</summary>
[FieldOffset(0x2D8)] public StdVector DeployedEntities;
}
/// <summary>An entry in the active skills vector.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x28)]
public struct ActiveSkillStructure
{
[FieldOffset(0x00)] public nint SkillDetailsPtr;
[FieldOffset(0x08)] public short SkillId;
[FieldOffset(0x0C)] public byte CanBeUsed;
[FieldOffset(0x0D)] public byte CanBeUsedWithWeapon;
[FieldOffset(0x10)] public nint CooldownPtr;
}
/// <summary>Detailed info about a skill.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public struct ActiveSkillDetails
{
[FieldOffset(0x00)] public nint NamePtr;
[FieldOffset(0x08)] public nint InternalNamePtr;
[FieldOffset(0x10)] public int GrantedEffectsPerLevelIdx;
[FieldOffset(0x14)] public int IconIndex;
}
/// <summary>Cooldown state for a skill.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public struct ActiveSkillCooldown
{
[FieldOffset(0x00)] public nint CooldownGroupPtr;
[FieldOffset(0x08)] public int MaxUses;
[FieldOffset(0x0C)] public int CurrentUses;
[FieldOffset(0x10)] public int CooldownTimer;
}
/// <summary>Vaal soul tracking.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct VaalSoulStructure
{
public nint GrantedEffectsPtr;
public int CurrentSouls;
public int SoulCost;
}
/// <summary>A deployed entity (totem, mine, etc.).</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DeployedEntityStructure
{
public uint EntityId;
public int SkillIndex;
}

View file

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Animated component — reference to the animated entity.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x300)]
public struct Animated
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Pointer to the animated entity object.</summary>
[FieldOffset(0x2F8)] public nint AnimatedEntityPtr;
}

View file

@ -0,0 +1,25 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Components;
/// <summary>Buffs component — active status effects.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x178)]
public struct Buffs
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>StdVector of StatusEffectStruct pointers.</summary>
[FieldOffset(0x160)] public StdVector StatusEffects;
}
/// <summary>A single status effect (buff/debuff).</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x40)]
public struct StatusEffectStruct
{
[FieldOffset(0x00)] public nint NamePtr;
[FieldOffset(0x10)] public float Duration;
[FieldOffset(0x14)] public float Timer;
[FieldOffset(0x18)] public int Charges;
[FieldOffset(0x1C)] public short FlaskSlot;
}

View file

@ -0,0 +1,23 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Charges component — flask/skill charges.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public struct Charges
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Pointer to ChargesInternal struct.</summary>
[FieldOffset(0x10)] public nint ChargesInternalPtr;
/// <summary>Current charge count.</summary>
[FieldOffset(0x18)] public int Current;
}
/// <summary>Internal charges detail.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public struct ChargesInternal
{
[FieldOffset(0x18)] public int PerUseCharges;
}

View file

@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Chest component.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x170)]
public struct Chest
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Pointer to ChestInternal data.</summary>
[FieldOffset(0x160)] public nint ChestsDataPtr;
/// <summary>Whether the chest has been opened (byte bool).</summary>
[FieldOffset(0x168)] public byte IsOpened;
}
/// <summary>Internal chest data.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x28)]
public struct ChestInternal
{
/// <summary>Whether the chest label is visible (byte bool).</summary>
[FieldOffset(0x21)] public byte IsLabelVisible;
}

View file

@ -0,0 +1,11 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Common header at the start of every component.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct ComponentHeader
{
[FieldOffset(0x00)] public nint StaticPtr;
[FieldOffset(0x08)] public nint EntityPtr;
}

View file

@ -0,0 +1,27 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Life component — contains Health, Mana, and ES vitals.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x268)]
public struct Life
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Health vital block.</summary>
[FieldOffset(0x1A8)] public VitalStruct Health;
/// <summary>Mana vital block.</summary>
[FieldOffset(0x1F8)] public VitalStruct Mana;
/// <summary>Energy Shield vital block.</summary>
[FieldOffset(0x230)] public VitalStruct EnergyShield;
}
/// <summary>A single vital (HP/Mana/ES) with current and total values.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x38)]
public struct VitalStruct
{
[FieldOffset(0x2C)] public int Total;
[FieldOffset(0x30)] public int Current;
}

View file

@ -0,0 +1,59 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Components;
/// <summary>Mods component — item rarity, explicit/implicit mods.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x1A0)]
public struct Mods
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Pointer to ObjectMagicProperties.</summary>
[FieldOffset(0x98)] public nint ObjectMagicPropertiesPtr;
/// <summary>Pointer to AllModsType struct.</summary>
[FieldOffset(0xA0)] public nint AllModsPtr;
}
/// <summary>Magic properties of an item (rarity, etc.).</summary>
[StructLayout(LayoutKind.Explicit, Size = 0xA0)]
public struct ObjectMagicProperties
{
/// <summary>Item rarity: 0=Normal, 1=Magic, 2=Rare, 3=Unique.</summary>
[FieldOffset(0x94)] public int Rarity;
}
/// <summary>Combined mods and magic properties.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x150)]
public struct ModsAndObjectMagicProperties
{
[FieldOffset(0x00)] public nint ModsPtr;
[FieldOffset(0x08)] public nint ObjectMagicPropertiesPtr;
}
/// <summary>All mod arrays (implicit, explicit, enchant, etc.).</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x150)]
public struct AllModsType
{
/// <summary>Implicit mods StdVector.</summary>
[FieldOffset(0x00)] public StdVector ImplicitMods;
/// <summary>Explicit mods StdVector.</summary>
[FieldOffset(0x18)] public StdVector ExplicitMods;
/// <summary>Enchant mods StdVector.</summary>
[FieldOffset(0x30)] public StdVector EnchantMods;
/// <summary>Stats from mods StdVector.</summary>
[FieldOffset(0x148)] public StdVector StatsFromMods;
}
/// <summary>A single mod entry.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ModArrayStruct
{
public nint ModPtr;
public int Level;
public int Unknown;
}

View file

@ -0,0 +1,20 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Components;
/// <summary>Player component — name, XP, level.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x208)]
public struct Player
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Player name (MSVC std::wstring with SSO).</summary>
[FieldOffset(0x1B0)] public StdWString Name;
/// <summary>Total experience.</summary>
[FieldOffset(0x1D8)] public long Xp;
/// <summary>Character level.</summary>
[FieldOffset(0x204)] public int Level;
}

View file

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Positioned component — reaction (friendly/hostile/neutral).</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x1E8)]
public struct Positioned
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Reaction type: 1=Friendly, 2=Hostile, etc.</summary>
[FieldOffset(0x1E0)] public int Reaction;
}

View file

@ -0,0 +1,23 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Components;
/// <summary>Render component — world position, bounds, terrain height.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x148)]
public struct Render
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Current world position (float x, y, z).</summary>
[FieldOffset(0xB8)] public StdTuple3D<float> CurrentWorldPosition;
/// <summary>Character model bounds (float x, y, z).</summary>
[FieldOffset(0xC4)] public StdTuple3D<float> CharacterModelBounds;
/// <summary>Terrain height at entity position.</summary>
[FieldOffset(0x130)] public float TerrainHeight;
/// <summary>Grid position (float x, y, z). Confirmed offsets: X=0x138, Y=0x13C, Z=0x140.</summary>
[FieldOffset(0x138)] public StdTuple3D<float> Position;
}

View file

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Shrine component.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x28)]
public struct Shrine
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Whether the shrine has been used (byte bool).</summary>
[FieldOffset(0x24)] public byte IsUsed;
}

View file

@ -0,0 +1,17 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Components;
/// <summary>StateMachine component — entity state management.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x178)]
public struct StateMachine
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Pointer to states map/array.</summary>
[FieldOffset(0x158)] public nint StatesPtr;
/// <summary>StdVector of state values.</summary>
[FieldOffset(0x160)] public StdVector StatesValues;
}

View file

@ -0,0 +1,32 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Components;
/// <summary>Stats component — item stats, weapon index, shapeshift.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x180)]
public struct Stats
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Stats changed by items StdVector.</summary>
[FieldOffset(0x160)] public StdVector StatsChangedByItems;
/// <summary>Current weapon index.</summary>
[FieldOffset(0x178)] public int WeaponIndex;
}
/// <summary>Internal stats storage.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public struct StatsInternal
{
[FieldOffset(0x00)] public StdVector StatArray;
}
/// <summary>A single stat entry (key/value pair).</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StatArrayStruct
{
public int StatId;
public int Value;
}

View file

@ -0,0 +1,19 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Targetable component — whether entity can be targeted/highlighted.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x58)]
public struct Targetable
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Whether the entity is targetable (byte bool).</summary>
[FieldOffset(0x51)] public byte IsTargetable;
/// <summary>Whether the entity is highlightable (byte bool).</summary>
[FieldOffset(0x52)] public byte IsHighlightable;
/// <summary>Whether the entity is targetable through walls (byte bool).</summary>
[FieldOffset(0x53)] public byte IsTargetableThroughWalls;
}

View file

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>Transitionable component — area transition state.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x128)]
public struct Transitionable
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Current state enum value.</summary>
[FieldOffset(0x120)] public int CurrentStateEnum;
}

View file

@ -0,0 +1,13 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Components;
/// <summary>TriggerableBlockage component — door/gate blocked state.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x38)]
public struct TriggerableBlockage
{
[FieldOffset(0x00)] public ComponentHeader Header;
/// <summary>Whether the blockage is currently blocking (byte bool).</summary>
[FieldOffset(0x30)] public byte IsBlocked;
}

View file

@ -0,0 +1,64 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.Entities;
/// <summary>Item struct — wraps an entity pointer for inventory items.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ItemStruct
{
public nint EntityPtr;
}
/// <summary>Top-level entity struct in memory.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x88)]
public struct EntityStruct
{
/// <summary>VTable pointer.</summary>
[FieldOffset(0x00)] public nint VTable;
/// <summary>Pointer to EntityDetails (path, component lookup).</summary>
[FieldOffset(0x08)] public nint EntityDetailsPtr;
/// <summary>Component list (StdVector of component pointers).</summary>
[FieldOffset(0x10)] public StdVector ComponentList;
/// <summary>Entity ID (unique within area instance).</summary>
[FieldOffset(0x80)] public uint Id;
/// <summary>Entity validity/flags byte.</summary>
[FieldOffset(0x84)] public byte IsValid;
}
/// <summary>Entity details — path string and component lookup.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x30)]
public struct EntityDetails
{
/// <summary>MSVC std::string for entity path (e.g., "Metadata/Monsters/...").</summary>
[FieldOffset(0x08)] public StdWString Path;
/// <summary>Pointer to ComponentLookup object.</summary>
[FieldOffset(0x28)] public nint ComponentLookupPtr;
}
/// <summary>Component lookup — maps component names to indices.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x40)]
public struct ComponentLookup
{
/// <summary>StdVector of ComponentNameAndIndex entries.</summary>
[FieldOffset(0x28)] public StdVector NameEntries;
}
/// <summary>A single entry in the component name→index lookup.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct ComponentNameAndIndex
{
/// <summary>Pointer to null-terminated component name string (char*).</summary>
[FieldOffset(0x00)] public nint NamePtr;
/// <summary>Index into the entity's component list StdVector.</summary>
[FieldOffset(0x08)] public int Index;
/// <summary>Flags (purpose TBD).</summary>
[FieldOffset(0x0C)] public int Flags;
}

View file

@ -0,0 +1,42 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Entities;
/// <summary>MSVC std::map tree node for entity storage. Red-black tree node layout.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x30)]
public struct EntityTreeNode
{
/// <summary>Left child node pointer.</summary>
[FieldOffset(0x00)] public nint Left;
/// <summary>Parent node pointer.</summary>
[FieldOffset(0x08)] public nint Parent;
/// <summary>Right child node pointer.</summary>
[FieldOffset(0x10)] public nint Right;
/// <summary>Node color (0=red, 1=black) and nil flag.</summary>
[FieldOffset(0x19)] public byte IsNil;
/// <summary>Entity key/value data.</summary>
[FieldOffset(0x20)] public EntityNodeValue Data;
}
/// <summary>Key used in the entity map (entity ID).</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct EntityNodeKey
{
public uint EntityId;
public uint Padding;
}
/// <summary>Value stored in entity tree node — key + entity pointer.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct EntityNodeValue
{
/// <summary>Entity key (ID).</summary>
[FieldOffset(0x00)] public EntityNodeKey Key;
/// <summary>Pointer to entity struct (at tree node +0x28).</summary>
[FieldOffset(0x08)] public nint EntityPtr;
}

View file

@ -0,0 +1,11 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Natives;
/// <summary>Bucket structure used in hash containers.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public struct StdBucket
{
[FieldOffset(0x00)] public StdVector Data;
[FieldOffset(0x18)] public long Unknown;
}

View file

@ -0,0 +1,28 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Natives;
/// <summary>MSVC std::list layout: head node pointer + size.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StdList
{
public nint Head;
public long Size;
}
/// <summary>MSVC std::list node with typed data.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StdListNode<T> where T : unmanaged
{
public nint Next;
public nint Prev;
public T Data;
}
/// <summary>MSVC std::list node (untyped, pointers only).</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StdListNode
{
public nint Next;
public nint Prev;
}

View file

@ -0,0 +1,37 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Natives;
/// <summary>MSVC std::map layout: head node pointer + size.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StdMap
{
public nint Head;
public long Size;
}
/// <summary>MSVC std::map tree node with typed key/value data.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StdMapNode<TKey, TValue>
where TKey : unmanaged
where TValue : unmanaged
{
public nint Left;
public nint Parent;
public nint Right;
public byte Color;
public byte IsNil;
private readonly short _pad0;
private readonly int _pad1;
public StdMapNodeData<TKey, TValue> Data;
}
/// <summary>Key/value pair stored in a std::map node.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StdMapNodeData<TKey, TValue>
where TKey : unmanaged
where TValue : unmanaged
{
public TKey Key;
public TValue Value;
}

View file

@ -0,0 +1,20 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Natives;
/// <summary>2D tuple (e.g., terrain dimensions).</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StdTuple2D<T> where T : unmanaged
{
public T X;
public T Y;
}
/// <summary>3D tuple (e.g., world positions).</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StdTuple3D<T> where T : unmanaged
{
public T X;
public T Y;
public T Z;
}

View file

@ -0,0 +1,15 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Natives;
/// <summary>MSVC std::vector layout: begin/end/capacity pointers.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StdVector
{
public nint First;
public nint Last;
public nint End;
public readonly long TotalElements(int elementSize) =>
elementSize > 0 ? (Last - First) / elementSize : 0;
}

View file

@ -0,0 +1,23 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.Natives;
/// <summary>MSVC std::wstring (basic_string&lt;wchar_t&gt;) with SSO.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public unsafe struct StdWString
{
/// <summary>When non-inline: pointer to heap buffer. When inline: first 8 bytes of inline chars.</summary>
[FieldOffset(0x00)] public nint Buffer;
/// <summary>Raw inline storage (16 bytes = 8 wchars).</summary>
[FieldOffset(0x00)] public fixed byte ReservedBytes[16];
/// <summary>Number of wchar_t characters (not bytes).</summary>
[FieldOffset(0x10)] public long Length;
/// <summary>Capacity in wchar_t characters. If &lt;= 7, string is inline (SSO).</summary>
[FieldOffset(0x18)] public long Capacity;
/// <summary>True if the string data is stored inline (SSO), false if on heap.</summary>
public readonly bool IsInline => Capacity <= 7;
}

View file

@ -0,0 +1,10 @@
namespace Roboto.GameOffsets.Natives;
public static class Util
{
public static bool IsBitSet(byte value, int bitIndex) =>
(value & (1 << bitIndex)) != 0;
public static bool IsBitSet(uint value, int bitIndex) =>
(value & (1u << bitIndex)) != 0;
}

View file

@ -0,0 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,98 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.States;
/// <summary>AreaInstance (IngameData) — current area data, entities, terrain.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0xCD0)]
public struct AreaInstance
{
/// <summary>Current area level (byte). Confirmed: 0xC4.</summary>
[FieldOffset(0xC4)] public byte AreaLevel;
/// <summary>Area hash (unique per area instance). Offset: 0xEC.</summary>
[FieldOffset(0xEC)] public uint AreaHash;
/// <summary>Pointer to ServerData. Offset: 0x9F0.</summary>
[FieldOffset(0x9F0)] public nint ServerDataPtr;
/// <summary>Local player struct (inline). Offset: 0xA10.</summary>
[FieldOffset(0xA10)] public LocalPlayerStruct LocalPlayer;
/// <summary>Entity list (std::map). Confirmed: 0xB50.</summary>
[FieldOffset(0xB50)] public EntityList EntityList;
/// <summary>Terrain data (inline). Offset: 0xCC0.</summary>
[FieldOffset(0xCC0)] public Terrain Terrain;
}
/// <summary>Local player reference within AreaInstance.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x28)]
public struct LocalPlayerStruct
{
/// <summary>Pointer to ServerData.</summary>
[FieldOffset(0x00)] public nint ServerDataPtr;
/// <summary>Pointer to the local player entity.</summary>
[FieldOffset(0x20)] public nint LocalPlayerPtr;
}
/// <summary>Entity list — wraps a std::map of entities.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct EntityList
{
/// <summary>std::map head node pointer.</summary>
[FieldOffset(0x00)] public nint HeadPtr;
/// <summary>Entity count (std::map _Mysize).</summary>
[FieldOffset(0x08)] public long Count;
}
/// <summary>Terrain data — dimensions, walkability grid, sub-tile info.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x1B0)]
public struct Terrain
{
/// <summary>Terrain dimensions in tiles (StdTuple2D of long). Offset: +0x90 from terrain start.</summary>
[FieldOffset(0x90)] public StdTuple2D<long> Dimensions;
/// <summary>Walkable grid data StdVector. Offset: +0x148.</summary>
[FieldOffset(0x148)] public StdVector WalkableGrid;
/// <summary>Bytes per row in the walkability grid. Offset: +0x1A8.</summary>
[FieldOffset(0x1A8)] public int BytesPerRow;
}
/// <summary>Tile structure for terrain tiles.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x18)]
public struct TileStructure
{
[FieldOffset(0x00)] public nint TgtFilePtr;
[FieldOffset(0x08)] public int RotationSelector;
[FieldOffset(0x0C)] public byte IsTown;
[FieldOffset(0x0D)] public byte IsHideout;
}
/// <summary>Sub-tile data within a terrain tile.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SubTileStruct
{
public int SubTileX;
public int SubTileY;
}
/// <summary>TGT file reference for terrain graphics.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x18)]
public struct TgtFileStruct
{
[FieldOffset(0x00)] public nint NamePtr;
[FieldOffset(0x08)] public int TgtColumns;
[FieldOffset(0x0C)] public int TgtRows;
}
/// <summary>Environment data.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct EnvironmentStruct
{
[FieldOffset(0x00)] public nint EnvironmentPtr;
[FieldOffset(0x08)] public int EnvironmentHash;
}

View file

@ -0,0 +1,17 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.States;
/// <summary>AreaLoading state — loading screen info.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0xE58)]
public struct AreaLoading
{
/// <summary>Whether currently loading (int32 bool). Offset: 0x660.</summary>
[FieldOffset(0x660)] public int IsLoading;
/// <summary>Total time spent on loading screens (ms). Offset: 0xDB8.</summary>
[FieldOffset(0xDB8)] public long TotalLoadingScreenTimeMs;
/// <summary>Pointer to current area details. Offset: 0xE50.</summary>
[FieldOffset(0xE50)] public nint CurrentAreaDetailsPtr;
}

View file

@ -0,0 +1,24 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.States;
/// <summary>Important UI element pointers within InGameState.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x740)]
public struct ImportantUiElements
{
/// <summary>Chat parent UI element pointer.</summary>
[FieldOffset(0x570)] public nint ChatParentPtr;
/// <summary>Passive skill tree UI element pointer.</summary>
[FieldOffset(0x690)] public nint PassiveSkillTreePtr;
/// <summary>Map parent struct.</summary>
[FieldOffset(0x738)] public MapParentStruct MapParent;
}
/// <summary>Map parent — contains minimap and large map references.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct MapParentStruct
{
public nint MapParentPtr;
}

View file

@ -0,0 +1,28 @@
using System.Runtime.InteropServices;
namespace Roboto.GameOffsets.States;
/// <summary>InGameState — the main in-game state containing all sub-structures.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x310)]
public struct InGameState
{
/// <summary>Escape state flag: 0=closed, 1=open. Confirmed: 0x20C.</summary>
[FieldOffset(0x20C)] public int EscapeStateFlag;
/// <summary>Pointer to AreaInstance (IngameData). Confirmed: 0x290.</summary>
[FieldOffset(0x290)] public nint AreaInstanceDataPtr;
/// <summary>Pointer to WorldData. Offset: 0x2F8.</summary>
[FieldOffset(0x2F8)] public nint WorldDataPtr;
/// <summary>Pointer to Camera struct. Confirmed: 0x308.</summary>
[FieldOffset(0x308)] public nint CameraPtr;
}
/// <summary>UI root structure reference.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct UiRootStruct
{
[FieldOffset(0x00)] public nint UiRootPtr;
[FieldOffset(0x08)] public nint Unknown;
}

View file

@ -0,0 +1,48 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.States;
/// <summary>Pre-inventory wrapper — contains the actual inventory pointer.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x10)]
public struct PreInventory
{
[FieldOffset(0x00)] public nint InventoryPtr;
}
/// <summary>Inventory — item grid with hash-based item lookup.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x188)]
public struct InventoryStruct
{
/// <summary>Total number of grid boxes (width * height).</summary>
[FieldOffset(0x14C)] public int TotalBoxes;
/// <summary>Item list — StdBucket array for hash-based lookup.</summary>
[FieldOffset(0x170)] public StdVector ItemList;
}
/// <summary>Key in the inventory item hash map.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ItemHashKey
{
public int GridX;
public int GridY;
}
/// <summary>Value in the inventory item hash map.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct ItemHashValue
{
public nint ItemPtr;
}
/// <summary>An item entry in an inventory slot.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x18)]
public struct InventoryItem
{
[FieldOffset(0x00)] public nint EntityPtr;
[FieldOffset(0x08)] public int GridX;
[FieldOffset(0x0C)] public int GridY;
[FieldOffset(0x10)] public int Width;
[FieldOffset(0x14)] public int Height;
}

View file

@ -0,0 +1,29 @@
using System.Runtime.InteropServices;
using Roboto.GameOffsets.Natives;
namespace Roboto.GameOffsets.States;
/// <summary>ServerData — player server-side data.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x58)]
public struct ServerData
{
/// <summary>Pointer to the player's ServerDataStructure.</summary>
[FieldOffset(0x50)] public nint PlayerServerDataPtr;
}
/// <summary>Detailed server data for the player.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x338)]
public struct ServerDataStructure
{
/// <summary>Player inventories array StdVector.</summary>
[FieldOffset(0x320)] public StdVector PlayerInventories;
}
/// <summary>Entry in the player inventories array.</summary>
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct InventoryArrayStruct
{
public int InventoryId;
public int Unknown;
public nint InventoryPtr;
}

View file

@ -0,0 +1,33 @@
using System.Runtime.InteropServices;
using System.Numerics;
namespace Roboto.GameOffsets.States;
/// <summary>WorldData — world area details and camera.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0xA8)]
public struct WorldData
{
/// <summary>Pointer to WorldAreaDetails.</summary>
[FieldOffset(0x98)] public nint WorldAreaDetailsPtr;
/// <summary>Pointer to Camera struct.</summary>
[FieldOffset(0xA0)] public nint CameraPtr;
}
/// <summary>Details about the current world area (act, waypoint, etc.).</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x20)]
public struct WorldAreaDetails
{
[FieldOffset(0x00)] public nint NamePtr;
[FieldOffset(0x08)] public nint ActPtr;
[FieldOffset(0x10)] public int IsTown;
[FieldOffset(0x14)] public int HasWaypoint;
}
/// <summary>Camera structure — contains the world-to-screen projection matrix.</summary>
[StructLayout(LayoutKind.Explicit, Size = 0x1E0)]
public struct CameraStructure
{
/// <summary>4x4 world-to-screen projection matrix (64 bytes). Confirmed offset: 0x1A0 from camera base.</summary>
[FieldOffset(0x1A0)] public Matrix4x4 WorldToScreenMatrix;
}

View file

@ -1,7 +1,7 @@
using System.Text;
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
/// <summary>
/// Reads entity components via ECS: component list discovery, vitals, position, component lookup.
@ -16,6 +16,12 @@ public sealed class ComponentReader
private int _cachedRenderIndex = -1;
private nint _lastLocalPlayer;
/// <summary>Last resolved Render component pointer — used for fast per-frame position reads.</summary>
public nint CachedRenderComponentAddr { get; private set; }
/// <summary>Last resolved Life component pointer — used for fast per-frame vitals reads.</summary>
public nint CachedLifeComponentAddr { get; private set; }
public ComponentReader(MemoryContext ctx, MsvcStringReader strings)
{
_ctx = ctx;
@ -188,6 +194,7 @@ public sealed class ComponentReader
snap.ManaTotal = manaMax;
snap.EsCurrent = es;
snap.EsTotal = esMax;
CachedLifeComponentAddr = lifeComp;
return true;
}
@ -265,6 +272,7 @@ public sealed class ComponentReader
snap.PlayerX = x;
snap.PlayerY = y;
snap.PlayerZ = z;
CachedRenderComponentAddr = renderComp;
return true;
}

View file

@ -1,4 +1,4 @@
namespace Automata.Memory;
namespace Roboto.Memory;
public enum EntityType
{

View file

@ -1,6 +1,6 @@
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
/// <summary>
/// Reads entity list from AreaInstance's std::map red-black tree.

View file

@ -1,7 +1,7 @@
using System.Numerics;
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
public class GameMemoryReader : IDisposable
{
@ -29,6 +29,9 @@ public class GameMemoryReader : IDisposable
// Sub-readers (created on Attach)
private MemoryContext? _ctx;
private GameStateReader? _stateReader;
private nint _cachedCameraMatrixAddr;
private nint _lastInGameState;
private nint _lastController;
private ComponentReader? _components;
private EntityReader? _entities;
private TerrainReader? _terrain;
@ -37,6 +40,9 @@ public class GameMemoryReader : IDisposable
public ObjectRegistry Registry => _registry;
public MemoryDiagnostics? Diagnostics { get; private set; }
public MemoryContext? Context => _ctx;
public ComponentReader? Components => _components;
public GameStateReader? StateReader => _stateReader;
public GameMemoryReader()
{
@ -144,6 +150,8 @@ public class GameMemoryReader : IDisposable
if (inGameState == 0)
return snap;
snap.InGameStatePtr = inGameState;
_lastInGameState = inGameState;
_lastController = snap.ControllerPtr;
// Read all state slot pointers
_stateReader.ReadStateSlots(snap);
@ -247,6 +255,9 @@ public class GameMemoryReader : IDisposable
matrixAddr = inGameState + offsets.CameraMatrixOffset;
}
// Cache the resolved address for fast per-frame reads
_cachedCameraMatrixAddr = matrixAddr;
// Read 64-byte Matrix4x4 as 16 floats
var bytes = mem.ReadBytes(matrixAddr, 64);
if (bytes is null || bytes.Length < 64) return;
@ -263,6 +274,43 @@ public class GameMemoryReader : IDisposable
snap.CameraMatrix = m;
}
/// <summary>
/// Resolved addresses for hot-path reads (camera, player position, player vitals, InGameState).
/// </summary>
public readonly struct HotAddresses
{
public readonly nint CameraMatrixAddr;
public readonly nint PlayerRenderAddr;
public readonly nint PlayerLifeAddr;
public readonly nint InGameStateAddr;
public readonly nint ControllerAddr;
public readonly bool IsValid;
public HotAddresses(nint cameraMatrix, nint playerRender, nint playerLife, nint inGameState, nint controller)
{
CameraMatrixAddr = cameraMatrix;
PlayerRenderAddr = playerRender;
PlayerLifeAddr = playerLife;
InGameStateAddr = inGameState;
ControllerAddr = controller;
IsValid = cameraMatrix != 0 || playerRender != 0;
}
}
/// <summary>
/// Returns resolved addresses for the hot path.
/// Call after ReadSnapshot() has populated the cached addresses.
/// </summary>
public HotAddresses ResolveHotAddresses()
{
return new HotAddresses(
_cachedCameraMatrixAddr,
_components?.CachedRenderComponentAddr ?? 0,
_components?.CachedLifeComponentAddr ?? 0,
_lastInGameState,
_lastController);
}
public void Dispose()
{
if (_disposed) return;

View file

@ -2,7 +2,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
public sealed class GameOffsets
{

View file

@ -1,6 +1,6 @@
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
/// <summary>
/// Resolves GameState → Controller → InGameState, reads state slots, loading/escape state.

View file

@ -1,6 +1,6 @@
using System.Numerics;
namespace Automata.Memory;
namespace Roboto.Memory;
public class GameStateSnapshot
{

View file

@ -1,4 +1,4 @@
namespace Automata.Memory;
namespace Roboto.Memory;
/// <summary>
/// Shared state for all memory reader classes. Holds the process handle, offsets, registry,

View file

@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
using System.Text;
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
/// <summary>
/// Diagnostic and scan methods extracted from GameMemoryReader.

View file

@ -1,6 +1,6 @@
using System.Text;
namespace Automata.Memory;
namespace Roboto.Memory;
/// <summary>
/// Reads MSVC std::string and std::wstring from process memory.

View file

@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
namespace Automata.Memory;
namespace Roboto.Memory;
internal static partial class Native
{

View file

@ -1,7 +1,7 @@
using System.Text.Json;
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
/// <summary>
/// Persistent registry of discovered strings, organized by category.

View file

@ -1,6 +1,6 @@
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
public sealed class PatternScanner
{

View file

@ -2,7 +2,7 @@ using System.Diagnostics;
using System.Runtime.CompilerServices;
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
public sealed class ProcessMemory : IDisposable
{

View file

@ -10,5 +10,6 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
<ProjectReference Include="..\Roboto.GameOffsets\Roboto.GameOffsets.csproj" />
</ItemGroup>
</Project>

View file

@ -1,6 +1,6 @@
using System.Text;
namespace Automata.Memory;
namespace Roboto.Memory;
/// <summary>
/// Resolves MSVC x64 RTTI type names from vtable addresses and classifies pointers.

View file

@ -1,6 +1,6 @@
using Serilog;
namespace Automata.Memory;
namespace Roboto.Memory;
/// <summary>
/// Reads terrain walkability grid from AreaInstance, with caching and loading edge detection.

View file

@ -1,4 +1,4 @@
namespace Automata.Memory;
namespace Roboto.Memory;
public sealed class WalkabilityGrid
{