diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index dd1e7aa30..4d2a005ae 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -3,13 +3,19 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; +using Dalamud.Game.AddonEventManager; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Memory; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; -using Serilog; + +using DalamudAddonEventManager = Dalamud.Game.AddonEventManager.AddonEventManager; namespace Dalamud.Game.Gui.Dtr; @@ -25,7 +31,12 @@ namespace Dalamud.Game.Gui.Dtr; public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; + private const uint MouseOverEventIdOffset = 10000; + private const uint MouseOutEventIdOffset = 20000; + private const uint MouseClickEventIdOffset = 30000; + private static readonly ModuleLog Log = new("DtrBar"); + [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -35,12 +46,24 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - private List entries = new(); + [ServiceManager.ServiceDependency] + private readonly DalamudAddonEventManager uiEventManager = Service.Get(); + + private readonly DtrBarAddressResolver address; + private readonly List entries = new(); + private readonly Hook onAddonDrawHook; + private readonly Hook onAddonRequestedUpdateHook; private uint runningNodeIds = BaseNodeId; [ServiceManager.ServiceConstructor] - private DtrBar() + private DtrBar(SigScanner sigScanner) { + this.address = new DtrBarAddressResolver(); + this.address.Setup(sigScanner); + + this.onAddonDrawHook = Hook.FromAddress(this.address.AtkUnitBaseDraw, this.OnAddonDrawDetour); + this.onAddonRequestedUpdateHook = Hook.FromAddress(this.address.AddonRequestedUpdate, this.OnAddonRequestedUpdateDetour); + this.framework.Update += this.Update; this.configuration.DtrOrder ??= new List(); @@ -48,6 +71,10 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.configuration.QueueSave(); } + private delegate void AddonDrawDelegate(AtkUnitBase* addon); + + private delegate void AddonRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + /// public DtrBarEntry Get(string title, SeString? text = null) { @@ -70,6 +97,9 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar /// void IDisposable.Dispose() { + this.onAddonDrawHook.Dispose(); + this.onAddonRequestedUpdateHook.Dispose(); + foreach (var entry in this.entries) this.RemoveNode(entry.TextNode); @@ -130,6 +160,13 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar return xPos.CompareTo(yPos); }); } + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction() + { + this.onAddonDrawHook.Enable(); + this.onAddonRequestedUpdateHook.Enable(); + } private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); @@ -148,7 +185,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (!this.CheckForDalamudNodes()) this.RecreateNodes(); - var collisionNode = dtr->UldManager.NodeList[1]; + var collisionNode = dtr->GetNodeById(17); if (collisionNode == null) return; // If we are drawing backwards, we should start from the right side of the collision node. That is, @@ -157,28 +194,24 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar ? collisionNode->X + collisionNode->Width : collisionNode->X; - for (var i = 0; i < this.entries.Count; i++) + foreach (var data in this.entries) { - var data = this.entries[i]; var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown; - if (data.Dirty && data.Added && data.Text != null && data.TextNode != null) + if (data is { Dirty: true, Added: true, Text: not null, TextNode: not null }) { var node = data.TextNode; - node->SetText(data.Text?.Encode()); + node->SetText(data.Text.Encode()); ushort w = 0, h = 0; - if (isHide) + if (!isHide) { - node->AtkResNode.ToggleVisibility(false); - } - else - { - node->AtkResNode.ToggleVisibility(true); node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); node->AtkResNode.SetWidth(w); } + node->AtkResNode.ToggleVisibility(!isHide); + data.Dirty = false; } @@ -202,8 +235,62 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); } } + } + } - this.entries[i] = data; + // This hooks all AtkUnitBase.Draw calls, then checks for our specific addon name. + // AddonDtr doesn't implement it's own Draw method, would need to replace vtable entry to be more efficient. + private void OnAddonDrawDetour(AtkUnitBase* addon) + { + this.onAddonDrawHook!.Original(addon); + + try + { + if (MemoryHelper.ReadString((nint)addon->Name, 0x20) is not "_DTR") return; + + this.UpdateNodePositions(addon); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonDraw."); + } + } + + private void UpdateNodePositions(AtkUnitBase* addon) + { + var targetSize = (ushort)this.CalculateTotalSize(); + addon->RootNode->SetWidth(targetSize); + + // If we grow to the right, we need to left-justify the original elements. + // else if we grow to the left, the game right-justifies it for us. + if (this.configuration.DtrSwapDirection) + { + var sizeOffset = addon->GetNodeById(17)->GetX(); + + var node = addon->RootNode->ChildNode; + while (node is not null) + { + if (node->NodeID < 1000 && node->IsVisible) + { + node->SetX(node->GetX() - sizeOffset); + } + + node = node->PrevSiblingNode; + } + } + } + + private void OnAddonRequestedUpdateDetour(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + this.onAddonRequestedUpdateHook.Original(addon, numberArrayData, stringArrayData); + + try + { + this.UpdateNodePositions(addon); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRequestedUpdate."); } } @@ -235,11 +322,37 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } + // Calculates the total width the dtr bar should be + private float CalculateTotalSize() + { + var addon = this.GetDtr(); + if (addon is null || addon->RootNode is null || addon->UldManager.NodeList is null) return 0; + + var totalSize = 0.0f; + + foreach (var index in Enumerable.Range(0, addon->UldManager.NodeListCount)) + { + var node = addon->UldManager.NodeList[index]; + + // Node 17 is the default CollisionNode that fits over the existing elements + if (node->NodeID is 17) totalSize += node->Width; + + // Node > 1000, are our custom nodes + if (node->NodeID is > 1000) totalSize += node->Width + this.configuration.DtrSpacing; + } + + return totalSize; + } + private bool AddNode(AtkTextNode* node) { var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; + this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); + this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); + this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); + var lastChild = dtr->RootNode->ChildNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; Log.Debug($"Found last sibling: {(ulong)lastChild:X}"); @@ -251,6 +364,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar Log.Debug("Set last sibling of DTR and updated child count"); dtr->UldManager.UpdateDrawNodeList(); + dtr->UpdateCollisionNodeList(false); Log.Debug("Updated node draw list"); return true; } @@ -260,6 +374,10 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; + this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)node, AddonEventType.MouseOver); + this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)node, AddonEventType.MouseOut); + this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)node, AddonEventType.MouseClick); + var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; @@ -272,25 +390,23 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1); Log.Debug("Set last sibling of DTR and updated child count"); dtr->UldManager.UpdateDrawNodeList(); + dtr->UpdateCollisionNodeList(false); Log.Debug("Updated node draw list"); return true; } private AtkTextNode* MakeNode(uint nodeId) { - var newTextNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8); + var newTextNode = IMemorySpace.GetUISpace()->Create(); if (newTextNode == null) { - Log.Debug("Failed to allocate memory for text node"); + Log.Debug("Failed to allocate memory for AtkTextNode"); return null; } - IMemorySpace.Memset(newTextNode, 0, (ulong)sizeof(AtkTextNode)); - newTextNode->Ctor(); - newTextNode->AtkResNode.NodeID = nodeId; newTextNode->AtkResNode.Type = NodeType.Text; - newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop; + newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents; newTextNode->AtkResNode.DrawFlags = 12; newTextNode->AtkResNode.SetWidth(22); newTextNode->AtkResNode.SetHeight(22); @@ -304,16 +420,49 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar newTextNode->SetText(" "); - newTextNode->TextColor.R = 255; - newTextNode->TextColor.G = 255; - newTextNode->TextColor.B = 255; - newTextNode->TextColor.A = 255; - - newTextNode->EdgeColor.R = 142; - newTextNode->EdgeColor.G = 106; - newTextNode->EdgeColor.B = 12; - newTextNode->EdgeColor.A = 255; + newTextNode->TextColor = new ByteColor { R = 255, G = 255, B = 255, A = 255 }; + newTextNode->EdgeColor = new ByteColor { R = 142, G = 106, B = 12, A = 255 }; return newTextNode; } + + private void DtrEventHandler(AddonEventType atkEventType, IntPtr atkUnitBase, IntPtr atkResNode) + { + var addon = (AtkUnitBase*)atkUnitBase; + var node = (AtkResNode*)atkResNode; + + if (this.entries.FirstOrDefault(entry => entry.TextNode == node) is not { } dtrBarEntry) return; + + if (dtrBarEntry is { Tooltip: not null }) + { + switch (atkEventType) + { + case AddonEventType.MouseOver: + AtkStage.GetSingleton()->TooltipManager.ShowTooltip(addon->ID, node, dtrBarEntry.Tooltip.Encode()); + break; + + case AddonEventType.MouseOut: + AtkStage.GetSingleton()->TooltipManager.HideTooltip(addon->ID); + break; + } + } + + if (dtrBarEntry is { OnClick: not null }) + { + switch (atkEventType) + { + case AddonEventType.MouseOver: + this.uiEventManager.SetCursor(AddonCursorType.Clickable); + break; + + case AddonEventType.MouseOut: + this.uiEventManager.ResetCursor(); + break; + + case AddonEventType.MouseClick: + dtrBarEntry.OnClick.Invoke(); + break; + } + } + } } diff --git a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs new file mode 100644 index 000000000..1e6fd09cd --- /dev/null +++ b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs @@ -0,0 +1,29 @@ +namespace Dalamud.Game.Gui.Dtr; + +/// +/// DtrBar memory address resolver. +/// +public class DtrBarAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the AtkUnitBaseDraw method. + /// This is the base handler for all addons. + /// We will use this here because _DTR does not have a overloaded handler, so we must use the base handler. + /// + public nint AtkUnitBaseDraw { get; private set; } + + /// + /// Gets the address of the DTRRequestUpdate method. + /// + public nint AddonRequestedUpdate { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(SigScanner scanner) + { + this.AtkUnitBaseDraw = scanner.ScanText("48 83 EC 28 F6 81 ?? ?? ?? ?? ?? 4C 8B C1"); + this.AddonRequestedUpdate = scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B BA ?? ?? ?? ?? 48 8B F1 49 8B 98 ?? ?? ?? ?? 33 D2"); + } +} diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index c5bdb7e85..f04e1427d 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -41,6 +41,16 @@ public sealed unsafe class DtrBarEntry : IDisposable this.Dirty = true; } } + + /// + /// Gets or sets a tooltip to be shown when the user mouses over the dtr entry. + /// + public SeString? Tooltip { get; set; } + + /// + /// Gets or sets a action to be invoked when the user clicks on the dtr entry. + /// + public Action? OnClick { get; set; } /// /// Gets or sets a value indicating whether this entry is visible.