From b18b8b40e5b5505bc85c4895af2aa20e922e1ef8 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 25 Aug 2025 13:14:13 -0700 Subject: [PATCH 001/164] feat: Add IPC Context - Demo of the ipc context idea - Accessible via ipcPub.GetContext() --- .../Windows/Data/Widgets/PluginIpcWidget.cs | 36 +++++++---- Dalamud/Plugin/DalamudPluginInterface.cs | 36 +++++------ Dalamud/Plugin/Ipc/ICallGateProvider.cs | 3 + .../Plugin/Ipc/Internal/CallGateChannel.cs | 1 + Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs | 56 +++++++++--------- .../Plugin/Ipc/Internal/CallGatePubSubBase.cs | 59 +++++++++++++++++-- Dalamud/Plugin/Ipc/IpcContext.cs | 15 +++++ 7 files changed, 143 insertions(+), 63 deletions(-) create mode 100644 Dalamud/Plugin/Ipc/IpcContext.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs index 6c581604e..446a5e7a9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs @@ -5,6 +5,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Internal; using Dalamud.Utility; + using Serilog; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -48,12 +49,20 @@ internal class PluginIpcWidget : IDataWindowWidget this.ipcPub.RegisterAction(msg => { - Log.Information("Data action was called: {Msg}", msg); + Log.Information( + "Data action was called: {Msg}\n" + + " Context: {Context}", + msg, + this.ipcPub.GetContext()); }); this.ipcPub.RegisterFunc(msg => { - Log.Information("Data func was called: {Msg}", msg); + Log.Information( + "Data func was called: {Msg}\n" + + " Context: {Context}", + msg, + this.ipcPub.GetContext()); return Guid.NewGuid().ToString(); }); } @@ -61,14 +70,8 @@ internal class PluginIpcWidget : IDataWindowWidget if (this.ipcSub == null) { this.ipcSub = new CallGatePubSub("dataDemo1"); - this.ipcSub.Subscribe(_ => - { - Log.Information("PONG1"); - }); - this.ipcSub.Subscribe(_ => - { - Log.Information("PONG2"); - }); + this.ipcSub.Subscribe(_ => { Log.Information("PONG1"); }); + this.ipcSub.Subscribe(_ => { Log.Information("PONG2"); }); this.ipcSub.Subscribe(_ => throw new Exception("PONG3")); } @@ -78,12 +81,21 @@ internal class PluginIpcWidget : IDataWindowWidget this.ipcPubGo.RegisterAction(go => { - Log.Information("Data action was called: {Name}", go?.Name); + Log.Information( + "Data action was called: {Name}" + + "\n Context: {Context}", + go?.Name, + this.ipcPubGo.GetContext()); }); this.ipcPubGo.RegisterFunc(go => { - Log.Information("Data func was called: {Name}", go?.Name); + Log.Information( + "Data func was called: {Name}\n" + + " Context: {Context}", + go?.Name, + this.ipcPubGo.GetContext()); + return "test"; }); } diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 541071b63..db9320079 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -293,39 +293,39 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// An IPC provider. /// This is thrown when the requested types do not match the previously registered types are different. public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateProvider GetIpcProvider(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// /// Gets an IPC subscriber. @@ -334,39 +334,39 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// The name of the IPC registration. /// An IPC subscriber. public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); /// public ICallGateSubscriber GetIpcSubscriber(string name) - => new CallGatePubSub(name); + => new CallGatePubSub(name, this.plugin); #endregion diff --git a/Dalamud/Plugin/Ipc/ICallGateProvider.cs b/Dalamud/Plugin/Ipc/ICallGateProvider.cs index f4e5c76d7..387f0adf9 100644 --- a/Dalamud/Plugin/Ipc/ICallGateProvider.cs +++ b/Dalamud/Plugin/Ipc/ICallGateProvider.cs @@ -19,6 +19,9 @@ public interface ICallGateProvider /// public void UnregisterFunc(); + + /// + public IpcContext? GetContext(); } /// diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index ea94103f7..698f0917e 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -3,6 +3,7 @@ using System.Collections.Immutable; using System.Linq; using System.Reflection; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal.Converters; diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs index cc54a563b..8725ef733 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Internal.Types; + #pragma warning disable SA1402 // File may only contain a single type namespace Dalamud.Plugin.Ipc.Internal; @@ -5,9 +7,9 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -43,9 +45,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -81,9 +83,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider< /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -119,9 +121,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvi /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -157,9 +159,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateP /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -195,9 +197,9 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallG /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -233,9 +235,9 @@ internal class CallGatePubSub : CallGatePubSubBase, IC /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -271,9 +273,9 @@ internal class CallGatePubSub : CallGatePubSubBase /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } @@ -309,9 +311,9 @@ internal class CallGatePubSub : CallGatePubSub /// internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider, ICallGateSubscriber { - /// - public CallGatePubSub(string name) - : base(name) + /// + public CallGatePubSub(string name, LocalPlugin? owningPlugin = null) + : base(name, owningPlugin) { } diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs index 308457373..24cb5ca11 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs @@ -1,4 +1,11 @@ +using System.Reactive.Disposables; +using System.Threading; + +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Ipc.Exceptions; +using Dalamud.Utility; + +using Serilog; namespace Dalamud.Plugin.Ipc.Internal; @@ -7,13 +14,18 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal abstract class CallGatePubSubBase { + [ThreadStatic] + private static IpcContext? ipcExecutionContext; + /// /// Initializes a new instance of the class. /// /// The name of the IPC registration. - protected CallGatePubSubBase(string name) + /// The plugin that owns this IPC pubsub. + protected CallGatePubSubBase(string name, LocalPlugin? owningPlugin) { this.Channel = Service.Get().GetOrCreateChannel(name); + this.OwningPlugin = owningPlugin; } /// @@ -21,7 +33,7 @@ internal abstract class CallGatePubSubBase /// s. /// public bool HasAction => this.Channel.Action != null; - + /// /// Gets a value indicating whether this IPC call gate has an associated Function. Only exposed to /// s. @@ -33,12 +45,17 @@ internal abstract class CallGatePubSubBase /// s, and can be used to determine if messages should be sent through the gate. /// public int SubscriptionCount => this.Channel.Subscriptions.Count; - + /// /// Gets the underlying channel implementation. /// protected CallGateChannel Channel { get; init; } - + + /// + /// Gets the plugin that owns this pubsub instance. + /// + protected LocalPlugin? OwningPlugin { get; init; } + /// /// Removes the associated Action from this call gate, effectively disabling RPC calls. /// @@ -53,6 +70,16 @@ internal abstract class CallGatePubSubBase public void UnregisterFunc() => this.Channel.Func = null; + /// + /// Gets the current context for this IPC call. This will only be present when called from within an IPC action + /// or function handler, and will be null otherwise. + /// + /// Returns a potential IPC context. + public IpcContext? GetContext() + { + return ipcExecutionContext; + } + /// /// Registers a for use by other plugins via RPC. This Delegate must satisfy the constraints /// of an type as defined by the interface, meaning they may not return a value and must have @@ -105,7 +132,12 @@ internal abstract class CallGatePubSubBase /// /// private protected void InvokeAction(params object?[]? args) - => this.Channel.InvokeAction(args); + { + using (this.BuildContext()) + { + this.Channel.InvokeAction(args); + } + } /// /// Executes the Function registered for this IPC call gate via . This method is intended @@ -120,7 +152,12 @@ internal abstract class CallGatePubSubBase /// /// private protected TRet InvokeFunc(params object?[]? args) - => this.Channel.InvokeFunc(args); + { + using (this.BuildContext()) + { + return this.Channel.InvokeFunc(args); + } + } /// /// Send the given arguments to all subscribers (through ) of this IPC call gate. This method @@ -132,4 +169,14 @@ internal abstract class CallGatePubSubBase /// Delegate arguments. private protected void SendMessage(params object?[]? args) => this.Channel.SendMessage(args); + + private IDisposable BuildContext() + { + ipcExecutionContext = new IpcContext + { + SourcePlugin = this.OwningPlugin != null ? new ExposedPlugin(this.OwningPlugin) : null, + }; + + return Disposable.Create(() => { ipcExecutionContext = null; }); + } } diff --git a/Dalamud/Plugin/Ipc/IpcContext.cs b/Dalamud/Plugin/Ipc/IpcContext.cs new file mode 100644 index 000000000..25fde6a36 --- /dev/null +++ b/Dalamud/Plugin/Ipc/IpcContext.cs @@ -0,0 +1,15 @@ +namespace Dalamud.Plugin.Ipc; + +/// +/// The context associated for an IPC call. Reads from ThreadLocal. +/// +public class IpcContext +{ + /// + /// Gets the plugin that initiated this IPC call. + /// + public IExposedPlugin? SourcePlugin { get; init; } + + /// + public override string ToString() => $""; +} From 8cced4c1d7bece877a12cf69d0f1448a5b352c01 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 25 Aug 2025 13:31:05 -0700 Subject: [PATCH 002/164] fix: use channel threadlocal instead of a ThreadStatic --- Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs | 18 ++++++++++++++++++ .../Plugin/Ipc/Internal/CallGatePubSubBase.cs | 11 ++++------- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index 698f0917e..e177abab7 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Reflection; +using System.Threading; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Ipc.Exceptions; @@ -17,6 +18,8 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal class CallGateChannel { + private readonly ThreadLocal ipcExecutionContext = new(); + /// /// The actual storage. /// @@ -146,6 +149,21 @@ internal class CallGateChannel return (TRet)result; } + internal void SetInvocationContext(IpcContext ipcContext) + { + this.ipcExecutionContext.Value = ipcContext; + } + + internal IpcContext? GetInvocationContext() + { + return this.ipcExecutionContext.IsValueCreated ? this.ipcExecutionContext.Value : null; + } + + internal void ClearInvocationContext() + { + this.ipcExecutionContext.Value = null; + } + private void CheckAndConvertArgs(object?[]? args, MethodInfo methodInfo) { var paramTypes = methodInfo.GetParameters() diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs index 24cb5ca11..521824b7b 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs @@ -14,9 +14,6 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal abstract class CallGatePubSubBase { - [ThreadStatic] - private static IpcContext? ipcExecutionContext; - /// /// Initializes a new instance of the class. /// @@ -77,7 +74,7 @@ internal abstract class CallGatePubSubBase /// Returns a potential IPC context. public IpcContext? GetContext() { - return ipcExecutionContext; + return this.Channel.GetInvocationContext(); } /// @@ -172,11 +169,11 @@ internal abstract class CallGatePubSubBase private IDisposable BuildContext() { - ipcExecutionContext = new IpcContext + this.Channel.SetInvocationContext(new IpcContext { SourcePlugin = this.OwningPlugin != null ? new ExposedPlugin(this.OwningPlugin) : null, - }; + }); - return Disposable.Create(() => { ipcExecutionContext = null; }); + return Disposable.Create(() => { this.Channel.ClearInvocationContext(); }); } } From 4e87b4b0076460195f3be5e2fe76630e41ebec2e Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 20:15:12 +0100 Subject: [PATCH 003/164] Retarget to .NET 10 --- Directory.Build.props | 2 +- global.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 4ed87c809..5f6da3d94 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,7 +2,7 @@ - net9.0-windows + net10.0-windows x64 x64 13.0 diff --git a/global.json b/global.json index ab1a4a2ec..93dd0dd1f 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { - "version": "9.0.0", + "version": "10.0.0", "rollForward": "latestMinor", "allowPrerelease": true } -} +} \ No newline at end of file From 7d76d275559bd82ef0b53a9709a8d1530f007630 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 20:31:28 +0100 Subject: [PATCH 004/164] Upgrade packages --- Directory.Packages.props | 120 ++++++++++++++++++--------------------- build/build.csproj | 2 +- 2 files changed, 57 insertions(+), 65 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a1cef517e..91875e63e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,65 +1,57 @@ - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build/build.csproj b/build/build.csproj index b4aaa959d..32907677f 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -12,6 +12,6 @@ - + From e0eff2fe74a91a4d234c3b916da7d61760cb9c9f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:02:07 +0100 Subject: [PATCH 005/164] Use standard apphost for Dalamud.Injector --- .../Dalamud.Injector.Boot.vcxproj | 111 ------------------ .../Dalamud.Injector.Boot.vcxproj.filters | 67 ----------- Dalamud.Injector.Boot/main.cpp | 48 -------- Dalamud.Injector.Boot/pch.h | 1 - Dalamud.Injector.Boot/resources.rc | 1 - Dalamud.Injector/Dalamud.Injector.csproj | 3 +- .../{EntryPoint.cs => Program.cs} | 26 +--- .../dalamud.ico | Bin Dalamud.sln | 12 +- 9 files changed, 9 insertions(+), 260 deletions(-) delete mode 100644 Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj delete mode 100644 Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters delete mode 100644 Dalamud.Injector.Boot/main.cpp delete mode 100644 Dalamud.Injector.Boot/pch.h delete mode 100644 Dalamud.Injector.Boot/resources.rc rename Dalamud.Injector/{EntryPoint.cs => Program.cs} (98%) rename {Dalamud.Injector.Boot => Dalamud.Injector}/dalamud.ico (100%) diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj deleted file mode 100644 index 7f8de3843..000000000 --- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj +++ /dev/null @@ -1,111 +0,0 @@ - - - - {8874326B-E755-4D13-90B4-59AB263A3E6B} - Dalamud_Injector_Boot - Debug - x64 - - - - Debug - x64 - - - Release - x64 - - - - 16.0 - Win32Proj - 10.0 - Dalamud.Injector - - - - Application - true - v143 - false - Unicode - ..\bin\$(Configuration)\ - obj\$(Configuration)\ - - - - - Level3 - true - true - stdcpp23 - pch.h - ProgramDatabase - CPPDLLTEMPLATE_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions) - - - Console - true - false - ..\lib\CoreCLR;%(AdditionalLibraryDirectories) - $(OutDir)$(TargetName).Boot.pdb - - - - - true - false - MultiThreadedDebugDLL - _DEBUG;%(PreprocessorDefinitions) - - - false - false - - - - - true - true - MultiThreadedDLL - NDEBUG;%(PreprocessorDefinitions) - - - true - true - - - - - - nethost.dll - PreserveNewest - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters b/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters deleted file mode 100644 index 8f4372d89..000000000 --- a/Dalamud.Injector.Boot/Dalamud.Injector.Boot.vcxproj.filters +++ /dev/null @@ -1,67 +0,0 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd - - - {4faac519-3a73-4b2b-96e7-fb597f02c0be} - ico;rc - - - - - Resource Files - - - - - Resource Files - - - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - Source Files - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - \ No newline at end of file diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp deleted file mode 100644 index df4120009..000000000 --- a/Dalamud.Injector.Boot/main.cpp +++ /dev/null @@ -1,48 +0,0 @@ -#define WIN32_LEAN_AND_MEAN - -#include -#include -#include -#include "..\Dalamud.Boot\logging.h" -#include "..\lib\CoreCLR\CoreCLR.h" -#include "..\lib\CoreCLR\boot.h" - -int wmain(int argc, wchar_t** argv) -{ - // Take care: don't redirect stderr/out here, we need to write our pid to stdout for XL to read - //logging::start_file_logging("dalamud.injector.boot.log", false); - logging::I("Dalamud Injector, (c) 2021 XIVLauncher Contributors"); - logging::I("Built at : " __DATE__ "@" __TIME__); - - wchar_t _module_path[MAX_PATH]; - GetModuleFileNameW(NULL, _module_path, sizeof _module_path / 2); - std::filesystem::path fs_module_path(_module_path); - - std::wstring runtimeconfig_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.runtimeconfig.json").c_str()); - std::wstring module_path = _wcsdup(fs_module_path.replace_filename(L"Dalamud.Injector.dll").c_str()); - - // =========================================================================== // - - void* entrypoint_vfn; - const auto result = InitializeClrAndGetEntryPoint( - GetModuleHandleW(nullptr), - false, - runtimeconfig_path, - module_path, - L"Dalamud.Injector.EntryPoint, Dalamud.Injector", - L"Main", - L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector", - &entrypoint_vfn); - - if (FAILED(result)) - return result; - - typedef int (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**); - custom_component_entry_point_fn entrypoint_fn = reinterpret_cast(entrypoint_vfn); - - logging::I("Running Dalamud Injector..."); - const auto ret = entrypoint_fn(argc, argv); - logging::I("Done!"); - - return ret; -} diff --git a/Dalamud.Injector.Boot/pch.h b/Dalamud.Injector.Boot/pch.h deleted file mode 100644 index 6f70f09be..000000000 --- a/Dalamud.Injector.Boot/pch.h +++ /dev/null @@ -1 +0,0 @@ -#pragma once diff --git a/Dalamud.Injector.Boot/resources.rc b/Dalamud.Injector.Boot/resources.rc deleted file mode 100644 index 8369e82a1..000000000 --- a/Dalamud.Injector.Boot/resources.rc +++ /dev/null @@ -1 +0,0 @@ -MAINICON ICON "dalamud.ico" diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index 4a55174a1..a0b4f6451 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -13,12 +13,13 @@ - Library + Exe ..\bin\$(Configuration)\ false false true false + dalamud.ico diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/Program.cs similarity index 98% rename from Dalamud.Injector/EntryPoint.cs rename to Dalamud.Injector/Program.cs index b876aa6ed..e224791e6 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/Program.cs @@ -25,34 +25,20 @@ namespace Dalamud.Injector /// /// Entrypoint to the program. /// - public sealed class EntryPoint + public sealed class Program { - /// - /// A delegate used during initialization of the CLR from Dalamud.Injector.Boot. - /// - /// Count of arguments. - /// char** string arguments. - /// Return value (HRESULT). - public delegate int MainDelegate(int argc, IntPtr argvPtr); - /// /// Start the Dalamud injector. /// - /// Count of arguments. - /// byte** string arguments. + /// Command line arguments. /// Return value (HRESULT). - public static int Main(int argc, IntPtr argvPtr) + public static int Main(string[] argsArray) { try { - List args = new(argc); - - unsafe - { - var argv = (IntPtr*)argvPtr; - for (var i = 0; i < argc; i++) - args.Add(Marshal.PtrToStringUni(argv[i])); - } + // API14 TODO: Refactor + var args = argsArray.ToList(); + args.Insert(0, Assembly.GetExecutingAssembly().Location); Init(args); args.Remove("-v"); // Remove "verbose" flag diff --git a/Dalamud.Injector.Boot/dalamud.ico b/Dalamud.Injector/dalamud.ico similarity index 100% rename from Dalamud.Injector.Boot/dalamud.ico rename to Dalamud.Injector/dalamud.ico diff --git a/Dalamud.sln b/Dalamud.sln index c3af00f44..ee3c75b25 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.1.32319.34 MinimumVisualStudioVersion = 10.0.40219.1 @@ -27,8 +27,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Boot", "Dalamud.Boo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Injector", "Dalamud.Injector\Dalamud.Injector.csproj", "{5B832F73-5F54-4ADC-870F-D0095EF72C9A}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Dalamud.Injector.Boot", "Dalamud.Injector.Boot\Dalamud.Injector.Boot.vcxproj", "{8874326B-E755-4D13-90B4-59AB263A3E6B}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Test", "Dalamud.Test\Dalamud.Test.csproj", "{C8004563-1806-4329-844F-0EF6274291FC}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dependencies", "Dependencies", "{E15BDA6D-E881-4482-94BA-BE5527E917FF}" @@ -49,8 +47,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator", "lib\FFX EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InteropGenerator.Runtime", "lib\FFXIVClientStructs\InteropGenerator.Runtime\InteropGenerator.Runtime.csproj", "{A6AA1C3F-9470-4922-9D3F-D4549657AB22}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Injector", "Injector", "{19775C83-7117-4A5F-AA00-18889F46A490}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{8F079208-C227-4D96-9427-2BEBE0003944}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "cimgui", "external\cimgui\cimgui.vcxproj", "{8430077C-F736-4246-A052-8EA1CECE844E}" @@ -103,10 +99,6 @@ Global {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.ActiveCfg = Debug|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|Any CPU.Build.0 = Debug|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.ActiveCfg = Release|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|Any CPU.Build.0 = Release|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.ActiveCfg = Debug|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Debug|Any CPU.Build.0 = Debug|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64 @@ -188,8 +180,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {5B832F73-5F54-4ADC-870F-D0095EF72C9A} = {19775C83-7117-4A5F-AA00-18889F46A490} - {8874326B-E755-4D13-90B4-59AB263A3E6B} = {19775C83-7117-4A5F-AA00-18889F46A490} {4AFDB34A-7467-4D41-B067-53BC4101D9D0} = {8F079208-C227-4D96-9427-2BEBE0003944} {C9B87BD7-AF49-41C3-91F1-D550ADEB7833} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} {E0D51896-604F-4B40-8CFE-51941607B3A1} = {8BBACF2D-7AB8-4610-A115-0E363D35C291} From a37a13e0ba0ab5a661f71add85c0b9740dabdf1c Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:03:14 +0100 Subject: [PATCH 006/164] Use .NET 10 in CI --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index be44afacc..299d71e95 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,7 +23,7 @@ jobs: uses: microsoft/setup-msbuild@v1.0.2 - uses: actions/setup-dotnet@v3 with: - dotnet-version: '9.0.200' + dotnet-version: '10.0.100' - name: Define VERSION run: | $env:COMMIT = $env:GITHUB_SHA.Substring(0, 7) From 7bc921f54328924e2b29ef481da08d481a60e4b7 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:09:21 +0100 Subject: [PATCH 007/164] No analyzers on nuke build --- build/build.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/build/build.csproj b/build/build.csproj index 32907677f..1e1416d92 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -13,5 +13,6 @@ + From 928fbba4893ae2022a5b8b637c3fa875bc4afec4 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:13:50 +0100 Subject: [PATCH 008/164] Remove Injector.Boot targets --- build/DalamudBuild.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs index d374c79f8..ba2b09a4d 100644 --- a/build/DalamudBuild.cs +++ b/build/DalamudBuild.cs @@ -42,10 +42,7 @@ public class DalamudBuild : NukeBuild AbsolutePath InjectorProjectDir => RootDirectory / "Dalamud.Injector"; AbsolutePath InjectorProjectFile => InjectorProjectDir / "Dalamud.Injector.csproj"; - - AbsolutePath InjectorBootProjectDir => RootDirectory / "Dalamud.Injector.Boot"; - AbsolutePath InjectorBootProjectFile => InjectorBootProjectDir / "Dalamud.Injector.Boot.vcxproj"; - + AbsolutePath TestProjectDir => RootDirectory / "Dalamud.Test"; AbsolutePath TestProjectFile => TestProjectDir / "Dalamud.Test.csproj"; @@ -172,14 +169,6 @@ public class DalamudBuild : NukeBuild .EnableNoRestore()); }); - Target CompileInjectorBoot => _ => _ - .Executes(() => - { - MSBuildTasks.MSBuild(s => s - .SetTargetPath(InjectorBootProjectFile) - .SetConfiguration(Configuration)); - }); - Target SetCILogging => _ => _ .DependentFor(Compile) .OnlyWhenStatic(() => IsCIBuild) @@ -196,7 +185,6 @@ public class DalamudBuild : NukeBuild .DependsOn(CompileDalamudBoot) .DependsOn(CompileDalamudCrashHandler) .DependsOn(CompileInjector) - .DependsOn(CompileInjectorBoot) ; Target CI => _ => _ @@ -250,11 +238,6 @@ public class DalamudBuild : NukeBuild .SetProject(InjectorProjectFile) .SetConfiguration(Configuration)); - MSBuildTasks.MSBuild(s => s - .SetProjectFile(InjectorBootProjectFile) - .SetConfiguration(Configuration) - .SetTargets("Clean")); - FileSystemTasks.DeleteDirectory(ArtifactsDirectory); Directory.CreateDirectory(ArtifactsDirectory); }); From 6340afb6921bfd8915e22300bcf623c956ea0c1f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 12 Nov 2025 21:39:38 +0100 Subject: [PATCH 009/164] Nuke schema, also remove analyzers from imgui testbed --- .nuke/build.schema.json | 2 -- imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 8331affcc..03211ce8f 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -87,7 +87,6 @@ "CompileDalamudCrashHandler", "CompileImGuiNatives", "CompileInjector", - "CompileInjectorBoot", "Restore", "SetCILogging", "Test" @@ -115,7 +114,6 @@ "CompileDalamudCrashHandler", "CompileImGuiNatives", "CompileInjector", - "CompileInjectorBoot", "Restore", "SetCILogging", "Test" diff --git a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj index d56faa31e..da31c9a8e 100644 --- a/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj +++ b/imgui/StandaloneImGuiTestbed/StandaloneImGuiTestbed.csproj @@ -26,6 +26,7 @@ + From 2b2f628096f25b00994f5fc5abec1acc2eb6327e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:43:49 +0100 Subject: [PATCH 010/164] Convert ObjectTable enumerator to struct --- .../Game/ClientState/Objects/ObjectTable.cs | 40 ++----------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 84c1b5693..598daf518 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -12,8 +12,6 @@ using Dalamud.Utility; using FFXIVClientStructs.Interop; -using Microsoft.Extensions.ObjectPool; - using CSGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject; using CSGameObjectManager = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObjectManager; @@ -34,8 +32,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable private readonly ClientState clientState; private readonly CachedEntry[] cachedObjectTable; - private readonly Enumerator?[] frameworkThreadEnumerators = new Enumerator?[4]; - [ServiceManager.ServiceConstructor] private unsafe ObjectTable(ClientState clientState) { @@ -47,9 +43,6 @@ internal sealed partial class ObjectTable : IServiceType, IObjectTable this.cachedObjectTable = new CachedEntry[objectTableLength]; for (var i = 0; i < this.cachedObjectTable.Length; i++) this.cachedObjectTable[i] = new(nativeObjectTable.GetPointer(i)); - - for (var i = 0; i < this.frameworkThreadEnumerators.Length; i++) - this.frameworkThreadEnumerators[i] = new(this, i); } /// @@ -239,30 +232,14 @@ internal sealed partial class ObjectTable public IEnumerator GetEnumerator() { ThreadSafety.AssertMainThread(); - - // If we're on the framework thread, see if there's an already allocated enumerator available for use. - foreach (ref var x in this.frameworkThreadEnumerators.AsSpan()) - { - if (x is not null) - { - var t = x; - x = null; - t.Reset(); - return t; - } - } - - // No reusable enumerator is available; allocate a new temporary one. - return new Enumerator(this, -1); + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - private sealed class Enumerator(ObjectTable owner, int slotId) : IEnumerator, IResettable + private struct Enumerator(ObjectTable owner) : IEnumerator { - private ObjectTable? owner = owner; - private int index = -1; public IGameObject Current { get; private set; } = null!; @@ -274,7 +251,7 @@ internal sealed partial class ObjectTable if (this.index == objectTableLength) return false; - var cache = this.owner!.cachedObjectTable.AsSpan(); + var cache = owner.cachedObjectTable.AsSpan(); for (this.index++; this.index < objectTableLength; this.index++) { if (cache[this.index].Update() is { } ao) @@ -291,17 +268,6 @@ internal sealed partial class ObjectTable public void Dispose() { - if (this.owner is not { } o) - return; - - if (slotId != -1) - o.frameworkThreadEnumerators[slotId] = this; - } - - public bool TryReset() - { - this.Reset(); - return true; } } } From dd70c5b8eea1627566f0b355fc5ce7807007e803 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:36:45 +0100 Subject: [PATCH 011/164] Add struct enumerator to AetheryteList --- .../ClientState/Aetherytes/AetheryteList.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index a3d44d423..f72339ed2 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -87,10 +87,7 @@ internal sealed partial class AetheryteList /// public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - yield return this[i]; - } + return new Enumerator(this); } /// @@ -98,4 +95,30 @@ internal sealed partial class AetheryteList { return this.GetEnumerator(); } + + private struct Enumerator(AetheryteList aetheryteList) : IEnumerator + { + private int index = 0; + + public IAetheryteEntry Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == aetheryteList.Length) return false; + this.Current = aetheryteList[this.index]; + this.index++; + return true; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 520e3ea028044395925e8d73d29a1a2fb4f5410f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:37:15 +0100 Subject: [PATCH 012/164] Convert AetheryteEntry to readonly struct --- .../ClientState/Aetherytes/AetheryteEntry.cs | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs index 89dd8b8b1..e0a5df06d 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteEntry.cs @@ -63,47 +63,37 @@ public interface IAetheryteEntry } /// -/// Class representing an aetheryte entry available to the game. +/// This struct represents an aetheryte entry available to the game. /// -internal sealed class AetheryteEntry : IAetheryteEntry +/// Data read from the Aetheryte List. +internal readonly struct AetheryteEntry(TeleportInfo data) : IAetheryteEntry { - private readonly TeleportInfo data; - - /// - /// Initializes a new instance of the class. - /// - /// Data read from the Aetheryte List. - internal AetheryteEntry(TeleportInfo data) - { - this.data = data; - } + /// + public uint AetheryteId => data.AetheryteId; /// - public uint AetheryteId => this.data.AetheryteId; + public uint TerritoryId => data.TerritoryId; /// - public uint TerritoryId => this.data.TerritoryId; + public byte SubIndex => data.SubIndex; /// - public byte SubIndex => this.data.SubIndex; + public byte Ward => data.Ward; /// - public byte Ward => this.data.Ward; + public byte Plot => data.Plot; /// - public byte Plot => this.data.Plot; + public uint GilCost => data.GilCost; /// - public uint GilCost => this.data.GilCost; + public bool IsFavourite => data.IsFavourite; /// - public bool IsFavourite => this.data.IsFavourite; + public bool IsSharedHouse => data.IsSharedHouse; /// - public bool IsSharedHouse => this.data.IsSharedHouse; - - /// - public bool IsApartment => this.data.IsApartment; + public bool IsApartment => data.IsApartment; /// public RowRef AetheryteData => LuminaUtils.CreateRef(this.AetheryteId); From 8a9b47c7a472987c33240594fbea645727fb7dc3 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:37:35 +0100 Subject: [PATCH 013/164] Add struct enumerator to BuddyList --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 31 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 84cfd24a3..71121e54e 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -130,12 +130,35 @@ internal sealed partial class BuddyList /// public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - yield return this[i]; - } + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private struct Enumerator(BuddyList buddyList) : IEnumerator + { + private int index = 0; + + public IBuddyMember Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == buddyList.Length) return false; + this.Current = buddyList[this.index]; + this.index++; + return true; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 23e7c164d86ca1d59713f7f7f8955f2eed2b29d6 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:42:07 +0100 Subject: [PATCH 014/164] Convert BuddyMember to readonly struct --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 33 +++++----- Dalamud/Game/ClientState/Buddy/BuddyMember.cs | 60 ++++++++++++------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 71121e54e..78809f8ba 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -8,6 +8,9 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.UI; +using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; +using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; + namespace Dalamud.Game.ClientState.Buddy; /// @@ -21,7 +24,7 @@ namespace Dalamud.Game.ClientState.Buddy; #pragma warning restore SA1015 internal sealed partial class BuddyList : IServiceType, IBuddyList { - private const uint InvalidObjectID = 0xE0000000; + private const uint InvalidEntityId = 0xE0000000; [ServiceManager.ServiceDependency] private readonly ClientState clientState = Service.Get(); @@ -69,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } } - private unsafe FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy* BuddyListStruct => &UIState.Instance()->Buddy; + private unsafe CSBuddy* BuddyListStruct => &UIState.Instance()->Buddy; /// public IBuddyMember? this[int index] @@ -82,37 +85,37 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } /// - public unsafe IntPtr GetCompanionBuddyMemberAddress() + public unsafe nint GetCompanionBuddyMemberAddress() { - return (IntPtr)this.BuddyListStruct->CompanionInfo.Companion; + return (nint)this.BuddyListStruct->CompanionInfo.Companion; } /// - public unsafe IntPtr GetPetBuddyMemberAddress() + public unsafe nint GetPetBuddyMemberAddress() { - return (IntPtr)this.BuddyListStruct->PetInfo.Pet; + return (nint)this.BuddyListStruct->PetInfo.Pet; } /// - public unsafe IntPtr GetBattleBuddyMemberAddress(int index) + public unsafe nint GetBattleBuddyMemberAddress(int index) { if (index < 0 || index >= 3) - return IntPtr.Zero; + return 0; - return (IntPtr)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]); + return (nint)Unsafe.AsPointer(ref this.BuddyListStruct->BattleBuddies[index]); } /// - public IBuddyMember? CreateBuddyMemberReference(IntPtr address) + public unsafe IBuddyMember? CreateBuddyMemberReference(nint address) { + if (address == 0) + return null; + if (this.clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) - return null; - - var buddy = new BuddyMember(address); - if (buddy.ObjectId == InvalidObjectID) + var buddy = new BuddyMember((CSBuddyMember*)address); + if (buddy.EntityId == InvalidEntityId) return null; return buddy; diff --git a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs index 393598d32..8018bafaf 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs @@ -1,20 +1,24 @@ +using System.Diagnostics.CodeAnalysis; + using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Excel; +using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; + namespace Dalamud.Game.ClientState.Buddy; /// /// Interface representing represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. /// -public interface IBuddyMember +public interface IBuddyMember : IEquatable { /// /// Gets the address of the buddy in memory. /// - IntPtr Address { get; } + nint Address { get; } /// /// Gets the object ID of this buddy. @@ -67,42 +71,34 @@ public interface IBuddyMember } /// -/// This class represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. +/// This struct represents a buddy such as the chocobo companion, summoned pets, squadron groups and trust parties. /// -internal unsafe class BuddyMember : IBuddyMember +/// A pointer to the BuddyMember. +internal readonly unsafe struct BuddyMember(CSBuddyMember* ptr) : IBuddyMember { [ServiceManager.ServiceDependency] private readonly ObjectTable objectTable = Service.Get(); - /// - /// Initializes a new instance of the class. - /// - /// Buddy address. - internal BuddyMember(IntPtr address) - { - this.Address = address; - } + /// + public nint Address => (nint)ptr; /// - public IntPtr Address { get; } + public uint ObjectId => this.EntityId; /// - public uint ObjectId => this.Struct->EntityId; + public uint EntityId => ptr->EntityId; /// - public uint EntityId => this.Struct->EntityId; + public IGameObject? GameObject => this.objectTable.SearchById(this.EntityId); /// - public IGameObject? GameObject => this.objectTable.SearchById(this.ObjectId); + public uint CurrentHP => ptr->CurrentHealth; /// - public uint CurrentHP => this.Struct->CurrentHealth; + public uint MaxHP => ptr->MaxHealth; /// - public uint MaxHP => this.Struct->MaxHealth; - - /// - public uint DataID => this.Struct->DataId; + public uint DataID => ptr->DataId; /// public RowRef MountData => LuminaUtils.CreateRef(this.DataID); @@ -113,5 +109,25 @@ internal unsafe class BuddyMember : IBuddyMember /// public RowRef TrustData => LuminaUtils.CreateRef(this.DataID); - private FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember*)this.Address; + public static bool operator ==(BuddyMember x, BuddyMember y) => x.Equals(y); + + public static bool operator !=(BuddyMember x, BuddyMember y) => !(x == y); + + /// + public bool Equals(IBuddyMember? other) + { + return this.EntityId == other.EntityId; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is BuddyMember fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return this.EntityId.GetHashCode(); + } } From d1bed3ebc5e5f4ec8a7104c2e718babb4b546425 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:42:31 +0100 Subject: [PATCH 015/164] Add struct enumerator to FateTable --- Dalamud/Game/ClientState/Fates/FateTable.cs | 31 ++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 1bf557ad5..942d1561f 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -110,12 +110,35 @@ internal sealed partial class FateTable /// public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - yield return this[i]; - } + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private struct Enumerator(FateTable fateTable) : IEnumerator + { + private int index = 0; + + public IFate Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == fateTable.Length) return false; + this.Current = fateTable[this.index]; + this.index++; + return true; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From a48eead85e9f9fb98fcaa841c34752b7dca700e2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:43:06 +0100 Subject: [PATCH 016/164] Convert Fate to readonly struct --- Dalamud/Game/ClientState/Fates/Fate.cs | 130 ++++++++------------ Dalamud/Game/ClientState/Fates/FateTable.cs | 22 ++-- 2 files changed, 59 insertions(+), 93 deletions(-) diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index 504b690c3..c40a8960e 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Dalamud.Data; @@ -6,10 +7,12 @@ using Dalamud.Memory; using Lumina.Excel; +using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext; + namespace Dalamud.Game.ClientState.Fates; /// -/// Interface representing an fate entry that can be seen in the current area. +/// Interface representing a fate entry that can be seen in the current area. /// public interface IFate : IEquatable { @@ -111,133 +114,96 @@ public interface IFate : IEquatable /// /// Gets the address of this Fate in memory. /// - IntPtr Address { get; } + nint Address { get; } } /// -/// This class represents an FFXIV Fate. +/// This struct represents a Fate. /// -internal unsafe partial class Fate +/// A pointer to the FateContext. +internal readonly unsafe struct Fate(CSFateContext* ptr) : IFate { - /// - /// Initializes a new instance of the class. - /// - /// The address of this fate in memory. - internal Fate(IntPtr address) - { - this.Address = address; - } - /// - public IntPtr Address { get; } - - private FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext*)this.Address; - - public static bool operator ==(Fate fate1, Fate fate2) - { - if (fate1 is null || fate2 is null) - return Equals(fate1, fate2); - - return fate1.Equals(fate2); - } - - public static bool operator !=(Fate fate1, Fate fate2) => !(fate1 == fate2); - - /// - /// Gets a value indicating whether this Fate is still valid in memory. - /// - /// The fate to check. - /// True or false. - public static bool IsValid(Fate fate) - { - var clientState = Service.GetNullable(); - - if (fate == null || clientState == null) - return false; - - if (clientState.LocalContentId == 0) - return false; - - return true; - } - - /// - /// Gets a value indicating whether this actor is still valid in memory. - /// - /// True or false. - public bool IsValid() => IsValid(this); + public nint Address => (nint)ptr; /// - bool IEquatable.Equals(IFate other) => this.FateId == other?.FateId; - - /// - public override bool Equals(object obj) => ((IEquatable)this).Equals(obj as IFate); - - /// - public override int GetHashCode() => this.FateId.GetHashCode(); -} - -/// -/// This class represents an FFXIV Fate. -/// -internal unsafe partial class Fate : IFate -{ - /// - public ushort FateId => this.Struct->FateId; + public ushort FateId => ptr->FateId; /// public RowRef GameData => LuminaUtils.CreateRef(this.FateId); /// - public int StartTimeEpoch => this.Struct->StartTimeEpoch; + public int StartTimeEpoch => ptr->StartTimeEpoch; /// - public short Duration => this.Struct->Duration; + public short Duration => ptr->Duration; /// public long TimeRemaining => this.StartTimeEpoch + this.Duration - DateTimeOffset.Now.ToUnixTimeSeconds(); /// - public SeString Name => MemoryHelper.ReadSeString(&this.Struct->Name); + public SeString Name => MemoryHelper.ReadSeString(&ptr->Name); /// - public SeString Description => MemoryHelper.ReadSeString(&this.Struct->Description); + public SeString Description => MemoryHelper.ReadSeString(&ptr->Description); /// - public SeString Objective => MemoryHelper.ReadSeString(&this.Struct->Objective); + public SeString Objective => MemoryHelper.ReadSeString(&ptr->Objective); /// - public FateState State => (FateState)this.Struct->State; + public FateState State => (FateState)ptr->State; /// - public byte HandInCount => this.Struct->HandInCount; + public byte HandInCount => ptr->HandInCount; /// - public byte Progress => this.Struct->Progress; + public byte Progress => ptr->Progress; /// - public bool HasBonus => this.Struct->IsBonus; + public bool HasBonus => ptr->IsBonus; /// - public uint IconId => this.Struct->IconId; + public uint IconId => ptr->IconId; /// - public byte Level => this.Struct->Level; + public byte Level => ptr->Level; /// - public byte MaxLevel => this.Struct->MaxLevel; + public byte MaxLevel => ptr->MaxLevel; /// - public Vector3 Position => this.Struct->Location; + public Vector3 Position => ptr->Location; /// - public float Radius => this.Struct->Radius; + public float Radius => ptr->Radius; /// - public uint MapIconId => this.Struct->MapIconId; + public uint MapIconId => ptr->MapIconId; /// /// Gets the territory this is located in. /// - public RowRef TerritoryType => LuminaUtils.CreateRef(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId); + public RowRef TerritoryType => LuminaUtils.CreateRef(ptr->MapMarkers[0].MapMarkerData.TerritoryTypeId); + + public static bool operator ==(Fate x, Fate y) => x.Equals(y); + + public static bool operator !=(Fate x, Fate y) => !(x == y); + + /// + public bool Equals(IFate? other) + { + return this.FateId == other.FateId; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is Fate fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return this.FateId.GetHashCode(); + } } diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 942d1561f..a6edf4a18 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -5,6 +5,7 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; +using CSFateContext = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateContext; using CSFateManager = FFXIVClientStructs.FFXIV.Client.Game.Fate.FateManager; namespace Dalamud.Game.ClientState.Fates; @@ -25,7 +26,7 @@ internal sealed partial class FateTable : IServiceType, IFateTable } /// - public unsafe IntPtr Address => (nint)CSFateManager.Instance(); + public unsafe nint Address => (nint)CSFateManager.Instance(); /// public unsafe int Length @@ -72,30 +73,29 @@ internal sealed partial class FateTable : IServiceType, IFateTable } /// - public unsafe IntPtr GetFateAddress(int index) + public unsafe nint GetFateAddress(int index) { if (index >= this.Length) - return IntPtr.Zero; + return 0; var fateManager = CSFateManager.Instance(); if (fateManager == null) - return IntPtr.Zero; + return 0; - return (IntPtr)fateManager->Fates[index].Value; + return (nint)fateManager->Fates[index].Value; } /// - public IFate? CreateFateReference(IntPtr offset) + public unsafe IFate? CreateFateReference(IntPtr address) { - var clientState = Service.Get(); + if (address == 0) + return null; + var clientState = Service.Get(); if (clientState.LocalContentId == 0) return null; - if (offset == IntPtr.Zero) - return null; - - return new Fate(offset); + return new Fate((CSFateContext*)address); } } From d1dc81318a8aa26fbf35da9de96fba3fb369edd5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:47:55 +0100 Subject: [PATCH 017/164] Add struct enumerator to PartyList --- Dalamud/Game/ClientState/Party/PartyList.cs | 46 ++++++++++++++++----- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index a016a8211..bfd423a79 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -133,18 +133,44 @@ internal sealed partial class PartyList /// public IEnumerator GetEnumerator() { - // Normally using Length results in a recursion crash, however we know the party size via ptr. - for (var i = 0; i < this.Length; i++) - { - var member = this[i]; - - if (member == null) - break; - - yield return member; - } + return new Enumerator(this); } /// IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private struct Enumerator(PartyList partyList) : IEnumerator + { + private int index = 0; + + public IPartyMember Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == partyList.Length) return false; + + for (; this.index < partyList.Length; this.index++) + { + var partyMember = partyList[this.index]; + if (partyMember != null) + { + this.Current = partyMember; + return true; + } + } + + return false; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 53b94caeb7470dc7f2ca639412188f38a7f19343 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 18:53:03 +0100 Subject: [PATCH 018/164] Convert PartyMember to readonly struct --- Dalamud/Game/ClientState/Party/PartyList.cs | 31 ++++---- Dalamud/Game/ClientState/Party/PartyMember.cs | 79 +++++++++++-------- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index bfd423a79..ec22932ab 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -8,6 +8,7 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; using CSGroupManager = FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager; +using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember; namespace Dalamud.Game.ClientState.Party; @@ -42,20 +43,20 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList public bool IsAlliance => this.GroupManagerStruct->MainGroup.AllianceFlags > 0; /// - public unsafe IntPtr GroupManagerAddress => (nint)CSGroupManager.Instance(); + public unsafe nint GroupManagerAddress => (nint)CSGroupManager.Instance(); /// - public IntPtr GroupListAddress => (IntPtr)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); + public nint GroupListAddress => (nint)Unsafe.AsPointer(ref GroupManagerStruct->MainGroup.PartyMembers[0]); /// - public IntPtr AllianceListAddress => (IntPtr)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); + public nint AllianceListAddress => (nint)Unsafe.AsPointer(ref this.GroupManagerStruct->MainGroup.AllianceMembers[0]); /// public long PartyId => this.GroupManagerStruct->MainGroup.PartyId; - private static int PartyMemberSize { get; } = Marshal.SizeOf(); + private static int PartyMemberSize { get; } = Marshal.SizeOf(); - private FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager* GroupManagerStruct => (FFXIVClientStructs.FFXIV.Client.Game.Group.GroupManager*)this.GroupManagerAddress; + private CSGroupManager* GroupManagerStruct => (CSGroupManager*)this.GroupManagerAddress; /// public IPartyMember? this[int index] @@ -80,45 +81,45 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList } /// - public IntPtr GetPartyMemberAddress(int index) + public nint GetPartyMemberAddress(int index) { if (index < 0 || index >= GroupLength) - return IntPtr.Zero; + return 0; return this.GroupListAddress + (index * PartyMemberSize); } /// - public IPartyMember? CreatePartyMemberReference(IntPtr address) + public IPartyMember? CreatePartyMemberReference(nint address) { if (this.clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; - return new PartyMember(address); + return new PartyMember((CSPartyMember*)address); } /// - public IntPtr GetAllianceMemberAddress(int index) + public nint GetAllianceMemberAddress(int index) { if (index < 0 || index >= AllianceLength) - return IntPtr.Zero; + return 0; return this.AllianceListAddress + (index * PartyMemberSize); } /// - public IPartyMember? CreateAllianceMemberReference(IntPtr address) + public IPartyMember? CreateAllianceMemberReference(nint address) { if (this.clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; - return new PartyMember(address); + return new PartyMember((CSPartyMember*)address); } } diff --git a/Dalamud/Game/ClientState/Party/PartyMember.cs b/Dalamud/Game/ClientState/Party/PartyMember.cs index 4c738d866..c9980d9f2 100644 --- a/Dalamud/Game/ClientState/Party/PartyMember.cs +++ b/Dalamud/Game/ClientState/Party/PartyMember.cs @@ -1,26 +1,27 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; -using System.Runtime.CompilerServices; using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Memory; using Lumina.Excel; +using CSPartyMember = FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember; + namespace Dalamud.Game.ClientState.Party; /// /// Interface representing a party member. /// -public interface IPartyMember +public interface IPartyMember : IEquatable { /// /// Gets the address of this party member in memory. /// - IntPtr Address { get; } + nint Address { get; } /// /// Gets a list of buffs or debuffs applied to this party member. @@ -108,69 +109,81 @@ public interface IPartyMember } /// -/// This class represents a party member in the group manager. +/// This struct represents a party member in the group manager. /// -internal unsafe class PartyMember : IPartyMember +/// A pointer to the PartyMember. +internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember { - /// - /// Initializes a new instance of the class. - /// - /// Address of the party member. - internal PartyMember(IntPtr address) - { - this.Address = address; - } + /// + public nint Address => (nint)ptr; /// - public IntPtr Address { get; } + public StatusList Statuses => new(&ptr->StatusManager); /// - public StatusList Statuses => new(&this.Struct->StatusManager); + public Vector3 Position => ptr->Position; /// - public Vector3 Position => this.Struct->Position; + public long ContentId => (long)ptr->ContentId; /// - public long ContentId => (long)this.Struct->ContentId; + public uint ObjectId => ptr->EntityId; /// - public uint ObjectId => this.Struct->EntityId; - - /// - public uint EntityId => this.Struct->EntityId; + public uint EntityId => ptr->EntityId; /// public IGameObject? GameObject => Service.Get().SearchById(this.EntityId); /// - public uint CurrentHP => this.Struct->CurrentHP; + public uint CurrentHP => ptr->CurrentHP; /// - public uint MaxHP => this.Struct->MaxHP; + public uint MaxHP => ptr->MaxHP; /// - public ushort CurrentMP => this.Struct->CurrentMP; + public ushort CurrentMP => ptr->CurrentMP; /// - public ushort MaxMP => this.Struct->MaxMP; + public ushort MaxMP => ptr->MaxMP; /// - public RowRef Territory => LuminaUtils.CreateRef(this.Struct->TerritoryType); + public RowRef Territory => LuminaUtils.CreateRef(ptr->TerritoryType); /// - public RowRef World => LuminaUtils.CreateRef(this.Struct->HomeWorld); + public RowRef World => LuminaUtils.CreateRef(ptr->HomeWorld); /// - public SeString Name => SeString.Parse(this.Struct->Name); + public SeString Name => SeString.Parse(ptr->Name); /// - public byte Sex => this.Struct->Sex; + public byte Sex => ptr->Sex; /// - public RowRef ClassJob => LuminaUtils.CreateRef(this.Struct->ClassJob); + public RowRef ClassJob => LuminaUtils.CreateRef(ptr->ClassJob); /// - public byte Level => this.Struct->Level; + public byte Level => ptr->Level; - private FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Group.PartyMember*)this.Address; + public static bool operator ==(PartyMember x, PartyMember y) => x.Equals(y); + + public static bool operator !=(PartyMember x, PartyMember y) => !(x == y); + + /// + public bool Equals(IPartyMember? other) + { + return this.EntityId == other.EntityId; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is PartyMember fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return this.EntityId.GetHashCode(); + } } From 7f2ed9adb6534934e6440b288fd6c753bddbe67c Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 19:01:04 +0100 Subject: [PATCH 019/164] Convert Status to readonly struct and add interface --- Dalamud/Game/ClientState/Statuses/Status.cs | 86 +++++++++++++++------ 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/Dalamud/Game/ClientState/Statuses/Status.cs b/Dalamud/Game/ClientState/Statuses/Status.cs index 2775f8f9b..160b15de5 100644 --- a/Dalamud/Game/ClientState/Statuses/Status.cs +++ b/Dalamud/Game/ClientState/Statuses/Status.cs @@ -1,61 +1,49 @@ +using System.Diagnostics.CodeAnalysis; + using Dalamud.Data; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Lumina.Excel; +using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status; + namespace Dalamud.Game.ClientState.Statuses; /// -/// This class represents a status effect an actor is afflicted by. +/// Interface representing a status. /// -public unsafe class Status +public interface IStatus : IEquatable { - /// - /// Initializes a new instance of the class. - /// - /// Status address. - internal Status(IntPtr address) - { - this.Address = address; - } - /// /// Gets the address of the status in memory. /// - public IntPtr Address { get; } + nint Address { get; } /// /// Gets the status ID of this status. /// - public uint StatusId => this.Struct->StatusId; + uint StatusId { get; } /// /// Gets the GameData associated with this status. /// - public RowRef GameData => LuminaUtils.CreateRef(this.Struct->StatusId); + RowRef GameData { get; } /// /// Gets the parameter value of the status. /// - public ushort Param => this.Struct->Param; - - /// - /// Gets the stack count of this status. - /// Only valid if this is a non-food status. - /// - [Obsolete($"Replaced with {nameof(Param)}", true)] - public byte StackCount => (byte)this.Struct->Param; + ushort Param { get; } /// /// Gets the time remaining of this status. /// - public float RemainingTime => this.Struct->RemainingTime; + float RemainingTime { get; } /// /// Gets the source ID of this status. /// - public uint SourceId => this.Struct->SourceObject.ObjectId; + uint SourceId { get; } /// /// Gets the source actor associated with this status. @@ -63,7 +51,55 @@ public unsafe class Status /// /// This iterates the actor table, it should be used with care. /// + IGameObject? SourceObject { get; } +} + +/// +/// This struct represents a status effect an actor is afflicted by. +/// +/// A pointer to the Status. +internal unsafe readonly struct Status(CSStatus* ptr) : IStatus +{ + /// + public nint Address => (nint)ptr; + + /// + public uint StatusId => ptr->StatusId; + + /// + public RowRef GameData => LuminaUtils.CreateRef(ptr->StatusId); + + /// + public ushort Param => ptr->Param; + + /// + public float RemainingTime => ptr->RemainingTime; + + /// + public uint SourceId => ptr->SourceObject.ObjectId; + + /// public IGameObject? SourceObject => Service.Get().SearchById(this.SourceId); - private FFXIVClientStructs.FFXIV.Client.Game.Status* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Status*)this.Address; + public static bool operator ==(Status x, Status y) => x.Equals(y); + + public static bool operator !=(Status x, Status y) => !(x == y); + + /// + public bool Equals(IStatus? other) + { + return this.StatusId == other.StatusId && this.SourceId == other.SourceId && this.Param == other.Param && this.RemainingTime == other.RemainingTime; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + return obj is Status fate && this.Equals(fate); + } + + /// + public override int GetHashCode() + { + return HashCode.Combine(this.StatusId, this.SourceId, this.Param, this.RemainingTime); + } } From 778c82fad2c369e59e442dab8b0a1e3eb7000373 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 19:02:50 +0100 Subject: [PATCH 020/164] Add struct enumerator to StatusList --- .../Game/ClientState/Statuses/StatusList.cs | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index a38e45ea3..50d242d33 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using CSStatus = FFXIVClientStructs.FFXIV.Client.Game.Status; + namespace Dalamud.Game.ClientState.Statuses; /// @@ -14,7 +16,7 @@ public sealed unsafe partial class StatusList /// Initializes a new instance of the class. /// /// Address of the status list. - internal StatusList(IntPtr address) + internal StatusList(nint address) { this.Address = address; } @@ -24,14 +26,14 @@ public sealed unsafe partial class StatusList /// /// Pointer to the status list. internal unsafe StatusList(void* pointer) - : this((IntPtr)pointer) + : this((nint)pointer) { } /// /// Gets the address of the status list in memory. /// - public IntPtr Address { get; } + public nint Address { get; } /// /// Gets the amount of status effect slots the actor has. @@ -47,7 +49,7 @@ public sealed unsafe partial class StatusList /// /// Status Index. /// The status at the specified index. - public Status? this[int index] + public IStatus? this[int index] { get { @@ -64,7 +66,7 @@ public sealed unsafe partial class StatusList /// /// The address of the status list in memory. /// The status object containing the requested data. - public static StatusList? CreateStatusListReference(IntPtr address) + public static StatusList? CreateStatusListReference(nint address) { // The use case for CreateStatusListReference and CreateStatusReference to be static is so // fake status lists can be generated. Since they aren't exposed as services, it's either @@ -74,7 +76,7 @@ public sealed unsafe partial class StatusList if (clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; return new StatusList(address); @@ -85,17 +87,17 @@ public sealed unsafe partial class StatusList /// /// The address of the status effect in memory. /// The status object containing the requested data. - public static Status? CreateStatusReference(IntPtr address) + public static IStatus? CreateStatusReference(nint address) { var clientState = Service.Get(); if (clientState.LocalContentId == 0) return null; - if (address == IntPtr.Zero) + if (address == 0) return null; - return new Status(address); + return new Status((CSStatus*)address); } /// @@ -103,22 +105,22 @@ public sealed unsafe partial class StatusList /// /// The index of the status. /// The memory address of the status. - public IntPtr GetStatusAddress(int index) + public nint GetStatusAddress(int index) { if (index < 0 || index >= this.Length) - return IntPtr.Zero; + return 0; - return (IntPtr)Unsafe.AsPointer(ref this.Struct->Status[index]); + return (nint)Unsafe.AsPointer(ref this.Struct->Status[index]); } } /// /// This collection represents the status effects an actor is afflicted by. /// -public sealed partial class StatusList : IReadOnlyCollection, ICollection +public sealed partial class StatusList : IReadOnlyCollection, ICollection { /// - int IReadOnlyCollection.Count => this.Length; + int IReadOnlyCollection.Count => this.Length; /// int ICollection.Count => this.Length; @@ -130,17 +132,9 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio object ICollection.SyncRoot => this; /// - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { - for (var i = 0; i < this.Length; i++) - { - var status = this[i]; - - if (status == null || status.StatusId == 0) - continue; - - yield return status; - } + return new Enumerator(this); } /// @@ -155,4 +149,39 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio index++; } } + + private struct Enumerator(StatusList statusList) : IEnumerator + { + private int index = 0; + + public IStatus Current { get; private set; } + + object IEnumerator.Current => this.Current; + + public bool MoveNext() + { + if (this.index == statusList.Length) return false; + + for (; this.index < statusList.Length; this.index++) + { + var status = statusList[this.index]; + if (status != null && status.StatusId != 0) + { + this.Current = status; + return true; + } + } + + return false; + } + + public void Reset() + { + this.index = 0; + } + + public void Dispose() + { + } + } } From 9d0879148c740942523da5310c5b8039cb3be707 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 13 Nov 2025 19:05:25 +0100 Subject: [PATCH 021/164] Remove unused StatusEffect struct --- .../Game/ClientState/Structs/StatusEffect.cs | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 Dalamud/Game/ClientState/Structs/StatusEffect.cs diff --git a/Dalamud/Game/ClientState/Structs/StatusEffect.cs b/Dalamud/Game/ClientState/Structs/StatusEffect.cs deleted file mode 100644 index 2a60a7d3b..000000000 --- a/Dalamud/Game/ClientState/Structs/StatusEffect.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Dalamud.Game.ClientState.Structs; - -/// -/// Native memory representation of a FFXIV status effect. -/// -[StructLayout(LayoutKind.Sequential)] -public struct StatusEffect -{ - /// - /// The effect ID. - /// - public short EffectId; - - /// - /// How many stacks are present. - /// - public byte StackCount; - - /// - /// Additional parameters. - /// - public byte Param; - - /// - /// The duration remaining. - /// - public float Duration; - - /// - /// The ID of the actor that caused this effect. - /// - public int OwnerId; -} From 78ed4a2b01f2fb4f4ff86ea5eab2c7eca7e6b015 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 16 Nov 2025 15:55:35 -0800 Subject: [PATCH 022/164] feat: Dalamud RPC service A draft for a simple RPC service for Dalamud. Enables use of Dalamud URIs, to be added later. --- Dalamud.Test/Pipes/DalamudUriTests.cs | 107 +++++++++++ Dalamud/Dalamud.csproj | 1 + .../Networking/Pipes/Api/PluginLinkHandler.cs | 53 ++++++ Dalamud/Networking/Pipes/DalamudUri.cs | 102 +++++++++++ .../Pipes/Internal/ClientHelloService.cs | 94 ++++++++++ .../Pipes/Internal/LinkHandlerService.cs | 129 ++++++++++++++ Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs | 167 ++++++++++++++++++ Dalamud/Networking/Pipes/Rpc/RpcConnection.cs | 92 ++++++++++ .../Networking/Pipes/Rpc/RpcHostService.cs | 49 +++++ .../Pipes/Rpc/RpcServiceRegistry.cs | 85 +++++++++ Dalamud/Plugin/Services/IPluginLinkHandler.cs | 20 +++ Directory.Packages.props | 13 +- 12 files changed, 911 insertions(+), 1 deletion(-) create mode 100644 Dalamud.Test/Pipes/DalamudUriTests.cs create mode 100644 Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs create mode 100644 Dalamud/Networking/Pipes/DalamudUri.cs create mode 100644 Dalamud/Networking/Pipes/Internal/ClientHelloService.cs create mode 100644 Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcConnection.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcHostService.cs create mode 100644 Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs create mode 100644 Dalamud/Plugin/Services/IPluginLinkHandler.cs diff --git a/Dalamud.Test/Pipes/DalamudUriTests.cs b/Dalamud.Test/Pipes/DalamudUriTests.cs new file mode 100644 index 000000000..4977f3814 --- /dev/null +++ b/Dalamud.Test/Pipes/DalamudUriTests.cs @@ -0,0 +1,107 @@ +using System; +using System.Linq; + +using Dalamud.Networking.Pipes; +using Xunit; + +namespace Dalamud.Test.Pipes +{ + public class DalamudUriTests + { + [Theory] + [InlineData("https://www.google.com/", false)] + [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)] + public void ValidatesScheme(string uri, bool valid) + { + Action act = () => { _ = DalamudUri.FromUri(uri); }; + + var ex = Record.Exception(act); + if (valid) + { + Assert.Null(ex); + } + else + { + Assert.NotNull(ex); + Assert.IsType(ex); + } + } + + [Theory] + [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")] + [InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")] + [InlineData("dalamud://Test", "test")] + public void ExtractsNamespace(string uri, string expectedNamespace) + { + var dalamudUri = DalamudUri.FromUri(uri); + Assert.Equal(expectedNamespace, dalamudUri.Namespace); + } + + [Theory] + [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")] + [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")] + [InlineData("dalamud://foo/bar/baz", "/bar/baz")] + [InlineData("dalamud://foo/bar", "/bar")] + [InlineData("dalamud://foo/bar/", "/bar/")] + [InlineData("dalamud://foo/", "/")] + public void ExtractsPath(string uri, string expectedPath) + { + var dalamudUri = DalamudUri.FromUri(uri); + Assert.Equal(expectedPath, dalamudUri.Path); + } + + [Theory] + [InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")] + [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")] + [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")] + [InlineData("dalamud://foo/bar/baz", "/bar/baz")] + [InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")] + [InlineData("dalamud://foo/bar", "/bar")] + [InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")] + [InlineData("dalamud://foo/bar/", "/bar/")] + [InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")] + [InlineData("dalamud://foo/?cow=moo", "/?cow=moo")] + [InlineData("dalamud://foo/", "/")] + public void ExtractsData(string uri, string expectedData) + { + var dalamudUri = DalamudUri.FromUri(uri); + + Assert.Equal(expectedData, dalamudUri.Data); + } + + [Theory] + [InlineData("dalamud://foo/bar", 0)] + [InlineData("dalamud://foo/bar?cow=moo", 1)] + [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)] + [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)] + public void ExtractsQueryParams(string uri, int queryCount) + { + var dalamudUri = DalamudUri.FromUri(uri); + Assert.Equal(queryCount, dalamudUri.QueryParams.Count); + } + + [Theory] + [InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)] + [InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)] + [InlineData("dalamud://foo/bar/baz/qux/meh", 5)] + [InlineData("dalamud://foo/bar/baz/qux", 4)] + [InlineData("dalamud://foo/bar/baz", 3)] + [InlineData("dalamud://foo/bar/", 2)] + [InlineData("dalamud://foo/bar", 2)] + public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false) + { + var dalamudUri = DalamudUri.FromUri(uri); + var segments = dalamudUri.Segments; + + // First segment must always be `/` + Assert.Equal("/", segments[0]); + + Assert.Equal(segmentCount, segments.Length); + + if (finalSegmentEndsWithSlash) + { + Assert.EndsWith("/", segments.Last()); + } + } + } +} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1e5f9f586..849a5ce7f 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -81,6 +81,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs new file mode 100644 index 000000000..2c99901b4 --- /dev/null +++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs @@ -0,0 +1,53 @@ +using System.Linq; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Networking.Pipes.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +namespace Dalamud.Networking.Pipes.Api; + +/// +[PluginInterface] +[ServiceManager.ScopedService] +[ResolveVia] +public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler +{ + private readonly LinkHandlerService linkHandler; + private readonly LocalPlugin localPlugin; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin to bind this service to. + /// The central link handler. + internal PluginLinkHandler(LocalPlugin localPlugin, LinkHandlerService linkHandler) + { + this.linkHandler = linkHandler; + this.localPlugin = localPlugin; + + this.linkHandler.Register("plugin", this.HandleUri); + } + + /// + public event IPluginLinkHandler.PluginUriReceived? OnUriReceived; + + /// + public void DisposeService() + { + this.OnUriReceived = null; + this.linkHandler.Unregister("plugin", this.HandleUri); + } + + private void HandleUri(DalamudUri uri) + { + var target = uri.Path.Split("/").FirstOrDefault(); + if (target == null || !string.Equals(target, this.localPlugin.InternalName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + this.OnUriReceived?.Invoke(uri); + } +} diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Pipes/DalamudUri.cs new file mode 100644 index 000000000..03ad15af1 --- /dev/null +++ b/Dalamud/Networking/Pipes/DalamudUri.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Web; + +namespace Dalamud.Networking.Pipes; + +/// +/// A Dalamud Uri, in the format: +/// dalamud://{NAMESPACE}/{ARBITRARY} +/// +public record DalamudUri +{ + private readonly Uri rawUri; + + private DalamudUri(Uri uri) + { + if (uri.Scheme != "dalamud") + { + throw new ArgumentOutOfRangeException(nameof(uri), "URI must be of scheme dalamud."); + } + + this.rawUri = uri; + } + + /// + /// Gets the namespace that this URI should be routed to. Generally a high level component like "PluginInstaller". + /// + public string Namespace => this.rawUri.Authority; + + /// + /// Gets the raw (untargeted) path and query params for this URI. + /// + public string Data => + this.rawUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped); + + /// + /// Gets the raw (untargeted) path for this URI. + /// + public string Path => this.rawUri.AbsolutePath; + + /// + /// Gets a list of segments based on the provided Data element. + /// + public string[] Segments => this.GetDataSegments(); + + /// + /// Gets the raw query parameters for this URI, if any. + /// + public string Query => this.rawUri.Query; + + /// + /// Gets the query params (as a parsed NameValueCollection) in this URI. + /// + public NameValueCollection QueryParams => HttpUtility.ParseQueryString(this.Query); + + /// + /// Gets the fragment (if one is specified) in this URI. + /// + public string Fragment => this.rawUri.Fragment; + + /// + public override string ToString() => this.rawUri.ToString(); + + private string[] GetDataSegments() + { + // reimplementation of the System.URI#Segments, under MIT license. + var path = this.Path; + + var segments = new List(); + var current = 0; + while (current < path.Length) + { + var next = path.IndexOf('/', current); + if (next == -1) + { + next = path.Length - 1; + } + + segments.Add(path.Substring(current, (next - current) + 1)); + current = next + 1; + } + + return segments.ToArray(); + } + + /// + /// Build a DalamudURI from a given URI. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(Uri uri) + { + return new DalamudUri(uri); + } + + /// + /// Build a DalamudURI from a URI in string format. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); +} diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs new file mode 100644 index 000000000..cc06560bd --- /dev/null +++ b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs @@ -0,0 +1,94 @@ +using System.Threading.Tasks; + +using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Networking.Pipes.Rpc; +using Dalamud.Utility; + +namespace Dalamud.Networking.Pipes.Internal; + +/// +/// A minimal service to respond with information about this client. +/// +[ServiceManager.EarlyLoadedService] +internal sealed class ClientHelloService : IInternalDisposableService +{ + /// + /// Initializes a new instance of the class. + /// + /// Injected host service. + [ServiceManager.ServiceConstructor] + public ClientHelloService(RpcHostService rpcHostService) + { + rpcHostService.AddMethod("hello", this.HandleHello); + } + + /// + /// Handle a hello request. + /// + /// . + /// Respond with information. + public async Task HandleHello(ClientHelloRequest request) + { + var framework = await Service.GetAsync(); + var dalamud = await Service.GetAsync(); + var clientState = await Service.GetAsync(); + + var response = await framework.RunOnFrameworkThread(() => new ClientHelloResponse + { + ApiVersion = "1.0", + DalamudVersion = Util.GetScmVersion(), + GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", + PlayerName = clientState.IsLoggedIn ? clientState.LocalPlayer?.Name.ToString() ?? "Unknown" : null, + }); + + return response; + } + + /// + public void DisposeService() + { + } +} + +/// +/// A request from a client to say hello. +/// +internal record ClientHelloRequest +{ + /// + /// Gets the API version this client is expecting. + /// + public string ApiVersion { get; init; } = string.Empty; + + /// + /// Gets the user agent of the client. + /// + public string UserAgent { get; init; } = string.Empty; +} + +/// +/// A response from Dalamud to a hello request. +/// +internal record ClientHelloResponse +{ + /// + /// Gets the API version this server has offered. + /// + public string? ApiVersion { get; init; } + + /// + /// Gets the current Dalamud version. + /// + public string? DalamudVersion { get; init; } + + /// + /// Gets the current game version. + /// + public string? GameVersion { get; init; } + + /// + /// Gets or sets the player name, or null if the player isn't logged in. + /// + public string? PlayerName { get; set; } +} diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs new file mode 100644 index 000000000..79bb1e017 --- /dev/null +++ b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs @@ -0,0 +1,129 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +using Dalamud.Logging.Internal; +using Dalamud.Networking.Pipes.Rpc; + +namespace Dalamud.Networking.Pipes.Internal; + +/// +/// A service responsible for handling Dalamud URIs and dispatching them accordingly. +/// +[ServiceManager.EarlyLoadedService] +internal class LinkHandlerService : IInternalDisposableService +{ + private readonly ModuleLog log = new("LinkHandler"); + + // key: namespace (e.g. "plugin" or "PluginInstaller") -> list of handlers + private readonly ConcurrentDictionary>> handlers + = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Initializes a new instance of the class. + /// + /// The injected RPC host service. + [ServiceManager.ServiceConstructor] + public LinkHandlerService(RpcHostService rpcHostService) + { + rpcHostService.AddMethod("handleLink", this.HandleLinkCall); + } + + /// + public void DisposeService() + { + } + + /// + /// Register a handler for a namespace. All URIs with this namespace will be dispatched to the handler. + /// + /// The namespace to use for this subscription. + /// The command handler. + public void Register(string ns, Action handler) + { + if (string.IsNullOrWhiteSpace(ns)) + throw new ArgumentNullException(nameof(ns)); + + var list = this.handlers.GetOrAdd(ns, _ => []); + lock (list) + { + list.Add(handler); + } + + this.log.Verbose("Registered handler for {Namespace}", ns); + } + + /// + /// Unregister a handler. + /// + /// The namespace to use for this subscription. + /// The command handler. + public void Unregister(string ns, Action handler) + { + if (string.IsNullOrWhiteSpace(ns)) + return; + + if (!this.handlers.TryGetValue(ns, out var list)) + return; + + lock (list) + { + list.RemoveAll(x => x == handler); + } + + if (list.Count == 0) + this.handlers.TryRemove(ns, out _); + + this.log.Verbose("Unregistered handler for {Namespace}", ns); + } + + /// + /// Dispatch a URI to matching handlers. + /// + /// The URI to parse and dispatch. + public void Dispatch(DalamudUri uri) + { + this.log.Information("Received URI: {Uri}", uri.ToString()); + + var ns = uri.Namespace; + if (!this.handlers.TryGetValue(ns, out var list)) + return; + + Action[] snapshot; + lock (list) + { + snapshot = list.ToArray(); + } + + foreach (var h in snapshot) + { + try + { + h(uri); + } + catch (Exception e) + { + this.log.Warning(e, "Link handler threw for {UriPath}", uri.Path); + } + } + } + + /// + /// The RPC-invokable link handler. + /// + /// A plain-text URI to parse. + public void HandleLinkCall(string uri) + { + if (string.IsNullOrWhiteSpace(uri)) + return; + + try + { + var du = DalamudUri.FromUri(uri); + this.Dispatch(du); + } + catch (Exception) + { + // swallow parse errors; clients shouldn't crash the host + } + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs new file mode 100644 index 000000000..07dc9d96a --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs @@ -0,0 +1,167 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO.Pipes; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc. +/// +internal class PipeRpcHost : IDisposable +{ + private readonly ModuleLog log = new("RPC/Host"); + + private readonly RpcServiceRegistry registry = new(); + private readonly CancellationTokenSource cts = new(); + private readonly ConcurrentDictionary sessions = new(); + private Task? acceptLoopTask; + + /// + /// Initializes a new instance of the class. + /// + /// The pipe name to create. + public PipeRpcHost(string? pipeName = null) + { + // Default pipe name based on current process ID for uniqueness per Dalamud instance. + this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}"; + } + + /// + /// Gets the name of the named pipe this RPC host is using. + /// + public string PipeName { get; } + + /// Adds a local object exposing RPC methods callable by clients. + /// An arbitrary service object that will be introspected to add to RPC. + public void AddService(object service) => this.registry.AddService(service); + + /// + /// Adds a standalone JSON-RPC method callable by clients. + /// + /// The name to add. + /// The delegate that acts as the handler. + public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); + + /// Starts accepting client connections. + public void Start() + { + if (this.acceptLoopTask != null) return; + this.acceptLoopTask = Task.Run(this.AcceptLoopAsync); + } + + /// Invoke an RPC request on a specific client expecting a result. + /// The client ID to invoke. + /// The method to invoke. + /// Any arguments to invoke. + /// An optional return based on the specified RPC. + /// The expected response type. + public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) + { + if (!this.sessions.TryGetValue(clientId, out var session)) + throw new KeyNotFoundException($"No client {clientId}"); + + return session.Rpc.InvokeAsync(method, arguments); + } + + /// Send a notification to all connected clients (no response expected). + /// The method name to broadcast. + /// The arguments to broadcast. + /// Returns a Task when completed. + public Task BroadcastNotifyAsync(string method, params object[] arguments) + { + var list = this.sessions.Values; + var tasks = new List(list.Count); + foreach (var s in list) + { + tasks.Add(s.Rpc.NotifyAsync(method, arguments)); + } + + return Task.WhenAll(tasks); + } + + /// + /// Gets a list of connected client IDs. + /// + /// Connected client IDs. + public IReadOnlyCollection GetClientIds() => this.sessions.Keys.AsReadOnlyCollection(); + + /// + public void Dispose() + { + this.cts.Cancel(); + this.acceptLoopTask?.Wait(1000); + + foreach (var kv in this.sessions) + { + kv.Value.Dispose(); + } + + this.sessions.Clear(); + this.cts.Dispose(); + this.log.Information("PipeRpcHost disposed ({Pipe})", this.PipeName); + GC.SuppressFinalize(this); + } + + private PipeSecurity BuildPipeSecurity() + { + var ps = new PipeSecurity(); + ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User!, PipeAccessRights.FullControl, AccessControlType.Allow)); + + return ps; + } + + private async Task AcceptLoopAsync() + { + this.log.Information("PipeRpcHost starting on pipe {Pipe}", this.PipeName); + var token = this.cts.Token; + var security = this.BuildPipeSecurity(); + + while (!token.IsCancellationRequested) + { + NamedPipeServerStream? server = null; + try + { + server = NamedPipeServerStreamAcl.Create( + this.PipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Message, + PipeOptions.Asynchronous, + 65536, + 65536, + security); + + await server.WaitForConnectionAsync(token).ConfigureAwait(false); + + var session = new RpcConnection(server, this.registry); + this.sessions.TryAdd(session.Id, session); + + this.log.Debug("RPC connection created: {Id}", session.Id); + + _ = session.Completion.ContinueWith(t => + { + this.sessions.TryRemove(session.Id, out _); + this.log.Debug("RPC connection removed: {Id}", session.Id); + }, TaskScheduler.Default); + } + catch (OperationCanceledException) + { + server?.Dispose(); + break; + } + catch (Exception ex) + { + server?.Dispose(); + this.log.Error(ex, "Error in pipe accept loop"); + await Task.Delay(500, token).ConfigureAwait(false); + } + } + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs b/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs new file mode 100644 index 000000000..8e1c3a085 --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs @@ -0,0 +1,92 @@ +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +using Serilog; +using StreamJsonRpc; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// A single RPC client session connected via named pipe. +/// +internal class RpcConnection : IDisposable +{ + private readonly NamedPipeServerStream pipe; + private readonly RpcServiceRegistry registry; + private readonly CancellationTokenSource cts = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The named pipe that this connection will handle. + /// A registry of RPC services. + public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry) + { + this.Id = Guid.CreateVersion7(); + this.pipe = pipe; + this.registry = registry; + + var formatter = new JsonMessageFormatter(); + var handler = new HeaderDelimitedMessageHandler(pipe, pipe, formatter); + + this.Rpc = new JsonRpc(handler); + this.Rpc.AllowModificationWhileListening = true; + this.Rpc.Disconnected += this.OnDisconnected; + this.registry.Attach(this.Rpc); + + this.Rpc.StartListening(); + } + + /// + /// Gets the GUID for this connection. + /// + public Guid Id { get; } + + /// + /// Gets the JsonRpc instance for this connection. + /// + public JsonRpc Rpc { get; } + + /// + /// Gets a task that's called on RPC completion. + /// + public Task Completion => this.Rpc.Completion; + + /// + public void Dispose() + { + if (!this.cts.IsCancellationRequested) + { + this.cts.Cancel(); + } + + try + { + this.Rpc.Dispose(); + } + catch (Exception ex) + { + Log.Debug(ex, "Error disposing JsonRpc for client {Id}", this.Id); + } + + try + { + this.pipe.Dispose(); + } + catch (Exception ex) + { + Log.Debug(ex, "Error disposing pipe for client {Id}", this.Id); + } + + this.cts.Dispose(); + GC.SuppressFinalize(this); + } + + private void OnDisconnected(object? sender, JsonRpcDisconnectedEventArgs e) + { + Log.Debug("RPC client {Id} disconnected: {Reason}", this.Id, e.Description); + this.registry.Detach(this.Rpc); + this.Dispose(); + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs new file mode 100644 index 000000000..78df27323 --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs @@ -0,0 +1,49 @@ +using Dalamud.Logging.Internal; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// The Dalamud service repsonsible for hosting the RPC. +/// +[ServiceManager.EarlyLoadedService] +internal class RpcHostService : IServiceType, IInternalDisposableService +{ + private readonly ModuleLog log = new("RPC"); + private readonly PipeRpcHost host; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public RpcHostService() + { + this.host = new PipeRpcHost(); + this.host.Start(); + + this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName); + } + + /// + /// Gets the RPC host to drill down. + /// + public PipeRpcHost Host => this.host; + + /// + /// Add a new service Object to the RPC host. + /// + /// The object to add. + public void AddService(object service) => this.host.AddService(service); + + /// + /// Add a new standalone method to the RPC host. + /// + /// The method name to add. + /// The handler to add. + public void AddMethod(string name, Delegate handler) => this.host.AddMethod(name, handler); + + /// + public void DisposeService() + { + this.host.Dispose(); + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs new file mode 100644 index 000000000..71037d45e --- /dev/null +++ b/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Threading; + +using StreamJsonRpc; + +namespace Dalamud.Networking.Pipes.Rpc; + +/// +/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session. +/// New sessions get all previously registered targets; newly added targets are attached to all active sessions. +/// +internal class RpcServiceRegistry +{ + private readonly Lock sync = new(); + private readonly List targets = []; + private readonly List<(string Name, Delegate Handler)> methods = []; + private readonly List activeRpcs = []; + + /// + /// Registers a new local RPC target object. Its public JSON-RPC methods become callable by clients. + /// Adds to the registry and attaches it to all active RPC sessions. + /// + /// The service instance containing JSON-RPC callable methods to expose. + public void AddService(object service) + { + lock (this.sync) + { + this.targets.Add(service); + foreach (var rpc in this.activeRpcs) + { + rpc.AddLocalRpcTarget(service); + } + } + } + + /// + /// Registers a new standalone JSON-RPC method. + /// + /// The name of the method to add. + /// The handler to add. + public void AddMethod(string name, Delegate handler) + { + lock (this.sync) + { + this.methods.Add((name, handler)); + foreach (var rpc in this.activeRpcs) + { + rpc.AddLocalRpcMethod(name, handler); + } + } + } + + /// + /// Attaches a JsonRpc instance to the registry so it receives all existing service targets. + /// + /// The JsonRpc instance to attach and populate with current targets. + internal void Attach(JsonRpc rpc) + { + lock (this.sync) + { + this.activeRpcs.Add(rpc); + foreach (var t in this.targets) + { + rpc.AddLocalRpcTarget(t); + } + + foreach (var m in this.methods) + { + rpc.AddLocalRpcMethod(m.Name, m.Handler); + } + } + } + + /// + /// Detaches a JsonRpc instance from the registry (e.g. when a client disconnects). + /// + /// The JsonRpc instance being detached. + internal void Detach(JsonRpc rpc) + { + lock (this.sync) + { + this.activeRpcs.Remove(rpc); + } + } +} diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs new file mode 100644 index 000000000..57f772768 --- /dev/null +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -0,0 +1,20 @@ +using Dalamud.Networking.Pipes; + +namespace Dalamud.Plugin.Services; + +/// +/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the +/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. +/// +public interface IPluginLinkHandler +{ + /// + /// A delegate containing the received URI. + /// + delegate void PluginUriReceived(DalamudUri uri); + + /// + /// The event fired when a URI targeting this plugin is received. + /// + event PluginUriReceived OnUriReceived; +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 91875e63e..903a8ee88 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,11 +3,13 @@ true false + + @@ -22,26 +24,35 @@ + + + + + + + + + @@ -54,4 +65,4 @@ - \ No newline at end of file + From 4937a2f4bd2e551669e7d158b44d0f6e681ffc1d Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 16 Nov 2025 18:14:02 -0800 Subject: [PATCH 023/164] CR changes --- .../Networking/Pipes/Api/PluginLinkHandler.cs | 4 ++- .../Pipes/Internal/LinkHandlerService.cs | 36 ++++--------------- Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs | 2 +- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 5 ++- 4 files changed, 15 insertions(+), 32 deletions(-) diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs index 2c99901b4..d8f43907c 100644 --- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs @@ -1,5 +1,6 @@ using System.Linq; +using Dalamud.Console; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Networking.Pipes.Internal; @@ -43,7 +44,8 @@ public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler private void HandleUri(DalamudUri uri) { var target = uri.Path.Split("/").FirstOrDefault(); - if (target == null || !string.Equals(target, this.localPlugin.InternalName, StringComparison.OrdinalIgnoreCase)) + var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName); + if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase)) { return; } diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs index 79bb1e017..3cc4af9f4 100644 --- a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs +++ b/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Dalamud.Logging.Internal; using Dalamud.Networking.Pipes.Rpc; +using Dalamud.Utility; namespace Dalamud.Networking.Pipes.Internal; @@ -65,10 +66,7 @@ internal class LinkHandlerService : IInternalDisposableService if (!this.handlers.TryGetValue(ns, out var list)) return; - lock (list) - { - list.RemoveAll(x => x == handler); - } + list.RemoveAll(x => x == handler); if (list.Count == 0) this.handlers.TryRemove(ns, out _); @@ -85,25 +83,12 @@ internal class LinkHandlerService : IInternalDisposableService this.log.Information("Received URI: {Uri}", uri.ToString()); var ns = uri.Namespace; - if (!this.handlers.TryGetValue(ns, out var list)) + if (!this.handlers.TryGetValue(ns, out var actions)) return; - Action[] snapshot; - lock (list) + foreach (var h in actions) { - snapshot = list.ToArray(); - } - - foreach (var h in snapshot) - { - try - { - h(uri); - } - catch (Exception e) - { - this.log.Warning(e, "Link handler threw for {UriPath}", uri.Path); - } + h.InvokeSafely(uri); } } @@ -116,14 +101,7 @@ internal class LinkHandlerService : IInternalDisposableService if (string.IsNullOrWhiteSpace(uri)) return; - try - { - var du = DalamudUri.FromUri(uri); - this.Dispatch(du); - } - catch (Exception) - { - // swallow parse errors; clients shouldn't crash the host - } + var du = DalamudUri.FromUri(uri); + this.Dispatch(du); } } diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs index 07dc9d96a..ad1cc72cd 100644 --- a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs +++ b/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs @@ -53,7 +53,7 @@ internal class PipeRpcHost : IDisposable public void Start() { if (this.acceptLoopTask != null) return; - this.acceptLoopTask = Task.Run(this.AcceptLoopAsync); + this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); } /// Invoke an RPC request on a specific client expecting a result. diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 57f772768..22139814d 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -1,4 +1,6 @@ -using Dalamud.Networking.Pipes; +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Networking.Pipes; namespace Dalamud.Plugin.Services; @@ -6,6 +8,7 @@ namespace Dalamud.Plugin.Services; /// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the /// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// +[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] public interface IPluginLinkHandler { /// From 19a3926051ce6aa30ac907a5fb7201536b971452 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 16 Nov 2025 21:35:33 -0800 Subject: [PATCH 024/164] Better hello message --- .../Networking/Pipes/Api/PluginLinkHandler.cs | 1 + .../Pipes/Internal/ClientHelloService.cs | 45 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs index d8f43907c..78fbb0d82 100644 --- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs @@ -6,6 +6,7 @@ using Dalamud.IoC.Internal; using Dalamud.Networking.Pipes.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +#pragma warning disable DAL_RPC namespace Dalamud.Networking.Pipes.Api; diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs index cc06560bd..9c182561e 100644 --- a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs +++ b/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs @@ -1,10 +1,13 @@ using System.Threading.Tasks; +using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Networking.Pipes.Rpc; using Dalamud.Utility; +using Lumina.Excel.Sheets; + namespace Dalamud.Networking.Pipes.Internal; /// @@ -30,25 +33,49 @@ internal sealed class ClientHelloService : IInternalDisposableService /// Respond with information. public async Task HandleHello(ClientHelloRequest request) { - var framework = await Service.GetAsync(); var dalamud = await Service.GetAsync(); - var clientState = await Service.GetAsync(); - var response = await framework.RunOnFrameworkThread(() => new ClientHelloResponse + return new ClientHelloResponse { ApiVersion = "1.0", DalamudVersion = Util.GetScmVersion(), GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", - PlayerName = clientState.IsLoggedIn ? clientState.LocalPlayer?.Name.ToString() ?? "Unknown" : null, - }); - - return response; + ClientIdentifier = await this.GetClientIdentifier(), + }; } /// public void DisposeService() { } + + private async Task GetClientIdentifier() + { + var framework = await Service.GetAsync(); + var clientState = await Service.GetAsync(); + var dataManager = await Service.GetAsync(); + + var clientIdentifier = $"FFXIV Process ${Environment.ProcessId}"; + + await framework.RunOnFrameworkThread(() => + { + if (clientState.IsLoggedIn) + { + var player = clientState.LocalPlayer; + if (player != null) + { + var world = dataManager.GetExcelSheet().GetRow(player.HomeWorld.RowId); + clientIdentifier = $"Logged in as {player.Name.TextValue} @ {world.Name.ExtractText()}"; + } + } + else + { + clientIdentifier = "On login screen"; + } + }); + + return clientIdentifier; + } } /// @@ -88,7 +115,7 @@ internal record ClientHelloResponse public string? GameVersion { get; init; } /// - /// Gets or sets the player name, or null if the player isn't logged in. + /// Gets an identifier for this client. /// - public string? PlayerName { get; set; } + public string? ClientIdentifier { get; init; } } From cc9191657453986cb6451f63f3238b3b29b7e26c Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 00:52:30 +0100 Subject: [PATCH 025/164] Fix bad merge --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 6 ++---- Dalamud/Game/ClientState/Party/PartyList.cs | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index a76b520af..4d5fc2aab 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -9,8 +9,6 @@ using Dalamud.Plugin.Services; using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; - -using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; namespace Dalamud.Game.ClientState.Buddy; @@ -74,7 +72,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } } - private unsafe CSBuddy* BuddyListStruct => &UIState.Instance()->Buddy; + private unsafe CSBuddy* BuddyListStruct => &CSUIState.Instance()->Buddy; /// public IBuddyMember? this[int index] @@ -113,7 +111,7 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList if (address == 0) return null; - if (this.clientState.LocalContentId == 0) + if (this.playerState.ContentId == 0) return null; var buddy = new BuddyMember((CSBuddyMember*)address); diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 1a5177b10..1dede1dd3 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -93,7 +93,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreatePartyMemberReference(nint address) { - if (this.clientState.LocalContentId == 0) + if (this.playerState.ContentId == 0) return null; if (address == 0) @@ -114,7 +114,7 @@ internal sealed unsafe partial class PartyList : IServiceType, IPartyList /// public IPartyMember? CreateAllianceMemberReference(nint address) { - if (this.clientState.LocalContentId == 0) + if (this.playerState.ContentId == 0) return null; if (address == 0) From 6a69a6e197ac6761d6c3d39fe3a879c852151cf6 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 18 Nov 2025 00:58:08 +0100 Subject: [PATCH 026/164] Fix some warnings --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 2 +- Dalamud/GlobalSuppressions.cs | 1 + Dalamud/Networking/Pipes/DalamudUri.cs | 34 +++++++++---------- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 5 +-- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 4d5fc2aab..b8e4c0fcc 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -8,8 +8,8 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; using CSBuddy = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy; -using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; using CSBuddyMember = FFXIVClientStructs.FFXIV.Client.Game.UI.Buddy.BuddyMember; +using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; namespace Dalamud.Game.ClientState.Buddy; diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs index 8a9d31b12..35754eb04 100644 --- a/Dalamud/GlobalSuppressions.cs +++ b/Dalamud/GlobalSuppressions.cs @@ -21,6 +21,7 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")] [assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:FileMayOnlyContainASingleType", Justification = "This would be nice, but a big refactor")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "I don't like this one so much")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1108:BlockStatementsMustNotContainEmbeddedComments", Justification = "I like having comments in blocks")] // ImRAII stuff [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Pipes/DalamudUri.cs index 03ad15af1..7e639cbbe 100644 --- a/Dalamud/Networking/Pipes/DalamudUri.cs +++ b/Dalamud/Networking/Pipes/DalamudUri.cs @@ -61,6 +61,23 @@ public record DalamudUri /// public override string ToString() => this.rawUri.ToString(); + /// + /// Build a DalamudURI from a given URI. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(Uri uri) + { + return new DalamudUri(uri); + } + + /// + /// Build a DalamudURI from a URI in string format. + /// + /// The URI to convert to a Dalamud URI. + /// Returns a DalamudUri. + public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); + private string[] GetDataSegments() { // reimplementation of the System.URI#Segments, under MIT license. @@ -82,21 +99,4 @@ public record DalamudUri return segments.ToArray(); } - - /// - /// Build a DalamudURI from a given URI. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(Uri uri) - { - return new DalamudUri(uri); - } - - /// - /// Build a DalamudURI from a URI in string format. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); } diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 22139814d..5d2d32728 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -6,7 +6,7 @@ namespace Dalamud.Plugin.Services; /// /// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the -/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. +/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// [Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] public interface IPluginLinkHandler @@ -14,7 +14,8 @@ public interface IPluginLinkHandler /// /// A delegate containing the received URI. /// - delegate void PluginUriReceived(DalamudUri uri); + /// The URI opened by the user. + public delegate void PluginUriReceived(DalamudUri uri); /// /// The event fired when a URI targeting this plugin is received. From 71927a8bf6fc0135e59fbd2e9e515aacb5e7d75f Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 18 Nov 2025 15:18:16 -0800 Subject: [PATCH 027/164] feat: Add unix sockets - Unix sockets run parallel to Named Pipes - Named Pipes will only run on non-Wine - If the game crashes, the next run will clean up an orphaned socket. - Restructure RPC to be a bit tidier --- .../{Pipes => Rpc}/DalamudUriTests.cs | 5 +- .../Networking/Pipes/Rpc/RpcHostService.cs | 49 ---- .../{Pipes => Rpc}/Api/PluginLinkHandler.cs | 6 +- .../{Pipes => Rpc/Model}/DalamudUri.cs | 2 +- .../{Pipes => }/Rpc/RpcConnection.cs | 23 +- Dalamud/Networking/Rpc/RpcHostService.cs | 105 +++++++++ .../{Pipes => }/Rpc/RpcServiceRegistry.cs | 2 +- .../Service}/ClientHelloService.cs | 3 +- .../Service}/LinkHandlerService.cs | 4 +- .../Networking/Rpc/Transport/IRpcTransport.cs | 32 +++ .../Transport/PipeRpcTransport.cs} | 30 +-- .../Rpc/Transport/UnixRpcTransport.cs | 223 ++++++++++++++++++ Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +- Dalamud/Utility/UnixSocketUtil.cs | 92 ++++++++ 14 files changed, 487 insertions(+), 91 deletions(-) rename Dalamud.Test/{Pipes => Rpc}/DalamudUriTests.cs (98%) delete mode 100644 Dalamud/Networking/Pipes/Rpc/RpcHostService.cs rename Dalamud/Networking/{Pipes => Rpc}/Api/PluginLinkHandler.cs (93%) rename Dalamud/Networking/{Pipes => Rpc/Model}/DalamudUri.cs (98%) rename Dalamud/Networking/{Pipes => }/Rpc/RpcConnection.cs (76%) create mode 100644 Dalamud/Networking/Rpc/RpcHostService.cs rename Dalamud/Networking/{Pipes => }/Rpc/RpcServiceRegistry.cs (98%) rename Dalamud/Networking/{Pipes/Internal => Rpc/Service}/ClientHelloService.cs (97%) rename Dalamud/Networking/{Pipes/Internal => Rpc/Service}/LinkHandlerService.cs (97%) create mode 100644 Dalamud/Networking/Rpc/Transport/IRpcTransport.cs rename Dalamud/Networking/{Pipes/Rpc/PipeRpcHost.cs => Rpc/Transport/PipeRpcTransport.cs} (81%) create mode 100644 Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs create mode 100644 Dalamud/Utility/UnixSocketUtil.cs diff --git a/Dalamud.Test/Pipes/DalamudUriTests.cs b/Dalamud.Test/Rpc/DalamudUriTests.cs similarity index 98% rename from Dalamud.Test/Pipes/DalamudUriTests.cs rename to Dalamud.Test/Rpc/DalamudUriTests.cs index 4977f3814..b371a5698 100644 --- a/Dalamud.Test/Pipes/DalamudUriTests.cs +++ b/Dalamud.Test/Rpc/DalamudUriTests.cs @@ -1,10 +1,11 @@ using System; using System.Linq; -using Dalamud.Networking.Pipes; +using Dalamud.Networking.Rpc.Model; + using Xunit; -namespace Dalamud.Test.Pipes +namespace Dalamud.Test.Rpc { public class DalamudUriTests { diff --git a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs b/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs deleted file mode 100644 index 78df27323..000000000 --- a/Dalamud/Networking/Pipes/Rpc/RpcHostService.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Dalamud.Logging.Internal; - -namespace Dalamud.Networking.Pipes.Rpc; - -/// -/// The Dalamud service repsonsible for hosting the RPC. -/// -[ServiceManager.EarlyLoadedService] -internal class RpcHostService : IServiceType, IInternalDisposableService -{ - private readonly ModuleLog log = new("RPC"); - private readonly PipeRpcHost host; - - /// - /// Initializes a new instance of the class. - /// - [ServiceManager.ServiceConstructor] - public RpcHostService() - { - this.host = new PipeRpcHost(); - this.host.Start(); - - this.log.Information("RpcHostService started on pipe {Pipe}", this.host.PipeName); - } - - /// - /// Gets the RPC host to drill down. - /// - public PipeRpcHost Host => this.host; - - /// - /// Add a new service Object to the RPC host. - /// - /// The object to add. - public void AddService(object service) => this.host.AddService(service); - - /// - /// Add a new standalone method to the RPC host. - /// - /// The method name to add. - /// The handler to add. - public void AddMethod(string name, Delegate handler) => this.host.AddMethod(name, handler); - - /// - public void DisposeService() - { - this.host.Dispose(); - } -} diff --git a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs similarity index 93% rename from Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs rename to Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs index 78fbb0d82..e9372bf0e 100644 --- a/Dalamud/Networking/Pipes/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs @@ -3,12 +3,14 @@ using Dalamud.Console; using Dalamud.IoC; using Dalamud.IoC.Internal; -using Dalamud.Networking.Pipes.Internal; +using Dalamud.Networking.Rpc.Model; +using Dalamud.Networking.Rpc.Service; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; + #pragma warning disable DAL_RPC -namespace Dalamud.Networking.Pipes.Api; +namespace Dalamud.Networking.Rpc.Api; /// [PluginInterface] diff --git a/Dalamud/Networking/Pipes/DalamudUri.cs b/Dalamud/Networking/Rpc/Model/DalamudUri.cs similarity index 98% rename from Dalamud/Networking/Pipes/DalamudUri.cs rename to Dalamud/Networking/Rpc/Model/DalamudUri.cs index 7e639cbbe..852478762 100644 --- a/Dalamud/Networking/Pipes/DalamudUri.cs +++ b/Dalamud/Networking/Rpc/Model/DalamudUri.cs @@ -2,7 +2,7 @@ using System.Collections.Specialized; using System.Web; -namespace Dalamud.Networking.Pipes; +namespace Dalamud.Networking.Rpc.Model; /// /// A Dalamud Uri, in the format: diff --git a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs b/Dalamud/Networking/Rpc/RpcConnection.cs similarity index 76% rename from Dalamud/Networking/Pipes/Rpc/RpcConnection.cs rename to Dalamud/Networking/Rpc/RpcConnection.cs index 8e1c3a085..5288948eb 100644 --- a/Dalamud/Networking/Pipes/Rpc/RpcConnection.cs +++ b/Dalamud/Networking/Rpc/RpcConnection.cs @@ -1,34 +1,37 @@ -using System.IO.Pipes; +using System.IO; using System.Threading; using System.Threading.Tasks; +using Dalamud.Networking.Rpc.Service; + using Serilog; + using StreamJsonRpc; -namespace Dalamud.Networking.Pipes.Rpc; +namespace Dalamud.Networking.Rpc; /// -/// A single RPC client session connected via named pipe. +/// A single RPC client session connected via a stream (named pipe or Unix socket). /// internal class RpcConnection : IDisposable { - private readonly NamedPipeServerStream pipe; + private readonly Stream stream; private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); /// /// Initializes a new instance of the class. /// - /// The named pipe that this connection will handle. + /// The stream that this connection will handle. /// A registry of RPC services. - public RpcConnection(NamedPipeServerStream pipe, RpcServiceRegistry registry) + public RpcConnection(Stream stream, RpcServiceRegistry registry) { this.Id = Guid.CreateVersion7(); - this.pipe = pipe; + this.stream = stream; this.registry = registry; var formatter = new JsonMessageFormatter(); - var handler = new HeaderDelimitedMessageHandler(pipe, pipe, formatter); + var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter); this.Rpc = new JsonRpc(handler); this.Rpc.AllowModificationWhileListening = true; @@ -72,11 +75,11 @@ internal class RpcConnection : IDisposable try { - this.pipe.Dispose(); + this.stream.Dispose(); } catch (Exception ex) { - Log.Debug(ex, "Error disposing pipe for client {Id}", this.Id); + Log.Debug(ex, "Error disposing stream for client {Id}", this.Id); } this.cts.Dispose(); diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs new file mode 100644 index 000000000..f164992eb --- /dev/null +++ b/Dalamud/Networking/Rpc/RpcHostService.cs @@ -0,0 +1,105 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Networking.Rpc.Transport; +using Dalamud.Utility; + +namespace Dalamud.Networking.Rpc; + +/// +/// The Dalamud service repsonsible for hosting the RPC. +/// +[ServiceManager.EarlyLoadedService] +internal class RpcHostService : IServiceType, IInternalDisposableService +{ + private readonly ModuleLog log = new("RPC"); + private readonly RpcServiceRegistry registry = new(); + private readonly List transports = []; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + public RpcHostService() + { + this.StartUnixTransport(); + this.StartPipeTransport(); + + if (this.transports.Count == 0) + { + this.log.Warning("No RPC hosts could be started on this platform"); + } + } + + /// + /// Gets all active RPC transports. + /// + public IReadOnlyList Transports => this.transports; + + /// + /// Add a new service Object to the RPC host. + /// + /// The object to add. + public void AddService(object service) => this.registry.AddService(service); + + /// + /// Add a new standalone method to the RPC host. + /// + /// The method name to add. + /// The handler to add. + public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); + + /// + public void DisposeService() + { + foreach (var host in this.transports) + { + host.Dispose(); + } + + this.transports.Clear(); + } + + /// + public async Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) + { + var clients = this.transports.SelectMany(t => t.Connections).ToImmutableDictionary(); + + if (!clients.TryGetValue(clientId, out var session)) + throw new KeyNotFoundException($"No client {clientId}"); + + return await session.Rpc.InvokeAsync(method, arguments).ConfigureAwait(false); + } + + /// + public async Task BroadcastNotifyAsync(string method, params object[] arguments) + { + await foreach (var transport in this.transports.ToAsyncEnumerable().ConfigureAwait(false)) + { + await transport.BroadcastNotifyAsync(method, arguments).ConfigureAwait(false); + } + } + + private void StartUnixTransport() + { + var transport = new UnixRpcTransport(this.registry); + this.transports.Add(transport); + transport.Start(); + this.log.Information("RpcHostService started Unix socket host: {Socket}", transport.SocketPath); + } + + private void StartPipeTransport() + { + // Wine doesn't support named pipes. + if (Util.IsWine()) + return; + + var transport = new PipeRpcTransport(this.registry); + this.transports.Add(transport); + transport.Start(); + this.log.Information("RpcHostService started named pipe host: {Pipe}", transport.PipeName); + } +} diff --git a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs similarity index 98% rename from Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs rename to Dalamud/Networking/Rpc/RpcServiceRegistry.cs index 71037d45e..6daea14bf 100644 --- a/Dalamud/Networking/Pipes/Rpc/RpcServiceRegistry.cs +++ b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs @@ -3,7 +3,7 @@ using System.Threading; using StreamJsonRpc; -namespace Dalamud.Networking.Pipes.Rpc; +namespace Dalamud.Networking.Rpc; /// /// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session. diff --git a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs similarity index 97% rename from Dalamud/Networking/Pipes/Internal/ClientHelloService.cs rename to Dalamud/Networking/Rpc/Service/ClientHelloService.cs index 9c182561e..041bc135f 100644 --- a/Dalamud/Networking/Pipes/Internal/ClientHelloService.cs +++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs @@ -3,12 +3,11 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState; -using Dalamud.Networking.Pipes.Rpc; using Dalamud.Utility; using Lumina.Excel.Sheets; -namespace Dalamud.Networking.Pipes.Internal; +namespace Dalamud.Networking.Rpc.Service; /// /// A minimal service to respond with information about this client. diff --git a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs similarity index 97% rename from Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs rename to Dalamud/Networking/Rpc/Service/LinkHandlerService.cs index 3cc4af9f4..9fa311ede 100644 --- a/Dalamud/Networking/Pipes/Internal/LinkHandlerService.cs +++ b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using Dalamud.Logging.Internal; -using Dalamud.Networking.Pipes.Rpc; +using Dalamud.Networking.Rpc.Model; using Dalamud.Utility; -namespace Dalamud.Networking.Pipes.Internal; +namespace Dalamud.Networking.Rpc.Service; /// /// A service responsible for handling Dalamud URIs and dispatching them accordingly. diff --git a/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs new file mode 100644 index 000000000..ad7578eb4 --- /dev/null +++ b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Dalamud.Networking.Rpc.Transport; + +/// +/// Interface for RPC host implementations (named pipes or Unix sockets). +/// +internal interface IRpcTransport : IDisposable +{ + /// + /// Gets a list of active RPC connections. + /// + IReadOnlyDictionary Connections { get; } + + /// Starts accepting client connections. + void Start(); + + /// Invoke an RPC request on a specific client expecting a result. + /// The client ID to invoke. + /// The method to invoke. + /// Any arguments to invoke. + /// An optional return based on the specified RPC. + /// The expected response type. + Task InvokeClientAsync(Guid clientId, string method, params object[] arguments); + + /// Send a notification to all connected clients (no response expected). + /// The method name to broadcast. + /// The arguments to broadcast. + /// Returns a Task when completed. + Task BroadcastNotifyAsync(string method, params object[] arguments); +} diff --git a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs similarity index 81% rename from Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs rename to Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs index ad1cc72cd..0cefeb853 100644 --- a/Dalamud/Networking/Pipes/Rpc/PipeRpcHost.cs +++ b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs @@ -9,26 +9,28 @@ using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Utility; -namespace Dalamud.Networking.Pipes.Rpc; +namespace Dalamud.Networking.Rpc.Transport; /// /// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc. /// -internal class PipeRpcHost : IDisposable +internal class PipeRpcTransport : IRpcTransport { private readonly ModuleLog log = new("RPC/Host"); - private readonly RpcServiceRegistry registry = new(); + private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); private readonly ConcurrentDictionary sessions = new(); private Task? acceptLoopTask; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The RPC service registry to use. /// The pipe name to create. - public PipeRpcHost(string? pipeName = null) + public PipeRpcTransport(RpcServiceRegistry registry, string? pipeName = null) { + this.registry = registry; // Default pipe name based on current process ID for uniqueness per Dalamud instance. this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}"; } @@ -38,16 +40,8 @@ internal class PipeRpcHost : IDisposable /// public string PipeName { get; } - /// Adds a local object exposing RPC methods callable by clients. - /// An arbitrary service object that will be introspected to add to RPC. - public void AddService(object service) => this.registry.AddService(service); - - /// - /// Adds a standalone JSON-RPC method callable by clients. - /// - /// The name to add. - /// The delegate that acts as the handler. - public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); + /// + public IReadOnlyDictionary Connections => this.sessions; /// Starts accepting client connections. public void Start() @@ -86,12 +80,6 @@ internal class PipeRpcHost : IDisposable return Task.WhenAll(tasks); } - /// - /// Gets a list of connected client IDs. - /// - /// Connected client IDs. - public IReadOnlyCollection GetClientIds() => this.sessions.Keys.AsReadOnlyCollection(); - /// public void Dispose() { diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs new file mode 100644 index 000000000..3019f5aaf --- /dev/null +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -0,0 +1,223 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Networking.Rpc.Transport; + +/// +/// Simple multi-client JSON-RPC Unix socket host using StreamJsonRpc. +/// +internal class UnixRpcTransport : IRpcTransport +{ + private readonly ModuleLog log = new("RPC/UnixHost"); + + private readonly RpcServiceRegistry registry; + private readonly CancellationTokenSource cts = new(); + private readonly ConcurrentDictionary sessions = new(); + private readonly string? cleanupSocketDirectory; + + private Task? acceptLoopTask; + private Socket? listenSocket; + + /// + /// Initializes a new instance of the class. + /// + /// The RPC service registry to use. + /// The Unix socket path to create. If null, defaults to a path based on process ID. + public UnixRpcTransport(RpcServiceRegistry registry, string? socketPath = null) + { + this.registry = registry; + + if (socketPath != null) + { + this.SocketPath = socketPath; + } + else + { + var dalamudConfigPath = Service.Get().StartInfo.ConfigurationPath; + var dalamudHome = Path.GetDirectoryName(dalamudConfigPath); + var socketName = $"DalamudRPC.{Environment.ProcessId}.sock"; + + if (dalamudHome == null) + { + this.SocketPath = Path.Combine(Path.GetTempPath(), socketName); + this.log.Warning("Dalamud home is empty! UDS socket will be in temp."); + } + else + { + this.SocketPath = Path.Combine(dalamudHome, socketName); + this.cleanupSocketDirectory = dalamudHome; + } + } + } + + /// + /// Gets the path of the Unix socket this RPC host is using. + /// + public string SocketPath { get; } + + /// + public IReadOnlyDictionary Connections => this.sessions; + + /// Starts accepting client connections. + public void Start() + { + if (this.acceptLoopTask != null) return; + + // Make the directory for the socket if it doesn't exist + var socketDir = Path.GetDirectoryName(this.SocketPath); + if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir)) + { + try + { + Directory.CreateDirectory(socketDir); + } + catch (Exception ex) + { + this.log.Error(ex, "Failed to create socket directory: {Path}", socketDir); + return; + } + } + + // Delete existing socket for this PID, if it exists. + if (File.Exists(this.SocketPath)) + { + try + { + File.Delete(this.SocketPath); + } + catch (Exception ex) + { + this.log.Warning(ex, "Failed to delete existing socket file: {Path}", this.SocketPath); + } + } + + this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); + + // note: needs to be run _after_ we're alive so that we don't delete our own socket. + if (this.cleanupSocketDirectory != null) + { + Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory)); + } + } + + /// Invoke an RPC request on a specific client expecting a result. + /// The client ID to invoke. + /// The method to invoke. + /// Any arguments to invoke. + /// An optional return based on the specified RPC. + /// The expected response type. + public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) + { + if (!this.sessions.TryGetValue(clientId, out var session)) + throw new KeyNotFoundException($"No client {clientId}"); + + return session.Rpc.InvokeAsync(method, arguments); + } + + /// Send a notification to all connected clients (no response expected). + /// The method name to broadcast. + /// The arguments to broadcast. + /// Returns a Task when completed. + public Task BroadcastNotifyAsync(string method, params object[] arguments) + { + var list = this.sessions.Values; + var tasks = new List(list.Count); + foreach (var s in list) + { + tasks.Add(s.Rpc.NotifyAsync(method, arguments)); + } + + return Task.WhenAll(tasks); + } + + /// + public void Dispose() + { + this.cts.Cancel(); + this.acceptLoopTask?.Wait(1000); + + foreach (var kv in this.sessions) + { + kv.Value.Dispose(); + } + + this.sessions.Clear(); + + this.listenSocket?.Dispose(); + + if (File.Exists(this.SocketPath)) + { + try + { + File.Delete(this.SocketPath); + } + catch (Exception ex) + { + this.log.Warning(ex, "Failed to delete socket file on dispose: {Path}", this.SocketPath); + } + } + + this.cts.Dispose(); + this.log.Information("UnixRpcHost disposed ({Socket})", this.SocketPath); + GC.SuppressFinalize(this); + } + + private async Task AcceptLoopAsync() + { + this.log.Information("UnixRpcHost starting on socket {Socket}", this.SocketPath); + var token = this.cts.Token; + + try + { + var endpoint = new UnixDomainSocketEndPoint(this.SocketPath); + this.listenSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + this.listenSocket.Bind(endpoint); + this.listenSocket.Listen(128); + + while (!token.IsCancellationRequested) + { + Socket? clientSocket = null; + try + { + clientSocket = await this.listenSocket.AcceptAsync(token).ConfigureAwait(false); + + var stream = new NetworkStream(clientSocket, ownsSocket: true); + var session = new RpcConnection(stream, this.registry); + this.sessions.TryAdd(session.Id, session); + + this.log.Debug("RPC connection created: {Id}", session.Id); + + _ = session.Completion.ContinueWith(t => + { + this.sessions.TryRemove(session.Id, out _); + this.log.Debug("RPC connection removed: {Id}", session.Id); + }, TaskScheduler.Default); + } + catch (OperationCanceledException) + { + clientSocket?.Dispose(); + break; + } + catch (Exception ex) + { + clientSocket?.Dispose(); + this.log.Error(ex, "Error in socket accept loop"); + await Task.Delay(500, token).ConfigureAwait(false); + } + } + } + catch (Exception ex) + { + this.log.Error(ex, "Fatal error in Unix socket accept loop"); + } + } +} diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 5d2d32728..bff5c8ba2 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -using Dalamud.Networking.Pipes; +using Dalamud.Networking.Rpc.Model; namespace Dalamud.Plugin.Services; diff --git a/Dalamud/Utility/UnixSocketUtil.cs b/Dalamud/Utility/UnixSocketUtil.cs new file mode 100644 index 000000000..46bb05c74 --- /dev/null +++ b/Dalamud/Utility/UnixSocketUtil.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Net.Sockets; +using System.Threading.Tasks; + +using Serilog; + +namespace Dalamud.Utility; + +/// +/// A set of utilities to help manage Unix sockets. +/// +internal static class UnixSocketUtil +{ + // Default probe timeout in milliseconds. + private const int DefaultProbeMs = 200; + + /// + /// Test whether a Unix socket is alive/listening. + /// + /// The path to test. + /// How long to wait for a connection success. + /// A task result representing if a socket is alive or not. + public static async Task IsSocketAlive(string path, int timeoutMs = DefaultProbeMs) + { + if (string.IsNullOrEmpty(path)) return false; + var endpoint = new UnixDomainSocketEndPoint(path); + using var client = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + + var connectTask = client.ConnectAsync(endpoint); + var completed = await Task.WhenAny(connectTask, Task.Delay(timeoutMs)).ConfigureAwait(false); + + if (completed == connectTask) + { + // Connected or failed very quickly. If the task is successful, the socket is alive. + if (connectTask.IsCompletedSuccessfully) + { + try + { + client.Shutdown(SocketShutdown.Both); + } + catch + { + // ignored + } + + return true; + } + } + + return false; + } + + /// + /// Find and remove stale Dalamud RPC sockets. + /// + /// The directory to scan for stale sockets. + /// The timeout to wait for a connection attempt to succeed. + /// A task that executes when sockets are purged. + public static async Task CleanStaleSockets(string directory, int probeTimeoutMs = DefaultProbeMs) + { + if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return; + + foreach (var file in Directory.EnumerateFiles(directory, "DalamudRPC.*.sock", SearchOption.TopDirectoryOnly)) + { + // we don't need to check ourselves. + if (file.Contains(Environment.ProcessId.ToString())) continue; + + bool shouldDelete; + + try + { + shouldDelete = !await IsSocketAlive(file, probeTimeoutMs); + } + catch + { + shouldDelete = true; + } + + if (shouldDelete) + { + try + { + File.Delete(file); + } + catch (Exception ex) + { + Log.Error(ex, "Could not delete stale socket file: {File}", file); + } + } + } + } +} From 01d8fc0c7ea177bdbe46ec5ebf8c9cd910556852 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 18 Nov 2025 15:57:37 -0800 Subject: [PATCH 028/164] fix: log tweaks - also fix a boot failure --- Dalamud/Networking/Rpc/RpcHostService.cs | 4 ++-- Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs | 3 +-- Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs | 3 +-- Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs | 4 +++- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs index f164992eb..60152b355 100644 --- a/Dalamud/Networking/Rpc/RpcHostService.cs +++ b/Dalamud/Networking/Rpc/RpcHostService.cs @@ -88,7 +88,7 @@ internal class RpcHostService : IServiceType, IInternalDisposableService var transport = new UnixRpcTransport(this.registry); this.transports.Add(transport); transport.Start(); - this.log.Information("RpcHostService started Unix socket host: {Socket}", transport.SocketPath); + this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath); } private void StartPipeTransport() @@ -100,6 +100,6 @@ internal class RpcHostService : IServiceType, IInternalDisposableService var transport = new PipeRpcTransport(this.registry); this.transports.Add(transport); transport.Start(); - this.log.Information("RpcHostService started named pipe host: {Pipe}", transport.PipeName); + this.log.Information("RpcHostService listening to named pipe: {Pipe}", transport.PipeName); } } diff --git a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs index 0cefeb853..727eb9125 100644 --- a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs +++ b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs @@ -16,7 +16,7 @@ namespace Dalamud.Networking.Rpc.Transport; /// internal class PipeRpcTransport : IRpcTransport { - private readonly ModuleLog log = new("RPC/Host"); + private readonly ModuleLog log = new("RPC/Transport/NamedPipe"); private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); @@ -107,7 +107,6 @@ internal class PipeRpcTransport : IRpcTransport private async Task AcceptLoopAsync() { - this.log.Information("PipeRpcHost starting on pipe {Pipe}", this.PipeName); var token = this.cts.Token; var security = this.BuildPipeSecurity(); diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs index 3019f5aaf..e1ef64f76 100644 --- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -17,7 +17,7 @@ namespace Dalamud.Networking.Rpc.Transport; /// internal class UnixRpcTransport : IRpcTransport { - private readonly ModuleLog log = new("RPC/UnixHost"); + private readonly ModuleLog log = new("RPC/Transport/UnixSocket"); private readonly RpcServiceRegistry registry; private readonly CancellationTokenSource cts = new(); @@ -173,7 +173,6 @@ internal class UnixRpcTransport : IRpcTransport private async Task AcceptLoopAsync() { - this.log.Information("UnixRpcHost starting on socket {Socket}", this.SocketPath); var token = this.cts.Token; try diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs index af3b583c9..7e9faf3f9 100644 --- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs +++ b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using Dalamud.Plugin.Services; + namespace Dalamud.Plugin.SelfTest; /// @@ -44,7 +46,7 @@ namespace Dalamud.Plugin.SelfTest; /// } /// /// -public interface ISelfTestRegistry +public interface ISelfTestRegistry : IDalamudService { /// /// Registers the self-test steps for this plugin. diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index bff5c8ba2..37101222a 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services; /// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// [Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] -public interface IPluginLinkHandler +public interface IPluginLinkHandler : IDalamudService { /// /// A delegate containing the received URI. From 0d8f577576800d9e00bdf262562f4ba4c3e910ad Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 18 Nov 2025 16:28:03 -0800 Subject: [PATCH 029/164] feat: add debug link handler as demo --- .../Rpc/Service/Links/DebugLinkHandler.cs | 67 +++++++++++++++++++ .../Links}/PluginLinkHandler.cs | 3 +- 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs rename Dalamud/Networking/Rpc/{Api => Service/Links}/PluginLinkHandler.cs (95%) diff --git a/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs new file mode 100644 index 000000000..269617fc0 --- /dev/null +++ b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs @@ -0,0 +1,67 @@ +using Dalamud.Game.Gui.Toast; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Networking.Rpc.Model; + +namespace Dalamud.Networking.Rpc.Service.Links; + +#if DEBUG + +/// +/// A debug controller for link handling. +/// +[ServiceManager.EarlyLoadedService] +internal sealed class DebugLinkHandler : IInternalDisposableService +{ + private readonly LinkHandlerService linkHandlerService; + + /// + /// Initializes a new instance of the class. + /// + /// Injected LinkHandler. + [ServiceManager.ServiceConstructor] + public DebugLinkHandler(LinkHandlerService linkHandler) + { + this.linkHandlerService = linkHandler; + + this.linkHandlerService.Register("debug", this.HandleLink); + } + + /// + public void DisposeService() + { + this.linkHandlerService.Unregister("debug", this.HandleLink); + } + + private void HandleLink(DalamudUri uri) + { + var action = uri.Path.Split("/").GetValue(1)?.ToString(); + switch (action) + { + case "toast": + this.ShowToast(uri); + break; + case "notification": + this.ShowNotification(uri); + break; + } + } + + private void ShowToast(DalamudUri uri) + { + var message = uri.QueryParams.Get("message") ?? "Hello, world!"; + Service.Get().ShowNormal(message); + } + + private void ShowNotification(DalamudUri uri) + { + Service.Get().AddNotification( + new Notification + { + Title = uri.QueryParams.Get("title"), + Content = uri.QueryParams.Get("content") ?? "Hello, world!", + }); + } +} + +#endif diff --git a/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs similarity index 95% rename from Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs rename to Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs index e9372bf0e..4dbe3fdf1 100644 --- a/Dalamud/Networking/Rpc/Api/PluginLinkHandler.cs +++ b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs @@ -4,13 +4,12 @@ using Dalamud.Console; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Networking.Rpc.Model; -using Dalamud.Networking.Rpc.Service; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; #pragma warning disable DAL_RPC -namespace Dalamud.Networking.Rpc.Api; +namespace Dalamud.Networking.Rpc.Service.Links; /// [PluginInterface] From 7b286c427cbd14859381ce7ee99bef1d9768e033 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Nov 2025 10:08:24 -0800 Subject: [PATCH 030/164] chore: remove named pipe transport, use startinfo for pathing --- Dalamud.Boot/DalamudStartInfo.cpp | 5 + Dalamud.Boot/DalamudStartInfo.h | 1 + Dalamud.Boot/veh.cpp | 15 +- Dalamud.Common/DalamudStartInfo.cs | 6 + Dalamud.Injector/Program.cs | 6 + Dalamud/Networking/Rpc/RpcHostService.cs | 14 -- .../Rpc/Transport/PipeRpcTransport.cs | 154 ------------------ .../Rpc/Transport/UnixRpcTransport.cs | 34 ++-- .../Rpc}/UnixSocketUtil.cs | 2 +- 9 files changed, 41 insertions(+), 196 deletions(-) delete mode 100644 Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs rename Dalamud/{Utility => Networking/Rpc}/UnixSocketUtil.cs (98%) diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 5be8f97d0..9c8fd9721 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -108,6 +108,11 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) { config.LogName = json.value("LogName", config.LogName); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory); + + if (json.contains("TempDirectory") && !json["TempDirectory"].is_null()) { + config.TempDirectory = json.value("TempDirectory", config.TempDirectory); + } + config.Language = json.value("Language", config.Language); config.Platform = json.value("Platform", config.Platform); config.GameVersion = json.value("GameVersion", config.GameVersion); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 0eeaddeed..308dcab7d 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -44,6 +44,7 @@ struct DalamudStartInfo { std::string ConfigurationPath; std::string LogPath; std::string LogName; + std::string TempDirectory; std::string PluginDirectory; std::string AssetDirectory; ClientLanguage Language = ClientLanguage::English; diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index b0ec1cefa..b75256af8 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -122,6 +122,7 @@ static DalamudExpected append_injector_launch_args(std::vector(g_startInfo.LogName) + L"\""); args.emplace_back(L"--dalamud-plugin-directory=\"" + unicode::convert(g_startInfo.PluginDirectory) + L"\""); args.emplace_back(L"--dalamud-asset-directory=\"" + unicode::convert(g_startInfo.AssetDirectory) + L"\""); + args.emplace_back(L"--dalamud-temp-directory=\"" + unicode::convert(g_startInfo.TempDirectory) + L"\""); args.emplace_back(std::format(L"--dalamud-client-language={}", static_cast(g_startInfo.Language))); args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs)); // NoLoadPlugins/NoLoadThirdPartyPlugins: supplied from DalamudCrashHandler @@ -268,7 +269,7 @@ LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) && !is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip)) - return EXCEPTION_CONTINUE_SEARCH; + return EXCEPTION_CONTINUE_SEARCH; } return exception_handler(ex); @@ -297,7 +298,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) if (HANDLE hReadPipeRaw, hWritePipeRaw; CreatePipe(&hReadPipeRaw, &hWritePipeRaw, nullptr, 65536)) { hWritePipe.emplace(hWritePipeRaw, &CloseHandle); - + if (HANDLE hReadPipeInheritableRaw; DuplicateHandle(GetCurrentProcess(), hReadPipeRaw, GetCurrentProcess(), &hReadPipeInheritableRaw, 0, TRUE, DUPLICATE_SAME_ACCESS | DUPLICATE_CLOSE_SOURCE)) { hReadPipeInheritable.emplace(hReadPipeInheritableRaw, &CloseHandle); @@ -315,9 +316,9 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } // additional information - STARTUPINFOEXW siex{}; + STARTUPINFOEXW siex{}; PROCESS_INFORMATION pi{}; - + siex.StartupInfo.cb = sizeof siex; siex.StartupInfo.dwFlags = STARTF_USESHOWWINDOW; siex.StartupInfo.wShowWindow = g_startInfo.CrashHandlerShow ? SW_SHOW : SW_HIDE; @@ -385,7 +386,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) argstr.push_back(L' '); } argstr.pop_back(); - + if (!handles.empty() && !UpdateProcThreadAttribute(siex.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, &handles[0], std::span(handles).size_bytes(), nullptr, nullptr)) { logging::W("Failed to launch DalamudCrashHandler.exe: UpdateProcThreadAttribute error 0x{:x}", GetLastError()); @@ -400,7 +401,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) TRUE, // Set handle inheritance to FALSE EXTENDED_STARTUPINFO_PRESENT, // lpStartupInfo actually points to a STARTUPINFOEX(W) nullptr, // Use parent's environment block - nullptr, // Use parent's starting directory + nullptr, // Use parent's starting directory &siex.StartupInfo, // Pointer to STARTUPINFO structure &pi // Pointer to PROCESS_INFORMATION structure (removed extra parentheses) )) @@ -416,7 +417,7 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) } CloseHandle(pi.hThread); - + g_crashhandler_process = pi.hProcess; g_crashhandler_pipe_write = hWritePipe->release(); logging::I("Launched DalamudCrashHandler.exe: PID {}", pi.dwProcessId); diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index a0d7f8b0b..8c66a85ba 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -34,6 +34,12 @@ public record DalamudStartInfo /// public string? ConfigurationPath { get; set; } + /// + /// Gets or sets the directory for temporary files. This directory needs to exist and be writable to the user. + /// It should also be predictable and easy for launchers to find. + /// + public string? TempDirectory { get; set; } + /// /// Gets or sets the path of the log files. /// diff --git a/Dalamud.Injector/Program.cs b/Dalamud.Injector/Program.cs index e224791e6..13fcacef2 100644 --- a/Dalamud.Injector/Program.cs +++ b/Dalamud.Injector/Program.cs @@ -291,6 +291,7 @@ namespace Dalamud.Injector var configurationPath = startInfo.ConfigurationPath; var pluginDirectory = startInfo.PluginDirectory; var assetDirectory = startInfo.AssetDirectory; + var tempDirectory = startInfo.TempDirectory; var delayInitializeMs = startInfo.DelayInitializeMs; var logName = startInfo.LogName; var logPath = startInfo.LogPath; @@ -321,6 +322,10 @@ namespace Dalamud.Injector { assetDirectory = args[i][key.Length..]; } + else if (args[i].StartsWith(key = "--dalamud-temp-directory=")) + { + tempDirectory = args[i][key.Length..]; + } else if (args[i].StartsWith(key = "--dalamud-delay-initialize=")) { delayInitializeMs = int.Parse(args[i][key.Length..]); @@ -433,6 +438,7 @@ namespace Dalamud.Injector startInfo.ConfigurationPath = configurationPath; startInfo.PluginDirectory = pluginDirectory; startInfo.AssetDirectory = assetDirectory; + startInfo.TempDirectory = tempDirectory; startInfo.Language = clientLanguage; startInfo.Platform = platform; startInfo.DelayInitializeMs = delayInitializeMs; diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs index 60152b355..bbe9dc8eb 100644 --- a/Dalamud/Networking/Rpc/RpcHostService.cs +++ b/Dalamud/Networking/Rpc/RpcHostService.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Networking.Rpc.Transport; -using Dalamud.Utility; namespace Dalamud.Networking.Rpc; @@ -26,7 +25,6 @@ internal class RpcHostService : IServiceType, IInternalDisposableService public RpcHostService() { this.StartUnixTransport(); - this.StartPipeTransport(); if (this.transports.Count == 0) { @@ -90,16 +88,4 @@ internal class RpcHostService : IServiceType, IInternalDisposableService transport.Start(); this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath); } - - private void StartPipeTransport() - { - // Wine doesn't support named pipes. - if (Util.IsWine()) - return; - - var transport = new PipeRpcTransport(this.registry); - this.transports.Add(transport); - transport.Start(); - this.log.Information("RpcHostService listening to named pipe: {Pipe}", transport.PipeName); - } } diff --git a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs deleted file mode 100644 index 727eb9125..000000000 --- a/Dalamud/Networking/Rpc/Transport/PipeRpcTransport.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO.Pipes; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Logging.Internal; -using Dalamud.Utility; - -namespace Dalamud.Networking.Rpc.Transport; - -/// -/// Simple multi-client JSON-RPC named pipe host using StreamJsonRpc. -/// -internal class PipeRpcTransport : IRpcTransport -{ - private readonly ModuleLog log = new("RPC/Transport/NamedPipe"); - - private readonly RpcServiceRegistry registry; - private readonly CancellationTokenSource cts = new(); - private readonly ConcurrentDictionary sessions = new(); - private Task? acceptLoopTask; - - /// - /// Initializes a new instance of the class. - /// - /// The RPC service registry to use. - /// The pipe name to create. - public PipeRpcTransport(RpcServiceRegistry registry, string? pipeName = null) - { - this.registry = registry; - // Default pipe name based on current process ID for uniqueness per Dalamud instance. - this.PipeName = pipeName ?? $"DalamudRPC.{Environment.ProcessId}"; - } - - /// - /// Gets the name of the named pipe this RPC host is using. - /// - public string PipeName { get; } - - /// - public IReadOnlyDictionary Connections => this.sessions; - - /// Starts accepting client connections. - public void Start() - { - if (this.acceptLoopTask != null) return; - this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); - } - - /// Invoke an RPC request on a specific client expecting a result. - /// The client ID to invoke. - /// The method to invoke. - /// Any arguments to invoke. - /// An optional return based on the specified RPC. - /// The expected response type. - public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) - { - if (!this.sessions.TryGetValue(clientId, out var session)) - throw new KeyNotFoundException($"No client {clientId}"); - - return session.Rpc.InvokeAsync(method, arguments); - } - - /// Send a notification to all connected clients (no response expected). - /// The method name to broadcast. - /// The arguments to broadcast. - /// Returns a Task when completed. - public Task BroadcastNotifyAsync(string method, params object[] arguments) - { - var list = this.sessions.Values; - var tasks = new List(list.Count); - foreach (var s in list) - { - tasks.Add(s.Rpc.NotifyAsync(method, arguments)); - } - - return Task.WhenAll(tasks); - } - - /// - public void Dispose() - { - this.cts.Cancel(); - this.acceptLoopTask?.Wait(1000); - - foreach (var kv in this.sessions) - { - kv.Value.Dispose(); - } - - this.sessions.Clear(); - this.cts.Dispose(); - this.log.Information("PipeRpcHost disposed ({Pipe})", this.PipeName); - GC.SuppressFinalize(this); - } - - private PipeSecurity BuildPipeSecurity() - { - var ps = new PipeSecurity(); - ps.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().User!, PipeAccessRights.FullControl, AccessControlType.Allow)); - - return ps; - } - - private async Task AcceptLoopAsync() - { - var token = this.cts.Token; - var security = this.BuildPipeSecurity(); - - while (!token.IsCancellationRequested) - { - NamedPipeServerStream? server = null; - try - { - server = NamedPipeServerStreamAcl.Create( - this.PipeName, - PipeDirection.InOut, - NamedPipeServerStream.MaxAllowedServerInstances, - PipeTransmissionMode.Message, - PipeOptions.Asynchronous, - 65536, - 65536, - security); - - await server.WaitForConnectionAsync(token).ConfigureAwait(false); - - var session = new RpcConnection(server, this.registry); - this.sessions.TryAdd(session.Id, session); - - this.log.Debug("RPC connection created: {Id}", session.Id); - - _ = session.Completion.ContinueWith(t => - { - this.sessions.TryRemove(session.Id, out _); - this.log.Debug("RPC connection removed: {Id}", session.Id); - }, TaskScheduler.Default); - } - catch (OperationCanceledException) - { - server?.Dispose(); - break; - } - catch (Exception ex) - { - server?.Dispose(); - this.log.Error(ex, "Error in pipe accept loop"); - await Task.Delay(500, token).ConfigureAwait(false); - } - } - } -} diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs index e1ef64f76..064ce375d 100644 --- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -31,30 +31,30 @@ internal class UnixRpcTransport : IRpcTransport /// Initializes a new instance of the class. /// /// The RPC service registry to use. - /// The Unix socket path to create. If null, defaults to a path based on process ID. - public UnixRpcTransport(RpcServiceRegistry registry, string? socketPath = null) + /// The Unix socket directory to use. If null, defaults to Dalamud home directory. + /// The name of the socket to create. + public UnixRpcTransport(RpcServiceRegistry registry, string? socketDirectory = null, string? socketName = null) { this.registry = registry; + socketName ??= $"DalamudRPC.{Environment.ProcessId}.sock"; - if (socketPath != null) + if (!socketDirectory.IsNullOrEmpty()) { - this.SocketPath = socketPath; + this.SocketPath = Path.Combine(socketDirectory, socketName); } else { - var dalamudConfigPath = Service.Get().StartInfo.ConfigurationPath; - var dalamudHome = Path.GetDirectoryName(dalamudConfigPath); - var socketName = $"DalamudRPC.{Environment.ProcessId}.sock"; + socketDirectory = Service.Get().StartInfo.TempDirectory; - if (dalamudHome == null) + if (socketDirectory == null) { this.SocketPath = Path.Combine(Path.GetTempPath(), socketName); - this.log.Warning("Dalamud home is empty! UDS socket will be in temp."); + this.log.Warning("Temp dir was not set in StartInfo; using system temp for unix socket."); } else { - this.SocketPath = Path.Combine(dalamudHome, socketName); - this.cleanupSocketDirectory = dalamudHome; + this.SocketPath = Path.Combine(socketDirectory, socketName); + this.cleanupSocketDirectory = socketDirectory; } } } @@ -76,15 +76,8 @@ internal class UnixRpcTransport : IRpcTransport var socketDir = Path.GetDirectoryName(this.SocketPath); if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir)) { - try - { - Directory.CreateDirectory(socketDir); - } - catch (Exception ex) - { - this.log.Error(ex, "Failed to create socket directory: {Path}", socketDir); - return; - } + this.log.Error("Directory for unix socket does not exist: {Path}", socketDir); + return; } // Delete existing socket for this PID, if it exists. @@ -103,6 +96,7 @@ internal class UnixRpcTransport : IRpcTransport this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); // note: needs to be run _after_ we're alive so that we don't delete our own socket. + // TODO: This should *probably* be handed by the launcher instead. if (this.cleanupSocketDirectory != null) { Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory)); diff --git a/Dalamud/Utility/UnixSocketUtil.cs b/Dalamud/Networking/Rpc/UnixSocketUtil.cs similarity index 98% rename from Dalamud/Utility/UnixSocketUtil.cs rename to Dalamud/Networking/Rpc/UnixSocketUtil.cs index 46bb05c74..b7500a946 100644 --- a/Dalamud/Utility/UnixSocketUtil.cs +++ b/Dalamud/Networking/Rpc/UnixSocketUtil.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using Serilog; -namespace Dalamud.Utility; +namespace Dalamud.Networking.Rpc; /// /// A set of utilities to help manage Unix sockets. From 8ab7b59ae47f5deaa59dc1001e92855c3b6e2a89 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 25 Nov 2025 10:17:12 -0800 Subject: [PATCH 031/164] fix: Missing service types causing injection failures --- Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs | 4 +++- Dalamud/Plugin/Services/IPluginLinkHandler.cs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs index af3b583c9..7e9faf3f9 100644 --- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs +++ b/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using Dalamud.Plugin.Services; + namespace Dalamud.Plugin.SelfTest; /// @@ -44,7 +46,7 @@ namespace Dalamud.Plugin.SelfTest; /// } /// /// -public interface ISelfTestRegistry +public interface ISelfTestRegistry : IDalamudService { /// /// Registers the self-test steps for this plugin. diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs index 5d2d32728..c05757ac7 100644 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ b/Dalamud/Plugin/Services/IPluginLinkHandler.cs @@ -9,7 +9,7 @@ namespace Dalamud.Plugin.Services; /// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. /// [Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] -public interface IPluginLinkHandler +public interface IPluginLinkHandler : IDalamudService { /// /// A delegate containing the received URI. From 9a1fae8246d0d55cf9d52b0c31c28956b456c6d8 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 17:27:48 -0800 Subject: [PATCH 032/164] Refactor Addon Lifecycle --- .../Game/Addon/AddonLifecyclePooledArgs.cs | 107 ----- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +- .../AddonArgTypes/AddonFinalizeArgs.cs | 8 +- .../AddonArgTypes/AddonGenericArgs.cs | 18 + .../AddonArgTypes/AddonReceiveEventArgs.cs | 16 +- .../AddonArgTypes/AddonRefreshArgs.cs | 12 +- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 12 +- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 12 +- .../AddonArgTypes/AddonUpdateArgs.cs | 10 +- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +- Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 54 ++- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 298 ++----------- .../AddonLifecycleAddressResolver.cs | 38 +- .../AddonLifecycleReceiveEventListener.cs | 112 ----- .../Game/Addon/Lifecycle/AddonSetupHook.cs | 80 ---- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 405 ++++++++++++++++++ Dalamud/Hooking/Internal/CallHook.cs | 100 ----- .../Data/Widgets/AddonLifecycleWidget.cs | 51 --- 19 files changed, 543 insertions(+), 825 deletions(-) delete mode 100644 Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs delete mode 100644 Dalamud/Hooking/Internal/CallHook.cs diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs deleted file mode 100644 index 14def2036..000000000 --- a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Threading; - -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -namespace Dalamud.Game.Addon; - -/// Argument pool for Addon Lifecycle services. -[ServiceManager.EarlyLoadedService] -internal sealed class AddonLifecyclePooledArgs : IServiceType -{ - private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64]; - private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64]; - private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64]; - private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64]; - private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64]; - private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64]; - private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64]; - - [ServiceManager.ServiceConstructor] - private AddonLifecyclePooledArgs() - { - } - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonRequestedUpdateArgs arg) => - new(out arg, this.addonRequestedUpdateArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonReceiveEventArgs arg) => - new(out arg, this.addonReceiveEventArgPool); - - /// Returns the object to the pool on dispose. - /// The type. - public readonly ref struct PooledEntry - where T : AddonArgs, new() - { - private readonly Span pool; - private readonly T obj; - - /// Initializes a new instance of the struct. - /// An instance of the argument. - /// The pool to rent from and return to. - public PooledEntry(out T arg, Span pool) - { - this.pool = pool; - foreach (ref var item in pool) - { - if (Interlocked.Exchange(ref item, null) is { } v) - { - this.obj = arg = v; - return; - } - } - - this.obj = arg = new(); - } - - /// Returns the item to the pool. - public void Dispose() - { - var tmp = this.obj; - foreach (ref var item in this.pool) - { - if (Interlocked.Exchange(ref item, tmp) is not { } tmp2) - return; - tmp = tmp2; - } - } - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index c008db08f..0b2ae1178 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Base class for AddonLifecycle AddonArgTypes. /// -public abstract unsafe class AddonArgs +public abstract class AddonArgs { /// /// Constant string representing the name of an addon that is invalid. diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 989e11912..7254ba7b3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -3,7 +3,7 @@ /// /// Addon argument data for Draw events. /// -public class AddonDrawArgs : AddonArgs, ICloneable +public class AddonDrawArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -15,10 +15,4 @@ public class AddonDrawArgs : AddonArgs, ICloneable /// public override AddonArgsType Type => AddonArgsType.Draw; - - /// - public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index d9401b414..12def3ad3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonFinalizeArgs : AddonArgs, ICloneable +public class AddonFinalizeArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -15,10 +15,4 @@ public class AddonFinalizeArgs : AddonArgs, ICloneable /// public override AddonArgsType Type => AddonArgsType.Finalize; - - /// - public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs new file mode 100644 index 000000000..f3078af69 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Draw events. +/// +public class AddonGenericArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonGenericArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Generic; +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 980fe4f2f..05f51b118 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonReceiveEventArgs : AddonArgs, ICloneable +public class AddonReceiveEventArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -36,19 +36,13 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable /// public nint Data { get; set; } - /// - public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkEventType = default; - this.EventParam = default; - this.AtkEvent = default; - this.Data = default; + this.AtkEventType = 0; + this.EventParam = 0; + this.AtkEvent = 0; + this.Data = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index d28631c3c..c01c065c1 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Refresh events. /// -public class AddonRefreshArgs : AddonArgs, ICloneable +public class AddonRefreshArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -33,17 +33,11 @@ public class AddonRefreshArgs : AddonArgs, ICloneable /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; + this.AtkValueCount = 0; + this.AtkValues = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index e87a980fd..bf00c5d6e 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for OnRequestedUpdate events. /// -public class AddonRequestedUpdateArgs : AddonArgs, ICloneable +public class AddonRequestedUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -26,17 +26,11 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable /// public nint StringArrayData { get; set; } - /// - public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.NumberArrayData = default; - this.StringArrayData = default; + this.NumberArrayData = 0; + this.StringArrayData = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 0dd9ecee2..9b7e86a61 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Setup events. /// -public class AddonSetupArgs : AddonArgs, ICloneable +public class AddonSetupArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -33,17 +33,11 @@ public class AddonSetupArgs : AddonArgs, ICloneable /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; + this.AtkValueCount = 0; + this.AtkValues = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index a263f6ae4..bab62fc89 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// -public class AddonUpdateArgs : AddonArgs, ICloneable +public class AddonUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -30,16 +30,10 @@ public class AddonUpdateArgs : AddonArgs, ICloneable /// internal float TimeDeltaInternal { get; set; } - /// - public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.TimeDeltaInternal = default; + this.TimeDeltaInternal = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index b58b5f4c7..95dc5f718 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -9,34 +9,39 @@ public enum AddonArgsType /// Contains argument data for Setup. /// Setup, - + /// /// Contains argument data for Update. /// Update, - + /// /// Contains argument data for Draw. - /// + /// Draw, - + /// /// Contains argument data for Finalize. - /// + /// Finalize, - + /// /// Contains argument data for RequestedUpdate. - /// + /// RequestedUpdate, - + /// /// Contains argument data for Refresh. - /// + /// Refresh, - + /// /// Contains argument data for ReceiveEvent. /// ReceiveEvent, + + /// + /// Generic arg type that contains no meaningful data + /// + Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 5fd0ac964..7738d6c6a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -16,7 +16,7 @@ public enum AddonEvent /// /// PreSetup, - + /// /// An event that is fired after an addon has finished its initial setup. This event is particularly useful for /// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data @@ -64,7 +64,7 @@ public enum AddonEvent /// /// PreFinalize, - + /// /// An event that is fired before a call to is made in response to a /// change in the subscribed or @@ -81,13 +81,13 @@ public enum AddonEvent /// to the Free Company's overview. /// PreRequestedUpdate, - + /// /// An event that is fired after an addon has finished processing an ArrayData update. /// See for more information. /// PostRequestedUpdate, - + /// /// An event that is fired before an addon calls its method. Refreshes are /// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to @@ -96,13 +96,13 @@ public enum AddonEvent /// /// PreRefresh, - + /// /// An event that is fired after an addon has finished its refresh. /// See for more information. /// PostRefresh, - + /// /// An event that is fired before an addon begins processing a user-driven event via /// , such as mousing over an element or clicking a button. This event @@ -112,10 +112,50 @@ public enum AddonEvent /// /// PreReceiveEvent, - + /// /// An event that is fired after an addon finishes calling its method. /// See for more information. /// PostReceiveEvent, + + /// + /// An event that is fired before an addon processes its open method. + /// + PreOpen, + + /// + /// An event that is fired after an addon has processed its open method. + /// + PostOpen, + + /// + /// An even that is fired before an addon processes its close method. + /// + PreClose, + + /// + /// An event that is fired after an addon has processed its close method. + /// + PostClose, + + /// + /// An event that is fired before an addon processes its show method. + /// + PreShow, + + /// + /// An event that is fired after an addon has processed its show method. + /// + PostShow, + + /// + /// An event that is fired before an addon processes its hide method. + /// + PreHide, + + /// + /// An event that is fired after an addon has processed its hide method. + /// + PostHide, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index b44ab8764..cea30d6be 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,16 +1,14 @@ using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Runtime.CompilerServices; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; -using Dalamud.Hooking.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle; @@ -26,69 +24,33 @@ internal unsafe class AddonLifecycle : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); + private readonly Dictionary modifiedTables = []; - private readonly nint disallowedReceiveEventAddress; - - private readonly AddonLifecycleAddressResolver address; - private readonly AddonSetupHook onAddonSetupHook; - private readonly Hook onAddonFinalizeHook; - private readonly CallHook onAddonDrawHook; - private readonly CallHook onAddonUpdateHook; - private readonly Hook onAddonRefreshHook; - private readonly CallHook onAddonRequestedUpdateHook; + private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) { - this.address = new AddonLifecycleAddressResolver(); - this.address.Setup(sigScanner); + this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); + this.onInitializeAddonHook.Enable(); - this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent; - - var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon; - - this.onAddonSetupHook = new AddonSetupHook(this.address.AddonSetup, this.OnAddonSetup); - this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); - this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); - this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); - this.onAddonRefreshHook = Hook.FromAddress(refreshAddonAddress, this.OnAddonRefresh); - this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); - - this.onAddonSetupHook.Enable(); - this.onAddonFinalizeHook.Enable(); - this.onAddonDrawHook.Enable(); - this.onAddonUpdateHook.Enable(); - this.onAddonRefreshHook.Enable(); - this.onAddonRequestedUpdateHook.Enable(); + Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}"); } - private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); - - /// - /// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks. - /// - internal List ReceiveEventListeners { get; } = new(); - /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal List EventListeners { get; } = new(); + internal List EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() { - this.onAddonSetupHook.Dispose(); - this.onAddonFinalizeHook.Dispose(); - this.onAddonDrawHook.Dispose(); - this.onAddonUpdateHook.Dispose(); - this.onAddonRefreshHook.Dispose(); - this.onAddonRequestedUpdateHook.Dispose(); + this.onInitializeAddonHook?.Dispose(); + this.onInitializeAddonHook = null; - foreach (var receiveEventListener in this.ReceiveEventListeners) + foreach (var virtualTable in this.modifiedTables.Values) { - receiveEventListener.Dispose(); + virtualTable.Dispose(); } } @@ -101,16 +63,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.framework.RunOnTick(() => { this.EventListeners.Add(listener); - - // If we want receive event messages have an already active addon, enable the receive event hook. - // If the addon isn't active yet, we'll grab the hook when it sets up. - if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) - { - receiveEventListener.TryEnable(); - } - } }); } @@ -122,24 +74,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. listener.Removed = true; - + this.framework.RunOnTick(() => { this.EventListeners.Remove(listener); - - // If we are disabling an ReceiveEvent listener, check if we should disable the hook. - if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - // Get the ReceiveEvent Listener for this addon - if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) - { - // If there are no other listeners listening for this event, disable the hook. - if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent)) - { - receiveEventListener.Disable(); - } - } - } }); } @@ -160,7 +98,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener. if (listener.Removed) continue; - + // Match on string.empty for listeners that want events for all addons. if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) continue; @@ -176,201 +114,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } } - private void RegisterReceiveEventHook(AtkUnitBase* addon) + private void OnAddonInitialize(AtkUnitBase* addon) { - // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. - // Disallows hooking the core internal event handler. - var addonName = addon->NameString; - var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent; - if (receiveEventAddress != this.disallowedReceiveEventAddress) + try { - // If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler. - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener) + this.LogInitialize(addon->NameString); + + if (!this.modifiedTables.ContainsKey(addon->NameString)) { - if (!existingListener.AddonNames.Contains(addonName)) + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + var managedVirtualTableEntry = new AddonVirtualTable(addon, this) { - existingListener.AddonNames.Add(addonName); - } - } + // This event is invoked when the game itself has disposed of an addon + // We can use this to know when to remove our virtual table entry + OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString), + }; - // Else, we have an addon that we don't have the ReceiveEvent for yet, make it. - else - { - this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress)); - } - - // If we have an active listener for this addon already, we need to activate this hook. - if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName)) - { - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener) - { - receiveEventListener.TryEnable(); - } + this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry); } } + catch (Exception e) + { + Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize."); + } + + this.onInitializeAddonHook!.Original(addon); } - private void UnregisterReceiveEventHook(string addonName) + [Conditional("DEBUG")] + private void LogInitialize(string addonName) { - // Remove this addons ReceiveEvent Registration - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener) - { - eventListener.AddonNames.Remove(addonName); - - // If there are no more listeners let's remove and dispose. - if (eventListener.AddonNames.Count is 0) - { - this.ReceiveEventListeners.Remove(eventListener); - eventListener.Dispose(); - } - } - } - - private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) - { - try - { - this.RegisterReceiveEventHook(addon); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); - } - - using var returner = this.argsPool.Rent(out AddonSetupArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkValueCount = valueCount; - arg.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreSetup, arg); - valueCount = arg.AtkValueCount; - values = (AtkValue*)arg.AtkValues; - - try - { - addon->OnSetup(valueCount, values); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostSetup, arg); - } - - private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) - { - try - { - var addonName = atkUnitBase[0]->NameString; - this.UnregisterReceiveEventHook(addonName); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); - } - - using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg); - arg.Clear(); - arg.Addon = (nint)atkUnitBase[0]; - this.InvokeListenersSafely(AddonEvent.PreFinalize, arg); - - try - { - this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); - } - } - - private void OnAddonDraw(AtkUnitBase* addon) - { - using var returner = this.argsPool.Rent(out AddonDrawArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - this.InvokeListenersSafely(AddonEvent.PreDraw, arg); - - try - { - addon->Draw(); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostDraw, arg); - } - - private void OnAddonUpdate(AtkUnitBase* addon, float delta) - { - using var returner = this.argsPool.Rent(out AddonUpdateArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.TimeDeltaInternal = delta; - this.InvokeListenersSafely(AddonEvent.PreUpdate, arg); - - try - { - addon->Update(delta); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostUpdate, arg); - } - - private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values) - { - var result = false; - - using var returner = this.argsPool.Rent(out AddonRefreshArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkValueCount = valueCount; - arg.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreRefresh, arg); - valueCount = arg.AtkValueCount; - values = (AtkValue*)arg.AtkValues; - - try - { - result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostRefresh, arg); - return result; - } - - private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) - { - using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.NumberArrayData = (nint)numberArrayData; - arg.StringArrayData = (nint)stringArrayData; - this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg); - numberArrayData = (NumberArrayData**)arg.NumberArrayData; - stringArrayData = (StringArrayData**)arg.StringArrayData; - - try - { - addon->OnRequestedUpdate(numberArrayData, stringArrayData); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg); + Log.Debug($"Initializing {addonName}"); } } @@ -387,7 +161,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); - private readonly List eventListeners = new(); + private readonly List eventListeners = []; /// void IInternalDisposableService.DisposeService() @@ -458,7 +232,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi this.eventListeners.RemoveAll(entry => { if (entry.FunctionDelegate != handler) return false; - + this.addonLifecycleService.UnregisterListener(entry); return true; }); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 854d666fd..1d767aac4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -1,56 +1,24 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Utility; namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver +[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")] +internal class AddonLifecycleAddressResolver : BaseAddressResolver { - /// - /// Gets the address of the addon setup hook invoked by the AtkUnitManager. - /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue. - /// This is called for a majority of all addon OnSetup's. - /// - public nint AddonSetup { get; private set; } - - /// - /// Gets the address of the other addon setup hook invoked by the AtkUnitManager. - /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue. - /// This seems to be called rarely for specific addons. - /// - public nint AddonSetup2 { get; private set; } - /// /// Gets the address of the addon finalize hook invoked by the AtkUnitManager. /// public nint AddonFinalize { get; private set; } - /// - /// Gets the address of the addon draw hook invoked by virtual function call. - /// - public nint AddonDraw { get; private set; } - - /// - /// Gets the address of the addon update hook invoked by virtual function call. - /// - public nint AddonUpdate { get; private set; } - - /// - /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. - /// - public nint AddonOnRequestedUpdate { get; private set; } - /// /// Scan for and setup any configured address pointers. /// /// The signature scanner to facilitate setup. protected override void Setup64Bit(ISigScanner sig) { - this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); - this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01"); - this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2"); - this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30"); } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs deleted file mode 100644 index 0d2bcc7f2..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Collections.Generic; - -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; -using Dalamud.Hooking; -using Dalamud.Logging.Internal; - -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent. -/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly. -/// -internal unsafe class AddonLifecycleReceiveEventListener : IDisposable -{ - private static readonly ModuleLog Log = new("AddonLifecycle"); - - [ServiceManager.ServiceDependency] - private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); - - /// - /// Initializes a new instance of the class. - /// - /// AddonLifecycle service instance. - /// Initial Addon Requesting this listener. - /// Address of Addon's ReceiveEvent function. - internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress) - { - this.AddonLifecycle = service; - this.AddonNames = [addonName]; - this.FunctionAddress = receiveEventAddress; - } - - /// - /// Gets the list of addons that use this receive event hook. - /// - public List AddonNames { get; init; } - - /// - /// Gets the address of the ReceiveEvent function as provided by the vtable on setup. - /// - public nint FunctionAddress { get; init; } - - /// - /// Gets the contained hook for these addons. - /// - public Hook? Hook { get; private set; } - - /// - /// Gets or sets the Reference to AddonLifecycle service instance. - /// - private AddonLifecycle AddonLifecycle { get; set; } - - /// - /// Try to hook and enable this receive event handler. - /// - public void TryEnable() - { - this.Hook ??= Hook.FromAddress(this.FunctionAddress, this.OnReceiveEvent); - this.Hook?.Enable(); - } - - /// - /// Disable the hook for this receive event handler. - /// - public void Disable() - { - this.Hook?.Disable(); - } - - /// - public void Dispose() - { - this.Hook?.Dispose(); - } - - private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) - { - // Check that we didn't get here through a call to another addons handler. - var addonName = addon->NameString; - if (!this.AddonNames.Contains(addonName)) - { - this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData); - return; - } - - using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkEventType = (byte)eventType; - arg.EventParam = eventParam; - arg.AtkEvent = (IntPtr)atkEvent; - arg.Data = (nint)atkEventData; - this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg); - eventType = (AtkEventType)arg.AtkEventType; - eventParam = arg.EventParam; - atkEvent = (AtkEvent*)arg.AtkEvent; - atkEventData = (AtkEventData*)arg.Data; - - try - { - this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); - } - - this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg); - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs deleted file mode 100644 index 297323b8f..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Runtime.InteropServices; - -using Reloaded.Hooks.Definitions; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// This class represents a callsite hook used to replace the address of the OnSetup function in r9. -/// -/// Delegate signature for this hook. -internal class AddonSetupHook : IDisposable where T : Delegate -{ - private readonly Reloaded.Hooks.AsmHook asmHook; - - private T? detour; - private bool activated; - - /// - /// Initializes a new instance of the class. - /// - /// Address of the instruction to replace. - /// Delegate to invoke. - internal AddonSetupHook(nint address, T detour) - { - this.detour = detour; - - var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); - var code = new[] - { - "use64", - $"mov r9, 0x{detourPtr:X8}", - }; - - var opt = new AsmHookOptions - { - PreferRelativeJump = true, - Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, - MaxOpcodeSize = 5, - }; - - this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); - } - - /// - /// Gets a value indicating whether the hook is enabled. - /// - public bool IsEnabled => this.asmHook.IsEnabled; - - /// - /// Starts intercepting a call to the function. - /// - public void Enable() - { - if (!this.activated) - { - this.activated = true; - this.asmHook.Activate(); - return; - } - - this.asmHook.Enable(); - } - - /// - /// Stops intercepting a call to the function. - /// - public void Disable() - { - this.asmHook.Disable(); - } - - /// - /// Remove a hook from the current process. - /// - public void Dispose() - { - this.asmHook.Disable(); - this.detour = null; - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs new file mode 100644 index 000000000..58e32a252 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -0,0 +1,405 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Logging.Internal; + +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon.Lifecycle; + +/// +/// Represents a class that holds references to an addons original and modified virtual table entries. +/// +internal unsafe class AddonVirtualTable : IDisposable +{ + // This need to be at minimum the largest virtual table size of all addons + // Copying extra entries is not problematic, and is considered safe. + private const int VirtualTableEntryCount = 200; + + private const bool EnableAdvancedLogging = true; + private const bool EnableSpammyLogging = false; + + private static readonly ModuleLog Log = new("LifecycleVT"); + + private readonly AddonLifecycle lifecycleService; + + // Obsolete warning is only to prevent users from creating their own event objects. +#pragma warning disable CS0618 // Type or member is obsolete + private readonly AddonSetupArgs addonSetupArg = new(); + private readonly AddonFinalizeArgs addonFinalizeArg = new(); + private readonly AddonDrawArgs addonDrawArg = new(); + private readonly AddonUpdateArgs addonUpdateArg = new(); + private readonly AddonRefreshArgs addonRefreshArg = new(); + private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); + private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); + private readonly AddonGenericArgs addonGenericArg = new(); +#pragma warning restore CS0618 // Type or member is obsolete + + private readonly AtkUnitBase* atkUnitBase; + + private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable; + private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable; + + // 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 AtkUnitBase.Delegates.Dtor destructorFunction; + private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction; + private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction; + private readonly AtkUnitBase.Delegates.Draw drawFunction; + private readonly AtkUnitBase.Delegates.Update updateFunction; + private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction; + private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction; + private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction; + private readonly AtkUnitBase.Delegates.Open openFunction; + private readonly AtkUnitBase.Delegates.Close closeFunction; + private readonly AtkUnitBase.Delegates.Show showFunction; + private readonly AtkUnitBase.Delegates.Hide hideFunction; + + /// + /// Initializes a new instance of the class. + /// + /// AtkUnitBase* for the addon to replace the table of. + /// Reference to AddonLifecycle service to callback and invoke listeners. + internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService) + { + this.atkUnitBase = addon; + this.lifecycleService = lifecycleService; + + // Save original virtual table + this.originalVirtualTable = addon->VirtualTable; + + // Create copy of original table + // Note this will copy any derived/overriden functions that this specific addon has. + // Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game + this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8); + NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + + // Overwrite the addons existing virtual table with our own + addon->VirtualTable = this.modifiedVirtualTable; + + // Pin each of our listener functions + this.destructorFunction = this.OnAddonDestructor; + this.onSetupFunction = this.OnAddonSetup; + this.finalizerFunction = this.OnAddonFinalize; + this.drawFunction = this.OnAddonDraw; + this.updateFunction = this.OnAddonUpdate; + this.onRefreshFunction = this.OnAddonRefresh; + this.onRequestedUpdateFunction = this.OnRequestedUpdate; + this.onReceiveEventFunction = this.OnAddonReceiveEvent; + this.openFunction = this.OnAddonOpen; + this.closeFunction = this.OnAddonClose; + this.showFunction = this.OnAddonShow; + this.hideFunction = this.OnAddonHide; + + // Overwrite specific virtual table entries + this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); + this.modifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction); + this.modifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction); + this.modifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction); + this.modifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); + this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction); + this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction); + this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction); + this.modifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction); + this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); + this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); + this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); + } + + /// + /// Gets an event that is invoked when this addon's Finalize method is called from native. + /// + public required Action OnAddonFinalized { get; init; } + + /// + /// WARNING! This should not be called at any time except during dalamud unload. + /// + public void Dispose() + { + this.atkUnitBase->VirtualTable = this.originalVirtualTable; + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + } + + private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) + { + this.LogEvent(); + + var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); + + if ((freeFlags & 1) == 1) + { + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + this.OnAddonFinalized(); + } + + return result; + } + + private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + this.LogEvent(); + + this.addonSetupArg.Clear(); + this.addonSetupArg.Addon = addon; + this.addonSetupArg.AtkValueCount = valueCount; + this.addonSetupArg.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg); + valueCount = this.addonSetupArg.AtkValueCount; + values = (AtkValue*)this.addonSetupArg.AtkValues; + + try + { + this.originalVirtualTable->OnSetup(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg); + } + + private void OnAddonFinalize(AtkUnitBase* thisPtr) + { + this.LogEvent(); + + this.addonFinalizeArg.Clear(); + this.addonFinalizeArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg); + + try + { + this.originalVirtualTable->Finalizer(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); + } + } + + private void OnAddonDraw(AtkUnitBase* addon) + { + this.LogEvent(); + + this.addonDrawArg.Clear(); + this.addonDrawArg.Addon = addon; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); + + try + { + this.originalVirtualTable->Draw(addon); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg); + } + + private void OnAddonUpdate(AtkUnitBase* addon, float delta) + { + this.LogEvent(); + + this.addonUpdateArg.Clear(); + this.addonUpdateArg.Addon = addon; + this.addonUpdateArg.TimeDeltaInternal = delta; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); + + try + { + this.originalVirtualTable->Update(addon, delta); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg); + } + + private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + this.LogEvent(); + + var result = false; + + this.addonRefreshArg.Clear(); + this.addonRefreshArg.Addon = addon; + this.addonRefreshArg.AtkValueCount = valueCount; + this.addonRefreshArg.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg); + valueCount = this.addonRefreshArg.AtkValueCount; + values = (AtkValue*)this.addonRefreshArg.AtkValues; + + try + { + result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg); + return result; + } + + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + this.LogEvent(); + + this.addonRequestedUpdateArg.Clear(); + this.addonRequestedUpdateArg.Addon = addon; + this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; + this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg); + numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData; + stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData; + + try + { + this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg); + } + + private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) + { + this.LogEvent(); + + this.addonReceiveEventArg.Clear(); + this.addonReceiveEventArg.Addon = (nint)addon; + this.addonReceiveEventArg.AtkEventType = (byte)eventType; + this.addonReceiveEventArg.EventParam = eventParam; + this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent; + this.addonReceiveEventArg.Data = (nint)atkEventData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg); + eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType; + eventParam = this.addonReceiveEventArg.EventParam; + atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent; + atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data; + + try + { + this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg); + } + + private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) + { + this.LogEvent(); + + var result = false; + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); + + try + { + result = this.originalVirtualTable->Open(thisPtr, depthLayer); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg); + + return result; + } + + private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) + { + this.LogEvent(); + + var result = false; + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); + + try + { + result = this.originalVirtualTable->Close(thisPtr, fireCallback); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg); + + return result; + } + + private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) + { + this.LogEvent(); + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); + + try + { + this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg); + } + + private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) + { + this.LogEvent(); + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); + + try + { + this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg); + } + + [Conditional("DEBUG")] + private void LogEvent([CallerMemberName] string caller = "") + { + if (EnableAdvancedLogging) + { + if (!EnableSpammyLogging) + { + if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") + return; + } + + Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}"); + } + } +} diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs deleted file mode 100644 index 92bc6e31a..000000000 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Runtime.InteropServices; - -using Reloaded.Hooks.Definitions; - -namespace Dalamud.Hooking.Internal; - -/// -/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook. -/// This is a destructive operation, no other callsite hooks can coexist at the same address. -/// -/// There's no .Original for this hook type. -/// This is only intended for be for functions where the parameters provided allow you to invoke the original call. -/// -/// This class was specifically added for hooking virtual function callsites. -/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered. -/// -/// Delegate signature for this hook. -internal class CallHook : IDalamudHook where T : Delegate -{ - private readonly Reloaded.Hooks.AsmHook asmHook; - - private T? detour; - private bool activated; - - /// - /// Initializes a new instance of the class. - /// - /// Address of the instruction to replace. - /// Delegate to invoke. - internal CallHook(nint address, T detour) - { - ArgumentNullException.ThrowIfNull(detour); - - this.detour = detour; - this.Address = address; - - var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); - var code = new[] - { - "use64", - $"mov rax, 0x{detourPtr:X8}", - "call rax", - }; - - var opt = new AsmHookOptions - { - PreferRelativeJump = true, - Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, - MaxOpcodeSize = 5, - }; - - this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); - } - - /// - /// Gets a value indicating whether the hook is enabled. - /// - public bool IsEnabled => this.asmHook.IsEnabled; - - /// - public IntPtr Address { get; } - - /// - public string BackendName => "Reloaded AsmHook"; - - /// - public bool IsDisposed => this.detour == null; - - /// - /// Starts intercepting a call to the function. - /// - public void Enable() - { - if (!this.activated) - { - this.activated = true; - this.asmHook.Activate(); - return; - } - - this.asmHook.Enable(); - } - - /// - /// Stops intercepting a call to the function. - /// - public void Disable() - { - this.asmHook.Disable(); - } - - /// - /// Remove a hook from the current process. - /// - public void Dispose() - { - this.asmHook.Disable(); - this.detour = null; - } -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index b58166e89..c336f895e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -54,13 +52,6 @@ public class AddonLifecycleWidget : IDataWindowWidget this.DrawEventListeners(); ImGui.Unindent(); } - - if (ImGui.CollapsingHeader("ReceiveEvent Hooks"u8)) - { - ImGui.Indent(); - this.DrawReceiveEventHooks(); - ImGui.Unindent(); - } } private void DrawEventListeners() @@ -100,46 +91,4 @@ public class AddonLifecycleWidget : IDataWindowWidget } } } - - private void DrawReceiveEventHooks() - { - if (!this.Ready) return; - - var listeners = this.AddonLifecycle.ReceiveEventListeners; - - if (listeners.Count == 0) - { - ImGui.Text("No ReceiveEvent Hooks are Registered"u8); - } - - foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners) - { - if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames))) - { - ImGui.Columns(2); - - var functionAddress = receiveEventListener.FunctionAddress; - - ImGui.Text("Hook Address"u8); - ImGui.NextColumn(); - ImGui.Text($"0x{functionAddress:X} (ffxiv_dx11.exe+{functionAddress - Process.GetCurrentProcess().MainModule!.BaseAddress:X})"); - - ImGui.NextColumn(); - ImGui.Text("Hook Status"u8); - ImGui.NextColumn(); - if (receiveEventListener.Hook is null) - { - ImGui.Text("Hook is null"u8); - } - else - { - var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed; - var text = receiveEventListener.Hook.IsEnabled ? "Enabled"u8 : "Disabled"u8; - ImGui.TextColored(color, text); - } - - ImGui.Columns(1); - } - } - } } From 2c1bb7664331d975c9398342b89cba88651df0b5 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 18:56:34 -0800 Subject: [PATCH 033/164] Minor cleanup --- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 11 ++++++----- .../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 95dc5f718..de32bd254 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -41,7 +41,7 @@ public enum AddonArgsType ReceiveEvent, /// - /// Generic arg type that contains no meaningful data + /// Generic arg type that contains no meaningful data. /// Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cea30d6be..0c23f5661 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -33,8 +33,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); this.onInitializeAddonHook.Enable(); - - Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}"); } /// @@ -48,10 +46,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - foreach (var virtualTable in this.modifiedTables.Values) + this.framework.RunOnFrameworkThread(() => { - virtualTable.Dispose(); - } + foreach (var virtualTable in this.modifiedTables.Values) + { + virtualTable.Dispose(); + } + }); } /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 1d767aac4..9359870a5 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")] +[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 58e32a252..ca5d970ef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,7 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = true; + private const bool EnableAdvancedLogging = false; private const bool EnableSpammyLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); From ab0500ca6f9ff49cf0c48da7c585ae8e7429fbdf Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 20:45:54 -0800 Subject: [PATCH 034/164] Fix unreachable code complaint --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index ca5d970ef..54c91248e 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,8 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = false; - private const bool EnableSpammyLogging = false; + private const bool EnableLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); @@ -125,7 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); @@ -140,7 +139,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; @@ -164,7 +163,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonFinalize(AtkUnitBase* thisPtr) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; @@ -182,7 +181,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonDraw(AtkUnitBase* addon) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; @@ -202,7 +201,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; @@ -223,7 +222,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -250,7 +249,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; @@ -274,7 +273,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; @@ -302,7 +301,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -326,7 +325,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -350,7 +349,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -370,7 +369,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -389,15 +388,13 @@ internal unsafe class AddonVirtualTable : IDisposable } [Conditional("DEBUG")] - private void LogEvent([CallerMemberName] string caller = "") + private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") { - if (EnableAdvancedLogging) + if (loggingEnabled) { - if (!EnableSpammyLogging) - { - if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") - return; - } + // Manually disable the really spammy log events, you can comment this out if you need to debug them. + if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") + return; Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}"); } From 2cef75bbbef1862f85eb582c0b732c58c6b4a135 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 26 Nov 2025 11:56:30 -0800 Subject: [PATCH 035/164] feat: remove socket cleanup tasks --- .../Rpc/Transport/UnixRpcTransport.cs | 9 -- Dalamud/Networking/Rpc/UnixSocketUtil.cs | 92 ------------------- 2 files changed, 101 deletions(-) delete mode 100644 Dalamud/Networking/Rpc/UnixSocketUtil.cs diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs index 064ce375d..17da51444 100644 --- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs +++ b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs @@ -8,8 +8,6 @@ using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Utility; -using TerraFX.Interop.Windows; - namespace Dalamud.Networking.Rpc.Transport; /// @@ -94,13 +92,6 @@ internal class UnixRpcTransport : IRpcTransport } this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); - - // note: needs to be run _after_ we're alive so that we don't delete our own socket. - // TODO: This should *probably* be handed by the launcher instead. - if (this.cleanupSocketDirectory != null) - { - Task.Run(async () => await UnixSocketUtil.CleanStaleSockets(this.cleanupSocketDirectory)); - } } /// Invoke an RPC request on a specific client expecting a result. diff --git a/Dalamud/Networking/Rpc/UnixSocketUtil.cs b/Dalamud/Networking/Rpc/UnixSocketUtil.cs deleted file mode 100644 index b7500a946..000000000 --- a/Dalamud/Networking/Rpc/UnixSocketUtil.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.IO; -using System.Net.Sockets; -using System.Threading.Tasks; - -using Serilog; - -namespace Dalamud.Networking.Rpc; - -/// -/// A set of utilities to help manage Unix sockets. -/// -internal static class UnixSocketUtil -{ - // Default probe timeout in milliseconds. - private const int DefaultProbeMs = 200; - - /// - /// Test whether a Unix socket is alive/listening. - /// - /// The path to test. - /// How long to wait for a connection success. - /// A task result representing if a socket is alive or not. - public static async Task IsSocketAlive(string path, int timeoutMs = DefaultProbeMs) - { - if (string.IsNullOrEmpty(path)) return false; - var endpoint = new UnixDomainSocketEndPoint(path); - using var client = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - - var connectTask = client.ConnectAsync(endpoint); - var completed = await Task.WhenAny(connectTask, Task.Delay(timeoutMs)).ConfigureAwait(false); - - if (completed == connectTask) - { - // Connected or failed very quickly. If the task is successful, the socket is alive. - if (connectTask.IsCompletedSuccessfully) - { - try - { - client.Shutdown(SocketShutdown.Both); - } - catch - { - // ignored - } - - return true; - } - } - - return false; - } - - /// - /// Find and remove stale Dalamud RPC sockets. - /// - /// The directory to scan for stale sockets. - /// The timeout to wait for a connection attempt to succeed. - /// A task that executes when sockets are purged. - public static async Task CleanStaleSockets(string directory, int probeTimeoutMs = DefaultProbeMs) - { - if (string.IsNullOrEmpty(directory) || !Directory.Exists(directory)) return; - - foreach (var file in Directory.EnumerateFiles(directory, "DalamudRPC.*.sock", SearchOption.TopDirectoryOnly)) - { - // we don't need to check ourselves. - if (file.Contains(Environment.ProcessId.ToString())) continue; - - bool shouldDelete; - - try - { - shouldDelete = !await IsSocketAlive(file, probeTimeoutMs); - } - catch - { - shouldDelete = true; - } - - if (shouldDelete) - { - try - { - File.Delete(file); - } - catch (Exception ex) - { - Log.Error(ex, "Could not delete stale socket file: {File}", file); - } - } - } - } -} From c661faea6be8142a9005431ddaccf88ac2ce9025 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 12 Nov 2025 21:49:28 +0100 Subject: [PATCH 036/164] Fix services using wrong namespaces --- .../Game/Addon/Events/AddonEventManagerAddressResolver.cs | 4 +++- .../Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +- Dalamud/Game/BaseAddressResolver.cs | 2 ++ Dalamud/Game/ClientState/ClientStateAddressResolver.cs | 2 ++ Dalamud/Game/ClientState/Objects/TargetManager.cs | 1 + Dalamud/Game/Config/GameConfigAddressResolver.cs | 4 +++- Dalamud/Game/DutyState/DutyStateAddressResolver.cs | 2 ++ Dalamud/Game/Gui/GameGuiAddressResolver.cs | 2 ++ Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs | 2 ++ Dalamud/Game/Network/GameNetworkAddressResolver.cs | 2 ++ .../Game/Network/Internal/NetworkHandlersAddressResolver.cs | 4 +++- Dalamud/Game/SigScanner.cs | 2 ++ Dalamud/Game/TargetSigScanner.cs | 3 ++- Dalamud/Plugin/{SelfTest => Services}/ISelfTestRegistry.cs | 4 ++-- Dalamud/Plugin/Services/ISigScanner.cs | 4 +--- Dalamud/Plugin/Services/ITargetManager.cs | 6 +++--- 16 files changed, 33 insertions(+), 13 deletions(-) rename Dalamud/Plugin/{SelfTest => Services}/ISelfTestRegistry.cs (95%) diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs index 415e1b169..ec1c51a12 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Addon.Events; +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Addon.Events; /// /// AddonEventManager memory address resolver. diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 854d666fd..bc9e4b639 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -1,4 +1,4 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Plugin.Services; namespace Dalamud.Game.Addon.Lifecycle; diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index 4133117d7..d41b1d9d8 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; +using Dalamud.Plugin.Services; + namespace Dalamud.Game; /// diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 2fc859d09..53774121d 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.ClientState; /// diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index f81154693..a6432e242 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -1,6 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Control; diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs index 2491c4033..e03f4f40b 100644 --- a/Dalamud/Game/Config/GameConfigAddressResolver.cs +++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Config; +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Config; /// /// Game config system address resolver. diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs index 1bca93efb..480b699a0 100644 --- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs +++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.DutyState; /// diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs index 92b89c5a9..1295e2047 100644 --- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.Gui; /// diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs index 450e1fa9f..f97450c28 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateGuiAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.Gui.NamePlate; /// diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs index de92f7c10..48abc2d97 100644 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ b/Dalamud/Game/Network/GameNetworkAddressResolver.cs @@ -1,3 +1,5 @@ +using Dalamud.Plugin.Services; + namespace Dalamud.Game.Network; /// diff --git a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs index 9cd46f798..34c071556 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Game.Network.Internal; +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.Network.Internal; /// /// Internal address resolver for the network handlers. diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index c8a371aee..262e98fa5 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -8,6 +8,8 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Threading; +using Dalamud.Plugin.Services; + using Iced.Intel; using Newtonsoft.Json; using Serilog; diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index f60c32d9a..540d0ea47 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -1,8 +1,9 @@ -using System.Diagnostics; +using System.Diagnostics; using System.IO; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; namespace Dalamud.Game; diff --git a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs b/Dalamud/Plugin/Services/ISelfTestRegistry.cs similarity index 95% rename from Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs rename to Dalamud/Plugin/Services/ISelfTestRegistry.cs index 7e9faf3f9..50d3d35ce 100644 --- a/Dalamud/Plugin/SelfTest/ISelfTestRegistry.cs +++ b/Dalamud/Plugin/Services/ISelfTestRegistry.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.SelfTest; -namespace Dalamud.Plugin.SelfTest; +namespace Dalamud.Plugin.Services; /// /// Interface for registering and unregistering self-test steps from plugins. diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs index fbbd8b05a..017c4fe9d 100644 --- a/Dalamud/Plugin/Services/ISigScanner.cs +++ b/Dalamud/Plugin/Services/ISigScanner.cs @@ -2,9 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; -using Dalamud.Plugin.Services; - -namespace Dalamud.Game; +namespace Dalamud.Plugin.Services; /// /// A SigScanner facilitates searching for memory signatures in a given ProcessModule. diff --git a/Dalamud/Plugin/Services/ITargetManager.cs b/Dalamud/Plugin/Services/ITargetManager.cs index 9c9fce550..0c14571c5 100644 --- a/Dalamud/Plugin/Services/ITargetManager.cs +++ b/Dalamud/Plugin/Services/ITargetManager.cs @@ -1,7 +1,7 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; -namespace Dalamud.Game.ClientState.Objects; +namespace Dalamud.Plugin.Services; /// /// Get and set various kinds of targets for the player. @@ -37,13 +37,13 @@ public interface ITargetManager : IDalamudService /// Set to null to clear the target. /// public IGameObject? SoftTarget { get; set; } - + /// /// Gets or sets the gpose target. /// Set to null to clear the target. /// public IGameObject? GPoseTarget { get; set; } - + /// /// Gets or sets the mouseover nameplate target. /// Set to null to clear the target. From c525655be66778cd3d092d32f6ae5aba16c0fbea Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:24:35 -0800 Subject: [PATCH 037/164] Improve LifecycleInvoke efficiency with Dictionary --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 0c23f5661..cf1270803 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal List EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,10 +61,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => - { - this.EventListeners.Add(listener); - }); + this.EventListeners.TryAdd(listener.EventType, [ listener ]); + this.EventListeners[listener.EventType].Add(listener); } /// @@ -73,13 +71,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. - listener.Removed = true; - - this.framework.RunOnTick(() => + if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) { - this.EventListeners.Remove(listener); - }); + listenerList.Remove(listener); + } } /// @@ -90,16 +85,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// What to blame on errors. internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { + // Early return if we don't have any listeners of this type + if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in this.EventListeners) + foreach (var listener in listenerList) { - if (listener.EventType != eventType) - continue; - - // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener. - if (listener.Removed) - continue; - // Match on string.empty for listeners that want events for all addons. if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) continue; From 166f249e13ed310db4bc44a658d8d473b92ae6a2 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:30:40 -0800 Subject: [PATCH 038/164] Use hashset to prevent duplicate entries --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cf1270803..403671920 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() From 29c154f9b5a2d7ab1cabd41cbaac432d06c2b27d Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 08:35:54 -0800 Subject: [PATCH 039/164] Fix accidentally breaking widget --- .../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index c336f895e..73c4e540a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; @@ -58,12 +57,11 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var eventType in Enum.GetValues()) + foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(eventType.ToString())) + if (ImGui.CollapsingHeader(listenerType.ToString())) { ImGui.Indent(); - var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList(); if (listeners.Count == 0) { From 325d28ee3211d7743fc407f9f934e4bbc66ec48a Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:08:24 -0800 Subject: [PATCH 040/164] further improve performance --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 61 ++++++++++++++----- .../Data/Widgets/AddonLifecycleWidget.cs | 40 ++++++------ 2 files changed, 67 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 403671920..e38f56921 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + /// Mapping is: EventType -> AddonName -> ListenerList + internal Dictionary>> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,8 +62,18 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.EventListeners.TryAdd(listener.EventType, [ listener ]); - this.EventListeners[listener.EventType].Add(listener); + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + this.EventListeners.TryAdd(listener.EventType, []); + } + + // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) + { + this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []); + } + + this.EventListeners[listener.EventType][listener.AddonName].Add(listener); } /// @@ -71,9 +82,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) + if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) { - listenerList.Remove(listener); + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + { + addonListener.Remove(listener); + } } } @@ -86,22 +100,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { // Early return if we don't have any listeners of this type - if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; - // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in listenerList) + // Handle listeners for this event type that don't care which addon is triggering it + if (addonListeners.TryGetValue(string.Empty, out var globalListeners)) { - // Match on string.empty for listeners that want events for all addons. - if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) - continue; - - try + foreach (var listener in globalListeners) { - listener.FunctionDelegate.Invoke(eventType, args); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener."); + } } - catch (Exception e) + } + + // Handle listeners that are listening for this addon and event type specifically + if (addonListeners.TryGetValue(args.AddonName, out var addonListener)) + { + foreach (var listener in addonListener) { - Log.Error(e, $"Exception in {blame} during {eventType} invoke."); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}."); + } } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 73c4e540a..0f193556b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -2,7 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -57,35 +58,38 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) + foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(listenerType.ToString())) + using var eventId = ImRaii.PushId(eventType.ToString()); + + if (ImGui.CollapsingHeader(eventType.ToString())) { - ImGui.Indent(); + using var eventIndent = ImRaii.PushIndent(); - if (listeners.Count == 0) + if (addonListeners.Count == 0) { - ImGui.Text("No Listeners Registered for Event"u8); + ImGui.Text("No Addons Registered for Event"u8); } - if (ImGui.BeginTable("AddonLifecycleListenersTable"u8, 2)) + foreach (var (addonName, listeners) in addonListeners) { - ImGui.TableSetupColumn("##AddonName"u8, ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##MethodInvoke"u8, ImGuiTableColumnFlags.WidthStretch); + using var addonId = ImRaii.PushId(addonName); - foreach (var listener in listeners) + if (ImGui.CollapsingHeader(addonName.IsNullOrEmpty() ? "GLOBAL" : addonName)) { - ImGui.TableNextColumn(); - ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); + using var addonIndent = ImRaii.PushIndent(); - ImGui.TableNextColumn(); - ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + if (listeners.Count == 0) + { + ImGui.Text("No Listeners Registered for Event"u8); + } + + foreach (var listener in listeners) + { + ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + } } - - ImGui.EndTable(); } - - ImGui.Unindent(); } } } From 170f6e08599d4853acc260aa3a442171d30a1731 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:11:13 -0800 Subject: [PATCH 041/164] Remove redundant header --- .../Windows/Data/Widgets/AddonLifecycleWidget.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 0f193556b..4fb13b81a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -46,18 +46,6 @@ public class AddonLifecycleWidget : IDataWindowWidget return; } - if (ImGui.CollapsingHeader("Listeners"u8)) - { - ImGui.Indent(); - this.DrawEventListeners(); - ImGui.Unindent(); - } - } - - private void DrawEventListeners() - { - if (!this.Ready) return; - foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { using var eventId = ImRaii.PushId(eventType.ToString()); From b8724f7a59b5cb3dd0b454dff384b7c0c7b0d355 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:44:35 -0800 Subject: [PATCH 042/164] Fix copy paste error --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index e38f56921..d3d0fcebe 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -37,7 +37,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. - /// + ///
/// Mapping is: EventType -> AddonName -> ListenerList internal Dictionary>> EventListeners { get; } = []; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 54c91248e..db698e626 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -167,7 +167,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); try { From ead1c705a427ee596ca5a4eff741b0d68fe07fc0 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 29 Nov 2025 17:07:51 -0800 Subject: [PATCH 043/164] fix: Route URIs to the specified InternalName --- Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs index 4dbe3fdf1..3b7f18437 100644 --- a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs +++ b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs @@ -45,7 +45,7 @@ public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler private void HandleUri(DalamudUri uri) { - var target = uri.Path.Split("/").FirstOrDefault(); + var target = uri.Path.Split("/").ElementAtOrDefault(1); var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName); if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase)) { From 874745651b1be57b391a078c627a66d7932e30aa Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 29 Nov 2025 21:07:51 -0800 Subject: [PATCH 044/164] feat: Add PID, process time, rename ClientIdentifer to ClientState --- .../Rpc/Service/ClientHelloService.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs index 041bc135f..c5a4c851a 100644 --- a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs +++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System.Diagnostics; +using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Game; @@ -39,7 +40,9 @@ internal sealed class ClientHelloService : IInternalDisposableService ApiVersion = "1.0", DalamudVersion = Util.GetScmVersion(), GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", - ClientIdentifier = await this.GetClientIdentifier(), + ProcessId = Environment.ProcessId, + ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(), + ClientState = await this.GetClientIdentifier(), }; } @@ -114,7 +117,17 @@ internal record ClientHelloResponse public string? GameVersion { get; init; } /// - /// Gets an identifier for this client. + /// Gets the process ID of this client. /// - public string? ClientIdentifier { get; init; } + public int? ProcessId { get; init; } + + /// + /// Gets the time this process started. + /// + public long? ProcessStartTime { get; init; } + + /// + /// Gets a state for this client for user display. + /// + public string? ClientState { get; init; } } From c51e65e0bd07bc58bfab65128cb7719ce56c996b Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:08:40 -0800 Subject: [PATCH 045/164] Better unload --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 33 +++++-------------- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 15 +++------ 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index d3d0fcebe..5d121bea4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -19,13 +19,13 @@ namespace Dalamud.Game.Addon.Lifecycle; [ServiceManager.EarlyLoadedService] internal unsafe class AddonLifecycle : IInternalDisposableService { + /// + /// Gets a list of all allocated addon virtual tables. + /// + public static readonly List AllocatedTables = []; + private static readonly ModuleLog Log = new("AddonLifecycle"); - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - - private readonly Dictionary modifiedTables = []; - private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] @@ -47,13 +47,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - this.framework.RunOnFrameworkThread(() => - { - foreach (var virtualTable in this.modifiedTables.Values) - { - virtualTable.Dispose(); - } - }); + AllocatedTables.ForEach(entry => entry.Dispose()); + AllocatedTables.Clear(); } /// @@ -141,18 +136,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.LogInitialize(addon->NameString); - if (!this.modifiedTables.ContainsKey(addon->NameString)) - { - // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions - var managedVirtualTableEntry = new AddonVirtualTable(addon, this) - { - // This event is invoked when the game itself has disposed of an addon - // We can use this to know when to remove our virtual table entry - OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString), - }; - - this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry); - } + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + AllocatedTables.Add(new AddonVirtualTable(addon, this)); } catch (Exception e) { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index db698e626..d91cd648f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Logging.Internal; @@ -108,17 +109,11 @@ internal unsafe class AddonVirtualTable : IDisposable this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); } - /// - /// Gets an event that is invoked when this addon's Finalize method is called from native. - /// - public required Action OnAddonFinalized { get; init; } - - /// - /// WARNING! This should not be called at any time except during dalamud unload. - /// + /// public void Dispose() { - this.atkUnitBase->VirtualTable = this.originalVirtualTable; + // Ensure restoration is done atomically. + Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable); IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); } @@ -131,7 +126,7 @@ internal unsafe class AddonVirtualTable : IDisposable if ((freeFlags & 1) == 1) { IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); - this.OnAddonFinalized(); + AddonLifecycle.AllocatedTables.Remove(this); } return result; From 26f119096bad6fd3111a6c5f0ad977f53e396384 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:39:35 -0800 Subject: [PATCH 046/164] Bunch of stuff... --- Dalamud/Configuration/PluginConfigurations.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 31 ++----------------- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 9 ++++-- .../AddonArgTypes/AddonFinalizeArgs.cs | 7 +++-- .../AddonArgTypes/AddonGenericArgs.cs | 3 +- .../AddonArgTypes/AddonReceiveEventArgs.cs | 18 +++-------- .../AddonArgTypes/AddonRefreshArgs.cs | 15 +++------ .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 11 +------ .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 15 +++------ .../AddonArgTypes/AddonUpdateArgs.cs | 26 +++++++--------- .../AddonLifecycleAddressResolver.cs | 2 +- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 14 --------- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +- Dalamud/Interface/Animation/Easing.cs | 2 +- ...ToDoAttribute.cs => Api14ToDoAttribute.cs} | 6 ++-- Dalamud/Utility/Api15ToDoAttribute.cs | 25 +++++++++++++++ Dalamud/Utility/Util.cs | 2 +- 17 files changed, 74 insertions(+), 116 deletions(-) rename Dalamud/Utility/{Api13ToDoAttribute.cs => Api14ToDoAttribute.cs} (75%) create mode 100644 Dalamud/Utility/Api15ToDoAttribute.cs diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index fa2969d31..c01ab2af0 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -11,7 +11,7 @@ namespace Dalamud.Configuration; /// /// Configuration to store settings for a dalamud plugin. /// -[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] +[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 0b2ae1178..62ca47238 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -33,41 +33,14 @@ public abstract class AddonArgs /// public abstract AddonArgsType Type { get; } - /// - /// Checks if addon name matches the given span of char. - /// - /// The name to check. - /// Whether it is the case. - internal bool IsAddon(string name) - { - if (this.Addon.IsNull) - return false; - - if (name.Length is 0 or > 32) - return false; - - if (string.IsNullOrEmpty(this.Addon.Name)) - return false; - - return name == this.Addon.Name; - } - - /// - /// Clears this AddonArgs values. - /// - internal virtual void Clear() - { - this.addonName = null; - this.Addon = 0; - } - /// /// Helper method for ensuring the name of the addon is valid. /// /// The name of the addon for this object. when invalid. private string GetAddonName() { - if (this.Addon.IsNull) return InvalidAddon; + if (this.Addon.IsNull) + return InvalidAddon; var name = this.Addon.Name; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 7254ba7b3..a834d2983 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -1,15 +1,18 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Utility; + +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Draw events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonDrawArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonDrawArgs() + internal AddonDrawArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index 12def3ad3..11d15a081 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -1,15 +1,18 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonFinalizeArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonFinalizeArgs() + internal AddonFinalizeArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs index f3078af69..a20e9d23b 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs @@ -8,8 +8,7 @@ public class AddonGenericArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonGenericArgs() + internal AddonGenericArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 05f51b118..bb8168075 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// @@ -8,8 +10,7 @@ public class AddonReceiveEventArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonReceiveEventArgs() + internal AddonReceiveEventArgs() { } @@ -32,17 +33,8 @@ public class AddonReceiveEventArgs : AddonArgs public nint AtkEvent { get; set; } /// - /// Gets or sets the pointer to a block of data for this event message. + /// Gets or sets the pointer to an AtkEventData for this event message. /// + [Api14ToDo("Rename to AtkEventData")] public nint Data { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - this.AtkEventType = 0; - this.EventParam = 0; - this.AtkEvent = 0; - this.Data = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index c01c065c1..8af017318 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRefreshArgs() + internal AddonRefreshArgs() { } @@ -31,13 +32,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = 0; - this.AtkValues = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index bf00c5d6e..7005b77c2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -8,8 +8,7 @@ public class AddonRequestedUpdateArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRequestedUpdateArgs() + internal AddonRequestedUpdateArgs() { } @@ -25,12 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs /// Gets or sets the StringArrayData** for this event. /// public nint StringArrayData { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - this.NumberArrayData = 0; - this.StringArrayData = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 9b7e86a61..9fd7b6dd0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonSetupArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonSetupArgs() + internal AddonSetupArgs() { } @@ -31,13 +32,7 @@ public class AddonSetupArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = 0; - this.AtkValues = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index bab62fc89..e6147d0eb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -1,39 +1,35 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonUpdateArgs() + internal AddonUpdateArgs() { } /// public override AddonArgsType Type => AddonArgsType.Update; - /// - /// Gets the time since the last update. - /// - public float TimeDelta - { - get => this.TimeDeltaInternal; - init => this.TimeDeltaInternal = value; - } - /// /// Gets or sets the time since the last update. /// internal float TimeDeltaInternal { get; set; } - /// - internal override void Clear() + /// + /// Gets the time since the last update. + /// + private float TimeDelta { - base.Clear(); - this.TimeDeltaInternal = 0; + get => this.TimeDeltaInternal; + init => this.TimeDeltaInternal = value; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 9359870a5..2fa3c5b91 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] +[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index d91cd648f..49ffdc7fb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -26,8 +26,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonLifecycle lifecycleService; - // Obsolete warning is only to prevent users from creating their own event objects. -#pragma warning disable CS0618 // Type or member is obsolete private readonly AddonSetupArgs addonSetupArg = new(); private readonly AddonFinalizeArgs addonFinalizeArg = new(); private readonly AddonDrawArgs addonDrawArg = new(); @@ -36,7 +34,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); private readonly AddonGenericArgs addonGenericArg = new(); -#pragma warning restore CS0618 // Type or member is obsolete private readonly AtkUnitBase* atkUnitBase; @@ -136,7 +133,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; this.addonSetupArg.AtkValueCount = valueCount; this.addonSetupArg.AtkValues = (nint)values; @@ -160,7 +156,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); @@ -178,7 +173,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); @@ -198,7 +192,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; this.addonUpdateArg.TimeDeltaInternal = delta; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); @@ -221,7 +214,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonRefreshArg.Clear(); this.addonRefreshArg.Addon = addon; this.addonRefreshArg.AtkValueCount = valueCount; this.addonRefreshArg.AtkValues = (nint)values; @@ -246,7 +238,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; @@ -270,7 +261,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; this.addonReceiveEventArg.AtkEventType = (byte)eventType; this.addonReceiveEventArg.EventParam = eventParam; @@ -300,7 +290,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); @@ -324,7 +313,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); @@ -346,7 +334,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); @@ -366,7 +353,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index f5b7011fe..af85f9228 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry } /// - [Api13ToDo("Maybe make this config scoped to internal name?")] + [Api14ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index 0d2057b3b..cc1f48ce7 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -48,7 +48,7 @@ public abstract class Easing /// Gets the current value of the animation, following unclamped logic. /// [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] - [Api13ToDo("Map this field to ValueClamped, probably.")] + [Api14ToDo("Map this field to ValueClamped, probably.")] public double Value => this.ValueUnclamped; /// diff --git a/Dalamud/Utility/Api13ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs similarity index 75% rename from Dalamud/Utility/Api13ToDoAttribute.cs rename to Dalamud/Utility/Api14ToDoAttribute.cs index 576401cda..945b6e4db 100644 --- a/Dalamud/Utility/Api13ToDoAttribute.cs +++ b/Dalamud/Utility/Api14ToDoAttribute.cs @@ -4,7 +4,7 @@ namespace Dalamud.Utility; /// Utility class for marking something to be changed for API 13, for ease of lookup. /// [AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api13ToDoAttribute : Attribute +internal sealed class Api14ToDoAttribute : Attribute { /// /// Marks that this should be made internal. @@ -12,11 +12,11 @@ internal sealed class Api13ToDoAttribute : Attribute public const string MakeInternal = "Make internal."; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The explanation. /// The explanation 2. - public Api13ToDoAttribute(string what, string what2 = "") + public Api14ToDoAttribute(string what, string what2 = "") { _ = what; _ = what2; diff --git a/Dalamud/Utility/Api15ToDoAttribute.cs b/Dalamud/Utility/Api15ToDoAttribute.cs new file mode 100644 index 000000000..646c260e8 --- /dev/null +++ b/Dalamud/Utility/Api15ToDoAttribute.cs @@ -0,0 +1,25 @@ +namespace Dalamud.Utility; + +/// +/// Utility class for marking something to be changed for API 13, for ease of lookup. +/// Intended to represent not the upcoming API, but the one after it for more major changes. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class Api15ToDoAttribute : Attribute +{ + /// + /// Marks that this should be made internal. + /// + public const string MakeInternal = "Make internal."; + + /// + /// Initializes a new instance of the class. + /// + /// The explanation. + /// The explanation 2. + public Api15ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 2a3733303..ba31f47e5 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -79,7 +79,7 @@ public static partial class Util /// /// Gets the Dalamud version. /// - [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] + [Api14ToDo("Remove. Make both versions here internal. Add an API somewhere.")] public static string AssemblyVersion { get; } = Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); From 54bac7f32a7efe5268d8fa3697f185184134bab7 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 17:27:48 -0800 Subject: [PATCH 047/164] Refactor Addon Lifecycle --- .../Game/Addon/AddonLifecyclePooledArgs.cs | 107 ----- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +- .../AddonArgTypes/AddonFinalizeArgs.cs | 8 +- .../AddonArgTypes/AddonGenericArgs.cs | 18 + .../AddonArgTypes/AddonReceiveEventArgs.cs | 16 +- .../AddonArgTypes/AddonRefreshArgs.cs | 12 +- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 12 +- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 12 +- .../AddonArgTypes/AddonUpdateArgs.cs | 10 +- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +- Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 54 ++- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 298 ++----------- .../AddonLifecycleAddressResolver.cs | 38 +- .../AddonLifecycleReceiveEventListener.cs | 112 ----- .../Game/Addon/Lifecycle/AddonSetupHook.cs | 80 ---- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 405 ++++++++++++++++++ Dalamud/Hooking/Internal/CallHook.cs | 100 ----- .../Data/Widgets/AddonLifecycleWidget.cs | 51 --- 19 files changed, 543 insertions(+), 825 deletions(-) delete mode 100644 Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs delete mode 100644 Dalamud/Hooking/Internal/CallHook.cs diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs deleted file mode 100644 index 14def2036..000000000 --- a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Threading; - -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -namespace Dalamud.Game.Addon; - -/// Argument pool for Addon Lifecycle services. -[ServiceManager.EarlyLoadedService] -internal sealed class AddonLifecyclePooledArgs : IServiceType -{ - private readonly AddonSetupArgs?[] addonSetupArgPool = new AddonSetupArgs?[64]; - private readonly AddonFinalizeArgs?[] addonFinalizeArgPool = new AddonFinalizeArgs?[64]; - private readonly AddonDrawArgs?[] addonDrawArgPool = new AddonDrawArgs?[64]; - private readonly AddonUpdateArgs?[] addonUpdateArgPool = new AddonUpdateArgs?[64]; - private readonly AddonRefreshArgs?[] addonRefreshArgPool = new AddonRefreshArgs?[64]; - private readonly AddonRequestedUpdateArgs?[] addonRequestedUpdateArgPool = new AddonRequestedUpdateArgs?[64]; - private readonly AddonReceiveEventArgs?[] addonReceiveEventArgPool = new AddonReceiveEventArgs?[64]; - - [ServiceManager.ServiceConstructor] - private AddonLifecyclePooledArgs() - { - } - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonSetupArgs arg) => new(out arg, this.addonSetupArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonFinalizeArgs arg) => new(out arg, this.addonFinalizeArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonDrawArgs arg) => new(out arg, this.addonDrawArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonUpdateArgs arg) => new(out arg, this.addonUpdateArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonRefreshArgs arg) => new(out arg, this.addonRefreshArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonRequestedUpdateArgs arg) => - new(out arg, this.addonRequestedUpdateArgPool); - - /// Rents an instance of an argument. - /// The rented instance. - /// The returner. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public PooledEntry Rent(out AddonReceiveEventArgs arg) => - new(out arg, this.addonReceiveEventArgPool); - - /// Returns the object to the pool on dispose. - /// The type. - public readonly ref struct PooledEntry - where T : AddonArgs, new() - { - private readonly Span pool; - private readonly T obj; - - /// Initializes a new instance of the struct. - /// An instance of the argument. - /// The pool to rent from and return to. - public PooledEntry(out T arg, Span pool) - { - this.pool = pool; - foreach (ref var item in pool) - { - if (Interlocked.Exchange(ref item, null) is { } v) - { - this.obj = arg = v; - return; - } - } - - this.obj = arg = new(); - } - - /// Returns the item to the pool. - public void Dispose() - { - var tmp = this.obj; - foreach (ref var item in this.pool) - { - if (Interlocked.Exchange(ref item, tmp) is not { } tmp2) - return; - tmp = tmp2; - } - } - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index c008db08f..0b2ae1178 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Base class for AddonLifecycle AddonArgTypes. /// -public abstract unsafe class AddonArgs +public abstract class AddonArgs { /// /// Constant string representing the name of an addon that is invalid. diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 989e11912..7254ba7b3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -3,7 +3,7 @@ /// /// Addon argument data for Draw events. /// -public class AddonDrawArgs : AddonArgs, ICloneable +public class AddonDrawArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -15,10 +15,4 @@ public class AddonDrawArgs : AddonArgs, ICloneable /// public override AddonArgsType Type => AddonArgsType.Draw; - - /// - public AddonDrawArgs Clone() => (AddonDrawArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index d9401b414..12def3ad3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonFinalizeArgs : AddonArgs, ICloneable +public class AddonFinalizeArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -15,10 +15,4 @@ public class AddonFinalizeArgs : AddonArgs, ICloneable /// public override AddonArgsType Type => AddonArgsType.Finalize; - - /// - public AddonFinalizeArgs Clone() => (AddonFinalizeArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs new file mode 100644 index 000000000..f3078af69 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Draw events. +/// +public class AddonGenericArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonGenericArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Generic; +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 980fe4f2f..05f51b118 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonReceiveEventArgs : AddonArgs, ICloneable +public class AddonReceiveEventArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -36,19 +36,13 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable /// public nint Data { get; set; } - /// - public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkEventType = default; - this.EventParam = default; - this.AtkEvent = default; - this.Data = default; + this.AtkEventType = 0; + this.EventParam = 0; + this.AtkEvent = 0; + this.Data = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index d28631c3c..c01c065c1 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Refresh events. /// -public class AddonRefreshArgs : AddonArgs, ICloneable +public class AddonRefreshArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -33,17 +33,11 @@ public class AddonRefreshArgs : AddonArgs, ICloneable /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; + this.AtkValueCount = 0; + this.AtkValues = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index e87a980fd..bf00c5d6e 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for OnRequestedUpdate events. /// -public class AddonRequestedUpdateArgs : AddonArgs, ICloneable +public class AddonRequestedUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -26,17 +26,11 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable /// public nint StringArrayData { get; set; } - /// - public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.NumberArrayData = default; - this.StringArrayData = default; + this.NumberArrayData = 0; + this.StringArrayData = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 0dd9ecee2..9b7e86a61 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Setup events. /// -public class AddonSetupArgs : AddonArgs, ICloneable +public class AddonSetupArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -33,17 +33,11 @@ public class AddonSetupArgs : AddonArgs, ICloneable /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - /// - public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.AtkValueCount = default; - this.AtkValues = default; + this.AtkValueCount = 0; + this.AtkValues = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index a263f6ae4..bab62fc89 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// -public class AddonUpdateArgs : AddonArgs, ICloneable +public class AddonUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. @@ -30,16 +30,10 @@ public class AddonUpdateArgs : AddonArgs, ICloneable /// internal float TimeDeltaInternal { get; set; } - /// - public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); - - /// - object ICloneable.Clone() => this.Clone(); - /// internal override void Clear() { base.Clear(); - this.TimeDeltaInternal = default; + this.TimeDeltaInternal = 0; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index b58b5f4c7..95dc5f718 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -9,34 +9,39 @@ public enum AddonArgsType /// Contains argument data for Setup. /// Setup, - + /// /// Contains argument data for Update. /// Update, - + /// /// Contains argument data for Draw. - /// + /// Draw, - + /// /// Contains argument data for Finalize. - /// + /// Finalize, - + /// /// Contains argument data for RequestedUpdate. - /// + /// RequestedUpdate, - + /// /// Contains argument data for Refresh. - /// + ///
Refresh, - + /// /// Contains argument data for ReceiveEvent. /// ReceiveEvent, + + /// + /// Generic arg type that contains no meaningful data + /// + Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 5fd0ac964..7738d6c6a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -16,7 +16,7 @@ public enum AddonEvent ///
/// PreSetup, - + /// /// An event that is fired after an addon has finished its initial setup. This event is particularly useful for /// developers seeking to add custom elements to now-initialized and populated node lists, as well as reading data @@ -64,7 +64,7 @@ public enum AddonEvent /// /// PreFinalize, - + /// /// An event that is fired before a call to is made in response to a /// change in the subscribed or @@ -81,13 +81,13 @@ public enum AddonEvent /// to the Free Company's overview. /// PreRequestedUpdate, - + /// /// An event that is fired after an addon has finished processing an ArrayData update. /// See for more information. /// PostRequestedUpdate, - + /// /// An event that is fired before an addon calls its method. Refreshes are /// generally triggered in response to certain user interactions such as changing tabs, and are primarily used to @@ -96,13 +96,13 @@ public enum AddonEvent /// /// PreRefresh, - + /// /// An event that is fired after an addon has finished its refresh. /// See for more information. /// PostRefresh, - + /// /// An event that is fired before an addon begins processing a user-driven event via /// , such as mousing over an element or clicking a button. This event @@ -112,10 +112,50 @@ public enum AddonEvent /// /// PreReceiveEvent, - + /// /// An event that is fired after an addon finishes calling its method. /// See for more information. /// PostReceiveEvent, + + /// + /// An event that is fired before an addon processes its open method. + /// + PreOpen, + + /// + /// An event that is fired after an addon has processed its open method. + /// + PostOpen, + + /// + /// An even that is fired before an addon processes its close method. + /// + PreClose, + + /// + /// An event that is fired after an addon has processed its close method. + /// + PostClose, + + /// + /// An event that is fired before an addon processes its show method. + /// + PreShow, + + /// + /// An event that is fired after an addon has processed its show method. + /// + PostShow, + + /// + /// An event that is fired before an addon processes its hide method. + /// + PreHide, + + /// + /// An event that is fired after an addon has processed its hide method. + /// + PostHide, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index b44ab8764..cea30d6be 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,16 +1,14 @@ using System.Collections.Generic; -using System.Linq; +using System.Diagnostics; using System.Runtime.CompilerServices; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; -using Dalamud.Hooking.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle; @@ -26,69 +24,33 @@ internal unsafe class AddonLifecycle : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); + private readonly Dictionary modifiedTables = []; - private readonly nint disallowedReceiveEventAddress; - - private readonly AddonLifecycleAddressResolver address; - private readonly AddonSetupHook onAddonSetupHook; - private readonly Hook onAddonFinalizeHook; - private readonly CallHook onAddonDrawHook; - private readonly CallHook onAddonUpdateHook; - private readonly Hook onAddonRefreshHook; - private readonly CallHook onAddonRequestedUpdateHook; + private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) { - this.address = new AddonLifecycleAddressResolver(); - this.address.Setup(sigScanner); + this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); + this.onInitializeAddonHook.Enable(); - this.disallowedReceiveEventAddress = (nint)AtkUnitBase.StaticVirtualTablePointer->ReceiveEvent; - - var refreshAddonAddress = (nint)RaptureAtkUnitManager.StaticVirtualTablePointer->RefreshAddon; - - this.onAddonSetupHook = new AddonSetupHook(this.address.AddonSetup, this.OnAddonSetup); - this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); - this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); - this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); - this.onAddonRefreshHook = Hook.FromAddress(refreshAddonAddress, this.OnAddonRefresh); - this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); - - this.onAddonSetupHook.Enable(); - this.onAddonFinalizeHook.Enable(); - this.onAddonDrawHook.Enable(); - this.onAddonUpdateHook.Enable(); - this.onAddonRefreshHook.Enable(); - this.onAddonRequestedUpdateHook.Enable(); + Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}"); } - private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); - - /// - /// Gets a list of all AddonLifecycle ReceiveEvent Listener Hooks. - /// - internal List ReceiveEventListeners { get; } = new(); - /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal List EventListeners { get; } = new(); + internal List EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() { - this.onAddonSetupHook.Dispose(); - this.onAddonFinalizeHook.Dispose(); - this.onAddonDrawHook.Dispose(); - this.onAddonUpdateHook.Dispose(); - this.onAddonRefreshHook.Dispose(); - this.onAddonRequestedUpdateHook.Dispose(); + this.onInitializeAddonHook?.Dispose(); + this.onInitializeAddonHook = null; - foreach (var receiveEventListener in this.ReceiveEventListeners) + foreach (var virtualTable in this.modifiedTables.Values) { - receiveEventListener.Dispose(); + virtualTable.Dispose(); } } @@ -101,16 +63,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.framework.RunOnTick(() => { this.EventListeners.Add(listener); - - // If we want receive event messages have an already active addon, enable the receive event hook. - // If the addon isn't active yet, we'll grab the hook when it sets up. - if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) - { - receiveEventListener.TryEnable(); - } - } }); } @@ -122,24 +74,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. listener.Removed = true; - + this.framework.RunOnTick(() => { this.EventListeners.Remove(listener); - - // If we are disabling an ReceiveEvent listener, check if we should disable the hook. - if (listener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - // Get the ReceiveEvent Listener for this addon - if (this.ReceiveEventListeners.FirstOrDefault(listeners => listeners.AddonNames.Contains(listener.AddonName)) is { } receiveEventListener) - { - // If there are no other listeners listening for this event, disable the hook. - if (!this.EventListeners.Any(listeners => listeners.AddonName.Contains(listener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent)) - { - receiveEventListener.Disable(); - } - } - } }); } @@ -160,7 +98,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener. if (listener.Removed) continue; - + // Match on string.empty for listeners that want events for all addons. if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) continue; @@ -176,201 +114,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } } - private void RegisterReceiveEventHook(AtkUnitBase* addon) + private void OnAddonInitialize(AtkUnitBase* addon) { - // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. - // Disallows hooking the core internal event handler. - var addonName = addon->NameString; - var receiveEventAddress = (nint)addon->VirtualTable->ReceiveEvent; - if (receiveEventAddress != this.disallowedReceiveEventAddress) + try { - // If we have a ReceiveEvent listener already made for this hook address, add this addon's name to that handler. - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.FunctionAddress == receiveEventAddress) is { } existingListener) + this.LogInitialize(addon->NameString); + + if (!this.modifiedTables.ContainsKey(addon->NameString)) { - if (!existingListener.AddonNames.Contains(addonName)) + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + var managedVirtualTableEntry = new AddonVirtualTable(addon, this) { - existingListener.AddonNames.Add(addonName); - } - } + // This event is invoked when the game itself has disposed of an addon + // We can use this to know when to remove our virtual table entry + OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString), + }; - // Else, we have an addon that we don't have the ReceiveEvent for yet, make it. - else - { - this.ReceiveEventListeners.Add(new AddonLifecycleReceiveEventListener(this, addonName, receiveEventAddress)); - } - - // If we have an active listener for this addon already, we need to activate this hook. - if (this.EventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName)) - { - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } receiveEventListener) - { - receiveEventListener.TryEnable(); - } + this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry); } } + catch (Exception e) + { + Log.Error(e, "Exception in AddonLifecycle during OnAddonInitialize."); + } + + this.onInitializeAddonHook!.Original(addon); } - private void UnregisterReceiveEventHook(string addonName) + [Conditional("DEBUG")] + private void LogInitialize(string addonName) { - // Remove this addons ReceiveEvent Registration - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(addonName)) is { } eventListener) - { - eventListener.AddonNames.Remove(addonName); - - // If there are no more listeners let's remove and dispose. - if (eventListener.AddonNames.Count is 0) - { - this.ReceiveEventListeners.Remove(eventListener); - eventListener.Dispose(); - } - } - } - - private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) - { - try - { - this.RegisterReceiveEventHook(addon); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); - } - - using var returner = this.argsPool.Rent(out AddonSetupArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkValueCount = valueCount; - arg.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreSetup, arg); - valueCount = arg.AtkValueCount; - values = (AtkValue*)arg.AtkValues; - - try - { - addon->OnSetup(valueCount, values); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostSetup, arg); - } - - private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) - { - try - { - var addonName = atkUnitBase[0]->NameString; - this.UnregisterReceiveEventHook(addonName); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); - } - - using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg); - arg.Clear(); - arg.Addon = (nint)atkUnitBase[0]; - this.InvokeListenersSafely(AddonEvent.PreFinalize, arg); - - try - { - this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); - } - } - - private void OnAddonDraw(AtkUnitBase* addon) - { - using var returner = this.argsPool.Rent(out AddonDrawArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - this.InvokeListenersSafely(AddonEvent.PreDraw, arg); - - try - { - addon->Draw(); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostDraw, arg); - } - - private void OnAddonUpdate(AtkUnitBase* addon, float delta) - { - using var returner = this.argsPool.Rent(out AddonUpdateArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.TimeDeltaInternal = delta; - this.InvokeListenersSafely(AddonEvent.PreUpdate, arg); - - try - { - addon->Update(delta); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostUpdate, arg); - } - - private bool OnAddonRefresh(AtkUnitManager* thisPtr, AtkUnitBase* addon, uint valueCount, AtkValue* values) - { - var result = false; - - using var returner = this.argsPool.Rent(out AddonRefreshArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkValueCount = valueCount; - arg.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreRefresh, arg); - valueCount = arg.AtkValueCount; - values = (AtkValue*)arg.AtkValues; - - try - { - result = this.onAddonRefreshHook.Original(thisPtr, addon, valueCount, values); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostRefresh, arg); - return result; - } - - private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) - { - using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.NumberArrayData = (nint)numberArrayData; - arg.StringArrayData = (nint)stringArrayData; - this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg); - numberArrayData = (NumberArrayData**)arg.NumberArrayData; - stringArrayData = (StringArrayData**)arg.StringArrayData; - - try - { - addon->OnRequestedUpdate(numberArrayData, stringArrayData); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); - } - - this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg); + Log.Debug($"Initializing {addonName}"); } } @@ -387,7 +161,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); - private readonly List eventListeners = new(); + private readonly List eventListeners = []; /// void IInternalDisposableService.DisposeService() @@ -458,7 +232,7 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi this.eventListeners.RemoveAll(entry => { if (entry.FunctionDelegate != handler) return false; - + this.addonLifecycleService.UnregisterListener(entry); return true; }); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 854d666fd..1d767aac4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -1,56 +1,24 @@ -using FFXIVClientStructs.FFXIV.Component.GUI; +using Dalamud.Utility; namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -internal unsafe class AddonLifecycleAddressResolver : BaseAddressResolver +[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")] +internal class AddonLifecycleAddressResolver : BaseAddressResolver { - /// - /// Gets the address of the addon setup hook invoked by the AtkUnitManager. - /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue. - /// This is called for a majority of all addon OnSetup's. - /// - public nint AddonSetup { get; private set; } - - /// - /// Gets the address of the other addon setup hook invoked by the AtkUnitManager. - /// There are two callsites for this vFunc, we need to hook both of them to catch both normal UI and special UI cases like dialogue. - /// This seems to be called rarely for specific addons. - /// - public nint AddonSetup2 { get; private set; } - /// /// Gets the address of the addon finalize hook invoked by the AtkUnitManager. /// public nint AddonFinalize { get; private set; } - /// - /// Gets the address of the addon draw hook invoked by virtual function call. - /// - public nint AddonDraw { get; private set; } - - /// - /// Gets the address of the addon update hook invoked by virtual function call. - /// - public nint AddonUpdate { get; private set; } - - /// - /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. - /// - public nint AddonOnRequestedUpdate { get; private set; } - /// /// Scan for and setup any configured address pointers. /// /// The signature scanner to facilitate setup. protected override void Setup64Bit(ISigScanner sig) { - this.AddonSetup = sig.ScanText("4C 8B 88 ?? ?? ?? ?? 66 44 39 BB"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); - this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C4 48 81 EF ?? ?? ?? ?? 48 83 ED 01"); - this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF ?? ?? ?? ?? 45 33 D2"); - this.AddonOnRequestedUpdate = sig.ScanText("FF 90 A0 01 00 00 48 8B 5C 24 30"); } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs deleted file mode 100644 index 0d2bcc7f2..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Collections.Generic; - -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; -using Dalamud.Hooking; -using Dalamud.Logging.Internal; - -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// This class is a helper for tracking and invoking listener delegates for Addon_OnReceiveEvent. -/// Multiple addons may use the same ReceiveEvent function, this helper makes sure that those addon events are handled properly. -/// -internal unsafe class AddonLifecycleReceiveEventListener : IDisposable -{ - private static readonly ModuleLog Log = new("AddonLifecycle"); - - [ServiceManager.ServiceDependency] - private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); - - /// - /// Initializes a new instance of the class. - /// - /// AddonLifecycle service instance. - /// Initial Addon Requesting this listener. - /// Address of Addon's ReceiveEvent function. - internal AddonLifecycleReceiveEventListener(AddonLifecycle service, string addonName, nint receiveEventAddress) - { - this.AddonLifecycle = service; - this.AddonNames = [addonName]; - this.FunctionAddress = receiveEventAddress; - } - - /// - /// Gets the list of addons that use this receive event hook. - /// - public List AddonNames { get; init; } - - /// - /// Gets the address of the ReceiveEvent function as provided by the vtable on setup. - /// - public nint FunctionAddress { get; init; } - - /// - /// Gets the contained hook for these addons. - /// - public Hook? Hook { get; private set; } - - /// - /// Gets or sets the Reference to AddonLifecycle service instance. - /// - private AddonLifecycle AddonLifecycle { get; set; } - - /// - /// Try to hook and enable this receive event handler. - /// - public void TryEnable() - { - this.Hook ??= Hook.FromAddress(this.FunctionAddress, this.OnReceiveEvent); - this.Hook?.Enable(); - } - - /// - /// Disable the hook for this receive event handler. - /// - public void Disable() - { - this.Hook?.Disable(); - } - - /// - public void Dispose() - { - this.Hook?.Dispose(); - } - - private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) - { - // Check that we didn't get here through a call to another addons handler. - var addonName = addon->NameString; - if (!this.AddonNames.Contains(addonName)) - { - this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData); - return; - } - - using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg); - arg.Clear(); - arg.Addon = (nint)addon; - arg.AtkEventType = (byte)eventType; - arg.EventParam = eventParam; - arg.AtkEvent = (IntPtr)atkEvent; - arg.Data = (nint)atkEventData; - this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg); - eventType = (AtkEventType)arg.AtkEventType; - eventParam = arg.EventParam; - atkEvent = (AtkEvent*)arg.AtkEvent; - atkEventData = (AtkEventData*)arg.Data; - - try - { - this.Hook!.Original(addon, eventType, eventParam, atkEvent, atkEventData); - } - catch (Exception e) - { - Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); - } - - this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg); - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs deleted file mode 100644 index 297323b8f..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Runtime.InteropServices; - -using Reloaded.Hooks.Definitions; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// This class represents a callsite hook used to replace the address of the OnSetup function in r9. -/// -/// Delegate signature for this hook. -internal class AddonSetupHook : IDisposable where T : Delegate -{ - private readonly Reloaded.Hooks.AsmHook asmHook; - - private T? detour; - private bool activated; - - /// - /// Initializes a new instance of the class. - /// - /// Address of the instruction to replace. - /// Delegate to invoke. - internal AddonSetupHook(nint address, T detour) - { - this.detour = detour; - - var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); - var code = new[] - { - "use64", - $"mov r9, 0x{detourPtr:X8}", - }; - - var opt = new AsmHookOptions - { - PreferRelativeJump = true, - Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, - MaxOpcodeSize = 5, - }; - - this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); - } - - /// - /// Gets a value indicating whether the hook is enabled. - /// - public bool IsEnabled => this.asmHook.IsEnabled; - - /// - /// Starts intercepting a call to the function. - /// - public void Enable() - { - if (!this.activated) - { - this.activated = true; - this.asmHook.Activate(); - return; - } - - this.asmHook.Enable(); - } - - /// - /// Stops intercepting a call to the function. - /// - public void Disable() - { - this.asmHook.Disable(); - } - - /// - /// Remove a hook from the current process. - /// - public void Dispose() - { - this.asmHook.Disable(); - this.detour = null; - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs new file mode 100644 index 000000000..58e32a252 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -0,0 +1,405 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Logging.Internal; + +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon.Lifecycle; + +/// +/// Represents a class that holds references to an addons original and modified virtual table entries. +/// +internal unsafe class AddonVirtualTable : IDisposable +{ + // This need to be at minimum the largest virtual table size of all addons + // Copying extra entries is not problematic, and is considered safe. + private const int VirtualTableEntryCount = 200; + + private const bool EnableAdvancedLogging = true; + private const bool EnableSpammyLogging = false; + + private static readonly ModuleLog Log = new("LifecycleVT"); + + private readonly AddonLifecycle lifecycleService; + + // Obsolete warning is only to prevent users from creating their own event objects. +#pragma warning disable CS0618 // Type or member is obsolete + private readonly AddonSetupArgs addonSetupArg = new(); + private readonly AddonFinalizeArgs addonFinalizeArg = new(); + private readonly AddonDrawArgs addonDrawArg = new(); + private readonly AddonUpdateArgs addonUpdateArg = new(); + private readonly AddonRefreshArgs addonRefreshArg = new(); + private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); + private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); + private readonly AddonGenericArgs addonGenericArg = new(); +#pragma warning restore CS0618 // Type or member is obsolete + + private readonly AtkUnitBase* atkUnitBase; + + private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable; + private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable; + + // 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 AtkUnitBase.Delegates.Dtor destructorFunction; + private readonly AtkUnitBase.Delegates.OnSetup onSetupFunction; + private readonly AtkUnitBase.Delegates.Finalizer finalizerFunction; + private readonly AtkUnitBase.Delegates.Draw drawFunction; + private readonly AtkUnitBase.Delegates.Update updateFunction; + private readonly AtkUnitBase.Delegates.OnRefresh onRefreshFunction; + private readonly AtkUnitBase.Delegates.OnRequestedUpdate onRequestedUpdateFunction; + private readonly AtkUnitBase.Delegates.ReceiveEvent onReceiveEventFunction; + private readonly AtkUnitBase.Delegates.Open openFunction; + private readonly AtkUnitBase.Delegates.Close closeFunction; + private readonly AtkUnitBase.Delegates.Show showFunction; + private readonly AtkUnitBase.Delegates.Hide hideFunction; + + /// + /// Initializes a new instance of the class. + /// + /// AtkUnitBase* for the addon to replace the table of. + /// Reference to AddonLifecycle service to callback and invoke listeners. + internal AddonVirtualTable(AtkUnitBase* addon, AddonLifecycle lifecycleService) + { + this.atkUnitBase = addon; + this.lifecycleService = lifecycleService; + + // Save original virtual table + this.originalVirtualTable = addon->VirtualTable; + + // Create copy of original table + // Note this will copy any derived/overriden functions that this specific addon has. + // Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game + this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8); + NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + + // Overwrite the addons existing virtual table with our own + addon->VirtualTable = this.modifiedVirtualTable; + + // Pin each of our listener functions + this.destructorFunction = this.OnAddonDestructor; + this.onSetupFunction = this.OnAddonSetup; + this.finalizerFunction = this.OnAddonFinalize; + this.drawFunction = this.OnAddonDraw; + this.updateFunction = this.OnAddonUpdate; + this.onRefreshFunction = this.OnAddonRefresh; + this.onRequestedUpdateFunction = this.OnRequestedUpdate; + this.onReceiveEventFunction = this.OnAddonReceiveEvent; + this.openFunction = this.OnAddonOpen; + this.closeFunction = this.OnAddonClose; + this.showFunction = this.OnAddonShow; + this.hideFunction = this.OnAddonHide; + + // Overwrite specific virtual table entries + this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); + this.modifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction); + this.modifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction); + this.modifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction); + this.modifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); + this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction); + this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction); + this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction); + this.modifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction); + this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); + this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); + this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); + } + + /// + /// Gets an event that is invoked when this addon's Finalize method is called from native. + /// + public required Action OnAddonFinalized { get; init; } + + /// + /// WARNING! This should not be called at any time except during dalamud unload. + /// + public void Dispose() + { + this.atkUnitBase->VirtualTable = this.originalVirtualTable; + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + } + + private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) + { + this.LogEvent(); + + var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); + + if ((freeFlags & 1) == 1) + { + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + this.OnAddonFinalized(); + } + + return result; + } + + private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + this.LogEvent(); + + this.addonSetupArg.Clear(); + this.addonSetupArg.Addon = addon; + this.addonSetupArg.AtkValueCount = valueCount; + this.addonSetupArg.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg); + valueCount = this.addonSetupArg.AtkValueCount; + values = (AtkValue*)this.addonSetupArg.AtkValues; + + try + { + this.originalVirtualTable->OnSetup(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg); + } + + private void OnAddonFinalize(AtkUnitBase* thisPtr) + { + this.LogEvent(); + + this.addonFinalizeArg.Clear(); + this.addonFinalizeArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg); + + try + { + this.originalVirtualTable->Finalizer(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); + } + } + + private void OnAddonDraw(AtkUnitBase* addon) + { + this.LogEvent(); + + this.addonDrawArg.Clear(); + this.addonDrawArg.Addon = addon; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); + + try + { + this.originalVirtualTable->Draw(addon); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg); + } + + private void OnAddonUpdate(AtkUnitBase* addon, float delta) + { + this.LogEvent(); + + this.addonUpdateArg.Clear(); + this.addonUpdateArg.Addon = addon; + this.addonUpdateArg.TimeDeltaInternal = delta; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); + + try + { + this.originalVirtualTable->Update(addon, delta); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg); + } + + private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + this.LogEvent(); + + var result = false; + + this.addonRefreshArg.Clear(); + this.addonRefreshArg.Addon = addon; + this.addonRefreshArg.AtkValueCount = valueCount; + this.addonRefreshArg.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg); + valueCount = this.addonRefreshArg.AtkValueCount; + values = (AtkValue*)this.addonRefreshArg.AtkValues; + + try + { + result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg); + return result; + } + + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + this.LogEvent(); + + this.addonRequestedUpdateArg.Clear(); + this.addonRequestedUpdateArg.Addon = addon; + this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; + this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg); + numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData; + stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData; + + try + { + this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg); + } + + private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) + { + this.LogEvent(); + + this.addonReceiveEventArg.Clear(); + this.addonReceiveEventArg.Addon = (nint)addon; + this.addonReceiveEventArg.AtkEventType = (byte)eventType; + this.addonReceiveEventArg.EventParam = eventParam; + this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent; + this.addonReceiveEventArg.Data = (nint)atkEventData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg); + eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType; + eventParam = this.addonReceiveEventArg.EventParam; + atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent; + atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data; + + try + { + this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg); + } + + private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) + { + this.LogEvent(); + + var result = false; + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); + + try + { + result = this.originalVirtualTable->Open(thisPtr, depthLayer); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg); + + return result; + } + + private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) + { + this.LogEvent(); + + var result = false; + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); + + try + { + result = this.originalVirtualTable->Close(thisPtr, fireCallback); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg); + + return result; + } + + private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) + { + this.LogEvent(); + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); + + try + { + this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg); + } + + private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) + { + this.LogEvent(); + + this.addonGenericArg.Clear(); + this.addonGenericArg.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); + + try + { + this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg); + } + + [Conditional("DEBUG")] + private void LogEvent([CallerMemberName] string caller = "") + { + if (EnableAdvancedLogging) + { + if (!EnableSpammyLogging) + { + if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") + return; + } + + Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}"); + } + } +} diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs deleted file mode 100644 index 92bc6e31a..000000000 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System.Runtime.InteropServices; - -using Reloaded.Hooks.Definitions; - -namespace Dalamud.Hooking.Internal; - -/// -/// This class represents a callsite hook. Only the specific address's instructions are replaced with this hook. -/// This is a destructive operation, no other callsite hooks can coexist at the same address. -/// -/// There's no .Original for this hook type. -/// This is only intended for be for functions where the parameters provided allow you to invoke the original call. -/// -/// This class was specifically added for hooking virtual function callsites. -/// Only the specific callsite hooked is modified, if the game calls the virtual function from other locations this hook will not be triggered. -/// -/// Delegate signature for this hook. -internal class CallHook : IDalamudHook where T : Delegate -{ - private readonly Reloaded.Hooks.AsmHook asmHook; - - private T? detour; - private bool activated; - - /// - /// Initializes a new instance of the class. - /// - /// Address of the instruction to replace. - /// Delegate to invoke. - internal CallHook(nint address, T detour) - { - ArgumentNullException.ThrowIfNull(detour); - - this.detour = detour; - this.Address = address; - - var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); - var code = new[] - { - "use64", - $"mov rax, 0x{detourPtr:X8}", - "call rax", - }; - - var opt = new AsmHookOptions - { - PreferRelativeJump = true, - Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, - MaxOpcodeSize = 5, - }; - - this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); - } - - /// - /// Gets a value indicating whether the hook is enabled. - /// - public bool IsEnabled => this.asmHook.IsEnabled; - - /// - public IntPtr Address { get; } - - /// - public string BackendName => "Reloaded AsmHook"; - - /// - public bool IsDisposed => this.detour == null; - - /// - /// Starts intercepting a call to the function. - /// - public void Enable() - { - if (!this.activated) - { - this.activated = true; - this.asmHook.Activate(); - return; - } - - this.asmHook.Enable(); - } - - /// - /// Stops intercepting a call to the function. - /// - public void Disable() - { - this.asmHook.Disable(); - } - - /// - /// Remove a hook from the current process. - /// - public void Dispose() - { - this.asmHook.Disable(); - this.detour = null; - } -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index b58166e89..c336f895e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -54,13 +52,6 @@ public class AddonLifecycleWidget : IDataWindowWidget this.DrawEventListeners(); ImGui.Unindent(); } - - if (ImGui.CollapsingHeader("ReceiveEvent Hooks"u8)) - { - ImGui.Indent(); - this.DrawReceiveEventHooks(); - ImGui.Unindent(); - } } private void DrawEventListeners() @@ -100,46 +91,4 @@ public class AddonLifecycleWidget : IDataWindowWidget } } } - - private void DrawReceiveEventHooks() - { - if (!this.Ready) return; - - var listeners = this.AddonLifecycle.ReceiveEventListeners; - - if (listeners.Count == 0) - { - ImGui.Text("No ReceiveEvent Hooks are Registered"u8); - } - - foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners) - { - if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames))) - { - ImGui.Columns(2); - - var functionAddress = receiveEventListener.FunctionAddress; - - ImGui.Text("Hook Address"u8); - ImGui.NextColumn(); - ImGui.Text($"0x{functionAddress:X} (ffxiv_dx11.exe+{functionAddress - Process.GetCurrentProcess().MainModule!.BaseAddress:X})"); - - ImGui.NextColumn(); - ImGui.Text("Hook Status"u8); - ImGui.NextColumn(); - if (receiveEventListener.Hook is null) - { - ImGui.Text("Hook is null"u8); - } - else - { - var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed; - var text = receiveEventListener.Hook.IsEnabled ? "Enabled"u8 : "Disabled"u8; - ImGui.TextColored(color, text); - } - - ImGui.Columns(1); - } - } - } } From 27a7adfdb9851173835fb09ec2af0141d4a329e5 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 18:56:34 -0800 Subject: [PATCH 048/164] Minor cleanup --- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 11 ++++++----- .../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 95dc5f718..de32bd254 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -41,7 +41,7 @@ public enum AddonArgsType ReceiveEvent, /// - /// Generic arg type that contains no meaningful data + /// Generic arg type that contains no meaningful data. /// Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cea30d6be..0c23f5661 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -33,8 +33,6 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); this.onInitializeAddonHook.Enable(); - - Log.Warning($"FOUND INITIALIZE HOOK AT {this.onInitializeAddonHook.Address:X}"); } /// @@ -48,10 +46,13 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - foreach (var virtualTable in this.modifiedTables.Values) + this.framework.RunOnFrameworkThread(() => { - virtualTable.Dispose(); - } + foreach (var virtualTable in this.modifiedTables.Values) + { + virtualTable.Dispose(); + } + }); } /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 1d767aac4..9359870a5 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycleAnymore, and use something else for HookWidget")] +[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 58e32a252..ca5d970ef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,7 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = true; + private const bool EnableAdvancedLogging = false; private const bool EnableSpammyLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); From 0533872a73f2caccc9ce6f6563070554bf7de59f Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Tue, 25 Nov 2025 20:45:54 -0800 Subject: [PATCH 049/164] Fix unreachable code complaint --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 39 +++++++++---------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index ca5d970ef..54c91248e 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -19,8 +19,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableAdvancedLogging = false; - private const bool EnableSpammyLogging = false; + private const bool EnableLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); @@ -125,7 +124,7 @@ internal unsafe class AddonVirtualTable : IDisposable private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); @@ -140,7 +139,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; @@ -164,7 +163,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonFinalize(AtkUnitBase* thisPtr) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; @@ -182,7 +181,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonDraw(AtkUnitBase* addon) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; @@ -202,7 +201,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; @@ -223,7 +222,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -250,7 +249,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; @@ -274,7 +273,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; @@ -302,7 +301,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -326,7 +325,7 @@ internal unsafe class AddonVirtualTable : IDisposable private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) { - this.LogEvent(); + this.LogEvent(EnableLogging); var result = false; @@ -350,7 +349,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -370,7 +369,7 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) { - this.LogEvent(); + this.LogEvent(EnableLogging); this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; @@ -389,15 +388,13 @@ internal unsafe class AddonVirtualTable : IDisposable } [Conditional("DEBUG")] - private void LogEvent([CallerMemberName] string caller = "") + private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") { - if (EnableAdvancedLogging) + if (loggingEnabled) { - if (!EnableSpammyLogging) - { - if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") - return; - } + // Manually disable the really spammy log events, you can comment this out if you need to debug them. + if (caller is "OnAddonUpdate" or "OnAddonDraw" or "OnAddonReceiveEvent" or "OnRequestedUpdate") + return; Log.Debug($"[{caller}]: {this.atkUnitBase->NameString}"); } From 4f59e0951303b2d4eca5700478982b93d2ca7ab6 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:24:35 -0800 Subject: [PATCH 050/164] Improve LifecycleInvoke efficiency with Dictionary --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 0c23f5661..cf1270803 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal List EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,10 +61,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.framework.RunOnTick(() => - { - this.EventListeners.Add(listener); - }); + this.EventListeners.TryAdd(listener.EventType, [ listener ]); + this.EventListeners[listener.EventType].Add(listener); } /// @@ -73,13 +71,10 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - // Set removed state to true immediately, then lazily remove it from the EventListeners list on next Framework Update. - listener.Removed = true; - - this.framework.RunOnTick(() => + if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) { - this.EventListeners.Remove(listener); - }); + listenerList.Remove(listener); + } } /// @@ -90,16 +85,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// What to blame on errors. internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { + // Early return if we don't have any listeners of this type + if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in this.EventListeners) + foreach (var listener in listenerList) { - if (listener.EventType != eventType) - continue; - - // If the listener is pending removal, and is waiting until the next Framework Update, don't invoke listener. - if (listener.Removed) - continue; - // Match on string.empty for listeners that want events for all addons. if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) continue; From b82b4f40cec89a9c5212c9ec1ba8ac8143352e18 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Thu, 27 Nov 2025 14:30:40 -0800 Subject: [PATCH 051/164] Use hashset to prevent duplicate entries --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index cf1270803..403671920 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + internal Dictionary> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() From c3e3e4aa8582e83c8efb74b991c59714111411ad Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 08:35:54 -0800 Subject: [PATCH 052/164] Fix accidentally breaking widget --- .../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index c336f895e..73c4e540a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; @@ -58,12 +57,11 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var eventType in Enum.GetValues()) + foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(eventType.ToString())) + if (ImGui.CollapsingHeader(listenerType.ToString())) { ImGui.Indent(); - var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList(); if (listeners.Count == 0) { From f8725e5f37e4a9f6a620eaf328da147124c7e8a9 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:08:24 -0800 Subject: [PATCH 053/164] further improve performance --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 61 ++++++++++++++----- .../Data/Widgets/AddonLifecycleWidget.cs | 40 ++++++------ 2 files changed, 67 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 403671920..e38f56921 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. /// - internal Dictionary> EventListeners { get; } = []; + /// Mapping is: EventType -> AddonName -> ListenerList + internal Dictionary>> EventListeners { get; } = []; /// void IInternalDisposableService.DisposeService() @@ -61,8 +62,18 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.EventListeners.TryAdd(listener.EventType, [ listener ]); - this.EventListeners[listener.EventType].Add(listener); + if (!this.EventListeners.ContainsKey(listener.EventType)) + { + this.EventListeners.TryAdd(listener.EventType, []); + } + + // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type + if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) + { + this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []); + } + + this.EventListeners[listener.EventType][listener.AddonName].Add(listener); } /// @@ -71,9 +82,12 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - if (this.EventListeners.TryGetValue(listener.EventType, out var listenerList)) + if (this.EventListeners.TryGetValue(listener.EventType, out var addonListeners)) { - listenerList.Remove(listener); + if (addonListeners.TryGetValue(listener.AddonName, out var addonListener)) + { + addonListener.Remove(listener); + } } } @@ -86,22 +100,37 @@ internal unsafe class AddonLifecycle : IInternalDisposableService internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { // Early return if we don't have any listeners of this type - if (!this.EventListeners.TryGetValue(eventType, out var listenerList)) return; + if (!this.EventListeners.TryGetValue(eventType, out var addonListeners)) return; - // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. - foreach (var listener in listenerList) + // Handle listeners for this event type that don't care which addon is triggering it + if (addonListeners.TryGetValue(string.Empty, out var globalListeners)) { - // Match on string.empty for listeners that want events for all addons. - if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) - continue; - - try + foreach (var listener in globalListeners) { - listener.FunctionDelegate.Invoke(eventType, args); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for global addon event listener."); + } } - catch (Exception e) + } + + // Handle listeners that are listening for this addon and event type specifically + if (addonListeners.TryGetValue(args.AddonName, out var addonListener)) + { + foreach (var listener in addonListener) { - Log.Error(e, $"Exception in {blame} during {eventType} invoke."); + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke, for specific addon {args.AddonName}."); + } } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 73c4e540a..0f193556b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -2,7 +2,8 @@ using System.Diagnostics.CodeAnalysis; using Dalamud.Bindings.ImGui; using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -57,35 +58,38 @@ public class AddonLifecycleWidget : IDataWindowWidget { if (!this.Ready) return; - foreach (var (listenerType, listeners) in this.AddonLifecycle.EventListeners) + foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { - if (ImGui.CollapsingHeader(listenerType.ToString())) + using var eventId = ImRaii.PushId(eventType.ToString()); + + if (ImGui.CollapsingHeader(eventType.ToString())) { - ImGui.Indent(); + using var eventIndent = ImRaii.PushIndent(); - if (listeners.Count == 0) + if (addonListeners.Count == 0) { - ImGui.Text("No Listeners Registered for Event"u8); + ImGui.Text("No Addons Registered for Event"u8); } - if (ImGui.BeginTable("AddonLifecycleListenersTable"u8, 2)) + foreach (var (addonName, listeners) in addonListeners) { - ImGui.TableSetupColumn("##AddonName"u8, ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##MethodInvoke"u8, ImGuiTableColumnFlags.WidthStretch); + using var addonId = ImRaii.PushId(addonName); - foreach (var listener in listeners) + if (ImGui.CollapsingHeader(addonName.IsNullOrEmpty() ? "GLOBAL" : addonName)) { - ImGui.TableNextColumn(); - ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); + using var addonIndent = ImRaii.PushIndent(); - ImGui.TableNextColumn(); - ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + if (listeners.Count == 0) + { + ImGui.Text("No Listeners Registered for Event"u8); + } + + foreach (var listener in listeners) + { + ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType?.FullName ?? "Unknown Declaring Type"}::{listener.FunctionDelegate.Method.Name}"); + } } - - ImGui.EndTable(); } - - ImGui.Unindent(); } } } From e01acb4a80727def0dd77b2e72202404b139f099 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:11:13 -0800 Subject: [PATCH 054/164] Remove redundant header --- .../Windows/Data/Widgets/AddonLifecycleWidget.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 0f193556b..4fb13b81a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -46,18 +46,6 @@ public class AddonLifecycleWidget : IDataWindowWidget return; } - if (ImGui.CollapsingHeader("Listeners"u8)) - { - ImGui.Indent(); - this.DrawEventListeners(); - ImGui.Unindent(); - } - } - - private void DrawEventListeners() - { - if (!this.Ready) return; - foreach (var (eventType, addonListeners) in this.AddonLifecycle.EventListeners) { using var eventId = ImRaii.PushId(eventType.ToString()); From be3f71dc734c13ed4ad222ce46fcff7338bb0f82 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 28 Nov 2025 09:44:35 -0800 Subject: [PATCH 055/164] Fix copy paste error --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index e38f56921..d3d0fcebe 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -37,7 +37,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService /// /// Gets a list of all AddonLifecycle Event Listeners. - /// + ///
/// Mapping is: EventType -> AddonName -> ListenerList internal Dictionary>> EventListeners { get; } = []; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 54c91248e..db698e626 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -167,7 +167,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonDrawArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); try { From eb9555ee22c3d240a7b770f985207d172c6c8284 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:08:40 -0800 Subject: [PATCH 056/164] Better unload --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 33 +++++-------------- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 15 +++------ 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index d3d0fcebe..5d121bea4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -19,13 +19,13 @@ namespace Dalamud.Game.Addon.Lifecycle; [ServiceManager.EarlyLoadedService] internal unsafe class AddonLifecycle : IInternalDisposableService { + /// + /// Gets a list of all allocated addon virtual tables. + /// + public static readonly List AllocatedTables = []; + private static readonly ModuleLog Log = new("AddonLifecycle"); - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - - private readonly Dictionary modifiedTables = []; - private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] @@ -47,13 +47,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService this.onInitializeAddonHook?.Dispose(); this.onInitializeAddonHook = null; - this.framework.RunOnFrameworkThread(() => - { - foreach (var virtualTable in this.modifiedTables.Values) - { - virtualTable.Dispose(); - } - }); + AllocatedTables.ForEach(entry => entry.Dispose()); + AllocatedTables.Clear(); } /// @@ -141,18 +136,8 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { this.LogInitialize(addon->NameString); - if (!this.modifiedTables.ContainsKey(addon->NameString)) - { - // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions - var managedVirtualTableEntry = new AddonVirtualTable(addon, this) - { - // This event is invoked when the game itself has disposed of an addon - // We can use this to know when to remove our virtual table entry - OnAddonFinalized = () => this.modifiedTables.Remove(addon->NameString), - }; - - this.modifiedTables.Add(addon->NameString, managedVirtualTableEntry); - } + // AddonVirtualTable class handles creating the virtual table, and overriding each of the tracked virtual functions + AllocatedTables.Add(new AddonVirtualTable(addon, this)); } catch (Exception e) { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index db698e626..d91cd648f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -1,6 +1,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Logging.Internal; @@ -108,17 +109,11 @@ internal unsafe class AddonVirtualTable : IDisposable this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); } - /// - /// Gets an event that is invoked when this addon's Finalize method is called from native. - /// - public required Action OnAddonFinalized { get; init; } - - /// - /// WARNING! This should not be called at any time except during dalamud unload. - /// + /// public void Dispose() { - this.atkUnitBase->VirtualTable = this.originalVirtualTable; + // Ensure restoration is done atomically. + Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable); IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); } @@ -131,7 +126,7 @@ internal unsafe class AddonVirtualTable : IDisposable if ((freeFlags & 1) == 1) { IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); - this.OnAddonFinalized(); + AddonLifecycle.AllocatedTables.Remove(this); } return result; From 08c176828639fefeae28e74030c34112d963cba8 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 10:39:35 -0800 Subject: [PATCH 057/164] Bunch of stuff... --- Dalamud/Configuration/PluginConfigurations.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 31 ++----------------- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 9 ++++-- .../AddonArgTypes/AddonFinalizeArgs.cs | 7 +++-- .../AddonArgTypes/AddonGenericArgs.cs | 3 +- .../AddonArgTypes/AddonReceiveEventArgs.cs | 18 +++-------- .../AddonArgTypes/AddonRefreshArgs.cs | 15 +++------ .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 11 +------ .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 15 +++------ .../AddonArgTypes/AddonUpdateArgs.cs | 26 +++++++--------- .../AddonLifecycleAddressResolver.cs | 2 +- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 14 --------- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +- Dalamud/Interface/Animation/Easing.cs | 2 +- ...ToDoAttribute.cs => Api14ToDoAttribute.cs} | 6 ++-- Dalamud/Utility/Api15ToDoAttribute.cs | 25 +++++++++++++++ Dalamud/Utility/Util.cs | 2 +- 17 files changed, 74 insertions(+), 116 deletions(-) rename Dalamud/Utility/{Api13ToDoAttribute.cs => Api14ToDoAttribute.cs} (75%) create mode 100644 Dalamud/Utility/Api15ToDoAttribute.cs diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index fa2969d31..c01ab2af0 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -11,7 +11,7 @@ namespace Dalamud.Configuration; /// /// Configuration to store settings for a dalamud plugin. /// -[Api13ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] +[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 0b2ae1178..62ca47238 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -33,41 +33,14 @@ public abstract class AddonArgs /// public abstract AddonArgsType Type { get; } - /// - /// Checks if addon name matches the given span of char. - /// - /// The name to check. - /// Whether it is the case. - internal bool IsAddon(string name) - { - if (this.Addon.IsNull) - return false; - - if (name.Length is 0 or > 32) - return false; - - if (string.IsNullOrEmpty(this.Addon.Name)) - return false; - - return name == this.Addon.Name; - } - - /// - /// Clears this AddonArgs values. - /// - internal virtual void Clear() - { - this.addonName = null; - this.Addon = 0; - } - /// /// Helper method for ensuring the name of the addon is valid. /// /// The name of the addon for this object. when invalid. private string GetAddonName() { - if (this.Addon.IsNull) return InvalidAddon; + if (this.Addon.IsNull) + return InvalidAddon; var name = this.Addon.Name; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 7254ba7b3..a834d2983 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -1,15 +1,18 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Utility; + +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Draw events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonDrawArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonDrawArgs() + internal AddonDrawArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index 12def3ad3..11d15a081 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -1,15 +1,18 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonFinalizeArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonFinalizeArgs() + internal AddonFinalizeArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs index f3078af69..a20e9d23b 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs @@ -8,8 +8,7 @@ public class AddonGenericArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonGenericArgs() + internal AddonGenericArgs() { } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 05f51b118..bb8168075 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// @@ -8,8 +10,7 @@ public class AddonReceiveEventArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonReceiveEventArgs() + internal AddonReceiveEventArgs() { } @@ -32,17 +33,8 @@ public class AddonReceiveEventArgs : AddonArgs public nint AtkEvent { get; set; } /// - /// Gets or sets the pointer to a block of data for this event message. + /// Gets or sets the pointer to an AtkEventData for this event message. /// + [Api14ToDo("Rename to AtkEventData")] public nint Data { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - this.AtkEventType = 0; - this.EventParam = 0; - this.AtkEvent = 0; - this.Data = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index c01c065c1..8af017318 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRefreshArgs() + internal AddonRefreshArgs() { } @@ -31,13 +32,7 @@ public class AddonRefreshArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = 0; - this.AtkValues = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index bf00c5d6e..7005b77c2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -8,8 +8,7 @@ public class AddonRequestedUpdateArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonRequestedUpdateArgs() + internal AddonRequestedUpdateArgs() { } @@ -25,12 +24,4 @@ public class AddonRequestedUpdateArgs : AddonArgs /// Gets or sets the StringArrayData** for this event. /// public nint StringArrayData { get; set; } - - /// - internal override void Clear() - { - base.Clear(); - this.NumberArrayData = 0; - this.StringArrayData = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 9b7e86a61..9fd7b6dd0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -10,8 +12,7 @@ public class AddonSetupArgs : AddonArgs /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonSetupArgs() + internal AddonSetupArgs() { } @@ -31,13 +32,7 @@ public class AddonSetupArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// + [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] + [Api15ToDo("Remove this")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); - - /// - internal override void Clear() - { - base.Clear(); - this.AtkValueCount = 0; - this.AtkValues = 0; - } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index bab62fc89..e6147d0eb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -1,39 +1,35 @@ +using Dalamud.Utility; + namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Update events. /// +[Obsolete("Use AddonGenericArgs instead.")] +[Api15ToDo("Remove this")] public class AddonUpdateArgs : AddonArgs { /// /// Initializes a new instance of the class. /// - [Obsolete("Not intended for public construction.", false)] - public AddonUpdateArgs() + internal AddonUpdateArgs() { } /// public override AddonArgsType Type => AddonArgsType.Update; - /// - /// Gets the time since the last update. - /// - public float TimeDelta - { - get => this.TimeDeltaInternal; - init => this.TimeDeltaInternal = value; - } - /// /// Gets or sets the time since the last update. /// internal float TimeDeltaInternal { get; set; } - /// - internal override void Clear() + /// + /// Gets the time since the last update. + /// + private float TimeDelta { - base.Clear(); - this.TimeDeltaInternal = 0; + get => this.TimeDeltaInternal; + init => this.TimeDeltaInternal = value; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 9359870a5..2fa3c5b91 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. /// -[Api13ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] +[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index d91cd648f..49ffdc7fb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -26,8 +26,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonLifecycle lifecycleService; - // Obsolete warning is only to prevent users from creating their own event objects. -#pragma warning disable CS0618 // Type or member is obsolete private readonly AddonSetupArgs addonSetupArg = new(); private readonly AddonFinalizeArgs addonFinalizeArg = new(); private readonly AddonDrawArgs addonDrawArg = new(); @@ -36,7 +34,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); private readonly AddonGenericArgs addonGenericArg = new(); -#pragma warning restore CS0618 // Type or member is obsolete private readonly AtkUnitBase* atkUnitBase; @@ -136,7 +133,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonSetupArg.Clear(); this.addonSetupArg.Addon = addon; this.addonSetupArg.AtkValueCount = valueCount; this.addonSetupArg.AtkValues = (nint)values; @@ -160,7 +156,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonFinalizeArg.Clear(); this.addonFinalizeArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); @@ -178,7 +173,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonDrawArg.Clear(); this.addonDrawArg.Addon = addon; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); @@ -198,7 +192,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonUpdateArg.Clear(); this.addonUpdateArg.Addon = addon; this.addonUpdateArg.TimeDeltaInternal = delta; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); @@ -221,7 +214,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonRefreshArg.Clear(); this.addonRefreshArg.Addon = addon; this.addonRefreshArg.AtkValueCount = valueCount; this.addonRefreshArg.AtkValues = (nint)values; @@ -246,7 +238,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonRequestedUpdateArg.Clear(); this.addonRequestedUpdateArg.Addon = addon; this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; @@ -270,7 +261,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonReceiveEventArg.Clear(); this.addonReceiveEventArg.Addon = (nint)addon; this.addonReceiveEventArg.AtkEventType = (byte)eventType; this.addonReceiveEventArg.EventParam = eventParam; @@ -300,7 +290,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); @@ -324,7 +313,6 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); @@ -346,7 +334,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); @@ -366,7 +353,6 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Clear(); this.addonGenericArg.Addon = thisPtr; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index f5b7011fe..af85f9228 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry } /// - [Api13ToDo("Maybe make this config scoped to internal name?")] + [Api14ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index 0d2057b3b..cc1f48ce7 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -48,7 +48,7 @@ public abstract class Easing /// Gets the current value of the animation, following unclamped logic. /// [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] - [Api13ToDo("Map this field to ValueClamped, probably.")] + [Api14ToDo("Map this field to ValueClamped, probably.")] public double Value => this.ValueUnclamped; /// diff --git a/Dalamud/Utility/Api13ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs similarity index 75% rename from Dalamud/Utility/Api13ToDoAttribute.cs rename to Dalamud/Utility/Api14ToDoAttribute.cs index 576401cda..945b6e4db 100644 --- a/Dalamud/Utility/Api13ToDoAttribute.cs +++ b/Dalamud/Utility/Api14ToDoAttribute.cs @@ -4,7 +4,7 @@ namespace Dalamud.Utility; /// Utility class for marking something to be changed for API 13, for ease of lookup. /// [AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api13ToDoAttribute : Attribute +internal sealed class Api14ToDoAttribute : Attribute { /// /// Marks that this should be made internal. @@ -12,11 +12,11 @@ internal sealed class Api13ToDoAttribute : Attribute public const string MakeInternal = "Make internal."; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The explanation. /// The explanation 2. - public Api13ToDoAttribute(string what, string what2 = "") + public Api14ToDoAttribute(string what, string what2 = "") { _ = what; _ = what2; diff --git a/Dalamud/Utility/Api15ToDoAttribute.cs b/Dalamud/Utility/Api15ToDoAttribute.cs new file mode 100644 index 000000000..646c260e8 --- /dev/null +++ b/Dalamud/Utility/Api15ToDoAttribute.cs @@ -0,0 +1,25 @@ +namespace Dalamud.Utility; + +/// +/// Utility class for marking something to be changed for API 13, for ease of lookup. +/// Intended to represent not the upcoming API, but the one after it for more major changes. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class Api15ToDoAttribute : Attribute +{ + /// + /// Marks that this should be made internal. + /// + public const string MakeInternal = "Make internal."; + + /// + /// Initializes a new instance of the class. + /// + /// The explanation. + /// The explanation 2. + public Api15ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } +} diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 19610ef64..f6abc336c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -79,7 +79,7 @@ public static partial class Util /// /// Gets the Dalamud version. /// - [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] + [Api14ToDo("Remove. Make both versions here internal. Add an API somewhere.")] public static string AssemblyVersion { get; } = Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); From 386828005b02d16f24f5710e9aec9453aa3faae5 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 12:37:51 -0800 Subject: [PATCH 058/164] Apply breaking changes --- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 38 +++--- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 21 --- .../AddonArgTypes/AddonFinalizeArgs.cs | 21 --- .../AddonArgTypes/AddonGenericArgs.cs | 17 --- .../AddonArgTypes/AddonReceiveEventArgs.cs | 5 +- .../AddonArgTypes/AddonUpdateArgs.cs | 35 ----- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 25 +--- Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 3 - .../Game/Addon/Lifecycle/AddonLifecycle.cs | 6 +- .../AddonLifecycleAddressResolver.cs | 24 ---- .../Lifecycle/AddonLifecycleEventListener.cs | 9 +- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 129 +++++++++--------- .../Windows/Data/Widgets/HookWidget.cs | 36 ++--- .../Internal/Windows/TitleScreenMenuWindow.cs | 4 +- Directory.Build.props | 2 +- 15 files changed, 114 insertions(+), 261 deletions(-) delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 62ca47238..c4a7e8f53 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -5,19 +5,24 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Base class for AddonLifecycle AddonArgTypes. /// -public abstract class AddonArgs +public class AddonArgs { /// /// Constant string representing the name of an addon that is invalid. /// public const string InvalidAddon = "NullAddon"; - private string? addonName; + /// + /// Initializes a new instance of the class. + /// + internal AddonArgs() + { + } /// /// Gets the name of the addon this args referrers to. /// - public string AddonName => this.GetAddonName(); + public string AddonName { get; private set; } = InvalidAddon; /// /// Gets the pointer to the addons AtkUnitBase. @@ -25,28 +30,17 @@ public abstract class AddonArgs public AtkUnitBasePtr Addon { get; - internal set; + internal set + { + field = value; + + if (!this.Addon.IsNull && !string.IsNullOrEmpty(value.Name)) + this.AddonName = value.Name; + } } /// /// Gets the type of these args. /// - public abstract AddonArgsType Type { get; } - - /// - /// Helper method for ensuring the name of the addon is valid. - /// - /// The name of the addon for this object. when invalid. - private string GetAddonName() - { - if (this.Addon.IsNull) - return InvalidAddon; - - var name = this.Addon.Name; - - if (string.IsNullOrEmpty(name)) - return InvalidAddon; - - return this.addonName ??= name; - } + public virtual AddonArgsType Type => AddonArgsType.Generic; } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs deleted file mode 100644 index a834d2983..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Dalamud.Utility; - -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Draw events. -/// -[Obsolete("Use AddonGenericArgs instead.")] -[Api15ToDo("Remove this")] -public class AddonDrawArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonDrawArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Draw; -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs deleted file mode 100644 index 11d15a081..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Dalamud.Utility; - -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for ReceiveEvent events. -/// -[Obsolete("Use AddonGenericArgs instead.")] -[Api15ToDo("Remove this")] -public class AddonFinalizeArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonFinalizeArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Finalize; -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs deleted file mode 100644 index a20e9d23b..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Draw events. -/// -public class AddonGenericArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonGenericArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Generic; -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index bb8168075..785cd199f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -1,5 +1,3 @@ -using Dalamud.Utility; - namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// @@ -35,6 +33,5 @@ public class AddonReceiveEventArgs : AddonArgs /// /// Gets or sets the pointer to an AtkEventData for this event message. /// - [Api14ToDo("Rename to AtkEventData")] - public nint Data { get; set; } + public nint AtkEventData { get; set; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs deleted file mode 100644 index e6147d0eb..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Dalamud.Utility; - -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Update events. -/// -[Obsolete("Use AddonGenericArgs instead.")] -[Api15ToDo("Remove this")] -public class AddonUpdateArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonUpdateArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Update; - - /// - /// Gets or sets the time since the last update. - /// - internal float TimeDeltaInternal { get; set; } - - /// - /// Gets the time since the last update. - /// - private float TimeDelta - { - get => this.TimeDeltaInternal; - init => this.TimeDeltaInternal = value; - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index de32bd254..9d7815cef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -5,26 +5,16 @@ /// public enum AddonArgsType { + /// + /// Generic arg type that contains no meaningful data. + /// + Generic, + /// /// Contains argument data for Setup. /// Setup, - /// - /// Contains argument data for Update. - /// - Update, - - /// - /// Contains argument data for Draw. - /// - Draw, - - /// - /// Contains argument data for Finalize. - /// - Finalize, - /// /// Contains argument data for RequestedUpdate. /// @@ -39,9 +29,4 @@ public enum AddonArgsType /// Contains argument data for ReceiveEvent. /// ReceiveEvent, - - /// - /// Generic arg type that contains no meaningful data. - /// - Generic, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 7738d6c6a..5ec57b5e3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -29,7 +29,6 @@ public enum AddonEvent /// An event that is fired before an addon begins its update cycle via . This event /// is fired every frame that an addon is loaded, regardless of visibility. /// - /// PreUpdate, /// @@ -42,7 +41,6 @@ public enum AddonEvent /// An event that is fired before an addon begins drawing to screen via . Unlike /// , this event is only fired if an addon is visible or otherwise drawing to screen. /// - /// PreDraw, /// @@ -62,7 +60,6 @@ public enum AddonEvent ///
/// As this is part of the destruction process for an addon, this event does not have an associated Post event. /// - /// PreFinalize, /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 5d121bea4..ddcebe718 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -59,13 +59,15 @@ internal unsafe class AddonLifecycle : IInternalDisposableService { if (!this.EventListeners.ContainsKey(listener.EventType)) { - this.EventListeners.TryAdd(listener.EventType, []); + if (!this.EventListeners.TryAdd(listener.EventType, [])) + return; } // Note: string.Empty is a valid addon name, as that will trigger on any addon for this event type if (!this.EventListeners[listener.EventType].ContainsKey(listener.AddonName)) { - this.EventListeners[listener.EventType].TryAdd(listener.AddonName, []); + if (!this.EventListeners[listener.EventType].TryAdd(listener.AddonName, [])) + return; } this.EventListeners[listener.EventType][listener.AddonName].Add(listener); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs deleted file mode 100644 index 2fa3c5b91..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Dalamud.Utility; - -namespace Dalamud.Game.Addon.Lifecycle; - -/// -/// AddonLifecycleService memory address resolver. -/// -[Api14ToDo("Remove this class entirely, its not used by AddonLifecycle anymore, also need to use something else for HookWidget")] -internal class AddonLifecycleAddressResolver : BaseAddressResolver -{ - /// - /// Gets the address of the addon finalize hook invoked by the AtkUnitManager. - /// - public nint AddonFinalize { get; private set; } - - /// - /// Scan for and setup any configured address pointers. - /// - /// The signature scanner to facilitate setup. - protected override void Setup64Bit(ISigScanner sig) - { - this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); - } -} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs index 9d411cdbc..fc82e0582 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs @@ -25,17 +25,12 @@ internal class AddonLifecycleEventListener /// string.Empty if it wants to be called for any addon. /// public string AddonName { get; init; } - - /// - /// Gets or sets a value indicating whether this event has been unregistered. - /// - public bool Removed { get; set; } - + /// /// Gets the event type this listener is looking for. /// public AddonEvent EventType { get; init; } - + /// /// Gets the delegate this listener invokes. /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 49ffdc7fb..1ce145946 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -26,14 +26,18 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonLifecycle lifecycleService; - private readonly AddonSetupArgs addonSetupArg = new(); - private readonly AddonFinalizeArgs addonFinalizeArg = new(); - private readonly AddonDrawArgs addonDrawArg = new(); - private readonly AddonUpdateArgs addonUpdateArg = new(); - private readonly AddonRefreshArgs addonRefreshArg = new(); - private readonly AddonRequestedUpdateArgs addonRequestedUpdateArg = new(); - private readonly AddonReceiveEventArgs addonReceiveEventArg = new(); - private readonly AddonGenericArgs addonGenericArg = new(); + // Each addon gets its own set of args that are used to mutate the original call when used in pre-calls + private readonly AddonSetupArgs setupArgs = new(); + private readonly AddonArgs finalizeArgs = new(); + private readonly AddonArgs drawArgs = new(); + private readonly AddonArgs updateArgs = new(); + private readonly AddonRefreshArgs refreshArgs = new(); + private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new(); + private readonly AddonReceiveEventArgs receiveEventArgs = new(); + private readonly AddonArgs openArgs = new(); + private readonly AddonArgs closeArgs = new(); + private readonly AddonArgs showArgs = new(); + private readonly AddonArgs hideArgs = new(); private readonly AtkUnitBase* atkUnitBase; @@ -133,12 +137,13 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonSetupArg.Addon = addon; - this.addonSetupArg.AtkValueCount = valueCount; - this.addonSetupArg.AtkValues = (nint)values; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.addonSetupArg); - valueCount = this.addonSetupArg.AtkValueCount; - values = (AtkValue*)this.addonSetupArg.AtkValues; + this.setupArgs.Addon = addon; + this.setupArgs.AtkValueCount = valueCount; + this.setupArgs.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs); + + valueCount = this.setupArgs.AtkValueCount; + values = (AtkValue*)this.setupArgs.AtkValues; try { @@ -149,15 +154,15 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.addonSetupArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs); } private void OnAddonFinalize(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); - this.addonFinalizeArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.addonFinalizeArg); + this.finalizeArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs); try { @@ -173,8 +178,8 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonDrawArg.Addon = addon; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.addonDrawArg); + this.drawArgs.Addon = addon; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs); try { @@ -185,16 +190,15 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.addonDrawArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs); } private void OnAddonUpdate(AtkUnitBase* addon, float delta) { this.LogEvent(EnableLogging); - this.addonUpdateArg.Addon = addon; - this.addonUpdateArg.TimeDeltaInternal = delta; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.addonUpdateArg); + this.updateArgs.Addon = addon; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs); try { @@ -205,7 +209,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.addonUpdateArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs); } private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) @@ -214,12 +218,13 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonRefreshArg.Addon = addon; - this.addonRefreshArg.AtkValueCount = valueCount; - this.addonRefreshArg.AtkValues = (nint)values; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.addonRefreshArg); - valueCount = this.addonRefreshArg.AtkValueCount; - values = (AtkValue*)this.addonRefreshArg.AtkValues; + this.refreshArgs.Addon = addon; + this.refreshArgs.AtkValueCount = valueCount; + this.refreshArgs.AtkValues = (nint)values; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs); + + valueCount = this.refreshArgs.AtkValueCount; + values = (AtkValue*)this.refreshArgs.AtkValues; try { @@ -230,7 +235,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.addonRefreshArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs); return result; } @@ -238,12 +243,13 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonRequestedUpdateArg.Addon = addon; - this.addonRequestedUpdateArg.NumberArrayData = (nint)numberArrayData; - this.addonRequestedUpdateArg.StringArrayData = (nint)stringArrayData; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.addonRequestedUpdateArg); - numberArrayData = (NumberArrayData**)this.addonRequestedUpdateArg.NumberArrayData; - stringArrayData = (StringArrayData**)this.addonRequestedUpdateArg.StringArrayData; + this.requestedUpdateArgs.Addon = addon; + this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData; + this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs); + + numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData; + stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData; try { @@ -254,23 +260,24 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.addonRequestedUpdateArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs); } private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { this.LogEvent(EnableLogging); - this.addonReceiveEventArg.Addon = (nint)addon; - this.addonReceiveEventArg.AtkEventType = (byte)eventType; - this.addonReceiveEventArg.EventParam = eventParam; - this.addonReceiveEventArg.AtkEvent = (IntPtr)atkEvent; - this.addonReceiveEventArg.Data = (nint)atkEventData; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.addonReceiveEventArg); - eventType = (AtkEventType)this.addonReceiveEventArg.AtkEventType; - eventParam = this.addonReceiveEventArg.EventParam; - atkEvent = (AtkEvent*)this.addonReceiveEventArg.AtkEvent; - atkEventData = (AtkEventData*)this.addonReceiveEventArg.Data; + this.receiveEventArgs.Addon = (nint)addon; + this.receiveEventArgs.AtkEventType = (byte)eventType; + this.receiveEventArgs.EventParam = eventParam; + this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent; + this.receiveEventArgs.AtkEventData = (nint)atkEventData; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs); + + eventType = (AtkEventType)this.receiveEventArgs.AtkEventType; + eventParam = this.receiveEventArgs.EventParam; + atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent; + atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData; try { @@ -281,7 +288,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.addonReceiveEventArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs); } private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) @@ -290,8 +297,8 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.addonGenericArg); + this.openArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs); try { @@ -302,7 +309,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.addonGenericArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs); return result; } @@ -313,8 +320,8 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; - this.addonGenericArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.addonGenericArg); + this.closeArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs); try { @@ -325,7 +332,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.addonGenericArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs); return result; } @@ -334,8 +341,8 @@ internal unsafe class AddonVirtualTable : IDisposable { this.LogEvent(EnableLogging); - this.addonGenericArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.addonGenericArg); + this.showArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs); try { @@ -346,15 +353,15 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.addonGenericArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs); } private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) { this.LogEvent(EnableLogging); - this.addonGenericArg.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.addonGenericArg); + this.hideArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs); try { @@ -365,7 +372,7 @@ internal unsafe class AddonVirtualTable : IDisposable Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.addonGenericArg); + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); } [Conditional("DEBUG")] diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index 3ad8f86c2..c5ae1d8f0 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -5,9 +5,8 @@ using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Game; -using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.ClientState; using Dalamud.Hooking; -using FFXIVClientStructs.FFXIV.Component.GUI; using Serilog; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; @@ -17,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying hook information. /// -internal unsafe class HookWidget : IDataWindowWidget +internal class HookWidget : IDataWindowWidget { private readonly List hookStressTestList = []; @@ -32,9 +31,9 @@ internal unsafe class HookWidget : IDataWindowWidget private bool hookStressTestRunning = false; private MessageBoxWDelegate? messageBoxWOriginal; - private AddonFinalizeDelegate? addonFinalizeOriginal; + private HandleZoneInitPacketDelegate? zoneInitOriginal; - private AddonLifecycleAddressResolver? address; + private ClientStateAddressResolver? address; private delegate int MessageBoxWDelegate( IntPtr hWnd, @@ -42,12 +41,12 @@ internal unsafe class HookWidget : IDataWindowWidget [MarshalAs(UnmanagedType.LPWStr)] string caption, MESSAGEBOX_STYLE type); - private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + private delegate void HandleZoneInitPacketDelegate(nint a1, uint localPlayerEntityId, nint packet, byte type); private enum StressTestHookTarget { MessageBoxW, - AddonFinalize, + ZoneInit, Random, } @@ -65,7 +64,7 @@ internal unsafe class HookWidget : IDataWindowWidget { this.Ready = true; - this.address = new AddonLifecycleAddressResolver(); + this.address = new ClientStateAddressResolver(); this.address.Setup(Service.Get()); } @@ -179,7 +178,7 @@ internal unsafe class HookWidget : IDataWindowWidget return target switch { StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)", - StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)", + StressTestHookTarget.ZoneInit => "ZoneInit (Hook)", _ => target.ToString(), }; } @@ -198,15 +197,10 @@ internal unsafe class HookWidget : IDataWindowWidget return result; } - private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) + private void OnZoneInit(IntPtr a1, uint localPlayerEntityId, IntPtr packet, byte type) { - Log.Information("OnAddonFinalize"); - this.addonFinalizeOriginal!(unitManager, atkUnitBase); - } - - private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta) - { - Log.Information("OnAddonUpdate"); + Log.Information("OnZoneInit"); + this.zoneInitOriginal!.Invoke(a1, localPlayerEntityId, packet, type); } private IDalamudHook HookMessageBoxW() @@ -222,11 +216,11 @@ internal unsafe class HookWidget : IDataWindowWidget return hook; } - private IDalamudHook HookAddonFinalize() + private IDalamudHook HookZoneInit() { - var hook = Hook.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize); + var hook = Hook.FromAddress(this.address!.HandleZoneInitPacket, this.OnZoneInit); - this.addonFinalizeOriginal = hook.Original; + this.zoneInitOriginal = hook.Original; hook.Enable(); return hook; } @@ -241,7 +235,7 @@ internal unsafe class HookWidget : IDataWindowWidget return target switch { StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(), - StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(), + StressTestHookTarget.ZoneInit => this.HookZoneInit(), _ => throw new ArgumentOutOfRangeException(nameof(target), target, null), }; } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index e3eb22a04..62f83f82f 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -471,9 +471,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable private unsafe void OnVersionStringDraw(AddonEvent ev, AddonArgs args) { - if (args is not AddonDrawArgs drawArgs) return; + if (ev is not (AddonEvent.PostDraw or AddonEvent.PreDraw)) return; - var addon = drawArgs.Addon.Struct; + var addon = args.Addon.Struct; var textNode = addon->GetTextNodeById(3); // look and feel init. should be harmless to set. diff --git a/Directory.Build.props b/Directory.Build.props index eabb727e8..3897256bf 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ net10.0-windows x64 x64 - 13.0 + 14.0 From d47a41b2953407335fcb136f4843ea8e096306fc Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 12:48:49 -0800 Subject: [PATCH 059/164] Fix NET14 Spans defaulting to ReadOnlySpan --- imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs | 6 +++--- imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs | 6 +++--- imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs | 6 +++--- imgui/Dalamud.Bindings.ImGui/ImU8String.cs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs index 665fa434f..3cf20bb30 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.DragScalar.cs @@ -238,7 +238,7 @@ public static unsafe partial class ImGui ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vSpeed, vMin, vMax, @@ -251,7 +251,7 @@ public static unsafe partial class ImGui ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vSpeed, vMin, vMax, @@ -264,7 +264,7 @@ public static unsafe partial class ImGui ImGuiSliderFlags flags = ImGuiSliderFlags.None) => DragScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vSpeed, vMin, vMax, diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs index fb86096ff..5881ac462 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.InputScalar.cs @@ -205,7 +205,7 @@ public static unsafe partial class ImGui InputScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref data)), + MemoryMarshal.Cast(new Span(ref data)), step, stepFast, format.MoveOrDefault("%.3f"u8), @@ -219,7 +219,7 @@ public static unsafe partial class ImGui InputScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref data)), + MemoryMarshal.Cast(new Span(ref data)), step, stepFast, format.MoveOrDefault("%.3f"u8), @@ -233,7 +233,7 @@ public static unsafe partial class ImGui InputScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref data)), + MemoryMarshal.Cast(new Span(ref data)), step, stepFast, format.MoveOrDefault("%.3f"u8), diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs index 20ee78ab6..b0c4b7c79 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.SliderScalar.cs @@ -210,7 +210,7 @@ public static unsafe partial class ImGui ImU8String format = default, ImGuiSliderFlags flags = ImGuiSliderFlags.None) => SliderScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vMin, vMax, format.MoveOrDefault("%.3f"u8), @@ -222,7 +222,7 @@ public static unsafe partial class ImGui SliderScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vMin, vMax, format.MoveOrDefault("%.3f"u8), @@ -236,7 +236,7 @@ public static unsafe partial class ImGui SliderScalar( label, ImGuiDataType.Float, - MemoryMarshal.Cast(new(ref v)), + MemoryMarshal.Cast(new Span(ref v)), vMin, vMax, format.MoveOrDefault("%.3f"u8), diff --git a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs index a62152c39..f2b635764 100644 --- a/imgui/Dalamud.Bindings.ImGui/ImU8String.cs +++ b/imgui/Dalamud.Bindings.ImGui/ImU8String.cs @@ -156,7 +156,7 @@ public ref struct ImU8String return this.rentedBuffer is { } buf ? buf.AsSpan() - : MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer))); + : MemoryMarshal.Cast(new Span(ref Unsafe.AsRef(ref this.fixedBuffer))); } } @@ -165,7 +165,7 @@ public ref struct ImU8String private ref byte FixedBufferByteRef => ref this.FixedBufferSpan[0]; private Span FixedBufferSpan => - MemoryMarshal.Cast(new(ref Unsafe.AsRef(ref this.fixedBuffer))); + MemoryMarshal.Cast(new Span(ref Unsafe.AsRef(ref this.fixedBuffer))); public static implicit operator ImU8String(ReadOnlySpan text) => new(text); public static implicit operator ImU8String(ReadOnlyMemory text) => new(text); From 8e8d0246bc40d4b7d172c48ad2bc076882811102 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 14:00:39 -0800 Subject: [PATCH 060/164] Restore original hookwidget logic --- .../Windows/Data/Widgets/HookWidget.cs | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index c5ae1d8f0..f3e25caf8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -5,8 +5,8 @@ using System.Threading.Tasks; using Dalamud.Bindings.ImGui; using Dalamud.Game; -using Dalamud.Game.ClientState; using Dalamud.Hooking; +using FFXIVClientStructs.FFXIV.Component.GUI; using Serilog; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; @@ -16,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying hook information. /// -internal class HookWidget : IDataWindowWidget +internal unsafe class HookWidget : IDataWindowWidget { private readonly List hookStressTestList = []; @@ -31,9 +31,9 @@ internal class HookWidget : IDataWindowWidget private bool hookStressTestRunning = false; private MessageBoxWDelegate? messageBoxWOriginal; - private HandleZoneInitPacketDelegate? zoneInitOriginal; + private AddonFinalizeDelegate? addonFinalizeOriginal; - private ClientStateAddressResolver? address; + private nint address; private delegate int MessageBoxWDelegate( IntPtr hWnd, @@ -41,12 +41,12 @@ internal class HookWidget : IDataWindowWidget [MarshalAs(UnmanagedType.LPWStr)] string caption, MESSAGEBOX_STYLE type); - private delegate void HandleZoneInitPacketDelegate(nint a1, uint localPlayerEntityId, nint packet, byte type); + private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); private enum StressTestHookTarget { MessageBoxW, - ZoneInit, + AddonFinalize, Random, } @@ -54,7 +54,7 @@ internal class HookWidget : IDataWindowWidget public string DisplayName { get; init; } = "Hook"; /// - public string[]? CommandShortcuts { get; init; } = { "hook" }; + public string[]? CommandShortcuts { get; init; } = ["hook"]; /// public bool Ready { get; set; } @@ -64,8 +64,8 @@ internal class HookWidget : IDataWindowWidget { this.Ready = true; - this.address = new ClientStateAddressResolver(); - this.address.Setup(Service.Get()); + var sigScanner = Service.Get(); + this.address = sigScanner.ScanText("E8 ?? ?? ?? ?? 48 83 EF 01 75 D5"); } /// @@ -178,7 +178,7 @@ internal class HookWidget : IDataWindowWidget return target switch { StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)", - StressTestHookTarget.ZoneInit => "ZoneInit (Hook)", + StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)", _ => target.ToString(), }; } @@ -197,10 +197,15 @@ internal class HookWidget : IDataWindowWidget return result; } - private void OnZoneInit(IntPtr a1, uint localPlayerEntityId, IntPtr packet, byte type) + private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { - Log.Information("OnZoneInit"); - this.zoneInitOriginal!.Invoke(a1, localPlayerEntityId, packet, type); + Log.Information("OnAddonFinalize"); + this.addonFinalizeOriginal!(unitManager, atkUnitBase); + } + + private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta) + { + Log.Information("OnAddonUpdate"); } private IDalamudHook HookMessageBoxW() @@ -216,11 +221,11 @@ internal class HookWidget : IDataWindowWidget return hook; } - private IDalamudHook HookZoneInit() + private IDalamudHook HookAddonFinalize() { - var hook = Hook.FromAddress(this.address!.HandleZoneInitPacket, this.OnZoneInit); + var hook = Hook.FromAddress(this.address, this.OnAddonFinalize); - this.zoneInitOriginal = hook.Original; + this.addonFinalizeOriginal = hook.Original; hook.Enable(); return hook; } @@ -235,7 +240,7 @@ internal class HookWidget : IDataWindowWidget return target switch { StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(), - StressTestHookTarget.ZoneInit => this.HookZoneInit(), + StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(), _ => throw new ArgumentOutOfRangeException(nameof(target), target, null), }; } From b81cb9c74c7e8fc5423f1568904d807daeca4115 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 14:07:44 -0800 Subject: [PATCH 061/164] Remove generic args class --- .../Lifecycle/AddonArgTypes/AddonGenericArgs.cs | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs deleted file mode 100644 index a20e9d23b..000000000 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonGenericArgs.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; - -/// -/// Addon argument data for Draw events. -/// -public class AddonGenericArgs : AddonArgs -{ - /// - /// Initializes a new instance of the class. - /// - internal AddonGenericArgs() - { - } - - /// - public override AddonArgsType Type => AddonArgsType.Generic; -} From 78781c8988bbd67be3ce6514fd2de1282c052163 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 21:43:26 -0800 Subject: [PATCH 062/164] Add Move, MouseOver, MouseOut, Focus --- .../Lifecycle/AddonArgTypes/AddonCloseArgs.cs | 22 ++++ .../Lifecycle/AddonArgTypes/AddonHideArgs.cs | 32 +++++ .../Lifecycle/AddonArgTypes/AddonShowArgs.cs | 27 ++++ Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 15 +++ Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 60 ++++++++- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 118 +++++++++++++++++- 6 files changed, 264 insertions(+), 10 deletions(-) create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs new file mode 100644 index 000000000..db3e442f8 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonCloseArgs.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Close events. +/// +public class AddonCloseArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonCloseArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Close; + + /// + /// Gets or sets a value indicating whether the window should fire the callback method on close. + /// + public bool FireCallback { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs new file mode 100644 index 000000000..3e3521bd0 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonHideArgs.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Hide events. +/// +public class AddonHideArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonHideArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Hide; + + /// + /// Gets or sets a value indicating whether to call the hide callback handler when this hides. + /// + public bool CallHideCallback { get; set; } + + /// + /// Gets or sets the flags that the window will set when it Shows/Hides. + /// + public uint SetShowHideFlags { get; set; } + + /// + /// Gets or sets a value indicating whether something for this event message. + /// + internal bool UnknownBool { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs new file mode 100644 index 000000000..3153d1208 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonShowArgs.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for Show events. +/// +public class AddonShowArgs : AddonArgs +{ + /// + /// Initializes a new instance of the class. + /// + internal AddonShowArgs() + { + } + + /// + public override AddonArgsType Type => AddonArgsType.Show; + + /// + /// Gets or sets a value indicating whether the window should play open sound effects. + /// + public bool SilenceOpenSoundEffect { get; set; } + + /// + /// Gets or sets the flags that the window will unset when it Shows/Hides. + /// + public uint UnsetShowHideFlags { get; set; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 9d7815cef..46ee479ac 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -29,4 +29,19 @@ public enum AddonArgsType /// Contains argument data for ReceiveEvent. ///
ReceiveEvent, + + /// + /// Contains argument data for Show. + /// + Show, + + /// + /// Contains argument data for Hide. + /// + Hide, + + /// + /// Contains argument data for Close. + /// + Close, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 5ec57b5e3..3b9c6e867 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -127,32 +127,80 @@ public enum AddonEvent PostOpen, /// - /// An even that is fired before an addon processes its close method. + /// An even that is fired before an addon processes its Close method. /// PreClose, /// - /// An event that is fired after an addon has processed its close method. + /// An event that is fired after an addon has processed its Close method. /// PostClose, /// - /// An event that is fired before an addon processes its show method. + /// An event that is fired before an addon processes its Show method. /// PreShow, /// - /// An event that is fired after an addon has processed its show method. + /// An event that is fired after an addon has processed its Show method. /// PostShow, /// - /// An event that is fired before an addon processes its hide method. + /// An event that is fired before an addon processes its Hide method. /// PreHide, /// - /// An event that is fired after an addon has processed its hide method. + /// An event that is fired after an addon has processed its Hide method. /// PostHide, + + /// + /// An event that is fired before an addon processes its OnMove method. + /// OnMove is triggered only when a move is completed. + /// + PreMove, + + /// + /// An event that is fired after an addon has processed its OnMove method. + /// OnMove is triggered only when a move is completed. + /// + PostMove, + + /// + /// An event that is fired before an addon processes its MouseOver method. + /// + PreMouseOver, + + /// + /// An event that is fired after an addon has processed its MouseOver method. + /// + PostMouseOver, + + /// + /// An event that is fired before an addon processes its MouseOut method. + /// + PreMouseOut, + + /// + /// An event that is fired after an addon has processed its MouseOut method. + /// + PostMouseOut, + + /// + /// An event that is fired before an addon processes its Focus method. + /// + /// + /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows. + /// + PreFocus, + + /// + /// An event that is fired after an addon has processed its Focus method. + /// + /// + /// Be aware this is only called for certain popup windows, it is not triggered when clicking on windows. + /// + PostFocus, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 1ce145946..b92466b5a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -20,7 +20,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableLogging = false; + private const bool EnableLogging = true; private static readonly ModuleLog Log = new("LifecycleVT"); @@ -35,9 +35,13 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AddonRequestedUpdateArgs requestedUpdateArgs = new(); private readonly AddonReceiveEventArgs receiveEventArgs = new(); private readonly AddonArgs openArgs = new(); - private readonly AddonArgs closeArgs = new(); - private readonly AddonArgs showArgs = new(); - private readonly AddonArgs hideArgs = new(); + private readonly AddonCloseArgs closeArgs = new(); + private readonly AddonShowArgs showArgs = new(); + private readonly AddonHideArgs hideArgs = new(); + private readonly AddonArgs onMoveArgs = new(); + private readonly AddonArgs onMouseOverArgs = new(); + private readonly AddonArgs onMouseOutArgs = new(); + private readonly AddonArgs focusArgs = new(); private readonly AtkUnitBase* atkUnitBase; @@ -58,6 +62,10 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AtkUnitBase.Delegates.Close closeFunction; private readonly AtkUnitBase.Delegates.Show showFunction; private readonly AtkUnitBase.Delegates.Hide hideFunction; + private readonly AtkUnitBase.Delegates.OnMove onMoveFunction; + private readonly AtkUnitBase.Delegates.OnMouseOver onMouseOverFunction; + private readonly AtkUnitBase.Delegates.OnMouseOut onMouseOutFunction; + private readonly AtkUnitBase.Delegates.Focus focusFunction; /// /// Initializes a new instance of the class. @@ -94,6 +102,10 @@ internal unsafe class AddonVirtualTable : IDisposable this.closeFunction = this.OnAddonClose; this.showFunction = this.OnAddonShow; this.hideFunction = this.OnAddonHide; + this.onMoveFunction = this.OnMove; + this.onMouseOverFunction = this.OnMouseOver; + this.onMouseOutFunction = this.OnMouseOut; + this.focusFunction = this.OnFocus; // Overwrite specific virtual table entries this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); @@ -108,6 +120,10 @@ internal unsafe class AddonVirtualTable : IDisposable this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); + this.modifiedVirtualTable->OnMove = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction); + this.modifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); + this.modifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); + this.modifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); } /// @@ -200,6 +216,9 @@ internal unsafe class AddonVirtualTable : IDisposable this.updateArgs.Addon = addon; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs); + // Note: Do not pass or allow manipulation of delta. + // It's realistically not something that should be needed. + try { this.originalVirtualTable->Update(addon, delta); @@ -321,8 +340,11 @@ internal unsafe class AddonVirtualTable : IDisposable var result = false; this.closeArgs.Addon = thisPtr; + this.closeArgs.FireCallback = fireCallback; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs); + fireCallback = this.closeArgs.FireCallback; + try { result = this.originalVirtualTable->Close(thisPtr, fireCallback); @@ -342,8 +364,13 @@ internal unsafe class AddonVirtualTable : IDisposable this.LogEvent(EnableLogging); this.showArgs.Addon = thisPtr; + this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect; + this.showArgs.UnsetShowHideFlags = unsetShowHideFlags; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs); + silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect; + unsetShowHideFlags = this.showArgs.UnsetShowHideFlags; + try { this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); @@ -361,8 +388,15 @@ internal unsafe class AddonVirtualTable : IDisposable this.LogEvent(EnableLogging); this.hideArgs.Addon = thisPtr; + this.hideArgs.UnknownBool = unkBool; + this.hideArgs.CallHideCallback = callHideCallback; + this.hideArgs.SetShowHideFlags = setShowHideFlags; this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs); + unkBool = this.hideArgs.UnknownBool; + callHideCallback = this.hideArgs.CallHideCallback; + setShowHideFlags = this.hideArgs.SetShowHideFlags; + try { this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); @@ -375,6 +409,82 @@ internal unsafe class AddonVirtualTable : IDisposable this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); } + private void OnMove(AtkUnitBase* thisPtr) + { + this.LogEvent(EnableLogging); + + this.onMoveArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs); + + try + { + this.originalVirtualTable->OnMove(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original OnMove. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs); + } + + private void OnMouseOver(AtkUnitBase* thisPtr) + { + this.LogEvent(EnableLogging); + + this.onMouseOverArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs); + + try + { + this.originalVirtualTable->OnMouseOver(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original OnMouseOver. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs); + } + + private void OnMouseOut(AtkUnitBase* thisPtr) + { + this.LogEvent(EnableLogging); + + this.onMouseOutArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs); + + try + { + this.originalVirtualTable->OnMouseOut(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original OnMouseOut. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs); + } + + private void OnFocus(AtkUnitBase* thisPtr) + { + this.LogEvent(EnableLogging); + + this.focusArgs.Addon = thisPtr; + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs); + + try + { + this.originalVirtualTable->Focus(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original OnFocus. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs); + } + [Conditional("DEBUG")] private void LogEvent(bool loggingEnabled, [CallerMemberName] string caller = "") { From c923884626fa52470700a65d5f0c8236c7905238 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 22:15:32 -0800 Subject: [PATCH 063/164] Disable Logging --- Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index b92466b5a..6a27cc8f9 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -20,7 +20,7 @@ internal unsafe class AddonVirtualTable : IDisposable // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; - private const bool EnableLogging = true; + private const bool EnableLogging = false; private static readonly ModuleLog Log = new("LifecycleVT"); From 85a7c60daedf7bf85dc0b4c914f1ff3fd9689c1f Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 30 Nov 2025 22:20:02 -0800 Subject: [PATCH 064/164] Fix name inconsistency --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 6a27cc8f9..8fbf77534 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -102,10 +102,10 @@ internal unsafe class AddonVirtualTable : IDisposable this.closeFunction = this.OnAddonClose; this.showFunction = this.OnAddonShow; this.hideFunction = this.OnAddonHide; - this.onMoveFunction = this.OnMove; - this.onMouseOverFunction = this.OnMouseOver; - this.onMouseOutFunction = this.OnMouseOut; - this.focusFunction = this.OnFocus; + this.onMoveFunction = this.OnAddonMove; + this.onMouseOverFunction = this.OnAddonMouseOver; + this.onMouseOutFunction = this.OnAddonMouseOut; + this.focusFunction = this.OnAddonFocus; // Overwrite specific virtual table entries this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); @@ -409,7 +409,7 @@ internal unsafe class AddonVirtualTable : IDisposable this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); } - private void OnMove(AtkUnitBase* thisPtr) + private void OnAddonMove(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); @@ -422,13 +422,13 @@ internal unsafe class AddonVirtualTable : IDisposable } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnMove. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception when calling original OnAddonMove. This may be a bug in the game or another plugin hooking this method."); } this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs); } - private void OnMouseOver(AtkUnitBase* thisPtr) + private void OnAddonMouseOver(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); @@ -441,13 +441,13 @@ internal unsafe class AddonVirtualTable : IDisposable } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnMouseOver. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception when calling original OnAddonMouseOver. This may be a bug in the game or another plugin hooking this method."); } this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs); } - private void OnMouseOut(AtkUnitBase* thisPtr) + private void OnAddonMouseOut(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); @@ -460,13 +460,13 @@ internal unsafe class AddonVirtualTable : IDisposable } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnMouseOut. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception when calling original OnAddonMouseOut. This may be a bug in the game or another plugin hooking this method."); } this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs); } - private void OnFocus(AtkUnitBase* thisPtr) + private void OnAddonFocus(AtkUnitBase* thisPtr) { this.LogEvent(EnableLogging); @@ -479,7 +479,7 @@ internal unsafe class AddonVirtualTable : IDisposable } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnFocus. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception when calling original OnAddonFocus. This may be a bug in the game or another plugin hooking this method."); } this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs); From 0112e17fdb052d0e3ebd6dc87b9b8bfaeaf9e1e0 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 4 Dec 2025 23:27:06 +0100 Subject: [PATCH 065/164] Replace internal SharpDX usage with TerraFX --- .../Internals/FontAtlasFactory.BuildToolkit.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 2a93cf093..41c87fd39 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -15,7 +15,6 @@ using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Storage.Assets; using Dalamud.Utility; -using SharpDX.DXGI; using TerraFX.Interop.DirectX; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -749,7 +748,7 @@ internal sealed partial class FontAtlasFactory new( width, height, - (int)(use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm), + (int)(use4 ? DXGI_FORMAT.DXGI_FORMAT_B4G4R4A4_UNORM : DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM), width * bpp), buf, name); From da7be64fdf3bfd69cd69d77b98d1a7eaf2f3a73a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 4 Dec 2025 23:31:31 +0100 Subject: [PATCH 066/164] Remove SharpDX --- Dalamud/Dalamud.csproj | 2 - Dalamud/Interface/UiBuilder.cs | 16 ------ Dalamud/Storage/Assets/DalamudAssetPurpose.cs | 6 +-- Dalamud/Utility/VectorExtensions.cs | 51 ------------------- Directory.Packages.props | 2 - 5 files changed, 3 insertions(+), 74 deletions(-) delete mode 100644 Dalamud/Utility/VectorExtensions.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index b9b453f89..e8c2516af 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -73,8 +73,6 @@ all - - diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index e38537018..6e4740b22 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -12,7 +12,6 @@ using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.Internal; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; -using Dalamud.Plugin; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Serilog; @@ -150,13 +149,6 @@ public interface IUiBuilder /// public ImFontPtr FontMono { get; } - /// - /// Gets the game's active Direct3D device. - /// - // TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud. - [Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")] - SharpDX.Direct3D11.Device Device { get; } - /// Gets the game's active Direct3D device. /// Pointer to the instance of IUnknown that the game is using and should be containing an ID3D11Device, /// or 0 if it is not available yet. @@ -302,8 +294,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder private IFontHandle? monoFontHandle; private IFontHandle? iconFontFixedWidthHandle; - private SharpDX.Direct3D11.Device? sdxDevice; - /// /// Initializes a new instance of the class and registers it. /// You do not have to call this manually. @@ -493,12 +483,6 @@ public sealed class UiBuilder : IDisposable, IUiBuilder this.InterfaceManagerWithScene?.MonoFontHandle ?? throw new InvalidOperationException("Scene is not yet ready."))); - /// - // TODO: Remove it on API11/APIXI, and remove SharpDX/PInvoke/etc. dependency from Dalamud. - [Obsolete($"Use {nameof(DeviceHandle)} and wrap it using DirectX wrapper library of your choice.")] - public SharpDX.Direct3D11.Device Device => - this.sdxDevice ??= new(this.InterfaceManagerWithScene!.Backend!.DeviceHandle); - /// public nint DeviceHandle => this.InterfaceManagerWithScene?.Backend?.DeviceHandle ?? 0; diff --git a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs index e6c7bd920..69de1f871 100644 --- a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs +++ b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs @@ -11,12 +11,12 @@ public enum DalamudAssetPurpose Empty = 0, /// - /// The asset is a .png file, and can be purposed as a . + /// The asset is a .png file, and can be purposed as a . /// TextureFromPng = 10, - + /// - /// The asset is a raw texture, and can be purposed as a . + /// The asset is a raw texture, and can be purposed as a . /// TextureFromRaw = 1001, diff --git a/Dalamud/Utility/VectorExtensions.cs b/Dalamud/Utility/VectorExtensions.cs deleted file mode 100644 index f617c8420..000000000 --- a/Dalamud/Utility/VectorExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Numerics; - -namespace Dalamud.Utility; - -/// -/// Extension methods for System.Numerics.VectorN and SharpDX.VectorN. -/// -public static class VectorExtensions -{ - /// - /// Converts a SharpDX vector to System.Numerics. - /// - /// Vector to convert. - /// A converted vector. - public static Vector2 ToSystem(this SharpDX.Vector2 vec) => new(x: vec.X, y: vec.Y); - - /// - /// Converts a SharpDX vector to System.Numerics. - /// - /// Vector to convert. - /// A converted vector. - public static Vector3 ToSystem(this SharpDX.Vector3 vec) => new(x: vec.X, y: vec.Y, z: vec.Z); - - /// - /// Converts a SharpDX vector to System.Numerics. - /// - /// Vector to convert. - /// A converted vector. - public static Vector4 ToSystem(this SharpDX.Vector4 vec) => new(x: vec.X, y: vec.Y, z: vec.Z, w: vec.W); - - /// - /// Converts a System.Numerics vector to SharpDX. - /// - /// Vector to convert. - /// A converted vector. - public static SharpDX.Vector2 ToSharpDX(this Vector2 vec) => new(x: vec.X, y: vec.Y); - - /// - /// Converts a System.Numerics vector to SharpDX. - /// - /// Vector to convert. - /// A converted vector. - public static SharpDX.Vector3 ToSharpDX(this Vector3 vec) => new(x: vec.X, y: vec.Y, z: vec.Z); - - /// - /// Converts a System.Numerics vector to SharpDX. - /// - /// Vector to convert. - /// A converted vector. - public static SharpDX.Vector4 ToSharpDX(this Vector4 vec) => new(x: vec.X, y: vec.Y, z: vec.Z, w: vec.W); -} diff --git a/Directory.Packages.props b/Directory.Packages.props index 903a8ee88..481e7591d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,8 +27,6 @@ - - From ddc31132444f350a8bb1f794a8d5e91bec00c9a3 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Dec 2025 00:37:25 +0100 Subject: [PATCH 067/164] Update TerraFX.Interop.Windows --- .../IObjectWithLocalizableName.cs | 4 +-- .../FontIdentifier/SystemFontFamilyId.cs | 4 +-- .../Interface/FontIdentifier/SystemFontId.cs | 6 ++-- .../ImGuiBackend/Helpers/ReShadePeeler.cs | 14 ++++---- .../InputHandler/Win32InputHandler.cs | 10 +++--- .../Interface/Internal/InterfaceManager.cs | 2 +- .../ReShadeAddonInterface.Exports.cs | 6 ++-- .../ReShadeHandling/ReShadeUnwrapper.cs | 2 +- .../Interface/Internal/StaThreadService.cs | 8 ++--- .../Textures/Internal/BitmapCodecInfo.cs | 4 +-- .../Internal/TextureManager.BlameTracker.cs | 6 ++-- .../Internal/TextureManager.Clipboard.cs | 6 ++-- Dalamud/Service/LoadingDialog.cs | 32 +++++++++---------- Dalamud/Utility/ClipboardFormats.cs | 4 +-- Dalamud/Utility/TerraFxCom/ManagedIStream.cs | 28 ++++++++-------- .../TerraFxComInterfaceExtensions.cs | 8 ++--- Directory.Packages.props | 2 +- 17 files changed, 73 insertions(+), 73 deletions(-) diff --git a/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs index 2b970a5fd..4b3860431 100644 --- a/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs +++ b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs @@ -64,9 +64,9 @@ public interface IObjectWithLocalizableName var result = new Dictionary((int)count); for (var i = 0u; i < count; i++) { - fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError(); + fn->GetLocaleName(i, buf, maxStrLen).ThrowOnError(); var key = new string(buf); - fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError(); + fn->GetString(i, buf, maxStrLen).ThrowOnError(); var value = new string(buf); result[key.ToLowerInvariant()] = value; } diff --git a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs index 420ee77a4..83a5e810d 100644 --- a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs +++ b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs @@ -133,8 +133,8 @@ public sealed class SystemFontFamilyId : IFontFamilyId var familyIndex = 0u; BOOL exists = false; - fixed (void* pName = this.EnglishName) - sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError(); + fixed (char* pName = this.EnglishName) + sfc.Get()->FindFamilyName(pName, &familyIndex, &exists).ThrowOnError(); if (!exists) throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found."); diff --git a/Dalamud/Interface/FontIdentifier/SystemFontId.cs b/Dalamud/Interface/FontIdentifier/SystemFontId.cs index e11759a88..8401f4c79 100644 --- a/Dalamud/Interface/FontIdentifier/SystemFontId.cs +++ b/Dalamud/Interface/FontIdentifier/SystemFontId.cs @@ -113,8 +113,8 @@ public sealed class SystemFontId : IFontId var familyIndex = 0u; BOOL exists = false; - fixed (void* name = this.Family.EnglishName) - sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError(); + fixed (char* name = this.Family.EnglishName) + sfc.Get()->FindFamilyName(name, &familyIndex, &exists).ThrowOnError(); if (!exists) throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found."); @@ -151,7 +151,7 @@ public sealed class SystemFontId : IFontId flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError(); var path = stackalloc char[(int)pathSize + 1]; - flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError(); + flocal.Get()->GetFilePathFromKey(refKey, refKeySize, path, pathSize + 1).ThrowOnError(); return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex()); } diff --git a/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs index 824ba382a..3f3c98c26 100644 --- a/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs +++ b/Dalamud/Interface/ImGuiBackend/Helpers/ReShadePeeler.cs @@ -104,19 +104,19 @@ internal static unsafe class ReShadePeeler fixed (byte* pfn5 = "glBegin"u8) fixed (byte* pfn6 = "vkCreateDevice"u8) { - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn0) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn1) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn2) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn3) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn4) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn5) == null) continue; - if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == 0) + if (GetProcAddress((HMODULE)dosh, (sbyte*)pfn6) == null) continue; } diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs index 596df4c67..18330d3a2 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs @@ -622,7 +622,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler hbrBackground = (HBRUSH)(1 + COLOR.COLOR_BACKGROUND), lpfnWndProc = (delegate* unmanaged)Marshal .GetFunctionPointerForDelegate(this.input.wndProcDelegate), - lpszClassName = (ushort*)windowClassNamePtr, + lpszClassName = windowClassNamePtr, }; if (RegisterClassExW(&wcex) == 0) @@ -658,7 +658,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler fixed (char* windowClassNamePtr = WindowClassName) { UnregisterClassW( - (ushort*)windowClassNamePtr, + windowClassNamePtr, (HINSTANCE)Marshal.GetHINSTANCE(typeof(ViewportHandler).Module)); } @@ -781,8 +781,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler { data->Hwnd = CreateWindowExW( (uint)data->DwExStyle, - (ushort*)windowClassNamePtr, - (ushort*)windowClassNamePtr, + windowClassNamePtr, + windowClassNamePtr, (uint)data->DwStyle, rect.left, rect.top, @@ -993,7 +993,7 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler { var data = (ImGuiViewportDataWin32*)viewport.PlatformUserData; fixed (char* pwszTitle = MemoryHelper.ReadStringNullTerminated((nint)title)) - SetWindowTextW(data->Hwnd, (ushort*)pwszTitle); + SetWindowTextW(data->Hwnd, pwszTitle); } [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 76a1b5172..96fcb7dfd 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -256,7 +256,7 @@ internal partial class InterfaceManager : IInternalDisposableService var gwh = default(HWND); fixed (char* pClass = "FFXIVGAME") { - while ((gwh = FindWindowExW(default, gwh, (ushort*)pClass, default)) != default) + while ((gwh = FindWindowExW(default, gwh, pClass, default)) != default) { uint pid; _ = GetWindowThreadProcessId(gwh, &pid); diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs index d8d210076..d7d3b56c3 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeAddonInterface.Exports.cs @@ -63,11 +63,11 @@ internal sealed unsafe partial class ReShadeAddonInterface return; - bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res) + static bool GetProcAddressInto(ProcessModule m, ReadOnlySpan name, void* res) { Span name8 = stackalloc byte[Encoding.UTF8.GetByteCount(name) + 1]; name8[Encoding.UTF8.GetBytes(name, name8)] = 0; - *(nint*)res = GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0])); + *(nint*)res = (nint)GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)Unsafe.AsPointer(ref name8[0])); return *(nint*)res != 0; } } @@ -174,7 +174,7 @@ internal sealed unsafe partial class ReShadeAddonInterface CERT.CERT_NAME_SIMPLE_DISPLAY_TYPE, CERT.CERT_NAME_ISSUER_FLAG, null, - (ushort*)Unsafe.AsPointer(ref issuerName[0]), + (char*)Unsafe.AsPointer(ref issuerName[0]), pcb); if (pcb == 0) throw new Win32Exception("CertGetNameStringW(2)"); diff --git a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs index f1210425d..711de6eb2 100644 --- a/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs +++ b/Dalamud/Interface/Internal/ReShadeHandling/ReShadeUnwrapper.cs @@ -94,7 +94,7 @@ internal static unsafe class ReShadeUnwrapper static bool HasProcExported(ProcessModule m, ReadOnlySpan name) { fixed (byte* p = name) - return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != 0; + return GetProcAddress((HMODULE)m.BaseAddress, (sbyte*)p) != null; } } diff --git a/Dalamud/Interface/Internal/StaThreadService.cs b/Dalamud/Interface/Internal/StaThreadService.cs index 87e003288..bb5caa281 100644 --- a/Dalamud/Interface/Internal/StaThreadService.cs +++ b/Dalamud/Interface/Internal/StaThreadService.cs @@ -216,7 +216,7 @@ internal partial class StaThreadService : IInternalDisposableService lpfnWndProc = &MessageReceiverWndProcStatic, hInstance = hInstance, hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1), - lpszClassName = (ushort*)name, + lpszClassName = name, }; wndClassAtom = RegisterClassExW(&wndClass); @@ -226,8 +226,8 @@ internal partial class StaThreadService : IInternalDisposableService this.messageReceiverHwndTask.SetResult( CreateWindowExW( 0, - (ushort*)wndClassAtom, - (ushort*)name, + (char*)wndClassAtom, + name, 0, CW_USEDEFAULT, CW_USEDEFAULT, @@ -275,7 +275,7 @@ internal partial class StaThreadService : IInternalDisposableService _ = OleFlushClipboard(); OleUninitialize(); if (wndClassAtom != 0) - UnregisterClassW((ushort*)wndClassAtom, hInstance); + UnregisterClassW((char*)wndClassAtom, hInstance); this.messageReceiverHwndTask.TrySetException(e); } } diff --git a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs index 3d5456500..ec56caadd 100644 --- a/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs +++ b/Dalamud/Interface/Textures/Internal/BitmapCodecInfo.cs @@ -44,12 +44,12 @@ internal sealed class BitmapCodecInfo : IBitmapCodecInfo private static unsafe string ReadStringUsing( IWICBitmapCodecInfo* codecInfo, - delegate* unmanaged readFuncPtr) + delegate* unmanaged[MemberFunction] readFuncPtr) { var cch = 0u; _ = readFuncPtr(codecInfo, 0, null, &cch); var buf = stackalloc char[(int)cch + 1]; - Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, (ushort*)buf, &cch)); + Marshal.ThrowExceptionForHR(readFuncPtr(codecInfo, cch + 1, buf, &cch)); return new(buf, 0, (int)cch); } } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs index 837b41271..fde40d462 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.BlameTracker.cs @@ -219,14 +219,14 @@ internal sealed partial class TextureManager return; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int QueryInterfaceStatic(IUnknown* pThis, Guid* riid, void** ppvObject) => ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static uint AddRefStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static uint ReleaseStatic(IUnknown* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); } diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs index 8a510e967..75f7ab975 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs @@ -133,7 +133,7 @@ internal sealed partial class TextureManager }, }, }; - namea.AsSpan().CopyTo(new(fgda.fgd.e0.cFileName, 260)); + namea.AsSpan().CopyTo(new(Unsafe.AsPointer(ref fgda.fgd.e0.cFileName[0]), 260)); AddToDataObject( pdo, @@ -157,7 +157,7 @@ internal sealed partial class TextureManager }, }, }; - preferredFileNameWithoutExtension.AsSpan().CopyTo(new(fgdw.fgd.e0.cFileName, 260)); + preferredFileNameWithoutExtension.AsSpan().CopyTo(new(Unsafe.AsPointer(ref fgdw.fgd.e0.cFileName[0]), 260)); AddToDataObject( pdo, @@ -450,7 +450,7 @@ internal sealed partial class TextureManager try { IStream* pfs; - SHCreateStreamOnFileW((ushort*)pPath, sharedRead, &pfs).ThrowOnError(); + SHCreateStreamOnFileW((char*)pPath, sharedRead, &pfs).ThrowOnError(); var stgm2 = new STGMEDIUM { diff --git a/Dalamud/Service/LoadingDialog.cs b/Dalamud/Service/LoadingDialog.cs index 424087743..ea45d3bb2 100644 --- a/Dalamud/Service/LoadingDialog.cs +++ b/Dalamud/Service/LoadingDialog.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Drawing; @@ -294,18 +294,18 @@ internal sealed class LoadingDialog ? null : Icon.ExtractAssociatedIcon(Path.Combine(workingDirectory, "Dalamud.Injector.exe")); - fixed (void* pszEmpty = "-") - fixed (void* pszWindowTitle = "Dalamud") - fixed (void* pszDalamudBoot = "Dalamud.Boot.dll") - fixed (void* pszThemesManifestResourceName = "RT_MANIFEST_THEMES") - fixed (void* pszHide = Loc.Localize("LoadingDialogHide", "Hide")) - fixed (void* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs")) - fixed (void* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs")) + fixed (char* pszEmpty = "-") + fixed (char* pszWindowTitle = "Dalamud") + fixed (char* pszDalamudBoot = "Dalamud.Boot.dll") + fixed (char* pszThemesManifestResourceName = "RT_MANIFEST_THEMES") + fixed (char* pszHide = Loc.Localize("LoadingDialogHide", "Hide")) + fixed (char* pszShowLatestLogs = Loc.Localize("LoadingDialogShowLatestLogs", "Show Latest Logs")) + fixed (char* pszHideLatestLogs = Loc.Localize("LoadingDialogHideLatestLogs", "Hide Latest Logs")) { var taskDialogButton = new TASKDIALOG_BUTTON { nButtonID = IDOK, - pszButtonText = (ushort*)pszHide, + pszButtonText = pszHide, }; var taskDialogConfig = new TASKDIALOGCONFIG { @@ -318,8 +318,8 @@ internal sealed class LoadingDialog (int)TDF_CALLBACK_TIMER | (extractedIcon is null ? 0 : (int)TDF_USE_HICON_MAIN), dwCommonButtons = 0, - pszWindowTitle = (ushort*)pszWindowTitle, - pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (ushort*)extractedIcon.Handle, + pszWindowTitle = pszWindowTitle, + pszMainIcon = extractedIcon is null ? TD.TD_INFORMATION_ICON : (char*)extractedIcon.Handle, pszMainInstruction = null, pszContent = null, cButtons = 1, @@ -329,9 +329,9 @@ internal sealed class LoadingDialog pRadioButtons = null, nDefaultRadioButton = 0, pszVerificationText = null, - pszExpandedInformation = (ushort*)pszEmpty, - pszExpandedControlText = (ushort*)pszShowLatestLogs, - pszCollapsedControlText = (ushort*)pszHideLatestLogs, + pszExpandedInformation = pszEmpty, + pszExpandedControlText = pszShowLatestLogs, + pszCollapsedControlText = pszHideLatestLogs, pszFooterIcon = null, pszFooter = null, pfCallback = &HResultFuncBinder, @@ -348,8 +348,8 @@ internal sealed class LoadingDialog { cbSize = (uint)sizeof(ACTCTXW), dwFlags = ACTCTX_FLAG_HMODULE_VALID | ACTCTX_FLAG_RESOURCE_NAME_VALID, - lpResourceName = (ushort*)pszThemesManifestResourceName, - hModule = GetModuleHandleW((ushort*)pszDalamudBoot), + lpResourceName = pszThemesManifestResourceName, + hModule = GetModuleHandleW(pszDalamudBoot), }; hActCtx = CreateActCtxW(&actctx); if (hActCtx == default) diff --git a/Dalamud/Utility/ClipboardFormats.cs b/Dalamud/Utility/ClipboardFormats.cs index 07b6c00d6..b80e05dd3 100644 --- a/Dalamud/Utility/ClipboardFormats.cs +++ b/Dalamud/Utility/ClipboardFormats.cs @@ -30,8 +30,8 @@ internal static class ClipboardFormats private static unsafe uint ClipboardFormatFromName(ReadOnlySpan name) { uint cf; - fixed (void* p = name) - cf = RegisterClipboardFormatW((ushort*)p); + fixed (char* p = name) + cf = RegisterClipboardFormatW(p); if (cf != 0) return cf; throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? diff --git a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs index caec65da2..eb1997daf 100644 --- a/Dalamud/Utility/TerraFxCom/ManagedIStream.cs +++ b/Dalamud/Utility/TerraFxCom/ManagedIStream.cs @@ -57,60 +57,60 @@ internal sealed unsafe class ManagedIStream : IStream.Interface, IRefCountable static ManagedIStream? ToManagedObject(void* pThis) => GCHandle.FromIntPtr(((nint*)pThis)[1]).Target as ManagedIStream; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int QueryInterfaceStatic(IStream* pThis, Guid* riid, void** ppvObject) => ToManagedObject(pThis)?.QueryInterface(riid, ppvObject) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static uint AddRefStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.AddRef() ?? 0); - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static uint ReleaseStatic(IStream* pThis) => (uint)(ToManagedObject(pThis)?.Release() ?? 0); - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int ReadStatic(IStream* pThis, void* pv, uint cb, uint* pcbRead) => ToManagedObject(pThis)?.Read(pv, cb, pcbRead) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int WriteStatic(IStream* pThis, void* pv, uint cb, uint* pcbWritten) => ToManagedObject(pThis)?.Write(pv, cb, pcbWritten) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int SeekStatic( IStream* pThis, LARGE_INTEGER dlibMove, uint dwOrigin, ULARGE_INTEGER* plibNewPosition) => ToManagedObject(pThis)?.Seek(dlibMove, dwOrigin, plibNewPosition) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int SetSizeStatic(IStream* pThis, ULARGE_INTEGER libNewSize) => ToManagedObject(pThis)?.SetSize(libNewSize) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int CopyToStatic( IStream* pThis, IStream* pstm, ULARGE_INTEGER cb, ULARGE_INTEGER* pcbRead, ULARGE_INTEGER* pcbWritten) => ToManagedObject(pThis)?.CopyTo(pstm, cb, pcbRead, pcbWritten) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int CommitStatic(IStream* pThis, uint grfCommitFlags) => ToManagedObject(pThis)?.Commit(grfCommitFlags) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int RevertStatic(IStream* pThis) => ToManagedObject(pThis)?.Revert() ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int LockRegionStatic(IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => ToManagedObject(pThis)?.LockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int UnlockRegionStatic( IStream* pThis, ULARGE_INTEGER libOffset, ULARGE_INTEGER cb, uint dwLockType) => ToManagedObject(pThis)?.UnlockRegion(libOffset, cb, dwLockType) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int StatStatic(IStream* pThis, STATSTG* pstatstg, uint grfStatFlag) => ToManagedObject(pThis)?.Stat(pstatstg, grfStatFlag) ?? E.E_UNEXPECTED; - [UnmanagedCallersOnly] + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] static int CloneStatic(IStream* pThis, IStream** ppstm) => ToManagedObject(pThis)?.Clone(ppstm) ?? E.E_UNEXPECTED; } diff --git a/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs index f9252839f..ec108403e 100644 --- a/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs +++ b/Dalamud/Utility/TerraFxCom/TerraFxComInterfaceExtensions.cs @@ -88,7 +88,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions fixed (char* pPath = path) { SHCreateStreamOnFileEx( - (ushort*)pPath, + pPath, grfMode, (uint)attributes, fCreate, @@ -115,7 +115,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions { fixed (char* pName = name) { - var option = new PROPBAG2 { pstrName = (ushort*)pName }; + var option = new PROPBAG2 { pstrName = pName }; return obj.Write(1, &option, &varValue); } } @@ -145,7 +145,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions try { fixed (char* pName = name) - return obj.SetMetadataByName((ushort*)pName, &propVarValue); + return obj.SetMetadataByName(pName, &propVarValue); } finally { @@ -165,7 +165,7 @@ internal static unsafe partial class TerraFxComInterfaceExtensions public static HRESULT RemoveMetadataByName(ref this IWICMetadataQueryWriter obj, string name) { fixed (char* pName = name) - return obj.RemoveMetadataByName((ushort*)pName); + return obj.RemoveMetadataByName(pName); } [LibraryImport("propsys.dll")] diff --git a/Directory.Packages.props b/Directory.Packages.props index 903a8ee88..d62d247c3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From fc983458fa16c698977b386fdefac4d385256f01 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Dec 2025 01:44:18 +0100 Subject: [PATCH 068/164] Update Nuke --- build/DalamudBuild.cs | 6 ++---- build/build.csproj | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/build/DalamudBuild.cs b/build/DalamudBuild.cs index ba2b09a4d..1a189f2c7 100644 --- a/build/DalamudBuild.cs +++ b/build/DalamudBuild.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using Nuke.Common; using Nuke.Common.Execution; using Nuke.Common.Git; @@ -128,7 +127,7 @@ public class DalamudBuild : NukeBuild if (IsCIBuild) { s = s - .SetProcessArgumentConfigurator(a => a.Add("/clp:NoSummary")); // Disable MSBuild summary on CI builds + .SetProcessAdditionalArguments("/clp:NoSummary"); // Disable MSBuild summary on CI builds } // We need to emit compiler generated files for the docs build, since docfx can't run generators directly // TODO: This fails every build after this because of redefinitions... @@ -238,7 +237,6 @@ public class DalamudBuild : NukeBuild .SetProject(InjectorProjectFile) .SetConfiguration(Configuration)); - FileSystemTasks.DeleteDirectory(ArtifactsDirectory); - Directory.CreateDirectory(ArtifactsDirectory); + ArtifactsDirectory.CreateOrCleanDirectory(); }); } diff --git a/build/build.csproj b/build/build.csproj index 1e1416d92..7096c7f8a 100644 --- a/build/build.csproj +++ b/build/build.csproj @@ -11,7 +11,7 @@ false - + From e7d4786a1fec6411908ed9e319f1a06b67738389 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Fri, 5 Dec 2025 18:18:57 +0100 Subject: [PATCH 069/164] Oops, wrong version --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 481e7591d..6c5070d35 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + From 7cf20fe102bf14bea99dead9ec0d4e5ad3c23c92 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Dec 2025 00:35:52 +0100 Subject: [PATCH 070/164] Update Microsoft.Windows.CsWin32 --- Dalamud/SafeMemory.cs | 15 +++++++++++++-- Dalamud/Utility/FilesystemUtil.cs | 10 +++++++--- Directory.Packages.props | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs index a8ac40a5d..9a1af0625 100644 --- a/Dalamud/SafeMemory.cs +++ b/Dalamud/SafeMemory.cs @@ -1,6 +1,8 @@ using System.Runtime.InteropServices; using System.Text; +using Windows.Win32.Foundation; + namespace Dalamud; /// @@ -28,12 +30,18 @@ public static class SafeMemory /// Whether the read succeeded. public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer) { + if (Handle.IsClosed || Handle.IsInvalid) + { + buffer = []; + return false; + } + buffer = new byte[count <= 0 ? 0 : count]; fixed (byte* p = buffer) { UIntPtr bytesRead; if (!Windows.Win32.PInvoke.ReadProcessMemory( - Handle, + (HANDLE)Handle.DangerousGetHandle(), address.ToPointer(), p, new UIntPtr((uint)count), @@ -54,6 +62,9 @@ public static class SafeMemory /// Whether the write succeeded. public static unsafe bool WriteBytes(IntPtr address, byte[] buffer) { + if (Handle.IsClosed || Handle.IsInvalid) + return false; + if (buffer.Length == 0) return true; @@ -61,7 +72,7 @@ public static class SafeMemory fixed (byte* p = buffer) { if (!Windows.Win32.PInvoke.WriteProcessMemory( - Handle, + (HANDLE)Handle.DangerousGetHandle(), address.ToPointer(), p, new UIntPtr((uint)buffer.Length), diff --git a/Dalamud/Utility/FilesystemUtil.cs b/Dalamud/Utility/FilesystemUtil.cs index 3b4298b37..560e06da3 100644 --- a/Dalamud/Utility/FilesystemUtil.cs +++ b/Dalamud/Utility/FilesystemUtil.cs @@ -1,7 +1,8 @@ -using System.ComponentModel; +using System.ComponentModel; using System.IO; using System.Text; +using Windows.Win32.Foundation; using Windows.Win32.Storage.FileSystem; namespace Dalamud.Utility; @@ -61,8 +62,11 @@ public static class FilesystemUtil // Write the data uint bytesWritten = 0; - if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan(bytes), &bytesWritten, null)) - throw new Win32Exception(); + fixed (byte* ptr = bytes) + { + if (!Windows.Win32.PInvoke.WriteFile((HANDLE)tempFile.DangerousGetHandle(), ptr, (uint)bytes.Length, &bytesWritten, null)) + throw new Win32Exception(); + } if (bytesWritten != bytes.Length) throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); diff --git a/Directory.Packages.props b/Directory.Packages.props index 6c5070d35..58e355400 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -27,7 +27,7 @@ - + From d94cacaac3bec7e2b64d3d13bde9d1dbd61d0714 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Dec 2025 19:10:31 +0100 Subject: [PATCH 071/164] Disable SafeHandles --- Dalamud/EntryPoint.cs | 2 +- Dalamud/Hooking/Hook.cs | 14 +++++++------- Dalamud/NativeMethods.json | 1 + Dalamud/SafeMemory.cs | 12 ++++++------ Dalamud/Utility/FilesystemUtil.cs | 16 +++++++++++----- Dalamud/Utility/Util.cs | 5 ++--- 6 files changed, 28 insertions(+), 22 deletions(-) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 15077f3d8..b5504b046 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -263,7 +263,7 @@ public sealed class EntryPoint var symbolPath = Path.Combine(info.AssetDirectory, "UIRes", "pdb"); var searchPath = $".;{symbolPath}"; - var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle(); + var currentProcess = Windows.Win32.PInvoke.GetCurrentProcess(); // Remove any existing Symbol Handler and Init a new one with our search path added Windows.Win32.PInvoke.SymCleanup(currentProcess); diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index faf4658a5..1cd3ef91d 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -201,19 +201,19 @@ public abstract class Hook : IDalamudHook where T : Delegate if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; - using var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName); - if (moduleHandle.IsInvalid) + var moduleHandle = Windows.Win32.PInvoke.GetModuleHandle(moduleName); + if (moduleHandle.IsNull) throw new Exception($"Could not get a handle to module {moduleName}"); - var procAddress = (nint)Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName); - if (procAddress == IntPtr.Zero) + var procAddress = Windows.Win32.PInvoke.GetProcAddress(moduleHandle, exportName); + if (procAddress.IsNull) throw new Exception($"Could not get the address of {moduleName}::{exportName}"); - procAddress = HookManager.FollowJmp(procAddress); + var address = HookManager.FollowJmp(procAddress.Value); if (useMinHook) - return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); + return new MinHookHook(address, detour, Assembly.GetCallingAssembly()); else - return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly()); + return new ReloadedHook(address, detour, Assembly.GetCallingAssembly()); } /// diff --git a/Dalamud/NativeMethods.json b/Dalamud/NativeMethods.json index ffb313dfc..46fd3504f 100644 --- a/Dalamud/NativeMethods.json +++ b/Dalamud/NativeMethods.json @@ -1,4 +1,5 @@ { "$schema": "https://aka.ms/CsWin32.schema.json", + "useSafeHandles": false, "allowMarshaling": false } diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs index 9a1af0625..ca0c8ff92 100644 --- a/Dalamud/SafeMemory.cs +++ b/Dalamud/SafeMemory.cs @@ -14,11 +14,11 @@ namespace Dalamud; /// public static class SafeMemory { - private static readonly SafeHandle Handle; + private static readonly HANDLE Handle; static SafeMemory() { - Handle = Windows.Win32.PInvoke.GetCurrentProcess_SafeHandle(); + Handle = Windows.Win32.PInvoke.GetCurrentProcess(); } /// @@ -30,7 +30,7 @@ public static class SafeMemory /// Whether the read succeeded. public static unsafe bool ReadBytes(IntPtr address, int count, out byte[] buffer) { - if (Handle.IsClosed || Handle.IsInvalid) + if (Handle.IsNull) { buffer = []; return false; @@ -41,7 +41,7 @@ public static class SafeMemory { UIntPtr bytesRead; if (!Windows.Win32.PInvoke.ReadProcessMemory( - (HANDLE)Handle.DangerousGetHandle(), + Handle, address.ToPointer(), p, new UIntPtr((uint)count), @@ -62,7 +62,7 @@ public static class SafeMemory /// Whether the write succeeded. public static unsafe bool WriteBytes(IntPtr address, byte[] buffer) { - if (Handle.IsClosed || Handle.IsInvalid) + if (Handle.IsNull) return false; if (buffer.Length == 0) @@ -72,7 +72,7 @@ public static class SafeMemory fixed (byte* p = buffer) { if (!Windows.Win32.PInvoke.WriteProcessMemory( - (HANDLE)Handle.DangerousGetHandle(), + Handle, address.ToPointer(), p, new UIntPtr((uint)buffer.Length), diff --git a/Dalamud/Utility/FilesystemUtil.cs b/Dalamud/Utility/FilesystemUtil.cs index 560e06da3..f1b62ee21 100644 --- a/Dalamud/Utility/FilesystemUtil.cs +++ b/Dalamud/Utility/FilesystemUtil.cs @@ -48,33 +48,39 @@ public static class FilesystemUtil // Open the temp file var tempPath = path + ".tmp"; - using var tempFile = Windows.Win32.PInvoke.CreateFile( + var tempFile = Windows.Win32.PInvoke.CreateFile( tempPath, (uint)(FILE_ACCESS_RIGHTS.FILE_GENERIC_READ | FILE_ACCESS_RIGHTS.FILE_GENERIC_WRITE), FILE_SHARE_MODE.FILE_SHARE_NONE, null, FILE_CREATION_DISPOSITION.CREATE_ALWAYS, FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_NORMAL, - null); + HANDLE.Null); - if (tempFile.IsInvalid) + if (tempFile.IsNull) throw new Win32Exception(); // Write the data uint bytesWritten = 0; fixed (byte* ptr = bytes) { - if (!Windows.Win32.PInvoke.WriteFile((HANDLE)tempFile.DangerousGetHandle(), ptr, (uint)bytes.Length, &bytesWritten, null)) + if (!Windows.Win32.PInvoke.WriteFile(tempFile, ptr, (uint)bytes.Length, &bytesWritten, null)) throw new Win32Exception(); } if (bytesWritten != bytes.Length) + { + Windows.Win32.PInvoke.CloseHandle(tempFile); throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); + } if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile)) + { + Windows.Win32.PInvoke.CloseHandle(tempFile); throw new Win32Exception(); + } - tempFile.Close(); + Windows.Win32.PInvoke.CloseHandle(tempFile); if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) throw new Win32Exception(); diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 19610ef64..f50efcf0d 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -858,7 +858,7 @@ public static partial class Util var sizeWithTerminators = pathBytesSize + (pathBytes.Length * 2); var dropFilesSize = sizeof(DROPFILES); - var hGlobal = Win32_PInvoke.GlobalAlloc_SafeHandle( + var hGlobal = Win32_PInvoke.GlobalAlloc( GLOBAL_ALLOC_FLAGS.GHND, // struct size + size of encoded strings + null terminator for each // string + two null terminators for end of list @@ -896,12 +896,11 @@ public static partial class Util { Win32_PInvoke.SetClipboardData( (uint)CLIPBOARD_FORMAT.CF_HDROP, - hGlobal); + (Windows.Win32.Foundation.HANDLE)hGlobal.Value); Win32_PInvoke.CloseClipboard(); return true; } - hGlobal.Dispose(); return false; } From a36e11574b14ea6887cb0f7d2513920ebdd820bf Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 6 Dec 2025 01:10:00 +0100 Subject: [PATCH 072/164] Add git status checks to workflow to see what's dirty --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 299d71e95..f552e446b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,8 +33,12 @@ jobs: ($env:REPO_NAME) >> VERSION ($env:BRANCH) >> VERSION ($env:COMMIT) >> VERSION + - name: git status + run: git status - name: Build and Test Dalamud run: .\build.ps1 ci + - name: git status + run: git status - name: Sign Dalamud if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} env: From 45366efd9fc888bf3908144f248b46da00c5d4ff Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Fri, 5 Dec 2025 17:10:58 -0800 Subject: [PATCH 073/164] Remove SigScanner from ctor --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index ddcebe718..716ce1bfb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -29,7 +29,7 @@ internal unsafe class AddonLifecycle : IInternalDisposableService private Hook? onInitializeAddonHook; [ServiceManager.ServiceConstructor] - private AddonLifecycle(TargetSigScanner sigScanner) + private AddonLifecycle() { this.onInitializeAddonHook = Hook.FromAddress((nint)AtkUnitBase.StaticVirtualTablePointer->Initialize, this.OnAddonInitialize); this.onInitializeAddonHook.Enable(); From 1d1db04f04f98a353995f836c53a4e3918b683a6 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 6 Dec 2025 16:09:42 +0100 Subject: [PATCH 074/164] Use ImFontPtr in SeStringDrawState --- .../ImGuiSeStringRenderer/SeStringDrawState.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 11c1120b4..3a21e0db9 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -76,7 +76,7 @@ public unsafe ref struct SeStringDrawState this.splitter = default; this.GetEntity = ssdp.GetEntity; this.ScreenOffset = new(MathF.Round(this.ScreenOffset.X), MathF.Round(this.ScreenOffset.Y)); - this.FontSizeScale = this.FontSize / this.Font->FontSize; + this.FontSizeScale = this.FontSize / this.Font.FontSize; this.LineHeight = MathF.Round(ssdp.EffectiveLineHeight); this.LinkUnderlineThickness = ssdp.LinkUnderlineThickness ?? 0f; this.Opacity = ssdp.EffectiveOpacity; @@ -106,7 +106,7 @@ public unsafe ref struct SeStringDrawState public Vector2 ScreenOffset { get; } /// - public ImFont* Font { get; } + public ImFontPtr Font { get; } /// public float FontSize { get; } @@ -256,7 +256,7 @@ public unsafe ref struct SeStringDrawState /// Offset of the glyph in pixels w.r.t. . internal void DrawGlyph(scoped in ImGuiHelpers.ImFontGlyphReal g, Vector2 offset) { - var texId = this.Font->ContainerAtlas->Textures.Ref(g.TextureIndex).TexID; + var texId = this.Font.ContainerAtlas.Textures.Ref(g.TextureIndex).TexID; var xy0 = new Vector2( MathF.Round(g.X0 * this.FontSizeScale), MathF.Round(g.Y0 * this.FontSizeScale)); @@ -313,7 +313,7 @@ public unsafe ref struct SeStringDrawState offset += this.ScreenOffset; offset.Y += (this.LinkUnderlineThickness - 1) / 2f; - offset.Y += MathF.Round(((this.LineHeight - this.FontSize) / 2) + (this.Font->Ascent * this.FontSizeScale)); + offset.Y += MathF.Round(((this.LineHeight - this.FontSize) / 2) + (this.Font.Ascent * this.FontSizeScale)); this.SetCurrentChannel(SeStringDrawChannel.Foreground); this.DrawList.AddLine( @@ -340,9 +340,9 @@ public unsafe ref struct SeStringDrawState internal readonly ref ImGuiHelpers.ImFontGlyphReal FindGlyph(Rune rune) { var p = rune.Value is >= ushort.MinValue and < ushort.MaxValue - ? this.Font->FindGlyph((ushort)rune.Value) - : this.Font->FallbackGlyph; - return ref *(ImGuiHelpers.ImFontGlyphReal*)p; + ? (ImFontGlyphPtr)this.Font.FindGlyph((ushort)rune.Value) + : this.Font.FallbackGlyph; + return ref *(ImGuiHelpers.ImFontGlyphReal*)p.Handle; } /// Gets the glyph corresponding to the given codepoint. @@ -375,7 +375,7 @@ public unsafe ref struct SeStringDrawState return 0; return MathF.Round( - this.Font->GetDistanceAdjustmentForPair( + this.Font.GetDistanceAdjustmentForPair( (ushort)left.Value, (ushort)right.Value) * this.FontSizeScale); } From b2d9480f9f83cb64e476d77f25068817beee6790 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 6 Dec 2025 18:38:13 +0100 Subject: [PATCH 075/164] Submit nuke schema --- .nuke/build.schema.json | 162 ++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 81 deletions(-) diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 03211ce8f..6ffb3bb01 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,19 +1,57 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Build Schema", - "$ref": "#/definitions/build", "definitions": { - "build": { - "type": "object", + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "CI", + "Clean", + "Compile", + "CompileCImGui", + "CompileCImGuizmo", + "CompileCImPlot", + "CompileDalamud", + "CompileDalamudBoot", + "CompileDalamudCrashHandler", + "CompileImGuiNatives", + "CompileInjector", + "Restore", + "SetCILogging", + "Test" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { "properties": { - "Configuration": { - "type": "string", - "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", - "enum": [ - "Debug", - "Release" - ] - }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" @@ -23,29 +61,8 @@ "description": "Shows the help text for this build assembly" }, "Host": { - "type": "string", "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitbucket", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] - }, - "IsDocsBuild": { - "type": "boolean", - "description": "Whether we are building for documentation - emits generated files" + "$ref": "#/definitions/Host" }, "NoLogo": { "type": "boolean", @@ -74,63 +91,46 @@ "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { - "type": "string", - "enum": [ - "CI", - "Clean", - "Compile", - "CompileCImGui", - "CompileCImGuizmo", - "CompileCImPlot", - "CompileDalamud", - "CompileDalamudBoot", - "CompileDalamudCrashHandler", - "CompileImGuiNatives", - "CompileInjector", - "Restore", - "SetCILogging", - "Test" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "CI", - "Clean", - "Compile", - "CompileCImGui", - "CompileCImGuizmo", - "CompileCImPlot", - "CompileDalamud", - "CompileDalamudBoot", - "CompileDalamudCrashHandler", - "CompileImGuiNatives", - "CompileInjector", - "Restore", - "SetCILogging", - "Test" - ] + "$ref": "#/definitions/ExecutableTarget" } }, "Verbosity": { - "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] + "$ref": "#/definitions/Verbosity" } } } - } -} \ No newline at end of file + }, + "allOf": [ + { + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "IsDocsBuild": { + "type": "boolean", + "description": "Whether we are building for documentation - emits generated files" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + } + } + }, + { + "$ref": "#/definitions/NukeBuild" + } + ] +} From 3d29157391da8bc50a60c2aed640510647f257ba Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 6 Dec 2025 18:38:23 +0100 Subject: [PATCH 076/164] Revert "Add git status checks to workflow to see what's dirty" This reverts commit a36e11574b14ea6887cb0f7d2513920ebdd820bf. --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f552e446b..299d71e95 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,12 +33,8 @@ jobs: ($env:REPO_NAME) >> VERSION ($env:BRANCH) >> VERSION ($env:COMMIT) >> VERSION - - name: git status - run: git status - name: Build and Test Dalamud run: .\build.ps1 ci - - name: git status - run: git status - name: Sign Dalamud if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} env: From 9cfa81c92d54cf1d8f8d8c08916ecabf316358eb Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 14:57:28 +0100 Subject: [PATCH 077/164] Remove unused packages --- Dalamud/Dalamud.csproj | 3 --- Directory.Packages.props | 3 --- 2 files changed, 6 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1c16891b7..a13df8cae 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -84,11 +84,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/Directory.Packages.props b/Directory.Packages.props index 58e355400..a94aae7c5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,11 +17,8 @@ - - - From 07f9e03010c53e2bda0422b6fada960072ff1657 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 15:13:35 +0100 Subject: [PATCH 078/164] Update packages --- Directory.Packages.props | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index a94aae7c5..ec2e7e276 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,35 +6,35 @@ - + - + - + - + - + - + - - - - + + + + - - + + @@ -49,15 +49,15 @@ - - + + - - - - - - - + + + + + + + From 9e5723359a2ab36915488edb5612776e7da93c00 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 15:35:38 +0100 Subject: [PATCH 079/164] Remove obsolete casts from Lumina.Text.SeString --- Dalamud/Game/Text/Evaluator/SeStringParameter.cs | 4 ---- Dalamud/Game/Text/SeStringHandling/SeString.cs | 8 -------- 2 files changed, 12 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs index 1c6dd96cb..036d1c921 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringParameter.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringParameter.cs @@ -3,7 +3,6 @@ using System.Globalization; using Lumina.Text.ReadOnly; using DSeString = Dalamud.Game.Text.SeStringHandling.SeString; -using LSeString = Lumina.Text.SeString; namespace Dalamud.Game.Text.Evaluator; @@ -71,9 +70,6 @@ public readonly struct SeStringParameter public static implicit operator SeStringParameter(ReadOnlySeStringSpan value) => new(new ReadOnlySeString(value)); - [Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)] - public static implicit operator SeStringParameter(LSeString value) => new(new ReadOnlySeString(value.RawData)); - public static implicit operator SeStringParameter(DSeString value) => new(new ReadOnlySeString(value.Encode())); public static implicit operator SeStringParameter(string value) => new(value); diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 8805c2177..a1ef5e936 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -113,14 +113,6 @@ public class SeString /// Equivalent SeString. public static implicit operator SeString(string str) => new(new TextPayload(str)); - /// - /// Implicitly convert a string into a SeString containing a . - /// - /// string to convert. - /// Equivalent SeString. - [Obsolete("Switch to using ReadOnlySeString instead of Lumina's SeString.", true)] - public static explicit operator SeString(Lumina.Text.SeString str) => str.ToDalamudString(); - /// /// Parse a binary game message into an SeString. /// From d4fe523d73925944d5151d7d6ee7428034ef4cf0 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 15:38:10 +0100 Subject: [PATCH 080/164] Clean up some warnings --- Dalamud/Interface/Windowing/Window.cs | 2 +- Dalamud/Utility/Util.cs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index b0786fbb5..48352daa2 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -908,7 +908,7 @@ public abstract class Window private void DrawErrorMessage() { // TODO: Once window systems are services, offer to reload the plugin - ImGui.TextColoredWrapped(ImGuiColors.DalamudRed,Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); ImGuiHelpers.ScaledDummy(5); diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index f50efcf0d..bde113904 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -158,16 +158,6 @@ public static partial class Util return branchInternal = gitBranch; } - /// - /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version - /// downloaded from webservices. - /// - /// The name of the track, or null. - internal static string? GetActiveTrack() - { - return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); - } - /// public static unsafe string DescribeAddress(void* p) => DescribeAddress((nint)p); @@ -703,6 +693,16 @@ public static partial class Util } } + /// + /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version + /// downloaded from webservices. + /// + /// The name of the track, or null. + internal static string? GetActiveTrack() + { + return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); + } + /// /// Gets a random, inoffensive, human-friendly string. /// From 8a5f1fd96d7192c76602b90bdbad1e95d8c03f26 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 15:55:43 +0100 Subject: [PATCH 081/164] Add PluginUISoundEffectsEnabled to UiBuilder --- Dalamud/Interface/UiBuilder.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 9f60aba6a..ea0e21e97 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -219,6 +219,12 @@ public interface IUiBuilder /// bool ShouldUseReducedMotion { get; } + /// + /// Gets a value indicating whether the user has enabled the "Enable sound effects for plugin windows" setting.
+ /// This setting is effected by the in-game "System Sounds" option and volume. + ///
+ bool PluginUISoundEffectsEnabled { get; } + /// /// Loads an ULD file that can load textures containing multiple icons in a single texture. /// @@ -560,6 +566,9 @@ public sealed class UiBuilder : IDisposable, IUiBuilder ///
public bool ShouldUseReducedMotion => Service.Get().ReduceMotions ?? false; + /// + public bool PluginUISoundEffectsEnabled => Service.Get().EnablePluginUISoundEffects; + /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// From 7199bfb0a90f886cd0c831c0eeed6303fbde4fcb Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 16:20:55 +0100 Subject: [PATCH 082/164] Remove targets --- targets/Dalamud.Plugin.Bootstrap.targets | 11 -------- targets/Dalamud.Plugin.targets | 35 ------------------------ 2 files changed, 46 deletions(-) delete mode 100644 targets/Dalamud.Plugin.Bootstrap.targets delete mode 100644 targets/Dalamud.Plugin.targets diff --git a/targets/Dalamud.Plugin.Bootstrap.targets b/targets/Dalamud.Plugin.Bootstrap.targets deleted file mode 100644 index db4bf6cd7..000000000 --- a/targets/Dalamud.Plugin.Bootstrap.targets +++ /dev/null @@ -1,11 +0,0 @@ - - - - $(appdata)\XIVLauncher\addon\Hooks\dev\ - $(HOME)/.xlcore/dalamud/Hooks/dev/ - $(HOME)/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/ - $(DALAMUD_HOME)/ - - - - diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets deleted file mode 100644 index 08d19735e..000000000 --- a/targets/Dalamud.Plugin.targets +++ /dev/null @@ -1,35 +0,0 @@ - - - - net8.0-windows - x64 - enable - latest - true - false - false - true - true - $(AssemblySearchPaths);$(DalamudLibPath) - - - - - - - - - - - - - - - - - - - - - - From 2f5f52b572850342ef09ec9fab81f6d564e651fe Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 16:23:13 +0100 Subject: [PATCH 083/164] Forgot to remove this too --- Dalamud/Dalamud.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 1c16891b7..d1b77b1fc 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -227,9 +227,4 @@ - - - - - From c254c8600e19f134ed4d526a7886e2620677f14f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 16:31:03 +0100 Subject: [PATCH 084/164] Update Font Awesome to 7.1.0 --- .../Interface/FontAwesome/FontAwesomeIcon.cs | 1171 ++++++++++------- 1 file changed, 671 insertions(+), 500 deletions(-) diff --git a/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs b/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs index f88d7f8f0..35df9cfbc 100644 --- a/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs +++ b/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs @@ -1,7 +1,7 @@ //------------------------------------------------------------------------------ // // Generated by Dalamud.FASharpGen - don't modify this file directly. -// Font-Awesome Version: 6.4.2 +// Font-Awesome Version: 7.1.0 // //------------------------------------------------------------------------------ @@ -29,14 +29,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "address-book" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "address book", "contact", "directory", "index", "little black book", "rolodex" })] + [FontAwesomeSearchTerms(new[] { "address book", "contact", "directory", "employee", "index", "little black book", "portfolio", "rolodex", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Users + People" })] AddressBook = 0xF2B9, /// /// The Font Awesome "address-card" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "address card", "about", "contact", "id", "identification", "postcard", "profile", "registration" })] + [FontAwesomeSearchTerms(new[] { "address card", "about", "contact", "employee", "id", "identification", "portfolio", "postcard", "profile", "registration", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Alphabet", "Business", "Communication", "Users + People" })] AddressCard = 0xF2BB, @@ -54,6 +54,13 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Automotive" })] AirFreshener = 0xF5D0, + /// + /// The Font Awesome "alarm-clock" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "alarm clock", "alarm", "alarm clock", "clock", "date", "late", "pending", "reminder", "sleep", "snooze", "timer", "timestamp", "watch" })] + [FontAwesomeCategoriesAttribute(new[] { "Alert", "Time", "Travel + Hotel" })] + AlarmClock = 0xF34E, + /// /// The Font Awesome "align-center" icon unicode character. /// @@ -113,28 +120,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "anchor-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anchor circle check", "marina", "not affected", "ok", "okay", "port" })] + [FontAwesomeSearchTerms(new[] { "anchor circle check", "enable", "marina", "not affected", "ok", "okay", "port", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime" })] AnchorCircleCheck = 0xE4AA, /// /// The Font Awesome "anchor-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anchor circle exclamation", "affected", "marina", "port" })] + [FontAwesomeSearchTerms(new[] { "anchor circle exclamation", "affected", "failed", "marina", "port" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime" })] AnchorCircleExclamation = 0xE4AB, /// /// The Font Awesome "anchor-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anchor circle xmark", "destroy", "marina", "port" })] + [FontAwesomeSearchTerms(new[] { "anchor circle xmark", "destroy", "marina", "port", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime" })] AnchorCircleXmark = 0xE4AC, /// /// The Font Awesome "anchor-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anchor lock", "closed", "lockdown", "marina", "port", "quarantine" })] + [FontAwesomeSearchTerms(new[] { "anchor lock", "closed", "lockdown", "marina", "padlock", "port", "privacy", "quarantine" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime" })] AnchorLock = 0xE4AD, @@ -169,7 +176,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "angle-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "angle down", "down arrowhead", "arrow", "caret", "download", "expand" })] + [FontAwesomeSearchTerms(new[] { "angle down", "down arrowhead", "arrow", "caret", "download", "expand", "insert" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] AngleDown = 0xF107, @@ -190,7 +197,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "angle-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "angle up", "up arrowhead", "arrow", "caret", "collapse", "upload" })] + [FontAwesomeSearchTerms(new[] { "angle up", "up arrowhead", "arrow", "caret", "collapse", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] AngleUp = 0xF106, @@ -253,7 +260,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "circle-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle up", "arrow-circle-o-up" })] + [FontAwesomeSearchTerms(new[] { "circle up", "arrow-circle-o-up", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowAltCircleUp = 0xF35B, @@ -281,7 +288,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "circle-arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle arrow up", "upload" })] + [FontAwesomeSearchTerms(new[] { "circle arrow up", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowCircleUp = 0xF0AA, @@ -309,7 +316,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-down-up-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow down up lock", "border", "closed", "crossing", "lockdown", "quarantine", "transfer" })] + [FontAwesomeSearchTerms(new[] { "arrow down up lock", "border", "closed", "crossing", "lockdown", "padlock", "privacy", "quarantine", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowDownUpLock = 0xE4B0, @@ -337,7 +344,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-right-arrow-left" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow right arrow left", "rightwards arrow over leftwards arrow", "arrow", "arrows", "reciprocate", "return", "swap", "transfer" })] + [FontAwesomeSearchTerms(new[] { "arrow right arrow left", "arrow", "arrows", "reciprocate", "return", "swap", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowRightArrowLeft = 0xF0EC, @@ -358,14 +365,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-right-to-bracket" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow right to bracket", "arrow", "enter", "join", "log in", "login", "sign in", "sign up", "sign-in", "signin", "signup" })] + [FontAwesomeSearchTerms(new[] { "arrow right to bracket", "arrow", "enter", "insert", "join", "log in", "login", "sign in", "sign up", "sign-in", "signin", "signup" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowRightToBracket = 0xF090, /// /// The Font Awesome "arrow-right-to-city" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow right to city", "building", "city", "exodus", "rural", "urban" })] + [FontAwesomeSearchTerms(new[] { "arrow right to city", "building", "city", "exodus", "insert", "rural", "urban" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] ArrowRightToCity = 0xE4B3, @@ -393,14 +400,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-down-to-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows down to line", "scale down", "sink" })] + [FontAwesomeSearchTerms(new[] { "arrows down to line", "insert", "scale down", "sink" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowsDownToLine = 0xE4B8, /// /// The Font Awesome "arrows-down-to-people" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows down to people", "affected", "focus", "targeted" })] + [FontAwesomeSearchTerms(new[] { "arrows down to people", "affected", "focus", "insert", "targeted", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] ArrowsDownToPeople = 0xE4B9, @@ -435,14 +442,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-to-circle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows to circle", "center", "concentrate", "coordinate", "coordination", "focal point", "focus" })] + [FontAwesomeSearchTerms(new[] { "arrows to circle", "center", "concentrate", "coordinate", "coordination", "focal point", "focus", "insert" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowsToCircle = 0xE4BD, /// /// The Font Awesome "arrows-to-dot" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows to dot", "assembly point", "center", "condense", "focus", "minimize" })] + [FontAwesomeSearchTerms(new[] { "arrows to dot", "assembly point", "center", "condense", "focus", "insert", "minimize" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Business", "Humanitarian", "Marketing" })] ArrowsToDot = 0xE4BE, @@ -463,7 +470,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-turn-to-dots" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows turn to dots", "destination", "nexus" })] + [FontAwesomeSearchTerms(new[] { "arrows turn to dots", "destination", "insert", "nexus" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowsTurnToDots = 0xE4C1, @@ -484,7 +491,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-up-to-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows up to line", "rise", "scale up" })] + [FontAwesomeSearchTerms(new[] { "arrows up to line", "rise", "scale up", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowsUpToLine = 0xE4C2, @@ -519,28 +526,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up", "upwards arrow", "forward", "upload" })] + [FontAwesomeSearchTerms(new[] { "arrow up", "upwards arrow", "forward", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowUp = 0xF062, /// /// The Font Awesome "arrow-up-from-bracket" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up from bracket", "share", "transfer", "upload" })] + [FontAwesomeSearchTerms(new[] { "arrow up from bracket", "share", "transfer", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ArrowUpFromBracket = 0xE09A, /// /// The Font Awesome "arrow-up-from-ground-water" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up from ground water", "groundwater", "spring", "water supply", "water table" })] + [FontAwesomeSearchTerms(new[] { "arrow up from ground water", "groundwater", "spring", "upgrade", "water supply", "water table" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Energy", "Humanitarian" })] ArrowUpFromGroundWater = 0xE4B5, /// /// The Font Awesome "arrow-up-from-water-pump" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up from water pump", "flood", "groundwater", "pump", "submersible", "sump pump" })] + [FontAwesomeSearchTerms(new[] { "arrow up from water pump", "flood", "groundwater", "pump", "submersible", "sump pump", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Humanitarian" })] ArrowUpFromWaterPump = 0xE4B6, @@ -554,14 +561,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-up-right-dots" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up right dots", "growth", "increase", "population" })] + [FontAwesomeSearchTerms(new[] { "arrow up right dots", "growth", "increase", "population", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowUpRightDots = 0xE4B7, /// /// The Font Awesome "arrow-up-right-from-square" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up right from square", "new", "open", "send", "share" })] + [FontAwesomeSearchTerms(new[] { "arrow up right from square", "new", "open", "send", "share", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowUpRightFromSquare = 0xF08E, @@ -576,7 +583,7 @@ public enum FontAwesomeIcon /// The Font Awesome "asterisk" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x2A. ///
- [FontAwesomeSearchTerms(new[] { "asterisk", "asterisk", "heavy asterisk", "annotation", "details", "reference", "star" })] + [FontAwesomeSearchTerms(new[] { "asterisk", "asterisk", "heavy asterisk", "annotation", "details", "reference", "required", "star" })] [FontAwesomeCategoriesAttribute(new[] { "Punctuation + Symbols", "Spinners" })] Asterisk = 0xF069, @@ -591,14 +598,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "book-atlas" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "book atlas", "book", "directions", "geography", "globe", "library", "map", "research", "travel", "wayfinding" })] + [FontAwesomeSearchTerms(new[] { "book atlas", "book", "directions", "geography", "globe", "knowledge", "library", "map", "research", "travel", "wayfinding" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Travel + Hotel" })] Atlas = 0xF558, /// /// The Font Awesome "atom" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "atheism", "atheist", "atom", "atom symbol", "chemistry", "electron", "ion", "isotope", "neutron", "nuclear", "proton", "science" })] + [FontAwesomeSearchTerms(new[] { "atheism", "atheist", "atom", "atom symbol", "chemistry", "electron", "ion", "isotope", "knowledge", "neutron", "nuclear", "proton", "science" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Energy", "Religion", "Science", "Science Fiction", "Spinners" })] Atom = 0xF5D2, @@ -619,14 +626,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "award" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "award", "honor", "praise", "prize", "recognition", "ribbon", "trophy" })] + [FontAwesomeSearchTerms(new[] { "award", "guarantee", "honor", "praise", "prize", "recognition", "ribbon", "trophy", "warranty" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Political" })] Award = 0xF559, /// /// The Font Awesome "baby" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "baby", "users-people" })] + [FontAwesomeSearchTerms(new[] { "baby", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Humanitarian", "Users + People" })] Baby = 0xF77C, @@ -668,7 +675,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bacterium" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bacterium", "antibiotic", "antibody", "covid-19", "health", "organism", "sick" })] + [FontAwesomeSearchTerms(new[] { "bacterium", "antibiotic", "antibody", "covid-19", "germ", "health", "organism", "sick" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] Bacterium = 0xE05A, @@ -710,14 +717,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "ban" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "abort", "ban", "block", "cancel", "delete", "entry", "forbidden", "hide", "no", "not", "prohibit", "prohibited", "remove", "stop", "trash" })] + [FontAwesomeSearchTerms(new[] { "404", "abort", "ban", "block", "cancel", "circle", "delete", "deny", "disabled", "entry", "failed", "forbidden", "hide", "no", "not", "not found", "prohibit", "prohibited", "remove", "slash", "stop", "trash" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security" })] Ban = 0xF05E, /// /// The Font Awesome "bandage" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "adhesive bandage", "bandage", "boo boo", "first aid", "ouch" })] + [FontAwesomeSearchTerms(new[] { "adhesive bandage", "bandage", "boo boo", "first aid", "modify", "ouch" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Medical + Health" })] BandAid = 0xF462, @@ -815,7 +822,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bed" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bed", "hospital", "hotel", "lodging", "mattress", "patient", "person in bed", "rest", "sleep", "travel" })] + [FontAwesomeSearchTerms(new[] { "bed", "hospital", "hotel", "lodging", "mattress", "patient", "person in bed", "rest", "sleep", "travel", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Humanitarian", "Maps", "Travel + Hotel", "Users + People" })] Bed = 0xF236, @@ -829,7 +836,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bell" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "alarm", "alert", "bel", "bell", "chime", "notification", "reminder" })] + [FontAwesomeSearchTerms(new[] { "alarm", "alert", "bel", "bell", "chime", "notification", "reminder", "request" })] [FontAwesomeCategoriesAttribute(new[] { "Alert", "Education", "Household", "Maps", "Shopping", "Social", "Time" })] Bell = 0xF0F3, @@ -864,14 +871,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-biking" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person biking", "bicycle", "bike", "biking", "cyclist", "pedal", "person biking", "summer", "wheel" })] + [FontAwesomeSearchTerms(new[] { "person biking", "bicycle", "bike", "biking", "cyclist", "pedal", "person biking", "summer", "uer", "wheel" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Sports + Fitness", "Users + People" })] Biking = 0xF84A, /// /// The Font Awesome "binoculars" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "binoculars", "glasses", "magnify", "scenic", "spyglass", "view" })] + [FontAwesomeSearchTerms(new[] { "binoculars", "glasses", "inspection", "magnifier", "magnify", "scenic", "spyglass", "view" })] [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Camping", "Maps", "Nature" })] Binoculars = 0xF1E5, @@ -913,7 +920,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-walking-with-cane" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking with cane", "blind", "cane" })] + [FontAwesomeSearchTerms(new[] { "person walking with cane", "blind", "cane", "follow", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Maps", "Users + People" })] Blind = 0xF29D, @@ -969,14 +976,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "book" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "book", "cover", "decorated", "diary", "documentation", "journal", "library", "notebook", "notebook with decorative cover", "read", "research" })] + [FontAwesomeSearchTerms(new[] { "book", "cover", "decorated", "diary", "documentation", "journal", "knowledge", "library", "notebook", "notebook with decorative cover", "read", "research", "scholar" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Maps", "Writing" })] Book = 0xF02D, /// /// The Font Awesome "book-bookmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "book bookmark", "library", "research" })] + [FontAwesomeSearchTerms(new[] { "book bookmark", "knowledge", "library", "research" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Writing" })] BookBookmark = 0xE0BB, @@ -1004,7 +1011,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "book-open" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "book open", "book", "book", "flyer", "library", "notebook", "open", "open book", "pamphlet", "reading", "research" })] + [FontAwesomeSearchTerms(new[] { "book open", "book", "book", "flyer", "knowledge", "library", "notebook", "open", "open book", "pamphlet", "reading", "research" })] [FontAwesomeCategoriesAttribute(new[] { "Education" })] BookOpen = 0xF518, @@ -1130,7 +1137,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "brain" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "brain", "cerebellum", "gray matter", "intellect", "intelligent", "medulla oblongata", "mind", "noodle", "wit" })] + [FontAwesomeSearchTerms(new[] { "brain", "cerebellum", "gray matter", "intellect", "intelligent", "knowledge", "medulla oblongata", "mind", "noodle", "scholar", "wit" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Science" })] Brain = 0xF5DC, @@ -1158,28 +1165,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bridge-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bridge circle check", "bridge", "not affected", "ok", "okay", "road" })] + [FontAwesomeSearchTerms(new[] { "bridge circle check", "bridge", "enable", "not affected", "ok", "okay", "road", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] BridgeCircleCheck = 0xE4C9, /// /// The Font Awesome "bridge-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bridge circle exclamation", "affected", "bridge", "road" })] + [FontAwesomeSearchTerms(new[] { "bridge circle exclamation", "affected", "bridge", "failed", "road" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] BridgeCircleExclamation = 0xE4CA, /// /// The Font Awesome "bridge-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bridge circle xmark", "bridge", "destroy", "road" })] + [FontAwesomeSearchTerms(new[] { "bridge circle xmark", "bridge", "destroy", "road", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] BridgeCircleXmark = 0xE4CB, /// /// The Font Awesome "bridge-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bridge lock", "bridge", "closed", "lockdown", "quarantine", "road" })] + [FontAwesomeSearchTerms(new[] { "bridge lock", "bridge", "closed", "lockdown", "padlock", "privacy", "quarantine", "road" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] BridgeLock = 0xE4CC, @@ -1193,7 +1200,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "briefcase" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bag", "briefcas", "briefcase", "business", "luggage", "office", "work" })] + [FontAwesomeSearchTerms(new[] { "bag", "briefcas", "briefcase", "business", "luggage", "offer", "office", "portfolio", "work" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Maps", "Travel + Hotel" })] Briefcase = 0xF0B1, @@ -1207,7 +1214,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "tower-broadcast" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tower broadcast", "airwaves", "antenna", "communication", "emergency", "radio", "reception", "waves" })] + [FontAwesomeSearchTerms(new[] { "tower broadcast", "airwaves", "antenna", "communication", "emergency", "radio", "reception", "signal", "waves" })] [FontAwesomeCategoriesAttribute(new[] { "Connectivity", "Energy", "Film + Video", "Humanitarian" })] BroadcastTower = 0xF519, @@ -1221,7 +1228,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "brush" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "brush", "art", "bristles", "color", "handle", "paint" })] + [FontAwesomeSearchTerms(new[] { "brush", "art", "bristles", "color", "handle", "maintenance", "modify", "paint" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Design", "Editing" })] Brush = 0xF55D, @@ -1249,7 +1256,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bug-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bug slash", "beetle", "fix", "glitch", "insect", "optimize", "repair", "report", "warning" })] + [FontAwesomeSearchTerms(new[] { "bug slash", "beetle", "disabled", "fix", "glitch", "insect", "optimize", "repair", "report", "warning" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Security" })] BugSlash = 0xE490, @@ -1270,42 +1277,42 @@ public enum FontAwesomeIcon /// /// The Font Awesome "building-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building circle check", "building", "city", "not affected", "office", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "building circle check", "building", "city", "enable", "not affected", "office", "ok", "okay", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingCircleCheck = 0xE4D2, /// /// The Font Awesome "building-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building circle exclamation", "affected", "building", "city", "office" })] + [FontAwesomeSearchTerms(new[] { "building circle exclamation", "affected", "building", "city", "failed", "office" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingCircleExclamation = 0xE4D3, /// /// The Font Awesome "building-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building circle xmark", "building", "city", "destroy", "office" })] + [FontAwesomeSearchTerms(new[] { "building circle xmark", "building", "city", "destroy", "office", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingCircleXmark = 0xE4D4, /// /// The Font Awesome "building-flag" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building flag", " city", "building", "diplomat", "embassy", "flag", "headquarters", "united nations" })] + [FontAwesomeSearchTerms(new[] { "building flag", "building", "city", "diplomat", "embassy", "flag", "headquarters", "united nations" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Political" })] BuildingFlag = 0xE4D5, /// /// The Font Awesome "building-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building lock", "building", "city", "closed", "lock", "lockdown", "quarantine", "secure" })] + [FontAwesomeSearchTerms(new[] { "building lock", "building", "city", "closed", "lock", "lockdown", "padlock", "privacy", "quarantine", "secure" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Security" })] BuildingLock = 0xE4D6, /// /// The Font Awesome "building-ngo" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building ngo", " city", "building", "non governmental organization", "office" })] + [FontAwesomeSearchTerms(new[] { "building ngo", "building", "city", "non governmental organization", "office" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingNgo = 0xE4D7, @@ -1326,7 +1333,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "building-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "building user", "apartment", "building", "city" })] + [FontAwesomeSearchTerms(new[] { "building user", "apartment", "building", "city", "employee", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] BuildingUser = 0xE4DA, @@ -1340,7 +1347,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bullhorn" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bullhorn", "bullhorn", "announcement", "broadcast", "loud", "louder", "loudspeaker", "megaphone", "public address", "share" })] + [FontAwesomeSearchTerms(new[] { "bullhorn", "bullhorn", "announcement", "broadcast", "loud", "louder", "loudspeaker", "megaphone", "public address", "request", "share" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Marketing", "Political", "Shopping" })] Bullhorn = 0xF0A1, @@ -1382,10 +1389,17 @@ public enum FontAwesomeIcon /// /// The Font Awesome "business-time" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "business time", "alarm", "briefcase", "business socks", "clock", "flight of the conchords", "reminder", "wednesday" })] + [FontAwesomeSearchTerms(new[] { "business time", "alarm", "briefcase", "business socks", "clock", "flight of the conchords", "portfolio", "reminder", "wednesday" })] [FontAwesomeCategoriesAttribute(new[] { "Business" })] BusinessTime = 0xF64A, + /// + /// The Font Awesome "bus-side" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "bus side", "bus", "public transportation", "transportation", "travel", "vehicle" })] + [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Humanitarian", "Logistics", "Transportation", "Travel + Hotel" })] + BusSide = 0xE81D, + /// /// The Font Awesome "calculator" icon unicode character. /// @@ -1410,7 +1424,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "calendar-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "calendar check", "accept", "agree", "appointment", "confirm", "correct", "date", "day", "done", "event", "month", "ok", "schedule", "select", "success", "tick", "time", "todo", "when", "year" })] + [FontAwesomeSearchTerms(new[] { "calendar check", "accept", "agree", "appointment", "confirm", "correct", "date", "day", "done", "enable", "event", "month", "ok", "schedule", "select", "success", "tick", "time", "todo", "validate", "warranty", "when", "working", "year" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] CalendarCheck = 0xF274, @@ -1438,7 +1452,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "calendar-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "calendar xmark", "archive", "calendar", "date", "day", "delete", "event", "month", "remove", "schedule", "time", "when", "x", "year" })] + [FontAwesomeSearchTerms(new[] { "calendar xmark", "archive", "calendar", "date", "day", "delete", "event", "month", "remove", "schedule", "time", "uncheck", "when", "x", "year" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] CalendarTimes = 0xF273, @@ -1452,21 +1466,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "camera" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "camera", "image", "lens", "photo", "picture", "record", "shutter", "video" })] + [FontAwesomeSearchTerms(new[] { "camera", "image", "img", "lens", "photo", "picture", "record", "shutter", "video" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware", "Photos + Images", "Shopping", "Social" })] Camera = 0xF030, /// /// The Font Awesome "camera-retro" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "camera retro", "camera", "image", "lens", "photo", "picture", "record", "shutter", "video" })] + [FontAwesomeSearchTerms(new[] { "camera retro", "camera", "image", "img", "lens", "photo", "picture", "record", "shutter", "video" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware", "Photos + Images", "Shopping" })] CameraRetro = 0xF083, /// /// The Font Awesome "camera-rotate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "camera rotate", "flip", "front-facing", "photo", "selfie" })] + [FontAwesomeSearchTerms(new[] { "camera rotate", "flip", "front-facing", "img", "photo", "selfie" })] [FontAwesomeCategoriesAttribute(new[] { "Photos + Images" })] CameraRotate = 0xE0D8, @@ -1557,7 +1571,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-caret-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square caret down", "arrow", "caret-square-o-down", "dropdown", "expand", "menu", "more", "triangle" })] + [FontAwesomeSearchTerms(new[] { "square caret down", "arrow", "caret-square-o-down", "dropdown", "expand", "insert", "menu", "more", "triangle" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] CaretSquareDown = 0xF150, @@ -1578,14 +1592,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-caret-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square caret up", "arrow", "caret-square-o-up", "collapse", "triangle", "upload" })] + [FontAwesomeSearchTerms(new[] { "square caret up", "arrow", "caret-square-o-up", "collapse", "triangle", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] CaretSquareUp = 0xF151, /// /// The Font Awesome "caret-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "caret up", "arrow", "collapse", "triangle" })] + [FontAwesomeSearchTerms(new[] { "caret up", "arrow", "collapse", "triangle", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] CaretUp = 0xF0D8, @@ -1613,7 +1627,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "cart-arrow-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "cart arrow down", "download", "save", "shopping" })] + [FontAwesomeSearchTerms(new[] { "cart arrow down", "download", "insert", "save", "shopping" })] [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] CartArrowDown = 0xF218, @@ -1662,7 +1676,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "certificate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "certificate", "badge", "star", "verified" })] + [FontAwesomeSearchTerms(new[] { "certificate", "badge", "guarantee", "star", "verified" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Shapes", "Shopping", "Spinners" })] Certificate = 0xF0A3, @@ -1683,91 +1697,98 @@ public enum FontAwesomeIcon /// /// The Font Awesome "chalkboard-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chalkboard user", "blackboard", "instructor", "learning", "professor", "school", "whiteboard", "writing" })] + [FontAwesomeSearchTerms(new[] { "chalkboard user", "blackboard", "instructor", "learning", "professor", "school", "uer", "whiteboard", "writing" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Users + People" })] ChalkboardTeacher = 0xF51C, /// /// The Font Awesome "charging-station" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "charging station", "electric", "ev", "tesla", "vehicle" })] + [FontAwesomeSearchTerms(new[] { "charging station", "car charger", "charge", "charging", "electric", "ev", "tesla", "vehicle" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Energy" })] ChargingStation = 0xF5E7, /// /// The Font Awesome "chart-area" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart area", "analytics", "area", "chart", "graph" })] + [FontAwesomeSearchTerms(new[] { "chart area", "analytics", "area", "chart", "graph", "performance", "revenue", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams" })] ChartArea = 0xF1FE, /// /// The Font Awesome "chart-bar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart bar", "analytics", "bar", "chart", "graph" })] + [FontAwesomeSearchTerms(new[] { "chart bar", "analytics", "bar", "chart", "graph", "performance", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams" })] ChartBar = 0xF080, /// /// The Font Awesome "chart-column" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart column", "bar", "bar chart", "chart", "graph", "track", "trend" })] + [FontAwesomeSearchTerms(new[] { "chart column", "bar", "bar chart", "chart", "graph", "performance", "revenue", "statistics", "track", "trend" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams" })] ChartColumn = 0xE0E3, + /// + /// The Font Awesome "chart-diagram" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "chart diagram", "algorithm", "analytics", "flow", "graph" })] + [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Coding" })] + ChartDiagram = 0xE695, + /// /// The Font Awesome "chart-gantt" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart gantt", "chart", "graph", "track", "trend" })] + [FontAwesomeSearchTerms(new[] { "chart gantt", "chart", "graph", "performance", "statistics", "track", "trend" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams" })] ChartGantt = 0xE0E4, /// /// The Font Awesome "chart-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart line", "activity", "analytics", "chart", "dashboard", "gain", "graph", "increase", "line" })] + [FontAwesomeSearchTerms(new[] { "chart line", "activity", "analytics", "chart", "dashboard", "gain", "graph", "increase", "line", "performance", "revenue", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Money" })] ChartLine = 0xF201, /// /// The Font Awesome "chart-pie" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart pie", "analytics", "chart", "diagram", "graph", "pie" })] + [FontAwesomeSearchTerms(new[] { "chart pie", "analytics", "chart", "diagram", "graph", "performance", "pie", "revenue", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Money" })] ChartPie = 0xF200, /// /// The Font Awesome "chart-simple" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chart simple", "analytics", "bar", "chart", "column", "graph", "row", "trend" })] + [FontAwesomeSearchTerms(new[] { "chart simple", "analytics", "bar", "chart", "column", "graph", "performance", "revenue", "row", "statistics", "trend" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Editing", "Logistics", "Marketing" })] ChartSimple = 0xE473, /// /// The Font Awesome "check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "check mark", "accept", "agree", "check", "check mark", "checkmark", "confirm", "correct", "done", "mark", "notice", "notification", "notify", "ok", "select", "success", "tick", "todo", "yes", "✓" })] + [FontAwesomeSearchTerms(new[] { "check mark", "accept", "agree", "check", "check mark", "checkmark", "confirm", "correct", "coupon", "done", "enable", "mark", "notice", "notification", "notify", "ok", "select", "success", "tick", "todo", "true", "validate", "working", "yes", "✓" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Punctuation + Symbols", "Text Formatting" })] Check = 0xF00C, /// /// The Font Awesome "circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle check", "accept", "affected", "agree", "clear", "confirm", "correct", "done", "ok", "select", "success", "tick", "todo", "yes" })] + [FontAwesomeSearchTerms(new[] { "circle check", "accept", "affected", "agree", "clear", "confirm", "correct", "coupon", "done", "enable", "ok", "select", "success", "tick", "todo", "validate", "working", "yes" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Text Formatting", "Toggle" })] CheckCircle = 0xF058, /// /// The Font Awesome "check-double" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "check double", "accept", "agree", "checkmark", "confirm", "correct", "done", "notice", "notification", "notify", "ok", "select", "success", "tick", "todo" })] + [FontAwesomeSearchTerms(new[] { "check double", "accept", "agree", "checkmark", "confirm", "correct", "coupon", "done", "enable", "notice", "notification", "notify", "ok", "select", "select all", "success", "tick", "todo", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Political", "Punctuation + Symbols", "Text Formatting" })] CheckDouble = 0xF560, /// /// The Font Awesome "square-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square check", "accept", "agree", "box", "button", "check", "check box with check", "check mark button", "checkmark", "confirm", "correct", "done", "mark", "ok", "select", "success", "tick", "todo", "yes", "✓" })] + [FontAwesomeSearchTerms(new[] { "square check", "accept", "agree", "box", "button", "check", "check box with check", "check mark button", "checkmark", "confirm", "correct", "coupon", "done", "enable", "mark", "ok", "select", "success", "tick", "todo", "validate", "working", "yes", "✓" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Text Formatting" })] CheckSquare = 0xF14A, @@ -1858,14 +1879,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "circle-chevron-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle chevron up", "arrow", "collapse", "upload" })] + [FontAwesomeSearchTerms(new[] { "circle chevron up", "arrow", "collapse", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ChevronCircleUp = 0xF139, /// /// The Font Awesome "chevron-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chevron down", "arrow", "download", "expand" })] + [FontAwesomeSearchTerms(new[] { "chevron down", "arrow", "download", "expand", "insert" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ChevronDown = 0xF078, @@ -1886,14 +1907,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "chevron-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "chevron up", "arrow", "collapse", "upload" })] + [FontAwesomeSearchTerms(new[] { "chevron up", "arrow", "collapse", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ChevronUp = 0xF077, /// /// The Font Awesome "child" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "child", "boy", "girl", "kid", "toddler", "young", "youth" })] + [FontAwesomeSearchTerms(new[] { "child", "boy", "girl", "kid", "toddler", "uer", "young", "youth" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Users + People" })] Child = 0xF1AE, @@ -1907,21 +1928,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "child-dress" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "child dress", "boy", "girl", "kid", "toddler", "young", "youth" })] + [FontAwesomeSearchTerms(new[] { "child dress", "boy", "girl", "kid", "toddler", "uer", "young", "youth" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Users + People" })] ChildDress = 0xE59C, /// /// The Font Awesome "child-reaching" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "child reaching", "boy", "girl", "kid", "toddler", "young", "youth" })] + [FontAwesomeSearchTerms(new[] { "child reaching", "boy", "girl", "kid", "toddler", "uer", "young", "youth" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Users + People" })] ChildReaching = 0xE59D, /// /// The Font Awesome "children" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "children", "boy", "child", "girl", "kid", "kids", "young", "youth" })] + [FontAwesomeSearchTerms(new[] { "children", "boy", "child", "girl", "kid", "kids", "together", "uer", "young", "youth" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Humanitarian", "Users + People" })] Children = 0xE4E1, @@ -1977,49 +1998,49 @@ public enum FontAwesomeIcon /// /// The Font Awesome "clipboard" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboar", "clipboard", "copy", "notes", "paste", "record" })] + [FontAwesomeSearchTerms(new[] { "clipboard", "copy", "notepad", "notes", "paste", "record" })] [FontAwesomeCategoriesAttribute(new[] { "Business" })] Clipboard = 0xF328, /// /// The Font Awesome "clipboard-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboard check", "accept", "agree", "confirm", "done", "ok", "select", "success", "tick", "todo", "yes" })] - [FontAwesomeCategoriesAttribute(new[] { "Logistics", "Science" })] + [FontAwesomeSearchTerms(new[] { "clipboard check", "accept", "agree", "confirm", "coupon", "done", "enable", "ok", "select", "success", "tick", "todo", "validate", "working", "yes" })] + [FontAwesomeCategoriesAttribute(new[] { "Business", "Logistics", "Science" })] ClipboardCheck = 0xF46C, /// /// The Font Awesome "clipboard-list" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboard list", "checklist", "completed", "done", "finished", "intinerary", "ol", "schedule", "tick", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "clipboard list", "cheatsheet", "checklist", "completed", "done", "finished", "intinerary", "ol", "schedule", "summary", "survey", "tick", "todo", "ul", "wishlist" })] [FontAwesomeCategoriesAttribute(new[] { "Logistics" })] ClipboardList = 0xF46D, /// /// The Font Awesome "clipboard-question" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboard question", "assistance", "interview", "query", "question" })] + [FontAwesomeSearchTerms(new[] { "clipboard question", "assistance", "faq", "interview", "query", "question" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Humanitarian", "Logistics" })] ClipboardQuestion = 0xE4E3, /// /// The Font Awesome "clipboard-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clipboard user", "attendance", "record", "roster", "staff" })] - [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Users + People" })] + [FontAwesomeSearchTerms(new[] { "clipboard user", "attendance", "employee", "record", "roster", "staff", "uer" })] + [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Medical + Health", "Users + People" })] ClipboardUser = 0xF7F3, /// /// The Font Awesome "clock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "00", "4", "4:00", "clock", "date", "four", "four o’clock", "hour", "late", "minute", "o'clock", "o’clock", "schedule", "ticking", "time", "timer", "timestamp", "watch" })] + [FontAwesomeSearchTerms(new[] { "00", "4", "4:00", "clock", "date", "four", "four o’clock", "hour", "late", "minute", "o'clock", "o’clock", "pending", "schedule", "ticking", "time", "timer", "timestamp", "watch" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] Clock = 0xF017, /// /// The Font Awesome "clone" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clone", "arrange", "copy", "duplicate", "paste" })] + [FontAwesomeSearchTerms(new[] { "clone", "add", "arrange", "copy", "duplicate", "new", "paste" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Files", "Photos + Images" })] Clone = 0xF24D, @@ -2112,7 +2133,7 @@ public enum FontAwesomeIcon /// The Font Awesome "cloud-arrow-up" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF0EE. ///
- [FontAwesomeSearchTerms(new[] { "cloud arrow up", "import", "save", "upload" })] + [FontAwesomeSearchTerms(new[] { "cloud arrow up", "import", "save", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Connectivity" })] CloudUploadAlt = 0xF382, @@ -2133,14 +2154,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "code" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "brackets", "code", "development", "html" })] + [FontAwesomeSearchTerms(new[] { "brackets", "code", "development", "html", "mysql", "sql" })] [FontAwesomeCategoriesAttribute(new[] { "Coding" })] Code = 0xF121, /// /// The Font Awesome "code-branch" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "code branch", "branch", "git", "github", "rebase", "svn", "vcs", "version" })] + [FontAwesomeSearchTerms(new[] { "code branch", "branch", "git", "github", "mysql", "rebase", "sql", "svn", "vcs", "version" })] [FontAwesomeCategoriesAttribute(new[] { "Coding" })] CodeBranch = 0xF126, @@ -2189,21 +2210,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "gear" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "cog", "cogwheel", "gear", "mechanical", "settings", "sprocket", "tool", "wheel" })] + [FontAwesomeSearchTerms(new[] { "cog", "cogwheel", "configuration", "gear", "mechanical", "modify", "settings", "sprocket", "tool", "wheel" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Editing", "Spinners" })] Cog = 0xF013, /// /// The Font Awesome "gears" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "gears", "mechanical", "settings", "sprocket", "wheel" })] + [FontAwesomeSearchTerms(new[] { "configuration", "gears", "mechanical", "modify", "settings", "sprocket", "wheel" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Logistics" })] Cogs = 0xF085, /// /// The Font Awesome "coins" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "coins", "currency", "dime", "financial", "gold", "money", "penny" })] + [FontAwesomeSearchTerms(new[] { "coins", "currency", "dime", "financial", "gold", "money", "penny", "premium" })] [FontAwesomeCategoriesAttribute(new[] { "Money" })] Coins = 0xF51E, @@ -2217,63 +2238,70 @@ public enum FontAwesomeIcon /// /// The Font Awesome "table-columns" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table columns", "browser", "dashboard", "organize", "panes", "split" })] + [FontAwesomeSearchTerms(new[] { "table columns", "browser", "category", "dashboard", "organize", "panes", "split" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Text Formatting" })] Columns = 0xF0DB, /// /// The Font Awesome "comment" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment", "right speech bubble", "bubble", "chat", "commenting", "conversation", "feedback", "message", "note", "notification", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment", "right speech bubble", "answer", "bubble", "chat", "commenting", "conversation", "conversation", "discussion", "feedback", "message", "note", "notification", "sms", "speech", "talk", "talking", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Shapes", "Social" })] Comment = 0xF075, /// /// The Font Awesome "message" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bubble", "chat", "commenting", "conversation", "feedback", "message", "note", "notification", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "answer", "bubble", "chat", "commenting", "conversation", "conversation", "discussion", "feedback", "message", "note", "notification", "sms", "speech", "talk", "talking", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Social" })] CommentAlt = 0xF27A, /// /// The Font Awesome "comment-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment dollar", "bubble", "chat", "commenting", "conversation", "feedback", "message", "money", "note", "notification", "pay", "sms", "speech", "spend", "texting", "transfer" })] + [FontAwesomeSearchTerms(new[] { "comment dollar", "answer", "bubble", "chat", "commenting", "conversation", "feedback", "message", "money", "note", "notification", "pay", "salary", "sms", "speech", "spend", "texting", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing", "Money" })] CommentDollar = 0xF651, /// /// The Font Awesome "comment-dots" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment dots", "balloon", "bubble", "chat", "comic", "commenting", "conversation", "dialog", "feedback", "message", "more", "note", "notification", "reply", "sms", "speech", "speech balloon", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment dots", "answer", "balloon", "bubble", "chat", "comic", "commenting", "conversation", "dialog", "feedback", "message", "more", "note", "notification", "reply", "request", "sms", "speech", "speech balloon", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication" })] CommentDots = 0xF4AD, /// /// The Font Awesome "comment-medical" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment medical", "advice", "bubble", "chat", "commenting", "conversation", "diagnose", "feedback", "message", "note", "notification", "prescription", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment medical", "advice", "answer", "bubble", "chat", "commenting", "conversation", "diagnose", "feedback", "message", "note", "notification", "prescription", "sms", "speech", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Medical + Health" })] CommentMedical = 0xF7F5, + /// + /// The Font Awesome "comment-nodes" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "comment nodes", "ai", "artificial intelligence", "cluster", "language", "model", "network", "neuronal" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Communication" })] + CommentNodes = 0xE696, + /// /// The Font Awesome "comments" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comments", "two speech bubbles", "bubble", "chat", "commenting", "conversation", "feedback", "message", "note", "notification", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "comments", "two speech bubbles", "answer", "bubble", "chat", "commenting", "conversation", "conversation", "discussion", "feedback", "message", "note", "notification", "sms", "speech", "talk", "talking", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication" })] Comments = 0xF086, /// /// The Font Awesome "comments-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comments dollar", "bubble", "chat", "commenting", "conversation", "feedback", "message", "money", "note", "notification", "pay", "sms", "speech", "spend", "texting", "transfer" })] + [FontAwesomeSearchTerms(new[] { "comments dollar", "answer", "bubble", "chat", "commenting", "conversation", "feedback", "message", "money", "note", "notification", "pay", "salary", "sms", "speech", "spend", "texting", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing", "Money" })] CommentsDollar = 0xF653, /// /// The Font Awesome "comment-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment slash", "bubble", "cancel", "chat", "commenting", "conversation", "feedback", "message", "mute", "note", "notification", "quiet", "sms", "speech", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment slash", "answer", "bubble", "cancel", "chat", "commenting", "conversation", "disabled", "feedback", "message", "mute", "note", "notification", "quiet", "sms", "speech", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication" })] CommentSlash = 0xF4B3, @@ -2301,7 +2329,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "down-left-and-up-right-to-center" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "down left and up right to center", "collapse", "fullscreen", "minimize", "move", "resize", "shrink", "smaller" })] + [FontAwesomeSearchTerms(new[] { "down left and up right to center", "collapse", "fullscreen", "minimize", "move", "resize", "scale", "shrink", "size", "smaller" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] CompressAlt = 0xF422, @@ -2322,7 +2350,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bell-concierge" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bell concierge", "attention", "bell", "bellhop", "bellhop bell", "hotel", "receptionist", "service", "support" })] + [FontAwesomeSearchTerms(new[] { "bell concierge", "attention", "bell", "bellhop", "bellhop bell", "hotel", "receptionist", "request", "service", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel" })] ConciergeBell = 0xF562, @@ -2378,14 +2406,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "crop" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "crop", "design", "frame", "mask", "resize", "shrink" })] + [FontAwesomeSearchTerms(new[] { "crop", "design", "frame", "mask", "modify", "resize", "shrink" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Editing" })] Crop = 0xF125, /// /// The Font Awesome "crop-simple" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "crop simple", "design", "frame", "mask", "resize", "shrink" })] + [FontAwesomeSearchTerms(new[] { "crop simple", "design", "frame", "mask", "modify", "resize", "shrink" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Editing" })] CropAlt = 0xF565, @@ -2413,7 +2441,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "crown" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "award", "clothing", "crown", "favorite", "king", "queen", "royal", "tiara" })] + [FontAwesomeSearchTerms(new[] { "award", "clothing", "crown", "favorite", "king", "queen", "royal", "tiara", "vip" })] [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] Crown = 0xF521, @@ -2455,14 +2483,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "scissors" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "black safety scissors", "white scissors", "clip", "cutting", "scissors", "snip", "tool" })] + [FontAwesomeSearchTerms(new[] { "black safety scissors", "white scissors", "clip", "cutting", "equipment", "modify", "scissors", "snip", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing", "Files" })] Cut = 0xF0C4, /// /// The Font Awesome "database" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "database", "computer", "development", "directory", "memory", "storage" })] + [FontAwesomeSearchTerms(new[] { "database", "computer", "development", "directory", "memory", "mysql", "sql", "storage" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] Database = 0xF1C0, @@ -2470,7 +2498,7 @@ public enum FontAwesomeIcon /// The Font Awesome "ear-deaf" icon unicode character. ///
[FontAwesomeSearchTerms(new[] { "ear deaf", "ear", "hearing", "sign language" })] - [FontAwesomeCategoriesAttribute(new[] { "Accessibility" })] + [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Communication" })] Deaf = 0xF2A4, /// @@ -2498,7 +2526,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-dots-from-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person dots from line", "allergy", "diagnosis" })] + [FontAwesomeSearchTerms(new[] { "person dots from line", "allergy", "diagnosis", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] Diagnoses = 0xF470, @@ -2526,7 +2554,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "diamond" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "diamond", "card", "cards", "diamond suit", "game", "gem", "gemstone", "poker", "suit" })] + [FontAwesomeSearchTerms(new[] { "diamond", "ace", "card", "cards", "diamond suit", "game", "gem", "gemstone", "poker", "suit" })] [FontAwesomeCategoriesAttribute(new[] { "Gaming", "Shapes" })] Diamond = 0xF219, @@ -2653,7 +2681,7 @@ public enum FontAwesomeIcon /// The Font Awesome "dollar-sign" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x24. /// - [FontAwesomeSearchTerms(new[] { "dollar sign", "dollar sign", "currency", "dollar", "heavy dollar sign", "money" })] + [FontAwesomeSearchTerms(new[] { "dollar sign", "dollar sign", "coupon", "currency", "dollar", "heavy dollar sign", "investment", "money", "premium", "revenue", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Maps", "Money" })] DollarSign = 0xF155, @@ -2674,7 +2702,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "circle-dollar-to-slot" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle dollar to slot", "contribute", "generosity", "gift", "give" })] + [FontAwesomeSearchTerms(new[] { "circle dollar to slot", "contribute", "generosity", "gift", "give", "premium" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Money", "Political" })] Donate = 0xF4B9, @@ -2688,7 +2716,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "door-closed" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "door closed", "doo", "door", "enter", "exit", "locked" })] + [FontAwesomeSearchTerms(new[] { "door closed", "doo", "door", "enter", "exit", "locked", "privacy" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Security", "Travel + Hotel" })] DoorClosed = 0xF52A, @@ -2716,7 +2744,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "download" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "download", "export", "hard drive", "save", "transfer" })] + [FontAwesomeSearchTerms(new[] { "download", "export", "hard drive", "insert", "save", "transfer" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Devices + Hardware" })] Download = 0xF019, @@ -2765,7 +2793,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "dumbbell" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "dumbbell", "exercise", "gym", "strength", "weight", "weight-lifting" })] + [FontAwesomeSearchTerms(new[] { "dumbbell", "exercise", "gym", "strength", "weight", "weight-lifting", "workout" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Travel + Hotel" })] Dumbbell = 0xF44B, @@ -2800,7 +2828,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "pen-to-square" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen to square", "edit", "pen", "pencil", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "pen to square", "edit", "modify", "pen", "pencil", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing", "Writing" })] Edit = 0xF044, @@ -2821,56 +2849,56 @@ public enum FontAwesomeIcon /// /// The Font Awesome "elevator" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "accessibility", "elevator", "hoist", "lift", "users-people" })] + [FontAwesomeSearchTerms(new[] { "accessibility", "elevator", "hoist", "lift", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel", "Users + People" })] Elevator = 0xE16D, /// /// The Font Awesome "ellipsis" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ellipsis", "dots", "drag", "kebab", "list", "menu", "nav", "navigation", "ol", "pacman", "reorder", "settings", "ul" })] + [FontAwesomeSearchTerms(new[] { "ellipsis", "dots", "drag", "kebab", "list", "menu", "nav", "navigation", "ol", "pacman", "reorder", "settings", "three dots", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] EllipsisH = 0xF141, /// /// The Font Awesome "ellipsis-vertical" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ellipsis vertical", "dots", "drag", "kebab", "list", "menu", "nav", "navigation", "ol", "reorder", "settings", "ul" })] + [FontAwesomeSearchTerms(new[] { "ellipsis vertical", "bullet", "dots", "drag", "kebab", "list", "menu", "nav", "navigation", "ol", "reorder", "settings", "three dots", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] EllipsisV = 0xF142, /// /// The Font Awesome "envelope" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "back of envelope", "e-mail", "email", "envelope", "letter", "mail", "message", "notification", "support" })] + [FontAwesomeSearchTerms(new[] { "back of envelope", "e-mail", "email", "envelope", "letter", "mail", "message", "newsletter", "notification", "offer", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Humanitarian", "Social", "Writing" })] Envelope = 0xF0E0, /// /// The Font Awesome "envelope-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "envelope circle check", "check", "email", "envelope", "mail", "not affected", "ok", "okay", "read", "sent" })] + [FontAwesomeSearchTerms(new[] { "envelope circle check", "check", "email", "enable", "envelope", "mail", "not affected", "ok", "okay", "read", "sent", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Humanitarian" })] EnvelopeCircleCheck = 0xE4E8, /// /// The Font Awesome "envelope-open" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "envelope open", "e-mail", "email", "letter", "mail", "message", "notification", "support" })] + [FontAwesomeSearchTerms(new[] { "envelope open", "e-mail", "email", "letter", "mail", "message", "newsletter", "notification", "offer", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Writing" })] EnvelopeOpen = 0xF2B6, /// /// The Font Awesome "envelope-open-text" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "envelope open text", "e-mail", "email", "letter", "mail", "message", "notification", "support" })] + [FontAwesomeSearchTerms(new[] { "envelope open text", "e-mail", "email", "letter", "mail", "message", "newsletter", "notification", "offer", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] EnvelopeOpenText = 0xF658, /// /// The Font Awesome "square-envelope" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square envelope", "e-mail", "email", "letter", "mail", "message", "notification", "support" })] + [FontAwesomeSearchTerms(new[] { "square envelope", "e-mail", "email", "letter", "mail", "message", "notification", "offer", "support" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication" })] EnvelopeSquare = 0xF199, @@ -2914,42 +2942,42 @@ public enum FontAwesomeIcon /// The Font Awesome "exclamation" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x21. ///
- [FontAwesomeSearchTerms(new[] { "!", "exclamation mark", "alert", "danger", "error", "exclamation", "important", "mark", "notice", "notification", "notify", "outlined", "problem", "punctuation", "red exclamation mark", "warning", "white exclamation mark" })] + [FontAwesomeSearchTerms(new[] { "!", "exclamation mark", "alert", "attention", "danger", "error", "exclamation", "failed", "important", "mark", "notice", "notification", "notify", "outlined", "problem", "punctuation", "red exclamation mark", "required", "warning", "white exclamation mark" })] [FontAwesomeCategoriesAttribute(new[] { "Alert", "Punctuation + Symbols" })] Exclamation = 0xF12A, /// /// The Font Awesome "circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle exclamation", "affect", "alert", "damage", "danger", "error", "important", "notice", "notification", "notify", "problem", "warning" })] + [FontAwesomeSearchTerms(new[] { "circle exclamation", "affect", "alert", "attention", "damage", "danger", "error", "failed", "important", "notice", "notification", "notify", "problem", "required", "warning" })] [FontAwesomeCategoriesAttribute(new[] { "Alert", "Punctuation + Symbols" })] ExclamationCircle = 0xF06A, /// /// The Font Awesome "triangle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "triangle exclamation", "alert", "danger", "error", "important", "notice", "notification", "notify", "problem", "warnin", "warning" })] + [FontAwesomeSearchTerms(new[] { "triangle exclamation", "alert", "attention", "danger", "error", "failed", "important", "notice", "notification", "notify", "problem", "required", "warnin", "warning" })] [FontAwesomeCategoriesAttribute(new[] { "Alert" })] ExclamationTriangle = 0xF071, /// /// The Font Awesome "expand" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "expand", "bigger", "crop", "enlarge", "focus", "fullscreen", "resize", "viewfinder" })] + [FontAwesomeSearchTerms(new[] { "arrows", "bigger", "enlarge", "expand", "fullscreen", "maximize", "resize", "resize", "scale", "size", "viewfinder" })] [FontAwesomeCategoriesAttribute(new[] { "Media Playback" })] Expand = 0xF065, /// /// The Font Awesome "up-right-and-down-left-from-center" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "up right and down left from center", "arrows", "bigger", "enlarge", "fullscreen", "resize" })] + [FontAwesomeSearchTerms(new[] { "up right and down left from center", "arrows", "bigger", "enlarge", "expand", "fullscreen", "maximize", "resize", "resize", "scale", "size" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] ExpandAlt = 0xF424, /// /// The Font Awesome "maximize" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "maximize", "bigger", "enlarge", "fullscreen", "move", "resize" })] + [FontAwesomeSearchTerms(new[] { "arrows", "bigger", "enlarge", "expand", "fullscreen", "maximize", "resize", "resize", "scale", "size" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] ExpandArrowsAlt = 0xF31E, @@ -2963,7 +2991,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "up-right-from-square" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "up right from square", "external-link", "new", "open", "share" })] + [FontAwesomeSearchTerms(new[] { "up right from square", "external-link", "new", "open", "share", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ExternalLinkAlt = 0xF35D, @@ -2991,7 +3019,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "eye-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "eye slash", "blind", "hide", "show", "toggle", "unseen", "views", "visible", "visiblity" })] + [FontAwesomeSearchTerms(new[] { "eye slash", "blind", "disabled", "hide", "show", "toggle", "unseen", "views", "visible", "visiblity" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Editing", "Maps", "Photos + Images", "Security" })] EyeSlash = 0xF070, @@ -3005,14 +3033,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "backward-fast" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "backward fast", "arrow", "beginning", "first", "last track button", "previous", "previous scene", "previous track", "rewind", "start", "triangle" })] + [FontAwesomeSearchTerms(new[] { "backward fast", "arrow", "beginning", "first", "last track button", "previous", "previous scene", "previous track", "quick", "rewind", "start", "triangle" })] [FontAwesomeCategoriesAttribute(new[] { "Media Playback" })] FastBackward = 0xF049, /// /// The Font Awesome "forward-fast" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "forward fast", "arrow", "end", "last", "next", "next scene", "next track", "next track button", "triangle" })] + [FontAwesomeSearchTerms(new[] { "forward fast", "arrow", "end", "last", "next", "next scene", "next track", "next track button", "quick", "triangle" })] [FontAwesomeCategoriesAttribute(new[] { "Media Playback" })] FastForward = 0xF050, @@ -3054,7 +3082,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-dress" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person dress", "man", "skirt", "woman" })] + [FontAwesomeSearchTerms(new[] { "person dress", "man", "skirt", "uer", "woman" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] Female = 0xF182, @@ -3075,7 +3103,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file", "empty document", "document", "new", "page", "page facing up", "pdf", "resume" })] + [FontAwesomeSearchTerms(new[] { "file", "empty document", "cv", "document", "new", "page", "page facing up", "pdf", "resume" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Coding", "Files", "Humanitarian", "Shapes", "Writing" })] File = 0xF15B, @@ -3103,14 +3131,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file circle check", "document", "file", "not affected", "ok", "okay", "paper" })] + [FontAwesomeSearchTerms(new[] { "file circle check", "document", "enable", "file", "not affected", "ok", "okay", "paper", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Humanitarian" })] FileCircleCheck = 0xE5A0, /// /// The Font Awesome "file-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file circle exclamation", "document", "file", "paper" })] + [FontAwesomeSearchTerms(new[] { "file circle exclamation", "document", "failed", "file", "paper" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Humanitarian" })] FileCircleExclamation = 0xE4EB, @@ -3138,21 +3166,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file circle xmark", "document", "file", "paper" })] + [FontAwesomeSearchTerms(new[] { "file circle xmark", "document", "file", "paper", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Humanitarian" })] FileCircleXmark = 0xE5A1, /// /// The Font Awesome "file-code" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file code", "css", "development", "document", "html" })] + [FontAwesomeSearchTerms(new[] { "file code", "css", "development", "document", "html", "mysql", "sql" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Files" })] FileCode = 0xF1C9, /// /// The Font Awesome "file-contract" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file contract", "agreement", "binding", "document", "legal", "signature" })] + [FontAwesomeSearchTerms(new[] { "file contract", "agreement", "binding", "document", "legal", "signature", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] FileContract = 0xF56C, @@ -3166,7 +3194,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-arrow-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file arrow down", "document", "export", "save" })] + [FontAwesomeSearchTerms(new[] { "file arrow down", "archive", "document", "export", "insert", "save" })] [FontAwesomeCategoriesAttribute(new[] { "Files" })] FileDownload = 0xF56D, @@ -3184,17 +3212,31 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Files" })] FileExport = 0xF56E, + /// + /// The Font Awesome "file-fragment" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "file fragment", "block", "data", "partial", "piece" })] + [FontAwesomeCategoriesAttribute(new[] { "Files" })] + FileFragment = 0xE697, + + /// + /// The Font Awesome "file-half-dashed" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "file half dashed", "data", "fragment", "partial", "piece" })] + [FontAwesomeCategoriesAttribute(new[] { "Files" })] + FileHalfDashed = 0xE698, + /// /// The Font Awesome "file-image" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file image", "document with picture", "document", "image", "jpg", "photo", "png" })] + [FontAwesomeSearchTerms(new[] { "file image", "document with picture", "document", "image", "img", "jpg", "photo", "png" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Photos + Images" })] FileImage = 0xF1C5, /// /// The Font Awesome "file-import" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file import", "copy", "document", "send", "upload" })] + [FontAwesomeSearchTerms(new[] { "file import", "copy", "document", "insert", "send", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Files" })] FileImport = 0xF56F, @@ -3208,7 +3250,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-invoice-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file invoice dollar", "$", "account", "bill", "charge", "document", "dollar-sign", "money", "payment", "receipt", "usd" })] + [FontAwesomeSearchTerms(new[] { "file invoice dollar", "$", "account", "bill", "charge", "document", "dollar-sign", "money", "payment", "receipt", "revenue", "salary", "usd" })] [FontAwesomeCategoriesAttribute(new[] { "Money" })] FileInvoiceDollar = 0xF571, @@ -3236,7 +3278,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-pen" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file pen", "edit", "memo", "pen", "pencil", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "file pen", "edit", "memo", "modify", "pen", "pencil", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Files", "Humanitarian" })] FilePen = 0xF31C, @@ -3264,14 +3306,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "file-signature" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file signature", "john hancock", "contract", "document", "name" })] + [FontAwesomeSearchTerms(new[] { "file signature", "john hancock", "contract", "document", "name", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] FileSignature = 0xF573, /// /// The Font Awesome "file-arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "file arrow up", "document", "import", "page", "save" })] + [FontAwesomeSearchTerms(new[] { "file arrow up", "document", "import", "page", "save", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Files" })] FileUpload = 0xF574, @@ -3320,14 +3362,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "filter-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "filter circle xmark", "cancel", "funnel", "options", "remove", "separate", "sort" })] + [FontAwesomeSearchTerms(new[] { "filter circle xmark", "cancel", "funnel", "options", "remove", "separate", "sort", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] FilterCircleXmark = 0xE17B, /// /// The Font Awesome "fingerprint" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "fingerprint", "human", "id", "identification", "lock", "smudge", "touch", "unique", "unlock" })] + [FontAwesomeSearchTerms(new[] { "fingerprint", "human", "id", "identification", "lock", "privacy", "smudge", "touch", "unique", "unlock" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Security" })] Fingerprint = 0xF577, @@ -3411,14 +3453,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "flask" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "flask", "beaker", "chemicals", "experiment", "experimental", "labs", "liquid", "potion", "science", "vial" })] + [FontAwesomeSearchTerms(new[] { "flask", "beaker", "chemicals", "experiment", "experimental", "knowledge", "labs", "liquid", "potion", "science", "vial" })] [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage", "Maps", "Medical + Health", "Science" })] Flask = 0xF0C3, /// /// The Font Awesome "flask-vial" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "flask vial", " beaker", " chemicals", " experiment", " experimental", " labs", " liquid", " science", " vial", "ampule", "chemistry", "lab", "laboratory", "potion", "test", "test tube" })] + [FontAwesomeSearchTerms(new[] { "flask vial", "ampule", "beaker", "chemicals", "chemistry", "experiment", "experimental", "lab", "laboratory", "labs", "liquid", "potion", "science", "test", "test tube", "vial" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Science" })] FlaskVial = 0xE4F3, @@ -3539,7 +3581,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "face-frown" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "face frown", "disapprove", "emoticon", "face", "frown", "frowning face", "rating", "sad" })] + [FontAwesomeSearchTerms(new[] { "face frown", "disapprove", "emoticon", "face", "frown", "frowning face", "rating", "sad", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Emoji", "Users + People" })] Frown = 0xF119, @@ -3553,7 +3595,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "filter-circle-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "filter circle dollar", "filter", "money", "options", "separate", "sort" })] + [FontAwesomeSearchTerms(new[] { "filter circle dollar", "filter", "money", "options", "premium", "separate", "sort" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] FunnelDollar = 0xF662, @@ -3567,7 +3609,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "gamepad" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "gamepad", "arcade", "controller", "d-pad", "joystick", "video", "video game" })] + [FontAwesomeSearchTerms(new[] { "gamepad", "arcade", "controller", "d-pad", "joystick", "playstore", "video", "video game" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Devices + Hardware", "Gaming", "Maps" })] Gamepad = 0xF11B, @@ -3595,7 +3637,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "gauge-simple-high" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "gauge simple high", "dashboard", "fast", "odometer", "speed", "speedometer" })] + [FontAwesomeSearchTerms(new[] { "gauge simple high", "dashboard", "fast", "odometer", "quick", "speed", "speedometer" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive" })] GaugeSimpleHigh = 0xF62A, @@ -3693,7 +3735,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "globe" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "all", "coordinates", "country", "earth", "global", "globe", "globe with meridians", "gps", "internet", "language", "localize", "location", "map", "meridians", "network", "online", "place", "planet", "translate", "travel", "world" })] + [FontAwesomeSearchTerms(new[] { "all", "coordinates", "country", "earth", "global", "globe", "globe with meridians", "gps", "internet", "language", "localize", "location", "map", "meridians", "network", "online", "place", "planet", "translate", "travel", "world", "www" })] [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Business", "Charity", "Connectivity", "Maps" })] Globe = 0xF0AC, @@ -3820,7 +3862,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "face-grin-stars" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "face grin stars", "emoticon", "eyes", "face", "grinning", "star", "star-struck", "starry-eyed" })] + [FontAwesomeSearchTerms(new[] { "face grin stars", "emoticon", "eyes", "face", "grinning", "quality", "star", "star-struck", "starry-eyed", "vip" })] [FontAwesomeCategoriesAttribute(new[] { "Emoji" })] GrinStars = 0xF587, @@ -3862,7 +3904,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "grip" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "grip", "affordance", "drag", "drop", "grab", "handle" })] + [FontAwesomeSearchTerms(new[] { "grip", "affordance", "app", "collection", "dashboard", "drag", "drop", "grab", "grid", "handle", "launcher", "square" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] GripHorizontal = 0xF58D, @@ -3925,7 +3967,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hammer" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "fix", "hammer", "recovery", "repair", "settings", "tool" })] + [FontAwesomeSearchTerms(new[] { "admin", "configuration", "equipment", "fix", "hammer", "maintenance", "modify", "recovery", "repair", "settings", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Humanitarian" })] Hammer = 0xF6E3, @@ -3953,8 +3995,8 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand-holding-droplet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand holding droplet", "carry", "covid-19", "drought", "grow", "lift", "sanitation" })] - [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands" })] + [FontAwesomeSearchTerms(new[] { "hand holding droplet", "blood", "carry", "covid-19", "drought", "grow", "lift", "sanitation" })] + [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Medical + Health" })] HandHoldingDroplet = 0xF4C1, /// @@ -3967,7 +4009,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand-holding-heart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand holding heart", "carry", "charity", "gift", "lift", "package" })] + [FontAwesomeSearchTerms(new[] { "hand holding heart", "carry", "charity", "gift", "lift", "package", "wishlist" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands" })] HandHoldingHeart = 0xF4BE, @@ -3981,7 +4023,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand-holding-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand holding dollar", "$", "carry", "dollar sign", "donation", "giving", "lift", "money", "price" })] + [FontAwesomeSearchTerms(new[] { "hand holding dollar", "$", "carry", "coupon", "dollar sign", "donate", "donation", "giving", "investment", "lift", "money", "premium", "price", "revenue", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Money" })] HandHoldingUsd = 0xF4C0, @@ -4002,7 +4044,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand", "raised hand", "backhand", "game", "halt", "palm", "raised", "raised back of hand", "roshambo", "stop" })] + [FontAwesomeSearchTerms(new[] { "hand", "raised hand", "backhand", "game", "halt", "palm", "raised", "raised back of hand", "request", "roshambo", "stop" })] [FontAwesomeCategoriesAttribute(new[] { "Hands", "Media Playback" })] HandPaper = 0xF256, @@ -4044,7 +4086,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hand-point-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hand point up", "finger", "hand", "hand-o-up", "index", "index pointing up", "point", "up" })] + [FontAwesomeSearchTerms(new[] { "hand point up", "finger", "hand", "hand-o-up", "index", "index pointing up", "point", "request", "up", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Hands" })] HandPointUp = 0xF0A6, @@ -4094,27 +4136,29 @@ public enum FontAwesomeIcon /// The Font Awesome "handshake" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "handshake", "agreement", "greeting", "meeting", "partnership" })] - [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Political", "Shopping" })] + [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Humanitarian", "Political", "Shopping" })] Handshake = 0xF2B5, /// - /// The Font Awesome "handshake-simple" icon unicode character. + /// The Font Awesome "handshake" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF2B5. /// - [FontAwesomeSearchTerms(new[] { "handshake simple", "agreement", "greeting", "hand", "handshake", "meeting", "partnership", "shake" })] - [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Humanitarian" })] + [FontAwesomeSearchTerms(new[] { "handshake", "agreement", "greeting", "meeting", "partnership" })] + [FontAwesomeCategoriesAttribute(new[] { "Charity", "Hands", "Humanitarian", "Political", "Shopping" })] HandshakeSimple = 0xF4C6, /// - /// The Font Awesome "handshake-simple-slash" icon unicode character. + /// The Font Awesome "handshake-slash" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xE060. /// - [FontAwesomeSearchTerms(new[] { "handshake simple slash", "broken", "covid-19", "social distance" })] + [FontAwesomeSearchTerms(new[] { "handshake slash", "broken", "covid-19", "disabled", "social distance" })] [FontAwesomeCategoriesAttribute(new[] { "Hands" })] HandshakeSimpleSlash = 0xE05F, /// /// The Font Awesome "handshake-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "handshake slash", "broken", "covid-19", "social distance" })] + [FontAwesomeSearchTerms(new[] { "handshake slash", "broken", "covid-19", "disabled", "social distance" })] [FontAwesomeCategoriesAttribute(new[] { "Hands" })] HandshakeSlash = 0xE060, @@ -4128,7 +4172,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hands-holding-child" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hands holding child", "care", "give", "help", "hold", "protect" })] + [FontAwesomeSearchTerms(new[] { "hands holding child", "care", "give", "help", "hold", "parent", "protect" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Childhood", "Hands", "Humanitarian", "Security" })] HandsHoldingChild = 0xE4FA, @@ -4163,7 +4207,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "helmet-safety" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "helmet safety", "construction", "hardhat", "helmet", "safety" })] + [FontAwesomeSearchTerms(new[] { "helmet safety", "construction", "hardhat", "helmet", "maintenance", "safety" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Logistics" })] HardHat = 0xF807, @@ -4218,10 +4262,11 @@ public enum FontAwesomeIcon Headphones = 0xF025, /// - /// The Font Awesome "headphones-simple" icon unicode character. + /// The Font Awesome "headphones" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF025. /// - [FontAwesomeSearchTerms(new[] { "headphones simple", "audio", "listen", "music", "sound", "speaker" })] - [FontAwesomeCategoriesAttribute(new[] { "Music + Audio" })] + [FontAwesomeSearchTerms(new[] { "headphones", "audio", "earbud", "headphone", "listen", "music", "sound", "speaker" })] + [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware", "Film + Video", "Music + Audio" })] HeadphonesAlt = 0xF58F, /// @@ -4234,35 +4279,35 @@ public enum FontAwesomeIcon /// /// The Font Awesome "head-side-cough" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "head side cough", "cough", "covid-19", "germs", "lungs", "respiratory", "sick" })] + [FontAwesomeSearchTerms(new[] { "head side cough", "cough", "covid-19", "germs", "lungs", "respiratory", "sick", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] HeadSideCough = 0xE061, /// /// The Font Awesome "head-side-cough-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "head side cough slash", "cough", "covid-19", "germs", "lungs", "respiratory", "sick" })] + [FontAwesomeSearchTerms(new[] { "head side cough slash", "cough", "covid-19", "disabled", "germs", "lungs", "respiratory", "sick", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] HeadSideCoughSlash = 0xE062, /// /// The Font Awesome "head-side-mask" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "head side mask", "breath", "coronavirus", "covid-19", "filter", "flu", "infection", "pandemic", "respirator", "virus" })] + [FontAwesomeSearchTerms(new[] { "head side mask", "breath", "coronavirus", "covid-19", "filter", "flu", "infection", "pandemic", "respirator", "uer", "virus" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] HeadSideMask = 0xE063, /// /// The Font Awesome "head-side-virus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "head side virus", "cold", "coronavirus", "covid-19", "flu", "infection", "pandemic", "sick" })] + [FontAwesomeSearchTerms(new[] { "head side virus", "cold", "coronavirus", "covid-19", "flu", "infection", "pandemic", "sick", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] HeadSideVirus = 0xE064, /// /// The Font Awesome "heart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "black", "black heart", "blue", "blue heart", "brown", "brown heart", "card", "evil", "favorite", "game", "green", "green heart", "heart", "heart suit", "like", "love", "orange", "orange heart", "purple", "purple heart", "red heart", "relationship", "valentine", "white", "white heart", "wicked", "yellow", "yellow heart" })] + [FontAwesomeSearchTerms(new[] { "ace", "card", "favorite", "game", "heart", "heart suit", "like", "love", "relationship", "valentine", "wishlist" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Gaming", "Holidays", "Maps", "Medical + Health", "Shapes", "Shopping", "Social", "Sports + Fitness" })] Heart = 0xF004, @@ -4290,14 +4335,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "heart-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "heart circle check", "favorite", "heart", "love", "not affected", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "heart circle check", "enable", "favorite", "heart", "love", "not affected", "ok", "okay", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] HeartCircleCheck = 0xE4FD, /// /// The Font Awesome "heart-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "heart circle exclamation", "favorite", "heart", "love" })] + [FontAwesomeSearchTerms(new[] { "heart circle exclamation", "failed", "favorite", "heart", "love" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] HeartCircleExclamation = 0xE4FE, @@ -4318,7 +4363,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "heart-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "heart circle xmark", "favorite", "heart", "love" })] + [FontAwesomeSearchTerms(new[] { "heart circle xmark", "favorite", "heart", "love", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] HeartCircleXmark = 0xE501, @@ -4343,17 +4388,38 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian" })] HelmetUn = 0xE503, + /// + /// The Font Awesome "hexagon" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "hexagon", "horizontal black hexagon", "geometry", "honeycomb", "polygon", "shape" })] + [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] + Hexagon = 0xF312, + + /// + /// The Font Awesome "hexagon-nodes" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "hexagon nodes", "action", "ai", "artificial intelligence", "cluster", "graph", "language", "llm", "model", "network", "neuronal" })] + [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Coding" })] + HexagonNodes = 0xE699, + + /// + /// The Font Awesome "hexagon-nodes-bolt" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "hexagon nodes bolt", "llm", "action", "ai", "artificial intelligence", "cluster", "graph", "language", "llm", "model", "network", "neuronal" })] + [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Coding" })] + HexagonNodesBolt = 0xE69A, + /// /// The Font Awesome "highlighter" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "highlighter", "edit", "marker", "sharpie", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "highlighter", "edit", "marker", "modify", "sharpie", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Text Formatting" })] Highlighter = 0xF591, /// /// The Font Awesome "person-hiking" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person hiking", "autumn", "fall", "hike", "mountain", "outdoors", "summer", "walk" })] + [FontAwesomeSearchTerms(new[] { "person hiking", "autumn", "fall", "follow", "hike", "mountain", "outdoors", "summer", "uer", "walk" })] [FontAwesomeCategoriesAttribute(new[] { "Camping", "Nature", "Sports + Fitness", "Users + People" })] Hiking = 0xF6EC, @@ -4381,7 +4447,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "clock-rotate-left" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clock rotate left", "rewind", "clock", "reverse", "time", "time machine", "time travel" })] + [FontAwesomeSearchTerms(new[] { "clock rotate left", "rewind", "clock", "pending", "reverse", "time", "time machine", "time travel", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Medical + Health" })] History = 0xF1DA, @@ -4445,7 +4511,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hospital-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hospital user", "covid-19", "doctor", "network", "patient", "primary care" })] + [FontAwesomeSearchTerms(new[] { "hospital user", "covid-19", "doctor", "network", "patient", "primary care", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Medical + Health", "Users + People" })] HospitalUser = 0xF80D, @@ -4466,7 +4532,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hot-tub-person" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hot tub person", "jacuzzi", "spa" })] + [FontAwesomeSearchTerms(new[] { "hot tub person", "jacuzzi", "spa", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel", "Users + People" })] HotTub = 0xF593, @@ -4480,21 +4546,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "hourglass-end" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hourglass end", "hour", "hourglass done", "minute", "sand", "stopwatch", "time", "timer" })] + [FontAwesomeSearchTerms(new[] { "hourglass end", "hour", "hourglass done", "minute", "pending", "sand", "stopwatch", "time", "timer", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] HourglassEnd = 0xF253, /// /// The Font Awesome "hourglass-half" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hourglass half", "hour", "minute", "sand", "stopwatch", "time" })] + [FontAwesomeSearchTerms(new[] { "hourglass half", "hour", "minute", "pending", "sand", "stopwatch", "time", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] HourglassHalf = 0xF252, /// /// The Font Awesome "hourglass-start" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "hourglass start", "hour", "minute", "sand", "stopwatch", "time" })] + [FontAwesomeSearchTerms(new[] { "hourglass start", "hour", "minute", "sand", "stopwatch", "time", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] HourglassStart = 0xF251, @@ -4508,7 +4574,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-chimney-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house chimney user", "covid-19", "home", "isolation", "quarantine" })] + [FontAwesomeSearchTerms(new[] { "house chimney user", "covid-19", "home", "isolation", "quarantine", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Users + People" })] HouseChimneyUser = 0xE065, @@ -4522,21 +4588,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house circle check", "abode", "home", "house", "not affected", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "house circle check", "abode", "enable", "home", "house", "not affected", "ok", "okay", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] HouseCircleCheck = 0xE509, /// /// The Font Awesome "house-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house circle exclamation", "abode", "affected", "home", "house" })] + [FontAwesomeSearchTerms(new[] { "house circle exclamation", "abode", "affected", "failed", "home", "house" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] HouseCircleExclamation = 0xE50A, /// /// The Font Awesome "house-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house circle xmark", "abode", "destroy", "home", "house" })] + [FontAwesomeSearchTerms(new[] { "house circle xmark", "abode", "destroy", "home", "house", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian" })] HouseCircleXmark = 0xE50B, @@ -4592,7 +4658,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house lock", "closed", "home", "house", "lockdown", "quarantine" })] + [FontAwesomeSearchTerms(new[] { "house lock", "closed", "home", "house", "lockdown", "padlock", "privacy", "quarantine" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Household", "Humanitarian", "Security" })] HouseLock = 0xE510, @@ -4606,21 +4672,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-medical-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house medical circle check", "clinic", "hospital", "not affected", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "house medical circle check", "clinic", "enable", "hospital", "not affected", "ok", "okay", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Medical + Health" })] HouseMedicalCircleCheck = 0xE511, /// /// The Font Awesome "house-medical-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house medical circle exclamation", "affected", "clinic", "hospital" })] + [FontAwesomeSearchTerms(new[] { "house medical circle exclamation", "affected", "clinic", "failed", "hospital" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Medical + Health" })] HouseMedicalCircleExclamation = 0xE512, /// /// The Font Awesome "house-medical-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house medical circle xmark", "clinic", "destroy", "hospital" })] + [FontAwesomeSearchTerms(new[] { "house medical circle xmark", "clinic", "destroy", "hospital", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Medical + Health" })] HouseMedicalCircleXmark = 0xE513, @@ -4634,7 +4700,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-signal" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house signal", "abode", "building", "connect", "family", "home", "residence", "smart home", "wifi" })] + [FontAwesomeSearchTerms(new[] { "house signal", "abode", "building", "connect", "family", "home", "residence", "smart home", "wifi", "www" })] [FontAwesomeCategoriesAttribute(new[] { "Connectivity", "Household", "Humanitarian" })] HouseSignal = 0xE012, @@ -4648,7 +4714,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "house-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "house user", "house" })] + [FontAwesomeSearchTerms(new[] { "house user", "house", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Household", "Users + People" })] HouseUser = 0xE1B0, @@ -4690,7 +4756,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "icons" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "icons", "bolt", "emoji", "heart", "image", "music", "photo", "symbols" })] + [FontAwesomeSearchTerms(new[] { "icons", "bolt", "category", "emoji", "heart", "image", "music", "photo", "symbols" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Design", "Social", "Text Formatting" })] Icons = 0xF86D, @@ -4704,21 +4770,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "id-badge" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "id badge", "address", "contact", "identification", "license", "profile" })] + [FontAwesomeSearchTerms(new[] { "id badge", "address", "contact", "identification", "license", "profile", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Photos + Images", "Security", "Users + People" })] IdBadge = 0xF2C1, /// /// The Font Awesome "id-card" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "id card", "contact", "demographics", "document", "identification", "issued", "profile", "registration" })] + [FontAwesomeSearchTerms(new[] { "id card", "contact", "demographics", "document", "identification", "issued", "profile", "registration", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Photos + Images", "Security", "Users + People" })] IdCard = 0xF2C2, /// /// The Font Awesome "id-card-clip" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "id card clip", "contact", "demographics", "document", "identification", "issued", "profile" })] + [FontAwesomeSearchTerms(new[] { "id card clip", "contact", "demographics", "document", "identification", "issued", "profile", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Security", "Users + People" })] IdCardAlt = 0xF47F, @@ -4732,14 +4798,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "image" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "image", "album", "landscape", "photo", "picture" })] + [FontAwesomeSearchTerms(new[] { "image", "album", "img", "landscape", "photo", "picture" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Photos + Images", "Social" })] Image = 0xF03E, /// /// The Font Awesome "images" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "images", "album", "landscape", "photo", "picture" })] + [FontAwesomeSearchTerms(new[] { "images", "album", "img", "landscape", "photo", "picture" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Photos + Images", "Social" })] Images = 0xF302, @@ -4792,6 +4858,12 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Maps" })] InfoCircle = 0xF05A, + /// + /// The Font Awesome "instagramsquare" icon unicode character. + /// + [Obsolete] + InstagramSquare = 0xF955, + /// /// The Font Awesome "italic" icon unicode character. /// @@ -4921,7 +4993,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "landmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "landmark", "building", "classical", "historic", "memorable", "monument", "museum", "politics" })] + [FontAwesomeSearchTerms(new[] { "landmark", "building", "classical", "historic", "memorable", "monument", "museum", "politics", "society" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Business", "Humanitarian", "Maps", "Money" })] Landmark = 0xF66F, @@ -4956,14 +5028,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "laptop" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "computer", "cpu", "dell", "demo", "device", "laptop", "mac", "macbook", "machine", "pc", "personal" })] + [FontAwesomeSearchTerms(new[] { "computer", "cpu", "dell", "demo", "device", "fabook", "fb", "laptop", "mac", "macbook", "machine", "pc", "personal" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware", "Humanitarian" })] Laptop = 0xF109, /// /// The Font Awesome "laptop-code" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "laptop code", "computer", "cpu", "dell", "demo", "develop", "device", "mac", "macbook", "machine", "pc" })] + [FontAwesomeSearchTerms(new[] { "laptop code", "computer", "cpu", "dell", "demo", "develop", "device", "fabook", "fb", "mac", "macbook", "machine", "mysql", "pc", "sql" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Education" })] LaptopCode = 0xF5FC, @@ -5019,7 +5091,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "layer-group" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "layer group", "arrange", "develop", "layers", "map", "stack" })] + [FontAwesomeSearchTerms(new[] { "layer group", "arrange", "category", "develop", "layers", "map", "platform", "stack" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Maps" })] LayerGroup = 0xF5FD, @@ -5076,7 +5148,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "lightbulb" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "lightbulb", " comic", " electric", " idea", " innovation", " inspiration", " light", " light bulb", " bulb", "bulb", "comic", "electric", "energy", "idea", "inspiration", "mechanical" })] + [FontAwesomeSearchTerms(new[] { "lightbulb", "bulb", "bulb", "comic", "comic", "electric", "electric", "energy", "idea", "idea", "innovation", "inspiration", "inspiration", "light", "light bulb", "mechanical" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Household", "Maps", "Marketing" })] Lightbulb = 0xF0EB, @@ -5104,28 +5176,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "list" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "list", "checklist", "completed", "done", "finished", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "list", "bullet", "category", "cheatsheet", "checklist", "completed", "done", "finished", "ol", "summary", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] List = 0xF03A, /// /// The Font Awesome "rectangle-list" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "rectangle list", "checklist", "completed", "done", "finished", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "rectangle list", "cheatsheet", "checklist", "completed", "done", "finished", "ol", "summary", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ListAlt = 0xF022, /// /// The Font Awesome "list-ol" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "list ol", "checklist", "completed", "done", "finished", "numbers", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "list ol", "cheatsheet", "checklist", "completed", "done", "finished", "numbers", "ol", "summary", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ListOl = 0xF0CB, /// /// The Font Awesome "list-ul" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "list ul", "checklist", "completed", "done", "finished", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "list ul", "bullet", "cheatsheet", "checklist", "completed", "done", "finished", "ol", "summary", "survey", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ListUl = 0xF0CA, @@ -5153,21 +5225,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "location-pin-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "location pin lock", "closed", "lockdown", "map", "quarantine" })] + [FontAwesomeSearchTerms(new[] { "location pin lock", "closed", "lockdown", "map", "padlock", "privacy", "quarantine" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Maps" })] LocationPinLock = 0xE51F, /// /// The Font Awesome "lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "closed", "lock", "locked", "open", "password", "private", "protect", "security" })] + [FontAwesomeSearchTerms(new[] { "admin", "closed", "lock", "locked", "open", "padlock", "password", "privacy", "private", "protect", "security" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] Lock = 0xF023, /// /// The Font Awesome "lock-open" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "lock open", "admin", "lock", "open", "password", "private", "protect", "security", "unlock" })] + [FontAwesomeSearchTerms(new[] { "lock open", "admin", "lock", "open", "padlock", "password", "privacy", "private", "protect", "security", "unlock" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] LockOpen = 0xF3C1, @@ -5202,7 +5274,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "up-long" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "up long", "long-arrow-up", "upload" })] + [FontAwesomeSearchTerms(new[] { "up long", "long-arrow-up", "upgrade", "upload" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] LongArrowAltUp = 0xF30C, @@ -5251,28 +5323,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "magnifying-glass-arrow-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass arrow right", "find", "next", "search" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass arrow right", "find", "magnifier", "next", "search" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Humanitarian", "Marketing" })] MagnifyingGlassArrowRight = 0xE521, /// /// The Font Awesome "magnifying-glass-chart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass chart", " data", " graph", " intelligence", "analysis", "chart", "market" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass chart", "analysis", "chart", "data", "graph", "intelligence", "magnifier", "market", "revenue" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Humanitarian", "Marketing" })] MagnifyingGlassChart = 0xE522, /// /// The Font Awesome "envelopes-bulk" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "envelopes bulk", "archive", "envelope", "letter", "post office", "postal", "postcard", "send", "stamp", "usps" })] + [FontAwesomeSearchTerms(new[] { "envelopes bulk", "archive", "envelope", "letter", "newsletter", "offer", "post office", "postal", "postcard", "send", "stamp", "usps" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] MailBulk = 0xF674, /// /// The Font Awesome "person" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person", "man", "person standing", "stand", "standing", "woman" })] + [FontAwesomeSearchTerms(new[] { "person", "default", "man", "person standing", "stand", "standing", "uer", "woman" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Maps", "Users + People" })] Male = 0xF183, @@ -5335,7 +5407,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "marker" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "marker", "design", "edit", "sharpie", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "marker", "design", "edit", "modify", "sharpie", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design" })] Marker = 0xF5A1, @@ -5349,7 +5421,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "mars-and-venus-burst" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "mars and venus burst", "gender", "violence" })] + [FontAwesomeSearchTerms(new[] { "mars and venus burst", "gender", "uer", "violence" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] MarsAndVenusBurst = 0xE523, @@ -5412,7 +5484,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "medal" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "award", "medal", "ribbon", "sports medal", "star", "trophy" })] + [FontAwesomeSearchTerms(new[] { "award", "guarantee", "medal", "quality", "ribbon", "sports medal", "star", "trophy", "warranty" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness" })] Medal = 0xF5A2, @@ -5426,7 +5498,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "face-meh" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "face meh", "deadpan", "emoticon", "face", "meh", "neutral", "neutral face", "rating" })] + [FontAwesomeSearchTerms(new[] { "face meh", "deadpan", "default", "emoticon", "face", "meh", "neutral", "neutral face", "rating", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Emoji", "Users + People" })] Meh = 0xF11A, @@ -5482,35 +5554,35 @@ public enum FontAwesomeIcon /// /// The Font Awesome "microphone" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "microphone", "address", "audio", "information", "podcast", "public", "record", "sing", "sound", "voice" })] + [FontAwesomeSearchTerms(new[] { "microphone", "address", "audio", "information", "podcast", "public", "record", "sing", "sound", "talking", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video", "Music + Audio", "Toggle" })] Microphone = 0xF130, /// /// The Font Awesome "microphone-lines" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "microphone lines", "audio", "mic", "microphone", "music", "podcast", "record", "sing", "sound", "studio", "studio microphone", "voice" })] + [FontAwesomeSearchTerms(new[] { "microphone lines", "audio", "mic", "microphone", "music", "podcast", "record", "sing", "sound", "studio", "studio microphone", "talking", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video", "Music + Audio" })] MicrophoneAlt = 0xF3C9, /// /// The Font Awesome "microphone-lines-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "microphone lines slash", "audio", "disable", "mute", "podcast", "record", "sing", "sound", "voice" })] + [FontAwesomeSearchTerms(new[] { "microphone lines slash", "audio", "disable", "disabled", "disconnect", "disconnect", "mute", "podcast", "record", "sing", "sound", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video", "Music + Audio" })] MicrophoneAltSlash = 0xF539, /// /// The Font Awesome "microphone-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "microphone slash", "audio", "disable", "mute", "podcast", "record", "sing", "sound", "voice" })] + [FontAwesomeSearchTerms(new[] { "microphone slash", "audio", "disable", "disabled", "mute", "podcast", "record", "sing", "sound", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video", "Music + Audio", "Toggle" })] MicrophoneSlash = 0xF131, /// /// The Font Awesome "microscope" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "covid-19", "electron", "lens", "microscope", "optics", "science", "shrink", "testing", "tool" })] + [FontAwesomeSearchTerms(new[] { "covid-19", "electron", "knowledge", "lens", "microscope", "optics", "science", "shrink", "testing", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Humanitarian", "Medical + Health", "Science" })] Microscope = 0xF610, @@ -5584,73 +5656,80 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Communication", "Devices + Hardware", "Humanitarian" })] MobileScreen = 0xF3CF, + /// + /// The Font Awesome "mobile-vibrate" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "mobile vibrate", "android", "call", "cell", "cell phone", "device", "haptic", "mobile", "mobile phone", "notification", "number", "phone", "screen", "telephone", "text" })] + [FontAwesomeCategoriesAttribute(new[] { "Communication", "Devices + Hardware" })] + MobileVibrate = 0xE816, + /// /// The Font Awesome "money-bill" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill", "buy", "cash", "checkout", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money bill", "buy", "cash", "checkout", "coupon", "investment", "money", "payment", "premium", "price", "purchase", "revenue", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Money" })] MoneyBill = 0xF0D6, /// /// The Font Awesome "money-bill-1" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill 1", "buy", "cash", "checkout", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money bill 1", "buy", "cash", "checkout", "money", "payment", "premium", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Money" })] MoneyBillAlt = 0xF3D1, /// /// The Font Awesome "money-bills" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bills", "atm", "cash", "money", "moolah" })] + [FontAwesomeSearchTerms(new[] { "money bills", "atm", "cash", "investment", "money", "moolah", "premium", "revenue", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] MoneyBills = 0xE1F3, /// /// The Font Awesome "money-bill-transfer" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill transfer", "bank", "conversion", "deposit", "money", "transfer", "withdrawal" })] + [FontAwesomeSearchTerms(new[] { "money bill transfer", "bank", "conversion", "deposit", "investment", "money", "salary", "transfer", "withdrawal" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] MoneyBillTransfer = 0xE528, /// /// The Font Awesome "money-bill-trend-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill trend up", "bank", "bonds", "inflation", "market", "stocks", "trade" })] + [FontAwesomeSearchTerms(new[] { "money bill trend up", "bank", "bonds", "inflation", "investment", "market", "revenue", "salary", "stocks", "trade" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] MoneyBillTrendUp = 0xE529, /// /// The Font Awesome "money-bill-wave" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill wave", "buy", "cash", "checkout", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money bill wave", "buy", "cash", "checkout", "money", "payment", "premium", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Money" })] MoneyBillWave = 0xF53A, /// /// The Font Awesome "money-bill-1-wave" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill 1 wave", "buy", "cash", "checkout", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money bill 1 wave", "buy", "cash", "checkout", "money", "payment", "premium", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Money" })] MoneyBillWaveAlt = 0xF53B, /// /// The Font Awesome "money-bill-wheat" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money bill wheat", "agribusiness", "agriculture", "farming", "food", "livelihood", "subsidy" })] + [FontAwesomeSearchTerms(new[] { "money bill wheat", "agribusiness", "agriculture", "farming", "food", "investment", "livelihood", "subsidy" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] MoneyBillWheat = 0xE52A, /// /// The Font Awesome "money-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money check", "bank check", "buy", "checkout", "cheque", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money check", "bank check", "buy", "checkout", "cheque", "money", "payment", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Money", "Shopping" })] MoneyCheck = 0xF53C, /// /// The Font Awesome "money-check-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "money check dollar", "bank check", "buy", "checkout", "cheque", "money", "payment", "price", "purchase" })] + [FontAwesomeSearchTerms(new[] { "money check dollar", "bank check", "buy", "checkout", "cheque", "money", "payment", "price", "purchase", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Money", "Shopping" })] MoneyCheckAlt = 0xF53D, @@ -5783,14 +5862,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "newspaper" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "article", "editorial", "headline", "journal", "journalism", "news", "newspaper", "paper", "press" })] + [FontAwesomeSearchTerms(new[] { "article", "editorial", "headline", "journal", "journalism", "news", "newsletter", "newspaper", "paper", "press" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Writing" })] Newspaper = 0xF1EA, + /// + /// The Font Awesome "non-binary" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "non binary", "female", "gender", "male", "nb", "queer" })] + [FontAwesomeCategoriesAttribute(new[] { "Genders" })] + NonBinary = 0xE807, + /// /// The Font Awesome "notdef" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "notdef", "close", "missing" })] + [FontAwesomeSearchTerms(new[] { "notdef", "404", "close", "missing", "not found" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Writing" })] Notdef = 0xE1FE, @@ -5822,6 +5908,13 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Design" })] ObjectUngroup = 0xF248, + /// + /// The Font Awesome "octagon" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "octagon", "octagonal", "shape", "sign", "stop", "stop sign" })] + [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] + Octagon = 0xF306, + /// /// The Font Awesome "oil-can" icon unicode character. /// @@ -5867,14 +5960,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "paintbrush" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "acrylic", "art", "brush", "color", "fill", "paint", "paintbrush", "painting", "pigment", "watercolor" })] + [FontAwesomeSearchTerms(new[] { "acrylic", "art", "brush", "color", "fill", "modify", "paint", "paintbrush", "painting", "pigment", "watercolor" })] [FontAwesomeCategoriesAttribute(new[] { "Design", "Editing" })] PaintBrush = 0xF1FC, /// /// The Font Awesome "paint-roller" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "paint roller", "acrylic", "art", "brush", "color", "fill", "paint", "pigment", "watercolor" })] + [FontAwesomeSearchTerms(new[] { "paint roller", "acrylic", "art", "brush", "color", "fill", "maintenance", "paint", "pigment", "watercolor" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Design" })] PaintRoller = 0xF5AA, @@ -5895,7 +5988,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "panorama" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "panorama", "image", "landscape", "photo", "wide" })] + [FontAwesomeSearchTerms(new[] { "panorama", "image", "img", "landscape", "photo", "wide" })] [FontAwesomeCategoriesAttribute(new[] { "Photos + Images" })] Panorama = 0xE209, @@ -5986,98 +6079,105 @@ public enum FontAwesomeIcon /// /// The Font Awesome "pen" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ballpoint", "design", "edit", "pen", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "ballpoint", "design", "edit", "modify", "pen", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing", "Writing" })] Pen = 0xF304, /// /// The Font Awesome "pen-clip" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen clip", "design", "edit", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "pen clip", "design", "edit", "modify", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing", "Writing" })] PenAlt = 0xF305, /// /// The Font Awesome "pencil" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "lower left pencil", "design", "draw", "edit", "lead", "pencil", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "lower left pencil", "design", "draw", "edit", "lead", "maintenance", "modify", "pencil", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Construction", "Design", "Editing", "Writing" })] PencilAlt = 0xF303, /// /// The Font Awesome "pen-ruler" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen ruler", "design", "draft", "draw", "pencil" })] + [FontAwesomeSearchTerms(new[] { "pen ruler", "design", "draft", "draw", "maintenance", "modify", "pencil" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Design", "Editing" })] PencilRuler = 0xF5AE, /// /// The Font Awesome "pen-fancy" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen fancy", "black nib", "design", "edit", "fountain", "fountain pen", "nib", "pen", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "pen fancy", "black nib", "design", "edit", "fountain", "fountain pen", "modify", "nib", "pen", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing" })] PenFancy = 0xF5AC, /// /// The Font Awesome "pen-nib" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "pen nib", "design", "edit", "fountain pen", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "pen nib", "design", "edit", "fountain pen", "modify", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing" })] PenNib = 0xF5AD, /// /// The Font Awesome "square-pen" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square pen", "edit", "pencil-square", "update", "write" })] + [FontAwesomeSearchTerms(new[] { "square pen", "edit", "modify", "pencil-square", "update", "write" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Editing", "Writing" })] PenSquare = 0xF14B, + /// + /// The Font Awesome "pentagon" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "5", "five", "pentagon", "shape" })] + [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] + Pentagon = 0xE790, + /// /// The Font Awesome "people-arrows" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people arrows", "distance", "isolation", "separate", "social distancing", "users-people" })] + [FontAwesomeSearchTerms(new[] { "people arrows", "conversation", "discussion", "distance", "insert", "isolation", "separate", "social distancing", "talk", "talking", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PeopleArrows = 0xE068, /// /// The Font Awesome "people-carry-box" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people carry box", "users-people" })] + [FontAwesomeSearchTerms(new[] { "people carry box", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Moving", "Users + People" })] PeopleCarry = 0xF4CE, /// /// The Font Awesome "people-group" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people group", "family", "group", "team" })] + [FontAwesomeSearchTerms(new[] { "people group", "crowd", "family", "group", "team", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Marketing", "Users + People" })] PeopleGroup = 0xE533, /// /// The Font Awesome "people-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people line", "group", "need" })] + [FontAwesomeSearchTerms(new[] { "people line", "crowd", "group", "need", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PeopleLine = 0xE534, /// /// The Font Awesome "people-pulling" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people pulling", "forced return", "yanking" })] + [FontAwesomeSearchTerms(new[] { "people pulling", "forced return", "together", "uer", "yanking" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PeoplePulling = 0xE535, /// /// The Font Awesome "people-robbery" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people robbery", "criminal", "hands up", "looting", "robbery", "steal" })] + [FontAwesomeSearchTerms(new[] { "people robbery", "criminal", "hands up", "looting", "robbery", "steal", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PeopleRobbery = 0xE536, /// /// The Font Awesome "people-roof" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "people roof", "family", "group", "manage", "people", "safe", "shelter" })] + [FontAwesomeSearchTerms(new[] { "people roof", "crowd", "family", "group", "manage", "people", "safe", "shelter", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Camping", "Household", "Humanitarian", "Users + People" })] PeopleRoof = 0xE537, @@ -6107,105 +6207,105 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-arrow-down-to-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person arrow down to line", "ground", "indigenous", "native" })] + [FontAwesomeSearchTerms(new[] { "person arrow down to line", "ground", "indigenous", "insert", "native", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonArrowDownToLine = 0xE538, /// /// The Font Awesome "person-arrow-up-from-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person arrow up from line", "population", "rise" })] + [FontAwesomeSearchTerms(new[] { "person arrow up from line", "population", "rise", "uer", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonArrowUpFromLine = 0xE539, /// /// The Font Awesome "person-booth" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person booth", "changing room", "curtain", "vote", "voting" })] + [FontAwesomeSearchTerms(new[] { "person booth", "changing room", "curtain", "uer", "vote", "voting" })] [FontAwesomeCategoriesAttribute(new[] { "Political", "Shopping", "Users + People" })] PersonBooth = 0xF756, /// /// The Font Awesome "person-breastfeeding" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person breastfeeding", "baby", "child", "infant", "mother", "nutrition", "sustenance" })] + [FontAwesomeSearchTerms(new[] { "person breastfeeding", "baby", "child", "infant", "mother", "nutrition", "parent", "sustenance", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Humanitarian", "Medical + Health", "Users + People" })] PersonBreastfeeding = 0xE53A, /// /// The Font Awesome "person-burst" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person burst", "abuse", "accident", "crash", "explode", "violence" })] + [FontAwesomeSearchTerms(new[] { "person burst", "abuse", "accident", "crash", "explode", "uer", "violence" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonBurst = 0xE53B, /// /// The Font Awesome "person-cane" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person cane", "aging", "cane", "elderly", "old", "staff" })] + [FontAwesomeSearchTerms(new[] { "person cane", "aging", "cane", "elderly", "old", "staff", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Humanitarian", "Medical + Health", "Users + People" })] PersonCane = 0xE53C, /// /// The Font Awesome "person-chalkboard" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person chalkboard", "blackboard", "instructor", "keynote", "lesson", "presentation", "teacher" })] + [FontAwesomeSearchTerms(new[] { "person chalkboard", "blackboard", "instructor", "keynote", "lesson", "presentation", "teacher", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Education", "Humanitarian", "Users + People" })] PersonChalkboard = 0xE53D, /// /// The Font Awesome "person-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle check", "approved", "not affected", "ok", "okay" })] + [FontAwesomeSearchTerms(new[] { "person circle check", "approved", "enable", "not affected", "ok", "okay", "uer", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleCheck = 0xE53E, /// /// The Font Awesome "person-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle exclamation", "affected", "alert", "lost", "missing" })] + [FontAwesomeSearchTerms(new[] { "person circle exclamation", "affected", "alert", "failed", "lost", "missing", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleExclamation = 0xE53F, /// /// The Font Awesome "person-circle-minus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle minus", "delete", "remove" })] + [FontAwesomeSearchTerms(new[] { "person circle minus", "delete", "remove", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleMinus = 0xE540, /// /// The Font Awesome "person-circle-plus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle plus", "add", "found" })] + [FontAwesomeSearchTerms(new[] { "person circle plus", "add", "follow", "found", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCirclePlus = 0xE541, /// /// The Font Awesome "person-circle-question" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle question", "lost", "missing" })] + [FontAwesomeSearchTerms(new[] { "person circle question", "faq", "lost", "missing", "request", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleQuestion = 0xE542, /// /// The Font Awesome "person-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person circle xmark", "dead", "removed" })] + [FontAwesomeSearchTerms(new[] { "person circle xmark", "dead", "removed", "uer", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonCircleXmark = 0xE543, /// /// The Font Awesome "person-digging" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person digging", "bury", "construction", "debris", "dig", "men at work" })] + [FontAwesomeSearchTerms(new[] { "person digging", "bury", "construction", "debris", "dig", "maintenance", "men at work", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Humanitarian", "Users + People" })] PersonDigging = 0xF85E, /// /// The Font Awesome "person-dress-burst" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person dress burst", "abuse", "accident", "crash", "explode", "violence" })] + [FontAwesomeSearchTerms(new[] { "person dress burst", "abuse", "accident", "crash", "explode", "uer", "violence" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonDressBurst = 0xE544, @@ -6219,112 +6319,112 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-falling" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person falling", "accident", "fall", "trip" })] + [FontAwesomeSearchTerms(new[] { "person falling", "accident", "fall", "trip", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonFalling = 0xE546, /// /// The Font Awesome "person-falling-burst" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person falling burst", "accident", "crash", "death", "fall", "homicide", "murder" })] + [FontAwesomeSearchTerms(new[] { "person falling burst", "accident", "crash", "death", "fall", "homicide", "murder", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonFallingBurst = 0xE547, /// /// The Font Awesome "person-half-dress" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person half dress", "gender", "man", "restroom", "transgender", "woman" })] + [FontAwesomeSearchTerms(new[] { "person half dress", "gender", "man", "restroom", "transgender", "uer", "woman" })] [FontAwesomeCategoriesAttribute(new[] { "Genders", "Humanitarian", "Medical + Health", "Users + People" })] PersonHalfDress = 0xE548, /// /// The Font Awesome "person-harassing" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person harassing", "abuse", "scream", "shame", "shout", "yell" })] + [FontAwesomeSearchTerms(new[] { "person harassing", "abuse", "scream", "shame", "shout", "uer", "yell" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonHarassing = 0xE549, /// /// The Font Awesome "person-military-pointing" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person military pointing", "army", "customs", "guard" })] + [FontAwesomeSearchTerms(new[] { "person military pointing", "army", "customs", "guard", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonMilitaryPointing = 0xE54A, /// /// The Font Awesome "person-military-rifle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person military rifle", "armed forces", "army", "military", "rifle", "war" })] + [FontAwesomeSearchTerms(new[] { "person military rifle", "armed forces", "army", "military", "rifle", "uer", "war" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonMilitaryRifle = 0xE54B, /// /// The Font Awesome "person-military-to-person" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person military to person", "civilian", "coordination", "military" })] + [FontAwesomeSearchTerms(new[] { "person military to person", "civilian", "coordination", "military", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonMilitaryToPerson = 0xE54C, /// /// The Font Awesome "person-pregnant" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person pregnant", "baby", "birth", "child", "pregnant", "pregnant woman", "woman" })] + [FontAwesomeSearchTerms(new[] { "person pregnant", "baby", "birth", "child", "parent", "pregnant", "pregnant woman", "uer", "woman" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] PersonPregnant = 0xE31E, /// /// The Font Awesome "person-rays" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person rays", "affected", "focus", "shine" })] + [FontAwesomeSearchTerms(new[] { "person rays", "affected", "focus", "shine", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Marketing", "Users + People" })] PersonRays = 0xE54D, /// /// The Font Awesome "person-rifle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person rifle", "army", "combatant", "gun", "military", "rifle", "war" })] + [FontAwesomeSearchTerms(new[] { "person rifle", "army", "combatant", "gun", "military", "rifle", "uer", "war" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Security", "Users + People" })] PersonRifle = 0xE54E, /// /// The Font Awesome "person-shelter" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person shelter", "house", "inside", "roof", "safe", "safety", "shelter" })] + [FontAwesomeSearchTerms(new[] { "person shelter", "house", "inside", "roof", "safe", "safety", "shelter", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Camping", "Humanitarian", "Security", "Users + People" })] PersonShelter = 0xE54F, /// /// The Font Awesome "person-through-window" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person through window", "door", "exit", "forced entry", "leave", "robbery", "steal", "window" })] + [FontAwesomeSearchTerms(new[] { "person through window", "door", "exit", "forced entry", "leave", "robbery", "steal", "uer", "window" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] PersonThroughWindow = 0xE5A9, /// /// The Font Awesome "person-walking-arrow-loop-left" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking arrow loop left", "population return", "return" })] + [FontAwesomeSearchTerms(new[] { "person walking arrow loop left", "follow", "population return", "return", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Users + People" })] PersonWalkingArrowLoopLeft = 0xE551, /// /// The Font Awesome "person-walking-arrow-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking arrow right", "exit", "internally displaced", "leave", "refugee" })] + [FontAwesomeSearchTerms(new[] { "person walking arrow right", "exit", "follow", "internally displaced", "leave", "refugee", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Users + People" })] PersonWalkingArrowRight = 0xE552, /// /// The Font Awesome "person-walking-dashed-line-arrow-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking dashed line arrow right", "exit", "refugee" })] + [FontAwesomeSearchTerms(new[] { "person walking dashed line arrow right", "exit", "follow", "refugee", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Users + People" })] PersonWalkingDashedLineArrowRight = 0xE553, /// /// The Font Awesome "person-walking-luggage" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking luggage", "bag", "baggage", "briefcase", "carry-on", "deployment", "rolling" })] + [FontAwesomeSearchTerms(new[] { "person walking luggage", "bag", "baggage", "briefcase", "carry-on", "deployment", "follow", "rolling", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Travel + Hotel", "Users + People" })] PersonWalkingLuggage = 0xE554, @@ -6345,7 +6445,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "phone" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "left hand telephone receiver", "call", "earphone", "number", "phone", "receiver", "support", "telephone", "telephone receiver", "voice" })] + [FontAwesomeSearchTerms(new[] { "left hand telephone receiver", "call", "earphone", "number", "phone", "receiver", "support", "talking", "telephone", "telephone receiver", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Maps" })] Phone = 0xF095, @@ -6359,7 +6459,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "phone-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "phone slash", "call", "cancel", "earphone", "mute", "number", "support", "telephone", "voice" })] + [FontAwesomeSearchTerms(new[] { "phone slash", "call", "cancel", "disabled", "disconnect", "earphone", "mute", "number", "support", "telephone", "voice" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication" })] PhoneSlash = 0xF3DD, @@ -6380,7 +6480,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "phone-volume" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "phone volume", "call", "earphone", "number", "sound", "support", "telephone", "voice", "volume-control-phone" })] + [FontAwesomeSearchTerms(new[] { "phone volume", "call", "earphone", "number", "ring", "ringing", "sound", "support", "talking", "telephone", "voice", "volume-control-phone" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Business", "Communication", "Maps", "Media Playback" })] PhoneVolume = 0xF2A0, @@ -6394,7 +6494,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "piggy-bank" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "piggy bank", "bank", "save", "savings" })] + [FontAwesomeSearchTerms(new[] { "piggy bank", "bank", "salary", "save", "savings" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Money", "Political" })] PiggyBank = 0xF4D3, @@ -6430,27 +6530,27 @@ public enum FontAwesomeIcon /// The Font Awesome "plane-arrival" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "plane arrival", "aeroplane", "airplane", "airplane arrival", "airport", "arrivals", "arriving", "destination", "fly", "land", "landing", "location", "mode", "travel", "trip" })] - [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel" })] + [FontAwesomeCategoriesAttribute(new[] { "Transportation", "Travel + Hotel" })] PlaneArrival = 0xF5AF, /// /// The Font Awesome "plane-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane circle check", "airplane", "airport", "flight", "fly", "not affected", "ok", "okay", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane circle check", "airplane", "airport", "enable", "flight", "fly", "not affected", "ok", "okay", "travel", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Travel + Hotel" })] PlaneCircleCheck = 0xE555, /// /// The Font Awesome "plane-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane circle exclamation", "affected", "airplane", "airport", "flight", "fly", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane circle exclamation", "affected", "airplane", "airport", "failed", "flight", "fly", "travel" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Travel + Hotel" })] PlaneCircleExclamation = 0xE556, /// /// The Font Awesome "plane-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane circle xmark", "airplane", "airport", "destroy", "flight", "fly", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane circle xmark", "airplane", "airport", "destroy", "flight", "fly", "travel", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Travel + Hotel" })] PlaneCircleXmark = 0xE557, @@ -6464,14 +6564,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "plane-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane lock", "airplane", "airport", "closed", "flight", "fly", "lockdown", "quarantine", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane lock", "airplane", "airport", "closed", "flight", "fly", "lockdown", "padlock", "privacy", "quarantine", "travel" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Travel + Hotel" })] PlaneLock = 0xE558, /// /// The Font Awesome "plane-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plane slash", "airplane mode", "airport", "canceled", "covid-19", "delayed", "grounded", "travel" })] + [FontAwesomeSearchTerms(new[] { "plane slash", "airplane mode", "airport", "canceled", "covid-19", "delayed", "disabled", "grounded", "travel" })] [FontAwesomeCategoriesAttribute(new[] { "Transportation", "Travel + Hotel" })] PlaneSlash = 0xE069, @@ -6527,21 +6627,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "plug-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plug circle check", "electric", "electricity", "not affected", "ok", "okay", "plug", "power" })] + [FontAwesomeSearchTerms(new[] { "plug circle check", "electric", "electricity", "enable", "not affected", "ok", "okay", "plug", "power", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Humanitarian" })] PlugCircleCheck = 0xE55C, /// /// The Font Awesome "plug-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plug circle exclamation", "affected", "electric", "electricity", "plug", "power" })] + [FontAwesomeSearchTerms(new[] { "plug circle exclamation", "affected", "electric", "electricity", "failed", "plug", "power" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Humanitarian" })] PlugCircleExclamation = 0xE55D, /// /// The Font Awesome "plug-circle-minus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plug circle minus", "electric", "electricity", "plug", "power" })] + [FontAwesomeSearchTerms(new[] { "plug circle minus", "disconnect", "electric", "electricity", "plug", "power" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Humanitarian" })] PlugCircleMinus = 0xE55E, @@ -6555,7 +6655,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "plug-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "plug circle xmark", "destroy", "electric", "electricity", "outage", "plug", "power" })] + [FontAwesomeSearchTerms(new[] { "plug circle xmark", "destroy", "disconnect", "electric", "electricity", "outage", "plug", "power", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Humanitarian" })] PlugCircleXmark = 0xE560, @@ -6563,7 +6663,7 @@ public enum FontAwesomeIcon /// The Font Awesome "plus" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x2B. ///
- [FontAwesomeSearchTerms(new[] { "+", "plus sign", "add", "create", "expand", "math", "new", "plus", "positive", "shape", "sign" })] + [FontAwesomeSearchTerms(new[] { "+", "plus sign", "add", "create", "expand", "follow", "math", "modify", "new", "plus", "positive", "shape", "sign" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Maps", "Mathematics", "Medical + Health", "Punctuation + Symbols" })] Plus = 0xF067, @@ -6598,21 +6698,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-poll-vertical" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square poll vertical", "chart", "graph", "results", "survey", "trend", "vote", "voting" })] + [FontAwesomeSearchTerms(new[] { "square poll vertical", "chart", "graph", "results", "revenue", "statistics", "survey", "trend", "vote", "voting" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Marketing", "Social" })] Poll = 0xF681, /// /// The Font Awesome "square-poll-horizontal" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square poll horizontal", "chart", "graph", "results", "survey", "trend", "vote", "voting" })] + [FontAwesomeSearchTerms(new[] { "square poll horizontal", "chart", "graph", "results", "statistics", "survey", "trend", "vote", "voting" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Charts + Diagrams", "Marketing", "Social" })] PollH = 0xF682, /// /// The Font Awesome "poo" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "crap", "dung", "face", "monster", "pile of poo", "poo", "poop", "shit", "smile", "turd" })] + [FontAwesomeSearchTerms(new[] { "crap", "dung", "face", "monster", "pile of poo", "poo", "poop", "shit", "smile", "turd", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Users + People" })] Poo = 0xF2FE, @@ -6633,7 +6733,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "image-portrait" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "image portrait", "id", "image", "photo", "picture", "selfie" })] + [FontAwesomeSearchTerms(new[] { "image portrait", "id", "image", "img", "photo", "picture", "selfie", "uer", "username" })] [FontAwesomeCategoriesAttribute(new[] { "Photos + Images", "Users + People" })] Portrait = 0xF3E0, @@ -6654,7 +6754,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-praying" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person praying", "kneel", "place of worship", "religion", "thank", "worship" })] + [FontAwesomeSearchTerms(new[] { "person praying", "kneel", "place of worship", "religion", "thank", "uer", "worship" })] [FontAwesomeCategoriesAttribute(new[] { "Religion", "Users + People" })] Pray = 0xF683, @@ -6703,7 +6803,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "diagram-project" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "diagram project", "chart", "graph", "network", "pert" })] + [FontAwesomeSearchTerms(new[] { "diagram project", "chart", "graph", "network", "pert", "statistics" })] [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Coding" })] ProjectDiagram = 0xF542, @@ -6731,22 +6831,22 @@ public enum FontAwesomeIcon /// /// The Font Awesome "qrcode" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "qrcode", "barcode", "info", "information", "scan" })] - [FontAwesomeCategoriesAttribute(new[] { "Coding" })] + [FontAwesomeSearchTerms(new[] { "qrcode", "barcode", "info", "information", "qr", "qr-code", "scan" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Shopping" })] Qrcode = 0xF029, /// /// The Font Awesome "question" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x3F. /// - [FontAwesomeSearchTerms(new[] { "?", "question mark", "help", "information", "mark", "outlined", "punctuation", "question", "red question mark", "support", "unknown", "white question mark" })] + [FontAwesomeSearchTerms(new[] { "?", "question mark", "faq", "help", "information", "mark", "outlined", "punctuation", "question", "red question mark", "request", "support", "unknown", "white question mark" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Alert", "Punctuation + Symbols" })] Question = 0xF128, /// /// The Font Awesome "circle-question" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle question", "help", "information", "support", "unknown" })] + [FontAwesomeSearchTerms(new[] { "circle question", "faq", "help", "information", "support", "unknown" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Punctuation + Symbols" })] QuestionCircle = 0xF059, @@ -6816,14 +6916,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "ranking-star" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ranking star", "chart", "first place", "podium", "rank", "win" })] + [FontAwesomeSearchTerms(new[] { "ranking star", "chart", "first place", "podium", "quality", "rank", "revenue", "win" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Marketing", "Sports + Fitness" })] RankingStar = 0xE561, /// /// The Font Awesome "receipt" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "accounting", "bookkeeping", "check", "evidence", "invoice", "money", "pay", "proof", "receipt", "table" })] + [FontAwesomeSearchTerms(new[] { "accounting", "bookkeeping", "check", "coupon", "evidence", "invoice", "money", "pay", "proof", "receipt", "table" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Money", "Shopping" })] Receipt = 0xF543, @@ -6844,15 +6944,15 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-rotate-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow rotate right", "clockwise open circle arrow", "forward", "refresh", "reload", "repeat" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] + [FontAwesomeSearchTerms(new[] { "arrow rotate right", "clockwise open circle arrow", "forward", "refresh", "reload", "renew", "repeat", "retry" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback", "Spinners" })] Redo = 0xF01E, /// /// The Font Awesome "rotate-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "rotate right", "forward", "refresh", "reload", "repeat" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] + [FontAwesomeSearchTerms(new[] { "rotate right", "forward", "refresh", "reload", "renew", "repeat", "retry" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback", "Spinners" })] RedoAlt = 0xF2F9, /// @@ -6865,14 +6965,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "text-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "text slash", "cancel", "font", "format", "remove", "style", "text" })] + [FontAwesomeSearchTerms(new[] { "text slash", "cancel", "disabled", "font", "format", "remove", "style", "text" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] RemoveFormat = 0xF87D, /// /// The Font Awesome "repeat" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow", "clockwise", "flip", "reload", "repeat", "repeat button", "rewind", "switch" })] + [FontAwesomeSearchTerms(new[] { "arrow", "clockwise", "flip", "reload", "renew", "repeat", "repeat button", "retry", "rewind", "switch" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] Repeat = 0xF363, @@ -6900,14 +7000,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "restroom" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "restroom", "bathroom", "toilet", "water closet", "wc" })] + [FontAwesomeSearchTerms(new[] { "restroom", "bathroom", "toilet", "uer", "water closet", "wc" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Users + People" })] Restroom = 0xF7BD, /// /// The Font Awesome "retweet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "retweet", "refresh", "reload", "share", "swap" })] + [FontAwesomeSearchTerms(new[] { "retweet", "refresh", "reload", "renew", "retry", "share", "swap" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Social" })] Retweet = 0xF079, @@ -6921,7 +7021,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "ring" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ring", "dungeons & dragons", "gollum", "band", "binding", "d&d", "dnd", "engagement", "fantasy", "gold", "jewelry", "marriage", "precious" })] + [FontAwesomeSearchTerms(new[] { "ring", "dungeons & dragons", "gollum", "band", "binding", "d&d", "dnd", "engagement", "fantasy", "gold", "jewelry", "marriage", "precious", "premium" })] [FontAwesomeCategoriesAttribute(new[] { "Gaming", "Spinners" })] Ring = 0xF70B, @@ -6949,28 +7049,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "road-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "road circle check", "freeway", "highway", "not affected", "ok", "okay", "pavement", "road" })] + [FontAwesomeSearchTerms(new[] { "road circle check", "enable", "freeway", "highway", "not affected", "ok", "okay", "pavement", "road", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] RoadCircleCheck = 0xE564, /// /// The Font Awesome "road-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "road circle exclamation", "affected", "freeway", "highway", "pavement", "road" })] + [FontAwesomeSearchTerms(new[] { "road circle exclamation", "affected", "failed", "freeway", "highway", "pavement", "road" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] RoadCircleExclamation = 0xE565, /// /// The Font Awesome "road-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "road circle xmark", "destroy", "freeway", "highway", "pavement", "road" })] + [FontAwesomeSearchTerms(new[] { "road circle xmark", "destroy", "freeway", "highway", "pavement", "road", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] RoadCircleXmark = 0xE566, /// /// The Font Awesome "road-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "road lock", "closed", "freeway", "highway", "lockdown", "pavement", "quarantine", "road" })] + [FontAwesomeSearchTerms(new[] { "road lock", "closed", "freeway", "highway", "lockdown", "padlock", "pavement", "privacy", "quarantine", "road" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics" })] RoadLock = 0xE567, @@ -7061,7 +7161,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-running" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person running", "exit", "flee", "marathon", "person running", "race", "running" })] + [FontAwesomeSearchTerms(new[] { "person running", "exit", "flee", "follow", "marathon", "person running", "race", "running", "uer", "workout" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] Running = 0xF70C, @@ -7082,14 +7182,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "sack-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sack dollar", "bag", "burlap", "cash", "dollar", "money", "money bag", "moneybag", "robber", "santa", "usd" })] + [FontAwesomeSearchTerms(new[] { "sack dollar", "bag", "burlap", "cash", "dollar", "investment", "money", "money bag", "moneybag", "premium", "robber", "salary", "santa", "usd" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] SackDollar = 0xF81D, /// /// The Font Awesome "sack-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sack xmark", "bag", "burlap", "rations" })] + [FontAwesomeSearchTerms(new[] { "sack xmark", "bag", "burlap", "coupon", "rations", "salary", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Money" })] SackXmark = 0xE56A, @@ -7145,21 +7245,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "school-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "school circle check", "not affected", "ok", "okay", "schoolhouse" })] + [FontAwesomeSearchTerms(new[] { "school circle check", "enable", "not affected", "ok", "okay", "schoolhouse", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Education", "Humanitarian" })] SchoolCircleCheck = 0xE56B, /// /// The Font Awesome "school-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "school circle exclamation", "affected", "schoolhouse" })] + [FontAwesomeSearchTerms(new[] { "school circle exclamation", "affected", "failed", "schoolhouse" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Education", "Humanitarian" })] SchoolCircleExclamation = 0xE56C, /// /// The Font Awesome "school-circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "school circle xmark", "destroy", "schoolhouse" })] + [FontAwesomeSearchTerms(new[] { "school circle xmark", "destroy", "schoolhouse", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Education", "Humanitarian" })] SchoolCircleXmark = 0xE56D, @@ -7173,63 +7273,63 @@ public enum FontAwesomeIcon /// /// The Font Awesome "school-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "school lock", "closed", "lockdown", "quarantine", "schoolhouse" })] + [FontAwesomeSearchTerms(new[] { "school lock", "closed", "lockdown", "padlock", "privacy", "quarantine", "schoolhouse" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Education", "Humanitarian" })] SchoolLock = 0xE56F, /// /// The Font Awesome "screwdriver" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "fix", "mechanic", "repair", "screw", "screwdriver", "settings", "tool" })] + [FontAwesomeSearchTerms(new[] { "admin", "configuration", "equipment", "fix", "maintenance", "mechanic", "modify", "repair", "screw", "screwdriver", "settings", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Construction" })] Screwdriver = 0xF54A, /// /// The Font Awesome "scroll" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "dungeons & dragons", "announcement", "d&d", "dnd", "fantasy", "paper", "script", "scroll" })] + [FontAwesomeSearchTerms(new[] { "dungeons & dragons", "announcement", "d&d", "dnd", "fantasy", "paper", "scholar", "script", "scroll" })] [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] Scroll = 0xF70E, /// /// The Font Awesome "sd-card" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sd card", "image", "memory", "photo", "save" })] + [FontAwesomeSearchTerms(new[] { "sd card", "image", "img", "memory", "photo", "save" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] SdCard = 0xF7C2, /// /// The Font Awesome "magnifying-glass" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass", "bigger", "enlarge", "find", "glass", "magnify", "magnifying", "magnifying glass tilted left", "preview", "search", "tool", "zoom" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass", "bigger", "enlarge", "equipment", "find", "glass", "inspection", "magnifier", "magnify", "magnifying", "magnifying glass tilted left", "preview", "search", "tool", "zoom" })] [FontAwesomeCategoriesAttribute(new[] { "Maps" })] Search = 0xF002, /// /// The Font Awesome "magnifying-glass-dollar" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass dollar", "bigger", "enlarge", "find", "magnify", "money", "preview", "zoom" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass dollar", "bigger", "enlarge", "find", "magnifier", "magnify", "money", "preview", "zoom" })] [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] SearchDollar = 0xF688, /// /// The Font Awesome "magnifying-glass-location" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass location", "bigger", "enlarge", "find", "magnify", "preview", "zoom" })] - [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass location", "bigger", "enlarge", "find", "magnifier", "magnify", "preview", "zoom" })] + [FontAwesomeCategoriesAttribute(new[] { "Maps", "Marketing" })] SearchLocation = 0xF689, /// /// The Font Awesome "magnifying-glass-minus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass minus", "minify", "negative", "smaller", "zoom", "zoom out" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass minus", "magnifier", "minify", "negative", "smaller", "zoom", "zoom out" })] [FontAwesomeCategoriesAttribute(new[] { "Maps" })] SearchMinus = 0xF010, /// /// The Font Awesome "magnifying-glass-plus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass plus", "bigger", "enlarge", "magnify", "positive", "zoom", "zoom in" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass plus", "bigger", "enlarge", "magnifier", "magnify", "positive", "zoom", "zoom in" })] [FontAwesomeCategoriesAttribute(new[] { "Maps" })] SearchPlus = 0xF00E, @@ -7243,14 +7343,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "seedling" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "environment", "flora", "grow", "plant", "sapling", "seedling", "vegan", "young" })] + [FontAwesomeSearchTerms(new[] { "environment", "flora", "grow", "investment", "plant", "sapling", "seedling", "vegan", "young" })] [FontAwesomeCategoriesAttribute(new[] { "Charity", "Energy", "Food + Beverage", "Fruits + Vegetables", "Humanitarian", "Nature", "Science" })] Seedling = 0xF4D8, + /// + /// The Font Awesome "septagon" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "septagon", "7", "heptagon", "seven", "shape" })] + [FontAwesomeCategoriesAttribute(new[] { "Shapes" })] + Septagon = 0xE820, + /// /// The Font Awesome "server" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "server", "computer", "cpu", "database", "hardware", "network" })] + [FontAwesomeSearchTerms(new[] { "server", "computer", "cpu", "database", "hardware", "mysql", "network", "sql" })] [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] Server = 0xF233, @@ -7313,7 +7420,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "shield-halved" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "shield halved", "achievement", "armor", "award", "block", "cleric", "defend", "defense", "holy", "paladin", "security", "shield", "weapon", "winner" })] + [FontAwesomeSearchTerms(new[] { "shield halved", "achievement", "armor", "award", "block", "cleric", "defend", "defense", "holy", "paladin", "privacy", "security", "shield", "weapon", "winner" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Gaming", "Security" })] ShieldAlt = 0xF3ED, @@ -7334,7 +7441,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "shield-heart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "shield heart", "love", "protect", "safe", "safety", "shield" })] + [FontAwesomeSearchTerms(new[] { "shield heart", "love", "protect", "safe", "safety", "shield", "wishlist" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security" })] ShieldHeart = 0xE574, @@ -7355,7 +7462,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "truck-fast" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "truck fast", "express", "fedex", "mail", "overnight", "package", "ups" })] + [FontAwesomeSearchTerms(new[] { "truck fast", "express", "fedex", "mail", "overnight", "package", "quick", "ups" })] [FontAwesomeCategoriesAttribute(new[] { "Logistics", "Shopping" })] ShippingFast = 0xF48B, @@ -7369,7 +7476,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "shop-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "shop lock", "bodega", "building", "buy", "closed", "lock", "lockdown", "market", "purchase", "quarantine", "shop", "shopping", "store" })] + [FontAwesomeSearchTerms(new[] { "shop lock", "bodega", "building", "buy", "closed", "lock", "lockdown", "market", "padlock", "privacy", "purchase", "quarantine", "shop", "shopping", "store" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Shopping" })] ShopLock = 0xE4A5, @@ -7397,7 +7504,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "shop-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "shop slash", "building", "buy", "closed", "covid-19", "purchase", "shopping" })] + [FontAwesomeSearchTerms(new[] { "shop slash", "building", "buy", "closed", "disabled", "purchase", "shopping" })] [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] ShopSlash = 0xE070, @@ -7418,7 +7525,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "van-shuttle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "van shuttle", "airport", "bus", "machine", "minibus", "public-transportation", "transportation", "travel", "vehicle" })] + [FontAwesomeSearchTerms(new[] { "van shuttle", "airport", "bus", "minibus", "public-transportation", "transportation", "travel", "vehicle" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Transportation", "Travel + Hotel" })] ShuttleVan = 0xF5B6, @@ -7439,7 +7546,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "signature" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "signature", "john hancock", "cursive", "name", "writing" })] + [FontAwesomeSearchTerms(new[] { "signature", "john hancock", "cursive", "name", "username", "writing" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Editing", "Writing" })] Signature = 0xF5B7, @@ -7471,6 +7578,20 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] SimCard = 0xF7C4, + /// + /// The Font Awesome "single-quote-left" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "single quote left", "left single quotation mark", "mention", "note", "phrase", "text", "type" })] + [FontAwesomeCategoriesAttribute(new[] { "Communication", "Punctuation + Symbols", "Writing" })] + SingleQuoteLeft = 0xE81B, + + /// + /// The Font Awesome "single-quote-right" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "single quote right", "mention", "note", "phrase", "right single quotation mark", "text", "type" })] + [FontAwesomeCategoriesAttribute(new[] { "Communication", "Punctuation + Symbols", "Writing" })] + SingleQuoteRight = 0xE81C, + /// /// The Font Awesome "sink" icon unicode character. /// @@ -7488,28 +7609,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-skating" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person skating", "figure skating", "ice", "olympics", "rink", "skate", "winter" })] + [FontAwesomeSearchTerms(new[] { "person skating", "figure skating", "ice", "olympics", "rink", "skate", "uer", "winter" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] Skating = 0xF7C5, /// /// The Font Awesome "person-skiing" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person skiing", "downhill", "olympics", "ski", "skier", "snow", "winter" })] + [FontAwesomeSearchTerms(new[] { "person skiing", "downhill", "olympics", "ski", "skier", "snow", "uer", "winter" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] Skiing = 0xF7C9, /// /// The Font Awesome "person-skiing-nordic" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person skiing nordic", "cross country", "olympics", "winter" })] + [FontAwesomeSearchTerms(new[] { "person skiing nordic", "cross country", "olympics", "uer", "winter" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] SkiingNordic = 0xF7CA, /// /// The Font Awesome "skull" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bones", "death", "face", "fairy tale", "monster", "skeleton", "skull", "x-ray", "yorick" })] + [FontAwesomeSearchTerms(new[] { "bones", "death", "face", "fairy tale", "monster", "skeleton", "skull", "uer", "x-ray", "yorick" })] [FontAwesomeCategoriesAttribute(new[] { "Halloween", "Medical + Health", "Users + People" })] Skull = 0xF54C, @@ -7537,14 +7658,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "sliders" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "adjust", "settings", "sliders", "toggle" })] - [FontAwesomeCategoriesAttribute(new[] { "Editing", "Media Playback", "Music + Audio", "Photos + Images" })] + [FontAwesomeSearchTerms(new[] { "adjust", "configuration", "modify", "settings", "sliders", "toggle" })] + [FontAwesomeCategoriesAttribute(new[] { "Editing", "Media Playback", "Music + Audio", "Photos + Images", "Toggle" })] SlidersH = 0xF1DE, /// /// The Font Awesome "face-smile" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "face smile", "approve", "emoticon", "face", "happy", "rating", "satisfied", "slightly smiling face", "smile" })] + [FontAwesomeSearchTerms(new[] { "face smile", "approve", "default", "emoticon", "face", "happy", "rating", "satisfied", "slightly smiling face", "smile", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Emoji", "Users + People" })] Smile = 0xF118, @@ -7579,21 +7700,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "ban-smoking" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ban smoking", "ban", "cancel", "forbidden", "no", "no smoking", "non-smoking", "not", "prohibited", "smoking" })] + [FontAwesomeSearchTerms(new[] { "ban smoking", "ban", "cancel", "circle", "deny", "disabled", "forbidden", "no", "no smoking", "non-smoking", "not", "prohibited", "slash", "smoking" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Travel + Hotel" })] SmokingBan = 0xF54D, /// /// The Font Awesome "comment-sms" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "comment sms", "chat", "conversation", "message", "mobile", "notification", "phone", "sms", "texting" })] + [FontAwesomeSearchTerms(new[] { "comment sms", "answer", "chat", "conversation", "message", "mobile", "notification", "phone", "sms", "texting" })] [FontAwesomeCategoriesAttribute(new[] { "Communication" })] Sms = 0xF7CD, /// /// The Font Awesome "person-snowboarding" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person snowboarding", "olympics", "ski", "snow", "snowboard", "snowboarder", "winter" })] + [FontAwesomeSearchTerms(new[] { "person snowboarding", "olympics", "ski", "snow", "snowboard", "snowboarder", "uer", "winter" })] [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] Snowboarding = 0xF7CE, @@ -7691,7 +7812,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrow-up-wide-short" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrow up wide short", "arrange", "filter", "order", "sort-amount-desc" })] + [FontAwesomeSearchTerms(new[] { "arrow up wide short", "arrange", "filter", "order", "sort-amount-desc", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] SortAmountUp = 0xF161, @@ -7705,7 +7826,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "sort-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sort down", "arrow", "descending", "filter", "order", "sort-desc" })] + [FontAwesomeSearchTerms(new[] { "sort down", "arrow", "descending", "filter", "insert", "order", "sort-desc" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] SortDown = 0xF0DD, @@ -7740,7 +7861,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "sort-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "sort up", "arrow", "ascending", "filter", "order", "sort-asc" })] + [FontAwesomeSearchTerms(new[] { "sort up", "arrow", "ascending", "filter", "order", "sort-asc", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] SortUp = 0xF0DE, @@ -7761,7 +7882,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "spell-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "spell check", "dictionary", "edit", "editor", "grammar", "text" })] + [FontAwesomeSearchTerms(new[] { "spell check", "dictionary", "edit", "editor", "enable", "grammar", "text", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] SpellCheck = 0xF891, @@ -7775,10 +7896,17 @@ public enum FontAwesomeIcon /// /// The Font Awesome "spinner" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "spinner", "circle", "loading", "progress" })] + [FontAwesomeSearchTerms(new[] { "spinner", "circle", "loading", "pending", "progress" })] [FontAwesomeCategoriesAttribute(new[] { "Spinners" })] Spinner = 0xF110, + /// + /// The Font Awesome "spiral" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "spiral", "design", "dizzy", "rotate", "spin", "swirl", "twist" })] + [FontAwesomeCategoriesAttribute(new[] { "Design", "Shapes" })] + Spiral = 0xE80A, + /// /// The Font Awesome "splotch" icon unicode character. /// @@ -7807,6 +7935,13 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] SquareArrowUpRight = 0xF14C, + /// + /// The Font Awesome "square-binary" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "square binary", "ai", "data", "language", "llm", "model", "programming", "token" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Shapes" })] + SquareBinary = 0xE69B, + /// /// The Font Awesome "square-full" icon unicode character. /// @@ -7824,7 +7959,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-person-confined" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square person confined", "captivity", "confined" })] + [FontAwesomeSearchTerms(new[] { "square person confined", "captivity", "confined", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Security", "Users + People" })] SquarePersonConfined = 0xE577, @@ -7845,7 +7980,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "square-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "square xmark", "close", "cross", "cross mark button", "incorrect", "mark", "notice", "notification", "notify", "problem", "square", "window", "wrong", "x", "×" })] + [FontAwesomeSearchTerms(new[] { "square xmark", "close", "cross", "cross mark button", "incorrect", "mark", "notice", "notification", "notify", "problem", "square", "uncheck", "window", "wrong", "x", "×" })] [FontAwesomeCategoriesAttribute(new[] { "Mathematics" })] SquareXmark = 0xF2D3, @@ -7880,7 +8015,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "star" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "achievement", "award", "favorite", "important", "night", "rating", "score", "star" })] + [FontAwesomeSearchTerms(new[] { "achievement", "award", "favorite", "important", "night", "quality", "rating", "score", "star", "vip" })] [FontAwesomeCategoriesAttribute(new[] { "Shapes", "Shopping", "Social", "Toggle" })] Star = 0xF005, @@ -7964,7 +8099,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "stopwatch" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "clock", "reminder", "stopwatch", "time" })] + [FontAwesomeSearchTerms(new[] { "clock", "reminder", "stopwatch", "time", "waiting" })] [FontAwesomeCategoriesAttribute(new[] { "Time" })] Stopwatch = 0xF2F2, @@ -7992,7 +8127,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "store-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "store slash", "building", "buy", "closed", "covid-19", "purchase", "shopping" })] + [FontAwesomeSearchTerms(new[] { "store slash", "building", "buy", "closed", "disabled", "purchase", "shopping" })] [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] StoreSlash = 0xE071, @@ -8006,14 +8141,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "street-view" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "street view", "directions", "location", "map", "navigation" })] + [FontAwesomeSearchTerms(new[] { "street view", "directions", "location", "map", "navigation", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Users + People" })] StreetView = 0xF21D, /// /// The Font Awesome "strikethrough" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "strikethrough", "cancel", "edit", "font", "format", "text", "type" })] + [FontAwesomeSearchTerms(new[] { "strikethrough", "cancel", "edit", "font", "format", "modify", "text", "type" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] Strikethrough = 0xF0CC, @@ -8090,7 +8225,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-swimming" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person swimming", "ocean", "person swimming", "pool", "sea", "swim", "water" })] + [FontAwesomeSearchTerms(new[] { "person swimming", "ocean", "person swimming", "pool", "sea", "swim", "uer", "water" })] [FontAwesomeCategoriesAttribute(new[] { "Maritime", "Sports + Fitness", "Travel + Hotel", "Users + People" })] Swimmer = 0xF5C4, @@ -8111,14 +8246,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "arrows-rotate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "arrows rotate", "clockwise right and left semicircle arrows", "exchange", "refresh", "reload", "rotate", "swap" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Editing", "Media Playback" })] + [FontAwesomeSearchTerms(new[] { "arrows rotate", "clockwise right and left semicircle arrows", "clockwise", "exchange", "modify", "refresh", "reload", "renew", "retry", "rotate", "swap" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Editing", "Media Playback", "Spinners" })] Sync = 0xF021, /// /// The Font Awesome "rotate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "anticlockwise", "arrow", "counterclockwise", "counterclockwise arrows button", "exchange", "refresh", "reload", "rotate", "swap", "withershins" })] + [FontAwesomeSearchTerms(new[] { "arrow", "clockwise", "exchange", "modify", "refresh", "reload", "renew", "retry", "rotate", "swap", "withershins" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Editing", "Media Playback", "Spinners" })] SyncAlt = 0xF2F1, @@ -8132,10 +8267,31 @@ public enum FontAwesomeIcon /// /// The Font Awesome "table" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table", "data", "excel", "spreadsheet" })] + [FontAwesomeSearchTerms(new[] { "table", "category", "data", "excel", "spreadsheet" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Text Formatting" })] Table = 0xF0CE, + /// + /// The Font Awesome "table-cells-column-lock" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "table cells column lock", "blocks", "boxes", "category", "column", "excel", "grid", "lock", "spreadsheet", "squares" })] + [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] + TableCellsColumnLock = 0xE678, + + /// + /// The Font Awesome "table-cells-row-lock" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "table cells row lock", "blocks", "boxes", "category", "column", "column", "excel", "grid", "lock", "lock", "spreadsheet", "squares" })] + [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] + TableCellsRowLock = 0xE67A, + + /// + /// The Font Awesome "table-cells-row-unlock" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "table cells row unlock", "blocks", "boxes", "category", "column", "column", "excel", "grid", "lock", "lock", "spreadsheet", "squares", "unlock" })] + [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] + TableCellsRowUnlock = 0xE691, + /// /// The Font Awesome "tablet" icon unicode character. /// @@ -8175,7 +8331,7 @@ public enum FontAwesomeIcon /// The Font Awesome "gauge-high" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF625. /// - [FontAwesomeSearchTerms(new[] { "gauge high", "dashboard", "fast", "odometer", "speed", "speedometer" })] + [FontAwesomeSearchTerms(new[] { "gauge high", "dashboard", "fast", "odometer", "quick", "speed", "speedometer" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive" })] TachometerAlt = 0xF3FD, @@ -8217,7 +8373,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "list-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "list check", "checklist", "downloading", "downloads", "loading", "progress", "project management", "settings", "to do" })] + [FontAwesomeSearchTerms(new[] { "list check", "bullet", "cheatsheet", "checklist", "downloading", "downloads", "enable", "loading", "progress", "project management", "settings", "summary", "to do", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Text Formatting" })] Tasks = 0xF0AE, @@ -8280,42 +8436,42 @@ public enum FontAwesomeIcon /// /// The Font Awesome "tent" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bivouac", "campground", "refugee", "shelter", "tent" })] + [FontAwesomeSearchTerms(new[] { "bivouac", "campground", "campsite", "refugee", "shelter", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] Tent = 0xE57D, /// /// The Font Awesome "tent-arrow-down-to-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tent arrow down to line", "permanent", "refugee", "shelter" })] + [FontAwesomeSearchTerms(new[] { "tent arrow down to line", "bivouac", "campground", "campsite", "permanent", "refugee", "refugee", "shelter", "shelter", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] TentArrowDownToLine = 0xE57E, /// /// The Font Awesome "tent-arrow-left-right" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tent arrow left right", "refugee", "shelter", "transition" })] + [FontAwesomeSearchTerms(new[] { "tent arrow left right", "bivouac", "campground", "campsite", "refugee", "refugee", "shelter", "shelter", "tent", "transition" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] TentArrowLeftRight = 0xE57F, /// /// The Font Awesome "tent-arrows-down" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tent arrows down", "refugee", "shelter", "spontaneous" })] + [FontAwesomeSearchTerms(new[] { "tent arrows down", "bivouac", "campground", "campsite", "insert", "refugee", "refugee", "shelter", "shelter", "spontaneous", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] TentArrowsDown = 0xE581, /// /// The Font Awesome "tent-arrow-turn-left" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tent arrow turn left", "refugee", "shelter", "temporary" })] + [FontAwesomeSearchTerms(new[] { "tent arrow turn left", "bivouac", "campground", "campsite", "refugee", "refugee", "shelter", "shelter", "temporary", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] TentArrowTurnLeft = 0xE580, /// /// The Font Awesome "tents" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tents", "bivouac", "campground", "refugee", "shelter", "tent" })] + [FontAwesomeSearchTerms(new[] { "tents", "bivouac", "bivouac", "campground", "campground", "campsite", "refugee", "refugee", "shelter", "shelter", "tent", "tent" })] [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping", "Humanitarian" })] Tents = 0xE582, @@ -8329,21 +8485,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "text-height" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "text height", "edit", "font", "format", "text", "type" })] + [FontAwesomeSearchTerms(new[] { "text height", "edit", "font", "format", "modify", "text", "type" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] TextHeight = 0xF034, /// /// The Font Awesome "text-width" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "text width", "edit", "font", "format", "text", "type" })] + [FontAwesomeSearchTerms(new[] { "text width", "edit", "font", "format", "modify", "text", "type" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] TextWidth = 0xF035, /// /// The Font Awesome "table-cells" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table cells", "blocks", "boxes", "grid", "squares" })] + [FontAwesomeSearchTerms(new[] { "table cells", "blocks", "boxes", "category", "excel", "grid", "spreadsheet", "squares" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] Th = 0xF00A, @@ -8399,14 +8555,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "table-cells-large" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table cells large", "blocks", "boxes", "grid", "squares" })] + [FontAwesomeSearchTerms(new[] { "table cells large", "blocks", "boxes", "category", "excel", "grid", "spreadsheet", "squares" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ThLarge = 0xF009, /// /// The Font Awesome "table-list" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "table list", "checklist", "completed", "done", "finished", "ol", "todo", "ul" })] + [FontAwesomeSearchTerms(new[] { "table list", "category", "cheatsheet", "checklist", "completed", "done", "finished", "ol", "summary", "todo", "ul" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] ThList = 0xF00B, @@ -8431,17 +8587,24 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Maps", "Social", "Writing" })] Thumbtack = 0xF08D, + /// + /// The Font Awesome "thumbtack-slash" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "thumbtack slash", "black pushpin", "coordinates", "location", "marker", "pin", "pushpin", "thumb-tack", "unpin" })] + [FontAwesomeCategoriesAttribute(new[] { "Business", "Maps", "Social", "Writing" })] + ThumbtackSlash = 0xE68F, + /// /// The Font Awesome "ticket" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admission", "admission tickets", "movie", "pass", "support", "ticket" })] + [FontAwesomeSearchTerms(new[] { "admission", "admission tickets", "coupon", "movie", "pass", "support", "ticket", "voucher" })] [FontAwesomeCategoriesAttribute(new[] { "Film + Video", "Maps" })] Ticket = 0xF145, /// /// The Font Awesome "ticket-simple" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "ticket simple", "movie", "pass", "support", "ticket" })] + [FontAwesomeSearchTerms(new[] { "ticket simple", "admission", "coupon", "movie", "pass", "support", "ticket", "voucher" })] [FontAwesomeCategoriesAttribute(new[] { "Maps", "Shapes" })] TicketAlt = 0xF3FF, @@ -8455,29 +8618,29 @@ public enum FontAwesomeIcon /// /// The Font Awesome "xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "xmark", "cancellation x", "multiplication sign", "multiplication x", "cancel", "close", "cross", "cross mark", "error", "exit", "incorrect", "mark", "multiplication", "multiply", "notice", "notification", "notify", "problem", "sign", "wrong", "x", "×" })] + [FontAwesomeSearchTerms(new[] { "xmark", "cancellation x", "multiplication sign", "multiplication x", "cancel", "close", "cross", "cross mark", "error", "exit", "incorrect", "mark", "multiplication", "multiply", "notice", "notification", "notify", "problem", "sign", "uncheck", "wrong", "x", "×" })] [FontAwesomeCategoriesAttribute(new[] { "Editing", "Mathematics" })] Times = 0xF00D, /// /// The Font Awesome "circle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle xmark", "close", "cross", "destroy", "exit", "incorrect", "notice", "notification", "notify", "problem", "wrong", "x" })] + [FontAwesomeSearchTerms(new[] { "circle xmark", "close", "cross", "destroy", "exit", "incorrect", "notice", "notification", "notify", "problem", "uncheck", "wrong", "x" })] [FontAwesomeCategoriesAttribute(new[] { "Mathematics" })] TimesCircle = 0xF057, /// /// The Font Awesome "droplet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "cold", "color", "comic", "drop", "droplet", "raindrop", "sweat", "waterdrop" })] - [FontAwesomeCategoriesAttribute(new[] { "Design", "Humanitarian", "Maps", "Photos + Images" })] + [FontAwesomeSearchTerms(new[] { "blood", "cold", "color", "comic", "drop", "droplet", "raindrop", "sweat", "waterdrop" })] + [FontAwesomeCategoriesAttribute(new[] { "Design", "Humanitarian", "Maps", "Medical + Health", "Photos + Images" })] Tint = 0xF043, /// /// The Font Awesome "droplet-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "droplet slash", "color", "drop", "droplet", "raindrop", "waterdrop" })] - [FontAwesomeCategoriesAttribute(new[] { "Design" })] + [FontAwesomeSearchTerms(new[] { "droplet slash", "blood", "color", "disabled", "drop", "droplet", "raindrop", "waterdrop" })] + [FontAwesomeCategoriesAttribute(new[] { "Design", "Medical + Health" })] TintSlash = 0xF5C7, /// @@ -8518,7 +8681,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "toilet-paper-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "toilet paper slash", "bathroom", "covid-19", "halloween", "holiday", "lavatory", "leaves", "prank", "privy", "restroom", "roll", "toilet", "trouble", "ut oh", "wipe" })] + [FontAwesomeSearchTerms(new[] { "toilet paper slash", "bathroom", "covid-19", "disabled", "halloween", "holiday", "lavatory", "leaves", "prank", "privy", "restroom", "roll", "toilet", "trouble", "ut oh", "wipe" })] [FontAwesomeCategoriesAttribute(new[] { "Household" })] ToiletPaperSlash = 0xE072, @@ -8539,14 +8702,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "toolbox" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "chest", "container", "fix", "mechanic", "repair", "settings", "tool", "toolbox", "tools" })] + [FontAwesomeSearchTerms(new[] { "admin", "chest", "configuration", "container", "equipment", "fix", "maintenance", "mechanic", "modify", "repair", "settings", "tool", "toolbox", "tools" })] [FontAwesomeCategoriesAttribute(new[] { "Construction" })] Toolbox = 0xF552, /// /// The Font Awesome "screwdriver-wrench" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "screwdriver wrench", "admin", "fix", "repair", "screwdriver", "settings", "tools", "wrench" })] + [FontAwesomeSearchTerms(new[] { "screwdriver wrench", "admin", "configuration", "equipment", "fix", "maintenance", "modify", "repair", "screwdriver", "settings", "tools", "wrench" })] [FontAwesomeCategoriesAttribute(new[] { "Construction" })] Tools = 0xF7D9, @@ -8581,7 +8744,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "tower-cell" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "tower cell", "airwaves", "antenna", "communication", "radio", "reception", "waves" })] + [FontAwesomeSearchTerms(new[] { "tower cell", "airwaves", "antenna", "communication", "radio", "reception", "signal", "waves" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Connectivity", "Film + Video", "Humanitarian" })] TowerCell = 0xE585, @@ -8609,7 +8772,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "traffic-light" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "traffic light", "direction", "light", "road", "signal", "traffic", "travel", "vertical traffic light" })] + [FontAwesomeSearchTerms(new[] { "traffic light", "direction", "go", "light", "road", "signal", "slow", "stop", "traffic", "travel", "vertical traffic light" })] [FontAwesomeCategoriesAttribute(new[] { "Maps" })] TrafficLight = 0xF637, @@ -8665,21 +8828,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "trash-arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "trash arrow up", "back", "control z", "delete", "garbage", "hide", "oops", "remove", "undo" })] + [FontAwesomeSearchTerms(new[] { "trash arrow up", "back", "control z", "delete", "garbage", "hide", "oops", "remove", "undo", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] TrashRestore = 0xF829, /// /// The Font Awesome "trash-can-arrow-up" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "trash can arrow up", "back", "control z", "delete", "garbage", "hide", "oops", "remove", "undo" })] + [FontAwesomeSearchTerms(new[] { "trash can arrow up", "back", "control z", "delete", "garbage", "hide", "oops", "remove", "undo", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] TrashRestoreAlt = 0xF82A, /// /// The Font Awesome "tree" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bark", "evergreen tree", "fall", "flora", "forest", "nature", "plant", "seasonal", "tree" })] + [FontAwesomeSearchTerms(new[] { "bark", "evergreen tree", "fall", "flora", "forest", "investment", "nature", "plant", "seasonal", "tree" })] [FontAwesomeCategoriesAttribute(new[] { "Camping", "Maps", "Nature" })] Tree = 0xF1BB, @@ -8700,14 +8863,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "trowel" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "trowel", "build", "construction", "tool" })] + [FontAwesomeSearchTerms(new[] { "trowel", "build", "construction", "equipment", "maintenance", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Humanitarian" })] Trowel = 0xE589, /// /// The Font Awesome "trowel-bricks" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "trowel bricks", "build", "construction", "reconstruction", "tool" })] + [FontAwesomeSearchTerms(new[] { "trowel bricks", "build", "construction", "maintenance", "reconstruction", "tool" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Humanitarian" })] TrowelBricks = 0xE58A, @@ -8728,8 +8891,8 @@ public enum FontAwesomeIcon /// /// The Font Awesome "truck-droplet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "truck droplet", "thirst", "truck", "water", "water supply" })] - [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Transportation" })] + [FontAwesomeSearchTerms(new[] { "truck droplet", "blood", "thirst", "truck", "water", "water supply" })] + [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Transportation" })] TruckDroplet = 0xE58C, /// @@ -8777,7 +8940,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "truck-pickup" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "truck pickup", "cargo", "pick-up", "pickup", "pickup truck", "truck", "vehicle" })] + [FontAwesomeSearchTerms(new[] { "truck pickup", "cargo", "maintenance", "pick-up", "pickup", "pickup truck", "truck", "vehicle" })] [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Construction", "Transportation" })] TruckPickup = 0xF63C, @@ -8833,7 +8996,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "underline" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "underline", "edit", "emphasis", "format", "text", "writing" })] + [FontAwesomeSearchTerms(new[] { "underline", "edit", "emphasis", "format", "modify", "text", "writing" })] [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] Underline = 0xF0CD, @@ -8841,20 +9004,20 @@ public enum FontAwesomeIcon /// The Font Awesome "arrow-rotate-left" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "arrow rotate left", "anticlockwise open circle arrow", "back", "control z", "exchange", "oops", "return", "rotate", "swap" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback", "Spinners" })] Undo = 0xF0E2, /// /// The Font Awesome "rotate-left" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "rotate left", "back", "control z", "exchange", "oops", "return", "swap" })] - [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] + [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback", "Spinners" })] UndoAlt = 0xF2EA, /// /// The Font Awesome "universal-access" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "universal access", "users-people" })] + [FontAwesomeSearchTerms(new[] { "universal access", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility" })] UniversalAccess = 0xF29A, @@ -8868,252 +9031,254 @@ public enum FontAwesomeIcon /// /// The Font Awesome "link-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "link slash", "attachment", "chain", "chain-broken", "remove" })] + [FontAwesomeSearchTerms(new[] { "link slash", "attachment", "chain", "chain-broken", "disabled", "disconnect", "remove" })] [FontAwesomeCategoriesAttribute(new[] { "Editing" })] Unlink = 0xF127, /// /// The Font Awesome "unlock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "admin", "lock", "open", "password", "private", "protect", "unlock", "unlocked" })] + [FontAwesomeSearchTerms(new[] { "admin", "lock", "open", "padlock", "password", "privacy", "private", "protect", "unlock", "unlocked" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] Unlock = 0xF09C, /// /// The Font Awesome "unlock-keyhole" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "unlock keyhole", "admin", "lock", "password", "private", "protect" })] + [FontAwesomeSearchTerms(new[] { "unlock keyhole", "admin", "lock", "padlock", "password", "privacy", "private", "protect" })] [FontAwesomeCategoriesAttribute(new[] { "Security" })] UnlockAlt = 0xF13E, /// /// The Font Awesome "upload" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "upload", "hard drive", "import", "publish" })] + [FontAwesomeSearchTerms(new[] { "upload", "hard drive", "import", "publish", "upgrade" })] [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Devices + Hardware" })] Upload = 0xF093, /// /// The Font Awesome "user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user", "adult", "bust", "bust in silhouette", "gender-neutral", "person", "profile", "silhouette", "unspecified gender", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user", "adult", "bust", "bust in silhouette", "default", "employee", "gender-neutral", "person", "profile", "silhouette", "uer", "unspecified gender", "username", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] User = 0xF007, /// - /// The Font Awesome "user-large" icon unicode character. + /// The Font Awesome "user" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF007. /// - [FontAwesomeSearchTerms(new[] { "user large", "users-people" })] - [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] + [FontAwesomeSearchTerms(new[] { "user", "adult", "bust", "bust in silhouette", "default", "employee", "gender-neutral", "person", "profile", "silhouette", "uer", "unspecified gender", "username", "users-people" })] + [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] UserAlt = 0xF406, /// - /// The Font Awesome "user-large-slash" icon unicode character. + /// The Font Awesome "user-slash" icon unicode character. + /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF506. /// - [FontAwesomeSearchTerms(new[] { "user large slash", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user slash", "ban", "delete", "deny", "disabled", "disconnect", "employee", "remove", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserAltSlash = 0xF4FA, /// /// The Font Awesome "user-astronaut" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user astronaut", "avatar", "clothing", "cosmonaut", "nasa", "space", "suit" })] + [FontAwesomeSearchTerms(new[] { "user astronaut", "avatar", "clothing", "cosmonaut", "nasa", "space", "suit", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Science Fiction", "Users + People" })] UserAstronaut = 0xF4FB, /// /// The Font Awesome "user-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user check", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user check", "employee", "enable", "uer", "users-people", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserCheck = 0xF4FC, /// /// The Font Awesome "circle-user" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "circle user", "users-people" })] + [FontAwesomeSearchTerms(new[] { "circle user", "employee", "uer", "username", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] UserCircle = 0xF2BD, /// /// The Font Awesome "user-clock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user clock", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user clock", "employee", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserClock = 0xF4FD, /// /// The Font Awesome "user-gear" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user gear", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user gear", "employee", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserCog = 0xF4FE, /// /// The Font Awesome "user-pen" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user pen", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user pen", "employee", "modify", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserEdit = 0xF4FF, /// /// The Font Awesome "user-group" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user group", "bust", "busts in silhouette", "silhouette", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user group", "bust", "busts in silhouette", "crowd", "employee", "silhouette", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] UserFriends = 0xF500, /// /// The Font Awesome "user-graduate" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user graduate", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user graduate", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Education", "Users + People" })] UserGraduate = 0xF501, /// /// The Font Awesome "user-injured" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user injured", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user injured", "employee", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UserInjured = 0xF728, /// /// The Font Awesome "user-lock" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user lock", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user lock", "employee", "padlock", "privacy", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Security", "Users + People" })] UserLock = 0xF502, /// /// The Font Awesome "user-doctor" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user doctor", "covid-19", "health", "job", "medical", "nurse", "occupation", "physician", "profile", "surgeon", "worker" })] + [FontAwesomeSearchTerms(new[] { "user doctor", "covid-19", "health", "job", "medical", "nurse", "occupation", "physician", "profile", "surgeon", "uer", "worker" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Users + People" })] UserMd = 0xF0F0, /// /// The Font Awesome "user-minus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user minus", "delete", "negative", "remove" })] + [FontAwesomeSearchTerms(new[] { "user minus", "delete", "employee", "negative", "remove", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserMinus = 0xF503, /// /// The Font Awesome "user-ninja" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user ninja", "assassin", "avatar", "dangerous", "deadly", "fighter", "hidden", "ninja", "sneaky", "stealth" })] + [FontAwesomeSearchTerms(new[] { "user ninja", "assassin", "avatar", "dangerous", "deadly", "fighter", "hidden", "ninja", "sneaky", "stealth", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserNinja = 0xF504, /// /// The Font Awesome "user-nurse" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user nurse", "covid-19", "doctor", "health", "md", "medical", "midwife", "physician", "practitioner", "surgeon", "worker" })] + [FontAwesomeSearchTerms(new[] { "user nurse", "covid-19", "doctor", "health", "md", "medical", "midwife", "physician", "practitioner", "surgeon", "uer", "worker" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Users + People" })] UserNurse = 0xF82F, /// /// The Font Awesome "user-plus" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user plus", "add", "avatar", "positive", "sign up", "signup", "team" })] + [FontAwesomeSearchTerms(new[] { "user plus", "add", "avatar", "employee", "follow", "positive", "sign up", "signup", "team", "user" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] UserPlus = 0xF234, /// /// The Font Awesome "users" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users", "users-people" })] + [FontAwesomeSearchTerms(new[] { "users", "employee", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Social", "Users + People" })] Users = 0xF0C0, /// /// The Font Awesome "users-between-lines" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users between lines", "covered", "group", "people" })] + [FontAwesomeSearchTerms(new[] { "users between lines", "covered", "crowd", "employee", "group", "people", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersBetweenLines = 0xE591, /// /// The Font Awesome "users-gear" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users gear", "users-people" })] + [FontAwesomeSearchTerms(new[] { "users gear", "employee", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UsersCog = 0xF509, /// /// The Font Awesome "user-secret" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user secret", "detective", "sleuth", "spy", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user secret", "detective", "sleuth", "spy", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Coding", "Security", "Users + People" })] UserSecret = 0xF21B, /// /// The Font Awesome "user-shield" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user shield", "protect", "safety" })] + [FontAwesomeSearchTerms(new[] { "user shield", "employee", "protect", "safety", "security", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Security", "Users + People" })] UserShield = 0xF505, /// /// The Font Awesome "user-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user slash", "ban", "delete", "remove" })] + [FontAwesomeSearchTerms(new[] { "user slash", "ban", "delete", "deny", "disabled", "disconnect", "employee", "remove", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserSlash = 0xF506, /// /// The Font Awesome "users-line" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users line", "group", "need", "people" })] + [FontAwesomeSearchTerms(new[] { "users line", "crowd", "employee", "group", "need", "people", "together", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersLine = 0xE592, /// /// The Font Awesome "users-rays" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users rays", "affected", "focused", "group", "people" })] + [FontAwesomeSearchTerms(new[] { "users rays", "affected", "crowd", "employee", "focused", "group", "people", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersRays = 0xE593, /// /// The Font Awesome "users-rectangle" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users rectangle", "focus", "group", "people", "reached" })] + [FontAwesomeSearchTerms(new[] { "users rectangle", "crowd", "employee", "focus", "group", "people", "reached", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersRectangle = 0xE594, /// /// The Font Awesome "users-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users slash", "users-people" })] + [FontAwesomeSearchTerms(new[] { "users slash", "disabled", "disconnect", "employee", "together", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UsersSlash = 0xE073, /// /// The Font Awesome "users-viewfinder" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "users viewfinder", "focus", "group", "people", "targeted" })] + [FontAwesomeSearchTerms(new[] { "users viewfinder", "crowd", "focus", "group", "people", "targeted", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Users + People" })] UsersViewfinder = 0xE595, /// /// The Font Awesome "user-tag" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user tag", "users-people" })] + [FontAwesomeSearchTerms(new[] { "user tag", "employee", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserTag = 0xF507, /// /// The Font Awesome "user-tie" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user tie", "avatar", "business", "clothing", "formal", "professional", "suit" })] + [FontAwesomeSearchTerms(new[] { "user tie", "administrator", "avatar", "business", "clothing", "employee", "formal", "offer", "portfolio", "professional", "suit", "uer" })] [FontAwesomeCategoriesAttribute(new[] { "Clothing + Fashion", "Users + People" })] UserTie = 0xF508, /// /// The Font Awesome "user-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "user xmark", "archive", "delete", "remove", "x" })] + [FontAwesomeSearchTerms(new[] { "user xmark", "archive", "delete", "employee", "remove", "uer", "uncheck", "x" })] [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserTimes = 0xF235, @@ -9134,15 +9299,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "vault" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vault", "bank", "important", "lock", "money", "safe" })] + [FontAwesomeSearchTerms(new[] { "vault", "bank", "important", "investment", "lock", "money", "premium", "privacy", "safe", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Money", "Security" })] Vault = 0xE2C5, /// - /// The Font Awesome "vector-square" icon unicode character. + /// The Font Awesome "vectorsquare" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vector square", "anchors", "lines", "object", "render", "shape" })] - [FontAwesomeCategoriesAttribute(new[] { "Design" })] + [Obsolete] VectorSquare = 0xF5CB, /// @@ -9183,21 +9347,21 @@ public enum FontAwesomeIcon /// /// The Font Awesome "vial" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vial", "ampule", "chemist", "chemistry", "experiment", "lab", "sample", "science", "test", "test tube" })] + [FontAwesomeSearchTerms(new[] { "vial", "ampule", "chemist", "chemistry", "experiment", "knowledge", "lab", "sample", "science", "test", "test tube" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Science" })] Vial = 0xF492, /// /// The Font Awesome "vial-circle-check" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vial circle check", "ampule", "chemist", "chemistry", "not affected", "ok", "okay", "success", "test tube", "tube", "vaccine" })] + [FontAwesomeSearchTerms(new[] { "vial circle check", "ampule", "chemist", "chemistry", "enable", "not affected", "ok", "okay", "success", "test tube", "tube", "vaccine", "validate", "working" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Science" })] VialCircleCheck = 0xE596, /// /// The Font Awesome "vials" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "vials", "ampule", "experiment", "lab", "sample", "science", "test", "test tube" })] + [FontAwesomeSearchTerms(new[] { "vials", "ampule", "experiment", "knowledge", "lab", "sample", "science", "test", "test tube" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Science" })] Vials = 0xF493, @@ -9218,7 +9382,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "video-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "video slash", "add", "create", "film", "new", "positive", "record", "video" })] + [FontAwesomeSearchTerms(new[] { "video slash", "add", "create", "disabled", "disconnect", "film", "new", "positive", "record", "video" })] [FontAwesomeCategoriesAttribute(new[] { "Communication", "Film + Video" })] VideoSlash = 0xF4E2, @@ -9246,7 +9410,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "virus-covid-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "virus covid slash", "bug", "covid-19", "flu", "health", "infection", "pandemic", "vaccine", "viral", "virus" })] + [FontAwesomeSearchTerms(new[] { "virus covid slash", "bug", "covid-19", "disabled", "flu", "health", "infection", "pandemic", "vaccine", "viral", "virus" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] VirusCovidSlash = 0xE4A9, @@ -9260,7 +9424,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "virus-slash" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "virus slash", "bug", "coronavirus", "covid-19", "cure", "eliminate", "flu", "health", "infection", "pandemic", "sick", "vaccine", "viral" })] + [FontAwesomeSearchTerms(new[] { "virus slash", "bug", "coronavirus", "covid-19", "cure", "disabled", "eliminate", "flu", "health", "infection", "pandemic", "sick", "vaccine", "viral" })] [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] VirusSlash = 0xE075, @@ -9316,7 +9480,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "check-to-slot" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "check to slot", "accept", "cast", "election", "politics", "positive", "voting", "yes" })] + [FontAwesomeSearchTerms(new[] { "check to slot", "accept", "cast", "election", "enable", "politics", "positive", "validate", "voting", "working", "yes" })] [FontAwesomeCategoriesAttribute(new[] { "Political" })] VoteYea = 0xF772, @@ -9337,14 +9501,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "person-walking" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "person walking", "crosswalk", "exercise", "hike", "move", "person walking", "walk", "walking" })] + [FontAwesomeSearchTerms(new[] { "person walking", "crosswalk", "exercise", "follow", "hike", "move", "person walking", "uer", "walk", "walking", "workout" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Sports + Fitness", "Users + People" })] Walking = 0xF554, /// /// The Font Awesome "wallet" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wallet", "billfold", "cash", "currency", "money" })] + [FontAwesomeSearchTerms(new[] { "wallet", "billfold", "cash", "currency", "money", "salary" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Money" })] Wallet = 0xF555, @@ -9380,9 +9544,16 @@ public enum FontAwesomeIcon /// The Font Awesome "wave-square" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "wave square", "frequency", "pulse", "signal" })] - [FontAwesomeCategoriesAttribute(new[] { "Mathematics" })] + [FontAwesomeCategoriesAttribute(new[] { "Mathematics", "Music + Audio" })] WaveSquare = 0xF83E, + /// + /// The Font Awesome "web-awesome" icon unicode character. + /// + [FontAwesomeSearchTerms(new[] { "web awesome", "awesome", "coding", "components", "crown", "web" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Design" })] + WebAwesome = 0xE682, + /// /// The Font Awesome "weight-scale" icon unicode character. /// @@ -9407,28 +9578,28 @@ public enum FontAwesomeIcon /// /// The Font Awesome "wheat-awn-circle-exclamation" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wheat awn circle exclamation", "affected", "famine", "food", "gluten", "hunger", "starve", "straw" })] + [FontAwesomeSearchTerms(new[] { "wheat awn circle exclamation", "affected", "failed", "famine", "food", "gluten", "hunger", "starve", "straw" })] [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Food + Beverage", "Humanitarian" })] WheatAwnCircleExclamation = 0xE598, /// /// The Font Awesome "wheelchair" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wheelchair", "users-people" })] + [FontAwesomeSearchTerms(new[] { "wheelchair", "disabled", "uer", "users-people" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Maps", "Medical + Health", "Transportation", "Travel + Hotel", "Users + People" })] Wheelchair = 0xF193, /// /// The Font Awesome "wheelchair-move" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wheelchair move", "access", "handicap", "impairment", "physical", "wheelchair symbol" })] + [FontAwesomeSearchTerms(new[] { "wheelchair move", "access", "disabled", "handicap", "impairment", "physical", "uer", "wheelchair symbol" })] [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Humanitarian", "Maps", "Medical + Health", "Transportation", "Travel + Hotel", "Users + People" })] WheelchairMove = 0xE2CE, /// /// The Font Awesome "wifi" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "wifi", "connection", "hotspot", "internet", "network", "wireless" })] + [FontAwesomeSearchTerms(new[] { "wifi", "connection", "hotspot", "internet", "network", "signal", "wireless", "www" })] [FontAwesomeCategoriesAttribute(new[] { "Connectivity", "Humanitarian", "Maps", "Toggle", "Travel + Hotel" })] Wifi = 0xF1EB, @@ -9442,7 +9613,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "rectangle-xmark" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "rectangle xmark", "browser", "cancel", "computer", "development" })] + [FontAwesomeSearchTerms(new[] { "rectangle xmark", "browser", "cancel", "computer", "development", "uncheck" })] [FontAwesomeCategoriesAttribute(new[] { "Coding" })] WindowClose = 0xF410, @@ -9505,7 +9676,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "wrench" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "construction", "fix", "mechanic", "plumbing", "settings", "spanner", "tool", "update", "wrench" })] + [FontAwesomeSearchTerms(new[] { "configuration", "construction", "equipment", "fix", "mechanic", "modify", "plumbing", "settings", "spanner", "tool", "update", "wrench" })] [FontAwesomeCategoriesAttribute(new[] { "Construction", "Maps" })] Wrench = 0xF0AD, From 624191d1e03a052e5eafe56bd921f1f0623e3969 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 7 Dec 2025 16:45:40 +0100 Subject: [PATCH 085/164] Update DalamudAssetPath to FontAwesome710FreeSolid.otf --- Dalamud/DalamudAsset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index 27771116e..e234fbb4c 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -151,7 +151,7 @@ public enum DalamudAsset /// : FontAwesome Free Solid. /// [DalamudAsset(DalamudAssetPurpose.Font)] - [DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")] + [DalamudAssetPath("UIRes", "FontAwesome710FreeSolid.otf")] FontAwesomeFreeSolid = 2003, /// From 2029a0f8a69e0d10b32b8795ce180ebf3c6eb8e5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 21:31:25 +0100 Subject: [PATCH 086/164] Also add fallback for SeStringDrawState.ScreenOffset for now, make sure that it is populated --- .../SeStringDrawParams.cs | 4 +++- .../ImGuiSeStringRenderer/SeStringDrawState.cs | 3 ++- .../Data/Widgets/SeStringRendererTestWidget.cs | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs index 972013328..09c3e9ed9 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawParams.cs @@ -25,7 +25,9 @@ public record struct SeStringDrawParams public SeStringReplacementEntity.GetEntityDelegate? GetEntity { get; set; } /// Gets or sets the screen offset of the left top corner. - /// Screen offset to draw at, or null to use . + /// Screen offset to draw at, or null to use , if no + /// is specified. Otherwise, you must specify it (for example, by passing when passing the window + /// draw list. public Vector2? ScreenOffset { get; set; } /// Gets or sets the font to use. diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 5e63ef160..5601100e9 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -63,11 +63,12 @@ public unsafe ref struct SeStringDrawState else { this.drawList = ssdp.TargetDrawList.Value; - this.ScreenOffset = Vector2.Zero; + this.ScreenOffset = ssdp.ScreenOffset ?? Vector2.Zero; // API14: Remove, always throw if (ThreadSafety.IsMainThread) { + this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos(); this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); } else diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs index 0f51e0322..6a07152e5 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringRendererTestWidget.cs @@ -177,6 +177,24 @@ internal unsafe class SeStringRendererTestWidget : IDataWindowWidget ImGuiHelpers.SeStringWrapped(this.logkind.Value.Data.Span, this.style); } + if (ImGui.CollapsingHeader("Draw into drawlist")) + { + ImGuiHelpers.ScaledDummy(100); + ImGui.SetCursorScreenPos(ImGui.GetItemRectMin() + ImGui.GetStyle().FramePadding); + var clipMin = ImGui.GetItemRectMin() + ImGui.GetStyle().FramePadding; + var clipMax = ImGui.GetItemRectMax() - ImGui.GetStyle().FramePadding; + clipMin.Y = MathF.Max(clipMin.Y, ImGui.GetWindowPos().Y); + clipMax.Y = MathF.Min(clipMax.Y, ImGui.GetWindowPos().Y + ImGui.GetWindowHeight()); + + var dl = ImGui.GetWindowDrawList(); + dl.PushClipRect(clipMin, clipMax); + ImGuiHelpers.CompileSeStringWrapped( + "Test test", + new SeStringDrawParams + { Color = 0xFFFFFFFF, WrapWidth = float.MaxValue, TargetDrawList = dl }); + dl.PopClipRect(); + } + if (ImGui.CollapsingHeader("Addon Table"u8)) { if (ImGui.BeginTable("Addon Sheet"u8, 3)) From c45c6aafe1a8a8561826155aae0efc46b478a2d8 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 21:57:54 +0100 Subject: [PATCH 087/164] Don't consider failed index integrity checks as having "modified game data files" --- Dalamud/Data/DataManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index ed0aa6c4d..559bd84dc 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -82,8 +82,10 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager var tsInfo = JsonConvert.DeserializeObject( dalamud.StartInfo.TroubleshootingPackData); + + // Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout this.HasModifiedGameDataFiles = - tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception; + tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed; if (this.HasModifiedGameDataFiles) Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData); From 8ed1af30dfa33c892fa1b0446054e512a4d0a760 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 7 Dec 2025 22:55:16 +0100 Subject: [PATCH 088/164] build: 13.0.0.14 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a50f12d79..a4b203406 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.13 + 13.0.0.14 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 2dbae055226d1dfb85662b2d68df92926a6ea343 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Sun, 7 Dec 2025 16:45:59 -0800 Subject: [PATCH 089/164] Add very thurough exception handling --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 498 +++++++++++------- 1 file changed, 318 insertions(+), 180 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 8fbf77534..47ff92c3d 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -136,14 +136,30 @@ internal unsafe class AddonVirtualTable : IDisposable private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) { - this.LogEvent(EnableLogging); + AtkEventListener* result = null; - var result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); - - if ((freeFlags & 1) == 1) + try { - IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); - AddonLifecycle.AllocatedTables.Remove(this); + this.LogEvent(EnableLogging); + + try + { + result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Dtor. This may be a bug in the game or another plugin hooking this method."); + } + + if ((freeFlags & 1) == 1) + { + IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + AddonLifecycle.AllocatedTables.Remove(this); + } + } + catch (Exception e) + { + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDestructor."); } return result; @@ -151,338 +167,460 @@ internal unsafe class AddonVirtualTable : IDisposable private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(EnableLogging); - - this.setupArgs.Addon = addon; - this.setupArgs.AtkValueCount = valueCount; - this.setupArgs.AtkValues = (nint)values; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs); - - valueCount = this.setupArgs.AtkValueCount; - values = (AtkValue*)this.setupArgs.AtkValues; - try { - this.originalVirtualTable->OnSetup(addon, valueCount, values); + this.LogEvent(EnableLogging); + + this.setupArgs.Addon = addon; + this.setupArgs.AtkValueCount = valueCount; + this.setupArgs.AtkValues = (nint)values; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreSetup, this.setupArgs); + + valueCount = this.setupArgs.AtkValueCount; + values = (AtkValue*)this.setupArgs.AtkValues; + + try + { + this.originalVirtualTable->OnSetup(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnSetup. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonSetup."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostSetup, this.setupArgs); } private void OnAddonFinalize(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.finalizeArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs); - try { - this.originalVirtualTable->Finalizer(thisPtr); + this.LogEvent(EnableLogging); + + this.finalizeArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFinalize, this.finalizeArgs); + + try + { + this.originalVirtualTable->Finalizer(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Finalizer. This may be a bug in the game or another plugin hooking this method."); + } } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonFinalize. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFinalize."); } } private void OnAddonDraw(AtkUnitBase* addon) { - this.LogEvent(EnableLogging); - - this.drawArgs.Addon = addon; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs); - try { - this.originalVirtualTable->Draw(addon); + this.LogEvent(EnableLogging); + + this.drawArgs.Addon = addon; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreDraw, this.drawArgs); + + try + { + this.originalVirtualTable->Draw(addon); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Draw. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonDraw."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostDraw, this.drawArgs); } private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.LogEvent(EnableLogging); - - this.updateArgs.Addon = addon; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs); - - // Note: Do not pass or allow manipulation of delta. - // It's realistically not something that should be needed. - try { - this.originalVirtualTable->Update(addon, delta); + this.LogEvent(EnableLogging); + + this.updateArgs.Addon = addon; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreUpdate, this.updateArgs); + + // Note: Do not pass or allow manipulation of delta. + // It's realistically not something that should be needed. + // And even if someone does, they are encouraged to hook Update themselves. + + try + { + this.originalVirtualTable->Update(addon, delta); + } + 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(AddonEvent.PostUpdate, this.updateArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonUpdate."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostUpdate, this.updateArgs); } private bool OnAddonRefresh(AtkUnitBase* addon, uint valueCount, AtkValue* values) { - this.LogEvent(EnableLogging); - var result = false; - this.refreshArgs.Addon = addon; - this.refreshArgs.AtkValueCount = valueCount; - this.refreshArgs.AtkValues = (nint)values; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs); - - valueCount = this.refreshArgs.AtkValueCount; - values = (AtkValue*)this.refreshArgs.AtkValues; - try { - result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + this.LogEvent(EnableLogging); + + this.refreshArgs.Addon = addon; + this.refreshArgs.AtkValueCount = valueCount; + this.refreshArgs.AtkValues = (nint)values; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRefresh, this.refreshArgs); + + valueCount = this.refreshArgs.AtkValueCount; + values = (AtkValue*)this.refreshArgs.AtkValues; + + try + { + result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnRefresh. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonRefresh."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRefresh, this.refreshArgs); return result; } private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.LogEvent(EnableLogging); - - this.requestedUpdateArgs.Addon = addon; - this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData; - this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs); - - numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData; - stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData; - try { - this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + this.LogEvent(EnableLogging); + + this.requestedUpdateArgs.Addon = addon; + this.requestedUpdateArgs.NumberArrayData = (nint)numberArrayData; + this.requestedUpdateArgs.StringArrayData = (nint)stringArrayData; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.requestedUpdateArgs); + + numberArrayData = (NumberArrayData**)this.requestedUpdateArgs.NumberArrayData; + stringArrayData = (StringArrayData**)this.requestedUpdateArgs.StringArrayData; + + try + { + this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnRequestedUpdate."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.requestedUpdateArgs); } private void OnAddonReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) { - this.LogEvent(EnableLogging); - - this.receiveEventArgs.Addon = (nint)addon; - this.receiveEventArgs.AtkEventType = (byte)eventType; - this.receiveEventArgs.EventParam = eventParam; - this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent; - this.receiveEventArgs.AtkEventData = (nint)atkEventData; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs); - - eventType = (AtkEventType)this.receiveEventArgs.AtkEventType; - eventParam = this.receiveEventArgs.EventParam; - atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent; - atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData; - try { - this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + this.LogEvent(EnableLogging); + + this.receiveEventArgs.Addon = (nint)addon; + this.receiveEventArgs.AtkEventType = (byte)eventType; + this.receiveEventArgs.EventParam = eventParam; + this.receiveEventArgs.AtkEvent = (IntPtr)atkEvent; + this.receiveEventArgs.AtkEventData = (nint)atkEventData; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.receiveEventArgs); + + eventType = (AtkEventType)this.receiveEventArgs.AtkEventType; + eventParam = this.receiveEventArgs.EventParam; + atkEvent = (AtkEvent*)this.receiveEventArgs.AtkEvent; + atkEventData = (AtkEventData*)this.receiveEventArgs.AtkEventData; + + try + { + this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon ReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonReceiveEvent."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.receiveEventArgs); } private bool OnAddonOpen(AtkUnitBase* thisPtr, uint depthLayer) { - this.LogEvent(EnableLogging); - var result = false; - this.openArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs); - try { - result = this.originalVirtualTable->Open(thisPtr, depthLayer); + this.LogEvent(EnableLogging); + + this.openArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreOpen, this.openArgs); + + try + { + result = this.originalVirtualTable->Open(thisPtr, depthLayer); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Open. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonOpen. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonOpen."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostOpen, this.openArgs); - return result; } private bool OnAddonClose(AtkUnitBase* thisPtr, bool fireCallback) { - this.LogEvent(EnableLogging); - var result = false; - this.closeArgs.Addon = thisPtr; - this.closeArgs.FireCallback = fireCallback; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs); - - fireCallback = this.closeArgs.FireCallback; - try { - result = this.originalVirtualTable->Close(thisPtr, fireCallback); + this.LogEvent(EnableLogging); + + this.closeArgs.Addon = thisPtr; + this.closeArgs.FireCallback = fireCallback; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreClose, this.closeArgs); + + fireCallback = this.closeArgs.FireCallback; + + try + { + result = this.originalVirtualTable->Close(thisPtr, fireCallback); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Close. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonClose. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonClose."); } - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostClose, this.closeArgs); - return result; } private void OnAddonShow(AtkUnitBase* thisPtr, bool silenceOpenSoundEffect, uint unsetShowHideFlags) { - this.LogEvent(EnableLogging); - - this.showArgs.Addon = thisPtr; - this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect; - this.showArgs.UnsetShowHideFlags = unsetShowHideFlags; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs); - - silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect; - unsetShowHideFlags = this.showArgs.UnsetShowHideFlags; - try { - this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + this.LogEvent(EnableLogging); + + this.showArgs.Addon = thisPtr; + this.showArgs.SilenceOpenSoundEffect = silenceOpenSoundEffect; + this.showArgs.UnsetShowHideFlags = unsetShowHideFlags; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreShow, this.showArgs); + + silenceOpenSoundEffect = this.showArgs.SilenceOpenSoundEffect; + unsetShowHideFlags = this.showArgs.UnsetShowHideFlags; + + try + { + this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + } + 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(AddonEvent.PostShow, this.showArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonShow. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonShow."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostShow, this.showArgs); } private void OnAddonHide(AtkUnitBase* thisPtr, bool unkBool, bool callHideCallback, uint setShowHideFlags) { - this.LogEvent(EnableLogging); - - this.hideArgs.Addon = thisPtr; - this.hideArgs.UnknownBool = unkBool; - this.hideArgs.CallHideCallback = callHideCallback; - this.hideArgs.SetShowHideFlags = setShowHideFlags; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs); - - unkBool = this.hideArgs.UnknownBool; - callHideCallback = this.hideArgs.CallHideCallback; - setShowHideFlags = this.hideArgs.SetShowHideFlags; - try { - this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + this.LogEvent(EnableLogging); + + this.hideArgs.Addon = thisPtr; + this.hideArgs.UnknownBool = unkBool; + this.hideArgs.CallHideCallback = callHideCallback; + this.hideArgs.SetShowHideFlags = setShowHideFlags; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreHide, this.hideArgs); + + unkBool = this.hideArgs.UnknownBool; + callHideCallback = this.hideArgs.CallHideCallback; + setShowHideFlags = this.hideArgs.SetShowHideFlags; + + try + { + this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + } + 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(AddonEvent.PostHide, this.hideArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original AddonHide. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonHide."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostHide, this.hideArgs); } private void OnAddonMove(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.onMoveArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs); - try { - this.originalVirtualTable->OnMove(thisPtr); + this.LogEvent(EnableLogging); + + this.onMoveArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMove, this.onMoveArgs); + + try + { + this.originalVirtualTable->OnMove(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnMove. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnAddonMove. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMove."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMove, this.onMoveArgs); } private void OnAddonMouseOver(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.onMouseOverArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs); - try { - this.originalVirtualTable->OnMouseOver(thisPtr); + this.LogEvent(EnableLogging); + + this.onMouseOverArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOver, this.onMouseOverArgs); + + try + { + this.originalVirtualTable->OnMouseOver(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnMouseOver. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnAddonMouseOver. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOver."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOver, this.onMouseOverArgs); } private void OnAddonMouseOut(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.onMouseOutArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs); - try { - this.originalVirtualTable->OnMouseOut(thisPtr); + this.LogEvent(EnableLogging); + + this.onMouseOutArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreMouseOut, this.onMouseOutArgs); + + try + { + this.originalVirtualTable->OnMouseOut(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon OnMouseOut. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnAddonMouseOut. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonMouseOut."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostMouseOut, this.onMouseOutArgs); } private void OnAddonFocus(AtkUnitBase* thisPtr) { - this.LogEvent(EnableLogging); - - this.focusArgs.Addon = thisPtr; - this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs); - try { - this.originalVirtualTable->Focus(thisPtr); + this.LogEvent(EnableLogging); + + this.focusArgs.Addon = thisPtr; + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PreFocus, this.focusArgs); + + try + { + this.originalVirtualTable->Focus(thisPtr); + } + catch (Exception e) + { + Log.Error(e, "Caught exception when calling original Addon Focus. This may be a bug in the game or another plugin hooking this method."); + } + + this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs); } catch (Exception e) { - Log.Error(e, "Caught exception when calling original OnAddonFocus. This may be a bug in the game or another plugin hooking this method."); + Log.Error(e, "Caught exception from Dalamud when attempting to process OnAddonFocus."); } - - this.lifecycleService.InvokeListenersSafely(AddonEvent.PostFocus, this.focusArgs); } [Conditional("DEBUG")] From d0110f7251d50b2a45fbc51d6a8b03662e7afa54 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 20:03:22 +0100 Subject: [PATCH 090/164] Hardcode HasModifiedGameDataFiles to false for now until XL is fixed --- Dalamud/Data/DataManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 559bd84dc..f53195a2d 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -84,8 +84,11 @@ internal sealed class DataManager : IInternalDisposableService, IDataManager dalamud.StartInfo.TroubleshootingPackData); // Don't fail for IndexIntegrityResult.Exception, since the check during launch has a very small timeout - this.HasModifiedGameDataFiles = - tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed; + // this.HasModifiedGameDataFiles = + // tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed; + + // TODO: Put above back when check in XL is fixed + this.HasModifiedGameDataFiles = false; if (this.HasModifiedGameDataFiles) Log.Verbose("Game data integrity check failed!\n{TsData}", dalamud.StartInfo.TroubleshootingPackData); From 5d08170333d4e460b3eeb5197e286df86d5bdffe Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 20:03:43 +0100 Subject: [PATCH 091/164] Keep rendering title bar buttons if one is not available clickthrough --- Dalamud/Interface/Windowing/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index b0786fbb5..7ecd5e15c 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -864,7 +864,7 @@ public abstract class Window foreach (var button in this.allButtons) { if (this.internalIsClickthrough && !button.AvailableClickthrough) - return; + continue; Vector2 position = new(titleBarRect.Max.X - padR - buttonSize, titleBarRect.Min.Y + style.FramePadding.Y); padR += buttonSize + style.ItemInnerSpacing.X; From 24caa1cb18f47c705b6706e718a645f3d25c056f Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 20:05:14 +0100 Subject: [PATCH 092/164] PresetWindow.IsDefault can be JsonIgnore --- Dalamud/Interface/Windowing/Persistence/PresetModel.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Windowing/Persistence/PresetModel.cs b/Dalamud/Interface/Windowing/Persistence/PresetModel.cs index f7910e0b2..4ddf55e51 100644 --- a/Dalamud/Interface/Windowing/Persistence/PresetModel.cs +++ b/Dalamud/Interface/Windowing/Persistence/PresetModel.cs @@ -53,6 +53,7 @@ internal class PresetModel /// /// Gets a value indicating whether this preset is in the default state. /// + [JsonIgnore] public bool IsDefault => !this.IsPinned && !this.IsClickThrough && From 2806e59dba4562e75f7721e40fd3d2bdd3935577 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 20:09:31 +0100 Subject: [PATCH 093/164] Also remove borders for dev bar, to prevent themes from causing weirdness --- Dalamud/Interface/Internal/DalamudInterface.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index bf55a5486..b0fbeb6c5 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -669,6 +669,8 @@ internal class DalamudInterface : IInternalDisposableService { using var barColor = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0.060f, 0.060f, 0.060f, 0.773f)); barColor.Push(ImGuiCol.MenuBarBg, Vector4.Zero); + barColor.Push(ImGuiCol.Border, Vector4.Zero); + barColor.Push(ImGuiCol.BorderShadow, Vector4.Zero); if (ImGui.BeginMainMenuBar()) { var pluginManager = Service.Get(); From 97df73acea61ddf94c135bc37e4f402b6e5a6cab Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 8 Dec 2025 21:00:08 +0100 Subject: [PATCH 094/164] Ensure that we don't catch mouse up events without corresponding mouse down events Fixes an issue wherein the cursor could get locked by the game if WantCaptureMouse becomes true in between down and up events --- .../InputHandler/Win32InputHandler.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs index 0b2e27b57..6b26ce37d 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs @@ -7,6 +7,7 @@ using System.Runtime.InteropServices; using System.Text; using Dalamud.Bindings.ImGui; +using Dalamud.Console; using Dalamud.Memory; using Dalamud.Utility; @@ -37,6 +38,8 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler private readonly WndProcDelegate wndProcDelegate; private readonly nint platformNamePtr; + private readonly IConsoleVariable cvLogMouseEvents; + private ViewportHandler viewportHandler; private int mouseButtonsDown; @@ -87,6 +90,11 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler this.cursors[(int)ImGuiMouseCursor.ResizeNwse] = LoadCursorW(default, IDC.IDC_SIZENWSE); this.cursors[(int)ImGuiMouseCursor.Hand] = LoadCursorW(default, IDC.IDC_HAND); this.cursors[(int)ImGuiMouseCursor.NotAllowed] = LoadCursorW(default, IDC.IDC_NO); + + this.cvLogMouseEvents = Service.Get().AddVariable( + "imgui.log_mouse_events", + "Log mouse events to console for debugging", + false); } /// @@ -267,11 +275,23 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler case WM.WM_XBUTTONDOWN: case WM.WM_XBUTTONDBLCLK: { + if (this.cvLogMouseEvents.Value) + { + Log.Verbose( + "Handle MouseDown {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}", + GetButton(msg, wParam), + io.WantCaptureMouse, + this.mouseButtonsDown); + } + var button = GetButton(msg, wParam); if (io.WantCaptureMouse) { if (this.mouseButtonsDown == 0 && GetCapture() == nint.Zero) + { SetCapture(hWndCurrent); + } + this.mouseButtonsDown |= 1 << button; io.AddMouseButtonEvent(button, true); return default(LRESULT); @@ -288,12 +308,28 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler case WM.WM_MBUTTONUP: case WM.WM_XBUTTONUP: { + if (this.cvLogMouseEvents.Value) + { + Log.Verbose( + "Handle MouseUp {Btn} WantCaptureMouse: {Want} mouseButtonsDown: {Down}", + GetButton(msg, wParam), + io.WantCaptureMouse, + this.mouseButtonsDown); + } + var button = GetButton(msg, wParam); - if (io.WantCaptureMouse) + + // Need to check if we captured the button event away from the game here, otherwise the game might get + // a down event but no up event, causing the cursor to get stuck. + // Can happen if WantCaptureMouse becomes true in between down and up + if (io.WantCaptureMouse && (this.mouseButtonsDown & (1 << button)) != 0) { this.mouseButtonsDown &= ~(1 << button); if (this.mouseButtonsDown == 0 && GetCapture() == hWndCurrent) + { ReleaseCapture(); + } + io.AddMouseButtonEvent(button, false); return default(LRESULT); } From e53ccdbcc03a4931e4594491c6250395e5a3a3e5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 9 Dec 2025 00:18:28 +0100 Subject: [PATCH 095/164] build: 13.0.0.15 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a4b203406..5aca47e0c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.14 + 13.0.0.15 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From b88a6bb61646d32bf2ca2df54b611f58a81a5e5b Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Wed, 10 Dec 2025 23:12:44 +0900 Subject: [PATCH 096/164] Always pop DalamudStandard style if pushed earlier in Draw --- Dalamud/Interface/Windowing/Window.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 7ecd5e15c..ed9318e49 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -672,16 +672,13 @@ public abstract class Window Task.FromResult(tex)); } - if (!this.hasError) + if (isErrorStylePushed) { - this.PostDraw(); + Style.StyleModelV1.DalamudStandard.Pop(); } else { - if (isErrorStylePushed) - { - Style.StyleModelV1.DalamudStandard.Pop(); - } + this.PostDraw(); } this.PostHandlePreset(persistence); From 201c9cfcf25c578f3bb2a3964b8674708b35d634 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 10 Dec 2025 01:42:45 +0100 Subject: [PATCH 097/164] Use game window to calculate offsets in fallback mouse position code --- .../ImGuiBackend/InputHandler/Win32InputHandler.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs index 6b26ce37d..8417a90e5 100644 --- a/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs +++ b/Dalamud/Interface/ImGuiBackend/InputHandler/Win32InputHandler.cs @@ -494,7 +494,12 @@ internal sealed unsafe partial class Win32InputHandler : IImGuiInputHandler // (This is the position you can get with ::GetCursorPos() or WM_MOUSEMOVE + ::ClientToScreen(). In theory adding viewport->Pos to a client position would also be the same.) var mousePos = mouseScreenPos; if ((io.ConfigFlags & ImGuiConfigFlags.ViewportsEnable) == 0) - ClientToScreen(focusedWindow, &mousePos); + { + // Use game window, otherwise, positions are calculated based on the focused window which might not be the game. + // Leads to offsets. + ClientToScreen(this.hWnd, &mousePos); + } + io.AddMousePosEvent(mousePos.x, mousePos.y); } From a39763f161b893e5735b0dbe3bd7dde42bb38d5f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 10 Dec 2025 18:32:18 +0100 Subject: [PATCH 098/164] Mark preset dirty when disabling clickthrough for a window --- Dalamud/Interface/Windowing/Window.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index ed9318e49..5a79a017a 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -84,7 +84,7 @@ public abstract class Window Click = _ => { this.internalIsClickthrough = false; - this.presetDirty = false; + this.presetDirty = true; ImGui.OpenPopup(AdditionsPopupName); }, Priority = int.MinValue, @@ -905,7 +905,7 @@ public abstract class Window private void DrawErrorMessage() { // TODO: Once window systems are services, offer to reload the plugin - ImGui.TextColoredWrapped(ImGuiColors.DalamudRed,Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("WindowSystemErrorOccurred", "An error occurred while rendering this window. Please contact the developer for details.")); ImGuiHelpers.ScaledDummy(5); From 0b55dc3e10b1a43013fee7893ed3dd88762a2f60 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 11 Dec 2025 22:59:50 +0100 Subject: [PATCH 099/164] Clear ImDrawListSplitter when disposing SeStringDrawState --- .../ImGuiSeStringRenderer/Internal/SeStringRenderer.cs | 2 +- .../Interface/ImGuiSeStringRenderer/SeStringDrawState.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index 397502b30..4a8e6517e 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -170,7 +170,7 @@ internal class SeStringRenderer : IServiceType // This also does argument validation for drawParams. Do it here. // `using var` makes a struct read-only, but we do want to modify it. - var stateStorage = new SeStringDrawState( + using var stateStorage = new SeStringDrawState( sss, drawParams, ThreadSafety.IsMainThread ? this.colorStackSetMainThread : new(this.colorStackSetMainThread.ColorTypes), diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 5601100e9..885508bed 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -10,6 +10,7 @@ using Dalamud.Interface.Utility; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; + using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; @@ -17,7 +18,7 @@ namespace Dalamud.Interface.ImGuiSeStringRenderer; /// Calculated values from using ImGui styles. [StructLayout(LayoutKind.Sequential)] -public unsafe ref struct SeStringDrawState +public unsafe ref struct SeStringDrawState : IDisposable { private static readonly int ChannelCount = Enum.GetValues().Length; @@ -194,6 +195,9 @@ public unsafe ref struct SeStringDrawState /// Gets the text fragments. internal List Fragments { get; } + /// + public void Dispose() => this.splitter.ClearFreeMemory(); + /// Sets the current channel in the ImGui draw list splitter. /// Channel to switch to. [MethodImpl(MethodImplOptions.AggressiveInlining)] From e100ec2abdc490838f3422bf5fd5cda695a886fd Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 12 Dec 2025 00:57:04 +0100 Subject: [PATCH 100/164] build: 13.0.0.16 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 5aca47e0c..3ec3c0865 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.15 + 13.0.0.16 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 2d096d9b334ee485764a1e00589cba65c273af3e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 13 Dec 2025 05:05:03 +0100 Subject: [PATCH 101/164] Properly initialize GameInventoryItems (#2504) --- Dalamud/Game/Inventory/GameInventory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 535b84372..5390c2707 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -305,7 +305,8 @@ internal class GameInventory : IInternalDisposableService private GameInventoryItem[] CreateItemsArray(int length) { var items = new GameInventoryItem[length]; - items.Initialize(); + foreach (ref var item in items.AsSpan()) + item = new(); return items; } From a1409096fdc0d882a659f1f84c83fa42646da81d Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 15 Dec 2025 21:20:24 +0100 Subject: [PATCH 102/164] Redo SeStringRenderer deprecations --- .../Internal/SeStringRenderer.cs | 3 +-- .../ImGuiSeStringRenderer/SeStringDrawState.cs | 15 ++++----------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs index 4a8e6517e..f161c1868 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/Internal/SeStringRenderer.cs @@ -162,8 +162,7 @@ internal class SeStringRenderer : IServiceType if (drawParams.Font.HasValue) font = drawParams.Font.Value; - // API14: Remove commented out code - if (ThreadSafety.IsMainThread /* && drawParams.TargetDrawList is null */ && font is null) + if (ThreadSafety.IsMainThread && drawParams.TargetDrawList is null && font is null) font = ImGui.GetFont(); if (font is null) throw new ArgumentException("Specified font is empty."); diff --git a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs index 5edf60e9d..dcbe123e7 100644 --- a/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs +++ b/Dalamud/Interface/ImGuiSeStringRenderer/SeStringDrawState.cs @@ -66,17 +66,10 @@ public unsafe ref struct SeStringDrawState : IDisposable this.drawList = ssdp.TargetDrawList.Value; this.ScreenOffset = ssdp.ScreenOffset ?? Vector2.Zero; - // API14: Remove, always throw - if (ThreadSafety.IsMainThread) - { - this.ScreenOffset = ssdp.ScreenOffset ?? ImGui.GetCursorScreenPos(); - this.FontSize = ssdp.FontSize ?? ImGui.GetFontSize(); - } - else - { - throw new ArgumentException( - $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); - } + this.ScreenOffset = ssdp.ScreenOffset ?? throw new ArgumentException( + $"{nameof(ssdp.ScreenOffset)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state. (GetCursorScreenPos?)"); + this.FontSize = ssdp.FontSize ?? throw new ArgumentException( + $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); // this.FontSize = ssdp.FontSize ?? throw new ArgumentException( // $"{nameof(ssdp.FontSize)} must be set when specifying a target draw list, as it cannot be fetched from the ImGui state."); From 20af5b40c74d882a7707906e1be034f3f76ff8d2 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 15 Dec 2025 21:31:25 +0100 Subject: [PATCH 103/164] Make all versioning functions internal, move to separate class --- Dalamud/EntryPoint.cs | 4 +- Dalamud/Game/ChatHandlers.cs | 6 +- Dalamud/Interface/Internal/DalamudCommands.cs | 4 +- .../Interface/Internal/DalamudInterface.cs | 8 +- .../Internal/Windows/BranchSwitcherWindow.cs | 2 +- .../Internal/Windows/ChangelogWindow.cs | 4 +- .../Data/Widgets/SeStringCreatorWidget.cs | 2 +- .../PluginInstaller/PluginInstallerWindow.cs | 8 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 2 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 2 +- Dalamud/Networking/Http/HappyHttpClient.cs | 2 +- .../Rpc/Service/ClientHelloService.cs | 2 +- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- .../Plugin/Internal/Types/PluginRepository.cs | 8 +- Dalamud/Support/BugBait.cs | 2 +- Dalamud/Support/DalamudReleases.cs | 2 +- Dalamud/Support/Troubleshooting.cs | 6 +- Dalamud/Utility/Util.cs | 96 ---------------- Dalamud/Utility/Versioning.cs | 108 ++++++++++++++++++ 20 files changed, 142 insertions(+), 130 deletions(-) create mode 100644 Dalamud/Utility/Versioning.cs diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 54e25b6f2..d9f6ef172 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -192,8 +192,8 @@ public sealed class EntryPoint var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", - Util.GetScmVersion(), - Util.GetGitHashClientStructs(), + Versioning.GetScmVersion(), + Versioning.GetGitHashClientStructs(), FFXIVClientStructs.ThisAssembly.Git.Commits); dalamud.WaitForUnload(); diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index b1b798a8a..279bf46e5 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -104,7 +104,7 @@ internal partial class ChatHandlers : IServiceType if (this.configuration.PrintDalamudWelcomeMsg) { - chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion()) + chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Versioning.GetScmVersion()) + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded))); } @@ -116,7 +116,7 @@ internal partial class ChatHandlers : IServiceType } } - if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion)) + if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Versioning.GetAssemblyVersion().StartsWith(this.configuration.LastVersion)) { var linkPayload = chatGui.AddChatLinkHandler( (_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs)); @@ -137,7 +137,7 @@ internal partial class ChatHandlers : IServiceType Type = XivChatType.Notice, }); - this.configuration.LastVersion = Util.AssemblyVersion; + this.configuration.LastVersion = Versioning.GetAssemblyVersion(); this.configuration.QueueSave(); } diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index b1fdb5232..3e4a5cec6 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -305,12 +305,12 @@ internal class DalamudCommands : IServiceType chatGui.Print(new SeStringBuilder() .AddItalics("Dalamud:") - .AddText($" {Util.GetScmVersion()}") + .AddText($" {Versioning.GetScmVersion()}") .Build()); chatGui.Print(new SeStringBuilder() .AddItalics("FFXIVCS:") - .AddText($" {Util.GetGitHashClientStructs()}") + .AddText($" {Versioning.GetGitHashClientStructs()}") .Build()); } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index b0fbeb6c5..be4228a81 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -182,7 +182,7 @@ internal class DalamudInterface : IInternalDisposableService () => Service.GetNullable()?.ToggleDevMenu(), VirtualKey.SHIFT); - if (Util.GetActiveTrack() != "release") + if (Versioning.GetActiveTrack() != "release") { titleScreenMenu.AddEntryCore( Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), @@ -865,7 +865,7 @@ internal class DalamudInterface : IInternalDisposableService } ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false, false); - ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false, false); + ImGui.MenuItem($"D: {Versioning.GetScmVersion()} CS: {Versioning.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false, false); ImGui.MenuItem($"CLR: {Environment.Version}", false, false); ImGui.EndMenu(); @@ -1076,8 +1076,8 @@ internal class DalamudInterface : IInternalDisposableService { ImGui.PushFont(InterfaceManager.MonoFont); - ImGui.BeginMenu($"{Util.GetActiveTrack() ?? "???"} on {Util.GetGitBranch() ?? "???"}", false); - ImGui.BeginMenu($"{Util.GetScmVersion()}", false); + ImGui.BeginMenu($"{Versioning.GetActiveTrack() ?? "???"} on {Versioning.GetGitBranch() ?? "???"}", false); + ImGui.BeginMenu($"{Versioning.GetScmVersion()}", false); ImGui.BeginMenu(this.FrameCount.ToString("000000"), false); ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false); diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index 9cc14ea14..51a9c48a6 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -47,7 +47,7 @@ public class BranchSwitcherWindow : Window this.branches = await client.GetFromJsonAsync>(BranchInfoUrl); Debug.Assert(this.branches != null, "this.branches != null"); - var trackName = Util.GetActiveTrack(); + var trackName = Versioning.GetActiveTrack(); this.selectedBranchIndex = this.branches.IndexOf(x => x.Value.Track == trackName); if (this.selectedBranchIndex == -1) { diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index b0a910ead..44626ba31 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -147,7 +147,7 @@ internal sealed class ChangelogWindow : Window, IDisposable var pmWantsChangelog = pm?.InstalledPlugins.Any() ?? true; return (string.IsNullOrEmpty(configuration.LastChangelogMajorMinor) || (!WarrantsChangelogForMajorMinor.StartsWith(configuration.LastChangelogMajorMinor) && - Util.AssemblyVersion.StartsWith(WarrantsChangelogForMajorMinor))) && pmWantsChangelog; + Versioning.GetAssemblyVersion().StartsWith(WarrantsChangelogForMajorMinor))) && pmWantsChangelog; } /// @@ -357,7 +357,7 @@ internal sealed class ChangelogWindow : Window, IDisposable { case State.WindowFadeIn: case State.ExplainerIntro: - ImGui.TextWrapped($"Welcome to Dalamud v{Util.GetScmVersion()}!"); + ImGui.TextWrapped($"Welcome to Dalamud v{Versioning.GetScmVersion()}!"); ImGuiHelpers.ScaledDummy(5); ImGui.TextWrapped(ChangeLog); ImGuiHelpers.ScaledDummy(5); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index a88f576f9..e9b4022e4 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -144,7 +144,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget new TextEntry(TextEntryType.Macro, " "), ]; - private SeStringParameter[]? localParameters = [Util.GetScmVersion()]; + private SeStringParameter[]? localParameters = [Versioning.GetScmVersion()]; private ReadOnlySeString input; private ClientLanguage? language; private Task? validImportSheetNamesTask; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index ac092bd25..3241015fc 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -302,7 +302,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.profileManagerWidget.Reset(); - if (this.staleDalamudNewVersion == null && !Util.GetActiveTrack().IsNullOrEmpty()) + if (this.staleDalamudNewVersion == null && !Versioning.GetActiveTrack().IsNullOrEmpty()) { Service.Get().GetVersionForCurrentTrack().ContinueWith(t => { @@ -310,7 +310,7 @@ internal class PluginInstallerWindow : Window, IDisposable return; var versionInfo = t.Result; - if (versionInfo.AssemblyVersion != Util.GetScmVersion()) + if (versionInfo.AssemblyVersion != Versioning.GetScmVersion()) { this.staleDalamudNewVersion = versionInfo.AssemblyVersion; } @@ -1670,7 +1670,7 @@ internal class PluginInstallerWindow : Window, IDisposable DrawWarningIcon(); DrawLinesCentered("A new version of Dalamud is available.\n" + "Please restart the game to ensure compatibility with updated plugins.\n" + - $"old: {Util.GetScmVersion()} new: {this.staleDalamudNewVersion}"); + $"old: {Versioning.GetScmVersion()} new: {this.staleDalamudNewVersion}"); ImGuiHelpers.ScaledDummy(10); } @@ -2461,7 +2461,7 @@ internal class PluginInstallerWindow : Window, IDisposable var isOutdated = effectiveApiLevel < PluginManager.DalamudApiLevel; var isIncompatible = manifest.MinimumDalamudVersion != null && - manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed; + manifest.MinimumDalamudVersion > Versioning.GetAssemblyVersionParsed(); var enableInstallButton = this.updateStatus != OperationStatus.InProgress && this.installStatus != OperationStatus.InProgress && diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 74b9b0fd7..4785ceb3c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -223,7 +223,7 @@ Contribute at: https://github.com/goatcorp/Dalamud .Select(plugin => $"{plugin.Manifest.Name} by {plugin.Manifest.Author}\n") .Aggregate(string.Empty, (current, next) => $"{current}{next}"); - this.creditsText = string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits, Util.GetGitHashClientStructs()); + this.creditsText = string.Format(CreditsTextTempl, typeof(Dalamud).Assembly.GetName().Version, pluginCredits, Versioning.GetGitHashClientStructs()); var gameGui = Service.Get(); var playerState = PlayerState.Instance(); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 69cdc4d28..e14dbc545 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -503,7 +503,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable lssb.PushEdgeColorType(701).PushColorType(539) .Append(SeIconChar.BoxedLetterD.ToIconChar()) .PopColorType().PopEdgeColorType(); - lssb.Append($" Dalamud: {Util.GetScmVersion()}"); + lssb.Append($" Dalamud: {Versioning.GetScmVersion()}"); lssb.Append($" - {count} {(count != 1 ? "plugins" : "plugin")} loaded"); diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index aeed98695..c6a476fff 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -36,7 +36,7 @@ internal class HappyHttpClient : IInternalDisposableService { UserAgent = { - new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + new ProductInfoHeaderValue("Dalamud", Versioning.GetAssemblyVersion()), }, }, }; diff --git a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs index c5a4c851a..ae8319f21 100644 --- a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs +++ b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs @@ -38,7 +38,7 @@ internal sealed class ClientHelloService : IInternalDisposableService return new ClientHelloResponse { ApiVersion = "1.0", - DalamudVersion = Util.GetScmVersion(), + DalamudVersion = Versioning.GetScmVersion(), GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", ProcessId = Environment.ProcessId, ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(), diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index e2eded57c..193a2d45f 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1790,7 +1790,7 @@ internal class PluginManager : IInternalDisposableService var updates = this.AvailablePlugins .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) .Where(remoteManifest => plugin.Manifest.InstalledFromUrl == remoteManifest.SourceRepo.PluginMasterUrl || !remoteManifest.SourceRepo.IsThirdParty) - .Where(remoteManifest => remoteManifest.MinimumDalamudVersion == null || Util.AssemblyVersionParsed >= remoteManifest.MinimumDalamudVersion) + .Where(remoteManifest => remoteManifest.MinimumDalamudVersion == null || Versioning.GetAssemblyVersionParsed() >= remoteManifest.MinimumDalamudVersion) .Where(remoteManifest => { var useTesting = this.UseTesting(remoteManifest); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 0197683ef..1fe18b95b 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -315,7 +315,7 @@ internal class LocalPlugin : IAsyncDisposable if (!this.CheckPolicy()) throw new PluginPreconditionFailedException($"Unable to load {this.Name} as a load policy forbids it"); - if (this.Manifest.MinimumDalamudVersion != null && this.Manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed) + if (this.Manifest.MinimumDalamudVersion != null && this.Manifest.MinimumDalamudVersion > Versioning.GetAssemblyVersionParsed()) throw new PluginPreconditionFailedException($"Unable to load {this.Name}, Dalamud version is lower than minimum required version {this.Manifest.MinimumDalamudVersion}"); this.State = PluginState.Loading; diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index c5e703e4b..d5c1131af 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -59,7 +59,7 @@ internal class PluginRepository }, UserAgent = { - new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + new ProductInfoHeaderValue("Dalamud", Versioning.GetAssemblyVersion()), }, }, }; @@ -164,7 +164,7 @@ internal class PluginRepository } this.PluginMaster = pluginMaster.Where(this.IsValidManifest).ToList().AsReadOnly(); - + // API9 HACK: Force IsHide to false, we should remove that if (!this.IsThirdParty) { @@ -197,7 +197,7 @@ internal class PluginRepository Log.Error("Plugin {PluginName} in {RepoLink} has an invalid Name.", manifest.InternalName, this.PluginMasterUrl); return false; } - + // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (manifest.AssemblyVersion == null) { @@ -224,7 +224,7 @@ internal class PluginRepository request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; using var requestCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); - + return await httpClient.SendAsync(request, requestCts.Token); } } diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index 7ce96208c..f0a98ca98 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -37,7 +37,7 @@ internal static class BugBait Name = plugin.InternalName, Version = isTesting ? plugin.TestingAssemblyVersion?.ToString() : plugin.AssemblyVersion.ToString(), Platform = Util.GetHostPlatform().ToString(), - DalamudHash = Util.GetScmVersion(), + DalamudHash = Versioning.GetScmVersion(), }; if (includeException) diff --git a/Dalamud/Support/DalamudReleases.cs b/Dalamud/Support/DalamudReleases.cs index 603c77487..949ebf94a 100644 --- a/Dalamud/Support/DalamudReleases.cs +++ b/Dalamud/Support/DalamudReleases.cs @@ -38,7 +38,7 @@ internal class DalamudReleases : IServiceType /// The version info for the current track. public async Task GetVersionForCurrentTrack() { - var currentTrack = Util.GetActiveTrack(); + var currentTrack = Versioning.GetActiveTrack(); if (currentTrack.IsNullOrEmpty()) return null; diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index 88048c462..de529a29b 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -69,11 +69,11 @@ public static class Troubleshooting LoadedPlugins = pluginManager?.InstalledPlugins?.Select(x => x.Manifest as LocalPluginManifest)?.OrderByDescending(x => x.InternalName).ToArray(), PluginStates = pluginManager?.InstalledPlugins?.Where(x => !x.IsDev).ToDictionary(x => x.Manifest.InternalName, x => x.IsBanned ? "Banned" : x.State.ToString()), EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(), - DalamudVersion = Util.GetScmVersion(), - DalamudGitHash = Util.GetGitHash() ?? "Unknown", + DalamudVersion = Versioning.GetScmVersion(), + DalamudGitHash = Versioning.GetGitHash() ?? "Unknown", GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown", Language = startInfo.Language.ToString(), - BetaKey = Util.GetActiveTrack(), + BetaKey = Versioning.GetActiveTrack(), DoPluginTest = configuration.DoPluginTest, LoadAllApiLevels = pluginManager?.LoadAllApiLevels == true, InterfaceLoaded = interfaceManager?.IsReady ?? false, diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index bde113904..0ea5bbcbf 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -68,96 +68,10 @@ public static partial class Util ]; private static readonly Type GenericSpanType = typeof(Span<>); - private static string? scmVersionInternal; - private static string? gitHashInternal; - private static string? gitHashClientStructsInternal; - private static string? branchInternal; private static ulong moduleStartAddr; private static ulong moduleEndAddr; - /// - /// Gets the Dalamud version. - /// - [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] - public static string AssemblyVersion { get; } = - Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); - - /// - /// Gets the Dalamud version. - /// - internal static Version AssemblyVersionParsed { get; } = - Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!; - - /// - /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return - /// the git describe output for this build, which will be a raw version if this is a stable build or an - /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string. - /// - /// The SCM version of the assembly. - public static string GetScmVersion() - { - if (scmVersionInternal != null) return scmVersionInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value - ?? asm.GetName().Version!.ToString(); - } - - /// - /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds, - /// and will be suffixed with `-dirty` if in release with pending changes. - /// - /// The git hash of the assembly. - public static string? GetGitHash() - { - if (gitHashInternal != null) - return gitHashInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - return gitHashInternal = attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value ?? "N/A"; - } - - /// - /// Gets the git hash value from the assembly or null if it cannot be found. - /// - /// The git hash of the assembly. - public static string? GetGitHashClientStructs() - { - if (gitHashClientStructsInternal != null) - return gitHashClientStructsInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - gitHashClientStructsInternal = attrs.First(a => a.Key == "GitHashClientStructs").Value; - - return gitHashClientStructsInternal; - } - - /// - /// Gets the Git branch name this version of Dalamud was built from, or null, if this is a Debug build. - /// - /// The branch name. - public static string? GetGitBranch() - { - if (branchInternal != null) - return branchInternal; - - var asm = typeof(Util).Assembly; - var attrs = asm.GetCustomAttributes(); - - var gitBranch = attrs.FirstOrDefault(a => a.Key == "GitBranch")?.Value; - if (gitBranch == null) - return null; - - return branchInternal = gitBranch; - } - /// public static unsafe string DescribeAddress(void* p) => DescribeAddress((nint)p); @@ -693,16 +607,6 @@ public static partial class Util } } - /// - /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version - /// downloaded from webservices. - /// - /// The name of the track, or null. - internal static string? GetActiveTrack() - { - return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); - } - /// /// Gets a random, inoffensive, human-friendly string. /// diff --git a/Dalamud/Utility/Versioning.cs b/Dalamud/Utility/Versioning.cs new file mode 100644 index 000000000..d3b30b834 --- /dev/null +++ b/Dalamud/Utility/Versioning.cs @@ -0,0 +1,108 @@ +using System.Linq; +using System.Reflection; + +namespace Dalamud.Utility; + +/// +/// Helpers to access Dalamud versioning information. +/// +internal static class Versioning +{ + private static string? scmVersionInternal; + private static string? gitHashInternal; + private static string? gitHashClientStructsInternal; + private static string? branchInternal; + + /// + /// Gets the Dalamud version. + /// + /// The raw Dalamud assembly version. + internal static string GetAssemblyVersion() => + Assembly.GetAssembly(typeof(Versioning))!.GetName().Version!.ToString(); + + /// + /// Gets the Dalamud version. + /// + /// The parsed Dalamud assembly version. + internal static Version GetAssemblyVersionParsed() => + Assembly.GetAssembly(typeof(Versioning))!.GetName().Version!; + + /// + /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return + /// the git describe output for this build, which will be a raw version if this is a stable build or an + /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string. + /// + /// The SCM version of the assembly. + internal static string GetScmVersion() + { + if (scmVersionInternal != null) return scmVersionInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + return scmVersionInternal = attrs.First(a => a.Key == "SCMVersion").Value + ?? asm.GetName().Version!.ToString(); + } + + /// + /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds, + /// and will be suffixed with `-dirty` if in release with pending changes. + /// + /// The git hash of the assembly. + internal static string? GetGitHash() + { + if (gitHashInternal != null) + return gitHashInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + return gitHashInternal = attrs.FirstOrDefault(a => a.Key == "GitHash")?.Value ?? "N/A"; + } + + /// + /// Gets the git hash value from the assembly or null if it cannot be found. + /// + /// The git hash of the assembly. + internal static string? GetGitHashClientStructs() + { + if (gitHashClientStructsInternal != null) + return gitHashClientStructsInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + gitHashClientStructsInternal = attrs.First(a => a.Key == "GitHashClientStructs").Value; + + return gitHashClientStructsInternal; + } + + /// + /// Gets the Git branch name this version of Dalamud was built from, or null, if this is a Debug build. + /// + /// The branch name. + internal static string? GetGitBranch() + { + if (branchInternal != null) + return branchInternal; + + var asm = typeof(Util).Assembly; + var attrs = asm.GetCustomAttributes(); + + var gitBranch = attrs.FirstOrDefault(a => a.Key == "GitBranch")?.Value; + if (gitBranch == null) + return null; + + return branchInternal = gitBranch; + } + + /// + /// Gets the active Dalamud track, if this instance was launched through XIVLauncher and used a version + /// downloaded from webservices. + /// + /// The name of the track, or null. + internal static string? GetActiveTrack() + { + return Environment.GetEnvironmentVariable("DALAMUD_BRANCH"); + } +} From ffd99d57914a9306c47c146257801b607ed2d9b3 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 15 Dec 2025 21:43:52 +0100 Subject: [PATCH 104/164] Add interface to obtain versioning info --- Dalamud/Plugin/DalamudPluginInterface.cs | 25 ++++++++----------- Dalamud/Plugin/IDalamudPluginInterface.cs | 8 +++++- .../Plugin/VersionInfo/DalamudVersionInfo.cs | 11 ++++++++ .../Plugin/VersionInfo/IDalamudVersionInfo.cs | 19 ++++++++++++++ 4 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs create mode 100644 Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 6fd9064b6..1051f908c 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -16,18 +16,15 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.Sanitizer; using Dalamud.Interface; using Dalamud.Interface.Internal; -using Dalamud.Interface.Internal.Windows.PluginInstaller; -using Dalamud.Interface.Internal.Windows.SelfTest; -using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; -using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.VersionInfo; +using Dalamud.Utility; using Serilog; @@ -204,11 +201,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa return true; } - /// - /// Gets the plugin the given assembly is part of. - /// - /// The assembly to check. - /// The plugin the given assembly is part of, or null if this is a shared assembly or if this information cannot be determined. + /// public IExposedPlugin? GetPlugin(Assembly assembly) => AssemblyLoadContext.GetLoadContext(assembly) switch { @@ -216,11 +209,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa var context => this.GetPlugin(context), }; - /// - /// Gets the plugin that loads in the given context. - /// - /// The context to check. - /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. + /// public IExposedPlugin? GetPlugin(AssemblyLoadContext context) => Service.Get().InstalledPlugins.FirstOrDefault(p => p.LoadsIn(context)) switch { @@ -228,6 +217,12 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa var p => new ExposedPlugin(p), }; + /// + public IDalamudVersionInfo GetDalamudVersion() + { + return new DalamudVersionInfo(Versioning.GetAssemblyVersionParsed(), Versioning.GetActiveTrack()); + } + #region IPC /// diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index d1b6977d4..92ecab006 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -15,7 +15,7 @@ using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; using Dalamud.Plugin.Ipc.Internal; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.VersionInfo; namespace Dalamud.Plugin; @@ -194,6 +194,12 @@ public interface IDalamudPluginInterface : IServiceProvider /// The plugin that loads in the given context, or null if this isn't a plugin's context or if this information cannot be determined. IExposedPlugin? GetPlugin(AssemblyLoadContext context); + /// + /// Gets information about the version of Dalamud this plugin is loaded into. + /// + /// Class containing version information. + IDalamudVersionInfo GetDalamudVersion(); + /// T GetOrCreateData(string tag, Func dataGenerator) where T : class; diff --git a/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs new file mode 100644 index 000000000..c87c012af --- /dev/null +++ b/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs @@ -0,0 +1,11 @@ +namespace Dalamud.Plugin.VersionInfo; + +/// +internal class DalamudVersionInfo(Version version, string? track) : IDalamudVersionInfo +{ + /// + public Version Version { get; } = version; + + /// + public string? BetaTrack { get; } = track; +} diff --git a/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs new file mode 100644 index 000000000..e6b6a9601 --- /dev/null +++ b/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Plugin.VersionInfo; + +/// +/// Interface exposing various information related to Dalamud versioning. +/// +public interface IDalamudVersionInfo +{ + /// + /// Gets the Dalamud version. + /// + Version Version { get; } + + /// + /// Gets the currently used beta track. + /// Please don't tell users to switch branches. They have it bad enough, fix your things instead. + /// Null if this build wasn't launched from XIVLauncher. + /// + string? BetaTrack { get; } +} From a715725a9d8475f5b6755d70724e5ca47d753254 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Mon, 15 Dec 2025 13:13:08 -0800 Subject: [PATCH 105/164] Add enumerable AtkValue helper --- .../AddonArgTypes/AddonRefreshArgs.cs | 30 +++++++++++++++++-- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 30 +++++++++++++++++-- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index 8af017318..cb9de8088 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; + +using Dalamud.Game.NativeWrapper; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -32,7 +35,30 @@ public class AddonRefreshArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// - [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] - [Api15ToDo("Remove this")] + [Obsolete("Pending removal, Use AtkValueEnumerable instead.")] + [Api15ToDo("Make this internal, remove obsolete")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + /// Gets an enumerable collection of of the event's AtkValues. + /// + /// + /// An of corresponding to the event's AtkValues. + /// + public IEnumerable AtkValueEnumerable + { + get + { + for (var i = 0; i < this.AtkValueCount; i++) + { + AtkValuePtr ptr; + unsafe + { + ptr = new AtkValuePtr((nint)this.AtkValueSpan[i].Pointer); + } + + yield return ptr; + } + } + } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 9fd7b6dd0..2501d159f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; + +using Dalamud.Game.NativeWrapper; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -32,7 +35,30 @@ public class AddonSetupArgs : AddonArgs /// /// Gets the AtkValues in the form of a span. /// - [Obsolete("Pending removal, unsafe to use when using custom ClientStructs")] - [Api15ToDo("Remove this")] + [Obsolete("Pending removal, Use AtkValueEnumerable instead.")] + [Api15ToDo("Make this internal, remove obsolete")] public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + /// Gets an enumerable collection of of the event's AtkValues. + /// + /// + /// An of corresponding to the event's AtkValues. + /// + public IEnumerable AtkValueEnumerable + { + get + { + for (var i = 0; i < this.AtkValueCount; i++) + { + AtkValuePtr ptr; + unsafe + { + ptr = new AtkValuePtr((nint)this.AtkValueSpan[i].Pointer); + } + + yield return ptr; + } + } + } } From 1bff6abae90eb86b48a4465acc7de5cb289dbafa Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Mon, 15 Dec 2025 13:22:39 -0800 Subject: [PATCH 106/164] Fix oopsie --- Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs | 3 ++- Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index cb9de8088..4fc81632a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -4,6 +4,7 @@ using Dalamud.Game.NativeWrapper; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; +using FFXIVClientStructs.Interop; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -54,7 +55,7 @@ public class AddonRefreshArgs : AddonArgs AtkValuePtr ptr; unsafe { - ptr = new AtkValuePtr((nint)this.AtkValueSpan[i].Pointer); + ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); } yield return ptr; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 2501d159f..e0b2defbf 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -4,6 +4,7 @@ using Dalamud.Game.NativeWrapper; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; +using FFXIVClientStructs.Interop; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -54,7 +55,7 @@ public class AddonSetupArgs : AddonArgs AtkValuePtr ptr; unsafe { - ptr = new AtkValuePtr((nint)this.AtkValueSpan[i].Pointer); + ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); } yield return ptr; From 56325afa7fa6f897f61a93396bb78714600cad1e Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Mon, 15 Dec 2025 23:14:46 +0100 Subject: [PATCH 107/164] Remove obsolete enum values from FlyTextKind They have been obsolete for nearly 7 months (before 7.3). --- Dalamud/Game/Gui/FlyText/FlyTextKind.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs index 2b8325927..da448c683 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs @@ -92,34 +92,16 @@ public enum FlyTextKind : int /// IslandExp = 15, - /// - /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. - /// - [Obsolete("Use Dataset instead", true)] - Unknown16 = 16, - /// /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. /// Dataset = 16, - /// - /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// - [Obsolete("Use Knowledge instead", true)] - Unknown17 = 17, - /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// Knowledge = 17, - /// - /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// - [Obsolete("Use PhantomExp instead", true)] - Unknown18 = 18, - /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// From 1c1b60efeee642577196a6a319cbc0539895819b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 16 Dec 2025 09:48:59 +0100 Subject: [PATCH 108/164] Better enumerator code --- .../Game/ClientState/Aetherytes/AetheryteList.cs | 16 ++++++++++------ Dalamud/Game/ClientState/Buddy/BuddyList.cs | 16 ++++++++++------ Dalamud/Game/ClientState/Fates/FateTable.cs | 16 ++++++++++------ Dalamud/Game/ClientState/Objects/ObjectTable.cs | 14 ++++++++------ Dalamud/Game/ClientState/Party/PartyList.cs | 9 ++++----- Dalamud/Game/ClientState/Statuses/StatusList.cs | 9 ++++----- 6 files changed, 46 insertions(+), 34 deletions(-) diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index a24302947..12a629958 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -99,7 +99,7 @@ internal sealed partial class AetheryteList private struct Enumerator(AetheryteList aetheryteList) : IEnumerator { - private int index = 0; + private int index = -1; public IAetheryteEntry Current { get; private set; } @@ -107,15 +107,19 @@ internal sealed partial class AetheryteList public bool MoveNext() { - if (this.index == aetheryteList.Length) return false; - this.Current = aetheryteList[this.index]; - this.index++; - return true; + if (++this.index < aetheryteList.Length) + { + this.Current = aetheryteList[this.index]; + return true; + } + + this.Current = default; + return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index b8e4c0fcc..3bec6d9f1 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -141,7 +141,7 @@ internal sealed partial class BuddyList private struct Enumerator(BuddyList buddyList) : IEnumerator { - private int index = 0; + private int index = -1; public IBuddyMember Current { get; private set; } @@ -149,15 +149,19 @@ internal sealed partial class BuddyList public bool MoveNext() { - if (this.index == buddyList.Length) return false; - this.Current = buddyList[this.index]; - this.index++; - return true; + if (++this.index < buddyList.Length) + { + this.Current = buddyList[this.index]; + return true; + } + + this.Current = default; + return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index fa75c7e53..41e974f04 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -115,7 +115,7 @@ internal sealed partial class FateTable private struct Enumerator(FateTable fateTable) : IEnumerator { - private int index = 0; + private int index = -1; public IFate Current { get; private set; } @@ -123,15 +123,19 @@ internal sealed partial class FateTable public bool MoveNext() { - if (this.index == fateTable.Length) return false; - this.Current = fateTable[this.index]; - this.index++; - return true; + if (++this.index < fateTable.Length) + { + this.Current = fateTable[this.index]; + return true; + } + + this.Current = default; + return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 6bbc43235..9a2c7343e 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -246,17 +246,15 @@ internal sealed partial class ObjectTable { private int index = -1; - public IGameObject Current { get; private set; } = null!; + public IGameObject Current { get; private set; } object IEnumerator.Current => this.Current; public bool MoveNext() { - if (this.index == objectTableLength) - return false; - var cache = owner.cachedObjectTable.AsSpan(); - for (this.index++; this.index < objectTableLength; this.index++) + + while (++this.index < objectTableLength) { if (cache[this.index].Update() is { } ao) { @@ -265,10 +263,14 @@ internal sealed partial class ObjectTable } } + this.Current = default; return false; } - public void Reset() => this.index = -1; + public void Reset() + { + this.index = -1; + } public void Dispose() { diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 1dede1dd3..90959f926 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -143,7 +143,7 @@ internal sealed partial class PartyList private struct Enumerator(PartyList partyList) : IEnumerator { - private int index = 0; + private int index = -1; public IPartyMember Current { get; private set; } @@ -151,9 +151,7 @@ internal sealed partial class PartyList public bool MoveNext() { - if (this.index == partyList.Length) return false; - - for (; this.index < partyList.Length; this.index++) + while (++this.index < partyList.Length) { var partyMember = partyList[this.index]; if (partyMember != null) @@ -163,12 +161,13 @@ internal sealed partial class PartyList } } + this.Current = default; return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index 81469ba93..43650a48c 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -153,7 +153,7 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollecti private struct Enumerator(StatusList statusList) : IEnumerator { - private int index = 0; + private int index = -1; public IStatus Current { get; private set; } @@ -161,9 +161,7 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollecti public bool MoveNext() { - if (this.index == statusList.Length) return false; - - for (; this.index < statusList.Length; this.index++) + while (++this.index < statusList.Length) { var status = statusList[this.index]; if (status != null && status.StatusId != 0) @@ -173,12 +171,13 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollecti } } + this.Current = default; return false; } public void Reset() { - this.index = 0; + this.index = -1; } public void Dispose() From 89fbe6c8b098af79f0b451c07b9b0c611a682ee5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 16 Dec 2025 17:21:19 +0100 Subject: [PATCH 109/164] Update UiConfigOption --- Dalamud/Game/Config/UiConfigOption.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Dalamud/Game/Config/UiConfigOption.cs b/Dalamud/Game/Config/UiConfigOption.cs index f6a9aaa21..0cfc3b1e9 100644 --- a/Dalamud/Game/Config/UiConfigOption.cs +++ b/Dalamud/Game/Config/UiConfigOption.cs @@ -4069,6 +4069,13 @@ public enum UiConfigOption [GameConfigOption("GposePortraitRotateType", ConfigType.UInt)] GposePortraitRotateType, + /// + /// UiConfig option with the internal name GroupPosePortraitUnlockAspectLimit. + /// This option is a UInt. + /// + [GameConfigOption("GroupPosePortraitUnlockAspectLimit", ConfigType.UInt)] + GroupPosePortraitUnlockAspectLimit, + /// /// UiConfig option with the internal name LsListSortPriority. /// This option is a UInt. From cdf4e2735504040df09be5a27b5fc984349e9c40 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 16 Dec 2025 19:28:09 +0100 Subject: [PATCH 110/164] Bump version to 14.0.0.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 869faf2da..9685b92ac 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 13.0.0.16 + 14.0.0.0 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 01901c237a178fb00bbf271ea684fc058df49109 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 16 Dec 2025 21:01:50 +0100 Subject: [PATCH 111/164] Downgrade Iced to resolve version conflict between Dalamud and Injector --- Directory.Packages.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ec2e7e276..06338efac 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,7 +33,8 @@ - + + From 46954e6add867b1eb5b86a46c1ce647084b664d7 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 16 Dec 2025 21:01:58 +0100 Subject: [PATCH 112/164] Remove plugin targets from SLN --- Dalamud.sln | 2 -- 1 file changed, 2 deletions(-) diff --git a/Dalamud.sln b/Dalamud.sln index ee3c75b25..de91e7ceb 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -7,8 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig .gitignore = .gitignore tools\BannedSymbols.txt = tools\BannedSymbols.txt - targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets - targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets tools\dalamud.ruleset = tools\dalamud.ruleset Directory.Build.props = Directory.Build.props Directory.Packages.props = Directory.Packages.props From f142fb1058887be55cb3b9cbebd370643f4bc3d5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 17 Dec 2025 00:50:14 +0100 Subject: [PATCH 113/164] Set language version to preview for now Fixes a docfx error, since they haven't upgraded to a Roslyn version that knows C# 14 --- Directory.Build.props | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 3897256bf..8a8df22d7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,9 @@ net10.0-windows x64 x64 - 14.0 + + + preview From 19660a20d94dd3af3693cebbae82bbdf477ed6c9 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 16 Dec 2025 21:33:42 +0100 Subject: [PATCH 114/164] Update Condition/ConditionFlag --- Dalamud/Game/ClientState/Conditions/Condition.cs | 2 +- .../Game/ClientState/Conditions/ConditionFlag.cs | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 99748f71b..6f61ab246 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -18,7 +18,7 @@ internal sealed class Condition : IInternalDisposableService, ICondition /// /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// - internal const int MaxConditionEntries = 104; + internal const int MaxConditionEntries = 112; [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); diff --git a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs index 19451dd5c..b5894d891 100644 --- a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs +++ b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs @@ -520,4 +520,17 @@ public enum ConditionFlag PilotingMech = 102, // Unknown103 = 103, + + /// + /// Unable to execute command while editing a strategy board. + /// + EditingStrategyBoard = 104, + + // Unknown105 = 105, + // Unknown106 = 106, + // Unknown107 = 107, + // Unknown108 = 108, + // Unknown109 = 109, + // Unknown110 = 110, + // Unknown111 = 111, } From 841cdf52bd86c3d52e8d1a20add48c9543894949 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 17 Dec 2025 16:13:44 +0100 Subject: [PATCH 115/164] Update Lumina and Lumina.Excel --- ...{ItemActionType.cs => ItemActionAction.cs} | 4 +- Dalamud/Game/UnlockState/RecipeData.cs | 58 ++--------------- Dalamud/Game/UnlockState/UnlockState.cs | 64 +++++++++---------- Directory.Packages.props | 2 +- lib/Lumina.Excel | 2 +- 5 files changed, 43 insertions(+), 87 deletions(-) rename Dalamud/Game/UnlockState/{ItemActionType.cs => ItemActionAction.cs} (96%) diff --git a/Dalamud/Game/UnlockState/ItemActionType.cs b/Dalamud/Game/UnlockState/ItemActionAction.cs similarity index 96% rename from Dalamud/Game/UnlockState/ItemActionType.cs rename to Dalamud/Game/UnlockState/ItemActionAction.cs index 8e3d79b84..0e86fcb67 100644 --- a/Dalamud/Game/UnlockState/ItemActionType.cs +++ b/Dalamud/Game/UnlockState/ItemActionAction.cs @@ -3,9 +3,9 @@ using Lumina.Excel.Sheets; namespace Dalamud.Game.UnlockState; /// -/// Enum for . +/// Enum for . /// -internal enum ItemActionType : ushort +internal enum ItemActionAction : ushort { /// /// No item action. diff --git a/Dalamud/Game/UnlockState/RecipeData.cs b/Dalamud/Game/UnlockState/RecipeData.cs index c419ba4fd..7fa0d4b8f 100644 --- a/Dalamud/Game/UnlockState/RecipeData.cs +++ b/Dalamud/Game/UnlockState/RecipeData.cs @@ -158,67 +158,23 @@ internal unsafe class RecipeData : IInternalDisposableService { noteBookDivisionIndex++; - // For future Lumina.Excel update, replace with: - // if (!notebookDivisionRow.AllowedCraftTypes[craftType]) - // continue; - - switch (craftTypeRow.RowId) - { - case 0 when !noteBookDivisionRow.CRPCraft: continue; - case 1 when !noteBookDivisionRow.BSMCraft: continue; - case 2 when !noteBookDivisionRow.ARMCraft: continue; - case 3 when !noteBookDivisionRow.GSMCraft: continue; - case 4 when !noteBookDivisionRow.LTWCraft: continue; - case 5 when !noteBookDivisionRow.WVRCraft: continue; - case 6 when !noteBookDivisionRow.ALCCraft: continue; - case 7 when !noteBookDivisionRow.CULCraft: continue; - } + if (!noteBookDivisionRow.AllowedCraftTypes[craftType]) + continue; if (noteBookDivisionRow.GatheringOpeningLevel != byte.MaxValue) continue; - // For future Lumina.Excel update, replace with: - // if (notebookDivisionRow.RequiresSecretRecipeBookGroupUnlock) - if (noteBookDivisionRow.Unknown1) + if (noteBookDivisionRow.RequiresSecretRecipeBookGroupUnlock) { var secretRecipeBookUnlocked = false; - // For future Lumina.Excel update, iterate over notebookDivisionRow.SecretRecipeBookGroups - for (var i = 0; i < 2; i++) + foreach (var secretRecipeBookGroup in noteBookDivisionRow.SecretRecipeBookGroups) { - // For future Lumina.Excel update, replace with: - // if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid) - // continue; - var secretRecipeBookGroupRowId = i switch - { - 0 => noteBookDivisionRow.Unknown2, - 1 => noteBookDivisionRow.Unknown2, - _ => default, - }; - - if (secretRecipeBookGroupRowId == 0) + if (secretRecipeBookGroup.RowId == 0 || !secretRecipeBookGroup.IsValid) continue; - if (!this.dataManager.GetExcelSheet().TryGetRow(secretRecipeBookGroupRowId, out var secretRecipeBookGroupRow)) - continue; - - // For future Lumina.Excel update, replace with: - // var bitIndex = secretRecipeBookGroup.Value.UnlockBitIndex[craftType]; - - var bitIndex = craftType switch - { - 0 => secretRecipeBookGroupRow.Unknown0, - 1 => secretRecipeBookGroupRow.Unknown1, - 2 => secretRecipeBookGroupRow.Unknown2, - 3 => secretRecipeBookGroupRow.Unknown3, - 4 => secretRecipeBookGroupRow.Unknown4, - 5 => secretRecipeBookGroupRow.Unknown5, - 6 => secretRecipeBookGroupRow.Unknown6, - 7 => secretRecipeBookGroupRow.Unknown7, - _ => default, - }; - - if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get(bitIndex)) + var bitIndex = secretRecipeBookGroup.Value.SecretRecipeBook[craftType].RowId; + if (PlayerState.Instance()->UnlockedSecretRecipeBooksBitArray.Get((int)bitIndex)) { secretRecipeBookUnlocked = true; break; diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index cd896ffb6..cc70a524c 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -209,7 +209,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState /// public bool IsEmjVoiceNpcUnlocked(EmjVoiceNpc row) { - return this.IsUnlockLinkUnlocked(row.Unknown26); + return this.IsUnlockLinkUnlocked(row.UnlockLink); } /// @@ -217,7 +217,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState { return this.dataManager.GetExcelSheet().TryGetRow(row.RowId, out var emjVoiceNpcRow) && this.IsEmjVoiceNpcUnlocked(emjVoiceNpcRow) - && QuestManager.IsQuestComplete(row.Unknown1); + && QuestManager.IsQuestComplete(row.UnlockQuest.RowId); } /// @@ -264,47 +264,47 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState // To avoid the ExdModule.GetItemRowById call, which can return null if the excel page // is not loaded, we're going to imitate the IsItemActionUnlocked call first: - switch ((ItemActionType)row.ItemAction.Value.Type) + switch ((ItemActionAction)row.ItemAction.Value.Action.RowId) { - case ItemActionType.Companion: + case ItemActionAction.Companion: return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.BuddyEquip: + case ItemActionAction.BuddyEquip: return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.Mount: + case ItemActionAction.Mount: return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.SecretRecipeBook: + case ItemActionAction.SecretRecipeBook: return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.UnlockLink: - case ItemActionType.OccultRecords: + case ItemActionAction.UnlockLink: + case ItemActionAction.OccultRecords: return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.TripleTriadCard when row.AdditionalData.Is(): + case ItemActionAction.TripleTriadCard when row.AdditionalData.Is(): return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId); - case ItemActionType.FolkloreTome: + case ItemActionAction.FolkloreTome: return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.OrchestrionRoll when row.AdditionalData.Is(): + case ItemActionAction.OrchestrionRoll when row.AdditionalData.Is(): return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId); - case ItemActionType.FramersKit: + case ItemActionAction.FramersKit: return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId); - case ItemActionType.Ornament: + case ItemActionAction.Ornament: return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]); - case ItemActionType.Glasses: + case ItemActionAction.Glasses: return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId); - case ItemActionType.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: + case ItemActionAction.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: var supportJobId = (byte)row.ItemAction.Value.Data[0]; return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0; - case ItemActionType.CompanySealVouchers: + case ItemActionAction.CompanySealVouchers: return false; } @@ -327,7 +327,7 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState /// public bool IsMKDLoreUnlocked(MKDLore row) { - return this.IsUnlockLinkUnlocked(row.Unknown2); + return this.IsUnlockLinkUnlocked(row.UnlockLink); } /// @@ -414,20 +414,20 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState if (row.ItemAction.RowId == 0) return false; - return (ItemActionType)row.ItemAction.Value.Type - is ItemActionType.Companion - or ItemActionType.BuddyEquip - or ItemActionType.Mount - or ItemActionType.SecretRecipeBook - or ItemActionType.UnlockLink - or ItemActionType.TripleTriadCard - or ItemActionType.FolkloreTome - or ItemActionType.OrchestrionRoll - or ItemActionType.FramersKit - or ItemActionType.Ornament - or ItemActionType.Glasses - or ItemActionType.OccultRecords - or ItemActionType.SoulShards; + return (ItemActionAction)row.ItemAction.Value.Action.RowId + is ItemActionAction.Companion + or ItemActionAction.BuddyEquip + or ItemActionAction.Mount + or ItemActionAction.SecretRecipeBook + or ItemActionAction.UnlockLink + or ItemActionAction.TripleTriadCard + or ItemActionAction.FolkloreTome + or ItemActionAction.OrchestrionRoll + or ItemActionAction.FramersKit + or ItemActionAction.Ornament + or ItemActionAction.Glasses + or ItemActionAction.OccultRecords + or ItemActionAction.SoulShards; } /// diff --git a/Directory.Packages.props b/Directory.Packages.props index 06338efac..77a4035a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -15,7 +15,7 @@ - + diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 5d01489c3..4650ad332 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 5d01489c34f33a3d645f49085d7fc0065a1ac801 +Subproject commit 4650ad332dd22aeff0d1f7ac33845b1c2aca4f8d From b3c4363e0fad6f0c9c651467330607d09df5cb01 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 17 Dec 2025 17:09:18 +0100 Subject: [PATCH 116/164] Fix crashing Context Menu --- Dalamud/Game/Gui/ContextMenu/ContextMenu.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs index 7512f4160..aada374ec 100644 --- a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -31,7 +31,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM private static readonly ModuleLog Log = new("ContextMenu"); private readonly Hook atkModuleVf22OpenAddonByAgentHook; - private readonly Hook addonContextMenuOnMenuSelectedHook; + private readonly Hook addonContextMenuOnMenuSelectedHook; private uint? addonContextSubNameId; @@ -40,7 +40,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM { var raptureAtkModuleVtable = (nint*)RaptureAtkModule.StaticVirtualTablePointer; this.atkModuleVf22OpenAddonByAgentHook = Hook.FromAddress(raptureAtkModuleVtable[22], this.AtkModuleVf22OpenAddonByAgentDetour); - this.addonContextMenuOnMenuSelectedHook = Hook.FromAddress((nint)AddonContextMenu.StaticVirtualTablePointer->OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour); + this.addonContextMenuOnMenuSelectedHook = Hook.FromAddress((nint)AddonContextMenu.StaticVirtualTablePointer->OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour); this.atkModuleVf22OpenAddonByAgentHook.Enable(); this.addonContextMenuOnMenuSelectedHook.Enable(); @@ -48,10 +48,6 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM private delegate ushort AtkModuleVf22OpenAddonByAgentDelegate(AtkModule* module, byte* addonName, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, bool a8); - private delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3); - - private delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2); - /// public event IContextMenu.OnMenuOpenedDelegate? OnMenuOpened; @@ -185,7 +181,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM values[0].ChangeType(ValueType.UInt); values[0].UInt = 0; values[1].ChangeType(ValueType.String); - values[1].SetManagedString(name.Encode().NullTerminate()); + values[1].SetManagedString(name.EncodeWithNullTerminator()); values[2].ChangeType(ValueType.Int); values[2].Int = x; values[3].ChangeType(ValueType.Int); @@ -265,7 +261,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM submenuMask |= 1u << i; nameData[i].ChangeType(ValueType.String); - nameData[i].SetManagedString(this.GetPrefixedName(item).Encode().NullTerminate()); + nameData[i].SetManagedString(this.GetPrefixedName(item).EncodeWithNullTerminator()); } for (var i = 0; i < prefixMenuSize; ++i) @@ -295,8 +291,9 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM // 2: UInt = Return Mask (?) // 3: UInt = Submenu Mask // 4: UInt = OpenAtCursorPosition ? 2 : 1 - // 5: UInt = 0 - // 6: UInt = 0 + // 5: UInt = ? + // 6: UInt = ? + // 7: UInt = ? foreach (var item in items) { @@ -312,7 +309,7 @@ internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextM } } - this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values); + this.SetupGenericMenu(8, 0, 2, 3, items, ref valueCount, ref values); } private void SetupContextSubMenu(IReadOnlyList items, ref int valueCount, ref AtkValue* values) From 2fc9884aad6217e61909f8b602a4782e7b29fd63 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 17 Dec 2025 17:32:39 +0100 Subject: [PATCH 117/164] Update HoverActionKind --- Dalamud/Game/Gui/HoverActionKind.cs | 61 ++++++++++++++++------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/Dalamud/Game/Gui/HoverActionKind.cs b/Dalamud/Game/Gui/HoverActionKind.cs index ef8fe6400..b786f12ee 100644 --- a/Dalamud/Game/Gui/HoverActionKind.cs +++ b/Dalamud/Game/Gui/HoverActionKind.cs @@ -14,140 +14,145 @@ public enum HoverActionKind /// /// A regular action is hovered. /// - Action = 28, + Action = 29, /// /// A crafting action is hovered. /// - CraftingAction = 29, + CraftingAction = 30, /// /// A general action is hovered. /// - GeneralAction = 30, + GeneralAction = 31, /// /// A companion order type of action is hovered. /// - CompanionOrder = 31, // Game Term: BuddyOrder + CompanionOrder = 32, // Game Term: BuddyOrder /// /// A main command type of action is hovered. /// - MainCommand = 32, + MainCommand = 33, /// /// An extras command type of action is hovered. /// - ExtraCommand = 33, + ExtraCommand = 34, /// /// A companion action is hovered. /// - Companion = 34, + Companion = 35, /// /// A pet order type of action is hovered. /// - PetOrder = 35, + PetOrder = 36, /// /// A trait is hovered. /// - Trait = 36, + Trait = 37, /// /// A buddy action is hovered. /// - BuddyAction = 37, + BuddyAction = 38, /// /// A company action is hovered. /// - CompanyAction = 38, + CompanyAction = 39, /// /// A mount is hovered. /// - Mount = 39, + Mount = 40, /// /// A chocobo race action is hovered. /// - ChocoboRaceAction = 40, + ChocoboRaceAction = 41, /// /// A chocobo race item is hovered. /// - ChocoboRaceItem = 41, + ChocoboRaceItem = 42, /// /// A deep dungeon equipment is hovered. /// - DeepDungeonEquipment = 42, + DeepDungeonEquipment = 43, /// /// A deep dungeon equipment 2 is hovered. /// - DeepDungeonEquipment2 = 43, + DeepDungeonEquipment2 = 44, /// /// A deep dungeon item is hovered. /// - DeepDungeonItem = 44, + DeepDungeonItem = 45, /// /// A quick chat is hovered. /// - QuickChat = 45, + QuickChat = 46, /// /// An action combo route is hovered. /// - ActionComboRoute = 46, + ActionComboRoute = 47, /// /// A pvp trait is hovered. /// - PvPSelectTrait = 47, + PvPSelectTrait = 48, /// /// A squadron action is hovered. /// - BgcArmyAction = 48, + BgcArmyAction = 49, /// /// A perform action is hovered. /// - Perform = 49, + Perform = 50, /// /// A deep dungeon magic stone is hovered. /// - DeepDungeonMagicStone = 50, + DeepDungeonMagicStone = 51, /// /// A deep dungeon demiclone is hovered. /// - DeepDungeonDemiclone = 51, + DeepDungeonDemiclone = 52, /// /// An eureka magia action is hovered. /// - EurekaMagiaAction = 52, + EurekaMagiaAction = 53, /// /// An island sanctuary temporary item is hovered. /// - MYCTemporaryItem = 53, + MYCTemporaryItem = 54, /// /// An ornament is hovered. /// - Ornament = 54, + Ornament = 55, /// /// Glasses are hovered. /// - Glasses = 55, + Glasses = 56, + + /// + /// Phantom Job Trait is hovered. + /// + MKDTrait = 58, } From fc804ba0d05f791e4c6d19d546c077b675b9dd8b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Dec 2025 18:40:02 +0000 Subject: [PATCH 118/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index e5dedba42..305c1629e 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e5dedba42a3fea8f050ea54ac583a5874bf51c6f +Subproject commit 305c1629eed0b1cdca5efb102e37de93d592d155 From 02d4081f2ff877be565858c2d4aacac1c5f5e46a Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:24:09 +0100 Subject: [PATCH 119/164] ci: disable rollup for now --- .github/workflows/rollup.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml index 8fe049ad7..f4e013258 100644 --- a/.github/workflows/rollup.yml +++ b/.github/workflows/rollup.yml @@ -1,8 +1,8 @@ name: Rollup changes to next version on: - push: - branches: - - master +# push: +# branches: +# - master workflow_dispatch: jobs: From 574e0d458201f0e0af172b0a8fda590cf9db2b4a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 00:24:11 +0000 Subject: [PATCH 120/164] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 4650ad332..c74841abc 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 4650ad332dd22aeff0d1f7ac33845b1c2aca4f8d +Subproject commit c74841abce0830ead4437ed2f560bceb6235a538 From 05037dccc7c6b899b1a4ab1483e763e789e28dee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 00:24:13 +0000 Subject: [PATCH 121/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 305c1629e..c0a862043 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 305c1629eed0b1cdca5efb102e37de93d592d155 +Subproject commit c0a8620439e647ccf443710e81acce021b299bf5 From 25dba5e23b9217b7d8f86af1c449a907b04c6f52 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 18 Dec 2025 01:29:48 +0100 Subject: [PATCH 122/164] ci: revert global concurrency change again because it breaks PR workflows Need to figure out something better for this soon, but it's better not to have this at all right now --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9466cb083..209ed90de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,9 +1,8 @@ name: Build Dalamud on: [push, pull_request, workflow_dispatch] -# Globally blocking because of git pushes in deploy step concurrency: - group: build_dalamud_${{ github.repository_owner }} + group: build_dalamud_${{ github.ref_name }} cancel-in-progress: false jobs: From 7f4352dc43df5d53c83b197603ee1383233c37a8 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Wed, 17 Dec 2025 16:38:34 -0800 Subject: [PATCH 123/164] Add address resolver --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 18 ++++ .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 91 ++++++++++--------- Dalamud/Plugin/Services/IAddonLifecycle.cs | 21 +++-- 3 files changed, 81 insertions(+), 49 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 716ce1bfb..78cea1a0f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Runtime.CompilerServices; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -132,6 +133,19 @@ internal unsafe class AddonLifecycle : IInternalDisposableService } } + /// + /// Resolves a virtual table address to the original virtual table address. + /// + /// The modified address to resolve. + /// The original address. + internal AtkUnitBase.AtkUnitBaseVirtualTable* GetOriginalVirtualTable(AtkUnitBase.AtkUnitBaseVirtualTable* tableAddress) + { + var matchedTable = AllocatedTables.FirstOrDefault(table => table.ModifiedVirtualTable == tableAddress); + if (matchedTable == null) return null; + + return matchedTable.OriginalVirtualTable; + } + private void OnAddonInitialize(AtkUnitBase* addon) { try @@ -246,4 +260,8 @@ internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLi }); } } + + /// + public unsafe nint GetOriginalVirtualTable(nint virtualTableAddress) + => (nint)this.addonLifecycleService.GetOriginalVirtualTable((AtkUnitBase.AtkUnitBaseVirtualTable*)virtualTableAddress); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 47ff92c3d..975ff027d 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -16,6 +16,16 @@ namespace Dalamud.Game.Addon.Lifecycle; /// internal unsafe class AddonVirtualTable : IDisposable { + /// + /// The original virtual table address for this addon. + /// + internal readonly AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable; + + /// + /// The modified virtual address for this addon. + /// + internal readonly AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable; + // This need to be at minimum the largest virtual table size of all addons // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; @@ -45,9 +55,6 @@ internal unsafe class AddonVirtualTable : IDisposable private readonly AtkUnitBase* atkUnitBase; - private readonly AtkUnitBase.AtkUnitBaseVirtualTable* originalVirtualTable; - private readonly AtkUnitBase.AtkUnitBaseVirtualTable* modifiedVirtualTable; - // 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 AtkUnitBase.Delegates.Dtor destructorFunction; @@ -78,16 +85,16 @@ internal unsafe class AddonVirtualTable : IDisposable this.lifecycleService = lifecycleService; // Save original virtual table - this.originalVirtualTable = addon->VirtualTable; + this.OriginalVirtualTable = addon->VirtualTable; // Create copy of original table // Note this will copy any derived/overriden functions that this specific addon has. // Note: currently there are 73 virtual functions, but there's no harm in copying more for when they add new virtual functions to the game - this.modifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8); - NativeMemory.Copy(addon->VirtualTable, this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + this.ModifiedVirtualTable = (AtkUnitBase.AtkUnitBaseVirtualTable*)IMemorySpace.GetUISpace()->Malloc(0x8 * VirtualTableEntryCount, 8); + NativeMemory.Copy(addon->VirtualTable, this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount); // Overwrite the addons existing virtual table with our own - addon->VirtualTable = this.modifiedVirtualTable; + addon->VirtualTable = this.ModifiedVirtualTable; // Pin each of our listener functions this.destructorFunction = this.OnAddonDestructor; @@ -108,30 +115,30 @@ internal unsafe class AddonVirtualTable : IDisposable this.focusFunction = this.OnAddonFocus; // Overwrite specific virtual table entries - this.modifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); - this.modifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction); - this.modifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction); - this.modifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction); - this.modifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); - this.modifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction); - this.modifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction); - this.modifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction); - this.modifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction); - this.modifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); - this.modifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); - this.modifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); - this.modifiedVirtualTable->OnMove = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction); - this.modifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); - this.modifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); - this.modifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); + this.ModifiedVirtualTable->Dtor = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.destructorFunction); + this.ModifiedVirtualTable->OnSetup = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onSetupFunction); + this.ModifiedVirtualTable->Finalizer = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.finalizerFunction); + this.ModifiedVirtualTable->Draw = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.drawFunction); + this.ModifiedVirtualTable->Update = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.updateFunction); + this.ModifiedVirtualTable->OnRefresh = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRefreshFunction); + this.ModifiedVirtualTable->OnRequestedUpdate = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onRequestedUpdateFunction); + this.ModifiedVirtualTable->ReceiveEvent = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onReceiveEventFunction); + this.ModifiedVirtualTable->Open = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.openFunction); + this.ModifiedVirtualTable->Close = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.closeFunction); + this.ModifiedVirtualTable->Show = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.showFunction); + this.ModifiedVirtualTable->Hide = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.hideFunction); + this.ModifiedVirtualTable->OnMove = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMoveFunction); + this.ModifiedVirtualTable->OnMouseOver = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOverFunction); + this.ModifiedVirtualTable->OnMouseOut = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.onMouseOutFunction); + this.ModifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); } /// public void Dispose() { // Ensure restoration is done atomically. - Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.originalVirtualTable); - IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + Interlocked.Exchange(ref *(nint*)&this.atkUnitBase->VirtualTable, (nint)this.OriginalVirtualTable); + IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount); } private AtkEventListener* OnAddonDestructor(AtkUnitBase* thisPtr, byte freeFlags) @@ -144,7 +151,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - result = this.originalVirtualTable->Dtor(thisPtr, freeFlags); + result = this.OriginalVirtualTable->Dtor(thisPtr, freeFlags); } catch (Exception e) { @@ -153,7 +160,7 @@ internal unsafe class AddonVirtualTable : IDisposable if ((freeFlags & 1) == 1) { - IMemorySpace.Free(this.modifiedVirtualTable, 0x8 * VirtualTableEntryCount); + IMemorySpace.Free(this.ModifiedVirtualTable, 0x8 * VirtualTableEntryCount); AddonLifecycle.AllocatedTables.Remove(this); } } @@ -182,7 +189,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnSetup(addon, valueCount, values); + this.OriginalVirtualTable->OnSetup(addon, valueCount, values); } catch (Exception e) { @@ -209,7 +216,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Finalizer(thisPtr); + this.OriginalVirtualTable->Finalizer(thisPtr); } catch (Exception e) { @@ -234,7 +241,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Draw(addon); + this.OriginalVirtualTable->Draw(addon); } catch (Exception e) { @@ -265,7 +272,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Update(addon, delta); + this.OriginalVirtualTable->Update(addon, delta); } catch (Exception e) { @@ -299,7 +306,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - result = this.originalVirtualTable->OnRefresh(addon, valueCount, values); + result = this.OriginalVirtualTable->OnRefresh(addon, valueCount, values); } catch (Exception e) { @@ -333,7 +340,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); + this.OriginalVirtualTable->OnRequestedUpdate(addon, numberArrayData, stringArrayData); } catch (Exception e) { @@ -369,7 +376,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); + this.OriginalVirtualTable->ReceiveEvent(addon, eventType, eventParam, atkEvent, atkEventData); } catch (Exception e) { @@ -398,7 +405,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - result = this.originalVirtualTable->Open(thisPtr, depthLayer); + result = this.OriginalVirtualTable->Open(thisPtr, depthLayer); } catch (Exception e) { @@ -432,7 +439,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - result = this.originalVirtualTable->Close(thisPtr, fireCallback); + result = this.OriginalVirtualTable->Close(thisPtr, fireCallback); } catch (Exception e) { @@ -466,7 +473,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); + this.OriginalVirtualTable->Show(thisPtr, silenceOpenSoundEffect, unsetShowHideFlags); } catch (Exception e) { @@ -500,7 +507,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); + this.OriginalVirtualTable->Hide(thisPtr, unkBool, callHideCallback, setShowHideFlags); } catch (Exception e) { @@ -527,7 +534,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnMove(thisPtr); + this.OriginalVirtualTable->OnMove(thisPtr); } catch (Exception e) { @@ -554,7 +561,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnMouseOver(thisPtr); + this.OriginalVirtualTable->OnMouseOver(thisPtr); } catch (Exception e) { @@ -581,7 +588,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->OnMouseOut(thisPtr); + this.OriginalVirtualTable->OnMouseOut(thisPtr); } catch (Exception e) { @@ -608,7 +615,7 @@ internal unsafe class AddonVirtualTable : IDisposable try { - this.originalVirtualTable->Focus(thisPtr); + this.OriginalVirtualTable->Focus(thisPtr); } catch (Exception e) { diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 1269b13dc..aeff29811 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -17,7 +17,7 @@ public interface IAddonLifecycle : IDalamudService /// The event type that triggered the message. /// Information about what addon triggered the message. public delegate void AddonEventDelegate(AddonEvent type, AddonArgs args); - + /// /// Register a listener that will trigger on the specified event and any of the specified addons. /// @@ -25,7 +25,7 @@ public interface IAddonLifecycle : IDalamudService /// Addon names that will trigger the handler to be invoked. /// The handler to invoke. void RegisterListener(AddonEvent eventType, IEnumerable addonNames, AddonEventDelegate handler); - + /// /// Register a listener that will trigger on the specified event only for the specified addon. /// @@ -33,14 +33,14 @@ public interface IAddonLifecycle : IDalamudService /// The addon name that will trigger the handler to be invoked. /// The handler to invoke. void RegisterListener(AddonEvent eventType, string addonName, AddonEventDelegate handler); - + /// /// Register a listener that will trigger on the specified event for any addon. /// /// Event type to trigger on. /// The handler to invoke. void RegisterListener(AddonEvent eventType, AddonEventDelegate handler); - + /// /// Unregister listener from specified event type and specified addon names. /// @@ -51,7 +51,7 @@ public interface IAddonLifecycle : IDalamudService /// Addon names to deregister. /// Optional specific handler to remove. void UnregisterListener(AddonEvent eventType, IEnumerable addonNames, [Optional] AddonEventDelegate handler); - + /// /// Unregister all listeners for the specified event type and addon name. /// @@ -62,7 +62,7 @@ public interface IAddonLifecycle : IDalamudService /// Addon name to deregister. /// Optional specific handler to remove. void UnregisterListener(AddonEvent eventType, string addonName, [Optional] AddonEventDelegate handler); - + /// /// Unregister an event type handler.
This will only remove a handler that is added via . ///
@@ -72,10 +72,17 @@ public interface IAddonLifecycle : IDalamudService /// Event type to deregister. /// Optional specific handler to remove. void UnregisterListener(AddonEvent eventType, [Optional] AddonEventDelegate handler); - + /// /// Unregister all events that use the specified handlers. /// /// Handlers to remove. void UnregisterListener(params AddonEventDelegate[] handlers); + + /// + /// Resolves an addons virtual table address back to the original unmodified table address. + /// + /// The address of a modified addons virtual table. + /// The address of the addons original virtual table. + nint GetOriginalVirtualTable(nint virtualTableAddress); } From 37fa40ab587002b4dd8180eb2872d5b5ef10fc37 Mon Sep 17 00:00:00 2001 From: MidoriKami Date: Wed, 17 Dec 2025 16:44:04 -0800 Subject: [PATCH 124/164] Make stylecop happy --- .../Game/Addon/Lifecycle/AddonVirtualTable.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs index 975ff027d..736415738 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonVirtualTable.cs @@ -16,16 +16,6 @@ namespace Dalamud.Game.Addon.Lifecycle; ///
internal unsafe class AddonVirtualTable : IDisposable { - /// - /// The original virtual table address for this addon. - /// - internal readonly AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable; - - /// - /// The modified virtual address for this addon. - /// - internal readonly AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable; - // This need to be at minimum the largest virtual table size of all addons // Copying extra entries is not problematic, and is considered safe. private const int VirtualTableEntryCount = 200; @@ -133,6 +123,16 @@ internal unsafe class AddonVirtualTable : IDisposable this.ModifiedVirtualTable->Focus = (delegate* unmanaged)Marshal.GetFunctionPointerForDelegate(this.focusFunction); } + /// + /// Gets the original virtual table address for this addon. + /// + internal AtkUnitBase.AtkUnitBaseVirtualTable* OriginalVirtualTable { get; private set; } + + /// + /// Gets the modified virtual address for this addon. + /// + internal AtkUnitBase.AtkUnitBaseVirtualTable* ModifiedVirtualTable { get; private set; } + /// public void Dispose() { From 3a1e1e6425acf5be0a4721aae505f732f1cc0977 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 06:40:14 +0000 Subject: [PATCH 125/164] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index c74841abc..d8d0b53e2 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit c74841abce0830ead4437ed2f560bceb6235a538 +Subproject commit d8d0b53e27393f509ac5397511cb8d251d562277 From 17c0527f2d7b99ca988776071539528491942b5e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 06:40:20 +0000 Subject: [PATCH 126/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index c0a862043..90168316b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit c0a8620439e647ccf443710e81acce021b299bf5 +Subproject commit 90168316b4c5e3af2746a1bdea52fb10f9113862 From bd87a3d25156e25fab43a14caef9fde6a134d0ea Mon Sep 17 00:00:00 2001 From: Critical Impact Date: Thu, 18 Dec 2025 22:01:49 +1000 Subject: [PATCH 127/164] Add git hash/scm version properties to DalamudVersionInfo --- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- .../Plugin/VersionInfo/DalamudVersionInfo.cs | 11 ++++++++++- .../Plugin/VersionInfo/IDalamudVersionInfo.cs | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 90850a08b..e42bbe608 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -220,7 +220,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// public IDalamudVersionInfo GetDalamudVersion() { - return new DalamudVersionInfo(Versioning.GetAssemblyVersionParsed(), Versioning.GetActiveTrack()); + return new DalamudVersionInfo(Versioning.GetAssemblyVersionParsed(), Versioning.GetActiveTrack(), Versioning.GetGitHash(), Versioning.GetGitHashClientStructs(), Versioning.GetScmVersion()); } #region IPC diff --git a/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs index c87c012af..0a6fad9c2 100644 --- a/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs +++ b/Dalamud/Plugin/VersionInfo/DalamudVersionInfo.cs @@ -1,11 +1,20 @@ namespace Dalamud.Plugin.VersionInfo; /// -internal class DalamudVersionInfo(Version version, string? track) : IDalamudVersionInfo +internal class DalamudVersionInfo(Version version, string? track, string? gitHash, string? gitHashClientStructs, string? scmVersion) : IDalamudVersionInfo { /// public Version Version { get; } = version; /// public string? BetaTrack { get; } = track; + + /// + public string? GitHash { get; } = gitHash; + + /// + public string? GitHashClientStructs { get; } = gitHashClientStructs; + + /// + public string? ScmVersion { get; } = scmVersion; } diff --git a/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs b/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs index e6b6a9601..6297ce196 100644 --- a/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs +++ b/Dalamud/Plugin/VersionInfo/IDalamudVersionInfo.cs @@ -16,4 +16,22 @@ public interface IDalamudVersionInfo /// Null if this build wasn't launched from XIVLauncher. ///
string? BetaTrack { get; } + + /// + /// Gets the git commit hash value from the assembly or null if it cannot be found. Will be null for Debug builds, + /// and will be suffixed with `-dirty` if in release with pending changes. + /// + string? GitHash { get; } + + /// + /// Gets the git hash value from the assembly or null if it cannot be found. + /// + string? GitHashClientStructs { get; } + + /// + /// Gets the SCM Version from the assembly, or null if it cannot be found. The value returned will generally be + /// the git describe output for this build, which will be a raw version if this is a stable build or an + /// appropriately-annotated version if this is *not* stable. Local builds will return a `Local Build` text string. + /// + string? ScmVersion { get; } } From 984bdbcf0ed616a040d0f344b502393f34ce4dbd Mon Sep 17 00:00:00 2001 From: Critical Impact Date: Thu, 18 Dec 2025 22:07:58 +1000 Subject: [PATCH 128/164] Use built dlls instead of csproj for docfx --- docfx.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docfx.json b/docfx.json index 30c85957c..cf7a80194 100644 --- a/docfx.json +++ b/docfx.json @@ -4,18 +4,17 @@ "src": [ { "files": [ - "Dalamud.Interface/Dalamud.Interface.csproj", - "Dalamud/Dalamud.csproj", - "lib/ImGuiScene/ImGuiScene/ImGuiScene.csproj", - "lib/ImGuiScene/deps/ImGui.NET/src/ImGui.NET-472/ImGui.NET-472.csproj", - "lib/ImGuiScene/deps/SDL2-CS/SDL2-CS.csproj" + "bin/Release/Dalamud.dll" ] } ], "dest": "api", "disableGitFeatures": false, "disableDefaultFilter": false, - "filter": "filterConfig.yml" + "filter": "filterConfig.yml", + "properties": { + "TargetFramework": "net10.0-windows" + } } ], "build": { From 0d533c18f8f80894c1f87f3cc001606c8dc3c656 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 18 Dec 2025 13:21:49 +0000 Subject: [PATCH 129/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 90168316b..df206b5f6 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 90168316b4c5e3af2746a1bdea52fb10f9113862 +Subproject commit df206b5f61855e3ba73f93fd57bc07056698ac4a From 3c8cef06dd25bf189e136c3649d81f2f455c6551 Mon Sep 17 00:00:00 2001 From: Loskh <1020612624@qq.com> Date: Thu, 18 Dec 2025 21:54:28 +0800 Subject: [PATCH 130/164] fix: EventItem name for Japanese client. --- Dalamud/Utility/ItemUtil.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs index 5f718bcee..b632d14d7 100644 --- a/Dalamud/Utility/ItemUtil.cs +++ b/Dalamud/Utility/ItemUtil.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.Text; + using Lumina.Excel.Sheets; using Lumina.Text; using Lumina.Text.ReadOnly; @@ -125,10 +126,15 @@ public static class ItemUtil if (IsEventItem(itemId)) { + // Only English, German, and French have a Name field. + // For other languages, the Name is an empty string, and the Singular field should be used instead. + language ??= dataManager.Language; + var useSingular = language is not (ClientLanguage.English or ClientLanguage.German or ClientLanguage.French); + return dataManager .GetExcelSheet(language) .TryGetRow(itemId, out var eventItem) - ? eventItem.Name + ? (useSingular ? eventItem.Singular : eventItem.Name) : default; } From 0b1a697d4df81fc996bfc791c9ac8464fec01512 Mon Sep 17 00:00:00 2001 From: Infi Date: Thu, 18 Dec 2025 15:38:57 +0100 Subject: [PATCH 131/164] - Comment out erroring unknown prints --- Dalamud/Interface/Internal/UiDebug.cs | 14 ++++++------ .../UiDebug2/Browsing/NodeTree.Component.cs | 22 +++++++------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 1211b505d..82554995b 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -420,13 +420,13 @@ internal unsafe class UiDebug ImGui.SameLine(); Service.Get().Draw(textInputComponent->AtkComponentInputBase.RawString); - ImGui.Text("Text1: "u8); - ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText01); - - ImGui.Text("Text2: "u8); - ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText02); + // ImGui.Text("Text1: "u8); + // ImGui.SameLine(); + // Service.Get().Draw(textInputComponent->UnkText01); + // + // ImGui.Text("Text2: "u8); + // ImGui.SameLine(); + // Service.Get().Draw(textInputComponent->UnkText02); ImGui.Text("AvailableLines: "u8); ImGui.SameLine(); diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs index a35195498..922d226b6 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs @@ -89,20 +89,14 @@ internal unsafe class ComponentNodeTree : ResNodeTree { case TextInput: var textInputComponent = (AtkComponentTextInput*)this.Component; - ImGui.Text( - $"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.EvaluatedString.StringPtr))}"); - ImGui.Text( - $"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.RawString.StringPtr))}"); - ImGui.Text( - $"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}"); - ImGui.Text( - $"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}"); - ImGui.Text( - $"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}"); - ImGui.Text( - $"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}"); - ImGui.Text( - $"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}"); + ImGui.Text($"InputBase Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.EvaluatedString.StringPtr))}"); + ImGui.Text($"InputBase Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->AtkComponentInputBase.RawString.StringPtr))}"); + // TODO: Reenable when unknowns have been unprivated / named + // ImGui.Text($"Text1: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText01.StringPtr))}"); + // ImGui.Text($"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}"); + ImGui.Text($"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}"); + ImGui.Text($"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}"); + ImGui.Text($"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}"); break; case List: case TreeList: From 3eb65c85c06c357c3af89725f75213caa4825cbf Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:08:27 +0100 Subject: [PATCH 132/164] Update ClientStructs (#2524) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index df206b5f6..a88271426 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit df206b5f61855e3ba73f93fd57bc07056698ac4a +Subproject commit a8827142678d35e62ab0c1bafe94d607271af010 From a56d2cf40be3a09c3d9105e722c2c84492e90887 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Dec 2025 20:28:03 +0100 Subject: [PATCH 133/164] Add verifier for hook signatures This one is real bad, so we should make sure everyone using a canonical signature --- Dalamud/Dalamud.cs | 6 + Dalamud/Hooking/Hook.cs | 3 + .../Verification/HookVerificationException.cs | 41 +++++++ .../Internal/Verification/HookVerifier.cs | 107 ++++++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 Dalamud/Hooking/Internal/Verification/HookVerificationException.cs create mode 100644 Dalamud/Hooking/Internal/Verification/HookVerifier.cs diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index a411883d5..2d32b8e8a 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; +using Dalamud.Hooking.Internal.Verification; using Dalamud.Plugin.Internal; using Dalamud.Storage; using Dalamud.Utility; @@ -73,6 +74,11 @@ internal sealed unsafe class Dalamud : IServiceType scanner, Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride)); + using (Timings.Start("HookVerifier Init")) + { + HookVerifier.Initialize(scanner); + } + // Set up FFXIVClientStructs this.SetupClientStructsResolver(cacheDir); diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 1cd3ef91d..b8fd78b4f 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; using Dalamud.Hooking.Internal; +using Dalamud.Hooking.Internal.Verification; namespace Dalamud.Hooking; @@ -230,6 +231,8 @@ public abstract class Hook : IDalamudHook where T : Delegate if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; + HookVerifier.Verify(procAddress); + procAddress = HookManager.FollowJmp(procAddress); if (useMinHook) return new MinHookHook(procAddress, detour, Assembly.GetCallingAssembly()); diff --git a/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs b/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs new file mode 100644 index 000000000..c43b5d540 --- /dev/null +++ b/Dalamud/Hooking/Internal/Verification/HookVerificationException.cs @@ -0,0 +1,41 @@ +using System.Linq; + +namespace Dalamud.Hooking.Internal.Verification; + +/// +/// Exception thrown when a provided delegate for a hook does not match a known delegate. +/// +public class HookVerificationException : Exception +{ + private HookVerificationException(string message) + : base(message) + { + } + + /// + /// Create a new exception. + /// + /// The address of the function that is being hooked. + /// The delegate passed by the user. + /// The delegate we think is correct. + /// Additional context to show to the user. + /// The created exception. + internal static HookVerificationException Create(IntPtr address, Type passed, Type enforced, string message) + { + return new HookVerificationException( + $"Hook verification failed for address 0x{address.ToInt64():X}\n\n" + + $"Why: {message}\n" + + $"Passed Delegate: {GetSignature(passed)}\n" + + $"Correct Delegate: {GetSignature(enforced)}\n\n" + + "The hook delegate must exactly match the provided signature to prevent memory corruption and wrong data passed to originals."); + } + + private static string GetSignature(Type delegateType) + { + var method = delegateType.GetMethod("Invoke"); + if (method == null) return delegateType.Name; + + var parameters = string.Join(", ", method.GetParameters().Select(p => p.ParameterType.Name)); + return $"{method.ReturnType.Name} ({parameters})"; + } +} diff --git a/Dalamud/Hooking/Internal/Verification/HookVerifier.cs b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs new file mode 100644 index 000000000..ad68ae38e --- /dev/null +++ b/Dalamud/Hooking/Internal/Verification/HookVerifier.cs @@ -0,0 +1,107 @@ +using System.Linq; + +using Dalamud.Game; +using Dalamud.Logging.Internal; + +namespace Dalamud.Hooking.Internal.Verification; + +/// +/// Global utility that can verify whether hook delegates are correctly declared. +/// Initialized out-of-band, since Hook is instantiated all over the place without a service, so this cannot be +/// a service either. +/// +internal static class HookVerifier +{ + private static readonly ModuleLog Log = new("HookVerifier"); + + private static readonly VerificationEntry[] ToVerify = + [ + new( + "ActorControlSelf", + "E8 ?? ?? ?? ?? 0F B7 0B 83 E9 64", + typeof(ActorControlSelfDelegate), + "Signature changed in Patch 7.4") // 7.4 (new parameters) + ]; + + private delegate void ActorControlSelfDelegate(uint category, uint eventId, uint param1, uint param2, uint param3, uint param4, uint param5, uint param6, uint param7, uint param8, ulong targetId, byte param9); + + /// + /// Initializes a new instance of the class. + /// + /// Process to scan in. + public static void Initialize(TargetSigScanner scanner) + { + foreach (var entry in ToVerify) + { + if (!scanner.TryScanText(entry.Signature, out var address)) + { + Log.Error("Could not resolve signature for hook {Name} ({Sig})", entry.Name, entry.Signature); + continue; + } + + entry.Address = address; + } + } + + /// + /// Verify the hook with the provided address and exception. + /// + /// The address of the function we are hooking. + /// The delegate type passed by the creator of the hook. + /// Exception thrown when we think the hook is not correctly declared. + public static void Verify(IntPtr address) where T : Delegate + { + var entry = ToVerify.FirstOrDefault(x => x.Address == address); + + // Nothing to verify for this hook? + if (entry == null) + { + return; + } + + var passedType = typeof(T); + + // Directly compare delegates + if (passedType == entry.TargetDelegateType) + { + return; + } + + var passedInvoke = passedType.GetMethod("Invoke")!; + var enforcedInvoke = entry.TargetDelegateType.GetMethod("Invoke")!; + + // Compare Return Type + var mismatch = passedInvoke.ReturnType != enforcedInvoke.ReturnType; + + // Compare Parameter Count + var passedParams = passedInvoke.GetParameters(); + var enforcedParams = enforcedInvoke.GetParameters(); + + if (passedParams.Length != enforcedParams.Length) + { + mismatch = true; + } + else + { + // Compare Parameter Types + for (var i = 0; i < passedParams.Length; i++) + { + if (passedParams[i].ParameterType != enforcedParams[i].ParameterType) + { + mismatch = true; + break; + } + } + } + + if (mismatch) + { + throw HookVerificationException.Create(address, passedType, entry.TargetDelegateType, entry.Message); + } + } + + private record VerificationEntry(string Name, string Signature, Type TargetDelegateType, string Message) + { + public nint Address { get; set; } + } +} From 19fca721e9f6e68a292b04ba9968e4f1454c934c Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Dec 2025 20:55:04 +0100 Subject: [PATCH 134/164] Make obsoletions for ClientState error --- Dalamud/Plugin/Services/IClientState.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 2555b3b30..28b8494b2 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -109,13 +109,13 @@ public interface IClientState : IDalamudService /// /// Gets the local player character, if one is present. /// - [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.")] + [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.", true)] public IPlayerCharacter? LocalPlayer { get; } /// /// Gets the content ID of the local character. /// - [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}")] + [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}", true)] public ulong LocalContentId { get; } /// From c005bae265a49ed33217d97151d863fb92a03324 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Dec 2025 21:00:07 +0100 Subject: [PATCH 135/164] Revert obsolete as error again, fix warnings, Api14ToDo => Api15ToDo --- Dalamud/Configuration/PluginConfigurations.cs | 2 +- .../AddonArgTypes/AddonRefreshArgs.cs | 2 ++ .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 2 ++ Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +- Dalamud/Interface/Animation/Easing.cs | 2 +- .../Plugin/Ipc/Internal/CallGateChannel.cs | 11 +++++++++ Dalamud/Plugin/Services/IClientState.cs | 7 ++++-- Dalamud/Utility/Api14ToDoAttribute.cs | 24 ------------------- 8 files changed, 23 insertions(+), 29 deletions(-) delete mode 100644 Dalamud/Utility/Api14ToDoAttribute.cs diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index c01ab2af0..7ce4697cb 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -11,7 +11,7 @@ namespace Dalamud.Configuration; /// /// Configuration to store settings for a dalamud plugin. /// -[Api14ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] +[Api15ToDo("Make this a service. We need to be able to dispose it reliably to write configs asynchronously. Maybe also let people write files with vfs.")] public sealed class PluginConfigurations { private readonly DirectoryInfo configDirectory; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index 4fc81632a..d81d262bf 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -55,7 +55,9 @@ public class AddonRefreshArgs : AddonArgs AtkValuePtr ptr; unsafe { +#pragma warning disable CS0618 // Type or member is obsolete ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); +#pragma warning restore CS0618 // Type or member is obsolete } yield return ptr; diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index e0b2defbf..1cc0eacf3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -55,7 +55,9 @@ public class AddonSetupArgs : AddonArgs AtkValuePtr ptr; unsafe { +#pragma warning disable CS0618 // Type or member is obsolete ptr = new AtkValuePtr((nint)this.AtkValueSpan.GetPointer(i)); +#pragma warning restore CS0618 // Type or member is obsolete } yield return ptr; diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index af85f9228..138484580 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -150,7 +150,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry } /// - [Api14ToDo("Maybe make this config scoped to internal name?")] + [Api15ToDo("Maybe make this config scoped to internal name?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index cc1f48ce7..a9dfad1f0 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -48,7 +48,7 @@ public abstract class Easing /// Gets the current value of the animation, following unclamped logic. /// [Obsolete($"This field has been deprecated. Use either {nameof(ValueClamped)} or {nameof(ValueUnclamped)} instead.", true)] - [Api14ToDo("Map this field to ValueClamped, probably.")] + [Api15ToDo("Map this field to ValueClamped, probably.")] public double Value => this.ValueUnclamped; /// diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index e177abab7..8bd631b0e 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -149,16 +149,27 @@ internal class CallGateChannel return (TRet)result; } + /// + /// Set the context for the invocations through this channel. + /// + /// The context to set. internal void SetInvocationContext(IpcContext ipcContext) { this.ipcExecutionContext.Value = ipcContext; } + /// + /// Get the context for invocations through this channel. + /// + /// The context, if one was set. internal IpcContext? GetInvocationContext() { return this.ipcExecutionContext.IsValueCreated ? this.ipcExecutionContext.Value : null; } + /// + /// Clear the context for this channel. + /// internal void ClearInvocationContext() { this.ipcExecutionContext.Value = null; diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 28b8494b2..9e7453c25 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -2,6 +2,7 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Utility; namespace Dalamud.Plugin.Services; @@ -109,13 +110,15 @@ public interface IClientState : IDalamudService /// /// Gets the local player character, if one is present. /// - [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.", true)] + [Api15ToDo("Remove")] + [Obsolete($"Use {nameof(IPlayerState)} or {nameof(IObjectTable)}.{nameof(IObjectTable.LocalPlayer)} if necessary.")] public IPlayerCharacter? LocalPlayer { get; } /// /// Gets the content ID of the local character. /// - [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}", true)] + [Api15ToDo("Remove")] + [Obsolete($"Use {nameof(IPlayerState)}.{nameof(IPlayerState.ContentId)}")] public ulong LocalContentId { get; } /// diff --git a/Dalamud/Utility/Api14ToDoAttribute.cs b/Dalamud/Utility/Api14ToDoAttribute.cs deleted file mode 100644 index 945b6e4db..000000000 --- a/Dalamud/Utility/Api14ToDoAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dalamud.Utility; - -/// -/// Utility class for marking something to be changed for API 13, for ease of lookup. -/// -[AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api14ToDoAttribute : Attribute -{ - /// - /// Marks that this should be made internal. - /// - public const string MakeInternal = "Make internal."; - - /// - /// Initializes a new instance of the class. - /// - /// The explanation. - /// The explanation 2. - public Api14ToDoAttribute(string what, string what2 = "") - { - _ = what; - _ = what2; - } -} From db5f27518fb6ec50cdfa5d6dcd39e10cbb4f1fd7 Mon Sep 17 00:00:00 2001 From: CMDRNuffin <4348470+CMDRNuffin@users.noreply.github.com> Date: Fri, 19 Dec 2025 01:24:43 +0100 Subject: [PATCH 136/164] Prevent ImGui text box methods from cloning unchanged input every frame The overloads taking a string by ref for the input text of the various ways to display a text box would all take the input string, copy it into a buffer for imgui and then unconditionally produce a new string once the imgui call returned. Now we only create a new string when the return value of the native function actually indicates that the text changed. This makes the GC happy, and also users like me who like to make the GC happy. Other side effects: The assumption that the reference doesn't change if the method returns false, which is very reasonable IMO, is now correct. --- .../Custom/ImGui.Manual.cs | 153 ++++++++++++++---- 1 file changed, 119 insertions(+), 34 deletions(-) diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs index 89b3cc3d6..ce1bf961d 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs @@ -127,8 +127,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -140,8 +145,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -153,8 +163,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -166,8 +181,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -287,8 +307,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -300,8 +325,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -314,8 +344,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -328,8 +363,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -388,8 +428,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -401,8 +446,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -414,8 +464,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -427,8 +482,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -477,8 +537,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -490,8 +555,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -503,8 +573,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -516,8 +591,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -541,8 +621,13 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = TempInputText(bb, id, label, t.Buffer[..(maxLength + 1)], flags); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + if (r) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } From 86e12f411d672a511ee3e51d6660ce3b6f4f489c Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 19 Dec 2025 03:26:53 +0100 Subject: [PATCH 137/164] Update UIColor widget --- .../Windows/Data/Widgets/UIColorWidget.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index e52a291ef..3550f053c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -44,7 +44,7 @@ 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, 5)) + if (!ImGui.BeginTable("UIColor"u8, 7)) return; ImGui.TableSetupScrollFreeze(0, 1); @@ -62,6 +62,8 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableSetupColumn("Light"u8, ImGuiTableColumnFlags.WidthFixed, colorw); ImGui.TableSetupColumn("Classic FF"u8, ImGuiTableColumnFlags.WidthFixed, colorw); ImGui.TableSetupColumn("Clear Blue"u8, ImGuiTableColumnFlags.WidthFixed, colorw); + ImGui.TableSetupColumn("Clear White"u8, ImGuiTableColumnFlags.WidthFixed, colorw); + ImGui.TableSetupColumn("Clear Green"u8, ImGuiTableColumnFlags.WidthFixed, colorw); ImGui.TableHeadersRow(); var clipper = ImGui.ImGuiListClipper(); @@ -120,6 +122,22 @@ internal class UiColorWidget : IDataWindowWidget adjacentRow.HasValue) DrawEdgePreview(id, row.ClearBlue, adjacentRow.Value.ClearBlue); ImGui.PopID(); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.PushID($"row{id}_white"); + if (this.DrawColorColumn(row.Unknown0) && + adjacentRow.HasValue) + DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0); + ImGui.PopID(); + + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.PushID($"row{id}_green"); + if (this.DrawColorColumn(row.Unknown1) && + adjacentRow.HasValue) + DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1); + ImGui.PopID(); } } From 7eea7d6182c6becc92175b158d6782038d9045af Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 19 Dec 2025 03:35:52 +0100 Subject: [PATCH 138/164] Update ClientStructs (#2525) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index a88271426..f60c282d6 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit a8827142678d35e62ab0c1bafe94d607271af010 +Subproject commit f60c282d63b4157a8f8fb7cbb7e0b35361cdaa12 From 7af0523e886f314461229067a541c993f1498e43 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 06:38:57 +0000 Subject: [PATCH 139/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index f60c282d6..7227f6b12 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit f60c282d63b4157a8f8fb7cbb7e0b35361cdaa12 +Subproject commit 7227f6b1222d1149e0b2e26d2dc31acf341df1cc From f3f4ced0495781bedd04d0345576f695699c4bb6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 06:39:07 +0000 Subject: [PATCH 140/164] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index d8d0b53e2..7d3f90e61 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit d8d0b53e27393f509ac5397511cb8d251d562277 +Subproject commit 7d3f90e61732df6aef63196d1abaab1074f6f3c9 From 89c46944b6832e6e562009c6aa756a5b871fef2f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 19 Dec 2025 10:20:39 +0000 Subject: [PATCH 141/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 7227f6b12..faf803a76 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7227f6b1222d1149e0b2e26d2dc31acf341df1cc +Subproject commit faf803a76813511768d45c137a543aaacf5420b8 From 4ddaaf3809e82adab97d6761931ece609b7f7c2f Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:41:33 +0100 Subject: [PATCH 142/164] Add new themes and update themed path logic --- .../Interface/Internal/Windows/Data/Widgets/UldWidget.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index bc12f4d28..019154b53 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -26,8 +26,8 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class UldWidget : IDataWindowWidget { // ULD styles can be hardcoded for now as they don't add new ones regularly. Can later try and find where to load these from in the game EXE. - private static readonly string[] ThemeDisplayNames = ["Dark", "Light", "Classic FF", "Clear Blue"]; - private static readonly string[] ThemeBasePaths = ["ui/uld/", "ui/uld/img01/", "ui/uld/img02/", "ui/uld/img03/"]; + private static readonly string[] ThemeDisplayNames = ["Dark", "Light", "Classic FF", "Clear Blue", "Clear White", "Clear Green"]; + private const string UldBaseBath = "ui/uld/"; // 48 8D 15 ?? ?? ?? ?? is the part of the signatures that contain the string location offset // 48 = 64 bit register prefix @@ -263,7 +263,7 @@ internal class UldWidget : IDataWindowWidget } private string ToThemedPath(string path) => - ThemeBasePaths[this.selectedTheme] + path[ThemeBasePaths[0].Length..]; + this.UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : "") + path[this.UldBaseBath.Length..]; private void DrawTextureEntry(UldRoot.TextureEntry textureEntry, TextureManager textureManager) { From c71d8889d791a3ba48dcb290c37ce63f148e70de Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:51:09 +0100 Subject: [PATCH 143/164] Access const as non instance --- Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs index 019154b53..4d858922a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UldWidget.cs @@ -263,7 +263,7 @@ internal class UldWidget : IDataWindowWidget } private string ToThemedPath(string path) => - this.UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : "") + path[this.UldBaseBath.Length..]; + UldBaseBath + (this.selectedTheme > 0 ? $"img{this.selectedTheme:D2}" : "") + path[UldBaseBath.Length..]; private void DrawTextureEntry(UldRoot.TextureEntry textureEntry, TextureManager textureManager) { From a3d930b8e2843f560ab2d850d793e0b90d228c8b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 19 Dec 2025 17:34:42 +0100 Subject: [PATCH 144/164] Use RentedSeStringBuilder more --- Dalamud/Game/Gui/ChatGui.cs | 41 ++- Dalamud/Game/Internal/DalamudCompletion.cs | 8 +- .../Game/Text/Evaluator/SeStringEvaluator.cs | 289 ++++++++---------- Dalamud/Game/Text/Noun/NounProcessor.cs | 105 +++---- .../Payloads/AutoTranslatePayload.cs | 16 +- .../Payloads/DalamudLinkPayload.cs | 27 +- .../Payloads/PlayerPayload.cs | 17 +- .../Game/Text/SeStringHandling/SeString.cs | 6 +- .../Data/Widgets/SeStringCreatorWidget.cs | 33 +- .../Steps/GamepadStateSelfTestStep.cs | 26 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 25 +- Dalamud/Utility/ItemUtil.cs | 13 +- Dalamud/Utility/SeStringExtensions.cs | 36 +-- 13 files changed, 275 insertions(+), 367 deletions(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index d7303c4ce..30e2b676c 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -26,7 +26,6 @@ using Lumina.Text; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; using SeString = Dalamud.Game.Text.SeStringHandling.SeString; using SeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder; @@ -207,21 +206,21 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui if (this.chatQueue.Count == 0) return; - var sb = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); Span namebuf = stackalloc byte[256]; using var sender = new Utf8String(); using var message = new Utf8String(); while (this.chatQueue.TryDequeue(out var chat)) { - sb.Clear(); + rssb.Builder.Clear(); foreach (var c in UtfEnumerator.From(chat.MessageBytes, UtfEnumeratorFlags.Utf8SeString)) { if (c.IsSeStringPayload) - sb.Append((ReadOnlySeStringSpan)chat.MessageBytes.AsSpan(c.ByteOffset, c.ByteLength)); + rssb.Builder.Append((ReadOnlySeStringSpan)chat.MessageBytes.AsSpan(c.ByteOffset, c.ByteLength)); else if (c.Value.IntValue == 0x202F) - sb.BeginMacro(MacroCode.NonBreakingSpace).EndMacro(); + rssb.Builder.BeginMacro(MacroCode.NonBreakingSpace).EndMacro(); else - sb.Append(c); + rssb.Builder.Append(c); } if (chat.NameBytes.Length + 1 < namebuf.Length) @@ -235,7 +234,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui sender.SetString(chat.NameBytes.NullTerminate()); } - message.SetString(sb.GetViewAsSpan()); + message.SetString(rssb.Builder.GetViewAsSpan()); var targetChannel = chat.Type ?? this.configuration.GeneralChatType; @@ -247,8 +246,6 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui chat.Timestamp, (byte)(chat.Silent ? 1 : 0)); } - - LSeStringBuilder.SharedPool.Return(sb); } /// @@ -326,29 +323,28 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui private void PrintTagged(ReadOnlySpan message, XivChatType channel, string? tag, ushort? color) { - var sb = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); if (!tag.IsNullOrEmpty()) { if (color is not null) { - sb.PushColorType(color.Value); - sb.Append($"[{tag}] "); - sb.PopColorType(); + rssb.Builder + .PushColorType(color.Value) + .Append($"[{tag}] ") + .PopColorType(); } else { - sb.Append($"[{tag}] "); + rssb.Builder.Append($"[{tag}] "); } } this.Print(new XivChatEntry { - MessageBytes = sb.Append((ReadOnlySeStringSpan)message).ToArray(), + MessageBytes = rssb.Builder.Append((ReadOnlySeStringSpan)message).ToArray(), Type = channel, }); - - LSeStringBuilder.SharedPool.Return(sb); } private void InventoryItemCopyDetour(InventoryItem* thisPtr, InventoryItem* otherPtr) @@ -457,7 +453,8 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); - var sb = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); + try { var seStringSpan = new ReadOnlySeStringSpan(linkData->Payload); @@ -465,7 +462,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui // read until link terminator foreach (var payload in seStringSpan) { - sb.Append(payload); + rssb.Builder.Append(payload); if (payload.Type == ReadOnlySePayloadType.Macro && payload.MacroCode == MacroCode.Link && @@ -477,7 +474,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui } } - var seStr = SeString.Parse(sb.ToArray()); + var seStr = SeString.Parse(rssb.Builder.ToArray()); if (seStr.Payloads.Count == 0 || seStr.Payloads[0] is not DalamudLinkPayload link) return; @@ -495,10 +492,6 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui { Log.Error(ex, "Exception in HandleLinkClickDetour"); } - finally - { - LSeStringBuilder.SharedPool.Return(sb); - } } } diff --git a/Dalamud/Game/Internal/DalamudCompletion.cs b/Dalamud/Game/Internal/DalamudCompletion.cs index e3564c823..50816a603 100644 --- a/Dalamud/Game/Internal/DalamudCompletion.cs +++ b/Dalamud/Game/Internal/DalamudCompletion.cs @@ -11,8 +11,6 @@ using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.Completion; using FFXIVClientStructs.FFXIV.Component.GUI; -using Lumina.Text; - namespace Dalamud.Game.Internal; /// @@ -253,16 +251,14 @@ internal sealed unsafe class DalamudCompletion : IInternalDisposableService { public EntryStrings(string command) { - var rssb = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - this.Display = Utf8String.FromSequence(rssb + this.Display = Utf8String.FromSequence(rssb.Builder .PushColorType(539) .Append(command) .PopColorType() .GetViewAsSpan()); - SeStringBuilder.SharedPool.Return(rssb); - this.Match = Utf8String.FromString(command); } diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 58bcdbd0b..f05c15263 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -102,16 +102,15 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator // TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append, // and then remove try...finally block (discard builder from the pool on exception) var previousCulture = CultureInfo.CurrentCulture; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); try { CultureInfo.CurrentCulture = Localization.GetCultureInfoFromLangCode(lang.ToCode()); - return this.EvaluateAndAppendTo(builder, str, localParameters, lang).ToReadOnlySeString(); + return this.EvaluateAndAppendTo(rssb.Builder, str, localParameters, lang).ToReadOnlySeString(); } finally { CultureInfo.CurrentCulture = previousCulture; - SeStringBuilder.SharedPool.Return(builder); } } @@ -930,7 +929,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator itemId += 1000000; } - var sb = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); + var sb = rssb.Builder; sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); @@ -956,7 +956,6 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator sb.PopLink(); text = sb.ToReadOnlySeString(); - SeStringBuilder.SharedPool.Return(sb); } private void CreateSheetLink(in SeStringContext context, string resolvedSheetName, ReadOnlySeString text, uint eRowIdValue, uint eColParamValue) @@ -1028,40 +1027,33 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + var pIdx = 0; + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + pIdx++; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - var str = builder.ToReadOnlySeString(); - var pIdx = 0; - - foreach (var p in str) + if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) { - pIdx++; - - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; - - if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToUpper(context.CultureInfo)); - continue; - } - - context.Builder.Append(p); + context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToUpper(context.CultureInfo)); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveHead(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1069,40 +1061,33 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + var pIdx = 0; + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + pIdx++; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - var str = builder.ToReadOnlySeString(); - var pIdx = 0; - - foreach (var p in str) + if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) { - pIdx++; - - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; - - if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToUpper(context.CultureInfo)); - continue; - } - - context.Builder.Append(p); + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToUpper(context.CultureInfo)); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveSplit(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1113,32 +1098,25 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!eSeparator.TryGetString(out var eSeparatorVal) || !eIndex.TryGetUInt(out var eIndexVal) || eIndexVal <= 0) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try - { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); - - if (!this.ResolveStringExpression(headContext, eText)) - return false; - - var separator = eSeparatorVal.ExtractText(); - if (separator.Length < 1) - return false; - - var splitted = builder.ToReadOnlySeString().ExtractText().Split(separator[0]); - if (eIndexVal <= splitted.Length) - { - context.Builder.Append(splitted[eIndexVal - 1]); - return true; - } + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + if (!this.ResolveStringExpression(headContext, eText)) return false; - } - finally + + var separator = eSeparatorVal.ExtractText(); + if (separator.Length < 1) + return false; + + var splitted = rssb.Builder.ToReadOnlySeString().ExtractText().Split(separator[0]); + if (eIndexVal <= splitted.Length) { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(splitted[eIndexVal - 1]); + return true; } + + return false; } private bool TryResolveHeadAll(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1146,37 +1124,30 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; - - var str = builder.ToReadOnlySeString(); - - foreach (var p in str) + if (p.Type == ReadOnlySePayloadType.Text) { - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; - - if (p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language)); - continue; - } - - context.Builder.Append(p); + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language)); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveFixed(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1306,14 +1277,13 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!this.dataManager.GetExcelSheet().TryGetRow(mapId, out var mapRow)) return false; - var sb = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - sb.Append(placeNameRow.Name); + rssb.Builder.Append(placeNameRow.Name); if (instance is > 0 and <= 9) - sb.Append((char)((char)0xE0B0 + (char)instance)); + rssb.Builder.Append((char)((char)0xE0B0 + (char)instance)); - var placeNameWithInstance = sb.ToReadOnlySeString(); - SeStringBuilder.SharedPool.Return(sb); + var placeNameWithInstance = rssb.Builder.ToReadOnlySeString(); var mapPosX = ConvertRawToMapPosX(mapRow, rawX / 1000f); var mapPosY = ConvertRawToMapPosY(mapRow, rawY / 1000f); @@ -1462,23 +1432,22 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator statusDescription = statusRow.Description.AsSpan(); } - var sb = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); switch (statusRow.StatusCategory) { case 1: - sb.Append(this.EvaluateFromAddon(376, default, context.Language)); + rssb.Builder.Append(this.EvaluateFromAddon(376, default, context.Language)); break; case 2: - sb.Append(this.EvaluateFromAddon(377, default, context.Language)); + rssb.Builder.Append(this.EvaluateFromAddon(377, default, context.Language)); break; } - sb.Append(statusName); + rssb.Builder.Append(statusName); - var linkText = sb.ToReadOnlySeString(); - SeStringBuilder.SharedPool.Return(sb); + var linkText = rssb.Builder.ToReadOnlySeString(); context.Builder .BeginMacro(MacroCode.Link) @@ -1733,38 +1702,31 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; - - var str = builder.ToReadOnlySeString(); - - foreach (var p in str) + if (p.Type == ReadOnlySePayloadType.Text) { - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; + context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToLower(context.CultureInfo)); - if (p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.ToArray()).ToLower(context.CultureInfo)); - - continue; - } - - context.Builder.Append(p); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveNoun(ClientLanguage language, in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -1834,40 +1796,33 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!payload.TryGetExpression(out var eStr)) return false; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - try + var headContext = new SeStringContext(rssb.Builder, context.LocalParameters, context.Language); + + if (!this.ResolveStringExpression(headContext, eStr)) + return false; + + var str = rssb.Builder.ToReadOnlySeString(); + var pIdx = 0; + + foreach (var p in str) { - var headContext = new SeStringContext(builder, context.LocalParameters, context.Language); + pIdx++; - if (!this.ResolveStringExpression(headContext, eStr)) - return false; + if (p.Type == ReadOnlySePayloadType.Invalid) + continue; - var str = builder.ToReadOnlySeString(); - var pIdx = 0; - - foreach (var p in str) + if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) { - pIdx++; - - if (p.Type == ReadOnlySePayloadType.Invalid) - continue; - - if (pIdx == 1 && p.Type == ReadOnlySePayloadType.Text) - { - context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToLower(context.CultureInfo)); - continue; - } - - context.Builder.Append(p); + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).FirstCharToLower(context.CultureInfo)); + continue; } - return true; - } - finally - { - SeStringBuilder.SharedPool.Return(builder); + context.Builder.Append(p); } + + return true; } private bool TryResolveColorType(in SeStringContext context, in ReadOnlySePayloadSpan payload) @@ -2132,19 +2087,19 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (operand1.TryGetString(out var strval1) && operand2.TryGetString(out var strval2)) { + using var rssb1 = new RentedSeStringBuilder(); + using var rssb2 = new RentedSeStringBuilder(); var resolvedStr1 = this.EvaluateAndAppendTo( - SeStringBuilder.SharedPool.Get(), + rssb1.Builder, strval1, context.LocalParameters, context.Language); var resolvedStr2 = this.EvaluateAndAppendTo( - SeStringBuilder.SharedPool.Get(), + rssb2.Builder, strval2, context.LocalParameters, context.Language); var equals = resolvedStr1.GetViewAsSpan().SequenceEqual(resolvedStr2.GetViewAsSpan()); - SeStringBuilder.SharedPool.Return(resolvedStr1); - SeStringBuilder.SharedPool.Return(resolvedStr2); if ((ExpressionType)exprType == ExpressionType.Equal) value = equals ? 1u : 0u; diff --git a/Dalamud/Game/Text/Noun/NounProcessor.cs b/Dalamud/Game/Text/Noun/NounProcessor.cs index 18f8cd4a9..993d341df 100644 --- a/Dalamud/Game/Text/Noun/NounProcessor.cs +++ b/Dalamud/Game/Text/Noun/NounProcessor.cs @@ -9,7 +9,6 @@ using Dalamud.Utility; using Lumina.Excel; using Lumina.Text.ReadOnly; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; using LSheets = Lumina.Excel.Sheets; namespace Dalamud.Game.Text.Noun; @@ -147,30 +146,28 @@ internal class NounProcessor : IServiceType var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); - var builder = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); // Ko-So-A-Do var ksad = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(nounParams.Quantity > 1 ? 1 : 0); if (!ksad.IsEmpty) { - builder.Append(ksad); + rssb.Builder.Append(ksad); if (nounParams.Quantity > 1) { - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); } } if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); var text = row.ReadStringColumn(nounParams.ColumnOffset); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); - var ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } /// @@ -200,7 +197,7 @@ internal class NounProcessor : IServiceType var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); - var builder = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); var isProperNounColumn = nounParams.ColumnOffset + ArticleColumnIdx; var isProperNoun = isProperNounColumn >= 0 ? row.ReadInt8Column(isProperNounColumn) : ~isProperNounColumn; @@ -216,21 +213,19 @@ internal class NounProcessor : IServiceType var article = attributiveSheet.GetRow((uint)nounParams.ArticleType) .ReadStringColumn(articleColumn + grammaticalNumberColumnOffset); if (!article.IsEmpty) - builder.Append(article); + rssb.Builder.Append(article); if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); } var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity == 1 ? SingularColumnIdx : PluralColumnIdx)); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - var ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } /// @@ -262,17 +257,13 @@ internal class NounProcessor : IServiceType var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); - var builder = LSeStringBuilder.SharedPool.Get(); - ReadOnlySeString ross; + using var rssb = new RentedSeStringBuilder(); if (nounParams.IsActionSheet) { - builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase)); - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - - ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + rssb.Builder.Append(row.ReadStringColumn(nounParams.GrammaticalCase)); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + return rssb.Builder.ToReadOnlySeString(); } var genderIndexColumn = nounParams.ColumnOffset + PronounColumnIdx; @@ -302,35 +293,32 @@ internal class NounProcessor : IServiceType var grammaticalGender = attributiveSheet.GetRow((uint)nounParams.ArticleType) .ReadStringColumn(caseColumnOffset + genderIndex); // Genus if (!grammaticalGender.IsEmpty) - builder.Append(grammaticalGender); + rssb.Builder.Append(grammaticalGender); } if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); - builder.Append(text); + rssb.Builder.Append(text); var plural = attributiveSheet.GetRow((uint)(caseRowOffset + 26)) .ReadStringColumn(caseColumnOffset + genderIndex); - if (builder.ContainsText("[p]"u8)) - builder.ReplaceText("[p]"u8, plural); + if (rssb.Builder.ContainsText("[p]"u8)) + rssb.Builder.ReplaceText("[p]"u8, plural); else - builder.Append(plural); + rssb.Builder.Append(plural); if (hasT) { var article = attributiveSheet.GetRow(39).ReadStringColumn(caseColumnOffset + genderIndex); // Definiter Artikel - builder.ReplaceText("[t]"u8, article); + rssb.Builder.ReplaceText("[t]"u8, article); } } - var pa = attributiveSheet.GetRow(24).ReadStringColumn(caseColumnOffset + genderIndex); - builder.ReplaceText("[pa]"u8, pa); + rssb.Builder.ReplaceText("[pa]"u8, attributiveSheet.GetRow(24).ReadStringColumn(caseColumnOffset + genderIndex)); - RawRow declensionRow; - - declensionRow = (GermanArticleType)nounParams.ArticleType switch + var declensionRow = (GermanArticleType)nounParams.ArticleType switch { // Schwache Flexion eines Adjektivs?! GermanArticleType.Possessive or GermanArticleType.Demonstrative => attributiveSheet.GetRow(25), @@ -347,14 +335,10 @@ internal class NounProcessor : IServiceType _ => attributiveSheet.GetRow(26), }; - var declension = declensionRow.ReadStringColumn(caseColumnOffset + genderIndex); - builder.ReplaceText("[a]"u8, declension); + rssb.Builder.ReplaceText("[a]"u8, declensionRow.ReadStringColumn(caseColumnOffset + genderIndex)); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - - ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } /// @@ -385,8 +369,7 @@ internal class NounProcessor : IServiceType var attributiveSheet = this.dataManager.Excel.GetSheet(nounParams.Language.ToLumina(), nameof(LSheets.Attributive)); - var builder = LSeStringBuilder.SharedPool.Get(); - ReadOnlySeString ross; + using var rssb = new RentedSeStringBuilder(); var startsWithVowelColumn = nounParams.ColumnOffset + StartsWithVowelColumnIdx; var startsWithVowel = startsWithVowelColumn >= 0 @@ -405,21 +388,19 @@ internal class NounProcessor : IServiceType { var v21 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20); if (!v21.IsEmpty) - builder.Append(v21); + rssb.Builder.Append(v21); if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); var text = row.ReadStringColumn(nounParams.ColumnOffset + (nounParams.Quantity <= 1 ? SingularColumnIdx : PluralColumnIdx)); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); if (nounParams.Quantity <= 1) - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } var v17 = row.ReadInt8Column(nounParams.ColumnOffset + Unknown5ColumnIdx); @@ -428,34 +409,32 @@ internal class NounProcessor : IServiceType var v29 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + 2); if (!v29.IsEmpty) { - builder.Append(v29); + rssb.Builder.Append(v29); if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); var text = row.ReadStringColumn(nounParams.ColumnOffset + PluralColumnIdx); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); } } else { var v27 = attributiveSheet.GetRow((uint)nounParams.ArticleType).ReadStringColumn(v20 + (v17 != 0 ? 1 : 3)); if (!v27.IsEmpty) - builder.Append(v27); + rssb.Builder.Append(v27); if (!nounParams.LinkMarker.IsEmpty) - builder.Append(nounParams.LinkMarker); + rssb.Builder.Append(nounParams.LinkMarker); var text = row.ReadStringColumn(nounParams.ColumnOffset + SingularColumnIdx); if (!text.IsEmpty) - builder.Append(text); + rssb.Builder.Append(text); } - builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); + rssb.Builder.ReplaceText("[n]"u8, ReadOnlySeString.FromText(nounParams.Quantity.ToString())); - ross = builder.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(builder); - return ross; + return rssb.Builder.ToReadOnlySeString(); } } diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs index 470e942c3..8178a6d33 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/AutoTranslatePayload.cs @@ -1,6 +1,7 @@ using System.IO; using Dalamud.Game.Text.Evaluator; +using Dalamud.Utility; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; @@ -32,13 +33,14 @@ public class AutoTranslatePayload : Payload, ITextProvider this.Group = group; this.Key = key; - var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get(); - this.payload = ssb.BeginMacro(MacroCode.Fixed) - .AppendUIntExpression(group - 1) - .AppendUIntExpression(key) - .EndMacro() - .ToReadOnlySeString(); - Lumina.Text.SeStringBuilder.SharedPool.Return(ssb); + using var rssb = new RentedSeStringBuilder(); + + this.payload = rssb.Builder + .BeginMacro(MacroCode.Fixed) + .AppendUIntExpression(group - 1) + .AppendUIntExpression(key) + .EndMacro() + .ToReadOnlySeString(); } /// diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs index 8b020b111..2becb815b 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs @@ -1,5 +1,7 @@ using System.IO; +using Dalamud.Utility; + using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; @@ -37,19 +39,18 @@ public class DalamudLinkPayload : Payload /// protected override byte[] EncodeImpl() { - var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get(); - var res = ssb.BeginMacro(MacroCode.Link) - .AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1) - .AppendUIntExpression(this.CommandId) - .AppendIntExpression(this.Extra1) - .AppendIntExpression(this.Extra2) - .BeginStringExpression() - .Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString })) - .EndExpression() - .EndMacro() - .ToArray(); - Lumina.Text.SeStringBuilder.SharedPool.Return(ssb); - return res; + using var rssb = new RentedSeStringBuilder(); + return rssb.Builder + .BeginMacro(MacroCode.Link) + .AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1) + .AppendUIntExpression(this.CommandId) + .AppendIntExpression(this.Extra1) + .AppendIntExpression(this.Extra2) + .BeginStringExpression() + .Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString })) + .EndExpression() + .EndMacro() + .ToArray(); } /// diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs index 55697782e..01ca1b955 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/PlayerPayload.cs @@ -1,8 +1,7 @@ -using System.Collections.Generic; using System.IO; -using System.Text; using Dalamud.Data; +using Dalamud.Utility; using Lumina.Excel; using Lumina.Excel.Sheets; @@ -87,14 +86,12 @@ public class PlayerPayload : Payload /// protected override byte[] EncodeImpl() { - var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get(); - var res = ssb - .PushLinkCharacter(this.playerName, this.serverId) - .Append(this.playerName) - .PopLink() - .ToArray(); - Lumina.Text.SeStringBuilder.SharedPool.Return(ssb); - return res; + using var rssb = new RentedSeStringBuilder(); + return rssb.Builder + .PushLinkCharacter(this.playerName, this.serverId) + .Append(this.playerName) + .PopLink() + .ToArray(); } /// diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index a1ef5e936..ca14299db 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -198,8 +198,9 @@ public class SeString var textColor = ItemUtil.GetItemRarityColorType(rawId); var textEdgeColor = textColor + 1u; - var sb = LSeStringBuilder.SharedPool.Get(); - var itemLink = sb + using var rssb = new RentedSeStringBuilder(); + + var itemLink = rssb.Builder .PushColorType(textColor) .PushEdgeColorType(textEdgeColor) .PushLinkItem(rawId, copyName) @@ -208,7 +209,6 @@ public class SeString .PopEdgeColorType() .PopColorType() .ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(sb); return SeString.Parse(seStringEvaluator.EvaluateFromAddon(371, [itemLink], clientState.ClientLanguage)); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index e9b4022e4..d43d3b7b2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -29,8 +29,6 @@ using Lumina.Text.Expressions; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; - namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// @@ -474,25 +472,25 @@ internal class SeStringCreatorWidget : IDataWindowWidget if (ImGui.Button("Print Evaluated"u8)) { - var sb = new LSeStringBuilder(); + using var rssb = new RentedSeStringBuilder(); foreach (var entry in this.entries) { switch (entry.Type) { case TextEntryType.String: - sb.Append(entry.Message); + rssb.Builder.Append(entry.Message); break; case TextEntryType.Macro: case TextEntryType.Fixed: - sb.AppendMacroString(entry.Message); + rssb.Builder.AppendMacroString(entry.Message); break; } } var evaluated = Service.Get().Evaluate( - sb.ToReadOnlySeString(), + rssb.Builder.ToReadOnlySeString(), this.localParameters, this.language); @@ -505,24 +503,24 @@ internal class SeStringCreatorWidget : IDataWindowWidget if (ImGui.Button("Copy MacroString"u8)) { - var sb = new LSeStringBuilder(); + using var rssb = new RentedSeStringBuilder(); foreach (var entry in this.entries) { switch (entry.Type) { case TextEntryType.String: - sb.Append(entry.Message); + rssb.Builder.Append(entry.Message); break; case TextEntryType.Macro: case TextEntryType.Fixed: - sb.AppendMacroString(entry.Message); + rssb.Builder.AppendMacroString(entry.Message); break; } } - ImGui.SetClipboardText(sb.ToReadOnlySeString().ToMacroString()); + ImGui.SetClipboardText(rssb.Builder.ToReadOnlySeString().ToMacroString()); } ImGui.SameLine(); @@ -802,24 +800,24 @@ internal class SeStringCreatorWidget : IDataWindowWidget private unsafe void UpdateInputString(bool resetLocalParameters = true) { - var sb = new LSeStringBuilder(); + using var rssb = new RentedSeStringBuilder(); foreach (var entry in this.entries) { switch (entry.Type) { case TextEntryType.String: - sb.Append(entry.Message); + rssb.Builder.Append(entry.Message); break; case TextEntryType.Macro: case TextEntryType.Fixed: - sb.AppendMacroString(entry.Message); + rssb.Builder.AppendMacroString(entry.Message); break; } } - this.input = sb.ToReadOnlySeString(); + this.input = rssb.Builder.ToReadOnlySeString(); if (resetLocalParameters) this.localParameters = null; @@ -998,10 +996,9 @@ internal class SeStringCreatorWidget : IDataWindowWidget } } - var builder = LSeStringBuilder.SharedPool.Get(); - builder.AppendIcon(iconId); - ImGuiHelpers.SeStringWrapped(builder.ToArray()); - LSeStringBuilder.SharedPool.Return(builder); + using var rssb = new RentedSeStringBuilder(); + rssb.Builder.AppendIcon(iconId); + ImGuiHelpers.SeStringWrapped(rssb.Builder.ToArray()); ImGui.SameLine(); } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs index d272032e7..e2ee676df 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/GamepadStateSelfTestStep.cs @@ -3,9 +3,9 @@ using System.Linq; using Dalamud.Game.ClientState.GamePad; using Dalamud.Interface.Utility; using Dalamud.Plugin.SelfTest; -using Lumina.Text.Payloads; +using Dalamud.Utility; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; +using Lumina.Text.Payloads; namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; @@ -29,25 +29,25 @@ internal class GamepadStateSelfTestStep : ISelfTestStep (GamepadButtons.L1, 12), }; - var builder = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - builder.Append("Hold down "); + rssb.Builder.Append("Hold down "); for (var i = 0; i < buttons.Length; i++) { var (button, iconId) = buttons[i]; - builder.BeginMacro(MacroCode.Icon).AppendUIntExpression(iconId).EndMacro(); - builder.PushColorRgba(gamepadState.Raw(button) == 1 ? 0x0000FF00u : 0x000000FF); - builder.Append(button.ToString()); - builder.PopColor(); - - builder.Append(i < buttons.Length - 1 ? ", " : "."); + rssb.Builder + .BeginMacro(MacroCode.Icon) + .AppendUIntExpression(iconId) + .EndMacro() + .PushColorRgba(gamepadState.Raw(button) == 1 ? 0x0000FF00u : 0x000000FF) + .Append(button.ToString()) + .PopColor() + .Append(i < buttons.Length - 1 ? ", " : "."); } - ImGuiHelpers.SeStringWrapped(builder.ToReadOnlySeString()); - - LSeStringBuilder.SharedPool.Return(builder); + ImGuiHelpers.SeStringWrapped(rssb.Builder.ToReadOnlySeString()); if (buttons.All(tuple => gamepadState.Raw(tuple.Button) == 1)) { diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index f826da622..ec9440e0e 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -26,8 +26,6 @@ using FFXIVClientStructs.FFXIV.Component.GUI; using Lumina.Text.ReadOnly; using Serilog; -using LSeStringBuilder = Lumina.Text.SeStringBuilder; - namespace Dalamud.Interface.Internal.Windows; /// @@ -498,20 +496,23 @@ internal class TitleScreenMenuWindow : Window, IDisposable return; this.lastLoadedPluginCount = count; - var lssb = LSeStringBuilder.SharedPool.Get(); - lssb.Append(new ReadOnlySeStringSpan(addon->AtkValues[1].String.Value)).Append("\n\n"); - lssb.PushEdgeColorType(701).PushColorType(539) - .Append(SeIconChar.BoxedLetterD.ToIconChar()) - .PopColorType().PopEdgeColorType(); - lssb.Append($" Dalamud: {Versioning.GetScmVersion()}"); + using var rssb = new RentedSeStringBuilder(); - lssb.Append($" - {count} {(count != 1 ? "plugins" : "plugin")} loaded"); + rssb.Builder + .Append(new ReadOnlySeStringSpan(addon->AtkValues[1].String.Value)) + .Append("\n\n") + .PushEdgeColorType(701) + .PushColorType(539) + .Append(SeIconChar.BoxedLetterD.ToIconChar()) + .PopColorType() + .PopEdgeColorType() + .Append($" Dalamud: {Versioning.GetScmVersion()}") + .Append($" - {count} {(count != 1 ? "plugins" : "plugin")} loaded"); if (pm?.SafeMode is true) - lssb.PushColorType(17).Append(" [SAFE MODE]").PopColorType(); + rssb.Builder.PushColorType(17).Append(" [SAFE MODE]").PopColorType(); - textNode->SetText(lssb.GetViewAsSpan()); - LSeStringBuilder.SharedPool.Return(lssb); + textNode->SetText(rssb.Builder.GetViewAsSpan()); } private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync(); diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs index b632d14d7..8ee465486 100644 --- a/Dalamud/Utility/ItemUtil.cs +++ b/Dalamud/Utility/ItemUtil.cs @@ -5,7 +5,6 @@ using Dalamud.Game; using Dalamud.Game.Text; using Lumina.Excel.Sheets; -using Lumina.Text; using Lumina.Text.ReadOnly; namespace Dalamud.Utility; @@ -150,23 +149,21 @@ public static class ItemUtil if (!includeIcon || kind is not (ItemKind.Hq or ItemKind.Collectible)) return item.Name; - var builder = SeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); - builder.Append(item.Name); + rssb.Builder.Append(item.Name); switch (kind) { case ItemKind.Hq: - builder.Append($" {(char)SeIconChar.HighQuality}"); + rssb.Builder.Append($" {(char)SeIconChar.HighQuality}"); break; case ItemKind.Collectible: - builder.Append($" {(char)SeIconChar.Collectible}"); + rssb.Builder.Append($" {(char)SeIconChar.Collectible}"); break; } - var itemName = builder.ToReadOnlySeString(); - SeStringBuilder.SharedPool.Return(builder); - return itemName; + return rssb.Builder.ToReadOnlySeString(); } /// diff --git a/Dalamud/Utility/SeStringExtensions.cs b/Dalamud/Utility/SeStringExtensions.cs index cd095c467..9de116f26 100644 --- a/Dalamud/Utility/SeStringExtensions.cs +++ b/Dalamud/Utility/SeStringExtensions.cs @@ -1,7 +1,3 @@ -using System.Linq; - -using InteropGenerator.Runtime; - using Lumina.Text.Parse; using Lumina.Text.ReadOnly; @@ -49,11 +45,9 @@ public static class SeStringExtensions /// this for method chaining. public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan macroString) { - var lssb = LSeStringBuilder.SharedPool.Get(); - lssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); - ssb.Append(DSeString.Parse(lssb.ToReadOnlySeString().Data.Span)); - LSeStringBuilder.SharedPool.Return(lssb); - return ssb; + using var rssb = new RentedSeStringBuilder(); + rssb.Builder.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); + return ssb.Append(DSeString.Parse(rssb.Builder.ToReadOnlySeString().Data.Span)); } /// Compiles and appends a macro string. @@ -62,11 +56,9 @@ public static class SeStringExtensions /// this for method chaining. public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan macroString) { - var lssb = LSeStringBuilder.SharedPool.Get(); - lssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); - ssb.Append(DSeString.Parse(lssb.ToReadOnlySeString().Data.Span)); - LSeStringBuilder.SharedPool.Return(lssb); - return ssb; + using var rssb = new RentedSeStringBuilder(); + rssb.Builder.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }); + return ssb.Append(DSeString.Parse(rssb.Builder.ToReadOnlySeString().Data.Span)); } /// @@ -163,7 +155,7 @@ public static class SeStringExtensions if (ross.IsEmpty) return ross; - var sb = LSeStringBuilder.SharedPool.Get(); + using var rssb = new RentedSeStringBuilder(); foreach (var payload in ross) { @@ -172,25 +164,25 @@ public static class SeStringExtensions if (payload.Type != ReadOnlySePayloadType.Text) { - sb.Append(payload); + rssb.Builder.Append(payload); continue; } var index = payload.Body.Span.IndexOf(toFind); if (index == -1) { - sb.Append(payload); + rssb.Builder.Append(payload); continue; } var lastIndex = 0; while (index != -1) { - sb.Append(payload.Body.Span[lastIndex..index]); + rssb.Builder.Append(payload.Body.Span[lastIndex..index]); if (!replacement.IsEmpty) { - sb.Append(replacement); + rssb.Builder.Append(replacement); } lastIndex = index + toFind.Length; @@ -200,12 +192,10 @@ public static class SeStringExtensions index += lastIndex; } - sb.Append(payload.Body.Span[lastIndex..]); + rssb.Builder.Append(payload.Body.Span[lastIndex..]); } - var output = sb.ToReadOnlySeString(); - LSeStringBuilder.SharedPool.Return(sb); - return output; + return rssb.Builder.ToReadOnlySeString(); } /// From 8a49a5ee484177e83ade8078486ee7b5f93d924e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 19 Dec 2025 17:39:31 +0100 Subject: [PATCH 145/164] Use spread element for TextArrowPayloads Also changes CreatePartyFinderLink to use client-language-based text. --- .../Game/Text/SeStringHandling/SeString.cs | 54 +++++++------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index ca14299db..b94b05cac 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -250,16 +250,12 @@ public class SeString var mapPayload = new MapLinkPayload(territoryId, mapId, rawX, rawY); var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString); - var payloads = new List(new Payload[] - { + return new SeString(new List([ mapPayload, - // arrow goes here + ..TextArrowPayloads, new TextPayload(nameString), RawPayload.LinkTerminator, - }); - payloads.InsertRange(1, TextArrowPayloads); - - return new SeString(payloads); + ])); } /// @@ -290,16 +286,12 @@ public class SeString var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor); var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString); - var payloads = new List(new Payload[] - { + return new SeString(new List([ mapPayload, - // arrow goes here + ..TextArrowPayloads, new TextPayload(nameString), RawPayload.LinkTerminator, - }); - payloads.InsertRange(1, TextArrowPayloads); - - return new SeString(payloads); + ])); } /// @@ -355,21 +347,15 @@ public class SeString /// An SeString containing all the payloads necessary to display a party finder link in the chat log. public static SeString CreatePartyFinderLink(uint listingId, string recruiterName, bool isCrossWorld = false) { - var payloads = new List() - { + var clientState = Service.Get(); + var seStringEvaluator = Service.Get(); + + return new SeString(new List([ new PartyFinderPayload(listingId, isCrossWorld ? PartyFinderPayload.PartyFinderLinkType.NotSpecified : PartyFinderPayload.PartyFinderLinkType.LimitedToHomeWorld), - // -> - new TextPayload($"Looking for Party ({recruiterName})" + (isCrossWorld ? " " : string.Empty)), - }; - - payloads.InsertRange(1, TextArrowPayloads); - - if (isCrossWorld) - payloads.Add(new IconPayload(BitmapFontIcon.CrossWorld)); - - payloads.Add(RawPayload.LinkTerminator); - - return new SeString(payloads); + ..TextArrowPayloads, + ..SeString.Parse(seStringEvaluator.EvaluateFromAddon(2265, [recruiterName, isCrossWorld ? 0 : 1], clientState.ClientLanguage)).Payloads, + RawPayload.LinkTerminator + ])); } /// @@ -379,16 +365,12 @@ public class SeString /// An SeString containing all the payloads necessary to display a link to the party finder search conditions. public static SeString CreatePartyFinderSearchConditionsLink(string message) { - var payloads = new List() - { + return new SeString(new List([ new PartyFinderPayload(), - // -> + ..TextArrowPayloads, new TextPayload(message), - }; - payloads.InsertRange(1, TextArrowPayloads); - payloads.Add(RawPayload.LinkTerminator); - - return new SeString(payloads); + RawPayload.LinkTerminator + ])); } /// From efed9ca20b74452bf332c9cd4f68f5588bb81156 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Dec 2025 20:55:30 +0100 Subject: [PATCH 146/164] Add badges --- .../Internal/DalamudConfiguration.cs | 10 ++ Dalamud/DalamudAsset.cs | 9 +- Dalamud/Interface/DalamudWindowOpenKinds.cs | 5 + Dalamud/Interface/Internal/Badge/BadgeInfo.cs | 48 +++++++ .../Interface/Internal/Badge/BadgeManager.cs | 85 ++++++++++++ .../Internal/Badge/BadgeUnlockMethod.cs | 22 +++ .../Interface/Internal/DalamudInterface.cs | 85 ++++++++++++ .../Windows/Settings/SettingsWindow.cs | 1 + .../Windows/Settings/Tabs/SettingsTabBadge.cs | 128 ++++++++++++++++++ 9 files changed, 392 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Interface/Internal/Badge/BadgeInfo.cs create mode 100644 Dalamud/Interface/Internal/Badge/BadgeManager.cs create mode 100644 Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs create mode 100644 Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index d546dc517..ddcb26914 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -495,6 +495,16 @@ internal sealed class DalamudConfiguration : IInternalDisposableService #pragma warning restore SA1516 #pragma warning restore SA1600 + /// + /// Gets or sets a list of badge passwords used to unlock badges. + /// + public List UsedBadgePasswords { get; set; } = []; + + /// + /// Gets or sets a value indicating whether badges should be shown on the title screen. + /// + public bool ShowBadgesOnTitleScreen { get; set; } = true; + /// /// Load a configuration from the provided path. /// diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index e234fbb4c..9c0f247ee 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -73,7 +73,7 @@ public enum DalamudAsset [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "troubleIcon.png")] TroubleIcon = 1006, - + /// /// : The plugin trouble icon overlay. /// @@ -124,6 +124,13 @@ public enum DalamudAsset [DalamudAssetPath("UIRes", "tsmShade.png")] TitleScreenMenuShade = 1013, + /// + /// : Atlas containing badges. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "badgeAtlas.png")] + BadgeAtlas = 1015, + /// /// : Noto Sans CJK JP Medium. /// diff --git a/Dalamud/Interface/DalamudWindowOpenKinds.cs b/Dalamud/Interface/DalamudWindowOpenKinds.cs index 35d2825f7..891f9281a 100644 --- a/Dalamud/Interface/DalamudWindowOpenKinds.cs +++ b/Dalamud/Interface/DalamudWindowOpenKinds.cs @@ -56,6 +56,11 @@ public enum SettingsOpenKind /// ServerInfoBar, + /// + /// Open to the "Badges" page. + /// + Badge, + /// /// Open to the "Experimental" page. /// diff --git a/Dalamud/Interface/Internal/Badge/BadgeInfo.cs b/Dalamud/Interface/Internal/Badge/BadgeInfo.cs new file mode 100644 index 000000000..0787f0658 --- /dev/null +++ b/Dalamud/Interface/Internal/Badge/BadgeInfo.cs @@ -0,0 +1,48 @@ +using System.Numerics; + +namespace Dalamud.Interface.Internal.Badge; + +/// +/// Represents information about a badge. +/// +/// Name of the badge. +/// Description of the badge. +/// Icon index. +/// Sha256 hash of the unlock password. +/// How the badge is unlocked. +internal record BadgeInfo( + Func Name, + Func Description, + int IconIndex, + string UnlockSha256, + BadgeUnlockMethod UnlockMethod) +{ + private const float BadgeWidth = 256; + private const float BadgeHeight = 256; + private const float BadgesPerRow = 2; + + /// + /// Gets the UV coordinates for the badge icon in the atlas. + /// + /// Width of the atlas. + /// Height of the atlas. + /// UV coordinates. + public (Vector2 Uv0, Vector2 Uv1) GetIconUv(float atlasWidthPx, float atlasHeightPx) + { + // Calculate row and column from icon index + var col = this.IconIndex % (int)BadgesPerRow; + var row = this.IconIndex / (int)BadgesPerRow; + + // Calculate pixel positions + var x0 = col * BadgeWidth; + var y0 = row * BadgeHeight; + var x1 = x0 + BadgeWidth; + var y1 = y0 + BadgeHeight; + + // Convert to UV coordinates (0.0 to 1.0) + var uv0 = new Vector2(x0 / atlasWidthPx, y0 / atlasHeightPx); + var uv1 = new Vector2(x1 / atlasWidthPx, y1 / atlasHeightPx); + + return (uv0, uv1); + } +} diff --git a/Dalamud/Interface/Internal/Badge/BadgeManager.cs b/Dalamud/Interface/Internal/Badge/BadgeManager.cs new file mode 100644 index 000000000..9290d6cc8 --- /dev/null +++ b/Dalamud/Interface/Internal/Badge/BadgeManager.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Configuration.Internal; + +namespace Dalamud.Interface.Internal.Badge; + +/// +/// Service responsible for managing user badges. +/// +[ServiceManager.EarlyLoadedService] +internal class BadgeManager : IServiceType +{ + private readonly DalamudConfiguration configuration; + + private readonly List badges = + [ + new(() => "Test Badge", + () => "Awarded for testing badges.", + 0, + "937e8d5fbb48bd4949536cd65b8d35c426b80d2f830c5c308e2cdec422ae2244", + BadgeUnlockMethod.User), + + new(() => "Fundraiser #1 Donor", + () => "Awarded for participating in the first patch fundraiser.", + 1, + "56e752257bd0cbb2944f95cc7b3cb3d0db15091dd043f7a195ed37028d079322", + BadgeUnlockMethod.User) + ]; + + private readonly List unlockedBadgeIndices = []; + + /// + /// Initializes a new instance of the class. + /// + /// Configuration to use. + [ServiceManager.ServiceConstructor] + public BadgeManager(DalamudConfiguration configuration) + { + this.configuration = configuration; + + foreach (var usedBadge in this.configuration.UsedBadgePasswords) + { + this.TryUnlockBadge(usedBadge, BadgeUnlockMethod.Startup, out _); + } + } + + /// + /// Gets the badges the user has unlocked. + /// + public IEnumerable UnlockedBadges + => this.badges.Where((_, index) => this.unlockedBadgeIndices.Contains(index)); + + /// + /// Unlock a badge with the given password and method. + /// + /// The password to unlock the badge with. + /// How we are unlocking this badge. + /// The badge that was unlocked, if the function returns true, null otherwise. + /// The unlocked badge, if one was unlocked by this call. + public bool TryUnlockBadge(string password, BadgeUnlockMethod method, out BadgeInfo unlockedBadge) + { + var sha256 = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(password)); + var hashString = Convert.ToHexString(sha256); + + foreach (var (idx, badge) in this.badges.Where(x => x.UnlockMethod == method || method == BadgeUnlockMethod.Startup).Index()) + { + if (!this.unlockedBadgeIndices.Contains(idx) && badge.UnlockSha256.Equals(hashString, StringComparison.OrdinalIgnoreCase)) + { + if (method != BadgeUnlockMethod.Startup) + { + this.configuration.UsedBadgePasswords.Add(password); + this.configuration.QueueSave(); + } + + this.unlockedBadgeIndices.Add(idx); + unlockedBadge = badge; + return true; + } + } + + unlockedBadge = null!; + return false; + } +} diff --git a/Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs b/Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs new file mode 100644 index 000000000..45828c097 --- /dev/null +++ b/Dalamud/Interface/Internal/Badge/BadgeUnlockMethod.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.Internal.Badge; + +/// +/// Method by which a badge can be unlocked. +/// +internal enum BadgeUnlockMethod +{ + /// + /// Badge can be unlocked by the user by entering a password. + /// + User, + + /// + /// Badge can be unlocked from Dalamud internal features. + /// + Internal, + + /// + /// Badge is no longer obtainable and can only be unlocked from the configuration file. + /// + Startup, +} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index be4228a81..13e7ff3f7 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -19,6 +19,9 @@ using Dalamud.Game.Gui; using Dalamud.Hooking; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal.Badge; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.Data; using Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -540,6 +543,25 @@ internal class DalamudInterface : IInternalDisposableService /// Widget to set current. public void SetDataWindowWidget(IDataWindowWidget widget) => this.dataWindow.CurrentWidget = widget; + /// + /// Play an animation when a badge has been unlocked. + /// + /// The badge that has been unlocked. + public void StartBadgeUnlockAnimation(BadgeInfo badge) + { + var badgeTexture = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); + var uvs = badge.GetIconUv(badgeTexture.Width, badgeTexture.Height); + + // TODO: Make it more fancy? + Service.Get().AddNotification( + new Notification + { + Title = "Badge unlocked!", + Content = $"You unlocked the badge '{badge.Name()}'", + Type = NotificationType.Success, + }); + } + private void OnDraw() { this.FrameCount++; @@ -561,6 +583,7 @@ internal class DalamudInterface : IInternalDisposableService { this.DrawHiddenDevMenuOpener(); this.DrawDevMenu(); + this.DrawTitleScreenBadges(); if (Service.Get().GameUiHidden) return; @@ -591,6 +614,68 @@ internal class DalamudInterface : IInternalDisposableService } } + private void DrawTitleScreenBadges() + { + if (!this.titleScreenMenuWindow.IsOpen) + return; + + var badgeManager = Service.Get(); + if (!this.configuration.ShowBadgesOnTitleScreen || !badgeManager.UnlockedBadges.Any()) + return; + + var vp = ImGui.GetMainViewport(); + ImGui.SetNextWindowPos(vp.Pos); + ImGui.SetNextWindowSize(vp.Size); + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowBgAlpha(0f); + + ImGui.Begin( + "###TitleScreenBadgeWindow"u8, + ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.NoNav); + + var badgeAtlas = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); + var badgeSize = ImGuiHelpers.GlobalScale * 80; + var spacing = ImGuiHelpers.GlobalScale * 10; + const float margin = 60f; + var startPos = vp.Pos + new Vector2(vp.Size.X - margin, margin); + + // Use the mouse position in screen space for hover detection because the usual ImGui hover checks + // don't work with this full-viewport overlay window setup. + var mouse = ImGui.GetMousePos(); + + foreach (var badge in badgeManager.UnlockedBadges) + { + var uvs = badge.GetIconUv(badgeAtlas.Width, badgeAtlas.Height); + + startPos.X -= badgeSize; + ImGui.SetCursorPos(startPos); + ImGui.Image(badgeAtlas.Handle, new Vector2(badgeSize), uvs.Uv0, uvs.Uv1); + + // Get the actual screen-space bounds of the image we just drew + var badgeMin = ImGui.GetItemRectMin(); + var badgeMax = ImGui.GetItemRectMax(); + + // add spacing to the left for the next badge + startPos.X -= spacing; + + // Manual hit test using mouse position + if (mouse.X >= badgeMin.X && mouse.X <= badgeMax.X && mouse.Y >= badgeMin.Y && mouse.Y <= badgeMax.Y) + { + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(300 * ImGuiHelpers.GlobalScale); + ImGui.TextWrapped(badge.Name()); + ImGui.Separator(); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, badge.Description()); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + } + + ImGui.End(); + } + private void DrawCreditsDarkeningAnimation() { using var style1 = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f); diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 581ef3746..62c931b20 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -46,6 +46,7 @@ internal sealed class SettingsWindow : Window new SettingsTabLook(), new SettingsTabAutoUpdates(), new SettingsTabDtr(), + new SettingsTabBadge(), new SettingsTabExperimental(), new SettingsTabAbout() ]; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs new file mode 100644 index 000000000..8e44ef7ea --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs @@ -0,0 +1,128 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using CheapLoc; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.Badge; +using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.Utility; +using Dalamud.Storage.Assets; +using Dalamud.Utility.Internal; + +namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; + +[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] +internal sealed class SettingsTabBadge : SettingsTab +{ + private string badgePassword = string.Empty; + private bool badgeWasError = false; + + public override string Title => Loc.Localize("DalamudSettingsBadge", "Badges"); + + public override SettingsOpenKind Kind => SettingsOpenKind.ServerInfoBar; + + public override SettingsEntry[] Entries { get; } = + [ + new SettingsEntry( + LazyLoc.Localize("DalamudSettingsShowBadgesOnTitleScreen", "Show Badges on Title Screen"), + LazyLoc.Localize("DalamudSettingsShowBadgesOnTitleScreenHint", "If enabled, your unlocked badges will also be shown on the title screen."), + c => c.ShowBadgesOnTitleScreen, + (v, c) => c.ShowBadgesOnTitleScreen = v), + ]; + + public override void Draw() + { + var badgeManager = Service.Get(); + var dalamudInterface = Service.Get(); + + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarHint", "Plugins can put additional information into your server information bar(where world & time can be seen).\nYou can reorder and disable these here.")); + + ImGuiHelpers.ScaledDummy(5); + + ImGui.Text(Loc.Localize("DalamudSettingsBadgesUnlock", "Unlock a badge")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsBadgesUnlockHint", "If you have received a code for a badge, enter it here to unlock the badge.\nCodes are usually given out during community events or contests.")); + ImGui.InputTextWithHint( + "##BadgePassword", + Loc.Localize("DalamudSettingsBadgesUnlockHintInput", "Enter badge code here"), + ref this.badgePassword, + 100); + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("DalamudSettingsBadgesUnlockButton", "Unlock Badge"))) + { + if (badgeManager.TryUnlockBadge(this.badgePassword.Trim(), BadgeUnlockMethod.User, out var unlockedBadge)) + { + dalamudInterface.StartBadgeUnlockAnimation(unlockedBadge); + this.badgeWasError = false; + } + else + { + this.badgeWasError = true; + } + } + + if (this.badgeWasError) + { + ImGuiHelpers.ScaledDummy(5); + ImGui.TextColored(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingsBadgesUnlockError", "Failed to unlock badge. The code may be invalid or you may have already unlocked this badge.")); + } + + ImGuiHelpers.ScaledDummy(5); + + base.Draw(); + + ImGui.Separator(); + + ImGuiHelpers.ScaledDummy(5); + + var haveBadges = badgeManager.UnlockedBadges.ToArray(); + + if (haveBadges.Length == 0) + { + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarDidNone", "You did not unlock any badges yet.\nBadges can be unlocked by participating in community events or contests.")); + } + + var badgeTexture = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); + foreach (var badge in haveBadges) + { + var uvs = badge.GetIconUv(badgeTexture.Width, badgeTexture.Height); + var sectionSize = ImGuiHelpers.GlobalScale * 66; + + var startCursor = ImGui.GetCursorPos(); + + ImGui.SetCursorPos(startCursor); + + var iconSize = ImGuiHelpers.ScaledVector2(64, 64); + var cursorBeforeImage = ImGui.GetCursorPos(); + var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos(); + + if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize)) + { + ImGui.Image(badgeTexture.Handle, iconSize, uvs.Uv0, uvs.Uv1); + ImGui.SameLine(); + ImGui.SetCursorPos(cursorBeforeImage); + } + + ImGui.SameLine(); + + ImGuiHelpers.ScaledDummy(5); + ImGui.SameLine(); + + var cursor = ImGui.GetCursorPos(); + + // Name + ImGui.Text(badge.Name()); + + cursor.Y += ImGui.GetTextLineHeightWithSpacing(); + ImGui.SetCursorPos(cursor); + + // Description + ImGui.TextWrapped(badge.Description()); + + startCursor.Y += sectionSize; + ImGui.SetCursorPos(startCursor); + + ImGuiHelpers.ScaledDummy(5); + } + } +} From c7dd694a538020d1809fbdffb29629251df5bea1 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 20 Dec 2025 02:02:57 +0100 Subject: [PATCH 147/164] Revert "Prevent ImGui text box methods from cloning unchanged input every frame" This reverts commit db5f27518fb6ec50cdfa5d6dcd39e10cbb4f1fd7. Causes issues with certain flags. --- .../Custom/ImGui.Manual.cs | 153 ++++-------------- 1 file changed, 34 insertions(+), 119 deletions(-) diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs index ce1bf961d..89b3cc3d6 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs @@ -127,13 +127,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -145,13 +140,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -163,13 +153,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -181,13 +166,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, in context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -307,13 +287,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -325,13 +300,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -344,13 +314,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, ref context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -363,13 +328,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, in context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -428,13 +388,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -446,13 +401,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -464,13 +414,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, ref context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -482,13 +427,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, in context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -537,13 +477,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -555,13 +490,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -573,13 +503,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -591,13 +516,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, in context); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } @@ -621,13 +541,8 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = TempInputText(bb, id, label, t.Buffer[..(maxLength + 1)], flags); - - if (r) - { - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); - } - + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); t.Recycle(); return r; } From 66fde2d4589516ac5cb687ac102ed6040f94b5d0 Mon Sep 17 00:00:00 2001 From: CMDRNuffin <4348470+CMDRNuffin@users.noreply.github.com> Date: Sat, 20 Dec 2025 03:19:20 +0100 Subject: [PATCH 148/164] Prevent unnecessary string creation in ImGui TextInput methods We now only create a new string if we either know the buffer changed or the EnterReturnsTrue flag was set (because that one does a LOT while still updating the buffer on every actual input), so I had to choose between replicating all that behavior in each of the various InputText methods (hell no, lol), scanning the buffer for actual changes (which would require making another copy) or accepting that in that case we would create a new string every frame. This still makes the GC happy in the majority of cases, while giving callers the option to take a slight performance hit for the convenience EnterReturnsTrue provides. --- .../Custom/ImGui.Manual.cs | 170 ++++++++++++++---- 1 file changed, 136 insertions(+), 34 deletions(-) diff --git a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs index 89b3cc3d6..e455e0778 100644 --- a/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs +++ b/imgui/Dalamud.Bindings.ImGui/Custom/ImGui.Manual.cs @@ -127,8 +127,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -140,8 +146,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -153,8 +165,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -166,8 +184,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputText(label, t.Buffer[..(maxLength + 1)], flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -287,8 +311,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -300,8 +330,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -314,8 +350,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -328,8 +370,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextEx(label, hint, t.Buffer[..(maxLength + 1)], sizeArg, flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -388,8 +436,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -401,8 +455,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -414,8 +474,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -427,8 +493,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextMultiline(label, t.Buffer[..(maxLength + 1)], size, flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -477,8 +549,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -490,8 +568,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -503,8 +587,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, ref context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -516,8 +606,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = InputTextWithHint(label, hint, t.Buffer[..(maxLength + 1)], flags, callback, in context); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } @@ -541,8 +637,14 @@ public unsafe partial class ImGui var t = new ImU8String(buf); t.Reserve(maxLength + 1); var r = TempInputText(bb, id, label, t.Buffer[..(maxLength + 1)], flags); - var i = t.Buffer.IndexOf((byte)0); - buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + + var e = (flags & ImGuiInputTextFlags.EnterReturnsTrue) != 0; + if (r | e) + { + var i = t.Buffer.IndexOf((byte)0); + buf = Encoding.UTF8.GetString(i == -1 ? t.Buffer : t.Buffer[..i]); + } + t.Recycle(); return r; } From bc2eac6006aad4c742ed8ffdeb0ec07e8958f2f9 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Fri, 19 Dec 2025 22:18:03 -0800 Subject: [PATCH 149/164] fix: Remove RPC (#2526) --- Dalamud.Test/Rpc/DalamudUriTests.cs | 108 --------- Dalamud/Dalamud.csproj | 1 - Dalamud/Networking/Rpc/Model/DalamudUri.cs | 102 --------- Dalamud/Networking/Rpc/RpcConnection.cs | 95 -------- Dalamud/Networking/Rpc/RpcHostService.cs | 91 -------- Dalamud/Networking/Rpc/RpcServiceRegistry.cs | 85 ------- .../Rpc/Service/ClientHelloService.cs | 133 ----------- .../Rpc/Service/LinkHandlerService.cs | 107 --------- .../Rpc/Service/Links/DebugLinkHandler.cs | 67 ------ .../Rpc/Service/Links/PluginLinkHandler.cs | 57 ----- .../Networking/Rpc/Transport/IRpcTransport.cs | 32 --- .../Rpc/Transport/UnixRpcTransport.cs | 207 ------------------ Dalamud/Plugin/Services/IPluginLinkHandler.cs | 24 -- Directory.Packages.props | 3 - 14 files changed, 1112 deletions(-) delete mode 100644 Dalamud.Test/Rpc/DalamudUriTests.cs delete mode 100644 Dalamud/Networking/Rpc/Model/DalamudUri.cs delete mode 100644 Dalamud/Networking/Rpc/RpcConnection.cs delete mode 100644 Dalamud/Networking/Rpc/RpcHostService.cs delete mode 100644 Dalamud/Networking/Rpc/RpcServiceRegistry.cs delete mode 100644 Dalamud/Networking/Rpc/Service/ClientHelloService.cs delete mode 100644 Dalamud/Networking/Rpc/Service/LinkHandlerService.cs delete mode 100644 Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs delete mode 100644 Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs delete mode 100644 Dalamud/Networking/Rpc/Transport/IRpcTransport.cs delete mode 100644 Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs delete mode 100644 Dalamud/Plugin/Services/IPluginLinkHandler.cs diff --git a/Dalamud.Test/Rpc/DalamudUriTests.cs b/Dalamud.Test/Rpc/DalamudUriTests.cs deleted file mode 100644 index b371a5698..000000000 --- a/Dalamud.Test/Rpc/DalamudUriTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Linq; - -using Dalamud.Networking.Rpc.Model; - -using Xunit; - -namespace Dalamud.Test.Rpc -{ - public class DalamudUriTests - { - [Theory] - [InlineData("https://www.google.com/", false)] - [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", true)] - public void ValidatesScheme(string uri, bool valid) - { - Action act = () => { _ = DalamudUri.FromUri(uri); }; - - var ex = Record.Exception(act); - if (valid) - { - Assert.Null(ex); - } - else - { - Assert.NotNull(ex); - Assert.IsType(ex); - } - } - - [Theory] - [InlineData("dalamud://PluginInstaller/Dalamud.FindAnything", "plugininstaller")] - [InlineData("dalamud://Plugin/Dalamud.FindAnything/OpenWindow", "plugin")] - [InlineData("dalamud://Test", "test")] - public void ExtractsNamespace(string uri, string expectedNamespace) - { - var dalamudUri = DalamudUri.FromUri(uri); - Assert.Equal(expectedNamespace, dalamudUri.Namespace); - } - - [Theory] - [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/")] - [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux")] - [InlineData("dalamud://foo/bar/baz", "/bar/baz")] - [InlineData("dalamud://foo/bar", "/bar")] - [InlineData("dalamud://foo/bar/", "/bar/")] - [InlineData("dalamud://foo/", "/")] - public void ExtractsPath(string uri, string expectedPath) - { - var dalamudUri = DalamudUri.FromUri(uri); - Assert.Equal(expectedPath, dalamudUri.Path); - } - - [Theory] - [InlineData("dalamud://foo/bar/baz/qux/?cow=moo#frag", "/bar/baz/qux/?cow=moo#frag")] - [InlineData("dalamud://foo/bar/baz/qux/?cow=moo", "/bar/baz/qux/?cow=moo")] - [InlineData("dalamud://foo/bar/baz/qux?cow=moo", "/bar/baz/qux?cow=moo")] - [InlineData("dalamud://foo/bar/baz", "/bar/baz")] - [InlineData("dalamud://foo/bar?cow=moo", "/bar?cow=moo")] - [InlineData("dalamud://foo/bar", "/bar")] - [InlineData("dalamud://foo/bar/?cow=moo", "/bar/?cow=moo")] - [InlineData("dalamud://foo/bar/", "/bar/")] - [InlineData("dalamud://foo/?cow=moo#chicken", "/?cow=moo#chicken")] - [InlineData("dalamud://foo/?cow=moo", "/?cow=moo")] - [InlineData("dalamud://foo/", "/")] - public void ExtractsData(string uri, string expectedData) - { - var dalamudUri = DalamudUri.FromUri(uri); - - Assert.Equal(expectedData, dalamudUri.Data); - } - - [Theory] - [InlineData("dalamud://foo/bar", 0)] - [InlineData("dalamud://foo/bar?cow=moo", 1)] - [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo", 2)] - [InlineData("dalamud://foo/bar?cow=moo&wolf=awoo&cat", 3)] - public void ExtractsQueryParams(string uri, int queryCount) - { - var dalamudUri = DalamudUri.FromUri(uri); - Assert.Equal(queryCount, dalamudUri.QueryParams.Count); - } - - [Theory] - [InlineData("dalamud://foo/bar/baz/qux/meh/?foo=bar", 5, true)] - [InlineData("dalamud://foo/bar/baz/qux/meh/", 5, true)] - [InlineData("dalamud://foo/bar/baz/qux/meh", 5)] - [InlineData("dalamud://foo/bar/baz/qux", 4)] - [InlineData("dalamud://foo/bar/baz", 3)] - [InlineData("dalamud://foo/bar/", 2)] - [InlineData("dalamud://foo/bar", 2)] - public void ExtractsSegments(string uri, int segmentCount, bool finalSegmentEndsWithSlash = false) - { - var dalamudUri = DalamudUri.FromUri(uri); - var segments = dalamudUri.Segments; - - // First segment must always be `/` - Assert.Equal("/", segments[0]); - - Assert.Equal(segmentCount, segments.Length); - - if (finalSegmentEndsWithSlash) - { - Assert.EndsWith("/", segments.Last()); - } - } - } -} diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 9685b92ac..a2538ebd4 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -79,7 +79,6 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/Networking/Rpc/Model/DalamudUri.cs b/Dalamud/Networking/Rpc/Model/DalamudUri.cs deleted file mode 100644 index 852478762..000000000 --- a/Dalamud/Networking/Rpc/Model/DalamudUri.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Web; - -namespace Dalamud.Networking.Rpc.Model; - -/// -/// A Dalamud Uri, in the format: -/// dalamud://{NAMESPACE}/{ARBITRARY} -/// -public record DalamudUri -{ - private readonly Uri rawUri; - - private DalamudUri(Uri uri) - { - if (uri.Scheme != "dalamud") - { - throw new ArgumentOutOfRangeException(nameof(uri), "URI must be of scheme dalamud."); - } - - this.rawUri = uri; - } - - /// - /// Gets the namespace that this URI should be routed to. Generally a high level component like "PluginInstaller". - /// - public string Namespace => this.rawUri.Authority; - - /// - /// Gets the raw (untargeted) path and query params for this URI. - /// - public string Data => - this.rawUri.GetComponents(UriComponents.PathAndQuery | UriComponents.Fragment, UriFormat.UriEscaped); - - /// - /// Gets the raw (untargeted) path for this URI. - /// - public string Path => this.rawUri.AbsolutePath; - - /// - /// Gets a list of segments based on the provided Data element. - /// - public string[] Segments => this.GetDataSegments(); - - /// - /// Gets the raw query parameters for this URI, if any. - /// - public string Query => this.rawUri.Query; - - /// - /// Gets the query params (as a parsed NameValueCollection) in this URI. - /// - public NameValueCollection QueryParams => HttpUtility.ParseQueryString(this.Query); - - /// - /// Gets the fragment (if one is specified) in this URI. - /// - public string Fragment => this.rawUri.Fragment; - - /// - public override string ToString() => this.rawUri.ToString(); - - /// - /// Build a DalamudURI from a given URI. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(Uri uri) - { - return new DalamudUri(uri); - } - - /// - /// Build a DalamudURI from a URI in string format. - /// - /// The URI to convert to a Dalamud URI. - /// Returns a DalamudUri. - public static DalamudUri FromUri(string uri) => FromUri(new Uri(uri)); - - private string[] GetDataSegments() - { - // reimplementation of the System.URI#Segments, under MIT license. - var path = this.Path; - - var segments = new List(); - var current = 0; - while (current < path.Length) - { - var next = path.IndexOf('/', current); - if (next == -1) - { - next = path.Length - 1; - } - - segments.Add(path.Substring(current, (next - current) + 1)); - current = next + 1; - } - - return segments.ToArray(); - } -} diff --git a/Dalamud/Networking/Rpc/RpcConnection.cs b/Dalamud/Networking/Rpc/RpcConnection.cs deleted file mode 100644 index 5288948eb..000000000 --- a/Dalamud/Networking/Rpc/RpcConnection.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Networking.Rpc.Service; - -using Serilog; - -using StreamJsonRpc; - -namespace Dalamud.Networking.Rpc; - -/// -/// A single RPC client session connected via a stream (named pipe or Unix socket). -/// -internal class RpcConnection : IDisposable -{ - private readonly Stream stream; - private readonly RpcServiceRegistry registry; - private readonly CancellationTokenSource cts = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The stream that this connection will handle. - /// A registry of RPC services. - public RpcConnection(Stream stream, RpcServiceRegistry registry) - { - this.Id = Guid.CreateVersion7(); - this.stream = stream; - this.registry = registry; - - var formatter = new JsonMessageFormatter(); - var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter); - - this.Rpc = new JsonRpc(handler); - this.Rpc.AllowModificationWhileListening = true; - this.Rpc.Disconnected += this.OnDisconnected; - this.registry.Attach(this.Rpc); - - this.Rpc.StartListening(); - } - - /// - /// Gets the GUID for this connection. - /// - public Guid Id { get; } - - /// - /// Gets the JsonRpc instance for this connection. - /// - public JsonRpc Rpc { get; } - - /// - /// Gets a task that's called on RPC completion. - /// - public Task Completion => this.Rpc.Completion; - - /// - public void Dispose() - { - if (!this.cts.IsCancellationRequested) - { - this.cts.Cancel(); - } - - try - { - this.Rpc.Dispose(); - } - catch (Exception ex) - { - Log.Debug(ex, "Error disposing JsonRpc for client {Id}", this.Id); - } - - try - { - this.stream.Dispose(); - } - catch (Exception ex) - { - Log.Debug(ex, "Error disposing stream for client {Id}", this.Id); - } - - this.cts.Dispose(); - GC.SuppressFinalize(this); - } - - private void OnDisconnected(object? sender, JsonRpcDisconnectedEventArgs e) - { - Log.Debug("RPC client {Id} disconnected: {Reason}", this.Id, e.Description); - this.registry.Detach(this.Rpc); - this.Dispose(); - } -} diff --git a/Dalamud/Networking/Rpc/RpcHostService.cs b/Dalamud/Networking/Rpc/RpcHostService.cs deleted file mode 100644 index bbe9dc8eb..000000000 --- a/Dalamud/Networking/Rpc/RpcHostService.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading.Tasks; - -using Dalamud.Logging.Internal; -using Dalamud.Networking.Rpc.Transport; - -namespace Dalamud.Networking.Rpc; - -/// -/// The Dalamud service repsonsible for hosting the RPC. -/// -[ServiceManager.EarlyLoadedService] -internal class RpcHostService : IServiceType, IInternalDisposableService -{ - private readonly ModuleLog log = new("RPC"); - private readonly RpcServiceRegistry registry = new(); - private readonly List transports = []; - - /// - /// Initializes a new instance of the class. - /// - [ServiceManager.ServiceConstructor] - public RpcHostService() - { - this.StartUnixTransport(); - - if (this.transports.Count == 0) - { - this.log.Warning("No RPC hosts could be started on this platform"); - } - } - - /// - /// Gets all active RPC transports. - /// - public IReadOnlyList Transports => this.transports; - - /// - /// Add a new service Object to the RPC host. - /// - /// The object to add. - public void AddService(object service) => this.registry.AddService(service); - - /// - /// Add a new standalone method to the RPC host. - /// - /// The method name to add. - /// The handler to add. - public void AddMethod(string name, Delegate handler) => this.registry.AddMethod(name, handler); - - /// - public void DisposeService() - { - foreach (var host in this.transports) - { - host.Dispose(); - } - - this.transports.Clear(); - } - - /// - public async Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) - { - var clients = this.transports.SelectMany(t => t.Connections).ToImmutableDictionary(); - - if (!clients.TryGetValue(clientId, out var session)) - throw new KeyNotFoundException($"No client {clientId}"); - - return await session.Rpc.InvokeAsync(method, arguments).ConfigureAwait(false); - } - - /// - public async Task BroadcastNotifyAsync(string method, params object[] arguments) - { - await foreach (var transport in this.transports.ToAsyncEnumerable().ConfigureAwait(false)) - { - await transport.BroadcastNotifyAsync(method, arguments).ConfigureAwait(false); - } - } - - private void StartUnixTransport() - { - var transport = new UnixRpcTransport(this.registry); - this.transports.Add(transport); - transport.Start(); - this.log.Information("RpcHostService listening to UNIX socket: {Socket}", transport.SocketPath); - } -} diff --git a/Dalamud/Networking/Rpc/RpcServiceRegistry.cs b/Dalamud/Networking/Rpc/RpcServiceRegistry.cs deleted file mode 100644 index 6daea14bf..000000000 --- a/Dalamud/Networking/Rpc/RpcServiceRegistry.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Collections.Generic; -using System.Threading; - -using StreamJsonRpc; - -namespace Dalamud.Networking.Rpc; - -/// -/// Thread-safe registry of local RPC target objects that are exposed to every connected JsonRpc session. -/// New sessions get all previously registered targets; newly added targets are attached to all active sessions. -/// -internal class RpcServiceRegistry -{ - private readonly Lock sync = new(); - private readonly List targets = []; - private readonly List<(string Name, Delegate Handler)> methods = []; - private readonly List activeRpcs = []; - - /// - /// Registers a new local RPC target object. Its public JSON-RPC methods become callable by clients. - /// Adds to the registry and attaches it to all active RPC sessions. - /// - /// The service instance containing JSON-RPC callable methods to expose. - public void AddService(object service) - { - lock (this.sync) - { - this.targets.Add(service); - foreach (var rpc in this.activeRpcs) - { - rpc.AddLocalRpcTarget(service); - } - } - } - - /// - /// Registers a new standalone JSON-RPC method. - /// - /// The name of the method to add. - /// The handler to add. - public void AddMethod(string name, Delegate handler) - { - lock (this.sync) - { - this.methods.Add((name, handler)); - foreach (var rpc in this.activeRpcs) - { - rpc.AddLocalRpcMethod(name, handler); - } - } - } - - /// - /// Attaches a JsonRpc instance to the registry so it receives all existing service targets. - /// - /// The JsonRpc instance to attach and populate with current targets. - internal void Attach(JsonRpc rpc) - { - lock (this.sync) - { - this.activeRpcs.Add(rpc); - foreach (var t in this.targets) - { - rpc.AddLocalRpcTarget(t); - } - - foreach (var m in this.methods) - { - rpc.AddLocalRpcMethod(m.Name, m.Handler); - } - } - } - - /// - /// Detaches a JsonRpc instance from the registry (e.g. when a client disconnects). - /// - /// The JsonRpc instance being detached. - internal void Detach(JsonRpc rpc) - { - lock (this.sync) - { - this.activeRpcs.Remove(rpc); - } - } -} diff --git a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs b/Dalamud/Networking/Rpc/Service/ClientHelloService.cs deleted file mode 100644 index ae8319f21..000000000 --- a/Dalamud/Networking/Rpc/Service/ClientHelloService.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Diagnostics; -using System.Threading.Tasks; - -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Utility; - -using Lumina.Excel.Sheets; - -namespace Dalamud.Networking.Rpc.Service; - -/// -/// A minimal service to respond with information about this client. -/// -[ServiceManager.EarlyLoadedService] -internal sealed class ClientHelloService : IInternalDisposableService -{ - /// - /// Initializes a new instance of the class. - /// - /// Injected host service. - [ServiceManager.ServiceConstructor] - public ClientHelloService(RpcHostService rpcHostService) - { - rpcHostService.AddMethod("hello", this.HandleHello); - } - - /// - /// Handle a hello request. - /// - /// . - /// Respond with information. - public async Task HandleHello(ClientHelloRequest request) - { - var dalamud = await Service.GetAsync(); - - return new ClientHelloResponse - { - ApiVersion = "1.0", - DalamudVersion = Versioning.GetScmVersion(), - GameVersion = dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown", - ProcessId = Environment.ProcessId, - ProcessStartTime = new DateTimeOffset(Process.GetCurrentProcess().StartTime).ToUnixTimeSeconds(), - ClientState = await this.GetClientIdentifier(), - }; - } - - /// - public void DisposeService() - { - } - - private async Task GetClientIdentifier() - { - var framework = await Service.GetAsync(); - var clientState = await Service.GetAsync(); - var dataManager = await Service.GetAsync(); - - var clientIdentifier = $"FFXIV Process ${Environment.ProcessId}"; - - await framework.RunOnFrameworkThread(() => - { - if (clientState.IsLoggedIn) - { - var player = clientState.LocalPlayer; - if (player != null) - { - var world = dataManager.GetExcelSheet().GetRow(player.HomeWorld.RowId); - clientIdentifier = $"Logged in as {player.Name.TextValue} @ {world.Name.ExtractText()}"; - } - } - else - { - clientIdentifier = "On login screen"; - } - }); - - return clientIdentifier; - } -} - -/// -/// A request from a client to say hello. -/// -internal record ClientHelloRequest -{ - /// - /// Gets the API version this client is expecting. - /// - public string ApiVersion { get; init; } = string.Empty; - - /// - /// Gets the user agent of the client. - /// - public string UserAgent { get; init; } = string.Empty; -} - -/// -/// A response from Dalamud to a hello request. -/// -internal record ClientHelloResponse -{ - /// - /// Gets the API version this server has offered. - /// - public string? ApiVersion { get; init; } - - /// - /// Gets the current Dalamud version. - /// - public string? DalamudVersion { get; init; } - - /// - /// Gets the current game version. - /// - public string? GameVersion { get; init; } - - /// - /// Gets the process ID of this client. - /// - public int? ProcessId { get; init; } - - /// - /// Gets the time this process started. - /// - public long? ProcessStartTime { get; init; } - - /// - /// Gets a state for this client for user display. - /// - public string? ClientState { get; init; } -} diff --git a/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs b/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs deleted file mode 100644 index 9fa311ede..000000000 --- a/Dalamud/Networking/Rpc/Service/LinkHandlerService.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; - -using Dalamud.Logging.Internal; -using Dalamud.Networking.Rpc.Model; -using Dalamud.Utility; - -namespace Dalamud.Networking.Rpc.Service; - -/// -/// A service responsible for handling Dalamud URIs and dispatching them accordingly. -/// -[ServiceManager.EarlyLoadedService] -internal class LinkHandlerService : IInternalDisposableService -{ - private readonly ModuleLog log = new("LinkHandler"); - - // key: namespace (e.g. "plugin" or "PluginInstaller") -> list of handlers - private readonly ConcurrentDictionary>> handlers - = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Initializes a new instance of the class. - /// - /// The injected RPC host service. - [ServiceManager.ServiceConstructor] - public LinkHandlerService(RpcHostService rpcHostService) - { - rpcHostService.AddMethod("handleLink", this.HandleLinkCall); - } - - /// - public void DisposeService() - { - } - - /// - /// Register a handler for a namespace. All URIs with this namespace will be dispatched to the handler. - /// - /// The namespace to use for this subscription. - /// The command handler. - public void Register(string ns, Action handler) - { - if (string.IsNullOrWhiteSpace(ns)) - throw new ArgumentNullException(nameof(ns)); - - var list = this.handlers.GetOrAdd(ns, _ => []); - lock (list) - { - list.Add(handler); - } - - this.log.Verbose("Registered handler for {Namespace}", ns); - } - - /// - /// Unregister a handler. - /// - /// The namespace to use for this subscription. - /// The command handler. - public void Unregister(string ns, Action handler) - { - if (string.IsNullOrWhiteSpace(ns)) - return; - - if (!this.handlers.TryGetValue(ns, out var list)) - return; - - list.RemoveAll(x => x == handler); - - if (list.Count == 0) - this.handlers.TryRemove(ns, out _); - - this.log.Verbose("Unregistered handler for {Namespace}", ns); - } - - /// - /// Dispatch a URI to matching handlers. - /// - /// The URI to parse and dispatch. - public void Dispatch(DalamudUri uri) - { - this.log.Information("Received URI: {Uri}", uri.ToString()); - - var ns = uri.Namespace; - if (!this.handlers.TryGetValue(ns, out var actions)) - return; - - foreach (var h in actions) - { - h.InvokeSafely(uri); - } - } - - /// - /// The RPC-invokable link handler. - /// - /// A plain-text URI to parse. - public void HandleLinkCall(string uri) - { - if (string.IsNullOrWhiteSpace(uri)) - return; - - var du = DalamudUri.FromUri(uri); - this.Dispatch(du); - } -} diff --git a/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs deleted file mode 100644 index 269617fc0..000000000 --- a/Dalamud/Networking/Rpc/Service/Links/DebugLinkHandler.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Dalamud.Game.Gui.Toast; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.Internal; -using Dalamud.Networking.Rpc.Model; - -namespace Dalamud.Networking.Rpc.Service.Links; - -#if DEBUG - -/// -/// A debug controller for link handling. -/// -[ServiceManager.EarlyLoadedService] -internal sealed class DebugLinkHandler : IInternalDisposableService -{ - private readonly LinkHandlerService linkHandlerService; - - /// - /// Initializes a new instance of the class. - /// - /// Injected LinkHandler. - [ServiceManager.ServiceConstructor] - public DebugLinkHandler(LinkHandlerService linkHandler) - { - this.linkHandlerService = linkHandler; - - this.linkHandlerService.Register("debug", this.HandleLink); - } - - /// - public void DisposeService() - { - this.linkHandlerService.Unregister("debug", this.HandleLink); - } - - private void HandleLink(DalamudUri uri) - { - var action = uri.Path.Split("/").GetValue(1)?.ToString(); - switch (action) - { - case "toast": - this.ShowToast(uri); - break; - case "notification": - this.ShowNotification(uri); - break; - } - } - - private void ShowToast(DalamudUri uri) - { - var message = uri.QueryParams.Get("message") ?? "Hello, world!"; - Service.Get().ShowNormal(message); - } - - private void ShowNotification(DalamudUri uri) - { - Service.Get().AddNotification( - new Notification - { - Title = uri.QueryParams.Get("title"), - Content = uri.QueryParams.Get("content") ?? "Hello, world!", - }); - } -} - -#endif diff --git a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs b/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs deleted file mode 100644 index 3b7f18437..000000000 --- a/Dalamud/Networking/Rpc/Service/Links/PluginLinkHandler.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System.Linq; - -using Dalamud.Console; -using Dalamud.IoC; -using Dalamud.IoC.Internal; -using Dalamud.Networking.Rpc.Model; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Plugin.Services; - -#pragma warning disable DAL_RPC - -namespace Dalamud.Networking.Rpc.Service.Links; - -/// -[PluginInterface] -[ServiceManager.ScopedService] -[ResolveVia] -public class PluginLinkHandler : IInternalDisposableService, IPluginLinkHandler -{ - private readonly LinkHandlerService linkHandler; - private readonly LocalPlugin localPlugin; - - /// - /// Initializes a new instance of the class. - /// - /// The plugin to bind this service to. - /// The central link handler. - internal PluginLinkHandler(LocalPlugin localPlugin, LinkHandlerService linkHandler) - { - this.linkHandler = linkHandler; - this.localPlugin = localPlugin; - - this.linkHandler.Register("plugin", this.HandleUri); - } - - /// - public event IPluginLinkHandler.PluginUriReceived? OnUriReceived; - - /// - public void DisposeService() - { - this.OnUriReceived = null; - this.linkHandler.Unregister("plugin", this.HandleUri); - } - - private void HandleUri(DalamudUri uri) - { - var target = uri.Path.Split("/").ElementAtOrDefault(1); - var thisPlugin = ConsoleManagerPluginUtil.GetSanitizedNamespaceName(this.localPlugin.InternalName); - if (target == null || !string.Equals(target, thisPlugin, StringComparison.OrdinalIgnoreCase)) - { - return; - } - - this.OnUriReceived?.Invoke(uri); - } -} diff --git a/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs deleted file mode 100644 index ad7578eb4..000000000 --- a/Dalamud/Networking/Rpc/Transport/IRpcTransport.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Dalamud.Networking.Rpc.Transport; - -/// -/// Interface for RPC host implementations (named pipes or Unix sockets). -/// -internal interface IRpcTransport : IDisposable -{ - /// - /// Gets a list of active RPC connections. - /// - IReadOnlyDictionary Connections { get; } - - /// Starts accepting client connections. - void Start(); - - /// Invoke an RPC request on a specific client expecting a result. - /// The client ID to invoke. - /// The method to invoke. - /// Any arguments to invoke. - /// An optional return based on the specified RPC. - /// The expected response type. - Task InvokeClientAsync(Guid clientId, string method, params object[] arguments); - - /// Send a notification to all connected clients (no response expected). - /// The method name to broadcast. - /// The arguments to broadcast. - /// Returns a Task when completed. - Task BroadcastNotifyAsync(string method, params object[] arguments); -} diff --git a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs b/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs deleted file mode 100644 index 17da51444..000000000 --- a/Dalamud/Networking/Rpc/Transport/UnixRpcTransport.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Logging.Internal; -using Dalamud.Utility; - -namespace Dalamud.Networking.Rpc.Transport; - -/// -/// Simple multi-client JSON-RPC Unix socket host using StreamJsonRpc. -/// -internal class UnixRpcTransport : IRpcTransport -{ - private readonly ModuleLog log = new("RPC/Transport/UnixSocket"); - - private readonly RpcServiceRegistry registry; - private readonly CancellationTokenSource cts = new(); - private readonly ConcurrentDictionary sessions = new(); - private readonly string? cleanupSocketDirectory; - - private Task? acceptLoopTask; - private Socket? listenSocket; - - /// - /// Initializes a new instance of the class. - /// - /// The RPC service registry to use. - /// The Unix socket directory to use. If null, defaults to Dalamud home directory. - /// The name of the socket to create. - public UnixRpcTransport(RpcServiceRegistry registry, string? socketDirectory = null, string? socketName = null) - { - this.registry = registry; - socketName ??= $"DalamudRPC.{Environment.ProcessId}.sock"; - - if (!socketDirectory.IsNullOrEmpty()) - { - this.SocketPath = Path.Combine(socketDirectory, socketName); - } - else - { - socketDirectory = Service.Get().StartInfo.TempDirectory; - - if (socketDirectory == null) - { - this.SocketPath = Path.Combine(Path.GetTempPath(), socketName); - this.log.Warning("Temp dir was not set in StartInfo; using system temp for unix socket."); - } - else - { - this.SocketPath = Path.Combine(socketDirectory, socketName); - this.cleanupSocketDirectory = socketDirectory; - } - } - } - - /// - /// Gets the path of the Unix socket this RPC host is using. - /// - public string SocketPath { get; } - - /// - public IReadOnlyDictionary Connections => this.sessions; - - /// Starts accepting client connections. - public void Start() - { - if (this.acceptLoopTask != null) return; - - // Make the directory for the socket if it doesn't exist - var socketDir = Path.GetDirectoryName(this.SocketPath); - if (!string.IsNullOrEmpty(socketDir) && !Directory.Exists(socketDir)) - { - this.log.Error("Directory for unix socket does not exist: {Path}", socketDir); - return; - } - - // Delete existing socket for this PID, if it exists. - if (File.Exists(this.SocketPath)) - { - try - { - File.Delete(this.SocketPath); - } - catch (Exception ex) - { - this.log.Warning(ex, "Failed to delete existing socket file: {Path}", this.SocketPath); - } - } - - this.acceptLoopTask = Task.Factory.StartNew(this.AcceptLoopAsync, TaskCreationOptions.LongRunning); - } - - /// Invoke an RPC request on a specific client expecting a result. - /// The client ID to invoke. - /// The method to invoke. - /// Any arguments to invoke. - /// An optional return based on the specified RPC. - /// The expected response type. - public Task InvokeClientAsync(Guid clientId, string method, params object[] arguments) - { - if (!this.sessions.TryGetValue(clientId, out var session)) - throw new KeyNotFoundException($"No client {clientId}"); - - return session.Rpc.InvokeAsync(method, arguments); - } - - /// Send a notification to all connected clients (no response expected). - /// The method name to broadcast. - /// The arguments to broadcast. - /// Returns a Task when completed. - public Task BroadcastNotifyAsync(string method, params object[] arguments) - { - var list = this.sessions.Values; - var tasks = new List(list.Count); - foreach (var s in list) - { - tasks.Add(s.Rpc.NotifyAsync(method, arguments)); - } - - return Task.WhenAll(tasks); - } - - /// - public void Dispose() - { - this.cts.Cancel(); - this.acceptLoopTask?.Wait(1000); - - foreach (var kv in this.sessions) - { - kv.Value.Dispose(); - } - - this.sessions.Clear(); - - this.listenSocket?.Dispose(); - - if (File.Exists(this.SocketPath)) - { - try - { - File.Delete(this.SocketPath); - } - catch (Exception ex) - { - this.log.Warning(ex, "Failed to delete socket file on dispose: {Path}", this.SocketPath); - } - } - - this.cts.Dispose(); - this.log.Information("UnixRpcHost disposed ({Socket})", this.SocketPath); - GC.SuppressFinalize(this); - } - - private async Task AcceptLoopAsync() - { - var token = this.cts.Token; - - try - { - var endpoint = new UnixDomainSocketEndPoint(this.SocketPath); - this.listenSocket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); - this.listenSocket.Bind(endpoint); - this.listenSocket.Listen(128); - - while (!token.IsCancellationRequested) - { - Socket? clientSocket = null; - try - { - clientSocket = await this.listenSocket.AcceptAsync(token).ConfigureAwait(false); - - var stream = new NetworkStream(clientSocket, ownsSocket: true); - var session = new RpcConnection(stream, this.registry); - this.sessions.TryAdd(session.Id, session); - - this.log.Debug("RPC connection created: {Id}", session.Id); - - _ = session.Completion.ContinueWith(t => - { - this.sessions.TryRemove(session.Id, out _); - this.log.Debug("RPC connection removed: {Id}", session.Id); - }, TaskScheduler.Default); - } - catch (OperationCanceledException) - { - clientSocket?.Dispose(); - break; - } - catch (Exception ex) - { - clientSocket?.Dispose(); - this.log.Error(ex, "Error in socket accept loop"); - await Task.Delay(500, token).ConfigureAwait(false); - } - } - } - catch (Exception ex) - { - this.log.Error(ex, "Fatal error in Unix socket accept loop"); - } - } -} diff --git a/Dalamud/Plugin/Services/IPluginLinkHandler.cs b/Dalamud/Plugin/Services/IPluginLinkHandler.cs deleted file mode 100644 index 37101222a..000000000 --- a/Dalamud/Plugin/Services/IPluginLinkHandler.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -using Dalamud.Networking.Rpc.Model; - -namespace Dalamud.Plugin.Services; - -/// -/// A service to allow plugins to subscribe to dalamud:// URIs targeting them. Plugins will receive any URI sent to the -/// dalamud://plugin/{PLUGIN_INTERNAL_NAME}/... namespace. -/// -[Experimental("DAL_RPC", Message = "This service will be finalized around 7.41 and may change before then.")] -public interface IPluginLinkHandler : IDalamudService -{ - /// - /// A delegate containing the received URI. - /// - /// The URI opened by the user. - public delegate void PluginUriReceived(DalamudUri uri); - - /// - /// The event fired when a URI targeting this plugin is received. - /// - event PluginUriReceived OnUriReceived; -} diff --git a/Directory.Packages.props b/Directory.Packages.props index 77a4035a4..18760037b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -46,9 +46,6 @@ - - - From 8b0bb343f9e78a167eec64ea9fc06832ac32fed4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 18:33:04 +0000 Subject: [PATCH 150/164] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 7d3f90e61..d6ff8cf46 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 7d3f90e61732df6aef63196d1abaab1074f6f3c9 +Subproject commit d6ff8cf46c7e341989843c28c7550f8d50bee851 From e603af5accdcdd1cba027d3b1182cbd10ab0013a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 18:33:10 +0000 Subject: [PATCH 151/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index faf803a76..2f0f4d2c8 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit faf803a76813511768d45c137a543aaacf5420b8 +Subproject commit 2f0f4d2c86989a7ef04bdbf975d0569948582fc9 From 6374c0d6ae7bdd055075c1e9aa780112a51dc9d8 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Dec 2025 21:25:49 +0100 Subject: [PATCH 152/164] Use UIModuleHandlePacketDetour for ZoneInit --- Dalamud/Game/ClientState/ClientState.cs | 30 ++++++------------- .../ClientState/ClientStateAddressResolver.cs | 6 ---- 2 files changed, 9 insertions(+), 27 deletions(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 93720e1db..f7c0b75ed 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -37,7 +37,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; - private readonly Hook handleZoneInitPacketHook; private readonly Hook uiModuleHandlePacketHook; private readonly Hook setCurrentInstanceHook; @@ -72,13 +71,11 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language; - this.handleZoneInitPacketHook = Hook.FromAddress(this.AddressResolver.HandleZoneInitPacket, this.HandleZoneInitPacketDetour); this.uiModuleHandlePacketHook = Hook.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour); this.setCurrentInstanceHook = Hook.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour); this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; - this.handleZoneInitPacketHook.Enable(); this.uiModuleHandlePacketHook.Enable(); this.setCurrentInstanceHook.Enable(); @@ -271,7 +268,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState /// void IInternalDisposableService.DisposeService() { - this.handleZoneInitPacketHook.Dispose(); this.uiModuleHandlePacketHook.Dispose(); this.onLogoutHook.Dispose(); this.setCurrentInstanceHook.Dispose(); @@ -294,23 +290,6 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.framework.Update += this.OnFrameworkUpdate; } - private void HandleZoneInitPacketDetour(nint a1, uint localPlayerEntityId, nint packet, byte type) - { - this.handleZoneInitPacketHook.Original(a1, localPlayerEntityId, packet, type); - - try - { - var eventArgs = ZoneInitEventArgs.Read(packet); - Log.Debug($"ZoneInit: {eventArgs}"); - this.ZoneInit?.InvokeSafely(eventArgs); - this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId; - } - catch (Exception ex) - { - Log.Error(ex, "Exception during ZoneInit"); - } - } - private unsafe void UIModuleHandlePacketDetour( UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet) { @@ -356,6 +335,15 @@ internal sealed class ClientState : IInternalDisposableService, IClientState break; } + + case (UIModulePacketType)5: // TODO: Use UIModulePacketType.InitZone when available + { + var eventArgs = ZoneInitEventArgs.Read((nint)packet); + Log.Debug($"ZoneInit: {eventArgs}"); + this.ZoneInit?.InvokeSafely(eventArgs); + this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId; + break; + } } } diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 53774121d..ae7549b97 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -21,11 +21,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver // Functions - /// - /// Gets the address of the method that handles the ZoneInit packet. - /// - public nint HandleZoneInitPacket { get; private set; } - /// /// Gets the address of the method that sets the current public instance. /// @@ -37,7 +32,6 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver /// The signature scanner to facilitate setup. protected override void Setup64Bit(ISigScanner sig) { - this.HandleZoneInitPacket = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 45"); this.SetCurrentInstance = sig.ScanText("E8 ?? ?? ?? ?? 0F B6 55 ?? 48 8D 0D ?? ?? ?? ?? C0 EA"); // NetworkModuleProxy.SetCurrentInstance // These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used. From 3ef6135f15d84125906858914fffa51db23753bc Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 20 Dec 2025 21:26:10 +0100 Subject: [PATCH 153/164] Fix reading ActiveFestivals in ZoneInitEventArgs --- Dalamud/Game/ClientState/ZoneInit.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/ClientState/ZoneInit.cs b/Dalamud/Game/ClientState/ZoneInit.cs index 5c2213c90..7eb4576aa 100644 --- a/Dalamud/Game/ClientState/ZoneInit.cs +++ b/Dalamud/Game/ClientState/ZoneInit.cs @@ -59,7 +59,7 @@ public class ZoneInitEventArgs : EventArgs eventArgs.ContentFinderCondition = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x06)); eventArgs.Weather = dataManager.GetExcelSheet().GetRow(*(byte*)(packet + 0x10)); - const int NumFestivals = 4; + const int NumFestivals = 8; eventArgs.ActiveFestivals = new Festival[NumFestivals]; eventArgs.ActiveFestivalPhases = new ushort[NumFestivals]; @@ -67,7 +67,7 @@ public class ZoneInitEventArgs : EventArgs // but it's unclear why they exist as separate entries and why they would be different. for (var i = 0; i < NumFestivals; i++) { - eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x2E + (i * 2))); + eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x26 + (i * 2))); eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2)); } From 1f5f6f89148e309e8d570cee5149d49f98ca0a91 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Dec 2025 20:45:31 +0000 Subject: [PATCH 154/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 2f0f4d2c8..63c259673 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 2f0f4d2c86989a7ef04bdbf975d0569948582fc9 +Subproject commit 63c2596738d2807d1d5ddf13c4dddbe0840d4df4 From da2b80156a957f704c2390f9ab174e9a3e56c7dd Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 20 Dec 2025 21:45:47 +0100 Subject: [PATCH 155/164] Fix some wording on badge tab --- .../Internal/Windows/Settings/Tabs/SettingsTabBadge.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs index 8e44ef7ea..e39c1952c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabBadge.cs @@ -20,7 +20,7 @@ internal sealed class SettingsTabBadge : SettingsTab public override string Title => Loc.Localize("DalamudSettingsBadge", "Badges"); - public override SettingsOpenKind Kind => SettingsOpenKind.ServerInfoBar; + public override SettingsOpenKind Kind => SettingsOpenKind.Badge; public override SettingsEntry[] Entries { get; } = [ @@ -36,12 +36,12 @@ internal sealed class SettingsTabBadge : SettingsTab var badgeManager = Service.Get(); var dalamudInterface = Service.Get(); - ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarHint", "Plugins can put additional information into your server information bar(where world & time can be seen).\nYou can reorder and disable these here.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingBadgesHint", "On this tab, you can unlock small badges that show on your title screen.\nBadge codes are usually given out during community events or contests.")); ImGuiHelpers.ScaledDummy(5); ImGui.Text(Loc.Localize("DalamudSettingsBadgesUnlock", "Unlock a badge")); - ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsBadgesUnlockHint", "If you have received a code for a badge, enter it here to unlock the badge.\nCodes are usually given out during community events or contests.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsBadgesUnlockHint", "If you have received a code for a badge, enter it here to unlock the badge.")); ImGui.InputTextWithHint( "##BadgePassword", Loc.Localize("DalamudSettingsBadgesUnlockHintInput", "Enter badge code here"), @@ -79,7 +79,7 @@ internal sealed class SettingsTabBadge : SettingsTab if (haveBadges.Length == 0) { - ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingServerInfoBarDidNone", "You did not unlock any badges yet.\nBadges can be unlocked by participating in community events or contests.")); + ImGui.TextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsBadgesDidNone", "You did not unlock any badges yet.")); } var badgeTexture = Service.Get().GetDalamudTextureWrap(DalamudAsset.BadgeAtlas); From 3abddbae2ce516301b512f2f716b76deb3a37a95 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 21 Dec 2025 00:51:50 +0100 Subject: [PATCH 156/164] build: 14.0.0.1 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a2538ebd4..fc46cffa3 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 14.0.0.0 + 14.0.0.1 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 69c24fdbb9deceafe4b37a61680a5bada84f15af Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 01:23:12 +0000 Subject: [PATCH 157/164] Update Excel Schema --- lib/Lumina.Excel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index d6ff8cf46..7737a7699 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit d6ff8cf46c7e341989843c28c7550f8d50bee851 +Subproject commit 7737a76995d6992e62fdc5d6a84871af4fe189bd From 96b5ad1b65fe4d4d3e0daecbe99d455c8a27e9b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 01:23:19 +0000 Subject: [PATCH 158/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 63c259673..7c3f1b81f 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 63c2596738d2807d1d5ddf13c4dddbe0840d4df4 +Subproject commit 7c3f1b81f5d3f1515bc8b3a62892d219638cee33 From a59efbd84c67c1560b117973df01cf31c54618ab Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 21 Dec 2025 02:41:22 +0100 Subject: [PATCH 159/164] Fixes for excel renamings --- Dalamud/Game/UnlockState/UnlockState.cs | 16 ++++++++-------- .../Windows/Data/Widgets/UIColorWidget.cs | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index cc70a524c..1878f54db 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -267,26 +267,26 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState switch ((ItemActionAction)row.ItemAction.Value.Action.RowId) { case ItemActionAction.Companion: - return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]); + return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.BuddyEquip: - return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]); + return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.Mount: - return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]); + return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.SecretRecipeBook: - return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]); + return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.UnlockLink: case ItemActionAction.OccultRecords: - return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]); + return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.TripleTriadCard when row.AdditionalData.Is(): return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId); case ItemActionAction.FolkloreTome: - return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]); + return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.OrchestrionRoll when row.AdditionalData.Is(): return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId); @@ -295,13 +295,13 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId); case ItemActionAction.Ornament: - return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]); + return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0].RowId); case ItemActionAction.Glasses: return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId); case ItemActionAction.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: - var supportJobId = (byte)row.ItemAction.Value.Data[0]; + var supportJobId = (byte)row.ItemAction.Value.Data[0].RowId; return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0; case ItemActionAction.CompanySealVouchers: diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index 3550f053c..fd3f1d11c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -126,17 +126,17 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_white"); - if (this.DrawColorColumn(row.Unknown0) && + if (this.DrawColorColumn(row.ClearWhite) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0); + DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite); ImGui.PopID(); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_green"); - if (this.DrawColorColumn(row.Unknown1) && + if (this.DrawColorColumn(row.ClearGreen) && adjacentRow.HasValue) - DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1); + DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen); ImGui.PopID(); } } From f307aded73e9e4e4f0ea7fc19e13b324257dc395 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 20 Dec 2025 18:31:17 -0800 Subject: [PATCH 160/164] Lumina revert (#2544) --- Dalamud/Game/UnlockState/UnlockState.cs | 16 ++++++++-------- .../Windows/Data/Widgets/UIColorWidget.cs | 8 ++++---- lib/Lumina.Excel | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Dalamud/Game/UnlockState/UnlockState.cs b/Dalamud/Game/UnlockState/UnlockState.cs index 1878f54db..cc70a524c 100644 --- a/Dalamud/Game/UnlockState/UnlockState.cs +++ b/Dalamud/Game/UnlockState/UnlockState.cs @@ -267,26 +267,26 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState switch ((ItemActionAction)row.ItemAction.Value.Action.RowId) { case ItemActionAction.Companion: - return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0].RowId); + return UIState.Instance()->IsCompanionUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.BuddyEquip: - return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0].RowId); + return UIState.Instance()->Buddy.CompanionInfo.IsBuddyEquipUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.Mount: - return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0].RowId); + return PlayerState.Instance()->IsMountUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.SecretRecipeBook: - return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0].RowId); + return PlayerState.Instance()->IsSecretRecipeBookUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.UnlockLink: case ItemActionAction.OccultRecords: - return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0].RowId); + return UIState.Instance()->IsUnlockLinkUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.TripleTriadCard when row.AdditionalData.Is(): return UIState.Instance()->IsTripleTriadCardUnlocked((ushort)row.AdditionalData.RowId); case ItemActionAction.FolkloreTome: - return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0].RowId); + return PlayerState.Instance()->IsFolkloreBookUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.OrchestrionRoll when row.AdditionalData.Is(): return PlayerState.Instance()->IsOrchestrionRollUnlocked(row.AdditionalData.RowId); @@ -295,13 +295,13 @@ internal unsafe class UnlockState : IInternalDisposableService, IUnlockState return PlayerState.Instance()->IsFramersKitUnlocked(row.AdditionalData.RowId); case ItemActionAction.Ornament: - return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0].RowId); + return PlayerState.Instance()->IsOrnamentUnlocked(row.ItemAction.Value.Data[0]); case ItemActionAction.Glasses: return PlayerState.Instance()->IsGlassesUnlocked((ushort)row.AdditionalData.RowId); case ItemActionAction.SoulShards when PublicContentOccultCrescent.GetState() is var occultCrescentState && occultCrescentState != null: - var supportJobId = (byte)row.ItemAction.Value.Data[0].RowId; + var supportJobId = (byte)row.ItemAction.Value.Data[0]; return supportJobId < occultCrescentState->SupportJobLevels.Length && occultCrescentState->SupportJobLevels[supportJobId] != 0; case ItemActionAction.CompanySealVouchers: diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index fd3f1d11c..3550f053c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -126,17 +126,17 @@ internal class UiColorWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_white"); - if (this.DrawColorColumn(row.ClearWhite) && + if (this.DrawColorColumn(row.Unknown0) && adjacentRow.HasValue) - DrawEdgePreview(id, row.ClearWhite, adjacentRow.Value.ClearWhite); + DrawEdgePreview(id, row.Unknown0, adjacentRow.Value.Unknown0); ImGui.PopID(); ImGui.TableNextColumn(); ImGui.AlignTextToFramePadding(); ImGui.PushID($"row{id}_green"); - if (this.DrawColorColumn(row.ClearGreen) && + if (this.DrawColorColumn(row.Unknown1) && adjacentRow.HasValue) - DrawEdgePreview(id, row.ClearGreen, adjacentRow.Value.ClearGreen); + DrawEdgePreview(id, row.Unknown1, adjacentRow.Value.Unknown1); ImGui.PopID(); } } diff --git a/lib/Lumina.Excel b/lib/Lumina.Excel index 7737a7699..d6ff8cf46 160000 --- a/lib/Lumina.Excel +++ b/lib/Lumina.Excel @@ -1 +1 @@ -Subproject commit 7737a76995d6992e62fdc5d6a84871af4fe189bd +Subproject commit d6ff8cf46c7e341989843c28c7550f8d50bee851 From 5513bd1633f376213cf44691c2c74816931a57b7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 21 Dec 2025 11:59:55 +0000 Subject: [PATCH 161/164] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 7c3f1b81f..9c5f93cf3 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7c3f1b81f5d3f1515bc8b3a62892d219638cee33 +Subproject commit 9c5f93cf3ac57236656cd2323b93cd258ea84a88 From 4dcfa9da98173fbf81ad98300a44e6ac2a5df331 Mon Sep 17 00:00:00 2001 From: Infi Date: Sun, 21 Dec 2025 14:37:03 +0100 Subject: [PATCH 162/164] - Add ToDo for ulong change --- Dalamud/Game/ClientState/Party/PartyMember.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Game/ClientState/Party/PartyMember.cs b/Dalamud/Game/ClientState/Party/PartyMember.cs index c9980d9f2..84e3f21c8 100644 --- a/Dalamud/Game/ClientState/Party/PartyMember.cs +++ b/Dalamud/Game/ClientState/Party/PartyMember.cs @@ -6,6 +6,7 @@ using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Statuses; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Utility; using Lumina.Excel; @@ -124,6 +125,7 @@ internal unsafe readonly struct PartyMember(CSPartyMember* ptr) : IPartyMember public Vector3 Position => ptr->Position; /// + [Api15ToDo("Change type to ulong.")] public long ContentId => (long)ptr->ContentId; /// From e2a18dee5eea748d5a088f3937af1b30740fce88 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 21 Dec 2025 15:32:24 +0100 Subject: [PATCH 163/164] build: 14.0.0.2 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index fc46cffa3..5f79eb274 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 14.0.0.1 + 14.0.0.2 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 8ccfac231830104492d5b53d71c08d36e693f16c Mon Sep 17 00:00:00 2001 From: Soreepeong <3614868+Soreepeong@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:46:29 +0900 Subject: [PATCH 164/164] Fix wrong CancellationToken usage --- Dalamud/Interface/Internal/StaThreadService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/StaThreadService.cs b/Dalamud/Interface/Internal/StaThreadService.cs index bb5caa281..5e93bbf75 100644 --- a/Dalamud/Interface/Internal/StaThreadService.cs +++ b/Dalamud/Interface/Internal/StaThreadService.cs @@ -113,7 +113,7 @@ internal partial class StaThreadService : IInternalDisposableService using var cts = CancellationTokenSource.CreateLinkedTokenSource( this.cancellationTokenSource.Token, cancellationToken); - await this.taskFactory.StartNew(action, cancellationToken).ConfigureAwait(true); + await this.taskFactory.StartNew(action, cts.Token).ConfigureAwait(true); } /// Runs a given delegate in the messaging thread. @@ -126,7 +126,7 @@ internal partial class StaThreadService : IInternalDisposableService using var cts = CancellationTokenSource.CreateLinkedTokenSource( this.cancellationTokenSource.Token, cancellationToken); - return await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + return await this.taskFactory.StartNew(func, cts.Token).ConfigureAwait(true); } /// Runs a given delegate in the messaging thread. @@ -138,7 +138,7 @@ internal partial class StaThreadService : IInternalDisposableService using var cts = CancellationTokenSource.CreateLinkedTokenSource( this.cancellationTokenSource.Token, cancellationToken); - await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + await await this.taskFactory.StartNew(func, cts.Token).ConfigureAwait(true); } /// Runs a given delegate in the messaging thread. @@ -151,7 +151,7 @@ internal partial class StaThreadService : IInternalDisposableService using var cts = CancellationTokenSource.CreateLinkedTokenSource( this.cancellationTokenSource.Token, cancellationToken); - return await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + return await await this.taskFactory.StartNew(func, cts.Token).ConfigureAwait(true); } [LibraryImport("ole32.dll")]