From 13306e24ba39e20e6ecebd3c65796a4d4a013f41 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:51:00 -0700 Subject: [PATCH] Refactor IAddonEventManager (#2299) --- Dalamud/Game/Addon/Events/AddonEventData.cs | 46 ++++++++++ Dalamud/Game/Addon/Events/AddonEventEntry.cs | 24 ++++-- .../Game/Addon/Events/AddonEventManager.cs | 67 ++++++++++----- .../Addon/Events/PluginEventController.cs | 85 ++++++++++++++++--- Dalamud/Plugin/Services/IAddonEventManager.cs | 19 +++++ 5 files changed, 199 insertions(+), 42 deletions(-) create mode 100644 Dalamud/Game/Addon/Events/AddonEventData.cs diff --git a/Dalamud/Game/Addon/Events/AddonEventData.cs b/Dalamud/Game/Addon/Events/AddonEventData.cs new file mode 100644 index 000000000..3a5c05660 --- /dev/null +++ b/Dalamud/Game/Addon/Events/AddonEventData.cs @@ -0,0 +1,46 @@ +namespace Dalamud.Game.Addon.Events; + +/// +/// Object representing data that is relevant in handling native events. +/// +public class AddonEventData +{ + /// + /// Gets the AtkEventType for this event. + /// + public AddonEventType AtkEventType { get; internal set; } + + /// + /// Gets the param field for this event. + /// + public uint Param { get; internal set; } + + /// + /// Gets the pointer to the AtkEvent object for this event. + /// + /// Note: This is not a pointer to the AtkEventData object.

+ /// Warning: AtkEvent->Node has been modified to be the AtkUnitBase*, and AtkEvent->Target has been modified to be the AtkResNode* that triggered this event. + public nint AtkEventPointer { get; internal set; } + + /// + /// Gets the pointer to the AtkEventData object for this event. + /// + /// This field will contain relevant data such as left vs right click, scroll up vs scroll down. + public nint AtkEventDataPointer { get; internal set; } + + /// + /// Gets the pointer to the AtkUnitBase that is handling this event. + /// + public nint AddonPointer { get; internal set; } + + /// + /// Gets the pointer to the AtkResNode that triggered this event. + /// + public nint NodeTargetPointer { get; internal set; } + + /// + /// Gets or sets a pointer to the AtkEventListener responsible for handling this event. + /// Note: As the event listener is dalamud allocated, there's no reason to expose this field. + /// + internal nint AtkEventListener { get; set; } +} diff --git a/Dalamud/Game/Addon/Events/AddonEventEntry.cs b/Dalamud/Game/Addon/Events/AddonEventEntry.cs index 8b5808087..50b9c7ec4 100644 --- a/Dalamud/Game/Addon/Events/AddonEventEntry.cs +++ b/Dalamud/Game/Addon/Events/AddonEventEntry.cs @@ -1,5 +1,6 @@ -using Dalamud.Memory; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Events; @@ -14,9 +15,9 @@ internal unsafe class AddonEventEntry /// Name of an invalid addon. /// public const string InvalidAddonName = "NullAddon"; - + private string? addonName; - + /// /// Gets the pointer to the addons AtkUnitBase. /// @@ -35,18 +36,25 @@ internal unsafe class AddonEventEntry /// /// Gets the handler that gets called when this event is triggered. /// - public required IAddonEventManager.AddonEventHandler Handler { get; init; } - + [Obsolete("Use AddonEventDelegate Delegate instead")] + public IAddonEventManager.AddonEventHandler Handler { get; init; } + + /// + /// Gets the delegate that gets called when this event is triggered. + /// + [Api13ToDo("Make this field required")] + public IAddonEventManager.AddonEventDelegate Delegate { get; init; } + /// /// Gets the unique id for this event. /// public required uint ParamKey { get; init; } - + /// /// Gets the event type for this event. /// public required AddonEventType EventType { get; init; } - + /// /// Gets the event handle for this event. /// diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index a7241dd58..0990c1f5f 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -24,21 +24,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService /// PluginName for Dalamud Internal use. /// public static readonly Guid DalamudInternalKey = Guid.NewGuid(); - + private static readonly ModuleLog Log = new("AddonEventManager"); - + [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycle = Service.Get(); private readonly AddonLifecycleEventListener finalizeEventListener; - + private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; private readonly ConcurrentDictionary pluginEventControllers; - + private AddonCursorType? cursorOverride; - + [ServiceManager.ServiceConstructor] private AddonEventManager(TargetSigScanner sigScanner) { @@ -47,7 +47,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService this.pluginEventControllers = new ConcurrentDictionary(); this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController()); - + this.cursorOverride = null; this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); @@ -69,7 +69,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService { pluginEventController.Dispose(); } - + this.addonLifecycle.UnregisterListener(this.finalizeEventListener); } @@ -92,7 +92,30 @@ internal unsafe class AddonEventManager : IInternalDisposableService { Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); } - + + return null; + } + + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// Unique ID for this plugin. + /// The parent addon for this event. + /// The node that will trigger this event. + /// The event type for this event. + /// The delegate to call when event is triggered. + /// IAddonEventHandle used to remove the event. + internal IAddonEventHandle? AddEvent(Guid pluginId, nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate) + { + if (this.pluginEventControllers.TryGetValue(pluginId, out var controller)) + { + return controller.AddEvent(atkUnitBase, atkResNode, eventType, eventDelegate); + } + else + { + Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); + } + return null; } @@ -112,7 +135,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService Log.Verbose($"Unable to locate controller for {pluginId}. No event was removed."); } } - + /// /// Force the game cursor to be the specified cursor. /// @@ -167,21 +190,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService pluginList.Value.RemoveForAddon(addonInfo.AddonName); } } - + private nint UpdateCursorDetour(RaptureAtkModule* module) { try { var atkStage = AtkStage.Instance(); - + if (this.cursorOverride is not null && atkStage is not null) { var cursor = (AddonCursorType)atkStage->AtkCursor.Type; - if (cursor != this.cursorOverride) + if (cursor != this.cursorOverride) { AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); } - + return nint.Zero; } } @@ -218,7 +241,7 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo public AddonEventManagerPluginScoped(LocalPlugin plugin) { this.plugin = plugin; - + this.eventManagerService.AddPluginEventController(plugin.EffectiveWorkingPluginId); } @@ -236,28 +259,32 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId); }).Wait(); } - + /// - public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) => this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler); + /// + public IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate) + => this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventDelegate); + /// public void RemoveEvent(IAddonEventHandle eventHandle) => this.eventManagerService.RemoveEvent(this.plugin.EffectiveWorkingPluginId, eventHandle); - + /// public void SetCursor(AddonCursorType cursor) { this.isForcingCursor = true; - + this.eventManagerService.SetCursor(cursor); } - + /// public void ResetCursor() { this.isForcingCursor = false; - + this.eventManagerService.ResetCursor(); } } diff --git a/Dalamud/Game/Addon/Events/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs index f32c7ad8f..0b1491e77 100644 --- a/Dalamud/Game/Addon/Events/PluginEventController.cs +++ b/Dalamud/Game/Addon/Events/PluginEventController.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Game.Gui; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -25,7 +26,7 @@ internal unsafe class PluginEventController : IDisposable } private AddonEventListener EventListener { get; init; } - + private List Events { get; } = new(); /// @@ -36,6 +37,7 @@ internal unsafe class PluginEventController : IDisposable /// The Event Type. /// The delegate to call when invoking this event. /// IAddonEventHandle used to remove the event. + [Obsolete("Use AddEvent that uses AddonEventDelegate instead of AddonEventHandler")] public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler) { var node = (AtkResNode*)atkResNode; @@ -43,7 +45,7 @@ internal unsafe class PluginEventController : IDisposable var eventType = (AtkEventType)atkEventType; var eventId = this.GetNextParamKey(); var eventGuid = Guid.NewGuid(); - + var eventHandle = new AddonEventHandle { AddonName = addon->NameString, @@ -51,11 +53,54 @@ internal unsafe class PluginEventController : IDisposable EventType = atkEventType, EventGuid = eventGuid, }; - + var eventEntry = new AddonEventEntry { Addon = atkUnitBase, Handler = handler, + Delegate = null, + Node = atkResNode, + EventType = atkEventType, + ParamKey = eventId, + Handle = eventHandle, + }; + + Log.Verbose($"Adding Event. {eventEntry.LogString}"); + this.EventListener.RegisterEvent(addon, node, eventType, eventId); + this.Events.Add(eventEntry); + + return eventHandle; + } + + /// + /// Adds a tracked event. + /// + /// The Parent addon for the event. + /// The Node for the event. + /// The Event Type. + /// The delegate to call when invoking this event. + /// IAddonEventHandle used to remove the event. + public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventDelegate eventDelegate) + { + var node = (AtkResNode*)atkResNode; + var addon = (AtkUnitBase*)atkUnitBase; + var eventType = (AtkEventType)atkEventType; + var eventId = this.GetNextParamKey(); + var eventGuid = Guid.NewGuid(); + + var eventHandle = new AddonEventHandle + { + AddonName = addon->NameString, + ParamKey = eventId, + EventType = atkEventType, + EventGuid = eventGuid, + }; + + var eventEntry = new AddonEventEntry + { + Addon = atkUnitBase, + Delegate = eventDelegate, + Handler = null, Node = atkResNode, EventType = atkEventType, ParamKey = eventId, @@ -91,14 +136,14 @@ internal unsafe class PluginEventController : IDisposable if (this.Events.Where(entry => entry.AddonName == addonName).ToList() is { Count: not 0 } events) { Log.Verbose($"Addon: {addonName} is Finalizing, removing {events.Count} events."); - + foreach (var registeredEvent in events) { this.RemoveEvent(registeredEvent.Handle); } } } - + /// public void Dispose() { @@ -106,7 +151,7 @@ internal unsafe class PluginEventController : IDisposable { this.RemoveEvent(registeredEvent.Handle); } - + this.EventListener.Dispose(); } @@ -119,7 +164,7 @@ internal unsafe class PluginEventController : IDisposable throw new OverflowException($"uint.MaxValue number of ParamKeys used for this event controller."); } - + /// /// Attempts to remove a tracked event from native UI. /// This method performs several safety checks to only remove events from a still active addon. @@ -153,7 +198,7 @@ internal unsafe class PluginEventController : IDisposable break; } } - + // If we didn't find the node, we can't remove the event. if (!nodeFound) return; @@ -167,33 +212,45 @@ internal unsafe class PluginEventController : IDisposable var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey; var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address; var eventTypeMatches = currentEvent->State.EventType == eventType; - + if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches) { eventFound = true; break; } - + // Move to the next event. currentEvent = currentEvent->NextEvent; } - + // If we didn't find the event, we can't remove the event. if (!eventFound) return; // We have a valid addon, valid node, valid event, and valid key. this.EventListener.UnregisterEvent(atkResNode, eventType, eventEntry.ParamKey); } - + + [Api13ToDo("Remove invoke from eventInfo.Handler, and remove nullability from eventInfo.Delegate?.Invoke")] private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr) { try { if (eventPtr is null) return; if (this.Events.FirstOrDefault(handler => handler.ParamKey == eventParam) is not { } eventInfo) return; - + // We stored the AtkUnitBase* in EventData->Node, and EventData->Target contains the node that triggered the event. - eventInfo.Handler.Invoke((AddonEventType)eventType, (nint)eventPtr->Node, (nint)eventPtr->Target); + eventInfo.Handler?.Invoke((AddonEventType)eventType, (nint)eventPtr->Node, (nint)eventPtr->Target); + + eventInfo.Delegate?.Invoke((AddonEventType)eventType, new AddonEventData + { + AddonPointer = (nint)eventPtr->Node, + NodeTargetPointer = (nint)eventPtr->Target, + AtkEventDataPointer = (nint)eventDataPtr, + AtkEventListener = (nint)self, + AtkEventType = (AddonEventType)eventType, + Param = eventParam, + AtkEventPointer = (nint)eventPtr, + }); } catch (Exception exception) { diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index c6ec5a941..e534eafb4 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -13,8 +13,16 @@ public interface IAddonEventManager /// Event type for this event handler. /// The parent addon for this event handler. /// The specific node that will trigger this event handler. + [Obsolete("Use AddonEventDelegate instead")] public delegate void AddonEventHandler(AddonEventType atkEventType, nint atkUnitBase, nint atkResNode); + /// + /// Delegate to be called when an event is received. + /// + /// The AtkEventType that triggered this event. + /// The event data object for use in handling this event. + public delegate void AddonEventDelegate(AddonEventType atkEventType, AddonEventData data); + /// /// Registers an event handler for the specified addon, node, and type. /// @@ -23,8 +31,19 @@ public interface IAddonEventManager /// The event type for this event. /// The handler to call when event is triggered. /// IAddonEventHandle used to remove the event. Null if no event was added. + [Obsolete("Use AddEvent with AddonEventDelegate instead of AddonEventHandler")] IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler); + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// 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. + /// IAddonEventHandle used to remove the event. Null if no event was added. + IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventDelegate eventDelegate); + /// /// Unregisters an event handler with the specified event id and event type. ///