From c095f99cd1378db36a31972ac870f13b354ec9a9 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Fri, 1 Sep 2023 21:53:34 -0700
Subject: [PATCH 01/13] Add AddonEventManager
---
.../AddonEventManager/AddonEventManager.cs | 207 ++++++++++++++++++
.../AddonEventManagerAddressResolver.cs | 21 ++
.../Game/AddonEventManager/AddonEventType.cs | 132 +++++++++++
Dalamud/Plugin/Services/IAddonEventManager.cs | 36 +++
4 files changed, 396 insertions(+)
create mode 100644 Dalamud/Game/AddonEventManager/AddonEventManager.cs
create mode 100644 Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs
create mode 100644 Dalamud/Game/AddonEventManager/AddonEventType.cs
create mode 100644 Dalamud/Plugin/Services/IAddonEventManager.cs
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
new file mode 100644
index 000000000..8a0f16d1e
--- /dev/null
+++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
@@ -0,0 +1,207 @@
+using System;
+using System.Collections.Generic;
+
+using Dalamud.Hooking;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.AddonEventManager;
+
+///
+/// Service provider for addon event management.
+///
+[InterfaceVersion("1.0")]
+[ServiceManager.EarlyLoadedService]
+internal unsafe class AddonEventManager : IDisposable, IServiceType
+{
+ // The starting value for param key ranges.
+ // ascii `DD` 0x4444 chosen for the key start, this just has to be larger than anything vanilla makes.
+ private const uint ParamKeyStart = 0x44440000;
+
+ // The range each plugin is allowed to use.
+ // 65,536 per plugin should be reasonable.
+ private const uint ParamKeyPluginRange = 0x10000;
+
+ // The maximum range allowed to be given to a plugin.
+ // 20,560 maximum plugins should be reasonable.
+ // 202,113,024 maximum event handlers should be reasonable.
+ private const uint ParamKeyMax = 0x50500000;
+
+ private static readonly ModuleLog Log = new("AddonEventManager");
+ private readonly AddonEventManagerAddressResolver address;
+ private readonly Hook onGlobalEventHook;
+ private readonly Dictionary eventHandlers;
+
+ private uint currentPluginParamStart = ParamKeyStart;
+
+ [ServiceManager.ServiceConstructor]
+ private AddonEventManager(SigScanner sigScanner)
+ {
+ this.address = new AddonEventManagerAddressResolver();
+ this.address.Setup(sigScanner);
+
+ this.eventHandlers = new Dictionary();
+
+ this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler);
+ }
+
+ private delegate nint GlobalEventHandlerDetour(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown);
+
+ ///
+ public void Dispose()
+ {
+ this.onGlobalEventHook.Dispose();
+ }
+
+ ///
+ /// Get the start value for a new plugin register.
+ ///
+ /// A unique starting range for event handlers.
+ /// Throws when attempting to register too many event handlers.
+ public uint GetPluginParamStart()
+ {
+ if (this.currentPluginParamStart >= ParamKeyMax)
+ {
+ throw new Exception("Maximum number of event handlers reached.");
+ }
+
+ var result = this.currentPluginParamStart;
+
+ this.currentPluginParamStart += ParamKeyPluginRange;
+ return result;
+ }
+
+ ///
+ /// Adds a event handler to be triggered when the specified id is received.
+ ///
+ /// Unique id for this event handler.
+ /// The event handler to be called.
+ public void AddEvent(uint eventId, IAddonEventManager.AddonEventHandler handler) => this.eventHandlers.Add(eventId, handler);
+
+ ///
+ /// Removes the event handler with the specified id.
+ ///
+ /// Event id to unregister.
+ public void RemoveEvent(uint eventId) => this.eventHandlers.Remove(eventId);
+
+ [ServiceManager.CallWhenServicesReady]
+ private void ContinueConstruction()
+ {
+ this.onGlobalEventHook.Enable();
+ }
+
+ private nint GlobalEventHandler(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown)
+ {
+ try
+ {
+ if (this.eventHandlers.TryGetValue(eventParam, out var handler))
+ {
+ try
+ {
+ handler?.Invoke((AddonEventType)eventType, (nint)atkUnitBase, (nint)eventData[0]);
+ return nint.Zero;
+ }
+ catch (Exception exception)
+ {
+ Log.Error(exception, "Exception in GlobalEventHandler custom event invoke.");
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in GlobalEventHandler attempting to retrieve event handler.");
+ }
+
+ return this.onGlobalEventHook!.Original(atkUnitBase, eventType, eventParam, eventData, unknown);
+ }
+}
+
+///
+/// Plugin-scoped version of a AddonEventManager service.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager
+{
+ private static readonly ModuleLog Log = new("AddonEventManager");
+
+ [ServiceManager.ServiceDependency]
+ private readonly AddonEventManager baseEventManager = Service.Get();
+
+ private readonly uint paramKeyStartRange;
+ private readonly List activeParamKeys;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public AddonEventManagerPluginScoped()
+ {
+ this.paramKeyStartRange = this.baseEventManager.GetPluginParamStart();
+ this.activeParamKeys = new List();
+ }
+
+ ///
+ public void Dispose()
+ {
+ foreach (var activeKey in this.activeParamKeys)
+ {
+ this.baseEventManager.RemoveEvent(activeKey);
+ }
+ }
+
+ ///
+ public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
+ {
+ if (eventId < 0x10000)
+ {
+ var type = (AtkEventType)eventType;
+ var node = (AtkResNode*)atkResNode;
+ var eventListener = (AtkEventListener*)atkUnitBase;
+ var uniqueId = eventId + this.paramKeyStartRange;
+
+ if (!this.activeParamKeys.Contains(uniqueId))
+ {
+ node->AddEvent(type, uniqueId, eventListener, node, true);
+ this.baseEventManager.AddEvent(uniqueId, eventHandler);
+
+ this.activeParamKeys.Add(uniqueId);
+ }
+ else
+ {
+ Log.Warning($"Attempted to register already registered eventId: {eventId}");
+ }
+ }
+ else
+ {
+ Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10000}");
+ }
+ }
+
+ ///
+ public void RemoveEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType)
+ {
+ var type = (AtkEventType)eventType;
+ var node = (AtkResNode*)atkResNode;
+ var eventListener = (AtkEventListener*)atkUnitBase;
+ var uniqueId = eventId + this.paramKeyStartRange;
+
+ if (this.activeParamKeys.Contains(uniqueId))
+ {
+ node->RemoveEvent(type, uniqueId, eventListener, true);
+ this.baseEventManager.RemoveEvent(uniqueId);
+
+ this.activeParamKeys.Remove(uniqueId);
+ }
+ else
+ {
+ Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}");
+ }
+ }
+}
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs
new file mode 100644
index 000000000..8dcf81580
--- /dev/null
+++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs
@@ -0,0 +1,21 @@
+namespace Dalamud.Game.AddonEventManager;
+
+///
+/// AddonEventManager memory address resolver.
+///
+internal class AddonEventManagerAddressResolver : BaseAddressResolver
+{
+ ///
+ /// Gets the address of the global atkevent handler
+ ///
+ public nint GlobalEventHandler { get; private set; }
+
+ ///
+ /// Scan for and setup any configured address pointers.
+ ///
+ /// The signature scanner to facilitate setup.
+ protected override void Setup64Bit(SigScanner scanner)
+ {
+ this.GlobalEventHandler = scanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2");
+ }
+}
diff --git a/Dalamud/Game/AddonEventManager/AddonEventType.cs b/Dalamud/Game/AddonEventManager/AddonEventType.cs
new file mode 100644
index 000000000..eef9763ff
--- /dev/null
+++ b/Dalamud/Game/AddonEventManager/AddonEventType.cs
@@ -0,0 +1,132 @@
+namespace Dalamud.Game.AddonEventManager;
+
+///
+/// Reimplementation of AtkEventType.
+///
+public enum AddonEventType : byte
+{
+ ///
+ /// Mouse Down.
+ ///
+ MouseDown = 3,
+
+ ///
+ /// Mouse Up.
+ ///
+ MouseUp = 4,
+
+ ///
+ /// Mouse Move.
+ ///
+ MouseMove = 5,
+
+ ///
+ /// Mouse Over.
+ ///
+ MouseOver = 6,
+
+ ///
+ /// Mouse Out.
+ ///
+ MouseOut = 7,
+
+ ///
+ /// Mouse Click.
+ ///
+ MouseClick = 9,
+
+ ///
+ /// Input Received.
+ ///
+ InputReceived = 12,
+
+ ///
+ /// Focus Start.
+ ///
+ FocusStart = 18,
+
+ ///
+ /// Focus Stop.
+ ///
+ FocusStop = 19,
+
+ ///
+ /// Button Press, sent on MouseDown on Button.
+ ///
+ ButtonPress = 23,
+
+ ///
+ /// Button Release, sent on MouseUp and MouseOut.
+ ///
+ ButtonRelease = 24,
+
+ ///
+ /// Button Click, sent on MouseUp and MouseClick on button.
+ ///
+ ButtonClick = 25,
+
+ ///
+ /// List Item RollOver.
+ ///
+ ListItemRollOver = 33,
+
+ ///
+ /// List Item Roll Out.
+ ///
+ ListItemRollOut = 34,
+
+ ///
+ /// List Item Toggle.
+ ///
+ ListItemToggle = 35,
+
+ ///
+ /// Drag Drop Roll Over.
+ ///
+ DragDropRollOver = 52,
+
+ ///
+ /// Drag Drop Roll Out.
+ ///
+ DragDropRollOut = 53,
+
+ ///
+ /// Drag Drop Unknown.
+ ///
+ DragDropUnk54 = 54,
+
+ ///
+ /// Drag Drop Unknown.
+ ///
+ DragDropUnk55 = 55,
+
+ ///
+ /// Icon Text Roll Over.
+ ///
+ IconTextRollOver = 56,
+
+ ///
+ /// Icon Text Roll Out.
+ ///
+ IconTextRollOut = 57,
+
+ ///
+ /// Icon Text Click.
+ ///
+ IconTextClick = 58,
+
+ ///
+ /// Window Roll Over.
+ ///
+ WindowRollOver = 67,
+
+ ///
+ /// Window Roll Out.
+ ///
+ WindowRollOut = 68,
+
+ ///
+ /// Window Change Scale.
+ ///
+ WindowChangeScale = 69,
+}
diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs
new file mode 100644
index 000000000..8e2b1f67b
--- /dev/null
+++ b/Dalamud/Plugin/Services/IAddonEventManager.cs
@@ -0,0 +1,36 @@
+using Dalamud.Game.AddonEventManager;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// Service provider for addon event management.
+///
+public interface IAddonEventManager
+{
+ ///
+ /// Delegate to be called when an event is received.
+ ///
+ /// Event type for this event handler.
+ /// The parent addon for this event handler.
+ /// The specific node that will trigger this event handler.
+ public delegate void AddonEventHandler(AddonEventType atkEventType, nint atkUnitBase, nint atkResNode);
+
+ ///
+ /// Registers an event handler for the specified addon, node, and type.
+ ///
+ /// Unique Id for this event, maximum 0x10000.
+ /// The parent addon for this event.
+ /// The node that will trigger this event.
+ /// The event type for this event.
+ /// The handler to call when event is triggered.
+ void AddEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler);
+
+ ///
+ /// Unregisters an event handler with the specified event id and event type.
+ ///
+ /// The Unique Id for this event.
+ /// The parent addon for this event.
+ /// The node for this event.
+ /// The event type for this event.
+ void RemoveEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType);
+}
From 7ac37a579b740541fc36b7f9d4131634c823b1ea Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Fri, 1 Sep 2023 23:33:10 -0700
Subject: [PATCH 02/13] [AddonEventManager] Add null check
---
Dalamud/Game/AddonEventManager/AddonEventManager.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
index 8a0f16d1e..77111421f 100644
--- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs
+++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
@@ -97,7 +97,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
{
try
{
- if (this.eventHandlers.TryGetValue(eventParam, out var handler))
+ if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null)
{
try
{
From ad06b5f05486d92be0d2ae28f95c05244159bd84 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Sat, 2 Sep 2023 00:06:54 -0700
Subject: [PATCH 03/13] [AddonEventManager] Add Cursor Control
---
.../Game/AddonEventManager/AddonCursorType.cs | 97 +++++++++++++++++++
.../AddonEventManager/AddonEventManager.cs | 84 +++++++++++++++-
.../AddonEventManagerAddressResolver.cs | 8 +-
Dalamud/Plugin/Services/IAddonEventManager.cs | 11 +++
4 files changed, 196 insertions(+), 4 deletions(-)
create mode 100644 Dalamud/Game/AddonEventManager/AddonCursorType.cs
diff --git a/Dalamud/Game/AddonEventManager/AddonCursorType.cs b/Dalamud/Game/AddonEventManager/AddonCursorType.cs
new file mode 100644
index 000000000..8ba3a901b
--- /dev/null
+++ b/Dalamud/Game/AddonEventManager/AddonCursorType.cs
@@ -0,0 +1,97 @@
+namespace Dalamud.Game.AddonEventManager;
+
+///
+/// Reimplementation of CursorType.
+///
+public enum AddonCursorType
+{
+ ///
+ /// Arrow.
+ ///
+ Arrow,
+
+ ///
+ /// Boot.
+ ///
+ Boot,
+
+ ///
+ /// Search.
+ ///
+ Search,
+
+ ///
+ /// Chat Pointer.
+ ///
+ ChatPointer,
+
+ ///
+ /// Interact.
+ ///
+ Interact,
+
+ ///
+ /// Attack.
+ ///
+ Attack,
+
+ ///
+ /// Hand.
+ ///
+ Hand,
+
+ ///
+ /// Resizeable Left-Right.
+ ///
+ ResizeWE,
+
+ ///
+ /// Resizeable Up-Down.
+ ///
+ ResizeNS,
+
+ ///
+ /// Resizeable.
+ ///
+ ResizeNWSR,
+
+ ///
+ /// Resizeable 4-way.
+ ///
+ ResizeNESW,
+
+ ///
+ /// Clickable.
+ ///
+ Clickable,
+
+ ///
+ /// Text Input.
+ ///
+ TextInput,
+
+ ///
+ /// Text Click.
+ ///
+ TextClick,
+
+ ///
+ /// Grab.
+ ///
+ Grab,
+
+ ///
+ /// Chat Bubble.
+ ///
+ ChatBubble,
+
+ ///
+ /// No Access.
+ ///
+ NoAccess,
+
+ ///
+ /// Hidden.
+ ///
+ Hidden,
+}
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
index 77111421f..ede59a9a9 100644
--- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs
+++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
@@ -6,6 +6,7 @@ using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.AddonEventManager;
@@ -32,9 +33,13 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
private static readonly ModuleLog Log = new("AddonEventManager");
private readonly AddonEventManagerAddressResolver address;
- private readonly Hook onGlobalEventHook;
+ private readonly Hook onGlobalEventHook;
+ private readonly Hook onUpdateCursor;
private readonly Dictionary eventHandlers;
+ private AddonCursorType currentCursor;
+ private bool cursorSet;
+
private uint currentPluginParamStart = ParamKeyStart;
[ServiceManager.ServiceConstructor]
@@ -44,16 +49,21 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
this.address.Setup(sigScanner);
this.eventHandlers = new Dictionary();
+ this.currentCursor = AddonCursorType.Arrow;
- this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler);
+ this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler);
+ this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
}
- private delegate nint GlobalEventHandlerDetour(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown);
+ private delegate nint GlobalEventHandlerDelegate(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown);
+
+ private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
///
public void Dispose()
{
this.onGlobalEventHook.Dispose();
+ this.onUpdateCursor.Dispose();
}
///
@@ -86,11 +96,31 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
///
/// Event id to unregister.
public void RemoveEvent(uint eventId) => this.eventHandlers.Remove(eventId);
+
+ ///
+ /// Sets the game cursor.
+ ///
+ /// Cursor type to set.
+ public void SetCursor(AddonCursorType cursor)
+ {
+ this.currentCursor = cursor;
+ this.cursorSet = true;
+ }
+
+ ///
+ /// Resets and un-forces custom cursor.
+ ///
+ public void ResetCursor()
+ {
+ this.currentCursor = AddonCursorType.Arrow;
+ this.cursorSet = false;
+ }
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.onGlobalEventHook.Enable();
+ this.onUpdateCursor.Enable();
}
private nint GlobalEventHandler(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown)
@@ -117,6 +147,31 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
return this.onGlobalEventHook!.Original(atkUnitBase, eventType, eventParam, eventData, unknown);
}
+
+ private nint UpdateCursorDetour(RaptureAtkModule* module)
+ {
+ try
+ {
+ var atkStage = AtkStage.GetSingleton();
+
+ if (this.cursorSet && atkStage is not null)
+ {
+ var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
+ if (cursor != this.currentCursor)
+ {
+ AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.currentCursor, 1);
+ }
+
+ return nint.Zero;
+ }
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in UpdateCursorDetour.");
+ }
+
+ return this.onUpdateCursor!.Original(module);
+ }
}
///
@@ -137,6 +192,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
private readonly uint paramKeyStartRange;
private readonly List activeParamKeys;
+ private bool isForcingCursor;
///
/// Initializes a new instance of the class.
@@ -154,6 +210,12 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
{
this.baseEventManager.RemoveEvent(activeKey);
}
+
+ // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared.
+ if (this.isForcingCursor)
+ {
+ this.baseEventManager.ResetCursor();
+ }
}
///
@@ -204,4 +266,20 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}");
}
}
+
+ ///
+ public void SetCursor(AddonCursorType cursor)
+ {
+ this.isForcingCursor = true;
+
+ this.baseEventManager.SetCursor(cursor);
+ }
+
+ ///
+ public void ResetCursor()
+ {
+ this.isForcingCursor = false;
+
+ this.baseEventManager.ResetCursor();
+ }
}
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs
index 8dcf81580..5cfa51149 100644
--- a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs
+++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs
@@ -6,9 +6,14 @@
internal class AddonEventManagerAddressResolver : BaseAddressResolver
{
///
- /// Gets the address of the global atkevent handler
+ /// Gets the address of the global AtkEvent handler.
///
public nint GlobalEventHandler { get; private set; }
+
+ ///
+ /// Gets the address of the AtkModule UpdateCursor method.
+ ///
+ public nint UpdateCursor { get; private set; }
///
/// Scan for and setup any configured address pointers.
@@ -17,5 +22,6 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver
protected override void Setup64Bit(SigScanner scanner)
{
this.GlobalEventHandler = scanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2");
+ this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE");
}
}
diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs
index 8e2b1f67b..f052ed607 100644
--- a/Dalamud/Plugin/Services/IAddonEventManager.cs
+++ b/Dalamud/Plugin/Services/IAddonEventManager.cs
@@ -33,4 +33,15 @@ public interface IAddonEventManager
/// The node for this event.
/// The event type for this event.
void RemoveEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType);
+
+ ///
+ /// Force the game cursor to be the specified cursor.
+ ///
+ /// Which cursor to use.
+ void SetCursor(AddonCursorType cursor);
+
+ ///
+ /// Un-forces the game cursor.
+ ///
+ void ResetCursor();
}
From 712a492c89b919ad38c4f293da0308a974d74e54 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Sat, 2 Sep 2023 09:41:21 -0700
Subject: [PATCH 04/13] [AddonEventController] Use custom AtkEventListener
---
.../AddonEventManager/AddonEventListener.cs | 87 +++++++++++++++
.../AddonEventManager/AddonEventManager.cs | 103 +++++++++---------
.../AddonEventManagerAddressResolver.cs | 6 -
3 files changed, 141 insertions(+), 55 deletions(-)
create mode 100644 Dalamud/Game/AddonEventManager/AddonEventListener.cs
diff --git a/Dalamud/Game/AddonEventManager/AddonEventListener.cs b/Dalamud/Game/AddonEventManager/AddonEventListener.cs
new file mode 100644
index 000000000..cb0aa1502
--- /dev/null
+++ b/Dalamud/Game/AddonEventManager/AddonEventListener.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Runtime.InteropServices;
+
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.AddonEventManager;
+
+///
+/// Event listener class for managing custom events.
+///
+// Custom event handler tech provided by Pohky, implemented by MidoriKami
+internal unsafe class AddonEventListener : IDisposable
+{
+ private ReceiveEventDelegate? receiveEventDelegate;
+
+ private AtkEventListener* eventListener;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The managed handler to send events to.
+ public AddonEventListener(ReceiveEventDelegate eventHandler)
+ {
+ this.receiveEventDelegate = eventHandler;
+
+ this.eventListener = (AtkEventListener*)Marshal.AllocHGlobal(sizeof(AtkEventListener));
+ this.eventListener->vtbl = (void*)Marshal.AllocHGlobal(sizeof(void*) * 3);
+ this.eventListener->vfunc[0] = (delegate* unmanaged)&NullSub;
+ this.eventListener->vfunc[1] = (delegate* unmanaged)&NullSub;
+ this.eventListener->vfunc[2] = (void*)Marshal.GetFunctionPointerForDelegate(this.receiveEventDelegate);
+ }
+
+ ///
+ /// Delegate for receiving custom events.
+ ///
+ /// Pointer to the event listener.
+ /// Event type.
+ /// Unique Id for this event.
+ /// Event Data.
+ /// Unknown Parameter.
+ public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown);
+
+ ///
+ public void Dispose()
+ {
+ if (this.eventListener is null) return;
+
+ Marshal.FreeHGlobal((nint)this.eventListener->vtbl);
+ Marshal.FreeHGlobal((nint)this.eventListener);
+
+ this.eventListener = null;
+ this.receiveEventDelegate = null;
+ }
+
+ ///
+ /// Register an event to this event handler.
+ ///
+ /// Addon that triggers this event.
+ /// Node to attach event to.
+ /// Event type to trigger this event.
+ /// Unique id for this event.
+ public void RegisterEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param)
+ {
+ if (node is null) return;
+
+ node->AddEvent(eventType, param, this.eventListener, (AtkResNode*)addon, false);
+ }
+
+ ///
+ /// Unregister an event from this event handler.
+ ///
+ /// Node to remove the event from.
+ /// Event type that this event is for.
+ /// Unique id for this event.
+ public void UnregisterEvent(AtkResNode* node, AtkEventType eventType, uint param)
+ {
+ if (node is null) return;
+
+ node->RemoveEvent(eventType, param, this.eventListener, false);
+ }
+
+ [UnmanagedCallersOnly]
+ private static void NullSub()
+ {
+ /* do nothing */
+ }
+}
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
index ede59a9a9..c6e61fe5a 100644
--- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs
+++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
@@ -19,23 +19,22 @@ namespace Dalamud.Game.AddonEventManager;
internal unsafe class AddonEventManager : IDisposable, IServiceType
{
// The starting value for param key ranges.
- // ascii `DD` 0x4444 chosen for the key start, this just has to be larger than anything vanilla makes.
- private const uint ParamKeyStart = 0x44440000;
+ private const uint ParamKeyStart = 0x0000_0000;
// The range each plugin is allowed to use.
- // 65,536 per plugin should be reasonable.
- private const uint ParamKeyPluginRange = 0x10000;
+ // 1,048,576 per plugin should be reasonable.
+ private const uint ParamKeyPluginRange = 0x10_0000;
// The maximum range allowed to be given to a plugin.
- // 20,560 maximum plugins should be reasonable.
- // 202,113,024 maximum event handlers should be reasonable.
- private const uint ParamKeyMax = 0x50500000;
+ // 1,048,576 maximum plugins should be reasonable.
+ private const uint ParamKeyMax = 0xFFF0_0000;
private static readonly ModuleLog Log = new("AddonEventManager");
+
private readonly AddonEventManagerAddressResolver address;
- private readonly Hook onGlobalEventHook;
private readonly Hook onUpdateCursor;
private readonly Dictionary eventHandlers;
+ private readonly AddonEventListener eventListener;
private AddonCursorType currentCursor;
private bool cursorSet;
@@ -48,21 +47,20 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
this.address = new AddonEventManagerAddressResolver();
this.address.Setup(sigScanner);
+ this.eventListener = new AddonEventListener(this.OnCustomEvent);
+
this.eventHandlers = new Dictionary();
this.currentCursor = AddonCursorType.Arrow;
- this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler);
this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
}
- private delegate nint GlobalEventHandlerDelegate(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown);
-
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
///
public void Dispose()
{
- this.onGlobalEventHook.Dispose();
+ this.eventListener.Dispose();
this.onUpdateCursor.Dispose();
}
@@ -85,17 +83,39 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
}
///
- /// Adds a event handler to be triggered when the specified id is received.
+ /// Attaches an event to a node.
///
- /// Unique id for this event handler.
+ /// Addon that contains the node.
+ /// The node that will trigger the event.
+ /// The event type to trigger on.
+ /// The unique id for this event.
/// The event handler to be called.
- public void AddEvent(uint eventId, IAddonEventManager.AddonEventHandler handler) => this.eventHandlers.Add(eventId, handler);
+ public void AddEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param, IAddonEventManager.AddonEventHandler handler)
+ {
+ this.eventListener.RegisterEvent(addon, node, eventType, param);
+ this.eventHandlers.TryAdd(param, handler);
+ }
///
- /// Removes the event handler with the specified id.
+ /// Detaches an event from a node.
///
- /// Event id to unregister.
- public void RemoveEvent(uint eventId) => this.eventHandlers.Remove(eventId);
+ /// The node to remove the event from.
+ /// The event type to remove.
+ /// The unique id of the event to remove.
+ public void RemoveEvent(AtkResNode* node, AtkEventType eventType, uint param)
+ {
+ this.eventListener.UnregisterEvent(node, eventType, param);
+ this.eventHandlers.Remove(param);
+ }
+
+ ///
+ /// Removes a delegate from the managed event handlers.
+ ///
+ /// Unique id of the delegate to remove.
+ public void RemoveHandler(uint param)
+ {
+ this.eventHandlers.Remove(param);
+ }
///
/// Sets the game cursor.
@@ -119,33 +139,23 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
- this.onGlobalEventHook.Enable();
this.onUpdateCursor.Enable();
}
- private nint GlobalEventHandler(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown)
+ private void OnCustomEvent(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown)
{
- try
+ if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null)
{
- if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null)
+ try
{
- try
- {
- handler?.Invoke((AddonEventType)eventType, (nint)atkUnitBase, (nint)eventData[0]);
- return nint.Zero;
- }
- catch (Exception exception)
- {
- Log.Error(exception, "Exception in GlobalEventHandler custom event invoke.");
- }
+ // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler
+ handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target);
+ }
+ catch (Exception exception)
+ {
+ Log.Error(exception, "Exception in OnCustomEvent custom event invoke.");
}
}
- catch (Exception e)
- {
- Log.Error(e, "Exception in GlobalEventHandler attempting to retrieve event handler.");
- }
-
- return this.onGlobalEventHook!.Original(atkUnitBase, eventType, eventParam, eventData, unknown);
}
private nint UpdateCursorDetour(RaptureAtkModule* module)
@@ -193,7 +203,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
private readonly uint paramKeyStartRange;
private readonly List activeParamKeys;
private bool isForcingCursor;
-
+
///
/// Initializes a new instance of the class.
///
@@ -208,7 +218,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
{
foreach (var activeKey in this.activeParamKeys)
{
- this.baseEventManager.RemoveEvent(activeKey);
+ this.baseEventManager.RemoveHandler(activeKey);
}
// if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared.
@@ -221,18 +231,16 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
///
public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
{
- if (eventId < 0x10000)
+ if (eventId < 0x10_000)
{
var type = (AtkEventType)eventType;
var node = (AtkResNode*)atkResNode;
- var eventListener = (AtkEventListener*)atkUnitBase;
+ var addon = (AtkUnitBase*)atkUnitBase;
var uniqueId = eventId + this.paramKeyStartRange;
if (!this.activeParamKeys.Contains(uniqueId))
{
- node->AddEvent(type, uniqueId, eventListener, node, true);
- this.baseEventManager.AddEvent(uniqueId, eventHandler);
-
+ this.baseEventManager.AddEvent(addon, node, type, uniqueId, eventHandler);
this.activeParamKeys.Add(uniqueId);
}
else
@@ -242,7 +250,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
}
else
{
- Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10000}");
+ Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10_000}");
}
}
@@ -251,14 +259,11 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
{
var type = (AtkEventType)eventType;
var node = (AtkResNode*)atkResNode;
- var eventListener = (AtkEventListener*)atkUnitBase;
var uniqueId = eventId + this.paramKeyStartRange;
if (this.activeParamKeys.Contains(uniqueId))
{
- node->RemoveEvent(type, uniqueId, eventListener, true);
- this.baseEventManager.RemoveEvent(uniqueId);
-
+ this.baseEventManager.RemoveEvent(node, type, uniqueId);
this.activeParamKeys.Remove(uniqueId);
}
else
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs
index 5cfa51149..ba1c07db8 100644
--- a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs
+++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs
@@ -5,11 +5,6 @@
///
internal class AddonEventManagerAddressResolver : BaseAddressResolver
{
- ///
- /// Gets the address of the global AtkEvent handler.
- ///
- public nint GlobalEventHandler { get; private set; }
-
///
/// Gets the address of the AtkModule UpdateCursor method.
///
@@ -21,7 +16,6 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver
/// The signature scanner to facilitate setup.
protected override void Setup64Bit(SigScanner scanner)
{
- this.GlobalEventHandler = scanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2");
this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE");
}
}
From 22a381b8746bfc58d14a84a815eabd030ac03f12 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Sat, 2 Sep 2023 09:49:21 -0700
Subject: [PATCH 05/13] [AddonEventManager] Reserve the first range for Dalamud
internal use
---
Dalamud/Game/AddonEventManager/AddonEventManager.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
index c6e61fe5a..b88c64253 100644
--- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs
+++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
@@ -19,7 +19,8 @@ namespace Dalamud.Game.AddonEventManager;
internal unsafe class AddonEventManager : IDisposable, IServiceType
{
// The starting value for param key ranges.
- private const uint ParamKeyStart = 0x0000_0000;
+ // Reserve the first 0x10_000 for dalamud internal use.
+ private const uint ParamKeyStart = 0x0010_0000;
// The range each plugin is allowed to use.
// 1,048,576 per plugin should be reasonable.
From 26e138c7834011f385fa9c0bd4cd66fbad55f247 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Sat, 2 Sep 2023 11:30:31 -0700
Subject: [PATCH 06/13] [AddonEventManager] Give each plugin their own event
listener.
---
.../AddonEventManager/AddonEventManager.cs | 180 +++++-------------
1 file changed, 44 insertions(+), 136 deletions(-)
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
index b88c64253..69dc27f3b 100644
--- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs
+++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
@@ -18,29 +18,12 @@ namespace Dalamud.Game.AddonEventManager;
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonEventManager : IDisposable, IServiceType
{
- // The starting value for param key ranges.
- // Reserve the first 0x10_000 for dalamud internal use.
- private const uint ParamKeyStart = 0x0010_0000;
-
- // The range each plugin is allowed to use.
- // 1,048,576 per plugin should be reasonable.
- private const uint ParamKeyPluginRange = 0x10_0000;
-
- // The maximum range allowed to be given to a plugin.
- // 1,048,576 maximum plugins should be reasonable.
- private const uint ParamKeyMax = 0xFFF0_0000;
-
private static readonly ModuleLog Log = new("AddonEventManager");
private readonly AddonEventManagerAddressResolver address;
private readonly Hook onUpdateCursor;
- private readonly Dictionary eventHandlers;
- private readonly AddonEventListener eventListener;
- private AddonCursorType currentCursor;
- private bool cursorSet;
-
- private uint currentPluginParamStart = ParamKeyStart;
+ private AddonCursorType? cursorOverride;
[ServiceManager.ServiceConstructor]
private AddonEventManager(SigScanner sigScanner)
@@ -48,10 +31,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
this.address = new AddonEventManagerAddressResolver();
this.address.Setup(sigScanner);
- this.eventListener = new AddonEventListener(this.OnCustomEvent);
-
- this.eventHandlers = new Dictionary();
- this.currentCursor = AddonCursorType.Arrow;
+ this.cursorOverride = null;
this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
}
@@ -61,116 +41,38 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
///
public void Dispose()
{
- this.eventListener.Dispose();
this.onUpdateCursor.Dispose();
}
- ///
- /// Get the start value for a new plugin register.
- ///
- /// A unique starting range for event handlers.
- /// Throws when attempting to register too many event handlers.
- public uint GetPluginParamStart()
- {
- if (this.currentPluginParamStart >= ParamKeyMax)
- {
- throw new Exception("Maximum number of event handlers reached.");
- }
-
- var result = this.currentPluginParamStart;
-
- this.currentPluginParamStart += ParamKeyPluginRange;
- return result;
- }
-
- ///
- /// Attaches an event to a node.
- ///
- /// Addon that contains the node.
- /// The node that will trigger the event.
- /// The event type to trigger on.
- /// The unique id for this event.
- /// The event handler to be called.
- public void AddEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param, IAddonEventManager.AddonEventHandler handler)
- {
- this.eventListener.RegisterEvent(addon, node, eventType, param);
- this.eventHandlers.TryAdd(param, handler);
- }
-
- ///
- /// Detaches an event from a node.
- ///
- /// The node to remove the event from.
- /// The event type to remove.
- /// The unique id of the event to remove.
- public void RemoveEvent(AtkResNode* node, AtkEventType eventType, uint param)
- {
- this.eventListener.UnregisterEvent(node, eventType, param);
- this.eventHandlers.Remove(param);
- }
-
- ///
- /// Removes a delegate from the managed event handlers.
- ///
- /// Unique id of the delegate to remove.
- public void RemoveHandler(uint param)
- {
- this.eventHandlers.Remove(param);
- }
-
///
/// Sets the game cursor.
///
/// Cursor type to set.
- public void SetCursor(AddonCursorType cursor)
- {
- this.currentCursor = cursor;
- this.cursorSet = true;
- }
+ public void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
///
/// Resets and un-forces custom cursor.
///
- public void ResetCursor()
- {
- this.currentCursor = AddonCursorType.Arrow;
- this.cursorSet = false;
- }
-
+ public void ResetCursor() => this.cursorOverride = null;
+
[ServiceManager.CallWhenServicesReady]
private void ContinueConstruction()
{
this.onUpdateCursor.Enable();
}
- private void OnCustomEvent(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown)
- {
- if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null)
- {
- try
- {
- // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler
- handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target);
- }
- catch (Exception exception)
- {
- Log.Error(exception, "Exception in OnCustomEvent custom event invoke.");
- }
- }
- }
-
private nint UpdateCursorDetour(RaptureAtkModule* module)
{
try
{
var atkStage = AtkStage.GetSingleton();
- if (this.cursorSet && atkStage is not null)
+ if (this.cursorOverride is not null && atkStage is not null)
{
var cursor = (AddonCursorType)atkStage->AtkCursor.Type;
- if (cursor != this.currentCursor)
+ if (cursor != this.cursorOverride)
{
- AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.currentCursor, 1);
+ AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1);
}
return nint.Zero;
@@ -200,9 +102,10 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
[ServiceManager.ServiceDependency]
private readonly AddonEventManager baseEventManager = Service.Get();
-
- private readonly uint paramKeyStartRange;
- private readonly List activeParamKeys;
+
+ private readonly AddonEventListener eventListener;
+ private readonly Dictionary eventHandlers;
+
private bool isForcingCursor;
///
@@ -210,62 +113,51 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
///
public AddonEventManagerPluginScoped()
{
- this.paramKeyStartRange = this.baseEventManager.GetPluginParamStart();
- this.activeParamKeys = new List();
+ this.eventHandlers = new Dictionary();
+ this.eventListener = new AddonEventListener(this.PluginAddonEventHandler);
}
-
+
///
public void Dispose()
{
- foreach (var activeKey in this.activeParamKeys)
- {
- this.baseEventManager.RemoveHandler(activeKey);
- }
-
// if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared.
if (this.isForcingCursor)
{
this.baseEventManager.ResetCursor();
}
+
+ this.eventListener.Dispose();
+ this.eventHandlers.Clear();
}
///
public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
{
- if (eventId < 0x10_000)
+ if (!this.eventHandlers.ContainsKey(eventId))
{
var type = (AtkEventType)eventType;
var node = (AtkResNode*)atkResNode;
var addon = (AtkUnitBase*)atkUnitBase;
- var uniqueId = eventId + this.paramKeyStartRange;
- if (!this.activeParamKeys.Contains(uniqueId))
- {
- this.baseEventManager.AddEvent(addon, node, type, uniqueId, eventHandler);
- this.activeParamKeys.Add(uniqueId);
- }
- else
- {
- Log.Warning($"Attempted to register already registered eventId: {eventId}");
- }
+ this.eventHandlers.Add(eventId, eventHandler);
+ this.eventListener.RegisterEvent(addon, node, type, eventId);
}
else
{
- Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10_000}");
+ Log.Warning($"Attempted to register already registered eventId: {eventId}");
}
}
///
public void RemoveEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType)
{
- var type = (AtkEventType)eventType;
- var node = (AtkResNode*)atkResNode;
- var uniqueId = eventId + this.paramKeyStartRange;
-
- if (this.activeParamKeys.Contains(uniqueId))
+ if (this.eventHandlers.ContainsKey(eventId))
{
- this.baseEventManager.RemoveEvent(node, type, uniqueId);
- this.activeParamKeys.Remove(uniqueId);
+ var type = (AtkEventType)eventType;
+ var node = (AtkResNode*)atkResNode;
+
+ this.eventListener.UnregisterEvent(node, type, eventId);
+ this.eventHandlers.Remove(eventId);
}
else
{
@@ -288,4 +180,20 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
this.baseEventManager.ResetCursor();
}
+
+ private void PluginAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown)
+ {
+ if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null)
+ {
+ try
+ {
+ // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler
+ handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target);
+ }
+ catch (Exception exception)
+ {
+ Log.Error(exception, "Exception in PluginAddonEventHandler custom event invoke.");
+ }
+ }
+ }
}
From 627a41f2363ee44956cd891950b8953cc1ff69c5 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Sat, 2 Sep 2023 13:08:14 -0700
Subject: [PATCH 07/13] [AddonEventManager] Remove AtkUnitBase from remove
event, it's not used to unregister events.
---
Dalamud/Plugin/Services/IAddonEventManager.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs
index f052ed607..dbbfd784b 100644
--- a/Dalamud/Plugin/Services/IAddonEventManager.cs
+++ b/Dalamud/Plugin/Services/IAddonEventManager.cs
@@ -29,10 +29,9 @@ public interface IAddonEventManager
/// Unregisters an event handler with the specified event id and event type.
///
/// The Unique Id for this event.
- /// The parent addon for this event.
/// The node for this event.
/// The event type for this event.
- void RemoveEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType);
+ void RemoveEvent(uint eventId, nint atkResNode, AddonEventType eventType);
///
/// Force the game cursor to be the specified cursor.
From ce4392e1093557aebeb5dfeb0a8c4c0148e18b78 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Sat, 2 Sep 2023 13:56:44 -0700
Subject: [PATCH 08/13] [AddonEventManager] Add Dalamud specific EventListener
for internal use
---
.../AddonEventManager/AddonEventManager.cs | 72 ++++++++++++++++---
1 file changed, 63 insertions(+), 9 deletions(-)
diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
index 69dc27f3b..4718d4800 100644
--- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs
+++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs
@@ -16,13 +16,16 @@ namespace Dalamud.Game.AddonEventManager;
///
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
-internal unsafe class AddonEventManager : IDisposable, IServiceType
+internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEventManager
{
private static readonly ModuleLog Log = new("AddonEventManager");
private readonly AddonEventManagerAddressResolver address;
private readonly Hook onUpdateCursor;
+ private readonly AddonEventListener eventListener;
+ private readonly Dictionary eventHandlers;
+
private AddonCursorType? cursorOverride;
[ServiceManager.ServiceConstructor]
@@ -31,28 +34,63 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
this.address = new AddonEventManagerAddressResolver();
this.address.Setup(sigScanner);
+ this.eventHandlers = new Dictionary();
+ this.eventListener = new AddonEventListener(this.DalamudAddonEventHandler);
+
this.cursorOverride = null;
this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour);
}
private delegate nint UpdateCursorDelegate(RaptureAtkModule* module);
+
+ ///
+ public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler)
+ {
+ if (!this.eventHandlers.ContainsKey(eventId))
+ {
+ var type = (AtkEventType)eventType;
+ var node = (AtkResNode*)atkResNode;
+ var addon = (AtkUnitBase*)atkUnitBase;
+
+ this.eventHandlers.Add(eventId, eventHandler);
+ this.eventListener.RegisterEvent(addon, node, type, eventId);
+ }
+ else
+ {
+ Log.Warning($"Attempted to register already registered eventId: {eventId}");
+ }
+ }
+ ///
+ public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType)
+ {
+ if (this.eventHandlers.ContainsKey(eventId))
+ {
+ var type = (AtkEventType)eventType;
+ var node = (AtkResNode*)atkResNode;
+
+ this.eventListener.UnregisterEvent(node, type, eventId);
+ this.eventHandlers.Remove(eventId);
+ }
+ else
+ {
+ Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}");
+ }
+ }
+
///
public void Dispose()
{
this.onUpdateCursor.Dispose();
+ this.eventListener.Dispose();
+ this.eventHandlers.Clear();
}
- ///
- /// Sets the game cursor.
- ///
- /// Cursor type to set.
+ ///
public void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor;
- ///
- /// Resets and un-forces custom cursor.
- ///
+ ///
public void ResetCursor() => this.cursorOverride = null;
[ServiceManager.CallWhenServicesReady]
@@ -85,6 +123,22 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType
return this.onUpdateCursor!.Original(module);
}
+
+ private void DalamudAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown)
+ {
+ if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null)
+ {
+ try
+ {
+ // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler
+ handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target);
+ }
+ catch (Exception exception)
+ {
+ Log.Error(exception, "Exception in DalamudAddonEventHandler custom event invoke.");
+ }
+ }
+ }
}
///
@@ -149,7 +203,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType,
}
///
- public void RemoveEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType)
+ public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType)
{
if (this.eventHandlers.ContainsKey(eventId))
{
From 2439bcccbd0b202d079290abd2ed0df8af7d7b09 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Mon, 4 Sep 2023 23:45:17 -0700
Subject: [PATCH 09/13] Add Tooltips, and OnClick actions to DtrBarEntries
---
Dalamud/Game/Gui/Dtr/DtrBar.cs | 209 +++++++++++++++---
Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs | 29 +++
Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 10 +
3 files changed, 218 insertions(+), 30 deletions(-)
create mode 100644 Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs
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.
From 153f7c45bf1475ccc7dcf58e88a83eb1bfc0558e Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Tue, 5 Sep 2023 00:46:45 -0700
Subject: [PATCH 10/13] Fix non-reversed resizing logic
---
Dalamud/Game/Gui/Dtr/DtrBar.cs | 22 ++++++++++++++++++----
1 file changed, 18 insertions(+), 4 deletions(-)
diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs
index 4d2a005ae..b1679c296 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBar.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs
@@ -249,6 +249,21 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
if (MemoryHelper.ReadString((nint)addon->Name, 0x20) is not "_DTR") return;
this.UpdateNodePositions(addon);
+
+ if (!this.configuration.DtrSwapDirection)
+ {
+ var targetSize = (ushort)this.CalculateTotalSize();
+ var sizeDelta = targetSize - addon->RootNode->Width;
+
+ if (addon->RootNode->Width != targetSize)
+ {
+ addon->RootNode->SetWidth(targetSize);
+ addon->SetX((short)(addon->GetX() - sizeDelta));
+
+ // force a RequestedUpdate immediately to force the game to right-justify it immediately.
+ this.onAddonRequestedUpdateHook.Original(addon, AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData());
+ }
+ }
}
catch (Exception e)
{
@@ -258,13 +273,12 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
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 targetSize = (ushort)this.CalculateTotalSize();
+ addon->RootNode->SetWidth(targetSize);
var sizeOffset = addon->GetNodeById(17)->GetX();
var node = addon->RootNode->ChildNode;
@@ -338,7 +352,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
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;
+ if (node->NodeID is > 1000 && node->IsVisible) totalSize += node->Width + this.configuration.DtrSpacing;
}
return totalSize;
From 166669597dea8e3d9735d457f06fe4056b257d9d Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Tue, 5 Sep 2023 14:03:37 -0700
Subject: [PATCH 11/13] [DtrBar] Probably fix concurrency issues
---
Dalamud/Game/Gui/Dtr/DtrBar.cs | 27 ++++++++++++++++++++++-----
1 file changed, 22 insertions(+), 5 deletions(-)
diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs
index b1679c296..8b021bc7a 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBar.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
@@ -50,6 +51,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
private readonly DalamudAddonEventManager uiEventManager = Service.Get();
private readonly DtrBarAddressResolver address;
+ private readonly ConcurrentBag newEntries = new();
private readonly List entries = new();
private readonly Hook onAddonDrawHook;
private readonly Hook onAddonRequestedUpdateHook;
@@ -78,18 +80,17 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
///
public DtrBarEntry Get(string title, SeString? text = null)
{
- if (this.entries.Any(x => x.Title == title))
+ if (this.entries.Any(x => x.Title == title) || this.newEntries.Any(x => x.Title == title))
throw new ArgumentException("An entry with the same title already exists.");
- var node = this.MakeNode(++this.runningNodeIds);
- var entry = new DtrBarEntry(title, node);
+ var entry = new DtrBarEntry(title, null);
entry.Text = text;
// Add the entry to the end of the order list, if it's not there already.
if (!this.configuration.DtrOrder!.Contains(title))
this.configuration.DtrOrder!.Add(title);
- this.entries.Add(entry);
- this.ApplySort();
+
+ this.newEntries.Add(entry);
return entry;
}
@@ -173,6 +174,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
private void Update(Framework unused)
{
this.HandleRemovedNodes();
+ this.HandleAddedNodes();
var dtr = this.GetDtr();
if (dtr == null) return;
@@ -238,6 +240,21 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
}
}
+ private void HandleAddedNodes()
+ {
+ if (this.newEntries.Any())
+ {
+ foreach (var newEntry in this.newEntries)
+ {
+ newEntry.TextNode = this.MakeNode(++this.runningNodeIds);
+ this.entries.Add(newEntry);
+ }
+
+ this.newEntries.Clear();
+ this.ApplySort();
+ }
+ }
+
// 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)
From 692113958b3481aa75ce7d727d2ae158e143d619 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Tue, 5 Sep 2023 14:35:08 -0700
Subject: [PATCH 12/13] Scope DTRBar
---
Dalamud/Game/Gui/Dtr/DtrBar.cs | 59 ++++++++++++++++++++++++++++--
Dalamud/Plugin/Services/IDtrBar.cs | 6 +++
2 files changed, 62 insertions(+), 3 deletions(-)
diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs
index 8b021bc7a..2ff99a450 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBar.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs
@@ -26,9 +26,6 @@ namespace Dalamud.Game.Gui.Dtr;
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService]
-#pragma warning disable SA1015
-[ResolveVia]
-#pragma warning restore SA1015
public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
{
private const uint BaseNodeId = 1000;
@@ -94,6 +91,15 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
return entry;
}
+
+ ///
+ public void Remove(string title)
+ {
+ if (this.entries.FirstOrDefault(entry => entry.Title == title) is { } dtrBarEntry)
+ {
+ dtrBarEntry.Remove();
+ }
+ }
///
void IDisposable.Dispose()
@@ -497,3 +503,50 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
}
}
}
+
+///
+/// Plugin-scoped version of a AddonEventManager service.
+///
+[PluginInterface]
+[InterfaceVersion("1.0")]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar
+{
+ [ServiceManager.ServiceDependency]
+ private readonly DtrBar dtrBarService = Service.Get();
+
+ private readonly Dictionary pluginEntries = new();
+
+ ///
+ public void Dispose()
+ {
+ foreach (var entry in this.pluginEntries)
+ {
+ entry.Value.Remove();
+ }
+
+ this.pluginEntries.Clear();
+ }
+
+ ///
+ public DtrBarEntry Get(string title, SeString? text = null)
+ {
+ // If we already have a known entry for this plugin, return it.
+ if (this.pluginEntries.TryGetValue(title, out var existingEntry)) return existingEntry;
+
+ return this.pluginEntries[title] = this.dtrBarService.Get(title, text);
+ }
+
+ ///
+ public void Remove(string title)
+ {
+ if (this.pluginEntries.TryGetValue(title, out var existingEntry))
+ {
+ existingEntry.Remove();
+ this.pluginEntries.Remove(title);
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Services/IDtrBar.cs b/Dalamud/Plugin/Services/IDtrBar.cs
index 6c2b8ad1e..a5a750cf6 100644
--- a/Dalamud/Plugin/Services/IDtrBar.cs
+++ b/Dalamud/Plugin/Services/IDtrBar.cs
@@ -19,4 +19,10 @@ public interface IDtrBar
/// The entry object used to update, hide and remove the entry.
/// Thrown when an entry with the specified title exists.
public DtrBarEntry Get(string title, SeString? text = null);
+
+ ///
+ /// Removes a DTR bar entry from the system.
+ ///
+ /// Title of the entry to remove.
+ public void Remove(string title);
}
From f1c8201f1b027f0e35400f1899e53bd0dd8ffe07 Mon Sep 17 00:00:00 2001
From: MidoriKami <9083275+MidoriKami@users.noreply.github.com>
Date: Sat, 9 Sep 2023 18:49:04 -0700
Subject: [PATCH 13/13] Use vfunc call instead of hook
---
Dalamud/Game/Gui/Dtr/DtrBar.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs
index 2ff99a450..ae01d4886 100644
--- a/Dalamud/Game/Gui/Dtr/DtrBar.cs
+++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs
@@ -284,7 +284,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar
addon->SetX((short)(addon->GetX() - sizeDelta));
// force a RequestedUpdate immediately to force the game to right-justify it immediately.
- this.onAddonRequestedUpdateHook.Original(addon, AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData());
+ addon->OnUpdate(AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData());
}
}
}