diff --git a/Automata.sln b/Automata.sln index a7d1e4f..26c298e 100644 --- a/Automata.sln +++ b/Automata.sln @@ -25,7 +25,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Ui", "src\Automata EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Navigation", "src\Automata.Navigation\Automata.Navigation.csproj", "{D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Automata.Memory", "src\Automata.Memory\Automata.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.Memory", "src\Roboto.Memory\Roboto.Memory.csproj", "{B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Roboto.GameOffsets", "src\Roboto.GameOffsets\Roboto.GameOffsets.csproj", "{C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "lib", "lib", "{652F700E-4F84-4E66-BD62-717D3A8D6FBC}" EndProject @@ -112,6 +114,10 @@ Global {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Debug|Any CPU.Build.0 = Debug|Any CPU {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.ActiveCfg = Release|Any CPU {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B}.Release|Any CPU.Build.0 = Release|Any CPU + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F}.Release|Any CPU.Build.0 = Release|Any CPU {B858F6F2-389F-475A-87FE-E4E01DA3E948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B858F6F2-389F-475A-87FE-E4E01DA3E948}.Debug|Any CPU.Build.0 = Debug|Any CPU {B858F6F2-389F-475A-87FE-E4E01DA3E948}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -180,7 +186,8 @@ Global {F186DDC8-6843-43E9-8BD3-9F914C5E784E} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {859F870E-F013-4C2B-AFEC-7A8C6A5FE3F3} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} {D3F7A2E1-9B4C-4E8D-A6F5-1C2D3E4F5A6B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} - {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {67A27DFE-D2C5-479D-86FE-7E156BD0CFAA} + {B7E3F1A2-4D5C-6E7F-8A9B-0C1D2E3F4A5B} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} + {C8D9E0F1-2A3B-4C5D-6E7F-8A9B0C1D2E3F} = {D1A2B3C4-E5F6-7890-ABCD-EF1234567890} {B858F6F2-389F-475A-87FE-E4E01DA3E948} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC} {6FEA655D-18E4-4DA1-839F-A41433B03FBB} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC} {74FD0F88-86BC-49AE-9A16-136D92A10090} = {652F700E-4F84-4E66-BD62-717D3A8D6FBC} diff --git a/entities.json b/entities.json index cc306b9..961d3fd 100644 --- a/entities.json +++ b/entities.json @@ -84,11 +84,13 @@ "Metadata/NPC/Four_Act1/UnaAfterIronCount", "Metadata/NPC/Four_Act1/UnaHoodedOneInjured", "Metadata/NPC/League/Incursion/AlvaIncursionWild", + "Metadata/Pet/AzmeriStag/AzmeriStag", "Metadata/Pet/BabyBossesHumans/BabyBrutus/BabyBrutus", "Metadata/Pet/BabyChimera/BabyChimera", "Metadata/Pet/BetaKiwis/BaronKiwi", "Metadata/Pet/BetaKiwis/FaridunKiwi", "Metadata/Pet/BookAndQuillPet/BookAndQuillPet_Abyss", + "Metadata/Pet/Cat/Sphynx/GiantSphynx/GiantSphynxBlack", "Metadata/Pet/FledglingBellcrow/FledglingBellcrow", "Metadata/Pet/LandSharkPet/LandSharkPet", "Metadata/Pet/OctopusParasite/OctopusParasiteCelestial", diff --git a/src/Automata.Ui/App.axaml b/src/Automata.Ui/App.axaml index 2f31dc6..a77fa7a 100644 --- a/src/Automata.Ui/App.axaml +++ b/src/Automata.Ui/App.axaml @@ -16,5 +16,8 @@ + + + diff --git a/src/Automata.Ui/App.axaml.cs b/src/Automata.Ui/App.axaml.cs index f370d39..92a293f 100644 --- a/src/Automata.Ui/App.axaml.cs +++ b/src/Automata.Ui/App.axaml.cs @@ -71,6 +71,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); var provider = services.BuildServiceProvider(); @@ -97,6 +98,7 @@ public partial class App : Application mainVm.CraftingVm = provider.GetRequiredService(); mainVm.MemoryVm = provider.GetRequiredService(); mainVm.RobotoVm = provider.GetRequiredService(); + mainVm.BrowserVm = provider.GetRequiredService(); var window = new MainWindow { DataContext = mainVm }; window.SetConfigStore(store); diff --git a/src/Automata.Ui/Automata.Ui.csproj b/src/Automata.Ui/Automata.Ui.csproj index 5c4965d..3c9e2b8 100644 --- a/src/Automata.Ui/Automata.Ui.csproj +++ b/src/Automata.Ui/Automata.Ui.csproj @@ -23,7 +23,8 @@ - + + diff --git a/src/Automata.Ui/Converters/ValueConverters.cs b/src/Automata.Ui/Converters/ValueConverters.cs index ac693be..7550d71 100644 --- a/src/Automata.Ui/Converters/ValueConverters.cs +++ b/src/Automata.Ui/Converters/ValueConverters.cs @@ -142,3 +142,33 @@ public class MatchedModBrushConverter : IValueConverter public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); } + +public class ChangedRowBrushConverter : IValueConverter +{ + private static readonly SolidColorBrush ChangedBrush = new(Color.Parse("#30d29922")); + private static readonly SolidColorBrush NormalBrush = new(Colors.Transparent); + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + => value is true ? ChangedBrush : NormalBrush; + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} + +public class BoolToHandCursorConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + => value is true ? new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Hand) : new Avalonia.Input.Cursor(Avalonia.Input.StandardCursorType.Arrow); + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} + +public class BoolToUnderlineConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + => value is true ? Avalonia.Media.TextDecorations.Underline : null; + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs b/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs index 18bec05..878fac5 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs +++ b/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs @@ -1,6 +1,7 @@ using System.Drawing; using System.Numerics; using Automata.Ui.ViewModels; +using Roboto.Data; using Vortice.DirectWrite; namespace Automata.Ui.Overlay.Layers; @@ -24,16 +25,41 @@ internal sealed class D2dEntityLabelLayer : ID2dOverlayLayer, IDisposable { var data = RobotoViewModel.OverlayData; if (data is null || data.Entries.Length == 0) return; - if (data.CameraMatrix is not { } matrix) return; + + var cache = RobotoViewModel.SharedCache; + if (cache is null) return; + + // Read camera and player position from centralized cache (updated at 60Hz) + var camData = cache.CameraMatrix; + if (camData is null) return; + var mat = camData.Matrix; var rt = ctx.RenderTarget; - var playerZ = data.PlayerZ; + + // Compute delta between fresh player position (60Hz) and snapshot position (10Hz) + // Entities near the snapshot player get shifted by this delta to compensate for movement + var freshPos = cache.PlayerPosition; + var playerZ = freshPos.HasPosition ? freshPos.Z : data.SnapshotPlayerZ; + var dx = freshPos.HasPosition ? freshPos.X - data.SnapshotPlayerPosition.X : 0f; + var dy = freshPos.HasPosition ? freshPos.Y - data.SnapshotPlayerPosition.Y : 0f; + var snapshotPx = data.SnapshotPlayerPosition.X; + var snapshotPy = data.SnapshotPlayerPosition.Y; foreach (ref readonly var entry in data.Entries.AsSpan()) { - // WorldToScreen using camera's view-projection matrix (same as ExileCore) - var worldPos = new Vector4(entry.X, entry.Y, playerZ, 1f); - var clip = Vector4.Transform(worldPos, matrix); + // If entity is near the snapshot player position, apply the movement delta + var ex = entry.X; + var ey = entry.Y; + var distToPlayer = MathF.Abs(ex - snapshotPx) + MathF.Abs(ey - snapshotPy); + if (distToPlayer < 50f) + { + ex += dx; + ey += dy; + } + + // WorldToScreen using camera's view-projection matrix + var worldPos = new Vector4(ex, ey, playerZ, 1f); + var clip = Vector4.Transform(worldPos, mat); // Perspective divide if (clip.W is 0f or float.NaN) continue; diff --git a/src/Automata.Ui/ViewModels/MainWindowViewModel.cs b/src/Automata.Ui/ViewModels/MainWindowViewModel.cs index d556faf..f75d7f5 100644 --- a/src/Automata.Ui/ViewModels/MainWindowViewModel.cs +++ b/src/Automata.Ui/ViewModels/MainWindowViewModel.cs @@ -184,6 +184,7 @@ public partial class MainWindowViewModel : ObservableObject public CraftingViewModel? CraftingVm { get; set; } public MemoryViewModel? MemoryVm { get; set; } public RobotoViewModel? RobotoVm { get; set; } + public ObjectBrowserViewModel? BrowserVm { get; set; } partial void OnBotModeChanged(BotMode value) { diff --git a/src/Automata.Ui/ViewModels/MemoryViewModel.cs b/src/Automata.Ui/ViewModels/MemoryViewModel.cs index 91bca0f..3dec42c 100644 --- a/src/Automata.Ui/ViewModels/MemoryViewModel.cs +++ b/src/Automata.Ui/ViewModels/MemoryViewModel.cs @@ -6,7 +6,7 @@ using Avalonia.Platform; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; -using Automata.Memory; +using Roboto.Memory; namespace Automata.Ui.ViewModels; @@ -378,7 +378,7 @@ public partial class MemoryViewModel : ObservableObject { var withPos = snap.Entities.Count(e => e.HasPosition); var withComps = snap.Entities.Count(e => e.Components is not null); - var monsters = snap.Entities.Count(e => e.Type == Automata.Memory.EntityType.Monster); + var monsters = snap.Entities.Count(e => e.Type == Roboto.Memory.EntityType.Monster); var knownComps = _reader?.Registry["components"].Count ?? 0; _entitySummary!.Set($"{snap.Entities.Count} total, {withComps} with comps, {knownComps} known, {monsters} monsters"); diff --git a/src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs b/src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs new file mode 100644 index 0000000..778ebc2 --- /dev/null +++ b/src/Automata.Ui/ViewModels/ObjectBrowserViewModel.cs @@ -0,0 +1,470 @@ +using System.Collections.ObjectModel; +using System.Text; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Roboto.Memory; + +namespace Automata.Ui.ViewModels; + +public partial class FieldRowViewModel : ObservableObject +{ + [ObservableProperty] private string _offset = ""; + [ObservableProperty] private string _typeTag = ""; + [ObservableProperty] private string _typeColor = "#8b949e"; + [ObservableProperty] private string _value = ""; + [ObservableProperty] private string _rawHex = ""; + [ObservableProperty] private bool _isPointer; + [ObservableProperty] private bool _isChanged; + [ObservableProperty] private string _offsetName = ""; + + public nint PointerTarget { get; set; } + + /// Number of 8-byte rows this field spans (1 for most, 4 for String). + public int RowSpan { get; set; } = 1; +} + +public partial class ObjectBrowserViewModel : ObservableObject +{ + private GameMemoryReader? _reader; + private readonly Stack _navStack = new(); + private Dictionary _previousValues = new(); + + [ObservableProperty] private string _statusText = "Not attached"; + [ObservableProperty] private string _currentAddress = ""; + [ObservableProperty] private string _currentLabel = "Object"; + [ObservableProperty] private string _objectSize = "400"; + [ObservableProperty] private bool _canGoBack; + [ObservableProperty] private string _breadcrumbText = ""; + [ObservableProperty] private string _goToAddressText = ""; + + public ObservableCollection Fields { get; } = []; + + private record NavEntry(nint Address, string Label, int ScrollIndex); + + [RelayCommand] + private void Attach() + { + if (_reader != null) + { + _reader.Dispose(); + _reader = null; + } + + _reader = new GameMemoryReader(); + if (!_reader.Attach()) + { + StatusText = "Process not found"; + _reader.Dispose(); + _reader = null; + return; + } + + // Do an initial snapshot read to resolve GameState pointers + var snap = _reader.ReadSnapshot(); + StatusText = $"Attached (PID {snap.ProcessId})"; + + if (snap.GameStateBase == 0) + { + StatusText = "Attached but GameState base not found"; + return; + } + + _navStack.Clear(); + NavigateTo(snap.GameStateBase, "GameState"); + } + + [RelayCommand] + private void GoBack() + { + if (_navStack.Count == 0) return; + var entry = _navStack.Pop(); + CanGoBack = _navStack.Count > 0; + ScanAndDisplay(entry.Address, entry.Label); + UpdateBreadcrumb(); + } + + [RelayCommand] + private void Refresh() + { + if (_reader?.Context == null) return; + if (!TryParseHex(CurrentAddress, out var addr)) return; + ScanAndDisplay(addr, CurrentLabel); + } + + [RelayCommand] + private void GoToAddress() + { + if (_reader?.Context == null) return; + if (!TryParseHex(GoToAddressText, out var addr)) return; + NavigateTo(addr, $"0x{addr:X}"); + } + + public void NavigateToPointer(FieldRowViewModel row) + { + if (!row.IsPointer || _reader?.Context == null) return; + + // Push current onto stack + if (TryParseHex(CurrentAddress, out var currentAddr)) + _navStack.Push(new NavEntry(currentAddr, CurrentLabel, 0)); + + var label = row.Value.StartsWith("-> ") ? row.Value[3..] : $"+{row.Offset}"; + NavigateTo(row.PointerTarget, label); + } + + private void NavigateTo(nint address, string label) + { + CanGoBack = _navStack.Count > 0; + ScanAndDisplay(address, label); + UpdateBreadcrumb(); + } + + private void ScanAndDisplay(nint address, string label) + { + CurrentAddress = $"0x{address:X}"; + CurrentLabel = label; + + if (!int.TryParse(ObjectSize, System.Globalization.NumberStyles.HexNumber, null, out var size)) + size = 0x400; + + var fields = ScanObject(address, size); + + // Detect changed values + var newValues = new Dictionary(); + foreach (var f in fields) + { + if (!int.TryParse(f.Offset.Replace("+0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var off)) + continue; + var rawVal = ParseRawHexToLong(f.RawHex); + newValues[off] = rawVal; + if (_previousValues.TryGetValue(off, out var prev) && prev != rawVal) + f.IsChanged = true; + } + _previousValues = newValues; + + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + Fields.Clear(); + foreach (var f in fields) + Fields.Add(f); + }); + } + + private List ScanObject(nint address, int size) + { + var ctx = _reader!.Context!; + var mem = ctx.Memory; + var rtti = new RttiResolver(ctx); + var strings = new MsvcStringReader(ctx); + var offsetLabels = BuildOffsetLabels(ctx.Offsets, CurrentLabel); + + var data = mem.ReadBytes(address, size); + if (data == null) + return [new FieldRowViewModel { Offset = "+0x000", TypeTag = "Error", Value = "Read failed", TypeColor = "#f85149" }]; + + var result = new List(); + var skip = new HashSet(); // offsets to skip (consumed by multi-row fields) + + for (var i = 0; i < data.Length; i += 8) + { + if (skip.Contains(i)) continue; + if (i + 8 > data.Length) break; + + var qword = BitConverter.ToInt64(data, i); + var ptr = (nint)qword; + var offsetStr = $"+0x{i:X3}"; + var rawHex = FormatHex(data, i, 8); + + // Priority 1: Vtable — offset 0, points into .rdata + if (i == 0 && ptr != 0 && ctx.IsModuleAddress(ptr)) + { + var className = rtti.ResolveRttiName(ptr); + if (className != null) + { + result.Add(new FieldRowViewModel + { + Offset = offsetStr, TypeTag = "Vtable", TypeColor = "#d2a8ff", + Value = className, RawHex = rawHex + }); + CurrentLabel = className; + offsetLabels = BuildOffsetLabels(ctx.Offsets, className); + continue; + } + } + + // Priority 4: MSVC std::string (32 bytes: _Bx[16] + size[8] + capacity[8]) + if (i + 32 <= data.Length) + { + var strSize = BitConverter.ToInt64(data, i + 0x10); + var strCap = BitConverter.ToInt64(data, i + 0x18); + if (strSize > 0 && strSize <= 512 && strCap >= strSize && strCap < 1_000_000) + { + // Try reading as std::string + var strVal = strings.ReadMsvcString(address + i); + if (strVal != null) + { + var fullHex = FormatHex(data, i, 32); + result.Add(new FieldRowViewModel + { + Offset = offsetStr, TypeTag = "String", TypeColor = "#a5d6ff", + Value = $"\"{strVal}\"", RawHex = fullHex, RowSpan = 4 + }); + // Skip the next 3 rows (24 bytes) since string occupies 32 bytes total + for (var s = i + 8; s < i + 32 && s < data.Length; s += 8) + skip.Add(s); + continue; + } + // Try wstring + var wstrVal = strings.ReadMsvcWString(address + i); + if (wstrVal != null) + { + var fullHex = FormatHex(data, i, 32); + result.Add(new FieldRowViewModel + { + Offset = offsetStr, TypeTag = "WString", TypeColor = "#a5d6ff", + Value = $"\"{wstrVal}\"", RawHex = fullHex, RowSpan = 4 + }); + for (var s = i + 8; s < i + 32 && s < data.Length; s += 8) + skip.Add(s); + continue; + } + } + } + + // Priority 2: Valid heap ptr whose first qword is in .rdata → Ptr with RTTI name + if (ptr != 0 && ctx.IsValidHeapPtr(ptr)) + { + var targetFirst = mem.ReadPointer(ptr); + if (targetFirst != 0 && ctx.IsModuleAddress(targetFirst)) + { + var className = rtti.ResolveRttiName(targetFirst); + if (className != null) + { + result.Add(new FieldRowViewModel + { + Offset = offsetStr, TypeTag = "Ptr->", TypeColor = "#58a6ff", + Value = $"-> {className}", RawHex = rawHex, + IsPointer = true, PointerTarget = ptr + }); + continue; + } + } + + // Priority 3: Valid heap ptr, dereferenceable + // Verify it's actually readable + var probeArr = new byte[8]; + if (mem.ReadBytes(ptr, probeArr.AsSpan())) + { + result.Add(new FieldRowViewModel + { + Offset = offsetStr, TypeTag = "Ptr", TypeColor = "#58a6ff", + Value = $"0x{ptr:X}", RawHex = rawHex, + IsPointer = true, PointerTarget = ptr + }); + continue; + } + } + + // Module pointer (not heap but in module range) + if (ptr != 0 && ctx.IsModuleAddress(ptr)) + { + var className = rtti.ResolveRttiName(ptr); + result.Add(new FieldRowViewModel + { + Offset = offsetStr, TypeTag = "Module", TypeColor = "#d2a8ff", + Value = className ?? $"0x{ptr:X}", RawHex = rawHex + }); + continue; + } + + // Priority 5 & 6: Float / Int guesses (look at low 4 bytes and high 4 bytes) + var lo32 = BitConverter.ToInt32(data, i); + var hi32 = BitConverter.ToInt32(data, i + 4); + var loFloat = BitConverter.ToSingle(data, i); + var hiFloat = BitConverter.ToSingle(data, i + 4); + + var loIsFloat = IsReasonableFloat(loFloat); + var hiIsFloat = IsReasonableFloat(hiFloat); + var loIsInt = IsReasonableInt(lo32); + var hiIsInt = IsReasonableInt(hi32); + + if (loIsFloat || hiIsFloat) + { + var parts = new List(); + if (loIsFloat) parts.Add($"{loFloat:F2}f"); + else if (loIsInt) parts.Add($"{lo32}"); + else parts.Add($"0x{(uint)lo32:X8}"); + + if (hiIsFloat) parts.Add($"{hiFloat:F2}f"); + else if (hiIsInt) parts.Add($"{hi32}"); + else parts.Add($"0x{(uint)hi32:X8}"); + + result.Add(new FieldRowViewModel + { + Offset = offsetStr, TypeTag = "Float?", TypeColor = "#f0883e", + Value = string.Join(" | ", parts), RawHex = rawHex + }); + continue; + } + + if (loIsInt || hiIsInt) + { + var parts = new List(); + parts.Add(loIsInt ? $"{lo32}" : $"0x{(uint)lo32:X8}"); + parts.Add(hiIsInt ? $"{hi32}" : $"0x{(uint)hi32:X8}"); + + result.Add(new FieldRowViewModel + { + Offset = offsetStr, TypeTag = "Int?", TypeColor = "#3fb950", + Value = string.Join(" | ", parts), RawHex = rawHex + }); + continue; + } + + // Priority 7: Raw hex + result.Add(new FieldRowViewModel + { + Offset = offsetStr, TypeTag = "Hex", TypeColor = "#484f58", + Value = $"0x{(ulong)qword:X16}", RawHex = rawHex + }); + } + + // Apply offset labels + if (offsetLabels.Count > 0) + { + foreach (var row in result) + { + if (int.TryParse(row.Offset.Replace("+0x", ""), System.Globalization.NumberStyles.HexNumber, null, out var off) + && offsetLabels.TryGetValue(off, out var name)) + { + row.OffsetName = name; + } + } + } + + return result; + } + + private void UpdateBreadcrumb() + { + var parts = new List(); + foreach (var entry in _navStack.Reverse()) + parts.Add(entry.Label); + parts.Add(CurrentLabel); + BreadcrumbText = string.Join(" -> ", parts); + } + + private static string FormatHex(byte[] data, int offset, int length) + { + var sb = new StringBuilder(); + for (var j = 0; j < length && offset + j < data.Length; j++) + { + if (j > 0) sb.Append(' '); + sb.Append(data[offset + j].ToString("X2")); + } + return sb.ToString(); + } + + private static bool IsReasonableFloat(float f) + { + if (float.IsNaN(f) || float.IsInfinity(f)) return false; + var abs = MathF.Abs(f); + // Must be nonzero and within a reasonable range, and not look like an integer + return abs > 0.001f && abs < 1e8f; + } + + private static bool IsReasonableInt(int v) + { + // Nonzero, not a likely pointer fragment, reasonable range + return v != 0 && v > -10_000_000 && v < 10_000_000; + } + + private static bool TryParseHex(string text, out nint result) + { + result = 0; + if (string.IsNullOrWhiteSpace(text)) return false; + var clean = text.Trim(); + if (clean.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + clean = clean[2..]; + if (long.TryParse(clean, System.Globalization.NumberStyles.HexNumber, null, out var val)) + { + result = (nint)val; + return true; + } + return false; + } + + private static long ParseRawHexToLong(string hex) + { + if (string.IsNullOrEmpty(hex)) return 0; + var bytes = hex.Split(' '); + long val = 0; + for (var i = 0; i < Math.Min(bytes.Length, 8); i++) + { + if (byte.TryParse(bytes[i], System.Globalization.NumberStyles.HexNumber, null, out var b)) + val |= (long)b << (i * 8); + } + return val; + } + + /// + /// Builds a map of offset → label name for the given object type, based on known GameOffsets. + /// + private static Dictionary BuildOffsetLabels(GameOffsets o, string objectType) + { + // Normalize: strip namespace prefixes (e.g. "GameStates@InGameState" → "InGameState") + var type = objectType; + var atIdx = type.LastIndexOf('@'); + if (atIdx >= 0) type = type[(atIdx + 1)..]; + + return type switch + { + "GameState" => new() + { + [0x00] = "Controller", + }, + "Controller" or "GameStateController" => new() + { + [o.StatesBeginOffset] = "StatesBegin", + [o.ActiveStatesOffset] = "ActiveStates", + [o.InGameStateDirectOffset] = "InGameState", + }, + "InGameState" or "IngameState" => new() + { + [o.EscapeStateOffset] = "EscapeState", + [o.IngameDataFromStateOffset] = "IngameData", + [o.WorldDataFromStateOffset] = "WorldData", + [o.CameraOffset] = "Camera", + }, + "AreaInstance" or "IngameData" or "WorldData" => new() + { + [o.AreaLevelOffset] = "AreaLevel", + [o.AreaHashOffset] = "AreaHash", + [o.ServerDataOffset] = "ServerData", + [o.LocalPlayerDirectOffset] = "LocalPlayer", + [o.EntityListOffset] = "EntityList", + [o.TerrainListOffset] = "Terrain", + [o.LifeComponentOffset1] = "LifeComp1", + }, + "Entity" => new() + { + [o.EntityDetailsOffset] = "Details", + [o.ComponentListOffset] = "Components", + [o.EntityIdOffset] = "EntityId", + [o.EntityFlagsOffset] = "Flags", + }, + "Life" => new() + { + [o.LifeHealthOffset] = "Health", + [o.LifeManaOffset] = "Mana", + [o.LifeEsOffset] = "ES", + }, + "Render" or "Positioned" => new() + { + [o.PositionXOffset] = "X", + [o.PositionYOffset] = "Y", + [o.PositionZOffset] = "Z", + }, + _ => [] + }; + } +} diff --git a/src/Automata.Ui/ViewModels/RobotoViewModel.cs b/src/Automata.Ui/ViewModels/RobotoViewModel.cs index d2def47..d310c12 100644 --- a/src/Automata.Ui/ViewModels/RobotoViewModel.cs +++ b/src/Automata.Ui/ViewModels/RobotoViewModel.cs @@ -1,8 +1,10 @@ using System.Collections.ObjectModel; using System.Numerics; +using Roboto.Memory; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Roboto.Core; +using Roboto.Data; using Roboto.Engine; using Roboto.Input; using Roboto.Navigation; @@ -14,9 +16,9 @@ namespace Automata.Ui.ViewModels; /// public sealed class EntityOverlayData { - public Vector2 PlayerPosition; - public float PlayerZ; - public Matrix4x4? CameraMatrix; + /// Player position at time the entity list was built (cold tick, 10Hz). + public Vector2 SnapshotPlayerPosition; + public float SnapshotPlayerZ; public EntityOverlayEntry[] Entries = []; } @@ -96,14 +98,19 @@ public partial class RobotoViewModel : ObservableObject, IDisposable /// public static volatile EntityOverlayData? OverlayData; + /// + /// Shared GameDataCache for the overlay layer to read camera/player data directly. + /// + public static volatile GameDataCache? SharedCache; + public RobotoViewModel() { var config = new BotConfig(); - var memory = new MemoryAdapter(); + var reader = new GameMemoryReader(); var humanizer = new Humanizer(config); var input = new InterceptionInputController(humanizer); - _engine = new BotEngine(config, memory, input); + _engine = new BotEngine(config, reader, input); _engine.StatusChanged += status => { @@ -119,7 +126,9 @@ public partial class RobotoViewModel : ObservableObject, IDisposable if (_engine.IsRunning) return; var ok = _engine.Start(); IsRunning = _engine.IsRunning; - if (!ok) + if (ok) + SharedCache = _engine.Cache; + else StatusText = _engine.Status; } @@ -142,6 +151,7 @@ public partial class RobotoViewModel : ObservableObject, IDisposable NavStatus = "—"; Entities.Clear(); OverlayData = null; + SharedCache = null; } [RelayCommand] @@ -232,9 +242,8 @@ public partial class RobotoViewModel : ObservableObject, IDisposable { OverlayData = new EntityOverlayData { - PlayerPosition = state.Player.Position, - PlayerZ = state.Player.Z, - CameraMatrix = state.CameraMatrix, + SnapshotPlayerPosition = state.Player.Position, + SnapshotPlayerZ = state.Player.Z, Entries = overlayEntries.ToArray(), }; } diff --git a/src/Automata.Ui/Views/MainWindow.axaml b/src/Automata.Ui/Views/MainWindow.axaml index b4c1ec5..7f1b152 100644 --- a/src/Automata.Ui/Views/MainWindow.axaml +++ b/src/Automata.Ui/Views/MainWindow.axaml @@ -1034,6 +1034,112 @@ + + + + + + + + +