diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs
new file mode 100644
index 000000000..b4a904dde
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs
@@ -0,0 +1,37 @@
+namespace Dalamud.Game.Agent.AgentArgTypes;
+
+///
+/// Base class for AgentLifecycle AgentArgTypes.
+///
+public unsafe class AgentArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal AgentArgs()
+ {
+ }
+
+ ///
+ /// Gets the pointer to the Agents AgentInterface*.
+ ///
+ public nint Agent { get; internal set; }
+
+ ///
+ /// Gets the agent id.
+ ///
+ public uint AgentId { get; internal set; }
+
+ ///
+ /// Gets the type of these args.
+ ///
+ public virtual AgentArgsType Type => AgentArgsType.Generic;
+
+ ///
+ /// Gets the typed pointer to the Agents AgentInterface*.
+ ///
+ /// AgentInterface.
+ /// Typed pointer to contained Agents AgentInterface.
+ public T* GetAgentPointer() where T : unmanaged
+ => (T*)this.Agent;
+}
diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs
new file mode 100644
index 000000000..351760963
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs
@@ -0,0 +1,22 @@
+namespace Dalamud.Game.Agent.AgentArgTypes;
+
+///
+/// Agent argument data for game events.
+///
+public class AgentClassJobChangeArgs : AgentArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal AgentClassJobChangeArgs()
+ {
+ }
+
+ ///
+ public override AgentArgsType Type => AgentArgsType.ClassJobChange;
+
+ ///
+ /// Gets or sets a value indicating what the new ClassJob is.
+ ///
+ public byte ClassJobId { get; set; }
+}
diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs
new file mode 100644
index 000000000..3da601707
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs
@@ -0,0 +1,22 @@
+namespace Dalamud.Game.Agent.AgentArgTypes;
+
+///
+/// Agent argument data for game events.
+///
+public class AgentGameEventArgs : AgentArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal AgentGameEventArgs()
+ {
+ }
+
+ ///
+ public override AgentArgsType Type => AgentArgsType.GameEvent;
+
+ ///
+ /// Gets or sets a value representing which gameEvent was triggered.
+ ///
+ public int GameEvent { get; set; }
+}
diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs
new file mode 100644
index 000000000..a74371ebb
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs
@@ -0,0 +1,27 @@
+namespace Dalamud.Game.Agent.AgentArgTypes;
+
+///
+/// Agent argument data for game events.
+///
+public class AgentLevelChangeArgs : AgentArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal AgentLevelChangeArgs()
+ {
+ }
+
+ ///
+ public override AgentArgsType Type => AgentArgsType.LevelChange;
+
+ ///
+ /// Gets or sets a value indicating which ClassJob was switched to.
+ ///
+ public byte ClassJobId { get; set; }
+
+ ///
+ /// Gets or sets a value indicating what the new level is.
+ ///
+ public ushort Level { get; set; }
+}
diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs
new file mode 100644
index 000000000..01e1f25f6
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs
@@ -0,0 +1,37 @@
+namespace Dalamud.Game.Agent.AgentArgTypes;
+
+///
+/// Agent argument data for ReceiveEvent events.
+///
+public class AgentReceiveEventArgs : AgentArgs
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ internal AgentReceiveEventArgs()
+ {
+ }
+
+ ///
+ public override AgentArgsType Type => AgentArgsType.ReceiveEvent;
+
+ ///
+ /// Gets or sets the AtkValue return value for this event message.
+ ///
+ public nint ReturnValue { get; set; }
+
+ ///
+ /// Gets or sets the AtkValue array for this event message.
+ ///
+ public nint AtkValues { get; set; }
+
+ ///
+ /// Gets or sets the AtkValue count for this event message.
+ ///
+ public uint ValueCount { get; set; }
+
+ ///
+ /// Gets or sets the event kind for this event message.
+ ///
+ public ulong EventKind { get; set; }
+}
diff --git a/Dalamud/Game/Agent/AgentArgsType.cs b/Dalamud/Game/Agent/AgentArgsType.cs
new file mode 100644
index 000000000..0c96c0135
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentArgsType.cs
@@ -0,0 +1,32 @@
+namespace Dalamud.Game.Agent;
+
+///
+/// Enumeration for available AgentLifecycle arg data.
+///
+public enum AgentArgsType
+{
+ ///
+ /// Generic arg type that contains no meaningful data.
+ ///
+ Generic,
+
+ ///
+ /// Contains argument data for ReceiveEvent.
+ ///
+ ReceiveEvent,
+
+ ///
+ /// Contains argument data for GameEvent.
+ ///
+ GameEvent,
+
+ ///
+ /// Contains argument data for LevelChange.
+ ///
+ LevelChange,
+
+ ///
+ /// Contains argument data for ClassJobChange.
+ ///
+ ClassJobChange,
+}
diff --git a/Dalamud/Game/Agent/AgentEvent.cs b/Dalamud/Game/Agent/AgentEvent.cs
new file mode 100644
index 000000000..2a3002daa
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentEvent.cs
@@ -0,0 +1,87 @@
+namespace Dalamud.Game.Agent;
+
+///
+/// Enumeration for available AgentLifecycle events.
+///
+public enum AgentEvent
+{
+ ///
+ /// An event that is fired before the agent processes its Receive Event Function.
+ ///
+ PreReceiveEvent,
+
+ ///
+ /// An event that is fired after the agent has processed its Receive Event Function.
+ ///
+ PostReceiveEvent,
+
+ ///
+ /// An event that is fired before the agent processes its Filtered Receive Event Function.
+ ///
+ PreReceiveFilteredEvent,
+
+ ///
+ /// An event that is fired after the agent has processed its Filtered Receive Event Function.
+ ///
+ PostReceiveFilteredEvent,
+
+ ///
+ /// An event that is fired before the agent processes its Show Function.
+ ///
+ PreShow,
+
+ ///
+ /// An event that is fired after the agent has processed its Show Function.
+ ///
+ PostShow,
+
+ ///
+ /// An event that is fired before the agent processes its Hide Function.
+ ///
+ PreHide,
+
+ ///
+ /// An event that is fired after the agent has processed its Hide Function.
+ ///
+ PostHide,
+
+ ///
+ /// An event that is fired before the agent processes its Update Function.
+ ///
+ PreUpdate,
+
+ ///
+ /// An event that is fired after the agent has processed its Update Function.
+ ///
+ PostUpdate,
+
+ ///
+ /// An event that is fired before the agent processes its Game Event Function.
+ ///
+ PreGameEvent,
+
+ ///
+ /// An event that is fired after the agent has processed its Game Event Function.
+ ///
+ PostGameEvent,
+
+ ///
+ /// An event that is fired before the agent processes its Game Event Function.
+ ///
+ PreLevelChange,
+
+ ///
+ /// An event that is fired after the agent has processed its Level Change Function.
+ ///
+ PostLevelChange,
+
+ ///
+ /// An event that is fired before the agent processes its ClassJob Change Function.
+ ///
+ PreClassJobChange,
+
+ ///
+ /// An event that is fired after the agent has processed its ClassJob Change Function.
+ ///
+ PostClassJobChange,
+}
diff --git a/Dalamud/Game/Agent/AgentLifecycle.cs b/Dalamud/Game/Agent/AgentLifecycle.cs
new file mode 100644
index 000000000..1306a92c1
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentLifecycle.cs
@@ -0,0 +1,315 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+using Dalamud.Game.Agent.AgentArgTypes;
+using Dalamud.Hooking;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Logging.Internal;
+using Dalamud.Plugin.Services;
+
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.Interop;
+
+namespace Dalamud.Game.Agent;
+
+///
+/// This class provides events for in-game agent lifecycles.
+///
+[ServiceManager.EarlyLoadedService]
+internal unsafe class AgentLifecycle : IInternalDisposableService
+{
+ ///
+ /// Gets a list of all allocated agent virtual tables.
+ ///
+ public static readonly List AllocatedTables = [];
+
+ private static readonly ModuleLog Log = new("AgentLifecycle");
+
+ [ServiceManager.ServiceDependency]
+ private readonly Framework framework = Service.Get();
+
+ private Hook? onInitializeAgentsHook;
+ private bool isInvokingListeners;
+
+ [ServiceManager.ServiceConstructor]
+ private AgentLifecycle()
+ {
+ var agentModuleInstance = AgentModule.Instance();
+
+ // Hook is only used to determine appropriate timing for replacing Agent Virtual Tables
+ // If the agent module is already initialized, then we can replace the tables safely.
+ if (agentModuleInstance is null)
+ {
+ this.onInitializeAgentsHook = Hook.FromAddress((nint)AgentModule.MemberFunctionPointers.Ctor, this.OnAgentModuleInitialize);
+ this.onInitializeAgentsHook.Enable();
+ }
+ else
+ {
+ // For safety because this might be injected async, we will make sure we are on the main thread first.
+ this.framework.RunOnFrameworkThread(() => this.ReplaceVirtualTables(agentModuleInstance));
+ }
+ }
+
+ ///
+ /// Gets a list of all AgentLifecycle Event Listeners.
+ ///
+ /// Mapping is: EventType -> ListenerList
+ internal Dictionary>> EventListeners { get; } = [];
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ this.onInitializeAgentsHook?.Dispose();
+ this.onInitializeAgentsHook = null;
+
+ AllocatedTables.ForEach(entry => entry.Dispose());
+ AllocatedTables.Clear();
+ }
+
+ ///
+ /// Register a listener for the target event and agent.
+ ///
+ /// The listener to register.
+ internal void RegisterListener(AgentLifecycleEventListener listener)
+ {
+ this.framework.RunOnTick(() =>
+ {
+ if (!this.EventListeners.ContainsKey(listener.EventType))
+ {
+ if (!this.EventListeners.TryAdd(listener.EventType, []))
+ return;
+ }
+
+ // Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type
+ if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId))
+ {
+ if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, []))
+ return;
+ }
+
+ this.EventListeners[listener.EventType][listener.AgentId].Add(listener);
+ },
+ delayTicks: this.isInvokingListeners ? 1 : 0);
+ }
+
+ ///
+ /// Unregisters the listener from events.
+ ///
+ /// The listener to unregister.
+ internal void UnregisterListener(AgentLifecycleEventListener listener)
+ {
+ this.framework.RunOnTick(() =>
+ {
+ if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners))
+ {
+ if (agentListeners.TryGetValue(listener.AgentId, out var agentListener))
+ {
+ agentListener.Remove(listener);
+ }
+ }
+ },
+ delayTicks: this.isInvokingListeners ? 1 : 0);
+ }
+
+ ///
+ /// Invoke listeners for the specified event type.
+ ///
+ /// Event Type.
+ /// AgentARgs.
+ /// What to blame on errors.
+ internal void InvokeListenersSafely(AgentEvent eventType, AgentArgs args, [CallerMemberName] string blame = "")
+ {
+ this.isInvokingListeners = true;
+
+ // Early return if we don't have any listeners of this type
+ if (!this.EventListeners.TryGetValue(eventType, out var agentListeners)) return;
+
+ // Handle listeners for this event type that don't care which agent is triggering it
+ if (agentListeners.TryGetValue(uint.MaxValue, out var globalListeners))
+ {
+ foreach (var listener in globalListeners)
+ {
+ try
+ {
+ listener.FunctionDelegate.Invoke(eventType, args);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global agent event listener.");
+ }
+ }
+ }
+
+ // Handle listeners that are listening for this agent and event type specifically
+ if (agentListeners.TryGetValue(args.AgentId, out var agentListener))
+ {
+ foreach (var listener in agentListener)
+ {
+ try
+ {
+ listener.FunctionDelegate.Invoke(eventType, args);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific agent {(AgentId)args.AgentId}.");
+ }
+ }
+ }
+
+ this.isInvokingListeners = false;
+ }
+
+ ///
+ /// Resolves a virtual table address to the original virtual table address.
+ ///
+ /// The modified address to resolve.
+ /// The original address.
+ internal AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress)
+ {
+ var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
+ if (matchedTable == null) return null;
+
+ return matchedTable.OriginalVirtualTable;
+ }
+
+ private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule)
+ {
+ this.onInitializeAgentsHook!.Original(thisPtr, uiModule);
+
+ try
+ {
+ this.ReplaceVirtualTables(thisPtr);
+
+ // We don't need this hook anymore, it did its job!
+ this.onInitializeAgentsHook!.Dispose();
+ this.onInitializeAgentsHook = null;
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in AgentLifecycle during AgentModule Ctor.");
+ }
+ }
+
+ private void ReplaceVirtualTables(AgentModule* agentModule)
+ {
+ foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length))
+ {
+ try
+ {
+ var agentPointer = agentModule->Agents.GetPointer((int)index);
+
+ if (agentPointer is null)
+ {
+ Log.Warning("Null Agent Found?");
+ continue;
+ }
+
+ // AgentVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
+ AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, index, this));
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Exception in AgentLifecycle during ReplaceVirtualTables.");
+ }
+ }
+ }
+}
+
+///
+/// Plugin-scoped version of a AgentLifecycle service.
+///
+[PluginInterface]
+[ServiceManager.ScopedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLifecycle
+{
+ [ServiceManager.ServiceDependency]
+ private readonly AgentLifecycle agentLifecycleService = Service.Get();
+
+ private readonly List eventListeners = [];
+
+ ///
+ void IInternalDisposableService.DisposeService()
+ {
+ foreach (var listener in this.eventListeners)
+ {
+ this.agentLifecycleService.UnregisterListener(listener);
+ }
+ }
+
+ ///
+ public void RegisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate handler)
+ {
+ foreach (var agentId in agentIds)
+ {
+ this.RegisterListener(eventType, agentId, handler);
+ }
+ }
+
+ ///
+ public void RegisterListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate handler)
+ {
+ var listener = new AgentLifecycleEventListener(eventType, agentId, handler);
+ this.eventListeners.Add(listener);
+ this.agentLifecycleService.RegisterListener(listener);
+ }
+
+ ///
+ public void RegisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate handler)
+ {
+ this.RegisterListener(eventType, uint.MaxValue, handler);
+ }
+
+ ///
+ public void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate? handler = null)
+ {
+ foreach (var agentId in agentIds)
+ {
+ this.UnregisterListener(eventType, agentId, handler);
+ }
+ }
+
+ ///
+ public void UnregisterListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate? handler = null)
+ {
+ this.eventListeners.RemoveAll(entry =>
+ {
+ if (entry.EventType != eventType) return false;
+ if (entry.AgentId != agentId) return false;
+ if (handler is not null && entry.FunctionDelegate != handler) return false;
+
+ this.agentLifecycleService.UnregisterListener(entry);
+ return true;
+ });
+ }
+
+ ///
+ public void UnregisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate? handler = null)
+ {
+ this.UnregisterListener(eventType, uint.MaxValue, handler);
+ }
+
+ ///
+ public void UnregisterListener(params IAgentLifecycle.AgentEventDelegate[] handlers)
+ {
+ foreach (var handler in handlers)
+ {
+ this.eventListeners.RemoveAll(entry =>
+ {
+ if (entry.FunctionDelegate != handler) return false;
+
+ this.agentLifecycleService.UnregisterListener(entry);
+ return true;
+ });
+ }
+ }
+
+ ///
+ public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
+ => (nint)this.agentLifecycleService.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress);
+}
diff --git a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs
new file mode 100644
index 000000000..3521d2c13
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs
@@ -0,0 +1,38 @@
+using Dalamud.Plugin.Services;
+
+namespace Dalamud.Game.Agent;
+
+///
+/// This class is a helper for tracking and invoking listener delegates.
+///
+public class AgentLifecycleEventListener
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Event type to listen for.
+ /// Agent id to listen for.
+ /// Delegate to invoke.
+ internal AgentLifecycleEventListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate functionDelegate)
+ {
+ this.EventType = eventType;
+ this.AgentId = agentId;
+ this.FunctionDelegate = functionDelegate;
+ }
+
+ ///
+ /// Gets the agentId of the agent this listener is looking for.
+ /// uint.MaxValue if it wants to be called for any agent.
+ ///
+ public uint AgentId { get; init; }
+
+ ///
+ /// Gets the event type this listener is looking for.
+ ///
+ public AgentEvent EventType { get; init; }
+
+ ///
+ /// Gets the delegate this listener invokes.
+ ///
+ public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; }
+}
diff --git a/Dalamud/Game/Agent/AgentVirtualTable.cs b/Dalamud/Game/Agent/AgentVirtualTable.cs
new file mode 100644
index 000000000..3c23616e8
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentVirtualTable.cs
@@ -0,0 +1,393 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+using Dalamud.Game.Agent.AgentArgTypes;
+using Dalamud.Logging.Internal;
+
+using FFXIVClientStructs.FFXIV.Client.System.Memory;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace Dalamud.Game.Agent;
+
+///
+/// Represents a class that holds references to an agents original and modified virtual table entries.
+///
+internal unsafe class AgentVirtualTable : IDisposable
+{
+ // This need to be at minimum the largest virtual table size of all agents
+ // Copying extra entries is not problematic, and is considered safe.
+ private const int VirtualTableEntryCount = 60;
+
+ private const bool EnableLogging = true;
+
+ private static readonly ModuleLog Log = new("AgentVT");
+
+ private readonly AgentLifecycle lifecycleService;
+
+ private readonly uint agentId;
+
+ // Each agent gets its own set of args that are used to mutate the original call when used in pre-calls
+ private readonly AgentReceiveEventArgs receiveEventArgs = new();
+ private readonly AgentReceiveEventArgs filteredReceiveEventArgs = new();
+ private readonly AgentArgs showArgs = new();
+ private readonly AgentArgs hideArgs = new();
+ private readonly AgentArgs updateArgs = new();
+ private readonly AgentGameEventArgs gameEventArgs = new();
+ private readonly AgentLevelChangeArgs levelChangeArgs = new();
+ private readonly AgentClassJobChangeArgs classJobChangeArgs = new();
+
+ private readonly AgentInterface* agentInterface;
+
+ // Pinned Function Delegates, as these functions get assigned to an unmanaged virtual table,
+ // the CLR needs to know they are in use, or it will invalidate them causing random crashing.
+ private readonly AgentInterface.Delegates.ReceiveEvent receiveEventFunction;
+ private readonly AgentInterface.Delegates.ReceiveEvent2 filteredReceiveEventFunction;
+ private readonly AgentInterface.Delegates.Show showFunction;
+ private readonly AgentInterface.Delegates.Hide hideFunction;
+ private readonly AgentInterface.Delegates.Update updateFunction;
+ private readonly AgentInterface.Delegates.OnGameEvent gameEventFunction;
+ private readonly AgentInterface.Delegates.OnLevelChange levelChangeFunction;
+ private readonly AgentInterface.Delegates.OnClassJobChange classJobChangeFunction;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// AgentInterface* for the agent to replace the table of.
+ /// Agent ID.
+ /// Reference to AgentLifecycle service to callback and invoke listeners.
+ internal AgentVirtualTable(AgentInterface* agent, uint agentId, AgentLifecycle lifecycleService)
+ {
+ Log.Debug($"Initializing AgentVirtualTable for {(AgentId)agentId}, Address: {(nint)agent:X}");
+
+ this.agentInterface = agent;
+ this.agentId = agentId;
+ this.lifecycleService = lifecycleService;
+
+ // Save original virtual table
+ this.OriginalVirtualTable = agent->VirtualTable;
+
+ // Create copy of original table
+ // Note this will copy any derived/overriden functions that this specific agent has.
+ // Note: currently there are 16 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game
+ this.ModifiedVirtualTable = (AgentInterface.AgentInterfaceVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8);
+ NativeMemory.Copy(agent->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
+
+ // Overwrite the agents existing virtual table with our own
+ agent->VirtualTable = this.ModifiedVirtualTable;
+
+ // Pin each of our listener functions
+ this.receiveEventFunction = this.OnAgentReceiveEvent;
+ this.filteredReceiveEventFunction = this.OnAgentFilteredReceiveEvent;
+ this.showFunction = this.OnAgentShow;
+ this.hideFunction = this.OnAgentHide;
+ this.updateFunction = this.OnAgentUpdate;
+ this.gameEventFunction = this.OnAgentGameEvent;
+ this.levelChangeFunction = this.OnAgentLevelChange;
+ this.classJobChangeFunction = this.OnClassJobChange;
+
+ // Overwrite specific virtual table entries
+ this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction);
+ this.ModifiedVirtualTable->ReceiveEvent2 = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.filteredReceiveEventFunction);
+ this.ModifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction);
+ this.ModifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
+ this.ModifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
+ this.ModifiedVirtualTable->OnGameEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.gameEventFunction);
+ this.ModifiedVirtualTable->OnLevelChange = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.levelChangeFunction);
+ this.ModifiedVirtualTable->OnClassJobChange = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.classJobChangeFunction);
+ }
+
+ ///
+ /// Gets the original virtual table address for this agent.
+ ///
+ internal AgentInterface.AgentInterfaceVirtualTable* OriginalVirtualTable { get; private set; }
+
+ ///
+ /// Gets the modified virtual address for this agent.
+ ///
+ internal AgentInterface.AgentInterfaceVirtualTable* ModifiedVirtualTable { get; private set; }
+
+ ///
+ public void Dispose()
+ {
+ // Ensure restoration is done atomically.
+ Interlocked.Exchange(ref *(nint*)&this.agentInterface->VirtualTable, (nint)this.OriginalVirtualTable);
+ IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
+ }
+
+ private AtkValue* OnAgentReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
+ {
+ AtkValue* result = null;
+
+ try
+ {
+ this.LogEvent(EnableLogging);
+
+ this.receiveEventArgs.Agent = (nint)thisPtr;
+ this.receiveEventArgs.AgentId = this.agentId;
+ this.receiveEventArgs.ReturnValue = (nint)returnValue;
+ this.receiveEventArgs.AtkValues = (nint)values;
+ this.receiveEventArgs.ValueCount = valueCount;
+ this.receiveEventArgs.EventKind = eventKind;
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveEvent, this.receiveEventArgs);
+
+ returnValue = (AtkValue*)this.receiveEventArgs.ReturnValue;
+ values = (AtkValue*)this.receiveEventArgs.AtkValues;
+ valueCount = this.receiveEventArgs.ValueCount;
+ eventKind = this.receiveEventArgs.EventKind;
+
+ try
+ {
+ result = this.OriginalVirtualTable->ReceiveEvent(thisPtr, returnValue, values, valueCount, eventKind);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original Agent ReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveEvent, this.receiveEventArgs);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentReceiveEvent.");
+ }
+
+ return result;
+ }
+
+ private AtkValue* OnAgentFilteredReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
+ {
+ AtkValue* result = null;
+
+ try
+ {
+ this.LogEvent(EnableLogging);
+
+ this.filteredReceiveEventArgs.Agent = (nint)thisPtr;
+ this.filteredReceiveEventArgs.AgentId = this.agentId;
+ this.filteredReceiveEventArgs.ReturnValue = (nint)returnValue;
+ this.filteredReceiveEventArgs.AtkValues = (nint)values;
+ this.filteredReceiveEventArgs.ValueCount = valueCount;
+ this.filteredReceiveEventArgs.EventKind = eventKind;
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PreReceiveFilteredEvent, this.filteredReceiveEventArgs);
+
+ returnValue = (AtkValue*)this.filteredReceiveEventArgs.ReturnValue;
+ values = (AtkValue*)this.filteredReceiveEventArgs.AtkValues;
+ valueCount = this.filteredReceiveEventArgs.ValueCount;
+ eventKind = this.filteredReceiveEventArgs.EventKind;
+
+ try
+ {
+ result = this.OriginalVirtualTable->ReceiveEvent2(thisPtr, returnValue, values, valueCount, eventKind);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original Agent FilteredReceiveEvent. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PostReceiveFilteredEvent, this.filteredReceiveEventArgs);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentFilteredReceiveEvent.");
+ }
+
+ return result;
+ }
+
+ private void OnAgentShow(AgentInterface* thisPtr)
+ {
+ try
+ {
+ this.LogEvent(EnableLogging);
+
+ this.showArgs.Agent = (nint)thisPtr;
+ this.showArgs.AgentId = this.agentId;
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PreShow, this.showArgs);
+
+ try
+ {
+ this.OriginalVirtualTable->Show(thisPtr);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original Addon Show. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PostShow, this.showArgs);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentShow.");
+ }
+ }
+
+ private void OnAgentHide(AgentInterface* thisPtr)
+ {
+ try
+ {
+ this.LogEvent(EnableLogging);
+
+ this.hideArgs.Agent = (nint)thisPtr;
+ this.hideArgs.AgentId = this.agentId;
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PreHide, this.hideArgs);
+
+ try
+ {
+ this.OriginalVirtualTable->Hide(thisPtr);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original Addon Hide. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PostHide, this.hideArgs);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentHide.");
+ }
+ }
+
+ private void OnAgentUpdate(AgentInterface* thisPtr, uint frameCount)
+ {
+ try
+ {
+ this.LogEvent(EnableLogging);
+
+ this.updateArgs.Agent = (nint)thisPtr;
+ this.updateArgs.AgentId = this.agentId;
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PreUpdate, this.updateArgs);
+
+ try
+ {
+ this.OriginalVirtualTable->Update(thisPtr, frameCount);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original Addon Update. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PostUpdate, this.updateArgs);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentUpdate.");
+ }
+ }
+
+ private void OnAgentGameEvent(AgentInterface* thisPtr, AgentInterface.GameEvent gameEvent)
+ {
+ try
+ {
+ this.LogEvent(EnableLogging);
+
+ this.gameEventArgs.Agent = (nint)thisPtr;
+ this.gameEventArgs.AgentId = this.agentId;
+ this.gameEventArgs.GameEvent = (int)gameEvent;
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PreGameEvent, this.gameEventArgs);
+
+ gameEvent = (AgentInterface.GameEvent)this.gameEventArgs.GameEvent;
+
+ try
+ {
+ this.OriginalVirtualTable->OnGameEvent(thisPtr, gameEvent);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original Addon OnGameEvent. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PostGameEvent, this.gameEventArgs);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentGameEvent.");
+ }
+ }
+
+ private void OnAgentLevelChange(AgentInterface* thisPtr, byte classJobId, ushort level)
+ {
+ try
+ {
+ this.LogEvent(EnableLogging);
+
+ this.levelChangeArgs.Agent = (nint)thisPtr;
+ this.levelChangeArgs.AgentId = this.agentId;
+ this.levelChangeArgs.ClassJobId = classJobId;
+ this.levelChangeArgs.Level = level;
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PreLevelChange, this.levelChangeArgs);
+
+ classJobId = this.levelChangeArgs.ClassJobId;
+ level = this.levelChangeArgs.Level;
+
+ try
+ {
+ this.OriginalVirtualTable->OnLevelChange(thisPtr, classJobId, level);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original Addon OnLevelChange. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PostLevelChange, this.levelChangeArgs);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception from Dalamud when attempting to process OnAgentLevelChange.");
+ }
+ }
+
+ private void OnClassJobChange(AgentInterface* thisPtr, byte classJobId)
+ {
+ try
+ {
+ this.LogEvent(EnableLogging);
+
+ this.classJobChangeArgs.Agent = (nint)thisPtr;
+ this.classJobChangeArgs.AgentId = this.agentId;
+ this.classJobChangeArgs.ClassJobId = classJobId;
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PreClassJobChange, this.classJobChangeArgs);
+
+ classJobId = this.classJobChangeArgs.ClassJobId;
+
+ try
+ {
+ this.OriginalVirtualTable->OnClassJobChange(thisPtr, classJobId);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception when calling original Addon OnClassJobChange. This may be a bug in the game or another plugin hooking this method.");
+ }
+
+ this.lifecycleService.InvokeListenersSafely(AgentEvent.PostClassJobChange, this.classJobChangeArgs);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "Caught exception from Dalamud when attempting to process OnClassJobChange.");
+ }
+ }
+
+ [Conditional("DEBUG")]
+ private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "")
+ {
+ if (loggingEnabled)
+ {
+ // Manually disable the really spammy log events, you can comment this out if you need to debug them.
+ if (caller is "OnAgentUpdate" || (AgentId)this.agentId is AgentId.PadMouseMode)
+ return;
+
+ Log.Debug($"[{caller}]: {(AgentId)this.agentId}");
+ }
+ }
+}
diff --git a/Dalamud/Plugin/Services/IAgentLifecycle.cs b/Dalamud/Plugin/Services/IAgentLifecycle.cs
new file mode 100644
index 000000000..a1ed26125
--- /dev/null
+++ b/Dalamud/Plugin/Services/IAgentLifecycle.cs
@@ -0,0 +1,88 @@
+using System.Collections.Generic;
+using System.Runtime.InteropServices;
+
+using Dalamud.Game.Agent;
+using Dalamud.Game.Agent.AgentArgTypes;
+
+namespace Dalamud.Plugin.Services;
+
+///
+/// This class provides events for in-game agent lifecycles.
+///
+public interface IAgentLifecycle : IDalamudService
+{
+ ///
+ /// Delegate for receiving agent lifecycle event messages.
+ ///
+ /// The event type that triggered the message.
+ /// Information about what agent triggered the message.
+ public delegate void AgentEventDelegate(AgentEvent type, AgentArgs args);
+
+ ///
+ /// Register a listener that will trigger on the specified event and any of the specified agent.
+ ///
+ /// Event type to trigger on.
+ /// Agent IDs that will trigger the handler to be invoked.
+ /// The handler to invoke.
+ void RegisterListener(AgentEvent eventType, IEnumerable agentIds, AgentEventDelegate handler);
+
+ ///
+ /// Register a listener that will trigger on the specified event only for the specified agent.
+ ///
+ /// Event type to trigger on.
+ /// The agent ID that will trigger the handler to be invoked.
+ /// The handler to invoke.
+ void RegisterListener(AgentEvent eventType, uint agentId, AgentEventDelegate handler);
+
+ ///
+ /// Register a listener that will trigger on the specified event for any agent.
+ ///
+ /// Event type to trigger on.
+ /// The handler to invoke.
+ void RegisterListener(AgentEvent eventType, AgentEventDelegate handler);
+
+ ///
+ /// Unregister listener from specified event type and specified agent IDs.
+ ///
+ ///
+ /// If a specific handler is not provided, all handlers for the event type and agent IDs will be unregistered.
+ ///
+ /// Event type to deregister.
+ /// Agent IDs to deregister.
+ /// Optional specific handler to remove.
+ void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, [Optional] AgentEventDelegate handler);
+
+ ///
+ /// Unregister all listeners for the specified event type and agent ID.
+ ///
+ ///
+ /// If a specific handler is not provided, all handlers for the event type and agents will be unregistered.
+ ///
+ /// Event type to deregister.
+ /// Agent id to deregister.
+ /// Optional specific handler to remove.
+ void UnregisterListener(AgentEvent eventType, uint agentId, [Optional] AgentEventDelegate handler);
+
+ ///
+ /// Unregister an event type handler.
This will only remove a handler that is added via .
+ ///
+ ///
+ /// If a specific handler is not provided, all handlers for the event type and agents will be unregistered.
+ ///
+ /// Event type to deregister.
+ /// Optional specific handler to remove.
+ void UnregisterListener(AgentEvent eventType, [Optional] AgentEventDelegate handler);
+
+ ///
+ /// Unregister all events that use the specified handlers.
+ ///
+ /// Handlers to remove.
+ void UnregisterListener(params AgentEventDelegate[] handlers);
+
+ ///
+ /// Resolves an agents virtual table address back to the original unmodified table address.
+ ///
+ /// The address of a modified agents virtual table.
+ /// The address of the agents original virtual table.
+ nint GetOriginalVirtualTable(nint virtualTableAddress);
+}