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