diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
index c42481d63..fcfe7766f 100644
--- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs
@@ -21,6 +21,16 @@ namespace Dalamud.Game.Addon.Lifecycle;
[ServiceManager.EarlyLoadedService]
internal unsafe class AddonLifecycle : IDisposable, IServiceType
{
+ ///
+ /// List of all AddonLifecycle ReceiveEvent Listener Hooks.
+ ///
+ internal readonly List ReceiveEventListeners = new();
+
+ ///
+ /// List of all AddonLifecycle Event Listeners.
+ ///
+ internal readonly List EventListeners = new();
+
private static readonly ModuleLog Log = new("AddonLifecycle");
[ServiceManager.ServiceDependency]
@@ -39,9 +49,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
private readonly ConcurrentBag newEventListeners = new();
private readonly ConcurrentBag removeEventListeners = new();
- private readonly List eventListeners = new();
-
- private readonly Dictionary> receiveEventHooks = new();
[ServiceManager.ServiceConstructor]
private AddonLifecycle(TargetSigScanner sigScanner)
@@ -75,8 +82,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
private delegate byte AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values);
- private delegate void AddonReceiveEventDelegate(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint a5);
-
///
public void Dispose()
{
@@ -90,9 +95,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
this.onAddonRefreshHook.Dispose();
this.onAddonRequestedUpdateHook.Dispose();
- foreach (var (_, hook) in this.receiveEventHooks)
+ foreach (var receiveEventListener in this.ReceiveEventListeners)
{
- hook.Dispose();
+ receiveEventListener.Dispose();
}
}
@@ -114,6 +119,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
this.removeEventListeners.Add(listener);
}
+ ///
+ /// Invoke listeners for the specified event type.
+ ///
+ /// Event Type.
+ /// AddonArgs.
+ internal void InvokeListeners(AddonEvent eventType, AddonArgs args)
+ {
+ // Match on string.empty for listeners that want events for all addons.
+ foreach (var listener in this.EventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty)))
+ {
+ listener.FunctionDelegate.Invoke(eventType, args);
+ }
+ }
+
// Used to prevent concurrency issues if plugins try to register during iteration of listeners.
private void OnFrameworkUpdate(IFramework unused)
{
@@ -121,15 +140,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
{
foreach (var toAddListener in this.newEventListeners)
{
- this.eventListeners.Add(toAddListener);
+ this.EventListeners.Add(toAddListener);
// If we want receive event messages have an already active addon, enable the receive event hook.
// If the addon isn't active yet, we'll grab the hook when it sets up.
if (toAddListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
{
- if (this.receiveEventHooks.TryGetValue(toAddListener.AddonName, out var hook))
+ if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toAddListener.AddonName)) is { } receiveEventListener)
{
- hook.Enable();
+ receiveEventListener.Hook?.Enable();
}
}
}
@@ -141,7 +160,21 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
{
foreach (var toRemoveListener in this.removeEventListeners)
{
- this.eventListeners.Remove(toRemoveListener);
+ this.EventListeners.Remove(toRemoveListener);
+
+ // If we are disabling an ReceiveEvent listener, check if we should disable the hook.
+ if (toRemoveListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent })
+ {
+ // Get the ReceiveEvent Listener for this addon
+ if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toRemoveListener.AddonName)) is { } receiveEventListener)
+ {
+ // If there are no other listeners listening for this event, disable the hook.
+ if (!this.EventListeners.Any(listener => listener.AddonName.Contains(toRemoveListener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent))
+ {
+ receiveEventListener.Hook?.Disable();
+ }
+ }
+ }
}
this.removeEventListeners.Clear();
@@ -160,12 +193,53 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
this.onAddonRequestedUpdateHook.Enable();
}
- private void InvokeListeners(AddonEvent eventType, AddonArgs args)
+ private void RegisterReceiveEventHook(AtkUnitBase* addon)
{
- // Match on string.empty for listeners that want events for all addons.
- foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty)))
+ // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
+ // Disallows hooking the core internal event handler.
+ var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name);
+ var receiveEventAddress = (nint)addon->VTable->ReceiveEvent;
+ if (receiveEventAddress != this.disallowedReceiveEventAddress)
{
- listener.FunctionDelegate.Invoke(eventType, args);
+ // If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler.
+ if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.HookAddress == receiveEventAddress) is { } existingListener)
+ {
+ if (!existingListener.AddonNames.Contains(addonName))
+ {
+ existingListener.AddonNames.Add(addonName);
+ }
+ }
+
+ // Else, we have an addon that we don't have the ReceiveEvent for yet, make it.
+ else
+ {
+ this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress));
+ }
+
+ // If we have an active listener for this addon already, we need to activate this hook.
+ if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
+ {
+ if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener)
+ {
+ receiveEventListener.Hook?.Enable();
+ }
+ }
+ }
+ }
+
+ private void UnregisterReceiveEventHook(string addonName)
+ {
+ // Remove this addons ReceiveEvent Registration
+ if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener)
+ {
+ eventListener.AddonNames.Remove(addonName);
+
+ // If there are no more listeners let's remove and dispose.
+ if (eventListener.AddonNames.Count is 0)
+ {
+ this.ReceiveEventListeners.Remove(eventListener);
+ eventListener.Dispose();
+ }
}
}
@@ -173,20 +247,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
{
try
{
- // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener.
- // Disallows hooking the core internal event handler.
- var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name);
- var receiveEventAddress = (nint)addon->VTable->ReceiveEvent;
- if (receiveEventAddress != this.disallowedReceiveEventAddress)
- {
- var receiveEventHook = Hook.FromAddress(receiveEventAddress, this.OnReceiveEvent);
- this.receiveEventHooks.TryAdd(addonName, receiveEventHook);
-
- if (this.eventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName))
- {
- receiveEventHook.Enable();
- }
- }
+ this.RegisterReceiveEventHook(addon);
}
catch (Exception e)
{
@@ -235,13 +296,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
{
try
{
- // Remove this addons ReceiveEvent Registration
var addonName = MemoryHelper.ReadStringNullTerminated((nint)atkUnitBase[0]->Name);
- if (this.receiveEventHooks.TryGetValue(addonName, out var hook))
- {
- hook.Dispose();
- this.receiveEventHooks.Remove(addonName);
- }
+ this.UnregisterReceiveEventHook(addonName);
}
catch (Exception e)
{
@@ -410,51 +466,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType
Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke.");
}
}
-
- private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint data)
- {
- try
- {
- this.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs
- {
- Addon = (nint)addon,
- AtkEventType = (byte)eventType,
- EventParam = eventParam,
- AtkEvent = (nint)atkEvent,
- Data = data,
- });
- }
- catch (Exception e)
- {
- Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke.");
- }
-
- try
- {
- var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name);
- this.receiveEventHooks[addonName].Original(addon, eventType, eventParam, atkEvent, data);
- }
- catch (Exception e)
- {
- Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
- }
-
- try
- {
- this.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs
- {
- Addon = (nint)addon,
- AtkEventType = (byte)eventType,
- EventParam = eventParam,
- AtkEvent = (nint)atkEvent,
- Data = data,
- });
- }
- catch (Exception e)
- {
- Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke.");
- }
- }
}
///
diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
new file mode 100644
index 000000000..10171eb16
--- /dev/null
+++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs
@@ -0,0 +1,119 @@
+using System.Collections.Generic;
+
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Hooking;
+using Dalamud.Logging.Internal;
+using Dalamud.Memory;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Addon.Lifecycle;
+
+///
+/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent.
+/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly.
+///
+internal unsafe class AddonLifecycleReceiveEventListener : IDisposable
+{
+ private static readonly ModuleLog Log = new("AddonLifecycle");
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// AddonLifecycle service instance.
+ /// Initial Addon Requesting this listener.
+ /// Address of Addon's ReceiveEvent function.
+ internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress)
+ {
+ this.AddonLifecycle = service;
+ this.AddonNames = new List { addonName };
+ this.Hook = Hook.FromAddress(receiveEventAddress, this.OnReceiveEvent);
+ }
+
+ ///
+ /// Addon Receive Event Function delegate.
+ ///
+ /// Addon Pointer.
+ /// Event Type.
+ /// Unique Event ID.
+ /// Event Data.
+ /// Unknown.
+ public delegate void AddonReceiveEventDelegate(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint a5);
+
+ ///
+ /// Gets the list of addons that use this receive event hook.
+ ///
+ public List AddonNames { get; init; }
+
+ ///
+ /// Gets the address of the registered hook.
+ ///
+ public nint HookAddress => this.Hook?.Address ?? nint.Zero;
+
+ ///
+ /// Gets the contained hook for these addons.
+ ///
+ public Hook? Hook { get; init; }
+
+ ///
+ /// Gets or sets the Reference to AddonLifecycle service instance.
+ ///
+ private AddonLifecycle AddonLifecycle { get; set; }
+
+ ///
+ public void Dispose()
+ {
+ this.Hook?.Dispose();
+ }
+
+ private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint data)
+ {
+ // Check that we didn't get here through a call to another addons handler.
+ var addonName = MemoryHelper.ReadString((nint)addon->Name, 0x20);
+ if (!this.AddonNames.Contains(addonName))
+ {
+ this.Hook!.Original(addon, eventType, eventParam, atkEvent, data);
+ return;
+ }
+
+ try
+ {
+ this.AddonLifecycle.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs
+ {
+ Addon = (nint)addon,
+ AtkEventType = (byte)eventType,
+ EventParam = eventParam,
+ AtkEvent = (nint)atkEvent,
+ Data = data,
+ });
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke.");
+ }
+
+ try
+ {
+ this.Hook!.Original(addon, eventType, eventParam, atkEvent, data);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ try
+ {
+ this.AddonLifecycle.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs
+ {
+ Addon = (nint)addon,
+ AtkEventType = (byte)eventType,
+ EventParam = eventParam,
+ AtkEvent = (nint)atkEvent,
+ Data = data,
+ });
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke.");
+ }
+ }
+}
diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
index ba47d2c8e..d59b50e58 100644
--- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs
@@ -50,6 +50,7 @@ internal class DataWindow : Window
new DataShareWidget(),
new NetworkMonitorWidget(),
new IconBrowserWidget(),
+ new AddonLifecycleWidget(),
};
private readonly IOrderedEnumerable orderedModules;
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
new file mode 100644
index 000000000..a0cba737c
--- /dev/null
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs
@@ -0,0 +1,135 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Drawing;
+using System.Linq;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Interface.Utility;
+using ImGuiNET;
+
+namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
+
+///
+/// Debug widget for displaying AddonLifecycle data.
+///
+public class AddonLifecycleWidget : IDataWindowWidget
+{
+ ///
+ public string[]? CommandShortcuts { get; init; } = { "AddonLifecycle" };
+
+ ///
+ public string DisplayName { get; init; } = "Addon Lifecycle";
+
+ ///
+ [MemberNotNullWhen(true, "AddonLifecycle")]
+ public bool Ready { get; set; }
+
+ private AddonLifecycle? AddonLifecycle { get; set; }
+
+ ///
+ public void Load()
+ {
+ this.AddonLifecycle = Service.GetNullable();
+ if (this.AddonLifecycle is not null) this.Ready = true;
+ }
+
+ ///
+ public void Draw()
+ {
+ if (!this.Ready)
+ {
+ ImGui.Text("AddonLifecycle Reference is null, reload module.");
+ return;
+ }
+
+ if (ImGui.CollapsingHeader("Listeners"))
+ {
+ ImGui.Indent();
+ this.DrawEventListeners();
+ ImGui.Unindent();
+ }
+
+ if (ImGui.CollapsingHeader("ReceiveEvent Hooks"))
+ {
+ ImGui.Indent();
+ this.DrawReceiveEventHooks();
+ ImGui.Unindent();
+ }
+ }
+
+ private void DrawEventListeners()
+ {
+ if (!this.Ready) return;
+
+ foreach (var eventType in Enum.GetValues())
+ {
+ if (ImGui.CollapsingHeader(eventType.ToString()))
+ {
+ ImGui.Indent();
+ var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList();
+
+ if (!listeners.Any())
+ {
+ ImGui.Text("No Listeners Registered for Event");
+ }
+
+ if (ImGui.BeginTable("AddonLifecycleListenersTable", 2))
+ {
+ ImGui.TableSetupColumn("##AddonName", ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale);
+ ImGui.TableSetupColumn("##MethodInvoke", ImGuiTableColumnFlags.WidthStretch);
+
+ foreach (var listener in listeners)
+ {
+ ImGui.TableNextColumn();
+ ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName);
+
+ ImGui.TableNextColumn();
+ ImGui.Text($"{listener.FunctionDelegate.Target}::{listener.FunctionDelegate.Method.Name}");
+ }
+
+ ImGui.EndTable();
+ }
+
+ ImGui.Unindent();
+ }
+ }
+ }
+
+ private void DrawReceiveEventHooks()
+ {
+ if (!this.Ready) return;
+
+ var listeners = this.AddonLifecycle.ReceiveEventListeners;
+
+ if (!listeners.Any())
+ {
+ ImGui.Text("No ReceiveEvent Hooks are Registered");
+ }
+
+ foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners)
+ {
+ if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames)))
+ {
+ ImGui.Columns(2);
+
+ ImGui.Text("Hook Address");
+ ImGui.NextColumn();
+ ImGui.Text(receiveEventListener.HookAddress.ToString("X"));
+
+ ImGui.NextColumn();
+ ImGui.Text("Hook Status");
+ ImGui.NextColumn();
+ if (receiveEventListener.Hook is null)
+ {
+ ImGui.Text("Hook is null");
+ }
+ else
+ {
+ var color = receiveEventListener.Hook.IsEnabled ? KnownColor.Green.Vector() : KnownColor.OrangeRed.Vector();
+ var text = receiveEventListener.Hook.IsEnabled ? "Enabled" : "Disabled";
+ ImGui.TextColored(color, text);
+ }
+
+ ImGui.Columns(1);
+ }
+ }
+ }
+}