diff --git a/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs b/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs index de046de..b5fcf77 100644 --- a/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs +++ b/src/Automata.Ui/Overlay/Layers/D2dEntityLabelLayer.cs @@ -39,7 +39,6 @@ internal sealed class D2dEntityLabelLayer : ID2dOverlayLayer, IDisposable // 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; @@ -57,8 +56,8 @@ internal sealed class D2dEntityLabelLayer : ID2dOverlayLayer, IDisposable ey += dy; } - // WorldToScreen using camera's view-projection matrix - var worldPos = new Vector4(ex, ey, playerZ, 1f); + // WorldToScreen using camera's view-projection matrix with per-entity Z + label offset + var worldPos = new Vector4(ex, ey, entry.Z + entry.LabelOffset, 1f); var clip = Vector4.Transform(worldPos, mat); // Perspective divide @@ -86,7 +85,7 @@ internal sealed class D2dEntityLabelLayer : ID2dOverlayLayer, IDisposable new RectangleF(labelX - 2, labelY - 1, m.Width + 4, m.Height + 2), ctx.LabelBgBrush); - var brush = entry.Rarity switch + var brush = entry.IsQuestItem ? ctx.Green : entry.Rarity switch { 1 => ctx.MagicBrush, 2 => ctx.RareBrush, diff --git a/src/Automata.Ui/ViewModels/MemoryViewModel.cs b/src/Automata.Ui/ViewModels/MemoryViewModel.cs index 1c9ac12..86093a3 100644 --- a/src/Automata.Ui/ViewModels/MemoryViewModel.cs +++ b/src/Automata.Ui/ViewModels/MemoryViewModel.cs @@ -918,9 +918,10 @@ public partial class MemoryViewModel : ObservableObject foreach (var e in snap.Entities) { if (!e.HasPosition) continue; - if (e.Type is not (EntityType.Player or EntityType.Monster or EntityType.Npc)) continue; + if (e.Type is not (EntityType.Player or EntityType.Monster or EntityType.Npc or EntityType.WorldItem)) continue; if (e.Address == snap.LocalPlayerPtr) continue; if (e.Type == EntityType.Monster && e.IsDead) continue; + if (e.Type == EntityType.WorldItem && !e.IsQuestItem) continue; // Entity position relative to player in grid coords var dx = e.X * worldToGrid - pgx; @@ -940,6 +941,8 @@ public partial class MemoryViewModel : ObservableObject b = 0x00; g = 0x8C; r = 0xFF; break; case EntityType.Monster: // red #FF4444 b = 0x44; g = 0x44; r = 0xFF; break; + case EntityType.WorldItem: // quest items — green #3FB950 + b = 0x50; g = 0xB9; r = 0x3F; break; default: continue; } @@ -1033,7 +1036,7 @@ public partial class MemoryViewModel : ObservableObject var e = sorted[i]; var label = FormatEntityName(e); var value = FormatEntityValue(e); - var color = RarityColor(e.Rarity); + var color = e.IsQuestItem ? "#3fb950" : RarityColor(e.Rarity); if (i < groupNode.Children.Count) { @@ -1608,4 +1611,28 @@ public partial class MemoryViewModel : ObservableObject ScanResult = _reader.Diagnostics!.ProbeQuestAddresses(addresses, 4); } + + [RelayCommand] + private void ScanWorldItemExecute() + { + if (_reader is null || !_reader.IsAttached) + { + ScanResult = "Error: not attached"; + return; + } + + ScanResult = _reader.Diagnostics!.ScanWorldItemComponents(); + } + + [RelayCommand] + private void ScanWorldItemLabelsExecute() + { + if (_reader is null || !_reader.IsAttached) + { + ScanResult = "Error: not attached"; + return; + } + + ScanResult = _reader.Diagnostics!.ScanWorldItemLabelOffsets(); + } } diff --git a/src/Automata.Ui/ViewModels/RobotoViewModel.cs b/src/Automata.Ui/ViewModels/RobotoViewModel.cs index f6a1251..6bd17a1 100644 --- a/src/Automata.Ui/ViewModels/RobotoViewModel.cs +++ b/src/Automata.Ui/ViewModels/RobotoViewModel.cs @@ -28,16 +28,21 @@ public sealed class EntityOverlayData public readonly struct EntityOverlayEntry { - public readonly float X, Y; + public readonly float X, Y, Z; + public readonly float LabelOffset; public readonly string Label; public readonly int Rarity; // 0=Normal, 1=Magic, 2=Rare, 3=Unique + public readonly bool IsQuestItem; - public EntityOverlayEntry(float x, float y, string label, int rarity = 0) + public EntityOverlayEntry(float x, float y, float z, string label, int rarity = 0, bool isQuestItem = false, float labelOffset = 0f) { X = x; Y = y; + Z = z; + LabelOffset = labelOffset; Label = label; Rarity = rarity; + IsQuestItem = isQuestItem; } } @@ -52,11 +57,14 @@ public partial class EntityListItem : ObservableObject public string Distance { get; set; } public float X { get; set; } public float Y { get; set; } + public float Z { get; set; } + public float LabelOffset { get; } public int Rarity { get; } + public bool IsQuestItem { get; } [ObservableProperty] private bool _isChecked; - public EntityListItem(uint id, string label, string category, float distance, float x, float y, int rarity = 0) + public EntityListItem(uint id, string label, string category, float distance, float x, float y, float z, int rarity = 0, bool isQuestItem = false, float labelOffset = 0f) { Id = id; Label = label; @@ -64,7 +72,10 @@ public partial class EntityListItem : ObservableObject Distance = $"{distance:F0}"; X = x; Y = y; + Z = z; + LabelOffset = labelOffset; Rarity = rarity; + IsQuestItem = isQuestItem; } } @@ -541,7 +552,7 @@ public partial class RobotoViewModel : ObservableObject, IDisposable var shortLabel = e.Category == EntityCategory.AreaTransition && e.TransitionName is not null ? $"AreaTransition — {e.TransitionName}" : GetShortLabel(e.Path); - var item = new EntityListItem(e.Id, shortLabel, e.Category.ToString(), e.DistanceToPlayer, e.Position.X, e.Position.Y, (int)e.Rarity); + var item = new EntityListItem(e.Id, shortLabel, e.Category.ToString(), e.DistanceToPlayer, e.Position.X, e.Position.Y, e.Z, (int)e.Rarity, e.IsQuestItem, e.LabelOffset); if (checkedIds.Contains(e.Id)) item.IsChecked = true; Entities.Add(item); @@ -553,7 +564,7 @@ public partial class RobotoViewModel : ObservableObject, IDisposable foreach (var item in Entities) { if (!showAll && !item.IsChecked) continue; - overlayEntries.Add(new EntityOverlayEntry(item.X, item.Y, item.Label, item.Rarity)); + overlayEntries.Add(new EntityOverlayEntry(item.X, item.Y, item.Z, item.Label, item.Rarity, item.IsQuestItem, item.LabelOffset)); } if (overlayEntries.Count > 0) diff --git a/src/Automata.Ui/Views/MainWindow.axaml b/src/Automata.Ui/Views/MainWindow.axaml index cb37b84..b47aa9e 100644 --- a/src/Automata.Ui/Views/MainWindow.axaml +++ b/src/Automata.Ui/Views/MainWindow.axaml @@ -802,6 +802,10 @@ Padding="10,4" FontWeight="Bold" Margin="0,0,6,4" />