refactoring
This commit is contained in:
parent
fbd0ba445a
commit
18d8721dd5
68 changed files with 2187 additions and 36 deletions
11
Automata.sln
11
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}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) -->
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
470
src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs
Normal file
470
src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs
Normal 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",
|
||||
},
|
||||
_ => []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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="<-" 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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
108
src/Roboto.Data/GameDataCache.cs
Normal file
108
src/Roboto.Data/GameDataCache.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
372
src/Roboto.Data/MemoryPoller.cs
Normal file
372
src/Roboto.Data/MemoryPoller.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -9,5 +9,6 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Roboto.Core\Roboto.Core.csproj" />
|
||||
<ProjectReference Include="..\Roboto.Memory\Roboto.Memory.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
|||
68
src/Roboto.GameOffsets/Components/Actor.cs
Normal file
68
src/Roboto.GameOffsets/Components/Actor.cs
Normal 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;
|
||||
}
|
||||
13
src/Roboto.GameOffsets/Components/Animated.cs
Normal file
13
src/Roboto.GameOffsets/Components/Animated.cs
Normal 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;
|
||||
}
|
||||
25
src/Roboto.GameOffsets/Components/Buffs.cs
Normal file
25
src/Roboto.GameOffsets/Components/Buffs.cs
Normal 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;
|
||||
}
|
||||
23
src/Roboto.GameOffsets/Components/Charges.cs
Normal file
23
src/Roboto.GameOffsets/Components/Charges.cs
Normal 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;
|
||||
}
|
||||
24
src/Roboto.GameOffsets/Components/Chest.cs
Normal file
24
src/Roboto.GameOffsets/Components/Chest.cs
Normal 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;
|
||||
}
|
||||
11
src/Roboto.GameOffsets/Components/ComponentHeader.cs
Normal file
11
src/Roboto.GameOffsets/Components/ComponentHeader.cs
Normal 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;
|
||||
}
|
||||
27
src/Roboto.GameOffsets/Components/Life.cs
Normal file
27
src/Roboto.GameOffsets/Components/Life.cs
Normal 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;
|
||||
}
|
||||
59
src/Roboto.GameOffsets/Components/Mods.cs
Normal file
59
src/Roboto.GameOffsets/Components/Mods.cs
Normal 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;
|
||||
}
|
||||
20
src/Roboto.GameOffsets/Components/Player.cs
Normal file
20
src/Roboto.GameOffsets/Components/Player.cs
Normal 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;
|
||||
}
|
||||
13
src/Roboto.GameOffsets/Components/Positioned.cs
Normal file
13
src/Roboto.GameOffsets/Components/Positioned.cs
Normal 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;
|
||||
}
|
||||
23
src/Roboto.GameOffsets/Components/Render.cs
Normal file
23
src/Roboto.GameOffsets/Components/Render.cs
Normal 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;
|
||||
}
|
||||
13
src/Roboto.GameOffsets/Components/Shrine.cs
Normal file
13
src/Roboto.GameOffsets/Components/Shrine.cs
Normal 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;
|
||||
}
|
||||
17
src/Roboto.GameOffsets/Components/StateMachine.cs
Normal file
17
src/Roboto.GameOffsets/Components/StateMachine.cs
Normal 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;
|
||||
}
|
||||
32
src/Roboto.GameOffsets/Components/Stats.cs
Normal file
32
src/Roboto.GameOffsets/Components/Stats.cs
Normal 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;
|
||||
}
|
||||
19
src/Roboto.GameOffsets/Components/Targetable.cs
Normal file
19
src/Roboto.GameOffsets/Components/Targetable.cs
Normal 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;
|
||||
}
|
||||
13
src/Roboto.GameOffsets/Components/Transitionable.cs
Normal file
13
src/Roboto.GameOffsets/Components/Transitionable.cs
Normal 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;
|
||||
}
|
||||
13
src/Roboto.GameOffsets/Components/TriggerableBlockage.cs
Normal file
13
src/Roboto.GameOffsets/Components/TriggerableBlockage.cs
Normal 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;
|
||||
}
|
||||
64
src/Roboto.GameOffsets/Entities/Entity.cs
Normal file
64
src/Roboto.GameOffsets/Entities/Entity.cs
Normal 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;
|
||||
}
|
||||
42
src/Roboto.GameOffsets/Entities/EntityTreeNode.cs
Normal file
42
src/Roboto.GameOffsets/Entities/EntityTreeNode.cs
Normal 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;
|
||||
}
|
||||
11
src/Roboto.GameOffsets/Natives/StdBucket.cs
Normal file
11
src/Roboto.GameOffsets/Natives/StdBucket.cs
Normal 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;
|
||||
}
|
||||
28
src/Roboto.GameOffsets/Natives/StdList.cs
Normal file
28
src/Roboto.GameOffsets/Natives/StdList.cs
Normal 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;
|
||||
}
|
||||
37
src/Roboto.GameOffsets/Natives/StdMap.cs
Normal file
37
src/Roboto.GameOffsets/Natives/StdMap.cs
Normal 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;
|
||||
}
|
||||
20
src/Roboto.GameOffsets/Natives/StdTuple.cs
Normal file
20
src/Roboto.GameOffsets/Natives/StdTuple.cs
Normal 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;
|
||||
}
|
||||
15
src/Roboto.GameOffsets/Natives/StdVector.cs
Normal file
15
src/Roboto.GameOffsets/Natives/StdVector.cs
Normal 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;
|
||||
}
|
||||
23
src/Roboto.GameOffsets/Natives/StdWString.cs
Normal file
23
src/Roboto.GameOffsets/Natives/StdWString.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Roboto.GameOffsets.Natives;
|
||||
|
||||
/// <summary>MSVC std::wstring (basic_string<wchar_t>) 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 <= 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;
|
||||
}
|
||||
10
src/Roboto.GameOffsets/Natives/Util.cs
Normal file
10
src/Roboto.GameOffsets/Natives/Util.cs
Normal 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;
|
||||
}
|
||||
8
src/Roboto.GameOffsets/Roboto.GameOffsets.csproj
Normal file
8
src/Roboto.GameOffsets/Roboto.GameOffsets.csproj
Normal 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>
|
||||
98
src/Roboto.GameOffsets/States/AreaInstance.cs
Normal file
98
src/Roboto.GameOffsets/States/AreaInstance.cs
Normal 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;
|
||||
}
|
||||
17
src/Roboto.GameOffsets/States/AreaLoading.cs
Normal file
17
src/Roboto.GameOffsets/States/AreaLoading.cs
Normal 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;
|
||||
}
|
||||
24
src/Roboto.GameOffsets/States/ImportantUiElements.cs
Normal file
24
src/Roboto.GameOffsets/States/ImportantUiElements.cs
Normal 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;
|
||||
}
|
||||
28
src/Roboto.GameOffsets/States/InGameState.cs
Normal file
28
src/Roboto.GameOffsets/States/InGameState.cs
Normal 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;
|
||||
}
|
||||
48
src/Roboto.GameOffsets/States/Inventory.cs
Normal file
48
src/Roboto.GameOffsets/States/Inventory.cs
Normal 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;
|
||||
}
|
||||
29
src/Roboto.GameOffsets/States/ServerData.cs
Normal file
29
src/Roboto.GameOffsets/States/ServerData.cs
Normal 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;
|
||||
}
|
||||
33
src/Roboto.GameOffsets/States/WorldData.cs
Normal file
33
src/Roboto.GameOffsets/States/WorldData.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
public enum EntityType
|
||||
{
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using Serilog;
|
||||
|
||||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Reads entity list from AreaInstance's std::map red-black tree.
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using Serilog;
|
||||
|
||||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Resolves GameState → Controller → InGameState, reads state slots, loading/escape state.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Numerics;
|
||||
|
||||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
public class GameStateSnapshot
|
||||
{
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Shared state for all memory reader classes. Holds the process handle, offsets, registry,
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text;
|
||||
|
||||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
/// <summary>
|
||||
/// Reads MSVC std::string and std::wstring from process memory.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
internal static partial class Native
|
||||
{
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using Serilog;
|
||||
|
||||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
public sealed class PatternScanner
|
||||
{
|
||||
|
|
@ -2,7 +2,7 @@ using System.Diagnostics;
|
|||
using System.Runtime.CompilerServices;
|
||||
using Serilog;
|
||||
|
||||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
public sealed class ProcessMemory : IDisposable
|
||||
{
|
||||
|
|
@ -10,5 +10,6 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Automata.Core\Automata.Core.csproj" />
|
||||
<ProjectReference Include="..\Roboto.GameOffsets\Roboto.GameOffsets.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
namespace Automata.Memory;
|
||||
namespace Roboto.Memory;
|
||||
|
||||
public sealed class WalkabilityGrid
|
||||
{
|
||||
Loading…
Add table
Add a link
Reference in a new issue