Add Agent Lifecycle

This commit is contained in:
MidoriKami 2026-01-04 21:40:31 -08:00
parent 27414d33dd
commit d0caf98eb3
11 changed files with 1098 additions and 0 deletions

View file

@ -0,0 +1,37 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Base class for AgentLifecycle AgentArgTypes.
/// </summary>
public unsafe class AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentArgs"/> class.
/// </summary>
internal AgentArgs()
{
}
/// <summary>
/// Gets the pointer to the Agents AgentInterface*.
/// </summary>
public nint Agent { get; internal set; }
/// <summary>
/// Gets the agent id.
/// </summary>
public uint AgentId { get; internal set; }
/// <summary>
/// Gets the type of these args.
/// </summary>
public virtual AgentArgsType Type => AgentArgsType.Generic;
/// <summary>
/// Gets the typed pointer to the Agents AgentInterface*.
/// </summary>
/// <typeparam name="T">AgentInterface.</typeparam>
/// <returns>Typed pointer to contained Agents AgentInterface.</returns>
public T* GetAgentPointer<T>() where T : unmanaged
=> (T*)this.Agent;
}

View file

@ -0,0 +1,22 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for game events.
/// </summary>
public class AgentClassJobChangeArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentClassJobChangeArgs"/> class.
/// </summary>
internal AgentClassJobChangeArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.ClassJobChange;
/// <summary>
/// Gets or sets a value indicating what the new ClassJob is.
/// </summary>
public byte ClassJobId { get; set; }
}

View file

@ -0,0 +1,22 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for game events.
/// </summary>
public class AgentGameEventArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentGameEventArgs"/> class.
/// </summary>
internal AgentGameEventArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.GameEvent;
/// <summary>
/// Gets or sets a value representing which gameEvent was triggered.
/// </summary>
public int GameEvent { get; set; }
}

View file

@ -0,0 +1,27 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for game events.
/// </summary>
public class AgentLevelChangeArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentLevelChangeArgs"/> class.
/// </summary>
internal AgentLevelChangeArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.LevelChange;
/// <summary>
/// Gets or sets a value indicating which ClassJob was switched to.
/// </summary>
public byte ClassJobId { get; set; }
/// <summary>
/// Gets or sets a value indicating what the new level is.
/// </summary>
public ushort Level { get; set; }
}

View file

@ -0,0 +1,37 @@
namespace Dalamud.Game.Agent.AgentArgTypes;
/// <summary>
/// Agent argument data for ReceiveEvent events.
/// </summary>
public class AgentReceiveEventArgs : AgentArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentReceiveEventArgs"/> class.
/// </summary>
internal AgentReceiveEventArgs()
{
}
/// <inheritdoc/>
public override AgentArgsType Type => AgentArgsType.ReceiveEvent;
/// <summary>
/// Gets or sets the AtkValue return value for this event message.
/// </summary>
public nint ReturnValue { get; set; }
/// <summary>
/// Gets or sets the AtkValue array for this event message.
/// </summary>
public nint AtkValues { get; set; }
/// <summary>
/// Gets or sets the AtkValue count for this event message.
/// </summary>
public uint ValueCount { get; set; }
/// <summary>
/// Gets or sets the event kind for this event message.
/// </summary>
public ulong EventKind { get; set; }
}

View file

@ -0,0 +1,32 @@
namespace Dalamud.Game.Agent;
/// <summary>
/// Enumeration for available AgentLifecycle arg data.
/// </summary>
public enum AgentArgsType
{
/// <summary>
/// Generic arg type that contains no meaningful data.
/// </summary>
Generic,
/// <summary>
/// Contains argument data for ReceiveEvent.
/// </summary>
ReceiveEvent,
/// <summary>
/// Contains argument data for GameEvent.
/// </summary>
GameEvent,
/// <summary>
/// Contains argument data for LevelChange.
/// </summary>
LevelChange,
/// <summary>
/// Contains argument data for ClassJobChange.
/// </summary>
ClassJobChange,
}

View file

@ -0,0 +1,87 @@
namespace Dalamud.Game.Agent;
/// <summary>
/// Enumeration for available AgentLifecycle events.
/// </summary>
public enum AgentEvent
{
/// <summary>
/// An event that is fired before the agent processes its Receive Event Function.
/// </summary>
PreReceiveEvent,
/// <summary>
/// An event that is fired after the agent has processed its Receive Event Function.
/// </summary>
PostReceiveEvent,
/// <summary>
/// An event that is fired before the agent processes its Filtered Receive Event Function.
/// </summary>
PreReceiveFilteredEvent,
/// <summary>
/// An event that is fired after the agent has processed its Filtered Receive Event Function.
/// </summary>
PostReceiveFilteredEvent,
/// <summary>
/// An event that is fired before the agent processes its Show Function.
/// </summary>
PreShow,
/// <summary>
/// An event that is fired after the agent has processed its Show Function.
/// </summary>
PostShow,
/// <summary>
/// An event that is fired before the agent processes its Hide Function.
/// </summary>
PreHide,
/// <summary>
/// An event that is fired after the agent has processed its Hide Function.
/// </summary>
PostHide,
/// <summary>
/// An event that is fired before the agent processes its Update Function.
/// </summary>
PreUpdate,
/// <summary>
/// An event that is fired after the agent has processed its Update Function.
/// </summary>
PostUpdate,
/// <summary>
/// An event that is fired before the agent processes its Game Event Function.
/// </summary>
PreGameEvent,
/// <summary>
/// An event that is fired after the agent has processed its Game Event Function.
/// </summary>
PostGameEvent,
/// <summary>
/// An event that is fired before the agent processes its Game Event Function.
/// </summary>
PreLevelChange,
/// <summary>
/// An event that is fired after the agent has processed its Level Change Function.
/// </summary>
PostLevelChange,
/// <summary>
/// An event that is fired before the agent processes its ClassJob Change Function.
/// </summary>
PreClassJobChange,
/// <summary>
/// An event that is fired after the agent has processed its ClassJob Change Function.
/// </summary>
PostClassJobChange,
}

View file

@ -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;
/// <summary>
/// This class provides events for in-game agent lifecycles.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class AgentLifecycle : IInternalDisposableService
{
/// <summary>
/// Gets a list of all allocated agent virtual tables.
/// </summary>
public static readonly List<AgentVirtualTable> AllocatedTables = [];
private static readonly ModuleLog Log = new("AgentLifecycle");
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private Hook<AgentModule.Delegates.Ctor>? 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<AgentModule.Delegates.Ctor>.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));
}
}
/// <summary>
/// Gets a list of all AgentLifecycle Event Listeners.
/// </summary> <br/>
/// Mapping is: EventType -> ListenerList
internal Dictionary<AgentEvent, Dictionary<uint, HashSet<AgentLifecycleEventListener>>> EventListeners { get; } = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onInitializeAgentsHook?.Dispose();
this.onInitializeAgentsHook = null;
AllocatedTables.ForEach(entry => entry.Dispose());
AllocatedTables.Clear();
}
/// <summary>
/// Register a listener for the target event and agent.
/// </summary>
/// <param name="listener">The listener to register.</param>
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);
}
/// <summary>
/// Unregisters the listener from events.
/// </summary>
/// <param name="listener">The listener to unregister.</param>
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);
}
/// <summary>
/// Invoke listeners for the specified event type.
/// </summary>
/// <param name="eventType">Event Type.</param>
/// <param name="args">AgentARgs.</param>
/// <param name="blame">What to blame on errors.</param>
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;
}
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
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.");
}
}
}
}
/// <summary>
/// Plugin-scoped version of a AgentLifecycle service.
/// </summary>
[PluginInterface]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IAgentLifecycle>]
#pragma warning restore SA1015
internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLifecycle
{
[ServiceManager.ServiceDependency]
private readonly AgentLifecycle agentLifecycleService = Service<AgentLifecycle>.Get();
private readonly List<AgentLifecycleEventListener> eventListeners = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
foreach (var listener in this.eventListeners)
{
this.agentLifecycleService.UnregisterListener(listener);
}
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, IEnumerable<uint> agentIds, IAgentLifecycle.AgentEventDelegate handler)
{
foreach (var agentId in agentIds)
{
this.RegisterListener(eventType, agentId, handler);
}
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate handler)
{
this.RegisterListener(eventType, uint.MaxValue, handler);
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, IEnumerable<uint> agentIds, IAgentLifecycle.AgentEventDelegate? handler = null)
{
foreach (var agentId in agentIds)
{
this.UnregisterListener(eventType, agentId, handler);
}
}
/// <inheritdoc/>
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;
});
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate? handler = null)
{
this.UnregisterListener(eventType, uint.MaxValue, handler);
}
/// <inheritdoc/>
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;
});
}
}
/// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)this.agentLifecycleService.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress);
}

View file

@ -0,0 +1,38 @@
using Dalamud.Plugin.Services;
namespace Dalamud.Game.Agent;
/// <summary>
/// This class is a helper for tracking and invoking listener delegates.
/// </summary>
public class AgentLifecycleEventListener
{
/// <summary>
/// Initializes a new instance of the <see cref="AgentLifecycleEventListener"/> class.
/// </summary>
/// <param name="eventType">Event type to listen for.</param>
/// <param name="agentId">Agent id to listen for.</param>
/// <param name="functionDelegate">Delegate to invoke.</param>
internal AgentLifecycleEventListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate functionDelegate)
{
this.EventType = eventType;
this.AgentId = agentId;
this.FunctionDelegate = functionDelegate;
}
/// <summary>
/// Gets the agentId of the agent this listener is looking for.
/// uint.MaxValue if it wants to be called for any agent.
/// </summary>
public uint AgentId { get; init; }
/// <summary>
/// Gets the event type this listener is looking for.
/// </summary>
public AgentEvent EventType { get; init; }
/// <summary>
/// Gets the delegate this listener invokes.
/// </summary>
public IAgentLifecycle.AgentEventDelegate FunctionDelegate { get; init; }
}

View file

@ -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;
/// <summary>
/// Represents a class that holds references to an agents original and modified virtual table entries.
/// </summary>
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;
/// <summary>
/// Initializes a new instance of the <see cref="AgentVirtualTable"/> class.
/// </summary>
/// <param name="agent">AgentInterface* for the agent to replace the table of.</param>
/// <param name="agentId">Agent ID.</param>
/// <param name="lifecycleService">Reference to AgentLifecycle service to callback and invoke listeners.</param>
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<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction);
this.ModifiedVirtualTable->ReceiveEvent2 = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.filteredReceiveEventFunction);
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AgentInterface*, uint, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
this.ModifiedVirtualTable->OnGameEvent = (delegate* unmanaged<AgentInterface*, AgentInterface.GameEvent, void>)Marshal.GetFunctionPointerForDelegate(this.gameEventFunction);
this.ModifiedVirtualTable->OnLevelChange = (delegate* unmanaged<AgentInterface*, byte, ushort, void>)Marshal.GetFunctionPointerForDelegate(this.levelChangeFunction);
this.ModifiedVirtualTable->OnClassJobChange = (delegate* unmanaged<AgentInterface*, byte, void>)Marshal.GetFunctionPointerForDelegate(this.classJobChangeFunction);
}
/// <summary>
/// Gets the original virtual table address for this agent.
/// </summary>
internal AgentInterface.AgentInterfaceVirtualTable* OriginalVirtualTable { get; private set; }
/// <summary>
/// Gets the modified virtual address for this agent.
/// </summary>
internal AgentInterface.AgentInterfaceVirtualTable* ModifiedVirtualTable { get; private set; }
/// <inheritdoc/>
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}");
}
}
}

View file

@ -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;
/// <summary>
/// This class provides events for in-game agent lifecycles.
/// </summary>
public interface IAgentLifecycle : IDalamudService
{
/// <summary>
/// Delegate for receiving agent lifecycle event messages.
/// </summary>
/// <param name="type">The event type that triggered the message.</param>
/// <param name="args">Information about what agent triggered the message.</param>
public delegate void AgentEventDelegate(AgentEvent type, AgentArgs args);
/// <summary>
/// Register a listener that will trigger on the specified event and any of the specified agent.
/// </summary>
/// <param name="eventType">Event type to trigger on.</param>
/// <param name="agentIds">Agent IDs that will trigger the handler to be invoked.</param>
/// <param name="handler">The handler to invoke.</param>
void RegisterListener(AgentEvent eventType, IEnumerable<uint> agentIds, AgentEventDelegate handler);
/// <summary>
/// Register a listener that will trigger on the specified event only for the specified agent.
/// </summary>
/// <param name="eventType">Event type to trigger on.</param>
/// <param name="agentId">The agent ID that will trigger the handler to be invoked.</param>
/// <param name="handler">The handler to invoke.</param>
void RegisterListener(AgentEvent eventType, uint agentId, AgentEventDelegate handler);
/// <summary>
/// Register a listener that will trigger on the specified event for any agent.
/// </summary>
/// <param name="eventType">Event type to trigger on.</param>
/// <param name="handler">The handler to invoke.</param>
void RegisterListener(AgentEvent eventType, AgentEventDelegate handler);
/// <summary>
/// Unregister listener from specified event type and specified agent IDs.
/// </summary>
/// <remarks>
/// If a specific handler is not provided, all handlers for the event type and agent IDs will be unregistered.
/// </remarks>
/// <param name="eventType">Event type to deregister.</param>
/// <param name="agentIds">Agent IDs to deregister.</param>
/// <param name="handler">Optional specific handler to remove.</param>
void UnregisterListener(AgentEvent eventType, IEnumerable<uint> agentIds, [Optional] AgentEventDelegate handler);
/// <summary>
/// Unregister all listeners for the specified event type and agent ID.
/// </summary>
/// <remarks>
/// If a specific handler is not provided, all handlers for the event type and agents will be unregistered.
/// </remarks>
/// <param name="eventType">Event type to deregister.</param>
/// <param name="agentId">Agent id to deregister.</param>
/// <param name="handler">Optional specific handler to remove.</param>
void UnregisterListener(AgentEvent eventType, uint agentId, [Optional] AgentEventDelegate handler);
/// <summary>
/// Unregister an event type handler.<br/>This will only remove a handler that is added via <see cref="RegisterListener(AgentEvent, AgentEventDelegate)"/>.
/// </summary>
/// <remarks>
/// If a specific handler is not provided, all handlers for the event type and agents will be unregistered.
/// </remarks>
/// <param name="eventType">Event type to deregister.</param>
/// <param name="handler">Optional specific handler to remove.</param>
void UnregisterListener(AgentEvent eventType, [Optional] AgentEventDelegate handler);
/// <summary>
/// Unregister all events that use the specified handlers.
/// </summary>
/// <param name="handlers">Handlers to remove.</param>
void UnregisterListener(params AgentEventDelegate[] handlers);
/// <summary>
/// Resolves an agents virtual table address back to the original unmodified table address.
/// </summary>
/// <param name="virtualTableAddress">The address of a modified agents virtual table.</param>
/// <returns>The address of the agents original virtual table.</returns>
nint GetOriginalVirtualTable(nint virtualTableAddress);
}