diff --git a/Dalamud.sln b/Dalamud.sln
index de91e7ceb..3b1c4fd91 100644
--- a/Dalamud.sln
+++ b/Dalamud.sln
@@ -75,6 +75,17 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel.Generator", "l
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lumina.Excel", "lib\Lumina.Excel\src\Lumina.Excel\Lumina.Excel.csproj", "{88FB719B-EB41-73C5-8D25-C03E0C69904F}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source Generators", "Source Generators", "{50BEC23B-FFFD-427B-A95D-27E1D1958FFF}"
+ ProjectSection(SolutionItems) = preProject
+ generators\Directory.Build.props = generators\Directory.Build.props
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj", "{27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Sample", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Sample\Dalamud.EnumGenerator.Sample.csproj", "{8CDAEB2D-5022-450A-A97F-181C6270185F}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.EnumGenerator.Tests", "generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator.Tests\Dalamud.EnumGenerator.Tests.csproj", "{F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -173,6 +184,18 @@ Global
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88FB719B-EB41-73C5-8D25-C03E0C69904F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Debug|Any CPU.Build.0 = Debug|x64
+ {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.ActiveCfg = Release|x64
+ {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8}.Release|Any CPU.Build.0 = Release|x64
+ {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {8CDAEB2D-5022-450A-A97F-181C6270185F}.Debug|Any CPU.Build.0 = Debug|x64
+ {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.ActiveCfg = Release|x64
+ {8CDAEB2D-5022-450A-A97F-181C6270185F}.Release|Any CPU.Build.0 = Release|x64
+ {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.ActiveCfg = Debug|x64
+ {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Debug|Any CPU.Build.0 = Debug|x64
+ {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.ActiveCfg = Release|x64
+ {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6}.Release|Any CPU.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -197,6 +220,9 @@ Global
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {E15BDA6D-E881-4482-94BA-BE5527E917FF}
{5A44DF0C-C9DA-940F-4D6B-4A11D13AEA3D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{88FB719B-EB41-73C5-8D25-C03E0C69904F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {27AA9F87-D2AA-41D9-A559-0F1EBA38C5F8} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
+ {8CDAEB2D-5022-450A-A97F-181C6270185F} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
+ {F5D92D2D-D36F-4471-B657-8B9AA6C98AD6} = {50BEC23B-FFFD-427B-A95D-27E1D1958FFF}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {79B65AC9-C940-410E-AB61-7EA7E12C7599}
diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj
index f5e75af63..bb8f5af7c 100644
--- a/Dalamud/Dalamud.csproj
+++ b/Dalamud/Dalamud.csproj
@@ -88,6 +88,15 @@
+
+
+
+
+
+
+
+
+
imgui-frag.hlsl.bytes
diff --git a/Dalamud/EnumCloneMap.txt b/Dalamud/EnumCloneMap.txt
new file mode 100644
index 000000000..bbc3c1eda
--- /dev/null
+++ b/Dalamud/EnumCloneMap.txt
@@ -0,0 +1,3 @@
+# Format: Target.Full.TypeName = Source.Full.EnumTypeName
+# Example: Generate a local enum MyGeneratedEnum in namespace Sample.Gen mapped to SourceEnums.SampleSourceEnum
+Dalamud.Game.Agent.AgentId = FFXIVClientStructs.FFXIV.Client.UI.Agent.AgentId
diff --git a/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs
new file mode 100644
index 000000000..1de80694f
--- /dev/null
+++ b/Dalamud/Game/Agent/AgentArgTypes/AgentArgs.cs
@@ -0,0 +1,39 @@
+using Dalamud.Game.NativeWrapper;
+
+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 AgentInterfacePtr Agent { get; internal set; }
+
+ ///
+ /// Gets the agent id.
+ ///
+ public AgentId 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.Address;
+}
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..75ed47d86
--- /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((AgentId)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 {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, (AgentId)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, AgentId 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, (AgentId)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, AgentId 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, (AgentId)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..91f8aa3d3
--- /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, AgentId 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 AgentId 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..e7f9a2f6e
--- /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 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();
+ 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, AgentId agentId, AgentLifecycle lifecycleService)
+ {
+ Log.Debug($"Initializing AgentVirtualTable for {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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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" || this.agentId is AgentId.PadMouseMode)
+ return;
+
+ Log.Debug($"[{caller}]: {this.agentId}");
+ }
+ }
+}
diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs
index cc70a524c..939548803 100644
--- a/Dalamud/Game/UnlockState/UnlockState.cs
+++ b/Dalamud/Game/UnlockState/UnlockState.cs
@@ -22,8 +22,6 @@ using PublicContentSheet = Lumina.Excel.Sheets.PublicContent;
namespace Dalamud.Game.UnlockState;
-#pragma warning disable Dalamud001
-
///
/// This class provides unlock state of various content in the game.
///
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs
index 8beb437ac..c19fed848 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs
@@ -5,9 +5,11 @@ using System.Threading.Tasks;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Components;
+using Dalamud.Interface.ImGuiSeStringRenderer;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Internal;
+using Dalamud.Interface.Utility.Raii;
using Lumina.Text.ReadOnly;
@@ -18,13 +20,15 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
///
internal class FontAwesomeTestWidget : IDataWindowWidget
{
+ private static readonly string[] First = ["(Show All)", "(Undefined)"];
+
private List? icons;
private List? iconNames;
private string[]? iconCategories;
private int selectedIconCategory;
private string iconSearchInput = string.Empty;
private bool iconSearchChanged = true;
- private bool useFixedWidth = false;
+ private bool useFixedWidth;
///
public string[]? CommandShortcuts { get; init; } = ["fa", "fatest", "fontawesome"];
@@ -44,11 +48,9 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
///
public void Draw()
{
- ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
+ using var pushedStyle = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
- this.iconCategories ??= new[] { "(Show All)", "(Undefined)" }
- .Concat(FontAwesomeHelpers.GetCategories().Skip(1))
- .ToArray();
+ this.iconCategories ??= First.Concat(FontAwesomeHelpers.GetCategories().Skip(1)).ToArray();
if (this.iconSearchChanged)
{
@@ -101,7 +103,8 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
ImGuiHelpers.ScaledRelativeSameLine(50f);
ImGui.Text($"{this.iconNames?[i]}");
ImGuiHelpers.ScaledRelativeSameLine(280f);
- ImGui.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont);
+
+ using var pushedFont = ImRaii.PushFont(this.useFixedWidth ? InterfaceManager.IconFontFixedWidth : InterfaceManager.IconFont);
ImGui.Text(this.icons[i].ToIconString());
ImGuiHelpers.ScaledRelativeSameLine(320f);
if (this.useFixedWidth
@@ -114,13 +117,10 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
Task.FromResult(
Service.Get().CreateTextureFromSeString(
ReadOnlySeString.FromText(this.icons[i].ToIconString()),
- new() { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize(), ScreenOffset = Vector2.Zero })));
+ new SeStringDrawParams { Font = ImGui.GetFont(), FontSize = ImGui.GetFontSize(), ScreenOffset = Vector2.Zero })));
}
- ImGui.PopFont();
ImGuiHelpers.ScaledDummy(2f);
}
-
- ImGui.PopStyleVar();
}
}
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
index 8f5fe7b8a..0e4e5792d 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs
@@ -11,6 +11,7 @@ using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Internal;
+using Dalamud.Interface.Utility.Raii;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
@@ -29,7 +30,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
///
internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
{
- private static readonly string[] ThemeNames = ["Dark", "Light", "Classic FF", "Clear Blue"];
+ private static readonly string[] ThemeNames = ["Dark", "Light", "Classic FF", "Clear Blue", "Clear White", "Clear Green"];
private ImVectorWrapper testStringBuffer;
private string testString = string.Empty;
private ReadOnlySeString? logkind;
@@ -119,9 +120,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGui.SameLine();
var t4 = this.style.ThemeIndex ?? AtkStage.Instance()->AtkUIColorHolder->ActiveColorThemeType;
- ImGui.PushItemWidth(ImGui.CalcTextSize("WWWWWWWWWWWWWW"u8).X);
- if (ImGui.Combo("##theme", ref t4, ThemeNames))
- this.style.ThemeIndex = t4;
+ using (ImRaii.ItemWidth(ImGui.CalcTextSize("WWWWWWWWWWWWWW"u8).X))
+ {
+ if (ImGui.Combo("##theme", ref t4, ThemeNames))
+ this.style.ThemeIndex = t4;
+ }
ImGui.SameLine();
t = this.style.LinkUnderlineThickness > 0f;
@@ -192,22 +195,19 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
dl.PushClipRect(clipMin, clipMax);
ImGuiHelpers.CompileSeStringWrapped(
"Test test",
- new SeStringDrawParams
- { Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = dl });
+ new SeStringDrawParams { Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = dl });
dl.PopClipRect();
}
if (ImGui.CollapsingHeader("Addon Table"u8))
{
- if (ImGui.BeginTable("Addon Sheet"u8, 3))
+ using var table = ImRaii.Table("Addon Sheet"u8, 3);
+ if (table.Success)
{
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn("Row ID"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("0000000"u8).X);
ImGui.TableSetupColumn("Text"u8, ImGuiTableColumnFlags.WidthStretch);
- ImGui.TableSetupColumn(
- "Misc"u8,
- ImGuiTableColumnFlags.WidthFixed,
- ImGui.CalcTextSize("AAAAAAAAAAAAAAAAA"u8).X);
+ ImGui.TableSetupColumn("Misc"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("AAAAAAAAAAAAAAAAA"u8).X);
ImGui.TableHeadersRow();
var addon = Service.GetNullable()?.GetExcelSheet() ??
@@ -222,7 +222,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
var row = addon.GetRowAt(i);
ImGui.TableNextRow();
- ImGui.PushID(i);
+ using var pushedId = ImRaii.PushId(i);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
@@ -234,14 +234,11 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
ImGui.TableNextColumn();
if (ImGui.Button("Print to Chat"u8))
- Service.Get().Print(row.Text.ToDalamudString());
-
- ImGui.PopID();
+ Service.Get().Print(row.Text);
}
}
clipper.Destroy();
- ImGui.EndTable();
}
}
@@ -258,9 +255,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
if (ImGui.Button("Print to Chat Log"u8))
{
- Service.Get().Print(
- Game.Text.SeStringHandling.SeString.Parse(
- Service.Get().CompileAndCache(this.testString).Data.Span));
+ Service.Get().Print(Service.Get().CompileAndCache(this.testString));
}
ImGui.SameLine();
@@ -315,6 +310,7 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget
var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0);
if (len + 4 >= this.testStringBuffer.Capacity)
this.testStringBuffer.EnsureCapacityExponential(len + 4);
+
if (len < this.testStringBuffer.Capacity)
{
this.testStringBuffer.LengthUnsafe = len;
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs
index d9cf0fea2..7f9606c4f 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs
@@ -266,8 +266,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget
ImGui.Text($"{this.downloadState.Downloaded:##,###}/{this.downloadState.Total:##,###} ({this.downloadState.Percentage:0.00}%)");
- using var disabled =
- ImRaii.Disabled(this.downloadTask?.IsCompleted is false || this.localPath[0] == 0);
+ using var disabled = ImRaii.Disabled(this.downloadTask?.IsCompleted is false || this.localPath[0] == 0);
ImGui.AlignTextToFramePadding();
ImGui.Text("Download"u8);
ImGui.SameLine();
@@ -388,27 +387,19 @@ internal class TaskSchedulerWidget : IDataWindowWidget
if (task.Task == null)
subTime = task.FinishTime;
- switch (task.Status)
+ using var pushedColor = task.Status switch
{
- case TaskStatus.Created:
- case TaskStatus.WaitingForActivation:
- case TaskStatus.WaitingToRun:
- ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudGrey);
- break;
- case TaskStatus.Running:
- case TaskStatus.WaitingForChildrenToComplete:
- ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedBlue);
- break;
- case TaskStatus.RanToCompletion:
- ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGreen);
- break;
- case TaskStatus.Canceled:
- case TaskStatus.Faulted:
- ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.DalamudRed);
- break;
- default:
- throw new ArgumentOutOfRangeException();
- }
+ TaskStatus.Created or TaskStatus.WaitingForActivation or TaskStatus.WaitingToRun
+ => ImRaii.PushColor(ImGuiCol.Header, ImGuiColors.DalamudGrey),
+ TaskStatus.Running or TaskStatus.WaitingForChildrenToComplete
+ => ImRaii.PushColor(ImGuiCol.Header, ImGuiColors.ParsedBlue),
+ TaskStatus.RanToCompletion
+ => ImRaii.PushColor(ImGuiCol.Header, ImGuiColors.ParsedGreen),
+ TaskStatus.Canceled or TaskStatus.Faulted
+ => ImRaii.PushColor(ImGuiCol.Header, ImGuiColors.DalamudRed),
+
+ _ => throw new ArgumentOutOfRangeException(),
+ };
if (ImGui.CollapsingHeader($"#{task.Id} - {task.Status} {(subTime - task.StartTime).TotalMilliseconds}ms###task{i}"))
{
@@ -418,8 +409,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget
{
try
{
- var cancelFunc =
- typeof(Task).GetMethod("InternalCancel", BindingFlags.NonPublic | BindingFlags.Instance);
+ var cancelFunc = typeof(Task).GetMethod("InternalCancel", BindingFlags.NonPublic | BindingFlags.Instance);
cancelFunc?.Invoke(task, null);
}
catch (Exception ex)
@@ -430,7 +420,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget
ImGuiHelpers.ScaledDummy(10);
- ImGui.Text(task.StackTrace?.ToString());
+ ImGui.Text(task.StackTrace?.ToString() ?? "Null StackTrace");
if (task.Exception != null)
{
@@ -443,8 +433,6 @@ internal class TaskSchedulerWidget : IDataWindowWidget
{
task.IsBeingViewed = false;
}
-
- ImGui.PopStyleColor(1);
}
this.fileDialogManager.Draw();
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
index e6a092b6e..866640996 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs
@@ -14,6 +14,7 @@ using Dalamud.Interface.Textures.Internal.SharedImmediateTextures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Internal;
+using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using Dalamud.Utility;
@@ -29,6 +30,10 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets;
///
internal class TexWidget : IDataWindowWidget
{
+ private const ImGuiTableFlags TableFlags = ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate | ImGuiTableFlags.SortMulti |
+ ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable | ImGuiTableFlags.NoBordersInBodyUntilResize |
+ ImGuiTableFlags.NoSavedSettings;
+
// TODO: move tracking implementation to PluginStats where applicable,
// and show stats over there instead of TexWidget.
private static readonly Dictionary<
@@ -49,7 +54,7 @@ internal class TexWidget : IDataWindowWidget
private string allLoadedTexturesTableName = "##table";
private string iconId = "18";
private bool hiRes = true;
- private bool hq = false;
+ private bool hq;
private string inputTexPath = string.Empty;
private string inputFilePath = string.Empty;
private Assembly[]? inputManifestResourceAssemblyCandidates;
@@ -140,46 +145,40 @@ internal class TexWidget : IDataWindowWidget
lock (this.textureManager.BlameTracker)
{
+ using var pushedId = ImRaii.PushId("blames"u8);
var allBlames = this.textureManager.BlameTracker;
- ImGui.PushID("blames"u8);
var sizeSum = allBlames.Sum(static x => Math.Max(0, x.RawSpecs.EstimatedBytes));
- if (ImGui.CollapsingHeader(
- $"All Loaded Textures: {allBlames.Count:n0} ({Util.FormatBytes(sizeSum)})###header"))
+ if (ImGui.CollapsingHeader($"All Loaded Textures: {allBlames.Count:n0} ({Util.FormatBytes(sizeSum)})###header"))
this.DrawBlame(allBlames);
- ImGui.PopID();
}
- ImGui.PushID("loadedGameTextures"u8);
- if (ImGui.CollapsingHeader(
- $"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:n0}###header"))
- this.DrawLoadedTextures(this.textureManager.Shared.ForDebugGamePathTextures);
- ImGui.PopID();
+ using (ImRaii.PushId("loadedGameTextures"u8))
+ {
+ if (ImGui.CollapsingHeader($"Loaded Game Textures: {this.textureManager.Shared.ForDebugGamePathTextures.Count:n0}###header"))
+ this.DrawLoadedTextures(this.textureManager.Shared.ForDebugGamePathTextures);
+ }
- ImGui.PushID("loadedFileTextures"u8);
- if (ImGui.CollapsingHeader(
- $"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:n0}###header"))
- this.DrawLoadedTextures(this.textureManager.Shared.ForDebugFileSystemTextures);
- ImGui.PopID();
+ using (ImRaii.PushId("loadedFileTextures"u8))
+ {
+ if (ImGui.CollapsingHeader($"Loaded File Textures: {this.textureManager.Shared.ForDebugFileSystemTextures.Count:n0}###header"))
+ this.DrawLoadedTextures(this.textureManager.Shared.ForDebugFileSystemTextures);
+ }
- ImGui.PushID("loadedManifestResourceTextures"u8);
- if (ImGui.CollapsingHeader(
- $"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:n0}###header"))
- this.DrawLoadedTextures(this.textureManager.Shared.ForDebugManifestResourceTextures);
- ImGui.PopID();
+ using (ImRaii.PushId("loadedManifestResourceTextures"u8))
+ {
+ if (ImGui.CollapsingHeader($"Loaded Manifest Resource Textures: {this.textureManager.Shared.ForDebugManifestResourceTextures.Count:n0}###header"))
+ this.DrawLoadedTextures(this.textureManager.Shared.ForDebugManifestResourceTextures);
+ }
lock (this.textureManager.Shared.ForDebugInvalidatedTextures)
{
- ImGui.PushID("invalidatedTextures"u8);
- if (ImGui.CollapsingHeader(
- $"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:n0}###header"))
- {
+ using var pushedId = ImRaii.PushId("invalidatedTextures"u8);
+ if (ImGui.CollapsingHeader($"Invalidated: {this.textureManager.Shared.ForDebugInvalidatedTextures.Count:n0}###header"))
this.DrawLoadedTextures(this.textureManager.Shared.ForDebugInvalidatedTextures);
- }
-
- ImGui.PopID();
}
- ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing()));
+ var textHeightSpacing = new Vector2(ImGui.GetTextLineHeightWithSpacing());
+ ImGui.Dummy(textHeightSpacing);
if (!this.textureManager.HasClipboardImage())
{
@@ -192,59 +191,53 @@ internal class TexWidget : IDataWindowWidget
if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon)))
{
- ImGui.PushID(nameof(this.DrawGetFromGameIcon));
+ using var pushedId = ImRaii.PushId(nameof(this.DrawGetFromGameIcon));
this.DrawGetFromGameIcon();
- ImGui.PopID();
}
if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGame)))
{
- ImGui.PushID(nameof(this.DrawGetFromGame));
+ using var pushedId = ImRaii.PushId(nameof(this.DrawGetFromGame));
this.DrawGetFromGame();
- ImGui.PopID();
}
if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromFile)))
{
- ImGui.PushID(nameof(this.DrawGetFromFile));
+ using var pushedId = ImRaii.PushId(nameof(this.DrawGetFromFile));
this.DrawGetFromFile();
- ImGui.PopID();
}
if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromManifestResource)))
{
- ImGui.PushID(nameof(this.DrawGetFromManifestResource));
+ using var pushedId = ImRaii.PushId(nameof(this.DrawGetFromManifestResource));
this.DrawGetFromManifestResource();
- ImGui.PopID();
}
if (ImGui.CollapsingHeader(nameof(ITextureProvider.CreateFromImGuiViewportAsync)))
{
- ImGui.PushID(nameof(this.DrawCreateFromImGuiViewportAsync));
+ using var pushedId = ImRaii.PushId(nameof(this.DrawCreateFromImGuiViewportAsync));
this.DrawCreateFromImGuiViewportAsync();
- ImGui.PopID();
}
if (ImGui.CollapsingHeader("UV"u8))
{
- ImGui.PushID(nameof(this.DrawUvInput));
+ using var pushedId = ImRaii.PushId(nameof(this.DrawUvInput));
this.DrawUvInput();
- ImGui.PopID();
}
if (ImGui.CollapsingHeader($"CropCopy##{nameof(this.DrawExistingTextureModificationArgs)}"))
{
- ImGui.PushID(nameof(this.DrawExistingTextureModificationArgs));
+ using var pushedId = ImRaii.PushId(nameof(this.DrawExistingTextureModificationArgs));
this.DrawExistingTextureModificationArgs();
- ImGui.PopID();
}
- ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing()));
+ ImGui.Dummy(textHeightSpacing);
Action? runLater = null;
foreach (var t in this.addedTextures)
{
- ImGui.PushID(t.Id);
+ using var pushedId = ImRaii.PushId(t.Id);
+
if (ImGui.CollapsingHeader($"Tex #{t.Id} {t}###header", ImGuiTreeNodeFlags.DefaultOpen))
{
if (ImGui.Button("X"u8))
@@ -336,8 +329,6 @@ internal class TexWidget : IDataWindowWidget
ImGui.Text(e.ToString());
}
}
-
- ImGui.PopID();
}
runLater?.Invoke();
@@ -357,18 +348,16 @@ internal class TexWidget : IDataWindowWidget
if (ImGui.Button("Reset Columns"u8))
this.allLoadedTexturesTableName = "##table" + Environment.TickCount64;
- if (!ImGui.BeginTable(
- this.allLoadedTexturesTableName,
- (int)DrawBlameTableColumnUserId.ColumnCount,
- ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate | ImGuiTableFlags.SortMulti |
- ImGuiTableFlags.Reorderable | ImGuiTableFlags.Resizable | ImGuiTableFlags.NoBordersInBodyUntilResize |
- ImGuiTableFlags.NoSavedSettings))
+ using var table = ImRaii.Table(this.allLoadedTexturesTableName, (int)DrawBlameTableColumnUserId.ColumnCount, TableFlags);
+ if (!table.Success)
return;
const int numIcons = 1;
float iconWidths;
using (im.IconFontHandle?.Push())
+ {
iconWidths = ImGui.CalcTextSize(FontAwesomeIcon.Save.ToIconString()).X;
+ }
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableSetupColumn(
@@ -463,7 +452,8 @@ internal class TexWidget : IDataWindowWidget
{
var wrap = allBlames[i];
ImGui.TableNextRow();
- ImGui.PushID(i);
+
+ using var pushedId = ImRaii.PushId(i);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
@@ -480,9 +470,8 @@ internal class TexWidget : IDataWindowWidget
if (ImGui.IsItemHovered())
{
- ImGui.BeginTooltip();
+ using var tooltip = ImRaii.Tooltip();
ImGui.Image(wrap.Handle, wrap.Size);
- ImGui.EndTooltip();
}
ImGui.TableNextColumn();
@@ -504,21 +493,19 @@ internal class TexWidget : IDataWindowWidget
ImGui.TableNextColumn();
lock (wrap.OwnerPlugins)
this.TextColumnCopiable(string.Join(", ", wrap.OwnerPlugins.Select(static x => x.Name)), false, true);
-
- ImGui.PopID();
}
}
clipper.Destroy();
- ImGui.EndTable();
ImGuiHelpers.ScaledDummy(10);
}
- private unsafe void DrawLoadedTextures(ICollection textures)
+ private void DrawLoadedTextures(ICollection textures)
{
var im = Service.Get();
- if (!ImGui.BeginTable("##table"u8, 6))
+ using var table = ImRaii.Table("##table"u8, 6);
+ if (!table.Success)
return;
const int numIcons = 4;
@@ -576,7 +563,7 @@ internal class TexWidget : IDataWindowWidget
}
var remain = texture.SelfReferenceExpiresInForDebug;
- ImGui.PushID(row);
+ using var pushedId = ImRaii.PushId(row);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
@@ -603,28 +590,26 @@ internal class TexWidget : IDataWindowWidget
if (ImGui.IsItemHovered() && texture.GetWrapOrDefault(null) is { } immediate)
{
- ImGui.BeginTooltip();
+ using var tooltip = ImRaii.Tooltip();
ImGui.Image(immediate.Handle, immediate.Size);
- ImGui.EndTooltip();
}
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.Sync))
this.textureManager.InvalidatePaths([texture.SourcePathForDebug]);
+
if (ImGui.IsItemHovered())
ImGui.SetTooltip($"Call {nameof(ITextureSubstitutionProvider.InvalidatePaths)}.");
ImGui.SameLine();
- if (remain <= 0)
- ImGui.BeginDisabled();
- if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash))
- texture.ReleaseSelfReference(true);
- if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
- ImGui.SetTooltip("Release self-reference immediately."u8);
- if (remain <= 0)
- ImGui.EndDisabled();
+ using (ImRaii.Disabled(remain <= 0))
+ {
+ if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash))
+ texture.ReleaseSelfReference(true);
- ImGui.PopID();
+ if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
+ ImGui.SetTooltip("Release self-reference immediately."u8);
+ }
}
if (!valid)
@@ -633,7 +618,6 @@ internal class TexWidget : IDataWindowWidget
}
clipper.Destroy();
- ImGui.EndTable();
ImGuiHelpers.ScaledDummy(10);
}
@@ -752,10 +736,7 @@ internal class TexWidget : IDataWindowWidget
{
ImGui.SameLine();
if (ImGui.Button("Load File (Async)"u8))
- {
- this.addedTextures.Add(
- new(Api10: this.textureManager.Shared.GetFromManifestResource(assembly, name).RentAsync()));
- }
+ this.addedTextures.Add(new(Api10: this.textureManager.Shared.GetFromManifestResource(assembly, name).RentAsync()));
ImGui.SameLine();
if (ImGui.Button("Load File (Immediate)"u8))
@@ -768,21 +749,20 @@ internal class TexWidget : IDataWindowWidget
private void DrawCreateFromImGuiViewportAsync()
{
var viewports = ImGui.GetPlatformIO().Viewports;
- if (ImGui.BeginCombo(
- nameof(this.viewportTextureArgs.ViewportId),
- $"{this.viewportIndexInt}. {viewports[this.viewportIndexInt].ID:X08}"))
+ using (var combo = ImRaii.Combo(nameof(this.viewportTextureArgs.ViewportId), $"{this.viewportIndexInt}. {viewports[this.viewportIndexInt].ID:X08}"))
{
- for (var i = 0; i < viewports.Size; i++)
+ if (combo.Success)
{
- var sel = this.viewportIndexInt == i;
- if (ImGui.Selectable($"#{i}: {viewports[i].ID:X08}", ref sel))
+ for (var i = 0; i < viewports.Size; i++)
{
- this.viewportIndexInt = i;
- ImGui.SetItemDefaultFocus();
+ var sel = this.viewportIndexInt == i;
+ if (ImGui.Selectable($"#{i}: {viewports[i].ID:X08}", ref sel))
+ {
+ this.viewportIndexInt = i;
+ ImGui.SetItemDefaultFocus();
+ }
}
}
-
- ImGui.EndCombo();
}
var b = this.viewportTextureArgs.KeepTransparency;
@@ -844,17 +824,12 @@ internal class TexWidget : IDataWindowWidget
}
this.supportedRenderTargetFormatNames ??= this.supportedRenderTargetFormats.Select(Enum.GetName).ToArray();
- ImGui.Combo(
- nameof(this.textureModificationArgs.DxgiFormat),
- ref this.renderTargetChoiceInt,
- this.supportedRenderTargetFormatNames);
+ ImGui.Combo(nameof(this.textureModificationArgs.DxgiFormat), ref this.renderTargetChoiceInt, this.supportedRenderTargetFormatNames);
Span wh = stackalloc int[2];
wh[0] = this.textureModificationArgs.NewWidth;
wh[1] = this.textureModificationArgs.NewHeight;
- if (ImGui.InputInt(
- $"{nameof(this.textureModificationArgs.NewWidth)}/{nameof(this.textureModificationArgs.NewHeight)}",
- wh))
+ if (ImGui.InputInt($"{nameof(this.textureModificationArgs.NewWidth)}/{nameof(this.textureModificationArgs.NewHeight)}", wh))
{
this.textureModificationArgs.NewWidth = wh[0];
this.textureModificationArgs.NewHeight = wh[1];
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs
index 029dc0b75..dc1ab4e30 100644
--- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs
+++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs
@@ -3,6 +3,7 @@ using System.Numerics;
using System.Text;
using Dalamud.Bindings.ImGui;
+using Dalamud.Interface.Utility.Raii;
using Dalamud.Data;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.ImGuiNotification.Internal;
@@ -33,7 +34,7 @@ internal class UiColorWidget : IDataWindowWidget
}
///
- public unsafe void Draw()
+ public void Draw()
{
var colors = Service.GetNullable()?.GetExcelSheet()
?? throw new InvalidOperationException("UIColor sheet not loaded.");
@@ -45,7 +46,9 @@ internal class UiColorWidget : IDataWindowWidget
"BB.
" +
"· Click on a color to copy the color code.
" +
"· Hover on a color to preview the text with edge, when the next color has been used together.");
- if (!ImGui.BeginTable("UIColor"u8, 7))
+
+ using var table = ImRaii.Table("UIColor"u8, 7);
+ if (!table.Success)
return;
ImGui.TableSetupScrollFreeze(0, 1);
@@ -94,61 +97,61 @@ internal class UiColorWidget : IDataWindowWidget
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
- ImGui.PushID($"row{id}_dark");
- if (this.DrawColorColumn(row.Dark) &&
- adjacentRow.HasValue)
- DrawEdgePreview(id, row.Dark, adjacentRow.Value.Dark);
- ImGui.PopID();
+ using (ImRaii.PushId($"row{id}_dark"))
+ {
+ if (this.DrawColorColumn(row.Dark) && adjacentRow.HasValue)
+ DrawEdgePreview(id, row.Dark, adjacentRow.Value.Dark);
+ }
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
- ImGui.PushID($"row{id}_light");
- if (this.DrawColorColumn(row.Light) &&
- adjacentRow.HasValue)
- DrawEdgePreview(id, row.Light, adjacentRow.Value.Light);
- ImGui.PopID();
+ using (ImRaii.PushId($"row{id}_light"))
+ {
+ if (this.DrawColorColumn(row.Light) && adjacentRow.HasValue)
+ DrawEdgePreview(id, row.Light, adjacentRow.Value.Light);
+ }
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
- ImGui.PushID($"row{id}_classic");
- if (this.DrawColorColumn(row.ClassicFF) &&
- adjacentRow.HasValue)
- DrawEdgePreview(id, row.ClassicFF, adjacentRow.Value.ClassicFF);
- ImGui.PopID();
+ using (ImRaii.PushId($"row{id}_classic"))
+ {
+ if (this.DrawColorColumn(row.ClassicFF) && adjacentRow.HasValue)
+ DrawEdgePreview(id, row.ClassicFF, adjacentRow.Value.ClassicFF);
+ }
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
- ImGui.PushID($"row{id}_blue");
- if (this.DrawColorColumn(row.ClearBlue) &&
- adjacentRow.HasValue)
- DrawEdgePreview(id, row.ClearBlue, adjacentRow.Value.ClearBlue);
- ImGui.PopID();
+ using (ImRaii.PushId($"row{id}_blue"))
+ {
+ if (this.DrawColorColumn(row.ClearBlue) && adjacentRow.HasValue)
+ DrawEdgePreview(id, row.ClearBlue, adjacentRow.Value.ClearBlue);
+ }
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
- ImGui.PushID($"row{id}_white");
- if (this.DrawColorColumn(row.ClearWhite) &&
- adjacentRow.HasValue)
- DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite);
- ImGui.PopID();
+ using (ImRaii.PushId($"row{id}_white"))
+ {
+ if (this.DrawColorColumn(row.ClearWhite) && adjacentRow.HasValue)
+ DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite);
+ }
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
- ImGui.PushID($"row{id}_green");
- if (this.DrawColorColumn(row.ClearGreen) &&
- adjacentRow.HasValue)
- DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen);
- ImGui.PopID();
+ using (ImRaii.PushId($"row{id}_green"))
+ {
+ if (this.DrawColorColumn(row.ClearGreen) && adjacentRow.HasValue)
+ DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen);
+ }
}
}
clipper.Destroy();
- ImGui.EndTable();
}
private static void DrawEdgePreview(uint id, uint sheetColor, uint sheetColor2)
{
- ImGui.BeginTooltip();
+ using var tooltip = ImRaii.Tooltip();
+
Span buf = stackalloc byte[256];
var ptr = 0;
ptr += Encoding.UTF8.GetBytes(" {frameData.EndFrame}");
- ImGui.Indent();
+
+ using var indent = ImRaii.PushIndent();
foreach (var frameDataKeyGroup in frameData.KeyGroups)
{
ImGui.Text($"{frameDataKeyGroup.Usage:G} {frameDataKeyGroup.Type:G}");
foreach (var keyframe in frameDataKeyGroup.Frames)
this.DrawTimelineKeyGroupFrame(keyframe);
}
-
- ImGui.Unindent();
}
private void DrawTimelineKeyGroupFrame(IKeyframe frame)
@@ -334,8 +334,7 @@ internal class UldWidget : IDataWindowWidget
switch (frame)
{
case BaseKeyframeData baseKeyframeData:
- ImGui.Text(
- $"Time: {baseKeyframeData.Time} | Interpolation: {baseKeyframeData.Interpolation} | Acceleration: {baseKeyframeData.Acceleration} | Deceleration: {baseKeyframeData.Deceleration}");
+ ImGui.Text($"Time: {baseKeyframeData.Time} | Interpolation: {baseKeyframeData.Interpolation} | Acceleration: {baseKeyframeData.Acceleration} | Deceleration: {baseKeyframeData.Deceleration}");
break;
case Float1Keyframe float1Keyframe:
this.DrawTimelineKeyGroupFrame(float1Keyframe.Keyframe);
@@ -350,8 +349,7 @@ internal class UldWidget : IDataWindowWidget
case Float3Keyframe float3Keyframe:
this.DrawTimelineKeyGroupFrame(float3Keyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | Value1: {float3Keyframe.Value[0]} | Value2: {float3Keyframe.Value[1]} | Value3: {float3Keyframe.Value[2]}");
+ ImGui.Text($" | Value1: {float3Keyframe.Value[0]} | Value2: {float3Keyframe.Value[1]} | Value3: {float3Keyframe.Value[2]}");
break;
case SByte1Keyframe sbyte1Keyframe:
this.DrawTimelineKeyGroupFrame(sbyte1Keyframe.Keyframe);
@@ -366,8 +364,7 @@ internal class UldWidget : IDataWindowWidget
case SByte3Keyframe sbyte3Keyframe:
this.DrawTimelineKeyGroupFrame(sbyte3Keyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | Value1: {sbyte3Keyframe.Value[0]} | Value2: {sbyte3Keyframe.Value[1]} | Value3: {sbyte3Keyframe.Value[2]}");
+ ImGui.Text($" | Value1: {sbyte3Keyframe.Value[0]} | Value2: {sbyte3Keyframe.Value[1]} | Value3: {sbyte3Keyframe.Value[2]}");
break;
case Byte1Keyframe byte1Keyframe:
this.DrawTimelineKeyGroupFrame(byte1Keyframe.Keyframe);
@@ -382,8 +379,7 @@ internal class UldWidget : IDataWindowWidget
case Byte3Keyframe byte3Keyframe:
this.DrawTimelineKeyGroupFrame(byte3Keyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | Value1: {byte3Keyframe.Value[0]} | Value2: {byte3Keyframe.Value[1]} | Value3: {byte3Keyframe.Value[2]}");
+ ImGui.Text($" | Value1: {byte3Keyframe.Value[0]} | Value2: {byte3Keyframe.Value[1]} | Value3: {byte3Keyframe.Value[2]}");
break;
case Short1Keyframe short1Keyframe:
this.DrawTimelineKeyGroupFrame(short1Keyframe.Keyframe);
@@ -398,8 +394,7 @@ internal class UldWidget : IDataWindowWidget
case Short3Keyframe short3Keyframe:
this.DrawTimelineKeyGroupFrame(short3Keyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | Value1: {short3Keyframe.Value[0]} | Value2: {short3Keyframe.Value[1]} | Value3: {short3Keyframe.Value[2]}");
+ ImGui.Text($" | Value1: {short3Keyframe.Value[0]} | Value2: {short3Keyframe.Value[1]} | Value3: {short3Keyframe.Value[2]}");
break;
case UShort1Keyframe ushort1Keyframe:
this.DrawTimelineKeyGroupFrame(ushort1Keyframe.Keyframe);
@@ -414,8 +409,7 @@ internal class UldWidget : IDataWindowWidget
case UShort3Keyframe ushort3Keyframe:
this.DrawTimelineKeyGroupFrame(ushort3Keyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | Value1: {ushort3Keyframe.Value[0]} | Value2: {ushort3Keyframe.Value[1]} | Value3: {ushort3Keyframe.Value[2]}");
+ ImGui.Text($" | Value1: {ushort3Keyframe.Value[0]} | Value2: {ushort3Keyframe.Value[1]} | Value3: {ushort3Keyframe.Value[2]}");
break;
case Int1Keyframe int1Keyframe:
this.DrawTimelineKeyGroupFrame(int1Keyframe.Keyframe);
@@ -430,8 +424,7 @@ internal class UldWidget : IDataWindowWidget
case Int3Keyframe int3Keyframe:
this.DrawTimelineKeyGroupFrame(int3Keyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | Value1: {int3Keyframe.Value[0]} | Value2: {int3Keyframe.Value[1]} | Value3: {int3Keyframe.Value[2]}");
+ ImGui.Text($" | Value1: {int3Keyframe.Value[0]} | Value2: {int3Keyframe.Value[1]} | Value3: {int3Keyframe.Value[2]}");
break;
case UInt1Keyframe uint1Keyframe:
this.DrawTimelineKeyGroupFrame(uint1Keyframe.Keyframe);
@@ -446,8 +439,7 @@ internal class UldWidget : IDataWindowWidget
case UInt3Keyframe uint3Keyframe:
this.DrawTimelineKeyGroupFrame(uint3Keyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | Value1: {uint3Keyframe.Value[0]} | Value2: {uint3Keyframe.Value[1]} | Value3: {uint3Keyframe.Value[2]}");
+ ImGui.Text($" | Value1: {uint3Keyframe.Value[0]} | Value2: {uint3Keyframe.Value[1]} | Value3: {uint3Keyframe.Value[2]}");
break;
case Bool1Keyframe bool1Keyframe:
this.DrawTimelineKeyGroupFrame(bool1Keyframe.Keyframe);
@@ -462,28 +454,22 @@ internal class UldWidget : IDataWindowWidget
case Bool3Keyframe bool3Keyframe:
this.DrawTimelineKeyGroupFrame(bool3Keyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | Value1: {bool3Keyframe.Value[0]} | Value2: {bool3Keyframe.Value[1]} | Value3: {bool3Keyframe.Value[2]}");
+ ImGui.Text($" | Value1: {bool3Keyframe.Value[0]} | Value2: {bool3Keyframe.Value[1]} | Value3: {bool3Keyframe.Value[2]}");
break;
case ColorKeyframe colorKeyframe:
this.DrawTimelineKeyGroupFrame(colorKeyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | Add: {colorKeyframe.AddRed} {colorKeyframe.AddGreen} {colorKeyframe.AddBlue} | Multiply: {colorKeyframe.MultiplyRed} {colorKeyframe.MultiplyGreen} {colorKeyframe.MultiplyBlue}");
+ ImGui.Text($" | Add: {colorKeyframe.AddRed} {colorKeyframe.AddGreen} {colorKeyframe.AddBlue} | Multiply: {colorKeyframe.MultiplyRed} {colorKeyframe.MultiplyGreen} {colorKeyframe.MultiplyBlue}");
break;
case LabelKeyframe labelKeyframe:
this.DrawTimelineKeyGroupFrame(labelKeyframe.Keyframe);
ImGui.SameLine(0, 0);
- ImGui.Text(
- $" | LabelCommand: {labelKeyframe.LabelCommand} | JumpId: {labelKeyframe.JumpId} | LabelId: {labelKeyframe.LabelId}");
+ ImGui.Text($" | LabelCommand: {labelKeyframe.LabelCommand} | JumpId: {labelKeyframe.JumpId} | LabelId: {labelKeyframe.LabelId}");
break;
}
}
- private void DrawParts(
- UldRoot.PartsData partsData,
- UldRoot.TextureEntry[] textureEntries,
- TextureManager textureManager)
+ private void DrawParts(UldRoot.PartsData partsData, UldRoot.TextureEntry[] textureEntries, TextureManager textureManager)
{
for (var index = 0; index < partsData.Parts.Length; index++)
{
@@ -549,10 +535,9 @@ internal class UldWidget : IDataWindowWidget
if (ImGui.IsItemHovered())
{
- ImGui.BeginTooltip();
+ using var tooltip = ImRaii.Tooltip();
ImGui.Text("Click to copy:"u8);
ImGui.Text(texturePath);
- ImGui.EndTooltip();
}
}
}
diff --git a/Dalamud/Plugin/Services/IAgentLifecycle.cs b/Dalamud/Plugin/Services/IAgentLifecycle.cs
new file mode 100644
index 000000000..62178408d
--- /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, AgentId 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, AgentId 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);
+}
diff --git a/Dalamud/Plugin/Services/IUnlockState.cs b/Dalamud/Plugin/Services/IUnlockState.cs
index 0409843c4..6703ece2e 100644
--- a/Dalamud/Plugin/Services/IUnlockState.cs
+++ b/Dalamud/Plugin/Services/IUnlockState.cs
@@ -10,7 +10,6 @@ namespace Dalamud.Plugin.Services;
///
/// Interface for determining unlock state of various content in the game.
///
-[Experimental("Dalamud001")]
public interface IUnlockState : IDalamudService
{
///
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj
new file mode 100644
index 000000000..225ea5f94
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/Dalamud.EnumGenerator.Sample.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net9.0
+ enable
+ Dalamud.EnumGenerator.Sample
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt
new file mode 100644
index 000000000..a7db08bf3
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/EnumCloneMap.txt
@@ -0,0 +1,4 @@
+# Format: Target.Full.TypeName = Source.Full.EnumTypeName
+# Example: Generate a local enum MyGeneratedEnum in namespace Sample.Gen mapped to SourceEnums.SampleSourceEnum
+Dalamud.EnumGenerator.Sample.Gen.MyGeneratedEnum = Dalamud.EnumGenerator.Sample.SourceEnums.SampleSourceEnum
+
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs
new file mode 100644
index 000000000..407b4c151
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Sample/SourceEnums.cs
@@ -0,0 +1,9 @@
+namespace Dalamud.EnumGenerator.Sample.SourceEnums
+{
+ public enum SampleSourceEnum : long
+ {
+ First = 1,
+ Second = 2,
+ Third = 10000000000L
+ }
+}
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj
new file mode 100644
index 000000000..50de4a7c8
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Dalamud.EnumGenerator.Tests.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net9.0
+ enable
+
+ false
+
+ Dalamud.EnumGenerator.Tests
+
+ false
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs
new file mode 100644
index 000000000..f14279c53
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/EnumCloneMapTests.cs
@@ -0,0 +1,47 @@
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Xunit;
+
+namespace Dalamud.EnumGenerator.Tests;
+
+public class EnumCloneMapTests
+{
+ [Fact]
+ public void ParseMappings_SimpleLines_ParsesCorrectly()
+ {
+ var text = @"# Comment line
+My.Namespace.Target = Other.Namespace.Source
+
+Another.Target = Some.Source";
+
+ var results = Dalamud.EnumGenerator.EnumCloneGenerator.ParseMappings(text);
+
+ Assert.Equal(2, results.Length);
+ Assert.Equal("My.Namespace.Target", results[0].TargetFullName);
+ Assert.Equal("Other.Namespace.Source", results[0].SourceFullName);
+ Assert.Equal("Another.Target", results[1].TargetFullName);
+ }
+
+ [Fact]
+ public void Generator_ProducesFile_WhenSourceResolved()
+ {
+ // We'll create a compilation that contains a source enum type and add an AdditionalText mapping
+ var sourceEnum = @"namespace Foo.Bar { public enum SourceEnum { A = 1, B = 2 } }";
+
+ var mapText = "GeneratedNs.TargetEnum = Foo.Bar.SourceEnum";
+
+ var generator = new EnumCloneGenerator();
+ var driver = CSharpGeneratorDriver.Create(generator)
+ .AddAdditionalTexts(ImmutableArray.Create(new Utils.TestAdditionalFile("EnumCloneMap.txt", mapText)));
+
+ var compilation = CSharpCompilation.Create("TestGen", [CSharpSyntaxTree.ParseText(sourceEnum)],
+ [MetadataReference.CreateFromFile(typeof(object).Assembly.Location)]);
+
+ driver.RunGeneratorsAndUpdateCompilation(compilation, out var newCompilation, out var diagnostics);
+
+ var generated = newCompilation.SyntaxTrees.Select(t => t.FilePath).Where(p => p.EndsWith("TargetEnum.CloneEnum.g.cs")).ToArray();
+ Assert.Single(generated);
+ }
+}
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs
new file mode 100644
index 000000000..e5c0df848
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator.Tests/Utils/TestAdditionalFile.cs
@@ -0,0 +1,21 @@
+using System.Threading;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Dalamud.EnumGenerator.Tests.Utils;
+
+public class TestAdditionalFile : AdditionalText
+{
+ private readonly SourceText text;
+
+ public TestAdditionalFile(string path, string text)
+ {
+ Path = path;
+ this.text = SourceText.From(text);
+ }
+
+ public override SourceText GetText(CancellationToken cancellationToken = new()) => this.text;
+
+ public override string Path { get; }
+}
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md
new file mode 100644
index 000000000..60b59dd99
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Shipped.md
@@ -0,0 +1,3 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md
new file mode 100644
index 000000000..e90084796
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,9 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+ENUMGEN001 | EnumGenerator | Warning | SourceGeneratorWithAttributes
+ENUMGEN002 | EnumGenerator | Warning | SourceGeneratorWithAttributes
\ No newline at end of file
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj
new file mode 100644
index 000000000..106b036a8
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Dalamud.EnumGenerator.csproj
@@ -0,0 +1,33 @@
+
+
+
+ netstandard2.0
+ false
+ enable
+ latest
+
+ true
+ true
+
+ Dalamud.EnumGenerator
+ Dalamud.EnumGenerator
+
+ false
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs
new file mode 100644
index 000000000..10cf0723c
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/EnumCloneGenerator.cs
@@ -0,0 +1,193 @@
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Globalization;
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Dalamud.EnumGenerator;
+
+[Generator]
+public class EnumCloneGenerator : IIncrementalGenerator
+{
+ private const string NewLine = "\r\n";
+
+ private const string MappingFileName = "EnumCloneMap.txt";
+
+ private static readonly DiagnosticDescriptor MissingSourceDescriptor = new(
+ id: "ENUMGEN001",
+ title: "Source enum not found",
+ messageFormat: "Source enum '{0}' could not be resolved by the compilation",
+ category: "EnumGenerator",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor DuplicateTargetDescriptor = new(
+ id: "ENUMGEN002",
+ title: "Duplicate target mapping",
+ messageFormat: "Target enum '{0}' is mapped multiple times; generation skipped for this target",
+ category: "EnumGenerator",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Read mappings from additional files named EnumCloneMap.txt
+ var mappingEntries = context.AdditionalTextsProvider
+ .Where(at => Path.GetFileName(at.Path).Equals(MappingFileName, StringComparison.OrdinalIgnoreCase))
+ .SelectMany((at, _) => ParseMappings(at.GetText()?.ToString() ?? string.Empty));
+
+ // Combine with compilation so we can resolve types
+ var compilationAndMaps = context.CompilationProvider.Combine(mappingEntries.Collect());
+
+ context.RegisterSourceOutput(compilationAndMaps, (spc, pair) =>
+ {
+ var compilation = pair.Left;
+ var maps = pair.Right;
+
+ // Detect duplicate targets first and report diagnostics
+ var duplicateTargets = maps.GroupBy(m => m.TargetFullName, StringComparer.OrdinalIgnoreCase)
+ .Where(g => g.Count() > 1)
+ .Select(g => g.Key)
+ .ToImmutableArray();
+ foreach (var dup in duplicateTargets)
+ {
+ var diag = Diagnostic.Create(DuplicateTargetDescriptor, Location.None, dup);
+ spc.ReportDiagnostic(diag);
+ }
+
+ foreach (var (targetFullName, sourceFullName) in maps)
+ {
+ if (string.IsNullOrWhiteSpace(targetFullName) || string.IsNullOrWhiteSpace(sourceFullName))
+ continue;
+
+ if (duplicateTargets.Contains(targetFullName, StringComparer.OrdinalIgnoreCase))
+ continue;
+
+ // Resolve the source enum type by metadata name (namespace.type)
+ var sourceSymbol = compilation.GetTypeByMetadataName(sourceFullName);
+ if (sourceSymbol is null)
+ {
+ // Report diagnostic for missing source type
+ var diag = Diagnostic.Create(MissingSourceDescriptor, Location.None, sourceFullName);
+ spc.ReportDiagnostic(diag);
+ continue;
+ }
+
+ if (sourceSymbol.TypeKind != TypeKind.Enum)
+ continue;
+
+ var sourceNamed = sourceSymbol; // GetTypeByMetadataName already returns INamedTypeSymbol
+
+ // Split target into namespace and type name
+ string? targetNamespace = null;
+ var targetName = targetFullName;
+ var lastDot = targetFullName.LastIndexOf('.');
+ if (lastDot >= 0)
+ {
+ targetNamespace = targetFullName.Substring(0, lastDot);
+ targetName = targetFullName.Substring(lastDot + 1);
+ }
+
+ var underlyingType = sourceNamed.EnumUnderlyingType;
+ var underlyingDisplay = underlyingType?.ToDisplayString() ?? "int";
+
+ var fields = sourceNamed.GetMembers()
+ .OfType()
+ .Where(f => f.IsStatic && f.HasConstantValue)
+ .ToArray();
+
+ var memberLines = fields.Select(f =>
+ {
+ var name = f.Name;
+ var constValue = f.ConstantValue;
+ string literal;
+
+ var st = underlyingType?.SpecialType ?? SpecialType.System_Int32;
+
+ if (constValue is null)
+ {
+ literal = "0";
+ }
+ else if (st == SpecialType.System_UInt64)
+ {
+ literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) + "UL";
+ }
+ else if (st == SpecialType.System_UInt32)
+ {
+ literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) + "U";
+ }
+ else if (st == SpecialType.System_Int64)
+ {
+ literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) + "L";
+ }
+ else
+ {
+ literal = Convert.ToString(constValue, CultureInfo.InvariantCulture) ?? throw new InvalidOperationException("Unable to convert enum constant value to string.");
+ }
+
+ return $" {name} = {literal},";
+ });
+
+ var membersText = string.Join(NewLine, memberLines);
+
+ var nsPrefix = targetNamespace is null ? string.Empty : $"namespace {targetNamespace};" + NewLine + NewLine;
+
+ var sourceFullyQualified = sourceNamed.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+
+ var code = "// " + NewLine + NewLine
+ + nsPrefix
+ + $"public enum {targetName} : {underlyingDisplay}" + NewLine
+ + "{" + NewLine
+ + membersText + NewLine
+ + "}" + NewLine + NewLine;
+
+ var extClassName = targetName + "Conversions";
+ var extMethodName = "ToDalamud" + targetName;
+
+ var extClass = $"public static class {extClassName}" + NewLine
+ + "{" + NewLine
+ + $" public static {targetName} {extMethodName}(this {sourceFullyQualified} value) => ({targetName})(({underlyingDisplay})value);" + NewLine
+ + "}" + NewLine;
+
+ code += extClass;
+
+ var hintName = $"{targetName}.CloneEnum.g.cs";
+ spc.AddSource(hintName, SourceText.From(code, Encoding.UTF8));
+ }
+ });
+ }
+
+ internal static ImmutableArray<(string TargetFullName, string SourceFullName)> ParseMappings(string text)
+ {
+ var builder = ImmutableArray.CreateBuilder<(string, string)>();
+ using var reader = new StringReader(text);
+ string? line;
+ while ((line = reader.ReadLine()) != null)
+ {
+ // Remove comments starting with #
+ var commentIndex = line.IndexOf('#');
+ var content = commentIndex >= 0 ? line.Substring(0, commentIndex) : line;
+ content = content.Trim();
+ if (string.IsNullOrEmpty(content))
+ continue;
+
+ // Expected format: Target.Full.Name = Source.Full.Name
+ var idx = content.IndexOf('=');
+ if (idx <= 0)
+ continue;
+
+ var left = content.Substring(0, idx).Trim();
+ var right = content.Substring(idx + 1).Trim();
+ if (string.IsNullOrEmpty(left) || string.IsNullOrEmpty(right))
+ continue;
+
+ builder.Add((left, right));
+ }
+
+ return builder.ToImmutable();
+ }
+}
diff --git a/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..6eac4d12e
--- /dev/null
+++ b/generators/Dalamud.EnumGenerator/Dalamud.EnumGenerator/Properties/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Dalamud.EnumGenerator.Tests")]
+
diff --git a/generators/Directory.Build.props b/generators/Directory.Build.props
new file mode 100644
index 000000000..f699838f7
--- /dev/null
+++ b/generators/Directory.Build.props
@@ -0,0 +1,5 @@
+
+
+
+
+