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()); } } }