diff --git a/Automata.sln b/Automata.sln
index a7d1e4f..26c298e 100644
--- a/Automata.sln
+++ b/Automata.sln
@@ -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}
diff --git a/entities.json b/entities.json
index cc306b9..961d3fd 100644
--- a/entities.json
+++ b/entities.json
@@ -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",
diff --git a/src/Automata.Ui/App.axaml b/src/Automata.Ui/App.axaml
index 2f31dc6..a77fa7a 100644
--- a/src/Automata.Ui/App.axaml
+++ b/src/Automata.Ui/App.axaml
@@ -16,5 +16,8 @@
+
+
+
diff --git a/src/Automata.Ui/App.axaml.cs b/src/Automata.Ui/App.axaml.cs
index f370d39..92a293f 100644
--- a/src/Automata.Ui/App.axaml.cs
+++ b/src/Automata.Ui/App.axaml.cs
@@ -71,6 +71,7 @@ public partial class App : Application
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+ services.AddSingleton();
var provider = services.BuildServiceProvider();
@@ -97,6 +98,7 @@ public partial class App : Application
mainVm.CraftingVm = provider.GetRequiredService();
mainVm.MemoryVm = provider.GetRequiredService();
mainVm.RobotoVm = provider.GetRequiredService();
+ mainVm.BrowserVm = provider.GetRequiredService();
var window = new MainWindow { DataContext = mainVm };
window.SetConfigStore(store);
diff --git a/src/Automata.Ui/Automata.Ui.csproj b/src/Automata.Ui/Automata.Ui.csproj
index 5c4965d..3c9e2b8 100644
--- a/src/Automata.Ui/Automata.Ui.csproj
+++ b/src/Automata.Ui/Automata.Ui.csproj
@@ -23,7 +23,8 @@
-
+
+
diff --git a/src/Automata.Ui/Converters/ValueConverters.cs b/src/Automata.Ui/Converters/ValueConverters.cs
index ac693be..7550d71 100644
--- a/src/Automata.Ui/Converters/ValueConverters.cs
+++ b/src/Automata.Ui/Converters/ValueConverters.cs
@@ -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();
+}
diff --git a/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs b/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs
index 18bec05..878fac5 100644
--- a/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs
+++ b/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs
@@ -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;
diff --git a/src/Automata.Ui/ViewModels/MainWindowViewModel.cs b/src/Automata.Ui/ViewModels/MainWindowViewModel.cs
index d556faf..f75d7f5 100644
--- a/src/Automata.Ui/ViewModels/MainWindowViewModel.cs
+++ b/src/Automata.Ui/ViewModels/MainWindowViewModel.cs
@@ -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)
{
diff --git a/src/Automata.Ui/ViewModels/MemoryViewModel.cs b/src/Automata.Ui/ViewModels/MemoryViewModel.cs
index 91bca0f..3dec42c 100644
--- a/src/Automata.Ui/ViewModels/MemoryViewModel.cs
+++ b/src/Automata.Ui/ViewModels/MemoryViewModel.cs
@@ -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");
diff --git a/src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs b/src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs
new file mode 100644
index 0000000..778ebc2
--- /dev/null
+++ b/src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs
@@ -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; }
+
+ /// Number of 8-byte rows this field spans (1 for most, 4 for String).
+ public int RowSpan { get; set; } = 1;
+}
+
+public partial class ObjectBrowserViewModel : ObservableObject
+{
+ private GameMemoryReader? _reader;
+ private readonly Stack _navStack = new();
+ private Dictionary _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 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();
+ 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 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();
+ var skip = new HashSet(); // 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();
+ 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();
+ 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();
+ 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;
+ }
+
+ ///
+ /// Builds a map of offset → label name for the given object type, based on known GameOffsets.
+ ///
+ private static Dictionary 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",
+ },
+ _ => []
+ };
+ }
+}
diff --git a/src/Automata.Ui/ViewModels/RobotoViewModel.cs b/src/Automata.Ui/ViewModels/RobotoViewModel.cs
index d2def47..d310c12 100644
--- a/src/Automata.Ui/ViewModels/RobotoViewModel.cs
+++ b/src/Automata.Ui/ViewModels/RobotoViewModel.cs
@@ -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;
///
public sealed class EntityOverlayData
{
- public Vector2 PlayerPosition;
- public float PlayerZ;
- public Matrix4x4? CameraMatrix;
+ /// Player position at time the entity list was built (cold tick, 10Hz).
+ public Vector2 SnapshotPlayerPosition;
+ public float SnapshotPlayerZ;
public EntityOverlayEntry[] Entries = [];
}
@@ -96,14 +98,19 @@ public partial class RobotoViewModel : ObservableObject, IDisposable
///
public static volatile EntityOverlayData? OverlayData;
+ ///
+ /// Shared GameDataCache for the overlay layer to read camera/player data directly.
+ ///
+ 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(),
};
}
diff --git a/src/Automata.Ui/Views/MainWindow.axaml b/src/Automata.Ui/Views/MainWindow.axaml
index b4c1ec5..7f1b152 100644
--- a/src/Automata.Ui/Views/MainWindow.axaml
+++ b/src/Automata.Ui/Views/MainWindow.axaml
@@ -1034,6 +1034,112 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/// Immutable snapshot of player position for lock-free cross-thread reads.
+/// Class (reference type) so volatile assignment is atomic.
+///
+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();
+}
+
+///
+/// Immutable snapshot of player vitals for lock-free cross-thread reads.
+/// Class (reference type) so volatile assignment is atomic.
+///
+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();
+}
+
+///
+/// Immutable wrapper for Matrix4x4 so volatile reference assignment works.
+///
+public sealed class CameraMatrixData
+{
+ public readonly Matrix4x4 Matrix;
+
+ public CameraMatrixData(Matrix4x4 matrix)
+ {
+ Matrix = matrix;
+ }
+}
+
+///
+/// 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.
+///
+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 Entities = [];
+ public volatile IReadOnlyList HostileMonsters = [];
+ public volatile IReadOnlyList 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);
+ }
+}
diff --git a/src/Roboto.Data/MemoryPoller.cs b/src/Roboto.Data/MemoryPoller.cs
new file mode 100644
index 0000000..0795316
--- /dev/null
+++ b/src/Roboto.Data/MemoryPoller.cs
@@ -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;
+
+///
+/// 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
+///
+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);
+ }
+ }
+
+ ///
+ /// Full snapshot: read everything, build GameState, update cache cold + hot fields, re-resolve addresses.
+ ///
+ 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;
+ }
+
+ ///
+ /// Hot tick: minimal reads (4 RPM calls) using pre-resolved addresses.
+ ///
+ 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(_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(snap.Entities.Count);
+ var hostiles = new List();
+ var loot = new List();
+
+ 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();
+ }
+}
diff --git a/src/Roboto.Data/Roboto.Data.csproj b/src/Roboto.Data/Roboto.Data.csproj
index bcebb88..38208e6 100644
--- a/src/Roboto.Data/Roboto.Data.csproj
+++ b/src/Roboto.Data/Roboto.Data.csproj
@@ -9,5 +9,6 @@
+
diff --git a/src/Roboto.GameOffsets/Components/Actor.cs b/src/Roboto.GameOffsets/Components/Actor.cs
new file mode 100644
index 0000000..ecb15f2
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Actor.cs
@@ -0,0 +1,68 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Actor component — skills, animations, deployments.
+[StructLayout(LayoutKind.Explicit, Size = 0x2E8)]
+public struct Actor
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Pointer to animation controller.
+ [FieldOffset(0x1D8)] public nint AnimationControllerPtr;
+
+ /// Active skills StdVector (of ActiveSkillStructure).
+ [FieldOffset(0x2C0)] public StdVector ActiveSkills;
+
+ /// Deployed entities StdVector (of DeployedEntityStructure).
+ [FieldOffset(0x2D8)] public StdVector DeployedEntities;
+}
+
+/// An entry in the active skills vector.
+[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;
+}
+
+/// Detailed info about a skill.
+[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;
+}
+
+/// Cooldown state for a skill.
+[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;
+}
+
+/// Vaal soul tracking.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct VaalSoulStructure
+{
+ public nint GrantedEffectsPtr;
+ public int CurrentSouls;
+ public int SoulCost;
+}
+
+/// A deployed entity (totem, mine, etc.).
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct DeployedEntityStructure
+{
+ public uint EntityId;
+ public int SkillIndex;
+}
diff --git a/src/Roboto.GameOffsets/Components/Animated.cs b/src/Roboto.GameOffsets/Components/Animated.cs
new file mode 100644
index 0000000..3766d1a
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Animated.cs
@@ -0,0 +1,13 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Animated component — reference to the animated entity.
+[StructLayout(LayoutKind.Explicit, Size = 0x300)]
+public struct Animated
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Pointer to the animated entity object.
+ [FieldOffset(0x2F8)] public nint AnimatedEntityPtr;
+}
diff --git a/src/Roboto.GameOffsets/Components/Buffs.cs b/src/Roboto.GameOffsets/Components/Buffs.cs
new file mode 100644
index 0000000..761392d
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Buffs.cs
@@ -0,0 +1,25 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Buffs component — active status effects.
+[StructLayout(LayoutKind.Explicit, Size = 0x178)]
+public struct Buffs
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// StdVector of StatusEffectStruct pointers.
+ [FieldOffset(0x160)] public StdVector StatusEffects;
+}
+
+/// A single status effect (buff/debuff).
+[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;
+}
diff --git a/src/Roboto.GameOffsets/Components/Charges.cs b/src/Roboto.GameOffsets/Components/Charges.cs
new file mode 100644
index 0000000..c2231b2
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Charges.cs
@@ -0,0 +1,23 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Charges component — flask/skill charges.
+[StructLayout(LayoutKind.Explicit, Size = 0x20)]
+public struct Charges
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Pointer to ChargesInternal struct.
+ [FieldOffset(0x10)] public nint ChargesInternalPtr;
+
+ /// Current charge count.
+ [FieldOffset(0x18)] public int Current;
+}
+
+/// Internal charges detail.
+[StructLayout(LayoutKind.Explicit, Size = 0x20)]
+public struct ChargesInternal
+{
+ [FieldOffset(0x18)] public int PerUseCharges;
+}
diff --git a/src/Roboto.GameOffsets/Components/Chest.cs b/src/Roboto.GameOffsets/Components/Chest.cs
new file mode 100644
index 0000000..b86d397
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Chest.cs
@@ -0,0 +1,24 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Chest component.
+[StructLayout(LayoutKind.Explicit, Size = 0x170)]
+public struct Chest
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Pointer to ChestInternal data.
+ [FieldOffset(0x160)] public nint ChestsDataPtr;
+
+ /// Whether the chest has been opened (byte bool).
+ [FieldOffset(0x168)] public byte IsOpened;
+}
+
+/// Internal chest data.
+[StructLayout(LayoutKind.Explicit, Size = 0x28)]
+public struct ChestInternal
+{
+ /// Whether the chest label is visible (byte bool).
+ [FieldOffset(0x21)] public byte IsLabelVisible;
+}
diff --git a/src/Roboto.GameOffsets/Components/ComponentHeader.cs b/src/Roboto.GameOffsets/Components/ComponentHeader.cs
new file mode 100644
index 0000000..f0f6910
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/ComponentHeader.cs
@@ -0,0 +1,11 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Common header at the start of every component.
+[StructLayout(LayoutKind.Explicit, Size = 0x10)]
+public struct ComponentHeader
+{
+ [FieldOffset(0x00)] public nint StaticPtr;
+ [FieldOffset(0x08)] public nint EntityPtr;
+}
diff --git a/src/Roboto.GameOffsets/Components/Life.cs b/src/Roboto.GameOffsets/Components/Life.cs
new file mode 100644
index 0000000..f85275b
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Life.cs
@@ -0,0 +1,27 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Life component — contains Health, Mana, and ES vitals.
+[StructLayout(LayoutKind.Explicit, Size = 0x268)]
+public struct Life
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Health vital block.
+ [FieldOffset(0x1A8)] public VitalStruct Health;
+
+ /// Mana vital block.
+ [FieldOffset(0x1F8)] public VitalStruct Mana;
+
+ /// Energy Shield vital block.
+ [FieldOffset(0x230)] public VitalStruct EnergyShield;
+}
+
+/// A single vital (HP/Mana/ES) with current and total values.
+[StructLayout(LayoutKind.Explicit, Size = 0x38)]
+public struct VitalStruct
+{
+ [FieldOffset(0x2C)] public int Total;
+ [FieldOffset(0x30)] public int Current;
+}
diff --git a/src/Roboto.GameOffsets/Components/Mods.cs b/src/Roboto.GameOffsets/Components/Mods.cs
new file mode 100644
index 0000000..39d7cc9
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Mods.cs
@@ -0,0 +1,59 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Mods component — item rarity, explicit/implicit mods.
+[StructLayout(LayoutKind.Explicit, Size = 0x1A0)]
+public struct Mods
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Pointer to ObjectMagicProperties.
+ [FieldOffset(0x98)] public nint ObjectMagicPropertiesPtr;
+
+ /// Pointer to AllModsType struct.
+ [FieldOffset(0xA0)] public nint AllModsPtr;
+}
+
+/// Magic properties of an item (rarity, etc.).
+[StructLayout(LayoutKind.Explicit, Size = 0xA0)]
+public struct ObjectMagicProperties
+{
+ /// Item rarity: 0=Normal, 1=Magic, 2=Rare, 3=Unique.
+ [FieldOffset(0x94)] public int Rarity;
+}
+
+/// Combined mods and magic properties.
+[StructLayout(LayoutKind.Explicit, Size = 0x150)]
+public struct ModsAndObjectMagicProperties
+{
+ [FieldOffset(0x00)] public nint ModsPtr;
+ [FieldOffset(0x08)] public nint ObjectMagicPropertiesPtr;
+}
+
+/// All mod arrays (implicit, explicit, enchant, etc.).
+[StructLayout(LayoutKind.Explicit, Size = 0x150)]
+public struct AllModsType
+{
+ /// Implicit mods StdVector.
+ [FieldOffset(0x00)] public StdVector ImplicitMods;
+
+ /// Explicit mods StdVector.
+ [FieldOffset(0x18)] public StdVector ExplicitMods;
+
+ /// Enchant mods StdVector.
+ [FieldOffset(0x30)] public StdVector EnchantMods;
+
+ /// Stats from mods StdVector.
+ [FieldOffset(0x148)] public StdVector StatsFromMods;
+}
+
+/// A single mod entry.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct ModArrayStruct
+{
+ public nint ModPtr;
+ public int Level;
+ public int Unknown;
+}
diff --git a/src/Roboto.GameOffsets/Components/Player.cs b/src/Roboto.GameOffsets/Components/Player.cs
new file mode 100644
index 0000000..7fa7af5
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Player.cs
@@ -0,0 +1,20 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Player component — name, XP, level.
+[StructLayout(LayoutKind.Explicit, Size = 0x208)]
+public struct Player
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Player name (MSVC std::wstring with SSO).
+ [FieldOffset(0x1B0)] public StdWString Name;
+
+ /// Total experience.
+ [FieldOffset(0x1D8)] public long Xp;
+
+ /// Character level.
+ [FieldOffset(0x204)] public int Level;
+}
diff --git a/src/Roboto.GameOffsets/Components/Positioned.cs b/src/Roboto.GameOffsets/Components/Positioned.cs
new file mode 100644
index 0000000..be35bda
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Positioned.cs
@@ -0,0 +1,13 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Positioned component — reaction (friendly/hostile/neutral).
+[StructLayout(LayoutKind.Explicit, Size = 0x1E8)]
+public struct Positioned
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Reaction type: 1=Friendly, 2=Hostile, etc.
+ [FieldOffset(0x1E0)] public int Reaction;
+}
diff --git a/src/Roboto.GameOffsets/Components/Render.cs b/src/Roboto.GameOffsets/Components/Render.cs
new file mode 100644
index 0000000..57557d3
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Render.cs
@@ -0,0 +1,23 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Render component — world position, bounds, terrain height.
+[StructLayout(LayoutKind.Explicit, Size = 0x148)]
+public struct Render
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Current world position (float x, y, z).
+ [FieldOffset(0xB8)] public StdTuple3D CurrentWorldPosition;
+
+ /// Character model bounds (float x, y, z).
+ [FieldOffset(0xC4)] public StdTuple3D CharacterModelBounds;
+
+ /// Terrain height at entity position.
+ [FieldOffset(0x130)] public float TerrainHeight;
+
+ /// Grid position (float x, y, z). Confirmed offsets: X=0x138, Y=0x13C, Z=0x140.
+ [FieldOffset(0x138)] public StdTuple3D Position;
+}
diff --git a/src/Roboto.GameOffsets/Components/Shrine.cs b/src/Roboto.GameOffsets/Components/Shrine.cs
new file mode 100644
index 0000000..680efe4
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Shrine.cs
@@ -0,0 +1,13 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Shrine component.
+[StructLayout(LayoutKind.Explicit, Size = 0x28)]
+public struct Shrine
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Whether the shrine has been used (byte bool).
+ [FieldOffset(0x24)] public byte IsUsed;
+}
diff --git a/src/Roboto.GameOffsets/Components/StateMachine.cs b/src/Roboto.GameOffsets/Components/StateMachine.cs
new file mode 100644
index 0000000..d912bf7
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/StateMachine.cs
@@ -0,0 +1,17 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.Components;
+
+/// StateMachine component — entity state management.
+[StructLayout(LayoutKind.Explicit, Size = 0x178)]
+public struct StateMachine
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Pointer to states map/array.
+ [FieldOffset(0x158)] public nint StatesPtr;
+
+ /// StdVector of state values.
+ [FieldOffset(0x160)] public StdVector StatesValues;
+}
diff --git a/src/Roboto.GameOffsets/Components/Stats.cs b/src/Roboto.GameOffsets/Components/Stats.cs
new file mode 100644
index 0000000..5f74b43
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Stats.cs
@@ -0,0 +1,32 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Stats component — item stats, weapon index, shapeshift.
+[StructLayout(LayoutKind.Explicit, Size = 0x180)]
+public struct Stats
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Stats changed by items StdVector.
+ [FieldOffset(0x160)] public StdVector StatsChangedByItems;
+
+ /// Current weapon index.
+ [FieldOffset(0x178)] public int WeaponIndex;
+}
+
+/// Internal stats storage.
+[StructLayout(LayoutKind.Explicit, Size = 0x20)]
+public struct StatsInternal
+{
+ [FieldOffset(0x00)] public StdVector StatArray;
+}
+
+/// A single stat entry (key/value pair).
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct StatArrayStruct
+{
+ public int StatId;
+ public int Value;
+}
diff --git a/src/Roboto.GameOffsets/Components/Targetable.cs b/src/Roboto.GameOffsets/Components/Targetable.cs
new file mode 100644
index 0000000..16fada1
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Targetable.cs
@@ -0,0 +1,19 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Targetable component — whether entity can be targeted/highlighted.
+[StructLayout(LayoutKind.Explicit, Size = 0x58)]
+public struct Targetable
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Whether the entity is targetable (byte bool).
+ [FieldOffset(0x51)] public byte IsTargetable;
+
+ /// Whether the entity is highlightable (byte bool).
+ [FieldOffset(0x52)] public byte IsHighlightable;
+
+ /// Whether the entity is targetable through walls (byte bool).
+ [FieldOffset(0x53)] public byte IsTargetableThroughWalls;
+}
diff --git a/src/Roboto.GameOffsets/Components/Transitionable.cs b/src/Roboto.GameOffsets/Components/Transitionable.cs
new file mode 100644
index 0000000..25ec8d9
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/Transitionable.cs
@@ -0,0 +1,13 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// Transitionable component — area transition state.
+[StructLayout(LayoutKind.Explicit, Size = 0x128)]
+public struct Transitionable
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Current state enum value.
+ [FieldOffset(0x120)] public int CurrentStateEnum;
+}
diff --git a/src/Roboto.GameOffsets/Components/TriggerableBlockage.cs b/src/Roboto.GameOffsets/Components/TriggerableBlockage.cs
new file mode 100644
index 0000000..4268448
--- /dev/null
+++ b/src/Roboto.GameOffsets/Components/TriggerableBlockage.cs
@@ -0,0 +1,13 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Components;
+
+/// TriggerableBlockage component — door/gate blocked state.
+[StructLayout(LayoutKind.Explicit, Size = 0x38)]
+public struct TriggerableBlockage
+{
+ [FieldOffset(0x00)] public ComponentHeader Header;
+
+ /// Whether the blockage is currently blocking (byte bool).
+ [FieldOffset(0x30)] public byte IsBlocked;
+}
diff --git a/src/Roboto.GameOffsets/Entities/Entity.cs b/src/Roboto.GameOffsets/Entities/Entity.cs
new file mode 100644
index 0000000..c6b7a00
--- /dev/null
+++ b/src/Roboto.GameOffsets/Entities/Entity.cs
@@ -0,0 +1,64 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.Entities;
+
+/// Item struct — wraps an entity pointer for inventory items.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct ItemStruct
+{
+ public nint EntityPtr;
+}
+
+/// Top-level entity struct in memory.
+[StructLayout(LayoutKind.Explicit, Size = 0x88)]
+public struct EntityStruct
+{
+ /// VTable pointer.
+ [FieldOffset(0x00)] public nint VTable;
+
+ /// Pointer to EntityDetails (path, component lookup).
+ [FieldOffset(0x08)] public nint EntityDetailsPtr;
+
+ /// Component list (StdVector of component pointers).
+ [FieldOffset(0x10)] public StdVector ComponentList;
+
+ /// Entity ID (unique within area instance).
+ [FieldOffset(0x80)] public uint Id;
+
+ /// Entity validity/flags byte.
+ [FieldOffset(0x84)] public byte IsValid;
+}
+
+/// Entity details — path string and component lookup.
+[StructLayout(LayoutKind.Explicit, Size = 0x30)]
+public struct EntityDetails
+{
+ /// MSVC std::string for entity path (e.g., "Metadata/Monsters/...").
+ [FieldOffset(0x08)] public StdWString Path;
+
+ /// Pointer to ComponentLookup object.
+ [FieldOffset(0x28)] public nint ComponentLookupPtr;
+}
+
+/// Component lookup — maps component names to indices.
+[StructLayout(LayoutKind.Explicit, Size = 0x40)]
+public struct ComponentLookup
+{
+ /// StdVector of ComponentNameAndIndex entries.
+ [FieldOffset(0x28)] public StdVector NameEntries;
+}
+
+/// A single entry in the component name→index lookup.
+[StructLayout(LayoutKind.Explicit, Size = 0x10)]
+public struct ComponentNameAndIndex
+{
+ /// Pointer to null-terminated component name string (char*).
+ [FieldOffset(0x00)] public nint NamePtr;
+
+ /// Index into the entity's component list StdVector.
+ [FieldOffset(0x08)] public int Index;
+
+ /// Flags (purpose TBD).
+ [FieldOffset(0x0C)] public int Flags;
+}
diff --git a/src/Roboto.GameOffsets/Entities/EntityTreeNode.cs b/src/Roboto.GameOffsets/Entities/EntityTreeNode.cs
new file mode 100644
index 0000000..9b9e0cd
--- /dev/null
+++ b/src/Roboto.GameOffsets/Entities/EntityTreeNode.cs
@@ -0,0 +1,42 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Entities;
+
+/// MSVC std::map tree node for entity storage. Red-black tree node layout.
+[StructLayout(LayoutKind.Explicit, Size = 0x30)]
+public struct EntityTreeNode
+{
+ /// Left child node pointer.
+ [FieldOffset(0x00)] public nint Left;
+
+ /// Parent node pointer.
+ [FieldOffset(0x08)] public nint Parent;
+
+ /// Right child node pointer.
+ [FieldOffset(0x10)] public nint Right;
+
+ /// Node color (0=red, 1=black) and nil flag.
+ [FieldOffset(0x19)] public byte IsNil;
+
+ /// Entity key/value data.
+ [FieldOffset(0x20)] public EntityNodeValue Data;
+}
+
+/// Key used in the entity map (entity ID).
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct EntityNodeKey
+{
+ public uint EntityId;
+ public uint Padding;
+}
+
+/// Value stored in entity tree node — key + entity pointer.
+[StructLayout(LayoutKind.Explicit, Size = 0x10)]
+public struct EntityNodeValue
+{
+ /// Entity key (ID).
+ [FieldOffset(0x00)] public EntityNodeKey Key;
+
+ /// Pointer to entity struct (at tree node +0x28).
+ [FieldOffset(0x08)] public nint EntityPtr;
+}
diff --git a/src/Roboto.GameOffsets/Natives/StdBucket.cs b/src/Roboto.GameOffsets/Natives/StdBucket.cs
new file mode 100644
index 0000000..94f2b84
--- /dev/null
+++ b/src/Roboto.GameOffsets/Natives/StdBucket.cs
@@ -0,0 +1,11 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Natives;
+
+/// Bucket structure used in hash containers.
+[StructLayout(LayoutKind.Explicit, Size = 0x20)]
+public struct StdBucket
+{
+ [FieldOffset(0x00)] public StdVector Data;
+ [FieldOffset(0x18)] public long Unknown;
+}
diff --git a/src/Roboto.GameOffsets/Natives/StdList.cs b/src/Roboto.GameOffsets/Natives/StdList.cs
new file mode 100644
index 0000000..a1284c7
--- /dev/null
+++ b/src/Roboto.GameOffsets/Natives/StdList.cs
@@ -0,0 +1,28 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Natives;
+
+/// MSVC std::list layout: head node pointer + size.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct StdList
+{
+ public nint Head;
+ public long Size;
+}
+
+/// MSVC std::list node with typed data.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct StdListNode where T : unmanaged
+{
+ public nint Next;
+ public nint Prev;
+ public T Data;
+}
+
+/// MSVC std::list node (untyped, pointers only).
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct StdListNode
+{
+ public nint Next;
+ public nint Prev;
+}
diff --git a/src/Roboto.GameOffsets/Natives/StdMap.cs b/src/Roboto.GameOffsets/Natives/StdMap.cs
new file mode 100644
index 0000000..6195ad1
--- /dev/null
+++ b/src/Roboto.GameOffsets/Natives/StdMap.cs
@@ -0,0 +1,37 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Natives;
+
+/// MSVC std::map layout: head node pointer + size.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct StdMap
+{
+ public nint Head;
+ public long Size;
+}
+
+/// MSVC std::map tree node with typed key/value data.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct StdMapNode
+ 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 Data;
+}
+
+/// Key/value pair stored in a std::map node.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct StdMapNodeData
+ where TKey : unmanaged
+ where TValue : unmanaged
+{
+ public TKey Key;
+ public TValue Value;
+}
diff --git a/src/Roboto.GameOffsets/Natives/StdTuple.cs b/src/Roboto.GameOffsets/Natives/StdTuple.cs
new file mode 100644
index 0000000..2258e00
--- /dev/null
+++ b/src/Roboto.GameOffsets/Natives/StdTuple.cs
@@ -0,0 +1,20 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Natives;
+
+/// 2D tuple (e.g., terrain dimensions).
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct StdTuple2D where T : unmanaged
+{
+ public T X;
+ public T Y;
+}
+
+/// 3D tuple (e.g., world positions).
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct StdTuple3D where T : unmanaged
+{
+ public T X;
+ public T Y;
+ public T Z;
+}
diff --git a/src/Roboto.GameOffsets/Natives/StdVector.cs b/src/Roboto.GameOffsets/Natives/StdVector.cs
new file mode 100644
index 0000000..beeb591
--- /dev/null
+++ b/src/Roboto.GameOffsets/Natives/StdVector.cs
@@ -0,0 +1,15 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Natives;
+
+/// MSVC std::vector layout: begin/end/capacity pointers.
+[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;
+}
diff --git a/src/Roboto.GameOffsets/Natives/StdWString.cs b/src/Roboto.GameOffsets/Natives/StdWString.cs
new file mode 100644
index 0000000..0587dfb
--- /dev/null
+++ b/src/Roboto.GameOffsets/Natives/StdWString.cs
@@ -0,0 +1,23 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.Natives;
+
+/// MSVC std::wstring (basic_string<wchar_t>) with SSO.
+[StructLayout(LayoutKind.Explicit, Size = 0x20)]
+public unsafe struct StdWString
+{
+ /// When non-inline: pointer to heap buffer. When inline: first 8 bytes of inline chars.
+ [FieldOffset(0x00)] public nint Buffer;
+
+ /// Raw inline storage (16 bytes = 8 wchars).
+ [FieldOffset(0x00)] public fixed byte ReservedBytes[16];
+
+ /// Number of wchar_t characters (not bytes).
+ [FieldOffset(0x10)] public long Length;
+
+ /// Capacity in wchar_t characters. If <= 7, string is inline (SSO).
+ [FieldOffset(0x18)] public long Capacity;
+
+ /// True if the string data is stored inline (SSO), false if on heap.
+ public readonly bool IsInline => Capacity <= 7;
+}
diff --git a/src/Roboto.GameOffsets/Natives/Util.cs b/src/Roboto.GameOffsets/Natives/Util.cs
new file mode 100644
index 0000000..e4972ca
--- /dev/null
+++ b/src/Roboto.GameOffsets/Natives/Util.cs
@@ -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;
+}
diff --git a/src/Roboto.GameOffsets/Roboto.GameOffsets.csproj b/src/Roboto.GameOffsets/Roboto.GameOffsets.csproj
new file mode 100644
index 0000000..5369201
--- /dev/null
+++ b/src/Roboto.GameOffsets/Roboto.GameOffsets.csproj
@@ -0,0 +1,8 @@
+
+
+ net8.0-windows10.0.19041.0
+ enable
+ enable
+ true
+
+
diff --git a/src/Roboto.GameOffsets/States/AreaInstance.cs b/src/Roboto.GameOffsets/States/AreaInstance.cs
new file mode 100644
index 0000000..6e02ac2
--- /dev/null
+++ b/src/Roboto.GameOffsets/States/AreaInstance.cs
@@ -0,0 +1,98 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.States;
+
+/// AreaInstance (IngameData) — current area data, entities, terrain.
+[StructLayout(LayoutKind.Explicit, Size = 0xCD0)]
+public struct AreaInstance
+{
+ /// Current area level (byte). Confirmed: 0xC4.
+ [FieldOffset(0xC4)] public byte AreaLevel;
+
+ /// Area hash (unique per area instance). Offset: 0xEC.
+ [FieldOffset(0xEC)] public uint AreaHash;
+
+ /// Pointer to ServerData. Offset: 0x9F0.
+ [FieldOffset(0x9F0)] public nint ServerDataPtr;
+
+ /// Local player struct (inline). Offset: 0xA10.
+ [FieldOffset(0xA10)] public LocalPlayerStruct LocalPlayer;
+
+ /// Entity list (std::map). Confirmed: 0xB50.
+ [FieldOffset(0xB50)] public EntityList EntityList;
+
+ /// Terrain data (inline). Offset: 0xCC0.
+ [FieldOffset(0xCC0)] public Terrain Terrain;
+}
+
+/// Local player reference within AreaInstance.
+[StructLayout(LayoutKind.Explicit, Size = 0x28)]
+public struct LocalPlayerStruct
+{
+ /// Pointer to ServerData.
+ [FieldOffset(0x00)] public nint ServerDataPtr;
+
+ /// Pointer to the local player entity.
+ [FieldOffset(0x20)] public nint LocalPlayerPtr;
+}
+
+/// Entity list — wraps a std::map of entities.
+[StructLayout(LayoutKind.Explicit, Size = 0x10)]
+public struct EntityList
+{
+ /// std::map head node pointer.
+ [FieldOffset(0x00)] public nint HeadPtr;
+
+ /// Entity count (std::map _Mysize).
+ [FieldOffset(0x08)] public long Count;
+}
+
+/// Terrain data — dimensions, walkability grid, sub-tile info.
+[StructLayout(LayoutKind.Explicit, Size = 0x1B0)]
+public struct Terrain
+{
+ /// Terrain dimensions in tiles (StdTuple2D of long). Offset: +0x90 from terrain start.
+ [FieldOffset(0x90)] public StdTuple2D Dimensions;
+
+ /// Walkable grid data StdVector. Offset: +0x148.
+ [FieldOffset(0x148)] public StdVector WalkableGrid;
+
+ /// Bytes per row in the walkability grid. Offset: +0x1A8.
+ [FieldOffset(0x1A8)] public int BytesPerRow;
+}
+
+/// Tile structure for terrain tiles.
+[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;
+}
+
+/// Sub-tile data within a terrain tile.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct SubTileStruct
+{
+ public int SubTileX;
+ public int SubTileY;
+}
+
+/// TGT file reference for terrain graphics.
+[StructLayout(LayoutKind.Explicit, Size = 0x18)]
+public struct TgtFileStruct
+{
+ [FieldOffset(0x00)] public nint NamePtr;
+ [FieldOffset(0x08)] public int TgtColumns;
+ [FieldOffset(0x0C)] public int TgtRows;
+}
+
+/// Environment data.
+[StructLayout(LayoutKind.Explicit, Size = 0x10)]
+public struct EnvironmentStruct
+{
+ [FieldOffset(0x00)] public nint EnvironmentPtr;
+ [FieldOffset(0x08)] public int EnvironmentHash;
+}
diff --git a/src/Roboto.GameOffsets/States/AreaLoading.cs b/src/Roboto.GameOffsets/States/AreaLoading.cs
new file mode 100644
index 0000000..6774c68
--- /dev/null
+++ b/src/Roboto.GameOffsets/States/AreaLoading.cs
@@ -0,0 +1,17 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.States;
+
+/// AreaLoading state — loading screen info.
+[StructLayout(LayoutKind.Explicit, Size = 0xE58)]
+public struct AreaLoading
+{
+ /// Whether currently loading (int32 bool). Offset: 0x660.
+ [FieldOffset(0x660)] public int IsLoading;
+
+ /// Total time spent on loading screens (ms). Offset: 0xDB8.
+ [FieldOffset(0xDB8)] public long TotalLoadingScreenTimeMs;
+
+ /// Pointer to current area details. Offset: 0xE50.
+ [FieldOffset(0xE50)] public nint CurrentAreaDetailsPtr;
+}
diff --git a/src/Roboto.GameOffsets/States/ImportantUiElements.cs b/src/Roboto.GameOffsets/States/ImportantUiElements.cs
new file mode 100644
index 0000000..b636ed3
--- /dev/null
+++ b/src/Roboto.GameOffsets/States/ImportantUiElements.cs
@@ -0,0 +1,24 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.States;
+
+/// Important UI element pointers within InGameState.
+[StructLayout(LayoutKind.Explicit, Size = 0x740)]
+public struct ImportantUiElements
+{
+ /// Chat parent UI element pointer.
+ [FieldOffset(0x570)] public nint ChatParentPtr;
+
+ /// Passive skill tree UI element pointer.
+ [FieldOffset(0x690)] public nint PassiveSkillTreePtr;
+
+ /// Map parent struct.
+ [FieldOffset(0x738)] public MapParentStruct MapParent;
+}
+
+/// Map parent — contains minimap and large map references.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct MapParentStruct
+{
+ public nint MapParentPtr;
+}
diff --git a/src/Roboto.GameOffsets/States/InGameState.cs b/src/Roboto.GameOffsets/States/InGameState.cs
new file mode 100644
index 0000000..ff49244
--- /dev/null
+++ b/src/Roboto.GameOffsets/States/InGameState.cs
@@ -0,0 +1,28 @@
+using System.Runtime.InteropServices;
+
+namespace Roboto.GameOffsets.States;
+
+/// InGameState — the main in-game state containing all sub-structures.
+[StructLayout(LayoutKind.Explicit, Size = 0x310)]
+public struct InGameState
+{
+ /// Escape state flag: 0=closed, 1=open. Confirmed: 0x20C.
+ [FieldOffset(0x20C)] public int EscapeStateFlag;
+
+ /// Pointer to AreaInstance (IngameData). Confirmed: 0x290.
+ [FieldOffset(0x290)] public nint AreaInstanceDataPtr;
+
+ /// Pointer to WorldData. Offset: 0x2F8.
+ [FieldOffset(0x2F8)] public nint WorldDataPtr;
+
+ /// Pointer to Camera struct. Confirmed: 0x308.
+ [FieldOffset(0x308)] public nint CameraPtr;
+}
+
+/// UI root structure reference.
+[StructLayout(LayoutKind.Explicit, Size = 0x10)]
+public struct UiRootStruct
+{
+ [FieldOffset(0x00)] public nint UiRootPtr;
+ [FieldOffset(0x08)] public nint Unknown;
+}
diff --git a/src/Roboto.GameOffsets/States/Inventory.cs b/src/Roboto.GameOffsets/States/Inventory.cs
new file mode 100644
index 0000000..81d0acc
--- /dev/null
+++ b/src/Roboto.GameOffsets/States/Inventory.cs
@@ -0,0 +1,48 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.States;
+
+/// Pre-inventory wrapper — contains the actual inventory pointer.
+[StructLayout(LayoutKind.Explicit, Size = 0x10)]
+public struct PreInventory
+{
+ [FieldOffset(0x00)] public nint InventoryPtr;
+}
+
+/// Inventory — item grid with hash-based item lookup.
+[StructLayout(LayoutKind.Explicit, Size = 0x188)]
+public struct InventoryStruct
+{
+ /// Total number of grid boxes (width * height).
+ [FieldOffset(0x14C)] public int TotalBoxes;
+
+ /// Item list — StdBucket array for hash-based lookup.
+ [FieldOffset(0x170)] public StdVector ItemList;
+}
+
+/// Key in the inventory item hash map.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct ItemHashKey
+{
+ public int GridX;
+ public int GridY;
+}
+
+/// Value in the inventory item hash map.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct ItemHashValue
+{
+ public nint ItemPtr;
+}
+
+/// An item entry in an inventory slot.
+[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;
+}
diff --git a/src/Roboto.GameOffsets/States/ServerData.cs b/src/Roboto.GameOffsets/States/ServerData.cs
new file mode 100644
index 0000000..de1e35c
--- /dev/null
+++ b/src/Roboto.GameOffsets/States/ServerData.cs
@@ -0,0 +1,29 @@
+using System.Runtime.InteropServices;
+using Roboto.GameOffsets.Natives;
+
+namespace Roboto.GameOffsets.States;
+
+/// ServerData — player server-side data.
+[StructLayout(LayoutKind.Explicit, Size = 0x58)]
+public struct ServerData
+{
+ /// Pointer to the player's ServerDataStructure.
+ [FieldOffset(0x50)] public nint PlayerServerDataPtr;
+}
+
+/// Detailed server data for the player.
+[StructLayout(LayoutKind.Explicit, Size = 0x338)]
+public struct ServerDataStructure
+{
+ /// Player inventories array StdVector.
+ [FieldOffset(0x320)] public StdVector PlayerInventories;
+}
+
+/// Entry in the player inventories array.
+[StructLayout(LayoutKind.Sequential, Pack = 1)]
+public struct InventoryArrayStruct
+{
+ public int InventoryId;
+ public int Unknown;
+ public nint InventoryPtr;
+}
diff --git a/src/Roboto.GameOffsets/States/WorldData.cs b/src/Roboto.GameOffsets/States/WorldData.cs
new file mode 100644
index 0000000..13f0ba4
--- /dev/null
+++ b/src/Roboto.GameOffsets/States/WorldData.cs
@@ -0,0 +1,33 @@
+using System.Runtime.InteropServices;
+using System.Numerics;
+
+namespace Roboto.GameOffsets.States;
+
+/// WorldData — world area details and camera.
+[StructLayout(LayoutKind.Explicit, Size = 0xA8)]
+public struct WorldData
+{
+ /// Pointer to WorldAreaDetails.
+ [FieldOffset(0x98)] public nint WorldAreaDetailsPtr;
+
+ /// Pointer to Camera struct.
+ [FieldOffset(0xA0)] public nint CameraPtr;
+}
+
+/// Details about the current world area (act, waypoint, etc.).
+[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;
+}
+
+/// Camera structure — contains the world-to-screen projection matrix.
+[StructLayout(LayoutKind.Explicit, Size = 0x1E0)]
+public struct CameraStructure
+{
+ /// 4x4 world-to-screen projection matrix (64 bytes). Confirmed offset: 0x1A0 from camera base.
+ [FieldOffset(0x1A0)] public Matrix4x4 WorldToScreenMatrix;
+}
diff --git a/src/Automata.Memory/ComponentReader.cs b/src/Roboto.Memory/ComponentReader.cs
similarity index 96%
rename from src/Automata.Memory/ComponentReader.cs
rename to src/Roboto.Memory/ComponentReader.cs
index 38c899d..4303895 100644
--- a/src/Automata.Memory/ComponentReader.cs
+++ b/src/Roboto.Memory/ComponentReader.cs
@@ -1,7 +1,7 @@
using System.Text;
using Serilog;
-namespace Automata.Memory;
+namespace Roboto.Memory;
///
/// 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;
+ /// Last resolved Render component pointer — used for fast per-frame position reads.
+ public nint CachedRenderComponentAddr { get; private set; }
+
+ /// Last resolved Life component pointer — used for fast per-frame vitals reads.
+ 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;
}
diff --git a/src/Automata.Memory/Entity.cs b/src/Roboto.Memory/Entity.cs
similarity index 99%
rename from src/Automata.Memory/Entity.cs
rename to src/Roboto.Memory/Entity.cs
index c4e55ba..20d6ef8 100644
--- a/src/Automata.Memory/Entity.cs
+++ b/src/Roboto.Memory/Entity.cs
@@ -1,4 +1,4 @@
-namespace Automata.Memory;
+namespace Roboto.Memory;
public enum EntityType
{
diff --git a/src/Automata.Memory/EntityReader.cs b/src/Roboto.Memory/EntityReader.cs
similarity index 99%
rename from src/Automata.Memory/EntityReader.cs
rename to src/Roboto.Memory/EntityReader.cs
index b2bd490..3cccf1c 100644
--- a/src/Automata.Memory/EntityReader.cs
+++ b/src/Roboto.Memory/EntityReader.cs
@@ -1,6 +1,6 @@
using Serilog;
-namespace Automata.Memory;
+namespace Roboto.Memory;
///
/// Reads entity list from AreaInstance's std::map red-black tree.
diff --git a/src/Automata.Memory/GameMemoryReader.cs b/src/Roboto.Memory/GameMemoryReader.cs
similarity index 83%
rename from src/Automata.Memory/GameMemoryReader.cs
rename to src/Roboto.Memory/GameMemoryReader.cs
index fc9e0ab..179c7de 100644
--- a/src/Automata.Memory/GameMemoryReader.cs
+++ b/src/Roboto.Memory/GameMemoryReader.cs
@@ -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;
}
+ ///
+ /// Resolved addresses for hot-path reads (camera, player position, player vitals, InGameState).
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Returns resolved addresses for the hot path.
+ /// Call after ReadSnapshot() has populated the cached addresses.
+ ///
+ public HotAddresses ResolveHotAddresses()
+ {
+ return new HotAddresses(
+ _cachedCameraMatrixAddr,
+ _components?.CachedRenderComponentAddr ?? 0,
+ _components?.CachedLifeComponentAddr ?? 0,
+ _lastInGameState,
+ _lastController);
+ }
+
public void Dispose()
{
if (_disposed) return;
diff --git a/src/Automata.Memory/GameOffsets.cs b/src/Roboto.Memory/GameOffsets.cs
similarity index 99%
rename from src/Automata.Memory/GameOffsets.cs
rename to src/Roboto.Memory/GameOffsets.cs
index ca988a2..8544f97 100644
--- a/src/Automata.Memory/GameOffsets.cs
+++ b/src/Roboto.Memory/GameOffsets.cs
@@ -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
{
diff --git a/src/Automata.Memory/GameStateReader.cs b/src/Roboto.Memory/GameStateReader.cs
similarity index 99%
rename from src/Automata.Memory/GameStateReader.cs
rename to src/Roboto.Memory/GameStateReader.cs
index a24547c..d8d6210 100644
--- a/src/Automata.Memory/GameStateReader.cs
+++ b/src/Roboto.Memory/GameStateReader.cs
@@ -1,6 +1,6 @@
using Serilog;
-namespace Automata.Memory;
+namespace Roboto.Memory;
///
/// Resolves GameState → Controller → InGameState, reads state slots, loading/escape state.
diff --git a/src/Automata.Memory/GameStateSnapshot.cs b/src/Roboto.Memory/GameStateSnapshot.cs
similarity index 98%
rename from src/Automata.Memory/GameStateSnapshot.cs
rename to src/Roboto.Memory/GameStateSnapshot.cs
index 3155fbc..e9a8c8f 100644
--- a/src/Automata.Memory/GameStateSnapshot.cs
+++ b/src/Roboto.Memory/GameStateSnapshot.cs
@@ -1,6 +1,6 @@
using System.Numerics;
-namespace Automata.Memory;
+namespace Roboto.Memory;
public class GameStateSnapshot
{
diff --git a/src/Automata.Memory/MemoryContext.cs b/src/Roboto.Memory/MemoryContext.cs
similarity index 97%
rename from src/Automata.Memory/MemoryContext.cs
rename to src/Roboto.Memory/MemoryContext.cs
index a99205f..2149081 100644
--- a/src/Automata.Memory/MemoryContext.cs
+++ b/src/Roboto.Memory/MemoryContext.cs
@@ -1,4 +1,4 @@
-namespace Automata.Memory;
+namespace Roboto.Memory;
///
/// Shared state for all memory reader classes. Holds the process handle, offsets, registry,
diff --git a/src/Automata.Memory/MemoryDiagnostics.cs b/src/Roboto.Memory/MemoryDiagnostics.cs
similarity index 99%
rename from src/Automata.Memory/MemoryDiagnostics.cs
rename to src/Roboto.Memory/MemoryDiagnostics.cs
index 3d2f4bc..80cb8f1 100644
--- a/src/Automata.Memory/MemoryDiagnostics.cs
+++ b/src/Roboto.Memory/MemoryDiagnostics.cs
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
using System.Text;
using Serilog;
-namespace Automata.Memory;
+namespace Roboto.Memory;
///
/// Diagnostic and scan methods extracted from GameMemoryReader.
diff --git a/src/Automata.Memory/MsvcStringReader.cs b/src/Roboto.Memory/MsvcStringReader.cs
similarity index 99%
rename from src/Automata.Memory/MsvcStringReader.cs
rename to src/Roboto.Memory/MsvcStringReader.cs
index 9274be2..818f7f8 100644
--- a/src/Automata.Memory/MsvcStringReader.cs
+++ b/src/Roboto.Memory/MsvcStringReader.cs
@@ -1,6 +1,6 @@
using System.Text;
-namespace Automata.Memory;
+namespace Roboto.Memory;
///
/// Reads MSVC std::string and std::wstring from process memory.
diff --git a/src/Automata.Memory/Native.cs b/src/Roboto.Memory/Native.cs
similarity index 98%
rename from src/Automata.Memory/Native.cs
rename to src/Roboto.Memory/Native.cs
index 3283c56..1294226 100644
--- a/src/Automata.Memory/Native.cs
+++ b/src/Roboto.Memory/Native.cs
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
-namespace Automata.Memory;
+namespace Roboto.Memory;
internal static partial class Native
{
diff --git a/src/Automata.Memory/ObjectRegistry.cs b/src/Roboto.Memory/ObjectRegistry.cs
similarity index 99%
rename from src/Automata.Memory/ObjectRegistry.cs
rename to src/Roboto.Memory/ObjectRegistry.cs
index 788b562..020ed1c 100644
--- a/src/Automata.Memory/ObjectRegistry.cs
+++ b/src/Roboto.Memory/ObjectRegistry.cs
@@ -1,7 +1,7 @@
using System.Text.Json;
using Serilog;
-namespace Automata.Memory;
+namespace Roboto.Memory;
///
/// Persistent registry of discovered strings, organized by category.
diff --git a/src/Automata.Memory/PatternScanner.cs b/src/Roboto.Memory/PatternScanner.cs
similarity index 99%
rename from src/Automata.Memory/PatternScanner.cs
rename to src/Roboto.Memory/PatternScanner.cs
index ba9d2fa..379d880 100644
--- a/src/Automata.Memory/PatternScanner.cs
+++ b/src/Roboto.Memory/PatternScanner.cs
@@ -1,6 +1,6 @@
using Serilog;
-namespace Automata.Memory;
+namespace Roboto.Memory;
public sealed class PatternScanner
{
diff --git a/src/Automata.Memory/ProcessMemory.cs b/src/Roboto.Memory/ProcessMemory.cs
similarity index 99%
rename from src/Automata.Memory/ProcessMemory.cs
rename to src/Roboto.Memory/ProcessMemory.cs
index 7330f0c..cfc2091 100644
--- a/src/Automata.Memory/ProcessMemory.cs
+++ b/src/Roboto.Memory/ProcessMemory.cs
@@ -2,7 +2,7 @@ using System.Diagnostics;
using System.Runtime.CompilerServices;
using Serilog;
-namespace Automata.Memory;
+namespace Roboto.Memory;
public sealed class ProcessMemory : IDisposable
{
diff --git a/src/Automata.Memory/Automata.Memory.csproj b/src/Roboto.Memory/Roboto.Memory.csproj
similarity index 85%
rename from src/Automata.Memory/Automata.Memory.csproj
rename to src/Roboto.Memory/Roboto.Memory.csproj
index 552a012..c819e73 100644
--- a/src/Automata.Memory/Automata.Memory.csproj
+++ b/src/Roboto.Memory/Roboto.Memory.csproj
@@ -10,5 +10,6 @@
+
diff --git a/src/Automata.Memory/RttiResolver.cs b/src/Roboto.Memory/RttiResolver.cs
similarity index 98%
rename from src/Automata.Memory/RttiResolver.cs
rename to src/Roboto.Memory/RttiResolver.cs
index a38598d..0337b4f 100644
--- a/src/Automata.Memory/RttiResolver.cs
+++ b/src/Roboto.Memory/RttiResolver.cs
@@ -1,6 +1,6 @@
using System.Text;
-namespace Automata.Memory;
+namespace Roboto.Memory;
///
/// Resolves MSVC x64 RTTI type names from vtable addresses and classifies pointers.
diff --git a/src/Automata.Memory/TerrainReader.cs b/src/Roboto.Memory/TerrainReader.cs
similarity index 99%
rename from src/Automata.Memory/TerrainReader.cs
rename to src/Roboto.Memory/TerrainReader.cs
index a97fcb0..0d30b20 100644
--- a/src/Automata.Memory/TerrainReader.cs
+++ b/src/Roboto.Memory/TerrainReader.cs
@@ -1,6 +1,6 @@
using Serilog;
-namespace Automata.Memory;
+namespace Roboto.Memory;
///
/// Reads terrain walkability grid from AreaInstance, with caching and loading edge detection.
diff --git a/src/Automata.Memory/WalkabilityGrid.cs b/src/Roboto.Memory/WalkabilityGrid.cs
similarity index 94%
rename from src/Automata.Memory/WalkabilityGrid.cs
rename to src/Roboto.Memory/WalkabilityGrid.cs
index d366c54..4b52073 100644
--- a/src/Automata.Memory/WalkabilityGrid.cs
+++ b/src/Roboto.Memory/WalkabilityGrid.cs
@@ -1,4 +1,4 @@
-namespace Automata.Memory;
+namespace Roboto.Memory;
public sealed class WalkabilityGrid
{