From d0caf98eb3d0333ee7fb3226fefca77fecc052f0 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 4 Jan 2026 21:40:31 -0800 Subject: [PATCH 1/4] Add Agent Lifecycle --- Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs | 37 ++ .../AgentArgTypes/AgentClassJobChangeArgs.cs | 22 + .../Agent/AgentArgTypes/AgentGameEventArgs.cs | 22 + .../AgentArgTypes/AgentLevelChangeArgs.cs | 27 ++ .../AgentArgTypes/AgentReceiveEventArgs.cs | 37 ++ Dalamud/Game/Agent/AgentArgsType.cs | 32 ++ Dalamud/Game/Agent/AgentEvent.cs | 87 ++++ Dalamud/Game/Agent/AgentLifecycle.cs | 315 ++++++++++++++ .../Game/Agent/AgentLifecycleEventListener.cs | 38 ++ Dalamud/Game/Agent/AgentVirtualTable.cs | 393 ++++++++++++++++++ Dalamud/Plugin/Services/IAgentLifecycle.cs | 88 ++++ 11 files changed, 1098 insertions(+) create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentClassJobChangeArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentGameEventArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentLevelChangeArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgTypes/AgentReceiveEventArgs.cs create mode 100644 Dalamud/Game/Agent/AgentArgsType.cs create mode 100644 Dalamud/Game/Agent/AgentEvent.cs create mode 100644 Dalamud/Game/Agent/AgentLifecycle.cs create mode 100644 Dalamud/Game/Agent/AgentLifecycleEventListener.cs create mode 100644 Dalamud/Game/Agent/AgentVirtualTable.cs create mode 100644 Dalamud/Plugin/Services/IAgentLifecycle.cs 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); +} From f635673ce91e33412a3ed1bcc477567906787940 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 9 Jan 2026 12:51:08 -0800 Subject: [PATCH 2/4] Use AgentInterfacePtr --- Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs index b4a904dde..ef0f9021a 100644 --- a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs +++ b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Agent.AgentArgTypes; +using Dalamud.Game.NativeWrapper; + +namespace Dalamud.Game.Agent.AgentArgTypes; /// /// Base class for AgentLifecycle AgentArgTypes. @@ -15,7 +17,7 @@ public unsafe class AgentArgs /// /// Gets the pointer to the Agents AgentInterface*. /// - public nint Agent { get; internal set; } + public AgentInterfacePtr Agent { get; internal set; } /// /// Gets the agent id. @@ -33,5 +35,5 @@ public unsafe class AgentArgs /// AgentInterface. /// Typed pointer to contained Agents AgentInterface. public T* GetAgentPointer() where T : unmanaged - => (T*)this.Agent; + => (T*)this.Agent.Address; } From 6c8b2b4a6d4b10791e388a568f5bd11fec530c2f Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 9 Jan 2026 12:52:33 -0800 Subject: [PATCH 3/4] Remove casts --- Dalamud/Game/Agent/AgentVirtualTable.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Agent/AgentVirtualTable.cs b/Dalamud/Game/Agent/AgentVirtualTable.cs index 3c23616e8..e00f9e433 100644 --- a/Dalamud/Game/Agent/AgentVirtualTable.cs +++ b/Dalamud/Game/Agent/AgentVirtualTable.cs @@ -125,7 +125,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.receiveEventArgs.Agent = (nint)thisPtr; + this.receiveEventArgs.Agent = thisPtr; this.receiveEventArgs.AgentId = this.agentId; this.receiveEventArgs.ReturnValue = (nint)returnValue; this.receiveEventArgs.AtkValues = (nint)values; @@ -166,7 +166,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.filteredReceiveEventArgs.Agent = (nint)thisPtr; + this.filteredReceiveEventArgs.Agent = thisPtr; this.filteredReceiveEventArgs.AgentId = this.agentId; this.filteredReceiveEventArgs.ReturnValue = (nint)returnValue; this.filteredReceiveEventArgs.AtkValues = (nint)values; @@ -205,7 +205,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.showArgs.Agent = (nint)thisPtr; + this.showArgs.Agent = thisPtr; this.showArgs.AgentId = this.agentId; this.lifecycleService.InvokeListenersSafely(AgentEvent.PreShow, this.showArgs); @@ -233,7 +233,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.hideArgs.Agent = (nint)thisPtr; + this.hideArgs.Agent = thisPtr; this.hideArgs.AgentId = this.agentId; this.lifecycleService.InvokeListenersSafely(AgentEvent.PreHide, this.hideArgs); @@ -261,7 +261,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.updateArgs.Agent = (nint)thisPtr; + this.updateArgs.Agent = thisPtr; this.updateArgs.AgentId = this.agentId; this.lifecycleService.InvokeListenersSafely(AgentEvent.PreUpdate, this.updateArgs); @@ -289,7 +289,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.gameEventArgs.Agent = (nint)thisPtr; + this.gameEventArgs.Agent = thisPtr; this.gameEventArgs.AgentId = this.agentId; this.gameEventArgs.GameEvent = (int)gameEvent; @@ -320,7 +320,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.levelChangeArgs.Agent = (nint)thisPtr; + this.levelChangeArgs.Agent = thisPtr; this.levelChangeArgs.AgentId = this.agentId; this.levelChangeArgs.ClassJobId = classJobId; this.levelChangeArgs.Level = level; @@ -353,7 +353,7 @@ internal unsafe class AgentVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.classJobChangeArgs.Agent = (nint)thisPtr; + this.classJobChangeArgs.Agent = thisPtr; this.classJobChangeArgs.AgentId = this.agentId; this.classJobChangeArgs.ClassJobId = classJobId; From 0c2ce097ed2243b304f06207d9cc9692be43de0c Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sat, 10 Jan 2026 08:30:15 -0800 Subject: [PATCH 4/4] Use generated AgentId --- Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs | 2 +- Dalamud/Game/Agent/AgentLifecycle.cs | 20 +++++++++---------- .../Game/Agent/AgentLifecycleEventListener.cs | 4 ++-- Dalamud/Game/Agent/AgentVirtualTable.cs | 10 +++++----- Dalamud/Plugin/Services/IAgentLifecycle.cs | 8 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs index ef0f9021a..1de80694f 100644 --- a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs +++ b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs @@ -22,7 +22,7 @@ public unsafe class AgentArgs /// /// Gets the agent id. /// - public uint AgentId { get; internal set; } + public AgentId AgentId { get; internal set; } /// /// Gets the type of these args. diff --git a/Dalamud/Game/Agent/AgentLifecycle.cs b/Dalamud/Game/Agent/AgentLifecycle.cs index 1306a92c1..75ed47d86 100644 --- a/Dalamud/Game/Agent/AgentLifecycle.cs +++ b/Dalamud/Game/Agent/AgentLifecycle.cs @@ -57,7 +57,7 @@ internal unsafe class AgentLifecycle : IInternalDisposableService /// Gets a list of all AgentLifecycle Event Listeners. ///
/// Mapping is: EventType -> ListenerList - internal Dictionary>> EventListeners { get; } = []; + internal Dictionary>> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -128,7 +128,7 @@ internal unsafe class AgentLifecycle : IInternalDisposableService 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)) + if (agentListeners.TryGetValue((AgentId)uint.MaxValue, out var globalListeners)) { foreach (var listener in globalListeners) { @@ -154,7 +154,7 @@ internal unsafe class AgentLifecycle : IInternalDisposableService } catch (Exception e) { - Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific agent {(AgentId)args.AgentId}."); + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific agent {args.AgentId}."); } } } @@ -208,7 +208,7 @@ internal unsafe class AgentLifecycle : IInternalDisposableService } // AgentVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions - AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, index, this)); + AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, (AgentId)index, this)); } catch (Exception e) { @@ -243,7 +243,7 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi } /// - public void RegisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate handler) + public void RegisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate handler) { foreach (var agentId in agentIds) { @@ -252,7 +252,7 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi } /// - public void RegisterListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate handler) + public void RegisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate handler) { var listener = new AgentLifecycleEventListener(eventType, agentId, handler); this.eventListeners.Add(listener); @@ -262,11 +262,11 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi /// public void RegisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate handler) { - this.RegisterListener(eventType, uint.MaxValue, handler); + this.RegisterListener(eventType, (AgentId)uint.MaxValue, handler); } /// - public void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate? handler = null) + public void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, IAgentLifecycle.AgentEventDelegate? handler = null) { foreach (var agentId in agentIds) { @@ -275,7 +275,7 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi } /// - public void UnregisterListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate? handler = null) + public void UnregisterListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate? handler = null) { this.eventListeners.RemoveAll(entry => { @@ -291,7 +291,7 @@ internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLi /// public void UnregisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate? handler = null) { - this.UnregisterListener(eventType, uint.MaxValue, handler); + this.UnregisterListener(eventType, (AgentId)uint.MaxValue, handler); } /// diff --git a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs index 3521d2c13..91f8aa3d3 100644 --- a/Dalamud/Game/Agent/AgentLifecycleEventListener.cs +++ b/Dalamud/Game/Agent/AgentLifecycleEventListener.cs @@ -13,7 +13,7 @@ public class AgentLifecycleEventListener /// Event type to listen for. /// Agent id to listen for. /// Delegate to invoke. - internal AgentLifecycleEventListener(AgentEvent eventType, uint agentId, IAgentLifecycle.AgentEventDelegate functionDelegate) + internal AgentLifecycleEventListener(AgentEvent eventType, AgentId agentId, IAgentLifecycle.AgentEventDelegate functionDelegate) { this.EventType = eventType; this.AgentId = agentId; @@ -24,7 +24,7 @@ public class AgentLifecycleEventListener /// 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; } + public AgentId AgentId { get; init; } /// /// Gets the event type this listener is looking for. diff --git a/Dalamud/Game/Agent/AgentVirtualTable.cs b/Dalamud/Game/Agent/AgentVirtualTable.cs index e00f9e433..e7f9a2f6e 100644 --- a/Dalamud/Game/Agent/AgentVirtualTable.cs +++ b/Dalamud/Game/Agent/AgentVirtualTable.cs @@ -27,7 +27,7 @@ internal unsafe class AgentVirtualTable : IDisposable private readonly AgentLifecycle lifecycleService; - private readonly uint agentId; + private readonly AgentId 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(); @@ -58,9 +58,9 @@ internal unsafe class AgentVirtualTable : IDisposable /// 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) + internal AgentVirtualTable(AgentInterface* agent, AgentId agentId, AgentLifecycle lifecycleService) { - Log.Debug($"Initializing AgentVirtualTable for {(AgentId)agentId}, Address: {(nint)agent:X}"); + Log.Debug($"Initializing AgentVirtualTable for {agentId}, Address: {(nint)agent:X}"); this.agentInterface = agent; this.agentId = agentId; @@ -384,10 +384,10 @@ internal unsafe class AgentVirtualTable : IDisposable 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) + if (caller is "OnAgentUpdate" || this.agentId is AgentId.PadMouseMode) return; - Log.Debug($"[{caller}]: {(AgentId)this.agentId}"); + Log.Debug($"[{caller}]: {this.agentId}"); } } } diff --git a/Dalamud/Plugin/Services/IAgentLifecycle.cs b/Dalamud/Plugin/Services/IAgentLifecycle.cs index a1ed26125..62178408d 100644 --- a/Dalamud/Plugin/Services/IAgentLifecycle.cs +++ b/Dalamud/Plugin/Services/IAgentLifecycle.cs @@ -24,7 +24,7 @@ public interface IAgentLifecycle : IDalamudService /// 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); + void RegisterListener(AgentEvent eventType, IEnumerable agentIds, AgentEventDelegate handler); /// /// Register a listener that will trigger on the specified event only for the specified agent. @@ -32,7 +32,7 @@ public interface IAgentLifecycle : IDalamudService /// 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); + void RegisterListener(AgentEvent eventType, AgentId agentId, AgentEventDelegate handler); /// /// Register a listener that will trigger on the specified event for any agent. @@ -50,7 +50,7 @@ public interface IAgentLifecycle : IDalamudService /// Event type to deregister. /// Agent IDs to deregister. /// Optional specific handler to remove. - void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, [Optional] AgentEventDelegate handler); + void UnregisterListener(AgentEvent eventType, IEnumerable agentIds, [Optional] AgentEventDelegate handler); /// /// Unregister all listeners for the specified event type and agent ID. @@ -61,7 +61,7 @@ public interface IAgentLifecycle : IDalamudService /// Event type to deregister. /// Agent id to deregister. /// Optional specific handler to remove. - void UnregisterListener(AgentEvent eventType, uint agentId, [Optional] AgentEventDelegate handler); + void UnregisterListener(AgentEvent eventType, AgentId agentId, [Optional] AgentEventDelegate handler); /// /// Unregister an event type handler.
This will only remove a handler that is added via .