Merge branch 'master' into ImRaii-Widgets2

This commit is contained in:
Infi 2026-01-13 20:34:29 +01:00 committed by GitHub
commit 0fd8e35c6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 1690 additions and 232 deletions

View file

@ -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}

View file

@ -6,7 +6,7 @@
<PropertyGroup Label="Feature">
<Description>XIV Launcher addon framework</Description>
<DalamudVersion>14.0.0.3</DalamudVersion>
<DalamudVersion>14.0.1.0</DalamudVersion>
<AssemblyVersion>$(DalamudVersion)</AssemblyVersion>
<Version>$(DalamudVersion)</Version>
<FileVersion>$(DalamudVersion)</FileVersion>
@ -88,6 +88,15 @@
<PackageReference Include="TerraFX.Interop.Windows" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\generators\Dalamud.EnumGenerator\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
<ItemGroup>
<None Remove="EnumCloneMap.txt"/>
<AdditionalFiles Include="EnumCloneMap.txt" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Interface\ImGuiBackend\Renderers\imgui-frag.hlsl.bytes">
<LogicalName>imgui-frag.hlsl.bytes</LogicalName>

3
Dalamud/EnumCloneMap.txt Normal file
View file

@ -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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,315 @@
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Game.Agent.AgentArgTypes;
using Dalamud.Hooking;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.Interop;
namespace Dalamud.Game.Agent;
/// <summary>
/// This class provides events for in-game agent lifecycles.
/// </summary>
[ServiceManager.EarlyLoadedService]
internal unsafe class AgentLifecycle : IInternalDisposableService
{
/// <summary>
/// Gets a list of all allocated agent virtual tables.
/// </summary>
public static readonly List<AgentVirtualTable> AllocatedTables = [];
private static readonly ModuleLog Log = new("AgentLifecycle");
[ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get();
private Hook<AgentModule.Delegates.Ctor>? onInitializeAgentsHook;
private bool isInvokingListeners;
[ServiceManager.ServiceConstructor]
private AgentLifecycle()
{
var agentModuleInstance = AgentModule.Instance();
// Hook is only used to determine appropriate timing for replacing Agent Virtual Tables
// If the agent module is already initialized, then we can replace the tables safely.
if (agentModuleInstance is null)
{
this.onInitializeAgentsHook = Hook<AgentModule.Delegates.Ctor>.FromAddress((nint)AgentModule.MemberFunctionPointers.Ctor, this.OnAgentModuleInitialize);
this.onInitializeAgentsHook.Enable();
}
else
{
// For safety because this might be injected async, we will make sure we are on the main thread first.
this.framework.RunOnFrameworkThread(() => this.ReplaceVirtualTables(agentModuleInstance));
}
}
/// <summary>
/// Gets a list of all AgentLifecycle Event Listeners.
/// </summary> <br/>
/// Mapping is: EventType -> ListenerList
internal Dictionary<AgentEvent, Dictionary<AgentId, HashSet<AgentLifecycleEventListener>>> EventListeners { get; } = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
this.onInitializeAgentsHook?.Dispose();
this.onInitializeAgentsHook = null;
AllocatedTables.ForEach(entry => entry.Dispose());
AllocatedTables.Clear();
}
/// <summary>
/// Register a listener for the target event and agent.
/// </summary>
/// <param name="listener">The listener to register.</param>
internal void RegisterListener(AgentLifecycleEventListener listener)
{
this.framework.RunOnTick(() =>
{
if (!this.EventListeners.ContainsKey(listener.EventType))
{
if (!this.EventListeners.TryAdd(listener.EventType, []))
return;
}
// Note: uint.MaxValue is a valid agent id, as that will trigger on any agent for this event type
if (!this.EventListeners[listener.EventType].ContainsKey(listener.AgentId))
{
if (!this.EventListeners[listener.EventType].TryAdd(listener.AgentId, []))
return;
}
this.EventListeners[listener.EventType][listener.AgentId].Add(listener);
},
delayTicks: this.isInvokingListeners ? 1 : 0);
}
/// <summary>
/// Unregisters the listener from events.
/// </summary>
/// <param name="listener">The listener to unregister.</param>
internal void UnregisterListener(AgentLifecycleEventListener listener)
{
this.framework.RunOnTick(() =>
{
if (this.EventListeners.TryGetValue(listener.EventType, out var agentListeners))
{
if (agentListeners.TryGetValue(listener.AgentId, out var agentListener))
{
agentListener.Remove(listener);
}
}
},
delayTicks: this.isInvokingListeners ? 1 : 0);
}
/// <summary>
/// Invoke listeners for the specified event type.
/// </summary>
/// <param name="eventType">Event Type.</param>
/// <param name="args">AgentARgs.</param>
/// <param name="blame">What to blame on errors.</param>
internal void InvokeListenersSafely(AgentEvent eventType, AgentArgs args, [CallerMemberName] string blame = "")
{
this.isInvokingListeners = true;
// Early return if we don't have any listeners of this type
if (!this.EventListeners.TryGetValue(eventType, out var agentListeners)) return;
// Handle listeners for this event type that don't care which agent is triggering it
if (agentListeners.TryGetValue((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;
}
/// <summary>
/// Resolves a virtual table address to the original virtual table address.
/// </summary>
/// <param name="tableAddress">The modified address to resolve.</param>
/// <returns>The original address.</returns>
internal AgentInterface.AgentInterfaceVirtualTable* GetOriginalVirtualTable(AgentInterface.AgentInterfaceVirtualTable* tableAddress)
{
var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress);
if (matchedTable == null) return null;
return matchedTable.OriginalVirtualTable;
}
private void OnAgentModuleInitialize(AgentModule* thisPtr, UIModule* uiModule)
{
this.onInitializeAgentsHook!.Original(thisPtr, uiModule);
try
{
this.ReplaceVirtualTables(thisPtr);
// We don't need this hook anymore, it did its job!
this.onInitializeAgentsHook!.Dispose();
this.onInitializeAgentsHook = null;
}
catch (Exception e)
{
Log.Error(e, "Exception in AgentLifecycle during AgentModule Ctor.");
}
}
private void ReplaceVirtualTables(AgentModule* agentModule)
{
foreach (uint index in Enumerable.Range(0, agentModule->Agents.Length))
{
try
{
var agentPointer = agentModule->Agents.GetPointer((int)index);
if (agentPointer is null)
{
Log.Warning("Null Agent Found?");
continue;
}
// AgentVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions
AllocatedTables.Add(new AgentVirtualTable(agentPointer->Value, (AgentId)index, this));
}
catch (Exception e)
{
Log.Error(e, "Exception in AgentLifecycle during ReplaceVirtualTables.");
}
}
}
}
/// <summary>
/// Plugin-scoped version of a AgentLifecycle service.
/// </summary>
[PluginInterface]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IAgentLifecycle>]
#pragma warning restore SA1015
internal class AgentLifecyclePluginScoped : IInternalDisposableService, IAgentLifecycle
{
[ServiceManager.ServiceDependency]
private readonly AgentLifecycle agentLifecycleService = Service<AgentLifecycle>.Get();
private readonly List<AgentLifecycleEventListener> eventListeners = [];
/// <inheritdoc/>
void IInternalDisposableService.DisposeService()
{
foreach (var listener in this.eventListeners)
{
this.agentLifecycleService.UnregisterListener(listener);
}
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, IEnumerable<AgentId> agentIds, IAgentLifecycle.AgentEventDelegate handler)
{
foreach (var agentId in agentIds)
{
this.RegisterListener(eventType, agentId, handler);
}
}
/// <inheritdoc/>
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);
}
/// <inheritdoc/>
public void RegisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate handler)
{
this.RegisterListener(eventType, (AgentId)uint.MaxValue, handler);
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, IEnumerable<AgentId> agentIds, IAgentLifecycle.AgentEventDelegate? handler = null)
{
foreach (var agentId in agentIds)
{
this.UnregisterListener(eventType, agentId, handler);
}
}
/// <inheritdoc/>
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;
});
}
/// <inheritdoc/>
public void UnregisterListener(AgentEvent eventType, IAgentLifecycle.AgentEventDelegate? handler = null)
{
this.UnregisterListener(eventType, (AgentId)uint.MaxValue, handler);
}
/// <inheritdoc/>
public void UnregisterListener(params IAgentLifecycle.AgentEventDelegate[] handlers)
{
foreach (var handler in handlers)
{
this.eventListeners.RemoveAll(entry =>
{
if (entry.FunctionDelegate != handler) return false;
this.agentLifecycleService.UnregisterListener(entry);
return true;
});
}
}
/// <inheritdoc/>
public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress)
=> (nint)this.agentLifecycleService.GetOriginalVirtualTable((AgentInterface.AgentInterfaceVirtualTable*)virtualTableAddress);
}

View file

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

View file

@ -0,0 +1,393 @@
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using Dalamud.Game.Agent.AgentArgTypes;
using Dalamud.Logging.Internal;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace Dalamud.Game.Agent;
/// <summary>
/// Represents a class that holds references to an agents original and modified virtual table entries.
/// </summary>
internal unsafe class AgentVirtualTable : IDisposable
{
// This need to be at minimum the largest virtual table size of all agents
// Copying extra entries is not problematic, and is considered safe.
private const int VirtualTableEntryCount = 60;
private const bool EnableLogging = true;
private static readonly ModuleLog Log = new("AgentVT");
private readonly AgentLifecycle lifecycleService;
private readonly 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;
/// <summary>
/// Initializes a new instance of the <see cref="AgentVirtualTable"/> class.
/// </summary>
/// <param name="agent">AgentInterface* for the agent to replace the table of.</param>
/// <param name="agentId">Agent ID.</param>
/// <param name="lifecycleService">Reference to AgentLifecycle service to callback and invoke listeners.</param>
internal AgentVirtualTable(AgentInterface* agent, 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<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.receiveEventFunction);
this.ModifiedVirtualTable->ReceiveEvent2 = (delegate* unmanaged<AgentInterface*, AtkValue*, AtkValue*, uint, ulong, AtkValue*>)Marshal.GetFunctionPointerForDelegate(this.filteredReceiveEventFunction);
this.ModifiedVirtualTable->Show = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.showFunction);
this.ModifiedVirtualTable->Hide = (delegate* unmanaged<AgentInterface*, void>)Marshal.GetFunctionPointerForDelegate(this.hideFunction);
this.ModifiedVirtualTable->Update = (delegate* unmanaged<AgentInterface*, uint, void>)Marshal.GetFunctionPointerForDelegate(this.updateFunction);
this.ModifiedVirtualTable->OnGameEvent = (delegate* unmanaged<AgentInterface*, AgentInterface.GameEvent, void>)Marshal.GetFunctionPointerForDelegate(this.gameEventFunction);
this.ModifiedVirtualTable->OnLevelChange = (delegate* unmanaged<AgentInterface*, byte, ushort, void>)Marshal.GetFunctionPointerForDelegate(this.levelChangeFunction);
this.ModifiedVirtualTable->OnClassJobChange = (delegate* unmanaged<AgentInterface*, byte, void>)Marshal.GetFunctionPointerForDelegate(this.classJobChangeFunction);
}
/// <summary>
/// Gets the original virtual table address for this agent.
/// </summary>
internal AgentInterface.AgentInterfaceVirtualTable* OriginalVirtualTable { get; private set; }
/// <summary>
/// Gets the modified virtual address for this agent.
/// </summary>
internal AgentInterface.AgentInterfaceVirtualTable* ModifiedVirtualTable { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
// Ensure restoration is done atomically.
Interlocked.Exchange(ref *(nint*)&this.agentInterface->VirtualTable, (nint)this.OriginalVirtualTable);
IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount);
}
private AtkValue* OnAgentReceiveEvent(AgentInterface* thisPtr, AtkValue* returnValue, AtkValue* values, uint valueCount, ulong eventKind)
{
AtkValue* result = null;
try
{
this.LogEvent(EnableLogging);
this.receiveEventArgs.Agent = 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}");
}
}
}

View file

@ -22,8 +22,6 @@ using PublicContentSheet = Lumina.Excel.Sheets.PublicContent;
namespace Dalamud.Game.UnlockState;
#pragma warning disable Dalamud001
/// <summary>
/// This class provides unlock state of various content in the game.
/// </summary>

View file

@ -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;
/// </summary>
internal class FontAwesomeTestWidget : IDataWindowWidget
{
private static readonly string[] First = ["(Show All)", "(Undefined)"];
private List<FontAwesomeIcon>? icons;
private List<string>? iconNames;
private string[]? iconCategories;
private int selectedIconCategory;
private string iconSearchInput = string.Empty;
private bool iconSearchChanged = true;
private bool useFixedWidth = false;
private bool useFixedWidth;
/// <inheritdoc/>
public string[]? CommandShortcuts { get; init; } = ["fa", "fatest", "fontawesome"];
@ -44,11 +48,9 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
/// <inheritdoc/>
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<TextureManager>.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();
}
}

View file

@ -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;
/// </summary>
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<byte> 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(
"<icon(1)>Test test<icon(1)>",
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<DataManager>.GetNullable()?.GetExcelSheet<Addon>() ??
@ -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<ChatGui>.Get().Print(row.Text.ToDalamudString());
ImGui.PopID();
Service<ChatGui>.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<ChatGui>.Get().Print(
Game.Text.SeStringHandling.SeString.Parse(
Service<SeStringRenderer>.Get().CompileAndCache(this.testString).Data.Span));
Service<ChatGui>.Get().Print(Service<SeStringRenderer>.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;

View file

@ -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();

View file

@ -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;
/// </summary>
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<SharedImmediateTexture> textures)
private void DrawLoadedTextures(ICollection<SharedImmediateTexture> textures)
{
var im = Service<InterfaceManager>.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<int> 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];

View file

@ -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
}
/// <inheritdoc/>
public unsafe void Draw()
public void Draw()
{
var colors = Service<DataManager>.GetNullable()?.GetExcelSheet<UIColor>()
?? throw new InvalidOperationException("UIColor sheet not loaded.");
@ -45,7 +46,9 @@ internal class UiColorWidget : IDataWindowWidget
"<edgecolor(0xEEEEFF)><color(0x0000FF)>BB<color(stackcolor)><edgecolor(stackcolor)>.<br>" +
"· Click on a color to copy the color code.<br>" +
"· 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<byte> buf = stackalloc byte[256];
var ptr = 0;
ptr += Encoding.UTF8.GetBytes("<colortype(", buf[ptr..]);
@ -185,7 +188,6 @@ internal class UiColorWidget : IDataWindowWidget
EdgeColor = BinaryPrimitives.ReverseEndianness(sheetColor) | 0xFF000000u,
WrapWidth = float.PositiveInfinity,
});
ImGui.EndTooltip();
}
private bool DrawColorColumn(uint sheetColor)

View file

@ -12,6 +12,7 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Textures.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Memory;
using Lumina.Data.Files;
@ -163,17 +164,19 @@ internal class UldWidget : IDataWindowWidget
ImGuiColors.DalamudRed,
$"Error: {nameof(UldFile.AssetData)} is not populated.");
}
else if (ImGui.BeginTable("##uldTextureEntries"u8, 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders))
else
{
ImGui.TableSetupColumn("Id"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("000000"u8).X);
ImGui.TableSetupColumn("Path"u8, ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Actions"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Preview___"u8).X);
ImGui.TableHeadersRow();
using var table = ImRaii.Table("##uldTextureEntries"u8, 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.Borders);
if (table.Success)
{
ImGui.TableSetupColumn("Id"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("000000"u8).X);
ImGui.TableSetupColumn("Path"u8, ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Actions"u8, ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Preview___"u8).X);
ImGui.TableHeadersRow();
foreach (var textureEntry in uld.AssetData)
this.DrawTextureEntry(textureEntry, textureManager);
ImGui.EndTable();
foreach (var textureEntry in uld.AssetData)
this.DrawTextureEntry(textureEntry, textureManager);
}
}
}
@ -287,7 +290,7 @@ internal class UldWidget : IDataWindowWidget
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
using var tooltip = ImRaii.Tooltip();
var texturePath = GetStringNullTerminated(textureEntry.Path);
ImGui.Text($"Base path at {texturePath}:");
@ -308,8 +311,6 @@ internal class UldWidget : IDataWindowWidget
ImGui.Text(e.ToString());
}
}
ImGui.EndTooltip();
}
}
@ -318,15 +319,14 @@ internal class UldWidget : IDataWindowWidget
ImGui.SliderInt("FrameData"u8, ref this.selectedFrameData, 0, timeline.FrameData.Length - 1);
var frameData = timeline.FrameData[this.selectedFrameData];
ImGui.Text($"FrameInfo: {frameData.StartFrame} -> {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();
}
}
}

View file

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

View file

@ -10,7 +10,6 @@ namespace Dalamud.Plugin.Services;
/// <summary>
/// Interface for determining unlock state of various content in the game.
/// </summary>
[Experimental("Dalamud001")]
public interface IUnlockState : IDalamudService
{
/// <summary>

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<RootNamespace>Dalamud.EnumGenerator.Sample</RootNamespace>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
</ItemGroup>
<ItemGroup>
<None Remove="EnumCloneMap.txt"/>
<AdditionalFiles Include="EnumCloneMap.txt" />
</ItemGroup>
</Project>

View file

@ -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

View file

@ -0,0 +1,9 @@
namespace Dalamud.EnumGenerator.Sample.SourceEnums
{
public enum SampleSourceEnum : long
{
First = 1,
Second = 2,
Third = 10000000000L
}
}

View file

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>Dalamud.EnumGenerator.Tests</RootNamespace>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing.XUnit" Version="1.1.1"/>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2"/>
<PackageReference Include="xunit" Version="2.4.2"/>
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Dalamud.EnumGenerator\Dalamud.EnumGenerator.csproj"/>
</ItemGroup>
</Project>

View file

@ -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<AdditionalText>(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);
}
}

View file

@ -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; }
}

View file

@ -0,0 +1,3 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

View file

@ -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

View file

@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<RootNamespace>Dalamud.EnumGenerator</RootNamespace>
<PackageId>Dalamud.EnumGenerator</PackageId>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
<AdditionalFiles Include="AnalyzerReleases.Unshipped.md" />
</ItemGroup>
</Project>

View file

@ -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<IFieldSymbol>()
.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 = "// <auto-generated/>" + 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();
}
}

View file

@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Dalamud.EnumGenerator.Tests")]

View file

@ -0,0 +1,5 @@
<Project>
<ItemGroup Label="Code Analysis">
<PackageReference Remove="Microsoft.CodeAnalysis.BannedApiAnalyzers" />
</ItemGroup>
</Project>