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