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