Add Tooltips, and OnClick actions to DtrBarEntries

This commit is contained in:
MidoriKami 2023-09-04 23:45:17 -07:00
parent ce4392e109
commit 2439bcccbd
3 changed files with 218 additions and 30 deletions

View file

@ -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<Framework>.Get();
@ -35,12 +46,24 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
[ServiceManager.ServiceDependency]
private readonly DalamudConfiguration configuration = Service<DalamudConfiguration>.Get();
private List<DtrBarEntry> entries = new();
[ServiceManager.ServiceDependency]
private readonly DalamudAddonEventManager uiEventManager = Service<DalamudAddonEventManager>.Get();
private readonly DtrBarAddressResolver address;
private readonly List<DtrBarEntry> entries = new();
private readonly Hook<AddonDrawDelegate> onAddonDrawHook;
private readonly Hook<AddonRequestedUpdateDelegate> onAddonRequestedUpdateHook;
private uint runningNodeIds = BaseNodeId;
[ServiceManager.ServiceConstructor]
private DtrBar()
private DtrBar(SigScanner sigScanner)
{
this.address = new DtrBarAddressResolver();
this.address.Setup(sigScanner);
this.onAddonDrawHook = Hook<AddonDrawDelegate>.FromAddress(this.address.AtkUnitBaseDraw, this.OnAddonDrawDetour);
this.onAddonRequestedUpdateHook = Hook<AddonRequestedUpdateDelegate>.FromAddress(this.address.AddonRequestedUpdate, this.OnAddonRequestedUpdateDetour);
this.framework.Update += this.Update;
this.configuration.DtrOrder ??= new List<string>();
@ -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);
/// <inheritdoc/>
public DtrBarEntry Get(string title, SeString? text = null)
{
@ -70,6 +97,9 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
/// <inheritdoc/>
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<AtkTextNode>();
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;
}
}
}
}

View file

@ -0,0 +1,29 @@
namespace Dalamud.Game.Gui.Dtr;
/// <summary>
/// DtrBar memory address resolver.
/// </summary>
public class DtrBarAddressResolver : BaseAddressResolver
{
/// <summary>
/// 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.
/// </summary>
public nint AtkUnitBaseDraw { get; private set; }
/// <summary>
/// Gets the address of the DTRRequestUpdate method.
/// </summary>
public nint AddonRequestedUpdate { get; private set; }
/// <summary>
/// Scan for and setup any configured address pointers.
/// </summary>
/// <param name="scanner">The signature scanner to facilitate setup.</param>
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");
}
}

View file

@ -41,6 +41,16 @@ public sealed unsafe class DtrBarEntry : IDisposable
this.Dirty = true;
}
}
/// <summary>
/// Gets or sets a tooltip to be shown when the user mouses over the dtr entry.
/// </summary>
public SeString? Tooltip { get; set; }
/// <summary>
/// Gets or sets a action to be invoked when the user clicks on the dtr entry.
/// </summary>
public Action? OnClick { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this entry is visible.