From f1f2b51cd69212e2a695938ef9d53f6825b60604 Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sat, 27 May 2023 16:31:10 -0700 Subject: [PATCH 001/585] start v9 branch, pin API level to major version --- Dalamud/Dalamud.csproj | 2 +- Dalamud/Plugin/Internal/PluginManager.cs | 3 ++- README.md | 23 +++++++++++------------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 116ebd008..6e6a01fa9 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.6.0.0 + 9.0.0.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 9df249dd7..0ff63f2c0 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -49,8 +49,9 @@ internal partial class PluginManager : IDisposable, IServiceType { /// /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded. + /// As of Dalamud 9.x, this always matches the major version number of Dalamud. /// - public const int DalamudApiLevel = 8; + public static int DalamudApiLevel => Assembly.GetExecutingAssembly().GetName().Version!.Major; /// /// Default time to wait between plugin unload and plugin assembly unload. diff --git a/README.md b/README.md index f98961c3e..97dd4e9dd 100644 --- a/README.md +++ b/README.md @@ -26,23 +26,22 @@ Thanks to Mino, whose work has made this possible! These components are used in order to load Dalamud into a target process. Dalamud can be loaded via DLL injection, or by rewriting a process' entrypoint. -| Name | Purpose | -|---|---| -| *Dalamud.Injector.Boot* (C++) | Loads the .NET Core runtime into a process via hostfxr and kicks off Dalamud.Injector | -| *Dalamud.Injector* (C#) | Performs DLL injection on the target process | -| *Dalamud.Boot* (C++) | Loads the .NET Core runtime into the active process and kicks off Dalamud, or rewrites a target process' entrypoint to do so | -| *Dalamud* (C#) | Core API, game bindings, plugin framework | -| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features | +| Name | Purpose | +|-------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| *Dalamud.Injector.Boot* (C++) | Loads the .NET Core runtime into a process via hostfxr and kicks off Dalamud.Injector | +| *Dalamud.Injector* (C#) | Performs DLL injection on the target process | +| *Dalamud.Boot* (C++) | Loads the .NET Core runtime into the active process and kicks off Dalamud, or rewrites a target process' entrypoint to do so | +| *Dalamud* (C#) | Core API, game bindings, plugin framework | +| *Dalamud.CorePlugin* (C#) | Testbed plugin that can access Dalamud internals, to prototype new Dalamud features | ## Branches We are currently working from the following branches. -| Name | Purpose | .NET Version | Track | -|---|---|---|---| -| *master* | Current release branch | .NET 6.0.3 (March 2022) | Release & Staging | -| *net7* | Upgrade to .NET 7 | .NET 7.0.0 (November 2022) | net7 | -| *api3* | Legacy version, no longer in active use | .NET Framework 4.7.2 (April 2017) | - | +| Name | API Level | Purpose | .NET Version | Track | +|----------|-----------|------------------------------------------------------------|----------------------------|-------------------| +| *master* | **8** | Current release branch | .NET 7.0.0 (November 2022) | Release & Staging | +| *v9* | **9** | Next major version, slated for release alongside Patch 6.5 | .NET 7.0.0 (November 2022) | v9 |
From f61e6335174b7ee2356c8ee1a7ae4acbabfdda96 Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sat, 27 May 2023 17:11:20 -0700 Subject: [PATCH 002/585] cleanup: remove obsolete FrameworkAddressResolver.BaseAddress property --- Dalamud/Game/FrameworkAddressResolver.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Dalamud/Game/FrameworkAddressResolver.cs b/Dalamud/Game/FrameworkAddressResolver.cs index e3d128f0f..36915d7a9 100644 --- a/Dalamud/Game/FrameworkAddressResolver.cs +++ b/Dalamud/Game/FrameworkAddressResolver.cs @@ -5,14 +5,8 @@ namespace Dalamud.Game; /// /// The address resolver for the class. /// -public sealed unsafe class FrameworkAddressResolver : BaseAddressResolver +public sealed class FrameworkAddressResolver : BaseAddressResolver { - /// - /// Gets the base address of the Framework object. - /// - [Obsolete("Please use FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance() instead.")] - public IntPtr BaseAddress => new(FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance()); - /// /// Gets the address for the function that is called once the Framework is destroyed. /// From ddf4e56ee666217a5b2c7b8828c5e6cab719b4ae Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sat, 27 May 2023 17:15:22 -0700 Subject: [PATCH 003/585] cleanup: remove obsolete SeStringManager class --- .../Text/SeStringHandling/SeStringManager.cs | 111 ------------------ Dalamud/Memory/MemoryHelper.cs | 20 ++-- 2 files changed, 10 insertions(+), 121 deletions(-) delete mode 100644 Dalamud/Game/Text/SeStringHandling/SeStringManager.cs diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs b/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs deleted file mode 100644 index f0b38d429..000000000 --- a/Dalamud/Game/Text/SeStringHandling/SeStringManager.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; - -using Dalamud.IoC; -using Dalamud.IoC.Internal; -using Lumina.Excel.GeneratedSheets; - -namespace Dalamud.Game.Text.SeStringHandling; - -/// -/// This class facilitates creating new SeStrings and breaking down existing ones into their individual payload components. -/// -[PluginInterface] -[InterfaceVersion("1.0")] -[ServiceManager.BlockingEarlyLoadedService] -[Obsolete("This class is obsolete. Please use the static methods on SeString instead.")] -public sealed class SeStringManager : IServiceType -{ - [ServiceManager.ServiceConstructor] - private SeStringManager() - { - } - - /// - /// Parse a binary game message into an SeString. - /// - /// Pointer to the string's data in memory. - /// Length of the string's data in memory. - /// An SeString containing parsed Payload objects for each payload in the data. - [Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)] - public unsafe SeString Parse(byte* ptr, int len) => SeString.Parse(ptr, len); - - /// - /// Parse a binary game message into an SeString. - /// - /// Binary message payload data in SE's internal format. - /// An SeString containing parsed Payload objects for each payload in the data. - [Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)] - public unsafe SeString Parse(ReadOnlySpan data) => SeString.Parse(data); - - /// - /// Parse a binary game message into an SeString. - /// - /// Binary message payload data in SE's internal format. - /// An SeString containing parsed Payload objects for each payload in the data. - [Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)] - public SeString Parse(byte[] bytes) => SeString.Parse(new ReadOnlySpan(bytes)); - - /// - /// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log. - /// - /// The id of the item to link. - /// Whether to link the high-quality variant of the item. - /// An optional name override to display, instead of the actual item name. - /// An SeString containing all the payloads necessary to display an item link in the chat log. - [Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)] - public SeString CreateItemLink(uint itemId, bool isHQ, string displayNameOverride = null) => SeString.CreateItemLink(itemId, isHQ, displayNameOverride); - - /// - /// Creates an SeString representing an entire Payload chain that can be used to link an item in the chat log. - /// - /// The Lumina Item to link. - /// Whether to link the high-quality variant of the item. - /// An optional name override to display, instead of the actual item name. - /// An SeString containing all the payloads necessary to display an item link in the chat log. - [Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)] - public SeString CreateItemLink(Item item, bool isHQ, string displayNameOverride = null) => SeString.CreateItemLink(item, isHQ, displayNameOverride); - - /// - /// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log. - /// - /// The id of the TerritoryType for this map link. - /// The id of the Map for this map link. - /// The raw x-coordinate for this link. - /// The raw y-coordinate for this link.. - /// An SeString containing all of the payloads necessary to display a map link in the chat log. - [Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)] - public SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY) => - SeString.CreateMapLink(territoryId, mapId, rawX, rawY); - - /// - /// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log. - /// - /// The id of the TerritoryType for this map link. - /// The id of the Map for this map link. - /// The human-readable x-coordinate for this link. - /// The human-readable y-coordinate for this link. - /// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases. - /// An SeString containing all of the payloads necessary to display a map link in the chat log. - [Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)] - public SeString CreateMapLink(uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) => SeString.CreateMapLink(territoryId, mapId, xCoord, yCoord, fudgeFactor); - - /// - /// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log, matching a specified zone name. - /// - /// The name of the location for this link. This should be exactly the name as seen in a displayed map link in-game for the same zone. - /// The human-readable x-coordinate for this link. - /// The human-readable y-coordinate for this link. - /// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases. - /// An SeString containing all of the payloads necessary to display a map link in the chat log. - [Obsolete("This method is obsolete. Please use the static methods on SeString instead.", true)] - public SeString CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) => SeString.CreateMapLink(placeName, xCoord, yCoord, fudgeFactor); - - /// - /// Creates a list of Payloads necessary to display the arrow link marker icon in chat - /// with the appropriate glow and coloring. - /// - /// A list of all the payloads required to insert the link marker. - [Obsolete("This data is obsolete. Please use the static version on SeString instead.", true)] - public List TextArrowPayloads() => new(SeString.TextArrowPayloads); -} diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs index 5b640f64c..3ceecf6a6 100644 --- a/Dalamud/Memory/MemoryHelper.cs +++ b/Dalamud/Memory/MemoryHelper.cs @@ -167,7 +167,7 @@ public static unsafe class MemoryHelper /// Read a UTF-8 encoded string from a specified memory address. ///
/// - /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. /// The read in string. @@ -178,7 +178,7 @@ public static unsafe class MemoryHelper /// Read a string with the given encoding from a specified memory address. /// /// - /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. /// The encoding to use to decode the string. @@ -193,7 +193,7 @@ public static unsafe class MemoryHelper /// Read a UTF-8 encoded string from a specified memory address. /// /// - /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. /// The maximum length of the string. @@ -205,7 +205,7 @@ public static unsafe class MemoryHelper /// Read a string with the given encoding from a specified memory address. /// /// - /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. /// The encoding to use to decode the string. @@ -284,7 +284,7 @@ public static unsafe class MemoryHelper /// Read a UTF-8 encoded string from a specified memory address. /// /// - /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. /// The read in string. @@ -295,7 +295,7 @@ public static unsafe class MemoryHelper /// Read a string with the given encoding from a specified memory address. /// /// - /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. /// The encoding to use to decode the string. @@ -307,7 +307,7 @@ public static unsafe class MemoryHelper /// Read a UTF-8 encoded string from a specified memory address. /// /// - /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. /// The read in string. @@ -319,7 +319,7 @@ public static unsafe class MemoryHelper /// Read a string with the given encoding from a specified memory address. /// /// - /// Attention! If this is an SeString, use the to decode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. /// The encoding to use to decode the string. @@ -426,7 +426,7 @@ public static unsafe class MemoryHelper /// Write a UTF-8 encoded string to a specified memory address. /// /// - /// Attention! If this is an SeString, use the to encode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to write to. /// The string to write. @@ -437,7 +437,7 @@ public static unsafe class MemoryHelper /// Write a string with the given encoding to a specified memory address. /// /// - /// Attention! If this is an SeString, use the to encode or the applicable helper method. + /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to write to. /// The string to write. From 276ad3733f67cc4f886c8f78b3bb3d3207a4d391 Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sat, 27 May 2023 17:19:14 -0700 Subject: [PATCH 004/585] cleanup: remove obsolete Hook ctors --- Dalamud/Hooking/Hook.cs | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 74b9e6384..558b6bde1 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -26,32 +26,6 @@ public class Hook : IDisposable, IDalamudHook where T : Delegate private readonly Hook? compatHookImpl; - /// - /// Initializes a new instance of the class. - /// Hook is not activated until Enable() method is called. - /// - /// A memory address to install a hook. - /// Callback function. Delegate must have a same original function prototype. - [Obsolete("Use Hook.FromAddress instead.")] - public Hook(IntPtr address, T detour) - : this(address, detour, false, Assembly.GetCallingAssembly()) - { - } - - /// - /// Initializes a new instance of the class. - /// Hook is not activated until Enable() method is called. - /// Please do not use MinHook unless you have thoroughly troubleshot why Reloaded does not work. - /// - /// A memory address to install a hook. - /// Callback function. Delegate must have a same original function prototype. - /// Use the MinHook hooking library instead of Reloaded. - [Obsolete("Use Hook.FromAddress instead.")] - public Hook(IntPtr address, T detour, bool useMinHook) - : this(address, detour, useMinHook, Assembly.GetCallingAssembly()) - { - } - /// /// Initializes a new instance of the class. /// @@ -61,19 +35,6 @@ public class Hook : IDisposable, IDalamudHook where T : Delegate this.address = address; } - [Obsolete("Use Hook.FromAddress instead.")] - private Hook(IntPtr address, T detour, bool useMinHook, Assembly callingAssembly) - { - if (EnvironmentConfiguration.DalamudForceMinHook) - useMinHook = true; - - this.address = address = HookManager.FollowJmp(address); - if (useMinHook) - this.compatHookImpl = new MinHookHook(address, detour, callingAssembly); - else - this.compatHookImpl = new ReloadedHook(address, detour, callingAssembly); - } - /// /// Gets a memory address of the target function. /// From efec6eada2557974433d6471a450391f4c522fb7 Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sat, 27 May 2023 17:21:15 -0700 Subject: [PATCH 005/585] cleanup: remove obsolete method to reload all plugins --- Dalamud/Plugin/Internal/PluginManager.cs | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 0ff63f2c0..47f96dea4 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -580,23 +580,6 @@ internal partial class PluginManager : IDisposable, IServiceType tokenSource.Token); } - /// - /// Reload all loaded plugins. - /// - /// A task. - [Obsolete("This method should no longer be used and will be removed in a future release.")] - public Task ReloadAllPluginsAsync() - { - lock (this.pluginListLock) - { - return Task.WhenAll(this.InstalledPlugins - .Where(x => x.IsLoaded) - .ToList() - .Select(x => Task.Run(async () => await x.ReloadAsync())) - .ToList()); - } - } - /// /// Reload the PluginMaster for each repo, filter, and event that the list has updated. /// From 292e0c2df84a0cd1f6c2afe07b3a88853792d76f Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sat, 27 May 2023 17:25:05 -0700 Subject: [PATCH 006/585] cleanup: remove obsoleted properties in DalamudPluginInterface --- Dalamud/Plugin/DalamudPluginInterface.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 35b8bbbc7..76e5a65d5 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -205,18 +205,6 @@ public sealed class DalamudPluginInterface : IDisposable /// public XivChatType GeneralChatType { get; private set; } - /// - /// Gets a list of installed plugin names. - /// - [Obsolete($"This property is obsolete. Use {nameof(InstalledPlugins)} instead.")] - public List PluginNames => Service.Get().InstalledPlugins.Select(p => p.Manifest.Name).ToList(); - - /// - /// Gets a list of installed plugin internal names. - /// - [Obsolete($"This property is obsolete. Use {nameof(InstalledPlugins)} instead.")] - public List PluginInternalNames => Service.Get().InstalledPlugins.Select(p => p.Manifest.InternalName).ToList(); - /// /// Gets a list of installed plugins along with their current state. /// From 8bd0cb0c57e821e4cbb88612f5a74ad6731c9a39 Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Sat, 27 May 2023 17:37:41 -0700 Subject: [PATCH 007/585] cleanup: remove obsolete Util.HttpClient, Util.CopyTo() --- Dalamud/Utility/Util.cs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index ecf672caf..3d472a2c7 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -35,13 +35,6 @@ public static class Util private static ulong moduleStartAddr; private static ulong moduleEndAddr; - /// - /// Gets an httpclient for usage. - /// Do NOT await this. - /// - [Obsolete($"Use Service<{nameof(HappyHttpClient)}> instead.")] - public static HttpClient HttpClient { get; } = Service.Get().SharedHttpClient; - /// /// Gets the assembly version of Dalamud. /// @@ -493,21 +486,6 @@ public static class Util return Encoding.UTF8.GetString(mso.ToArray()); } - /// - /// Copy one stream to another. - /// - /// The source stream. - /// The destination stream. - /// The maximum length to copy. - [Obsolete("Use Stream.CopyTo() instead", true)] - public static void CopyTo(Stream src, Stream dest, int len = 4069) - { - var bytes = new byte[len]; - int cnt; - - while ((cnt = src.Read(bytes, 0, bytes.Length)) != 0) dest.Write(bytes, 0, cnt); - } - /// /// Heuristically determine if Dalamud is running on Linux/WINE. /// From 6bc7ebaff2d0d1d1cd2cd1486bc2bbf611066d73 Mon Sep 17 00:00:00 2001 From: kal <35899782+kalilistic@users.noreply.github.com> Date: Mon, 29 May 2023 15:59:44 -0400 Subject: [PATCH 008/585] feat: remove obsolete icons (#1147) --- .../Interface/FontAwesome/FontAwesomeIcon.cs | 3087 ----------------- 1 file changed, 3087 deletions(-) diff --git a/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs b/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs index 368ca55fe..3d21ea86c 100644 --- a/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs +++ b/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs @@ -19,28 +19,6 @@ public enum FontAwesomeIcon /// None = 0, - /// - /// The Font Awesome "500px" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "500px" })] - _500Px = 0xF26E, - - /// - /// The Font Awesome "accessible-icon" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "accessible icon", "accessibility", "handicap", "person", "wheelchair", "wheelchair-alt" })] - [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Medical + Health", "Transportation", "Users + People" })] - AccessibleIcon = 0xF368, - - /// - /// The Font Awesome "accusoft" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "accusoft" })] - Accusoft = 0xF369, - /// /// The Font Awesome "acquisitionsincorporated" icon unicode character. /// @@ -75,40 +53,12 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Design", "Editing", "Photos + Images", "Shapes" })] Adjust = 0xF042, - /// - /// The Font Awesome "adn" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "adn" })] - Adn = 0xF170, - /// /// The Font Awesome "adobe" icon unicode character. /// [Obsolete] Adobe = 0xF778, - /// - /// The Font Awesome "adversal" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "adversal" })] - Adversal = 0xF36A, - - /// - /// The Font Awesome "affiliatetheme" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "affiliatetheme" })] - Affiliatetheme = 0xF36B, - - /// - /// The Font Awesome "airbnb" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "airbnb" })] - Airbnb = 0xF834, - /// /// The Font Awesome "spray-can-sparkles" icon unicode character. /// @@ -116,13 +66,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Automotive" })] AirFreshener = 0xF5D0, - /// - /// The Font Awesome "algolia" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "algolia" })] - Algolia = 0xF36C, - /// /// The Font Awesome "align-center" icon unicode character. /// @@ -151,14 +94,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] AlignRight = 0xF038, - /// - /// The Font Awesome "alipay" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "alipay" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - Alipay = 0xF642, - /// /// The Font Awesome "hand-dots" icon unicode character. /// @@ -166,21 +101,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Hands", "Medical + Health" })] Allergies = 0xF461, - /// - /// The Font Awesome "amazon" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "amazon" })] - Amazon = 0xF270, - - /// - /// The Font Awesome "amazon-pay" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "amazon pay" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - AmazonPay = 0xF42C, - /// /// The Font Awesome "truck-medical" icon unicode character. /// @@ -195,13 +115,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Communication" })] AmericanSignLanguageInterpreting = 0xF2A3, - /// - /// The Font Awesome "amilia" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "amilia" })] - Amilia = 0xF36D, - /// /// The Font Awesome "anchor" icon unicode character. /// @@ -237,20 +150,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime" })] AnchorLock = 0xE4AD, - /// - /// The Font Awesome "android" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "android", "robot" })] - Android = 0xF17B, - - /// - /// The Font Awesome "angellist" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "angellist" })] - Angellist = 0xF209, - /// /// The Font Awesome "angles-down" icon unicode character. /// @@ -314,20 +213,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Emoji" })] Angry = 0xF556, - /// - /// The Font Awesome "angrycreative" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "angrycreative" })] - Angrycreative = 0xF36E, - - /// - /// The Font Awesome "angular" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "angular" })] - Angular = 0xF420, - /// /// The Font Awesome "ankh" icon unicode character. /// @@ -335,20 +220,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Religion" })] Ankh = 0xF644, - /// - /// The Font Awesome "apper" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "apper" })] - Apper = 0xF371, - - /// - /// The Font Awesome "apple" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "apple", "fruit", "ios", "mac", "operating system", "os", "osx" })] - Apple = 0xF179, - /// /// The Font Awesome "apple-whole" icon unicode character. /// @@ -356,28 +227,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Education", "Food + Beverage", "Fruits + Vegetables" })] AppleAlt = 0xF5D1, - /// - /// The Font Awesome "apple-pay" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "apple pay" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - ApplePay = 0xF415, - - /// - /// The Font Awesome "app-store" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "app store" })] - AppStore = 0xF36F, - - /// - /// The Font Awesome "app-store-ios" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "app store ios" })] - AppStoreIos = 0xF370, - /// /// The Font Awesome "box-archive" icon unicode character. /// @@ -728,13 +577,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Humanitarian" })] ArrowUpRightFromSquare = 0xF08E, - /// - /// The Font Awesome "artstation" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "artstation" })] - Artstation = 0xF77A, - /// /// The Font Awesome "ear-listen" icon unicode character. /// @@ -750,13 +592,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Punctuation + Symbols", "Spinners" })] Asterisk = 0xF069, - /// - /// The Font Awesome "asymmetrik" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "asymmetrik" })] - Asymmetrik = 0xF372, - /// /// The Font Awesome "at" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x40. @@ -772,13 +607,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Maps", "Travel + Hotel" })] Atlas = 0xF558, - /// - /// The Font Awesome "atlassian" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "atlassian" })] - Atlassian = 0xF77B, - /// /// The Font Awesome "atom" icon unicode character. /// @@ -786,13 +614,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Education", "Energy", "Religion", "Science", "Science Fiction", "Spinners" })] Atom = 0xF5D2, - /// - /// The Font Awesome "audible" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "audible" })] - Audible = 0xF373, - /// /// The Font Awesome "audio-description" icon unicode character. /// @@ -807,27 +628,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] AustralSign = 0xE0A9, - /// - /// The Font Awesome "autoprefixer" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "autoprefixer" })] - Autoprefixer = 0xF41C, - - /// - /// The Font Awesome "avianex" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "avianex" })] - Avianex = 0xF374, - - /// - /// The Font Awesome "aviato" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "aviato" })] - Aviato = 0xF421, - /// /// The Font Awesome "award" icon unicode character. /// @@ -835,13 +635,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Education", "Political" })] Award = 0xF559, - /// - /// The Font Awesome "aws" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "aws" })] - Aws = 0xF375, - /// /// The Font Awesome "baby" icon unicode character. /// @@ -940,13 +733,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Editing", "Medical + Health" })] BandAid = 0xF462, - /// - /// The Font Awesome "bandcamp" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "bandcamp" })] - Bandcamp = 0xF2D5, - /// /// The Font Awesome "bangladeshi-taka-sign" icon unicode character. /// @@ -1038,13 +824,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Energy" })] BatteryThreeQuarters = 0xF241, - /// - /// The Font Awesome "battle-net" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "battle net" })] - BattleNet = 0xF835, - /// /// The Font Awesome "bed" icon unicode character. /// @@ -1059,20 +838,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage", "Maps" })] Beer = 0xF0FC, - /// - /// The Font Awesome "behance" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "behance" })] - Behance = 0xF1B4, - - /// - /// The Font Awesome "square-behance" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square behance" })] - BehanceSquare = 0xF1B5, - /// /// The Font Awesome "bell" icon unicode character. /// @@ -1115,13 +880,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Sports + Fitness", "Users + People" })] Biking = 0xF84A, - /// - /// The Font Awesome "bimobject" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "bimobject" })] - Bimobject = 0xF378, - /// /// The Font Awesome "binoculars" icon unicode character. /// @@ -1143,21 +901,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Childhood", "Food + Beverage", "Maps", "Social" })] BirthdayCake = 0xF1FD, - /// - /// The Font Awesome "bitbucket" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "bitbucket", "atlassian", "bitbucket-square", "git" })] - Bitbucket = 0xF171, - - /// - /// The Font Awesome "bitcoin" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "bitcoin" })] - [FontAwesomeCategoriesAttribute(new[] { "Money", "Shopping" })] - Bitcoin = 0xF379, - /// /// The Font Awesome "bitcoin-sign" icon unicode character. /// @@ -1165,27 +908,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] BitcoinSign = 0xE0B4, - /// - /// The Font Awesome "bity" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "bity" })] - Bity = 0xF37A, - - /// - /// The Font Awesome "blackberry" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "blackberry" })] - Blackberry = 0xF37B, - - /// - /// The Font Awesome "black-tie" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "black tie" })] - BlackTie = 0xF27E, - /// /// The Font Awesome "blender" icon unicode character. /// @@ -1214,36 +936,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Writing" })] Blog = 0xF781, - /// - /// The Font Awesome "blogger" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "blogger" })] - Blogger = 0xF37C, - - /// - /// The Font Awesome "blogger-b" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "blogger b" })] - BloggerB = 0xF37D, - - /// - /// The Font Awesome "bluetooth" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "bluetooth", "signal" })] - [FontAwesomeCategoriesAttribute(new[] { "Connectivity" })] - Bluetooth = 0xF293, - - /// - /// The Font Awesome "bluetooth-b" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "bluetooth b" })] - [FontAwesomeCategoriesAttribute(new[] { "Communication" })] - BluetoothB = 0xF294, - /// /// The Font Awesome "bold" icon unicode character. /// @@ -1342,13 +1034,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Religion" })] BookTanakh = 0xF827, - /// - /// The Font Awesome "bootstrap" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "bootstrap" })] - Bootstrap = 0xF836, - /// /// The Font Awesome "border-all" icon unicode character. /// @@ -1552,14 +1237,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Construction", "Design", "Editing" })] Brush = 0xF55D, - /// - /// The Font Awesome "btc" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "btc" })] - [FontAwesomeCategoriesAttribute(new[] { "Money", "Shopping" })] - Btc = 0xF15A, - /// /// The Font Awesome "bucket" icon unicode character. /// @@ -1567,13 +1244,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Camping", "Childhood", "Construction", "Humanitarian" })] Bucket = 0xE4CF, - /// - /// The Font Awesome "buffer" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "buffer" })] - Buffer = 0xF837, - /// /// The Font Awesome "bug" icon unicode character. /// @@ -1700,13 +1370,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Energy", "Humanitarian", "Medical + Health", "Science", "Sports + Fitness" })] Burn = 0xF46A, - /// - /// The Font Awesome "buromobelexperte" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "buromobelexperte" })] - Buromobelexperte = 0xF37F, - /// /// The Font Awesome "burst" icon unicode character. /// @@ -1735,20 +1398,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business" })] BusinessTime = 0xF64A, - /// - /// The Font Awesome "buy-n-large" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "buy n large" })] - BuyNLarge = 0xF8A6, - - /// - /// The Font Awesome "buysellads" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "buysellads" })] - Buysellads = 0xF20D, - /// /// The Font Awesome "calculator" icon unicode character. /// @@ -1840,13 +1489,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Camping" })] Campground = 0xF6BB, - /// - /// The Font Awesome "canadian-maple-leaf" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "canadian maple leaf", "canada", "flag", "flora", "nature", "plant" })] - CanadianMapleLeaf = 0xF785, - /// /// The Font Awesome "candy-cane" icon unicode character. /// @@ -2015,86 +1657,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Animals", "Halloween" })] Cat = 0xF6BE, - /// - /// The Font Awesome "cc-amazon-pay" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc amazon pay" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcAmazonPay = 0xF42D, - - /// - /// The Font Awesome "cc-amex" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc amex", "amex" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcAmex = 0xF1F3, - - /// - /// The Font Awesome "cc-apple-pay" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc apple pay" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcApplePay = 0xF416, - - /// - /// The Font Awesome "cc-diners-club" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc diners club" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcDinersClub = 0xF24C, - - /// - /// The Font Awesome "cc-discover" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc discover" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcDiscover = 0xF1F2, - - /// - /// The Font Awesome "cc-jcb" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc jcb" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcJcb = 0xF24B, - - /// - /// The Font Awesome "cc-mastercard" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc mastercard" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcMastercard = 0xF1F1, - - /// - /// The Font Awesome "cc-paypal" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc paypal" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcPaypal = 0xF1F4, - - /// - /// The Font Awesome "cc-stripe" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc stripe" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcStripe = 0xF1F5, - - /// - /// The Font Awesome "cc-visa" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cc visa" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - CcVisa = 0xF1F0, - /// /// The Font Awesome "cedi-sign" icon unicode character. /// @@ -2102,20 +1664,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] CediSign = 0xE0DF, - /// - /// The Font Awesome "centercode" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "centercode" })] - Centercode = 0xF380, - - /// - /// The Font Awesome "centos" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "centos", "linux", "operating system", "os" })] - Centos = 0xF789, - /// /// The Font Awesome "cent-sign" icon unicode character. /// @@ -2389,20 +1937,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Humanitarian", "Users + People" })] Children = 0xE4E1, - /// - /// The Font Awesome "chrome" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "chrome", "browser" })] - Chrome = 0xF268, - - /// - /// The Font Awesome "chromecast" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "chromecast" })] - Chromecast = 0xF838, - /// /// The Font Awesome "church" icon unicode character. /// @@ -2558,13 +2092,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Weather" })] CloudRain = 0xF73D, - /// - /// The Font Awesome "cloudscale" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cloudscale" })] - Cloudscale = 0xF383, - /// /// The Font Awesome "cloud-showers-heavy" icon unicode character. /// @@ -2579,13 +2106,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Weather" })] CloudShowersWater = 0xE4E4, - /// - /// The Font Awesome "cloudsmith" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cloudsmith" })] - Cloudsmith = 0xF384, - /// /// The Font Awesome "cloud-sun" icon unicode character. /// @@ -2608,13 +2128,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Connectivity" })] CloudUploadAlt = 0xF382, - /// - /// The Font Awesome "cloudversify" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cloudversify" })] - Cloudversify = 0xF385, - /// /// The Font Awesome "clover" icon unicode character. /// @@ -2671,13 +2184,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Coding" })] CodeMerge = 0xF387, - /// - /// The Font Awesome "codepen" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "codepen" })] - Codepen = 0xF1CB, - /// /// The Font Awesome "code-pull-request" icon unicode character. /// @@ -2685,13 +2191,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Coding" })] CodePullRequest = 0xE13C, - /// - /// The Font Awesome "codiepie" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "codiepie" })] - Codiepie = 0xF284, - /// /// The Font Awesome "mug-saucer" icon unicode character. /// @@ -2839,27 +2338,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel" })] ConciergeBell = 0xF562, - /// - /// The Font Awesome "confluence" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "confluence", "atlassian" })] - Confluence = 0xF78D, - - /// - /// The Font Awesome "connectdevelop" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "connectdevelop" })] - Connectdevelop = 0xF20E, - - /// - /// The Font Awesome "contao" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "contao" })] - Contao = 0xF26D, - /// /// The Font Awesome "cookie" icon unicode character. /// @@ -2888,13 +2366,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business" })] Copyright = 0xF1F9, - /// - /// The Font Awesome "cotton-bureau" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cotton bureau", "clothing", "t-shirts", "tshirts" })] - CottonBureau = 0xF89E, - /// /// The Font Awesome "couch" icon unicode character. /// @@ -2909,111 +2380,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Animals", "Humanitarian" })] Cow = 0xF6C8, - /// - /// The Font Awesome "cpanel" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cpanel" })] - Cpanel = 0xF388, - - /// - /// The Font Awesome "creative-commons" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons" })] - CreativeCommons = 0xF25E, - - /// - /// The Font Awesome "creative-commons-by" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons by" })] - CreativeCommonsBy = 0xF4E7, - - /// - /// The Font Awesome "creative-commons-nc" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons nc" })] - CreativeCommonsNc = 0xF4E8, - - /// - /// The Font Awesome "creative-commons-nc-eu" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons nc eu" })] - CreativeCommonsNcEu = 0xF4E9, - - /// - /// The Font Awesome "creative-commons-nc-jp" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons nc jp" })] - CreativeCommonsNcJp = 0xF4EA, - - /// - /// The Font Awesome "creative-commons-nd" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons nd" })] - CreativeCommonsNd = 0xF4EB, - - /// - /// The Font Awesome "creative-commons-pd" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons pd" })] - CreativeCommonsPd = 0xF4EC, - - /// - /// The Font Awesome "creative-commons-pd-alt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons pd alt" })] - CreativeCommonsPdAlt = 0xF4ED, - - /// - /// The Font Awesome "creative-commons-remix" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons remix" })] - CreativeCommonsRemix = 0xF4EE, - - /// - /// The Font Awesome "creative-commons-sa" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons sa" })] - CreativeCommonsSa = 0xF4EF, - - /// - /// The Font Awesome "creative-commons-sampling" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons sampling" })] - CreativeCommonsSampling = 0xF4F0, - - /// - /// The Font Awesome "creative-commons-sampling-plus" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons sampling plus" })] - CreativeCommonsSamplingPlus = 0xF4F1, - - /// - /// The Font Awesome "creative-commons-share" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons share" })] - CreativeCommonsShare = 0xF4F2, - - /// - /// The Font Awesome "creative-commons-zero" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "creative commons zero" })] - CreativeCommonsZero = 0xF4F3, - /// /// The Font Awesome "credit-card" icon unicode character. /// @@ -3021,14 +2387,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money", "Shopping" })] CreditCard = 0xF09D, - /// - /// The Font Awesome "critical-role" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "critical role", "dungeons & dragons", "d&d", "dnd", "fantasy", "game", "gaming", "tabletop" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - CriticalRole = 0xF6C9, - /// /// The Font Awesome "crop" icon unicode character. /// @@ -3085,20 +2443,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] CruzeiroSign = 0xE152, - /// - /// The Font Awesome "css3" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "css3", "code" })] - Css3 = 0xF13C, - - /// - /// The Font Awesome "css3-alt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "css3 alt" })] - Css3Alt = 0xF38B, - /// /// The Font Awesome "cube" icon unicode character. /// @@ -3127,43 +2471,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing", "Files" })] Cut = 0xF0C4, - /// - /// The Font Awesome "cuttlefish" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "cuttlefish" })] - Cuttlefish = 0xF38C, - - /// - /// The Font Awesome "dailymotion" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "dailymotion" })] - Dailymotion = 0xF952, - - /// - /// The Font Awesome "d-and-d" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "d and d" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - DAndD = 0xF38D, - - /// - /// The Font Awesome "d-and-d-beyond" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "d and d beyond", "dungeons & dragons", "d&d", "dnd", "fantasy", "gaming", "tabletop" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - DAndDBeyond = 0xF6CA, - - /// - /// The Font Awesome "dashcube" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "dashcube" })] - Dashcube = 0xF210, - /// /// The Font Awesome "database" icon unicode character. /// @@ -3178,13 +2485,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Accessibility" })] Deaf = 0xF2A4, - /// - /// The Font Awesome "delicious" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "delicious" })] - Delicious = 0xF1A5, - /// /// The Font Awesome "democrat" icon unicode character. /// @@ -3192,20 +2492,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Political" })] Democrat = 0xF747, - /// - /// The Font Awesome "deploydog" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "deploydog" })] - Deploydog = 0xF38E, - - /// - /// The Font Awesome "deskpro" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "deskpro" })] - Deskpro = 0xF38F, - /// /// The Font Awesome "desktop" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF390. @@ -3214,20 +2500,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] Desktop = 0xF108, - /// - /// The Font Awesome "dev" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "dev" })] - Dev = 0xF6CC, - - /// - /// The Font Awesome "deviantart" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "deviantart" })] - Deviantart = 0xF1BD, - /// /// The Font Awesome "dharmachakra" icon unicode character. /// @@ -3235,13 +2507,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Religion", "Spinners" })] Dharmachakra = 0xF655, - /// - /// The Font Awesome "dhl" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "dhl", "dalsey", "hillblom and lynn", "german", "package", "shipping" })] - Dhl = 0xF790, - /// /// The Font Awesome "person-dots-from-line" icon unicode character. /// @@ -3277,13 +2542,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Gaming", "Shapes" })] Diamond = 0xF219, - /// - /// The Font Awesome "diaspora" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "diaspora" })] - Diaspora = 0xF791, - /// /// The Font Awesome "dice" icon unicode character. /// @@ -3347,20 +2605,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] DiceTwo = 0xF528, - /// - /// The Font Awesome "digg" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "digg" })] - Digg = 0xF1A6, - - /// - /// The Font Awesome "digital-ocean" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "digital ocean" })] - DigitalOcean = 0xF391, - /// /// The Font Awesome "tachograph-digital" icon unicode character. /// @@ -3375,20 +2619,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Maps" })] Directions = 0xF5EB, - /// - /// The Font Awesome "discord" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "discord" })] - Discord = 0xF392, - - /// - /// The Font Awesome "discourse" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "discourse" })] - Discourse = 0xF393, - /// /// The Font Awesome "disease" icon unicode character. /// @@ -3424,20 +2654,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Science" })] Dna = 0xF471, - /// - /// The Font Awesome "dochub" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "dochub" })] - Dochub = 0xF394, - - /// - /// The Font Awesome "docker" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "docker" })] - Docker = 0xF395, - /// /// The Font Awesome "dog" icon unicode character. /// @@ -3516,13 +2732,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Devices + Hardware" })] Download = 0xF019, - /// - /// The Font Awesome "draft2digital" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "draft2digital" })] - Draft2digital = 0xF396, - /// /// The Font Awesome "compass-drafting" icon unicode character. /// @@ -3544,27 +2753,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Design", "Maps" })] DrawPolygon = 0xF5EE, - /// - /// The Font Awesome "dribbble" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "dribbble" })] - Dribbble = 0xF17D, - - /// - /// The Font Awesome "square-dribbble" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square dribbble" })] - DribbbleSquare = 0xF397, - - /// - /// The Font Awesome "dropbox" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "dropbox" })] - Dropbox = 0xF16B, - /// /// The Font Awesome "drum" icon unicode character. /// @@ -3586,13 +2774,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage" })] DrumstickBite = 0xF6D7, - /// - /// The Font Awesome "drupal" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "drupal" })] - Drupal = 0xF1A9, - /// /// The Font Awesome "dumbbell" icon unicode character. /// @@ -3621,20 +2802,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Gaming", "Household", "Security" })] Dungeon = 0xF6D9, - /// - /// The Font Awesome "dyalog" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "dyalog" })] - Dyalog = 0xF399, - - /// - /// The Font Awesome "earlybirds" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "earlybirds" })] - Earlybirds = 0xF39A, - /// /// The Font Awesome "earth-oceania" icon unicode character. /// @@ -3642,20 +2809,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel" })] EarthOceania = 0xE47B, - /// - /// The Font Awesome "ebay" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ebay" })] - Ebay = 0xF4F4, - - /// - /// The Font Awesome "edge" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "edge", "browser", "ie" })] - Edge = 0xF282, - /// /// The Font Awesome "pen-to-square" icon unicode character. /// @@ -3677,13 +2830,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Media Playback" })] Eject = 0xF052, - /// - /// The Font Awesome "elementor" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "elementor" })] - Elementor = 0xF430, - /// /// The Font Awesome "elevator" icon unicode character. /// @@ -3705,27 +2851,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Editing" })] EllipsisV = 0xF142, - /// - /// The Font Awesome "ello" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ello" })] - Ello = 0xF5F1, - - /// - /// The Font Awesome "ember" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ember" })] - Ember = 0xF423, - - /// - /// The Font Awesome "empire" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "empire" })] - Empire = 0xF1D1, - /// /// The Font Awesome "envelope" icon unicode character. /// @@ -3761,13 +2886,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication" })] EnvelopeSquare = 0xF199, - /// - /// The Font Awesome "envira" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "envira", "leaf" })] - Envira = 0xF299, - /// /// The Font Awesome "equals" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x3D. @@ -3783,21 +2901,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Writing" })] Eraser = 0xF12D, - /// - /// The Font Awesome "erlang" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "erlang" })] - Erlang = 0xF39D, - - /// - /// The Font Awesome "ethereum" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ethereum" })] - [FontAwesomeCategoriesAttribute(new[] { "Money", "Shopping" })] - Ethereum = 0xF42E, - /// /// The Font Awesome "ethernet" icon unicode character. /// @@ -3805,13 +2908,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Connectivity", "Devices + Hardware" })] Ethernet = 0xF796, - /// - /// The Font Awesome "etsy" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "etsy" })] - Etsy = 0xF2D7, - /// /// The Font Awesome "euro-sign" icon unicode character. /// @@ -3819,13 +2915,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] EuroSign = 0xF153, - /// - /// The Font Awesome "evernote" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "evernote" })] - Evernote = 0xF839, - /// /// The Font Awesome "right-left" icon unicode character. /// @@ -3876,13 +2965,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] ExpandArrowsAlt = 0xF31E, - /// - /// The Font Awesome "expeditedssl" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "expeditedssl" })] - Expeditedssl = 0xF23E, - /// /// The Font Awesome "explosion" icon unicode character. /// @@ -3925,34 +3007,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Design", "Editing", "Maps", "Photos + Images", "Security" })] EyeSlash = 0xF070, - /// - /// The Font Awesome "facebook" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "facebook", "facebook-official", "social network" })] - Facebook = 0xF09A, - - /// - /// The Font Awesome "facebook-f" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "facebook f", "facebook" })] - FacebookF = 0xF39E, - - /// - /// The Font Awesome "facebook-messenger" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "facebook messenger" })] - FacebookMessenger = 0xF39F, - - /// - /// The Font Awesome "square-facebook" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square facebook", "social network" })] - FacebookSquare = 0xF082, - /// /// The Font Awesome "fan" icon unicode character. /// @@ -3960,14 +3014,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Energy", "Household", "Spinners" })] Fan = 0xF863, - /// - /// The Font Awesome "fantasy-flight-games" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "fantasy flight games", "dungeons & dragons", "d&d", "dnd", "fantasy", "game", "gaming", "tabletop" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - FantasyFlightGames = 0xF6DC, - /// /// The Font Awesome "backward-fast" icon unicode character. /// @@ -4017,20 +3063,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Animals", "Nature" })] FeatherAlt = 0xF56B, - /// - /// The Font Awesome "fedex" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "fedex", "federal express", "package", "shipping" })] - Fedex = 0xF797, - - /// - /// The Font Awesome "fedora" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "fedora", "linux", "operating system", "os" })] - Fedora = 0xF798, - /// /// The Font Awesome "person-dress" icon unicode character. /// @@ -4052,13 +3084,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Maps", "Transportation" })] FighterJet = 0xF0FB, - /// - /// The Font Awesome "figma" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "figma", "app", "design", "interface" })] - Figma = 0xF799, - /// /// The Font Awesome "file" icon unicode character. /// @@ -4346,20 +3371,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Coding", "Maps" })] FireExtinguisher = 0xF134, - /// - /// The Font Awesome "firefox" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "firefox", "browser" })] - Firefox = 0xF269, - - /// - /// The Font Awesome "firefox-browser" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "firefox browser", "browser" })] - FirefoxBrowser = 0xF907, - /// /// The Font Awesome "kit-medical" icon unicode character. /// @@ -4367,27 +3378,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Camping", "Medical + Health" })] FirstAid = 0xF479, - /// - /// The Font Awesome "firstdraft" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "firstdraft" })] - Firstdraft = 0xF3A1, - - /// - /// The Font Awesome "first-order" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "first order" })] - FirstOrder = 0xF2B0, - - /// - /// The Font Awesome "first-order-alt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "first order alt" })] - FirstOrderAlt = 0xF50A, - /// /// The Font Awesome "fish" icon unicode character. /// @@ -4444,20 +3434,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Science" })] FlaskVial = 0xE4F3, - /// - /// The Font Awesome "flickr" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "flickr" })] - Flickr = 0xF16E, - - /// - /// The Font Awesome "flipboard" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "flipboard" })] - Flipboard = 0xF44D, - /// /// The Font Awesome "florin-sign" icon unicode character. /// @@ -4472,13 +3448,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Emoji" })] Flushed = 0xF579, - /// - /// The Font Awesome "fly" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "fly" })] - Fly = 0xF417, - /// /// The Font Awesome "folder" icon unicode character. /// @@ -4535,13 +3504,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Coding", "Design" })] FontAwesome = 0xF2B4, - /// - /// The Font Awesome "square-font-awesome-stroke" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square font awesome stroke" })] - FontAwesomeAlt = 0xF35C, - /// /// The Font Awesome "font-awesome" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF2B4. @@ -4558,20 +3520,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Coding", "Design" })] FontAwesomeLogoFull = 0xF4E6, - /// - /// The Font Awesome "fonticons" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "fonticons" })] - Fonticons = 0xF280, - - /// - /// The Font Awesome "fonticons-fi" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "fonticons fi" })] - FonticonsFi = 0xF3A2, - /// /// The Font Awesome "football" icon unicode character. /// @@ -4579,27 +3527,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness" })] FootballBall = 0xF44E, - /// - /// The Font Awesome "fort-awesome" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "fort awesome", "castle" })] - FortAwesome = 0xF286, - - /// - /// The Font Awesome "fort-awesome-alt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "fort awesome alt", "castle" })] - FortAwesomeAlt = 0xF3A3, - - /// - /// The Font Awesome "forumbee" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "forumbee" })] - Forumbee = 0xF211, - /// /// The Font Awesome "forward" icon unicode character. /// @@ -4607,13 +3534,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Media Playback" })] Forward = 0xF04E, - /// - /// The Font Awesome "foursquare" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "foursquare" })] - Foursquare = 0xF180, - /// /// The Font Awesome "franc-sign" icon unicode character. /// @@ -4621,20 +3541,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] FrancSign = 0xE18F, - /// - /// The Font Awesome "freebsd" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "freebsd" })] - Freebsd = 0xF3A4, - - /// - /// The Font Awesome "free-code-camp" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "free code camp" })] - FreeCodeCamp = 0xF2C5, - /// /// The Font Awesome "frog" icon unicode character. /// @@ -4656,13 +3562,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Emoji" })] FrownOpen = 0xF57A, - /// - /// The Font Awesome "fulcrum" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "fulcrum" })] - Fulcrum = 0xF50B, - /// /// The Font Awesome "filter-circle-dollar" icon unicode character. /// @@ -4677,22 +3576,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness" })] Futbol = 0xF1E3, - /// - /// The Font Awesome "galactic-republic" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "galactic republic", "politics", "star wars" })] - [FontAwesomeCategoriesAttribute(new[] { "Science Fiction" })] - GalacticRepublic = 0xF50C, - - /// - /// The Font Awesome "galactic-senate" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "galactic senate", "star wars" })] - [FontAwesomeCategoriesAttribute(new[] { "Science Fiction" })] - GalacticSenate = 0xF50D, - /// /// The Font Awesome "gamepad" icon unicode character. /// @@ -4749,29 +3632,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Genders" })] Genderless = 0xF22D, - /// - /// The Font Awesome "get-pocket" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "get pocket" })] - GetPocket = 0xF265, - - /// - /// The Font Awesome "gg" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "gg" })] - [FontAwesomeCategoriesAttribute(new[] { "Money" })] - Gg = 0xF260, - - /// - /// The Font Awesome "gg-circle" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "gg circle" })] - [FontAwesomeCategoriesAttribute(new[] { "Money" })] - GgCircle = 0xF261, - /// /// The Font Awesome "ghost" icon unicode character. /// @@ -4793,69 +3653,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Holidays", "Shopping" })] Gifts = 0xF79C, - /// - /// The Font Awesome "git" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "git" })] - Git = 0xF1D3, - - /// - /// The Font Awesome "git-alt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "git alt" })] - GitAlt = 0xF841, - - /// - /// The Font Awesome "github" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "github", "octocat" })] - Github = 0xF09B, - - /// - /// The Font Awesome "github-alt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "github alt", "octocat" })] - GithubAlt = 0xF113, - - /// - /// The Font Awesome "square-github" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square github", "octocat" })] - GithubSquare = 0xF092, - - /// - /// The Font Awesome "gitkraken" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "gitkraken" })] - Gitkraken = 0xF3A6, - - /// - /// The Font Awesome "gitlab" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "gitlab", "axosoft" })] - Gitlab = 0xF296, - - /// - /// The Font Awesome "square-git" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square git" })] - GitSquare = 0xF1D2, - - /// - /// The Font Awesome "gitter" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "gitter" })] - Gitter = 0xF426, - /// /// The Font Awesome "champagne-glasses" icon unicode character. /// @@ -4905,20 +3702,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage" })] GlassWhiskey = 0xF7A0, - /// - /// The Font Awesome "glide" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "glide" })] - Glide = 0xF2A5, - - /// - /// The Font Awesome "glide-g" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "glide g" })] - GlideG = 0xF2A6, - /// /// The Font Awesome "globe" icon unicode character. /// @@ -4954,13 +3737,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel" })] GlobeEurope = 0xF7A2, - /// - /// The Font Awesome "gofore" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "gofore" })] - Gofore = 0xF3A7, - /// /// The Font Awesome "golf-ball-tee" icon unicode character. /// @@ -4968,70 +3744,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness" })] GolfBall = 0xF450, - /// - /// The Font Awesome "goodreads" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "goodreads" })] - Goodreads = 0xF3A8, - - /// - /// The Font Awesome "goodreads-g" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "goodreads g" })] - GoodreadsG = 0xF3A9, - - /// - /// The Font Awesome "google" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "google" })] - Google = 0xF1A0, - - /// - /// The Font Awesome "google-drive" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "google drive" })] - GoogleDrive = 0xF3AA, - - /// - /// The Font Awesome "google-play" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "google play" })] - GooglePlay = 0xF3AB, - - /// - /// The Font Awesome "google-plus" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "google plus", "google-plus-circle", "google-plus-official" })] - GooglePlus = 0xF2B3, - - /// - /// The Font Awesome "google-plus-g" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "google plus g", "google-plus", "social network" })] - GooglePlusG = 0xF0D5, - - /// - /// The Font Awesome "square-google-plus" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square google plus", "social network" })] - GooglePlusSquare = 0xF0D4, - - /// - /// The Font Awesome "google-wallet" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "google wallet" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - GoogleWallet = 0xF1EE, - /// /// The Font Awesome "gopuram" icon unicode character. /// @@ -5046,20 +3758,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Clothing + Fashion", "Education", "Maps" })] GraduationCap = 0xF19D, - /// - /// The Font Awesome "gratipay" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "gratipay", "favorite", "heart", "like", "love" })] - Gratipay = 0xF184, - - /// - /// The Font Awesome "grav" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "grav" })] - Grav = 0xF2D6, - /// /// The Font Awesome "greater-than" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x3E. @@ -5173,13 +3871,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Emoji" })] GrinWink = 0xF58C, - /// - /// The Font Awesome "gripfire" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "gripfire" })] - Gripfire = 0xF3AC, - /// /// The Font Awesome "grip" icon unicode character. /// @@ -5215,13 +3906,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Marketing" })] GroupArrowsRotate = 0xE4F6, - /// - /// The Font Awesome "grunt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "grunt" })] - Grunt = 0xF3AD, - /// /// The Font Awesome "guarani-sign" icon unicode character. /// @@ -5236,13 +3920,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Music + Audio" })] Guitar = 0xF7A6, - /// - /// The Font Awesome "gulp" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "gulp" })] - Gulp = 0xF3AE, - /// /// The Font Awesome "gun" icon unicode character. /// @@ -5250,27 +3927,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Security" })] Gun = 0xE19B, - /// - /// The Font Awesome "hacker-news" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "hacker news" })] - HackerNews = 0xF1D4, - - /// - /// The Font Awesome "square-hacker-news" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square hacker news" })] - HackerNewsSquare = 0xF3AF, - - /// - /// The Font Awesome "hackerrank" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "hackerrank" })] - Hackerrank = 0xF5F7, - /// /// The Font Awesome "burger" icon unicode character. /// @@ -5734,20 +4390,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Animals" })] Hippo = 0xF6ED, - /// - /// The Font Awesome "hips" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "hips" })] - Hips = 0xF452, - - /// - /// The Font Awesome "hire-a-helper" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "hire a helper" })] - HireAHelper = 0xF3B0, - /// /// The Font Awesome "clock-rotate-left" icon unicode character. /// @@ -5776,20 +4418,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Maps" })] Home = 0xF015, - /// - /// The Font Awesome "hooli" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "hooli" })] - Hooli = 0xF427, - - /// - /// The Font Awesome "hornbill" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "hornbill" })] - Hornbill = 0xF592, - /// /// The Font Awesome "horse" icon unicode character. /// @@ -5847,13 +4475,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Travel + Hotel" })] Hotel = 0xF594, - /// - /// The Font Awesome "hotjar" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "hotjar" })] - Hotjar = 0xF3B1, - /// /// The Font Awesome "hot-tub-person" icon unicode character. /// @@ -6043,13 +4664,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Household", "Users + People" })] HouseUser = 0xE1B0, - /// - /// The Font Awesome "houzz" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "houzz" })] - Houzz = 0xF27C, - /// /// The Font Awesome "hryvnia-sign" icon unicode character. /// @@ -6064,20 +4678,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Alphabet", "Maps", "Medical + Health" })] HSquare = 0xF0FD, - /// - /// The Font Awesome "html5" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "html5" })] - Html5 = 0xF13B, - - /// - /// The Font Awesome "hubspot" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "hubspot" })] - Hubspot = 0xF3B2, - /// /// The Font Awesome "hurricane" icon unicode character. /// @@ -6134,13 +4734,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health", "Security", "Users + People" })] IdCardAlt = 0xF47F, - /// - /// The Font Awesome "ideal" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ideal" })] - Ideal = 0xF913, - /// /// The Font Awesome "igloo" icon unicode character. /// @@ -6162,13 +4755,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Maps", "Photos + Images", "Social" })] Images = 0xF302, - /// - /// The Font Awesome "imdb" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "imdb" })] - Imdb = 0xF2D8, - /// /// The Font Awesome "inbox" icon unicode character. /// @@ -6218,48 +4804,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Maps" })] InfoCircle = 0xF05A, - /// - /// The Font Awesome "instagram" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "instagram" })] - Instagram = 0xF16D, - - /// - /// The Font Awesome "square-instagram" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square instagram" })] - InstagramSquare = 0xF955, - - /// - /// The Font Awesome "intercom" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "intercom", "app", "customer", "messenger" })] - Intercom = 0xF7AF, - - /// - /// The Font Awesome "internet-explorer" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "internet explorer", "browser", "ie" })] - InternetExplorer = 0xF26B, - - /// - /// The Font Awesome "invision" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "invision", "app", "design", "interface" })] - Invision = 0xF7B0, - - /// - /// The Font Awesome "ioxhost" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ioxhost" })] - Ioxhost = 0xF208, - /// /// The Font Awesome "italic" icon unicode character. /// @@ -6267,27 +4811,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] Italic = 0xF033, - /// - /// The Font Awesome "itch-io" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "itch io" })] - ItchIo = 0xF83A, - - /// - /// The Font Awesome "itunes" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "itunes" })] - Itunes = 0xF3B4, - - /// - /// The Font Awesome "itunes-note" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "itunes note" })] - ItunesNote = 0xF3B5, - /// /// The Font Awesome "jar" icon unicode character. /// @@ -6302,13 +4825,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage", "Household", "Humanitarian" })] JarWheat = 0xE517, - /// - /// The Font Awesome "java" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "java" })] - Java = 0xF4E4, - /// /// The Font Awesome "jedi" icon unicode character. /// @@ -6316,21 +4832,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Religion", "Science Fiction" })] Jedi = 0xF669, - /// - /// The Font Awesome "jedi-order" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "jedi order", "star wars" })] - [FontAwesomeCategoriesAttribute(new[] { "Science Fiction" })] - JediOrder = 0xF50E, - - /// - /// The Font Awesome "jenkins" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "jenkins" })] - Jenkins = 0xF3B6, - /// /// The Font Awesome "jet-fighter-up" icon unicode character. /// @@ -6338,20 +4839,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Transportation" })] JetFighterUp = 0xE518, - /// - /// The Font Awesome "jira" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "jira", "atlassian" })] - Jira = 0xF7B1, - - /// - /// The Font Awesome "joget" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "joget" })] - Joget = 0xF3B7, - /// /// The Font Awesome "joint" icon unicode character. /// @@ -6359,13 +4846,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] Joint = 0xF595, - /// - /// The Font Awesome "joomla" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "joomla" })] - Joomla = 0xF1AA, - /// /// The Font Awesome "book-journal-whills" icon unicode character. /// @@ -6373,27 +4853,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Religion", "Science Fiction" })] JournalWhills = 0xF66A, - /// - /// The Font Awesome "js" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "js" })] - Js = 0xF3B8, - - /// - /// The Font Awesome "jsfiddle" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "jsfiddle" })] - Jsfiddle = 0xF1CC, - - /// - /// The Font Awesome "square-js" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square js" })] - JsSquare = 0xF3B9, - /// /// The Font Awesome "jug-detergent" icon unicode character. /// @@ -6408,13 +4867,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Religion" })] Kaaba = 0xF66B, - /// - /// The Font Awesome "kaggle" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "kaggle" })] - Kaggle = 0xF5FA, - /// /// The Font Awesome "key" icon unicode character. /// @@ -6422,13 +4874,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Maps", "Security", "Shopping", "Travel + Hotel" })] Key = 0xF084, - /// - /// The Font Awesome "keybase" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "keybase" })] - Keybase = 0xF4F5, - /// /// The Font Awesome "keyboard" icon unicode character. /// @@ -6436,13 +4881,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Coding", "Devices + Hardware", "Writing" })] Keyboard = 0xF11C, - /// - /// The Font Awesome "keycdn" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "keycdn" })] - Keycdn = 0xF3BA, - /// /// The Font Awesome "khanda" icon unicode character. /// @@ -6450,20 +4888,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Religion" })] Khanda = 0xF66D, - /// - /// The Font Awesome "kickstarter" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "kickstarter" })] - Kickstarter = 0xF3BB, - - /// - /// The Font Awesome "kickstarter-k" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "kickstarter k" })] - KickstarterK = 0xF3BC, - /// /// The Font Awesome "kip-sign" icon unicode character. /// @@ -6506,13 +4930,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Animals" })] KiwiBird = 0xF535, - /// - /// The Font Awesome "korvue" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "korvue" })] - Korvue = 0xF42F, - /// /// The Font Awesome "landmark" icon unicode character. /// @@ -6576,13 +4993,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] LaptopMedical = 0xF812, - /// - /// The Font Awesome "laravel" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "laravel" })] - Laravel = 0xF3BD, - /// /// The Font Awesome "lari-sign" icon unicode character. /// @@ -6590,20 +5000,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] LariSign = 0xE1C8, - /// - /// The Font Awesome "lastfm" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "lastfm" })] - Lastfm = 0xF202, - - /// - /// The Font Awesome "square-lastfm" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square lastfm" })] - LastfmSquare = 0xF203, - /// /// The Font Awesome "face-laugh" icon unicode character. /// @@ -6646,13 +5042,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Charity", "Energy", "Fruits + Vegetables", "Maps", "Nature" })] Leaf = 0xF06C, - /// - /// The Font Awesome "leanpub" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "leanpub" })] - Leanpub = 0xF212, - /// /// The Font Awesome "lemon" icon unicode character. /// @@ -6660,13 +5049,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage", "Fruits + Vegetables", "Maps" })] Lemon = 0xF094, - /// - /// The Font Awesome "less" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "less" })] - Less = 0xF41D, - /// /// The Font Awesome "less-than" icon unicode character. /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0x3C. @@ -6710,13 +5092,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Energy", "Household", "Maps", "Marketing" })] Lightbulb = 0xF0EB, - /// - /// The Font Awesome "line" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "line" })] - Line = 0xF3C0, - /// /// The Font Awesome "lines-leaning" icon unicode character. /// @@ -6731,34 +5106,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Editing" })] Link = 0xF0C1, - /// - /// The Font Awesome "linkedin" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "linkedin", "linkedin-square" })] - Linkedin = 0xF08C, - - /// - /// The Font Awesome "linkedin-in" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "linkedin in", "linkedin" })] - LinkedinIn = 0xF0E1, - - /// - /// The Font Awesome "linode" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "linode" })] - Linode = 0xF2B8, - - /// - /// The Font Awesome "linux" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "linux", "tux" })] - Linux = 0xF17C, - /// /// The Font Awesome "lira-sign" icon unicode character. /// @@ -6899,20 +5246,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] LungsVirus = 0xE067, - /// - /// The Font Awesome "lyft" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "lyft" })] - Lyft = 0xF3C3, - - /// - /// The Font Awesome "magento" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "magento" })] - Magento = 0xF3C4, - /// /// The Font Awesome "wand-magic" icon unicode character. /// @@ -6948,13 +5281,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] MailBulk = 0xF674, - /// - /// The Font Awesome "mailchimp" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "mailchimp" })] - Mailchimp = 0xF59E, - /// /// The Font Awesome "person" icon unicode character. /// @@ -6969,13 +5295,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] ManatSign = 0xE1D5, - /// - /// The Font Awesome "mandalorian" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "mandalorian" })] - Mandalorian = 0xF50F, - /// /// The Font Awesome "map" icon unicode character. /// @@ -7025,13 +5344,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Camping", "Maps", "Nature" })] MapSigns = 0xF277, - /// - /// The Font Awesome "markdown" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "markdown" })] - Markdown = 0xF60F, - /// /// The Font Awesome "marker" icon unicode character. /// @@ -7102,13 +5414,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] MaskVentilator = 0xE524, - /// - /// The Font Awesome "mastodon" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "mastodon" })] - Mastodon = 0xF4F6, - /// /// The Font Awesome "mattress-pillow" icon unicode character. /// @@ -7116,20 +5421,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Camping", "Household", "Humanitarian" })] MattressPillow = 0xE525, - /// - /// The Font Awesome "maxcdn" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "maxcdn" })] - Maxcdn = 0xF136, - - /// - /// The Font Awesome "mdb" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "mdb" })] - Mdb = 0xF8CA, - /// /// The Font Awesome "medal" icon unicode character. /// @@ -7137,28 +5428,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness" })] Medal = 0xF5A2, - /// - /// The Font Awesome "medapps" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "medapps" })] - Medapps = 0xF3C6, - - /// - /// The Font Awesome "medium" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "medium" })] - Medium = 0xF23A, - - /// - /// The Font Awesome "medium" icon unicode character. - /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF23A. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "medium" })] - MediumM = 0xF3C7, - /// /// The Font Awesome "suitcase-medical" icon unicode character. /// @@ -7166,27 +5435,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Maps", "Medical + Health" })] Medkit = 0xF0FA, - /// - /// The Font Awesome "medrt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "medrt" })] - Medrt = 0xF3C8, - - /// - /// The Font Awesome "meetup" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "meetup" })] - Meetup = 0xF2E0, - - /// - /// The Font Awesome "megaport" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "megaport" })] - Megaport = 0xF5A3, - /// /// The Font Awesome "face-meh" icon unicode character. /// @@ -7215,13 +5463,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] Memory = 0xF538, - /// - /// The Font Awesome "mendeley" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "mendeley" })] - Mendeley = 0xF7B3, - /// /// The Font Awesome "menorah" icon unicode character. /// @@ -7291,13 +5532,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Education", "Humanitarian", "Medical + Health", "Science" })] Microscope = 0xF610, - /// - /// The Font Awesome "microsoft" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "microsoft" })] - Microsoft = 0xF3CA, - /// /// The Font Awesome "mill-sign" icon unicode character. /// @@ -7333,34 +5567,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Clothing + Fashion" })] Mitten = 0xF7B5, - /// - /// The Font Awesome "mix" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "mix" })] - Mix = 0xF3CB, - - /// - /// The Font Awesome "mixcloud" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "mixcloud" })] - Mixcloud = 0xF289, - - /// - /// The Font Awesome "mixer" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "mixer" })] - Mixer = 0xF956, - - /// - /// The Font Awesome "mizuni" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "mizuni" })] - Mizuni = 0xF3CC, - /// /// The Font Awesome "mobile" icon unicode character. /// @@ -7396,20 +5602,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Communication", "Devices + Hardware", "Humanitarian" })] MobileScreen = 0xF3CF, - /// - /// The Font Awesome "modx" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "modx" })] - Modx = 0xF285, - - /// - /// The Font Awesome "monero" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "monero" })] - Monero = 0xF3D0, - /// /// The Font Awesome "money-bill" icon unicode character. /// @@ -7592,21 +5784,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] NairaSign = 0xE1F6, - /// - /// The Font Awesome "napster" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "napster" })] - [FontAwesomeCategoriesAttribute(new[] { "Music + Audio" })] - Napster = 0xF3D2, - - /// - /// The Font Awesome "neos" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "neos" })] - Neos = 0xF612, - /// /// The Font Awesome "network-wired" icon unicode character. /// @@ -7628,27 +5805,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Maps", "Writing" })] Newspaper = 0xF1EA, - /// - /// The Font Awesome "nimblr" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "nimblr" })] - Nimblr = 0xF5A8, - - /// - /// The Font Awesome "node" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "node" })] - Node = 0xF419, - - /// - /// The Font Awesome "node-js" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "node js" })] - NodeJs = 0xF3D3, - /// /// The Font Awesome "notdef" icon unicode character. /// @@ -7670,27 +5826,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] NotesMedical = 0xF481, - /// - /// The Font Awesome "npm" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "npm" })] - Npm = 0xF3D4, - - /// - /// The Font Awesome "ns8" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ns8" })] - Ns8 = 0xF3D5, - - /// - /// The Font Awesome "nutritionix" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "nutritionix" })] - Nutritionix = 0xF3D6, - /// /// The Font Awesome "object-group" icon unicode character. /// @@ -7705,20 +5840,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Design" })] ObjectUngroup = 0xF248, - /// - /// The Font Awesome "odnoklassniki" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "odnoklassniki" })] - Odnoklassniki = 0xF263, - - /// - /// The Font Awesome "square-odnoklassniki" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square odnoklassniki" })] - OdnoklassnikiSquare = 0xF264, - /// /// The Font Awesome "oil-can" icon unicode character. /// @@ -7733,14 +5854,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Energy", "Humanitarian" })] OilWell = 0xE532, - /// - /// The Font Awesome "old-republic" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "old republic", "politics", "star wars" })] - [FontAwesomeCategoriesAttribute(new[] { "Science Fiction" })] - OldRepublic = 0xF510, - /// /// The Font Awesome "om" icon unicode character. /// @@ -7748,48 +5861,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Religion" })] Om = 0xF679, - /// - /// The Font Awesome "opencart" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "opencart" })] - Opencart = 0xF23D, - - /// - /// The Font Awesome "openid" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "openid" })] - Openid = 0xF19B, - - /// - /// The Font Awesome "opera" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "opera" })] - Opera = 0xF26A, - - /// - /// The Font Awesome "optin-monster" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "optin monster" })] - OptinMonster = 0xF23C, - - /// - /// The Font Awesome "orcid" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "orcid" })] - Orcid = 0xF8D2, - - /// - /// The Font Awesome "osi" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "osi" })] - Osi = 0xF41A, - /// /// The Font Awesome "otter" icon unicode character. /// @@ -7804,20 +5875,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] Outdent = 0xF03B, - /// - /// The Font Awesome "page4" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "page4" })] - Page4 = 0xF3D7, - - /// - /// The Font Awesome "pagelines" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "pagelines", "eco", "flora", "leaf", "leaves", "nature", "plant", "tree" })] - Pagelines = 0xF18C, - /// /// The Font Awesome "pager" icon unicode character. /// @@ -7846,13 +5903,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Design", "Spinners" })] Palette = 0xF53F, - /// - /// The Font Awesome "palfed" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "palfed" })] - Palfed = 0xF3D8, - /// /// The Font Awesome "pallet" icon unicode character. /// @@ -7923,13 +5973,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Files" })] Paste = 0xF0EA, - /// - /// The Font Awesome "patreon" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "patreon" })] - Patreon = 0xF3D9, - /// /// The Font Awesome "pause" icon unicode character. /// @@ -7951,14 +5994,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Animals", "Maps" })] Paw = 0xF1B0, - /// - /// The Font Awesome "paypal" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "paypal" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - Paypal = 0xF1ED, - /// /// The Font Awesome "peace" icon unicode character. /// @@ -8093,13 +6128,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Mathematics", "Money", "Punctuation + Symbols" })] Percentage = 0xF541, - /// - /// The Font Awesome "periscope" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "periscope" })] - Periscope = 0xF3DA, - /// /// The Font Awesome "person-arrow-down-to-line" icon unicode character. /// @@ -8338,27 +6366,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] PesoSign = 0xE222, - /// - /// The Font Awesome "phabricator" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "phabricator" })] - Phabricator = 0xF3DB, - - /// - /// The Font Awesome "phoenix-framework" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "phoenix framework" })] - PhoenixFramework = 0xF3DC, - - /// - /// The Font Awesome "phoenix-squadron" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "phoenix squadron" })] - PhoenixSquadron = 0xF511, - /// /// The Font Awesome "phone" icon unicode character. /// @@ -8408,41 +6415,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Files", "Film + Video", "Photos + Images", "Social" })] PhotoVideo = 0xF87C, - /// - /// The Font Awesome "php" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "php" })] - Php = 0xF457, - - /// - /// The Font Awesome "pied-piper" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "pied piper" })] - PiedPiper = 0xF2AE, - - /// - /// The Font Awesome "pied-piper-alt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "pied piper alt" })] - PiedPiperAlt = 0xF1A8, - - /// - /// The Font Awesome "pied-piper-hat" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "pied piper hat", "clothing" })] - PiedPiperHat = 0xF4E5, - - /// - /// The Font Awesome "pied-piper-pp" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "pied piper pp" })] - PiedPiperPp = 0xF1A7, - /// /// The Font Awesome "piedpipersquare" icon unicode character. /// @@ -8463,27 +6435,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Science" })] Pills = 0xF484, - /// - /// The Font Awesome "pinterest" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "pinterest" })] - Pinterest = 0xF0D2, - - /// - /// The Font Awesome "pinterest-p" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "pinterest p" })] - PinterestP = 0xF231, - - /// - /// The Font Awesome "square-pinterest" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square pinterest" })] - PinterestSquare = 0xF0D3, - /// /// The Font Awesome "pizza-slice" icon unicode character. /// @@ -8589,14 +6540,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Media Playback" })] PlayCircle = 0xF144, - /// - /// The Font Awesome "playstation" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "playstation" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - Playstation = 0xF3DF, - /// /// The Font Awesome "plug" icon unicode character. /// @@ -8787,13 +6730,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] Procedures = 0xF487, - /// - /// The Font Awesome "product-hunt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "product hunt" })] - ProductHunt = 0xF288, - /// /// The Font Awesome "diagram-project" icon unicode character. /// @@ -8815,13 +6751,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Household", "Humanitarian" })] PumpSoap = 0xE06B, - /// - /// The Font Awesome "pushed" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "pushed" })] - Pushed = 0xF3E1, - /// /// The Font Awesome "puzzle-piece" icon unicode character. /// @@ -8829,20 +6758,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Childhood", "Gaming" })] PuzzlePiece = 0xF12E, - /// - /// The Font Awesome "python" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "python" })] - Python = 0xF3E2, - - /// - /// The Font Awesome "qq" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "qq" })] - Qq = 0xF1D6, - /// /// The Font Awesome "qrcode" icon unicode character. /// @@ -8872,20 +6787,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness" })] Quidditch = 0xF458, - /// - /// The Font Awesome "quinscape" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "quinscape" })] - Quinscape = 0xF459, - - /// - /// The Font Awesome "quora" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "quora" })] - Quora = 0xF2C4, - /// /// The Font Awesome "quote-left" icon unicode character. /// @@ -8949,48 +6850,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Marketing", "Sports + Fitness" })] RankingStar = 0xE561, - /// - /// The Font Awesome "raspberry-pi" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "raspberry pi" })] - RaspberryPi = 0xF7BB, - - /// - /// The Font Awesome "ravelry" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ravelry" })] - Ravelry = 0xF2D9, - - /// - /// The Font Awesome "react" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "react" })] - React = 0xF41B, - - /// - /// The Font Awesome "reacteurope" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "reacteurope" })] - Reacteurope = 0xF75D, - - /// - /// The Font Awesome "readme" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "readme" })] - Readme = 0xF4D5, - - /// - /// The Font Awesome "rebel" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "rebel" })] - Rebel = 0xF1D0, - /// /// The Font Awesome "receipt" icon unicode character. /// @@ -9012,34 +6871,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Maps" })] Recycle = 0xF1B8, - /// - /// The Font Awesome "reddit" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "reddit" })] - Reddit = 0xF1A1, - - /// - /// The Font Awesome "reddit-alien" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "reddit alien" })] - RedditAlien = 0xF281, - - /// - /// The Font Awesome "square-reddit" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square reddit" })] - RedditSquare = 0xF1A2, - - /// - /// The Font Awesome "redhat" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "redhat", "linux", "operating system", "os" })] - Redhat = 0xF7BC, - /// /// The Font Awesome "arrow-rotate-right" icon unicode character. /// @@ -9054,13 +6885,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] RedoAlt = 0xF2F9, - /// - /// The Font Awesome "red-river" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "red river" })] - RedRiver = 0xF3E3, - /// /// The Font Awesome "registered" icon unicode character. /// @@ -9075,13 +6899,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] RemoveFormat = 0xF87D, - /// - /// The Font Awesome "renren" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "renren" })] - Renren = 0xF18B, - /// /// The Font Awesome "repeat" icon unicode character. /// @@ -9103,13 +6920,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] ReplyAll = 0xF122, - /// - /// The Font Awesome "replyd" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "replyd" })] - Replyd = 0xF3E6, - /// /// The Font Awesome "republican" icon unicode character. /// @@ -9117,20 +6927,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Political" })] Republican = 0xF75E, - /// - /// The Font Awesome "researchgate" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "researchgate" })] - Researchgate = 0xF4F8, - - /// - /// The Font Awesome "resolving" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "resolving" })] - Resolving = 0xF3E7, - /// /// The Font Awesome "restroom" icon unicode character. /// @@ -9145,13 +6941,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Social" })] Retweet = 0xF079, - /// - /// The Font Awesome "rev" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "rev" })] - Rev = 0xF5B2, - /// /// The Font Awesome "ribbon" icon unicode character. /// @@ -9236,20 +7025,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Maps", "Science Fiction", "Transportation" })] Rocket = 0xF135, - /// - /// The Font Awesome "rocketchat" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "rocketchat" })] - Rocketchat = 0xF3E8, - - /// - /// The Font Awesome "rockrms" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "rockrms" })] - Rockrms = 0xF3E9, - /// /// The Font Awesome "route" icon unicode character. /// @@ -9257,13 +7032,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Camping", "Maps", "Moving" })] Route = 0xF4D7, - /// - /// The Font Awesome "r-project" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "r project" })] - RProject = 0xF4F7, - /// /// The Font Awesome "rss" icon unicode character. /// @@ -9369,13 +7137,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Emoji" })] SadTear = 0xF5B4, - /// - /// The Font Awesome "safari" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "safari", "browser" })] - Safari = 0xF267, - /// /// The Font Awesome "sailboat" icon unicode character. /// @@ -9383,20 +7144,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Logistics", "Maritime", "Transportation" })] Sailboat = 0xE445, - /// - /// The Font Awesome "salesforce" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "salesforce" })] - Salesforce = 0xF83B, - - /// - /// The Font Awesome "sass" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "sass" })] - Sass = 0xF41E, - /// /// The Font Awesome "satellite" icon unicode character. /// @@ -9418,13 +7165,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Devices + Hardware", "Files" })] Save = 0xF0C7, - /// - /// The Font Awesome "schlix" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "schlix" })] - Schlix = 0xF3EA, - /// /// The Font Awesome "school" icon unicode character. /// @@ -9474,13 +7214,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Construction" })] Screwdriver = 0xF54A, - /// - /// The Font Awesome "scribd" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "scribd" })] - Scribd = 0xF28A, - /// /// The Font Awesome "scroll" icon unicode character. /// @@ -9509,13 +7242,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Marketing" })] SearchDollar = 0xF688, - /// - /// The Font Awesome "searchengin" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "searchengin" })] - Searchengin = 0xF3EB, - /// /// The Font Awesome "magnifying-glass-location" icon unicode character. /// @@ -9551,20 +7277,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Charity", "Energy", "Food + Beverage", "Fruits + Vegetables", "Humanitarian", "Nature", "Science" })] Seedling = 0xF4D8, - /// - /// The Font Awesome "sellcast" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "sellcast", "eercast" })] - Sellcast = 0xF2DA, - - /// - /// The Font Awesome "sellsy" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "sellsy" })] - Sellsy = 0xF213, - /// /// The Font Awesome "server" icon unicode character. /// @@ -9572,13 +7284,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] Server = 0xF233, - /// - /// The Font Awesome "servicestack" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "servicestack" })] - Servicestack = 0xF3EC, - /// /// The Font Awesome "shapes" icon unicode character. /// @@ -9684,13 +7389,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Logistics", "Shopping" })] ShippingFast = 0xF48B, - /// - /// The Font Awesome "shirtsinbulk" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "shirtsinbulk" })] - Shirtsinbulk = 0xF214, - /// /// The Font Awesome "shoe-prints" icon unicode character. /// @@ -9698,13 +7396,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Clothing + Fashion", "Maps", "Sports + Fitness" })] ShoePrints = 0xF54B, - /// - /// The Font Awesome "shopify" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "shopify" })] - Shopify = 0xF957, - /// /// The Font Awesome "shop-lock" icon unicode character. /// @@ -9740,13 +7431,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] ShopSlash = 0xE070, - /// - /// The Font Awesome "shopware" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "shopware" })] - Shopware = 0xF5B5, - /// /// The Font Awesome "shower" icon unicode character. /// @@ -9817,13 +7501,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware" })] SimCard = 0xF7C4, - /// - /// The Font Awesome "simplybuilt" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "simplybuilt" })] - Simplybuilt = 0xF215, - /// /// The Font Awesome "sink" icon unicode character. /// @@ -9831,13 +7508,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Household" })] Sink = 0xE06D, - /// - /// The Font Awesome "sistrix" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "sistrix" })] - Sistrix = 0xF3EE, - /// /// The Font Awesome "sitemap" icon unicode character. /// @@ -9845,13 +7515,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Coding" })] Sitemap = 0xF0E8, - /// - /// The Font Awesome "sith" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "sith" })] - Sith = 0xF512, - /// /// The Font Awesome "person-skating" icon unicode character. /// @@ -9859,13 +7522,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness", "Users + People" })] Skating = 0xF7C5, - /// - /// The Font Awesome "sketch" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "sketch", "app", "design", "interface" })] - Sketch = 0xF7C6, - /// /// The Font Awesome "person-skiing" icon unicode character. /// @@ -9894,35 +7550,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Alert", "Gaming", "Halloween", "Humanitarian", "Medical + Health", "Science", "Security" })] SkullCrossbones = 0xF714, - /// - /// The Font Awesome "skyatlas" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "skyatlas" })] - Skyatlas = 0xF216, - - /// - /// The Font Awesome "skype" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "skype" })] - Skype = 0xF17E, - - /// - /// The Font Awesome "slack" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "slack", "anchor", "hash", "hashtag" })] - Slack = 0xF198, - - /// - /// The Font Awesome "slack" icon unicode character. - /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF198. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "slack", "anchor", "hash", "hashtag" })] - SlackHash = 0xF3EF, - /// /// The Font Awesome "slash" icon unicode character. /// @@ -9944,13 +7571,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Editing", "Media Playback", "Music + Audio", "Photos + Images" })] SlidersH = 0xF1DE, - /// - /// The Font Awesome "slideshare" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "slideshare" })] - Slideshare = 0xF1E7, - /// /// The Font Awesome "face-smile" icon unicode character. /// @@ -10000,28 +7620,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Communication" })] Sms = 0xF7CD, - /// - /// The Font Awesome "snapchat" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "snapchat" })] - Snapchat = 0xF2AB, - - /// - /// The Font Awesome "snapchat" icon unicode character. - /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF2AB. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "snapchat" })] - SnapchatGhost = 0xF2AC, - - /// - /// The Font Awesome "square-snapchat" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square snapchat" })] - SnapchatSquare = 0xF2AD, - /// /// The Font Awesome "person-snowboarding" icon unicode character. /// @@ -10176,21 +7774,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows" })] SortUp = 0xF0DE, - /// - /// The Font Awesome "soundcloud" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "soundcloud" })] - [FontAwesomeCategoriesAttribute(new[] { "Music + Audio" })] - Soundcloud = 0xF1BE, - - /// - /// The Font Awesome "sourcetree" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "sourcetree" })] - Sourcetree = 0xF7D3, - /// /// The Font Awesome "spa" icon unicode character. /// @@ -10205,20 +7788,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Transportation" })] SpaceShuttle = 0xF197, - /// - /// The Font Awesome "speakap" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "speakap" })] - Speakap = 0xF3F3, - - /// - /// The Font Awesome "speaker-deck" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "speaker deck" })] - SpeakerDeck = 0xF83C, - /// /// The Font Awesome "spell-check" icon unicode character. /// @@ -10247,14 +7816,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Design" })] Splotch = 0xF5BC, - /// - /// The Font Awesome "spotify" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "spotify" })] - [FontAwesomeCategoriesAttribute(new[] { "Music + Audio" })] - Spotify = 0xF1BC, - /// /// The Font Awesome "spray-can" icon unicode character. /// @@ -10304,13 +7865,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Mathematics" })] SquareRootAlt = 0xF698, - /// - /// The Font Awesome "squarespace" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "squarespace" })] - Squarespace = 0xF5BE, - /// /// The Font Awesome "square-virus" icon unicode character. /// @@ -10325,27 +7879,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Mathematics" })] SquareXmark = 0xF2D3, - /// - /// The Font Awesome "stack-exchange" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "stack exchange" })] - StackExchange = 0xF18D, - - /// - /// The Font Awesome "stack-overflow" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "stack overflow" })] - StackOverflow = 0xF16C, - - /// - /// The Font Awesome "stackpath" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "stackpath" })] - Stackpath = 0xF842, - /// /// The Font Awesome "staff-snake" icon unicode character. /// @@ -10416,37 +7949,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] StarOfLife = 0xF621, - /// - /// The Font Awesome "staylinked" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "staylinked" })] - Staylinked = 0xF3F5, - - /// - /// The Font Awesome "steam" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "steam" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - Steam = 0xF1B6, - - /// - /// The Font Awesome "square-steam" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square steam" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - SteamSquare = 0xF1B7, - - /// - /// The Font Awesome "steam-symbol" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "steam symbol" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - SteamSymbol = 0xF3F6, - /// /// The Font Awesome "backward-step" icon unicode character. /// @@ -10468,13 +7970,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health" })] Stethoscope = 0xF0F1, - /// - /// The Font Awesome "sticker-mule" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "sticker mule" })] - StickerMule = 0xF3F7, - /// /// The Font Awesome "note-sticky" icon unicode character. /// @@ -10531,13 +8026,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] StoreSlash = 0xE071, - /// - /// The Font Awesome "strava" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "strava" })] - Strava = 0xF428, - /// /// The Font Awesome "bars-staggered" icon unicode character. /// @@ -10559,22 +8047,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Text Formatting" })] Strikethrough = 0xF0CC, - /// - /// The Font Awesome "stripe" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "stripe" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - Stripe = 0xF429, - - /// - /// The Font Awesome "stripe-s" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "stripe s" })] - [FontAwesomeCategoriesAttribute(new[] { "Shopping" })] - StripeS = 0xF42A, - /// /// The Font Awesome "stroopwafel" icon unicode character. /// @@ -10582,27 +8054,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage", "Spinners" })] Stroopwafel = 0xF551, - /// - /// The Font Awesome "studiovinari" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "studiovinari" })] - Studiovinari = 0xF3F8, - - /// - /// The Font Awesome "stumbleupon" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "stumbleupon" })] - Stumbleupon = 0xF1A4, - - /// - /// The Font Awesome "stumbleupon-circle" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "stumbleupon circle" })] - StumbleuponCircle = 0xF1A3, - /// /// The Font Awesome "subscript" icon unicode character. /// @@ -10645,13 +8096,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Disaster + Crisis", "Humanitarian", "Weather" })] SunPlantWilt = 0xE57A, - /// - /// The Font Awesome "superpowers" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "superpowers" })] - Superpowers = 0xF2DD, - /// /// The Font Awesome "superscript" icon unicode character. /// @@ -10659,13 +8103,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Mathematics", "Text Formatting" })] Superscript = 0xF12B, - /// - /// The Font Awesome "supple" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "supple" })] - Supple = 0xF3F9, - /// /// The Font Awesome "face-surprise" icon unicode character. /// @@ -10673,13 +8110,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Emoji" })] Surprise = 0xF5C2, - /// - /// The Font Awesome "suse" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "suse", "linux", "operating system", "os" })] - Suse = 0xF7D6, - /// /// The Font Awesome "swatchbook" icon unicode character. /// @@ -10687,13 +8117,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Design" })] Swatchbook = 0xF5C3, - /// - /// The Font Awesome "swift" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "swift" })] - Swift = 0xF8E1, - /// /// The Font Awesome "person-swimming" icon unicode character. /// @@ -10708,13 +8131,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Travel + Hotel" })] SwimmingPool = 0xF5C5, - /// - /// The Font Awesome "symfony" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "symfony" })] - Symfony = 0xF83D, - /// /// The Font Awesome "synagogue" icon unicode character. /// @@ -10842,13 +8258,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Maps", "Transportation", "Travel + Hotel" })] Taxi = 0xF1BA, - /// - /// The Font Awesome "teamspeak" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "teamspeak" })] - Teamspeak = 0xF4F9, - /// /// The Font Awesome "teeth" icon unicode character. /// @@ -10863,21 +8272,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] TeethOpen = 0xF62F, - /// - /// The Font Awesome "telegram" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "telegram" })] - Telegram = 0xF2C6, - - /// - /// The Font Awesome "telegram" icon unicode character. - /// Uses a legacy unicode value for backwards compatability. The current unicode value is 0xF2C6. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "telegram" })] - TelegramPlane = 0xF3FE, - /// /// The Font Awesome "temperature-arrow-down" icon unicode character. /// @@ -10906,13 +8300,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Science", "Weather" })] TemperatureLow = 0xF76B, - /// - /// The Font Awesome "tencent-weibo" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "tencent weibo" })] - TencentWeibo = 0xF1D5, - /// /// The Font Awesome "tenge-sign" icon unicode character. /// @@ -10997,27 +8384,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Education" })] TheaterMasks = 0xF630, - /// - /// The Font Awesome "themeco" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "themeco" })] - Themeco = 0xF5C6, - - /// - /// The Font Awesome "themeisle" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "themeisle" })] - Themeisle = 0xF2B2, - - /// - /// The Font Awesome "the-red-yeti" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "the red yeti" })] - TheRedYeti = 0xF69D, - /// /// The Font Awesome "thermometer" icon unicode character. /// @@ -11060,13 +8426,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Weather" })] ThermometerThreeQuarters = 0xF2C8, - /// - /// The Font Awesome "think-peaks" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "think peaks" })] - ThinkPeaks = 0xF731, - /// /// The Font Awesome "table-cells-large" icon unicode character. /// @@ -11270,13 +8629,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Transportation" })] Tractor = 0xF722, - /// - /// The Font Awesome "trade-federation" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "trade federation" })] - TradeFederation = 0xF513, - /// /// The Font Awesome "trademark" icon unicode character. /// @@ -11291,14 +8643,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Maps" })] TrafficLight = 0xF637, - /// - /// The Font Awesome "trailer" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "trailer", "carry", "haul", "moving", "travel" })] - [FontAwesomeCategoriesAttribute(new[] { "Automotive", "Camping", "Moving" })] - Trailer = 0xF941, - /// /// The Font Awesome "train" icon unicode character. /// @@ -11376,13 +8720,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Travel + Hotel" })] TreeCity = 0xE587, - /// - /// The Font Awesome "trello" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "trello", "atlassian" })] - Trello = 0xF181, - /// /// The Font Awesome "tripadvisor" icon unicode character. /// @@ -11501,20 +8838,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Communication", "Maps" })] Tty = 0xF1E4, - /// - /// The Font Awesome "tumblr" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "tumblr" })] - Tumblr = 0xF173, - - /// - /// The Font Awesome "square-tumblr" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square tumblr" })] - TumblrSquare = 0xF174, - /// /// The Font Awesome "turkish-lira-sign" icon unicode character. /// @@ -11529,63 +8852,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Devices + Hardware", "Film + Video", "Household", "Travel + Hotel" })] Tv = 0xF26C, - /// - /// The Font Awesome "twitch" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "twitch" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - Twitch = 0xF1E8, - - /// - /// The Font Awesome "twitter" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "twitter", "social network", "tweet" })] - Twitter = 0xF099, - - /// - /// The Font Awesome "square-twitter" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square twitter", "social network", "tweet" })] - TwitterSquare = 0xF081, - - /// - /// The Font Awesome "typo3" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "typo3" })] - Typo3 = 0xF42B, - - /// - /// The Font Awesome "uber" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "uber" })] - Uber = 0xF402, - - /// - /// The Font Awesome "ubuntu" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ubuntu", "linux", "operating system", "os" })] - Ubuntu = 0xF7DF, - - /// - /// The Font Awesome "uikit" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "uikit" })] - Uikit = 0xF403, - - /// - /// The Font Awesome "umbraco" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "umbraco" })] - Umbraco = 0xF8E8, - /// /// The Font Awesome "umbrella" icon unicode character. /// @@ -11621,13 +8887,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] UndoAlt = 0xF2EA, - /// - /// The Font Awesome "uniregistry" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "uniregistry" })] - Uniregistry = 0xF404, - /// /// The Font Awesome "unity" icon unicode character. /// @@ -11669,13 +8928,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Security" })] UnlockAlt = 0xF13E, - /// - /// The Font Awesome "untappd" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "untappd" })] - Untappd = 0xF405, - /// /// The Font Awesome "upload" icon unicode character. /// @@ -11683,20 +8935,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Devices + Hardware" })] Upload = 0xF093, - /// - /// The Font Awesome "ups" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ups", "united parcel service", "package", "shipping" })] - Ups = 0xF7E0, - - /// - /// The Font Awesome "usb" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "usb" })] - Usb = 0xF287, - /// /// The Font Awesome "user" icon unicode character. /// @@ -11921,20 +9159,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Users + People" })] UserTimes = 0xF235, - /// - /// The Font Awesome "usps" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "usps", "american", "package", "shipping", "usa" })] - Usps = 0xF7E1, - - /// - /// The Font Awesome "ussunnah" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "ussunnah" })] - Ussunnah = 0xF407, - /// /// The Font Awesome "utensils" icon unicode character. /// @@ -11949,13 +9173,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Household", "Maps" })] UtensilSpoon = 0xF2E5, - /// - /// The Font Awesome "vaadin" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "vaadin" })] - Vaadin = 0xF408, - /// /// The Font Awesome "vault" icon unicode character. /// @@ -12005,27 +9222,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Clothing + Fashion", "Maps" })] VestPatches = 0xE086, - /// - /// The Font Awesome "viacoin" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "viacoin" })] - Viacoin = 0xF237, - - /// - /// The Font Awesome "viadeo" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "viadeo" })] - Viadeo = 0xF2A9, - - /// - /// The Font Awesome "square-viadeo" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square viadeo" })] - ViadeoSquare = 0xF2AA, - /// /// The Font Awesome "vial" icon unicode character. /// @@ -12054,13 +9250,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Science" })] VialVirus = 0xE597, - /// - /// The Font Awesome "viber" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "viber" })] - Viber = 0xF409, - /// /// The Font Awesome "video" icon unicode character. /// @@ -12082,34 +9271,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Religion" })] Vihara = 0xF6A7, - /// - /// The Font Awesome "vimeo" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "vimeo" })] - Vimeo = 0xF40A, - - /// - /// The Font Awesome "square-vimeo" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square vimeo" })] - VimeoSquare = 0xF194, - - /// - /// The Font Awesome "vimeo-v" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "vimeo v", "vimeo" })] - VimeoV = 0xF27D, - - /// - /// The Font Awesome "vine" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "vine" })] - Vine = 0xF1CA, - /// /// The Font Awesome "virus" icon unicode character. /// @@ -12145,20 +9306,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] VirusSlash = 0xE075, - /// - /// The Font Awesome "vk" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "vk" })] - Vk = 0xF189, - - /// - /// The Font Awesome "vnv" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "vnv" })] - Vnv = 0xF40B, - /// /// The Font Awesome "voicemail" icon unicode character. /// @@ -12222,13 +9369,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] VrCardboard = 0xF729, - /// - /// The Font Awesome "vuejs" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "vuejs" })] - Vuejs = 0xF41F, - /// /// The Font Awesome "walkie-talkie" icon unicode character. /// @@ -12285,27 +9425,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Mathematics" })] WaveSquare = 0xF83E, - /// - /// The Font Awesome "waze" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "waze" })] - Waze = 0xF83F, - - /// - /// The Font Awesome "weebly" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "weebly" })] - Weebly = 0xF5CC, - - /// - /// The Font Awesome "weibo" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "weibo" })] - Weibo = 0xF18A, - /// /// The Font Awesome "weight-scale" icon unicode character. /// @@ -12320,27 +9439,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Sports + Fitness" })] WeightHanging = 0xF5CD, - /// - /// The Font Awesome "weixin" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "weixin" })] - Weixin = 0xF1D7, - - /// - /// The Font Awesome "whatsapp" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "whatsapp" })] - Whatsapp = 0xF232, - - /// - /// The Font Awesome "square-whatsapp" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square whatsapp" })] - WhatsappSquare = 0xF40C, - /// /// The Font Awesome "wheat-awn" icon unicode character. /// @@ -12369,13 +9467,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Humanitarian", "Maps", "Medical + Health", "Transportation", "Travel + Hotel", "Users + People" })] WheelchairMove = 0xE2CE, - /// - /// The Font Awesome "whmcs" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "whmcs" })] - Whmcs = 0xF40D, - /// /// The Font Awesome "wifi" icon unicode character. /// @@ -12383,13 +9474,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Connectivity", "Humanitarian", "Maps", "Toggle", "Travel + Hotel" })] Wifi = 0xF1EB, - /// - /// The Font Awesome "wikipedia-w" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wikipedia w" })] - WikipediaW = 0xF266, - /// /// The Font Awesome "wind" icon unicode character. /// @@ -12425,13 +9509,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Coding" })] WindowRestore = 0xF2D2, - /// - /// The Font Awesome "windows" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "windows", "microsoft", "operating system", "os" })] - Windows = 0xF17A, - /// /// The Font Awesome "wine-bottle" icon unicode character. /// @@ -12453,28 +9530,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Food + Beverage", "Travel + Hotel" })] WineGlassAlt = 0xF5CE, - /// - /// The Font Awesome "wix" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wix" })] - Wix = 0xF5CF, - - /// - /// The Font Awesome "wizards-of-the-coast" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wizards of the coast", "dungeons & dragons", "d&d", "dnd", "fantasy", "game", "gaming", "tabletop" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - WizardsOfTheCoast = 0xF730, - - /// - /// The Font Awesome "wolf-pack-battalion" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wolf pack battalion" })] - WolfPackBattalion = 0xF514, - /// /// The Font Awesome "won-sign" icon unicode character. /// @@ -12482,20 +9537,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Money" })] WonSign = 0xF159, - /// - /// The Font Awesome "wordpress" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wordpress" })] - Wordpress = 0xF19A, - - /// - /// The Font Awesome "wordpress-simple" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wordpress simple" })] - WordpressSimple = 0xF411, - /// /// The Font Awesome "worm" icon unicode character. /// @@ -12503,34 +9544,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Animals", "Disaster + Crisis", "Humanitarian", "Nature" })] Worm = 0xE599, - /// - /// The Font Awesome "wpbeginner" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wpbeginner" })] - Wpbeginner = 0xF297, - - /// - /// The Font Awesome "wpexplorer" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wpexplorer" })] - Wpexplorer = 0xF2DE, - - /// - /// The Font Awesome "wpforms" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wpforms" })] - Wpforms = 0xF298, - - /// - /// The Font Awesome "wpressr" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "wpressr", "rendact" })] - Wpressr = 0xF3E4, - /// /// The Font Awesome "wrench" icon unicode character. /// @@ -12538,28 +9551,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Construction", "Maps" })] Wrench = 0xF0AD, - /// - /// The Font Awesome "xbox" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "xbox" })] - [FontAwesomeCategoriesAttribute(new[] { "Gaming" })] - Xbox = 0xF412, - - /// - /// The Font Awesome "xing" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "xing" })] - Xing = 0xF168, - - /// - /// The Font Awesome "square-xing" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square xing" })] - XingSquare = 0xF169, - /// /// The Font Awesome "xmarks-lines" icon unicode character. /// @@ -12574,55 +9565,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Medical + Health" })] XRay = 0xF497, - /// - /// The Font Awesome "yahoo" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "yahoo" })] - Yahoo = 0xF19E, - - /// - /// The Font Awesome "yammer" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "yammer" })] - Yammer = 0xF840, - - /// - /// The Font Awesome "yandex" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "yandex" })] - Yandex = 0xF413, - - /// - /// The Font Awesome "yandex-international" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "yandex international" })] - YandexInternational = 0xF414, - - /// - /// The Font Awesome "yarn" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "yarn" })] - Yarn = 0xF7E3, - - /// - /// The Font Awesome "y-combinator" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "y combinator" })] - YCombinator = 0xF23B, - - /// - /// The Font Awesome "yelp" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "yelp" })] - Yelp = 0xF1E9, - /// /// The Font Awesome "yen-sign" icon unicode character. /// @@ -12637,33 +9579,4 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Religion", "Spinners" })] YinYang = 0xF6AD, - /// - /// The Font Awesome "yoast" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "yoast" })] - Yoast = 0xF2B1, - - /// - /// The Font Awesome "youtube" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "youtube", "film", "video", "youtube-play", "youtube-square" })] - [FontAwesomeCategoriesAttribute(new[] { "Film + Video" })] - Youtube = 0xF167, - - /// - /// The Font Awesome "square-youtube" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "square youtube" })] - YoutubeSquare = 0xF431, - - /// - /// The Font Awesome "zhihu" icon unicode character. - /// - [Obsolete] - [FontAwesomeSearchTerms(new[] { "zhihu" })] - Zhihu = 0xF63F, - } From 538976667f4f976ac8422d6f886c3a2d510f5d81 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:10:47 -0700 Subject: [PATCH 009/585] Add IKeyState (v9) (#1267) --- Dalamud/Game/ClientState/Keys/KeyState.cs | 61 ++++++------------ Dalamud/Plugin/Services/IKeyState.cs | 77 +++++++++++++++++++++++ 2 files changed, 97 insertions(+), 41 deletions(-) create mode 100644 Dalamud/Plugin/Services/IKeyState.cs diff --git a/Dalamud/Game/ClientState/Keys/KeyState.cs b/Dalamud/Game/ClientState/Keys/KeyState.cs index 685973e17..ba5cd06d9 100644 --- a/Dalamud/Game/ClientState/Keys/KeyState.cs +++ b/Dalamud/Game/ClientState/Keys/KeyState.cs @@ -1,9 +1,11 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.ClientState.Keys; @@ -23,7 +25,10 @@ namespace Dalamud.Game.ClientState.Keys; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public class KeyState : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public class KeyState : IServiceType, IKeyState { // The array is accessed in a way that this limit doesn't appear to exist // but there is other state data past this point, and keys beyond here aren't @@ -31,7 +36,7 @@ public class KeyState : IServiceType private const int MaxKeyCode = 0xF0; private readonly IntPtr bufferBase; private readonly IntPtr indexBase; - private VirtualKey[] validVirtualKeyCache = null; + private VirtualKey[]? validVirtualKeyCache; [ServiceManager.ServiceConstructor] private KeyState(SigScanner sigScanner, ClientState clientState) @@ -44,46 +49,29 @@ public class KeyState : IServiceType Log.Verbose($"Keyboard state buffer address 0x{this.bufferBase.ToInt64():X}"); } - /// - /// Get or set the key-pressed state for a given vkCode. - /// - /// The virtual key to change. - /// Whether the specified key is currently pressed. - /// If the vkCode is not valid. Refer to or . - /// If the set value is non-zero. - public unsafe bool this[int vkCode] + /// + public bool this[int vkCode] { get => this.GetRawValue(vkCode) != 0; set => this.SetRawValue(vkCode, value ? 1 : 0); } - /// + /// public bool this[VirtualKey vkCode] { get => this[(int)vkCode]; set => this[(int)vkCode] = value; } - /// - /// Gets the value in the index array. - /// - /// The virtual key to change. - /// The raw value stored in the index array. - /// If the vkCode is not valid. Refer to or . + /// public int GetRawValue(int vkCode) => this.GetRefValue(vkCode); - /// + /// public int GetRawValue(VirtualKey vkCode) => this.GetRawValue((int)vkCode); - /// - /// Sets the value in the index array. - /// - /// The virtual key to change. - /// The raw value to set in the index array. - /// If the vkCode is not valid. Refer to or . - /// If the set value is non-zero. + /// public void SetRawValue(int vkCode, int value) { if (value != 0) @@ -92,32 +80,23 @@ public class KeyState : IServiceType this.GetRefValue(vkCode) = value; } - /// + /// public void SetRawValue(VirtualKey vkCode, int value) => this.SetRawValue((int)vkCode, value); - /// - /// Gets a value indicating whether the given VirtualKey code is regarded as valid input by the game. - /// - /// Virtual key code. - /// If the code is valid. + /// public bool IsVirtualKeyValid(int vkCode) => this.ConvertVirtualKey(vkCode) != 0; - /// + /// public bool IsVirtualKeyValid(VirtualKey vkCode) => this.IsVirtualKeyValid((int)vkCode); - /// - /// Gets an array of virtual keys the game considers valid input. - /// - /// An array of valid virtual keys. - public VirtualKey[] GetValidVirtualKeys() - => this.validVirtualKeyCache ??= Enum.GetValues().Where(vk => this.IsVirtualKeyValid(vk)).ToArray(); + /// + public IEnumerable GetValidVirtualKeys() + => this.validVirtualKeyCache ??= Enum.GetValues().Where(this.IsVirtualKeyValid).ToArray(); - /// - /// Clears the pressed state for all keys. - /// + /// public void ClearAll() { foreach (var vk in this.GetValidVirtualKeys()) diff --git a/Dalamud/Plugin/Services/IKeyState.cs b/Dalamud/Plugin/Services/IKeyState.cs new file mode 100644 index 000000000..c2bca7347 --- /dev/null +++ b/Dalamud/Plugin/Services/IKeyState.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using Dalamud.Game.ClientState.Keys; +using PInvoke; + +namespace Dalamud.Plugin.Services; + +/// +/// Wrapper around the game keystate buffer, which contains the pressed state for all keyboard keys, indexed by virtual vkCode. +/// +/// +/// The stored key state is actually a combination field, however the below ephemeral states are consumed each frame. Setting +/// the value may be mildly useful, however retrieving the value is largely pointless. In testing, it wasn't possible without +/// setting the statue manually. +/// index & 0 = key pressed. +/// index & 1 = key down (ephemeral). +/// index & 2 = key up (ephemeral). +/// index & 3 = short key press (ephemeral). +/// +public interface IKeyState +{ + /// + /// Get or set the key-pressed state for a given vkCode. + /// + /// The virtual key to change. + /// Whether the specified key is currently pressed. + /// If the vkCode is not valid. Refer to or . + /// If the set value is non-zero. + public bool this[int vkCode] { get; set; } + + /// + public bool this[VirtualKey vkCode] { get; set; } + + /// + /// Gets the value in the index array. + /// + /// The virtual key to change. + /// The raw value stored in the index array. + /// If the vkCode is not valid. Refer to or . + public int GetRawValue(int vkCode); + + /// + public int GetRawValue(VirtualKey vkCode); + + /// + /// Sets the value in the index array. + /// + /// The virtual key to change. + /// The raw value to set in the index array. + /// If the vkCode is not valid. Refer to or . + /// If the set value is non-zero. + public void SetRawValue(int vkCode, int value); + + /// + public void SetRawValue(VirtualKey vkCode, int value); + + /// + /// Gets a value indicating whether the given VirtualKey code is regarded as valid input by the game. + /// + /// Virtual key code. + /// If the code is valid. + public bool IsVirtualKeyValid(int vkCode); + + /// + public bool IsVirtualKeyValid(VirtualKey vkCode); + + /// + /// Gets an array of virtual keys the game considers valid input. + /// + /// An array of valid virtual keys. + public IEnumerable GetValidVirtualKeys(); + + /// + /// Clears the pressed state for all keys. + /// + public void ClearAll(); +} From 2677964fc547d3ef1198ea58561ed29a4b3a9f52 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 17:24:54 -0700 Subject: [PATCH 010/585] Add ICondition (v9) (#1262) --- .../Game/ClientState/Conditions/Condition.cs | 62 ++++++++++--------- Dalamud/Plugin/Services/ICondition.cs | 54 ++++++++++++++++ 2 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 Dalamud/Plugin/Services/ICondition.cs diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index f611a01c6..b72c91c74 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -2,6 +2,7 @@ using System; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.ClientState.Conditions; @@ -12,13 +13,16 @@ namespace Dalamud.Game.ClientState.Conditions; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed partial class Condition : IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed partial class Condition : IServiceType, ICondition { /// - /// The current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. + /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. /// - public const int MaxConditionEntries = 104; - + internal const int MaxConditionEntries = 104; + private readonly bool[] cache = new bool[MaxConditionEntries]; [ServiceManager.ServiceConstructor] @@ -27,29 +31,17 @@ public sealed partial class Condition : IServiceType var resolver = clientState.AddressResolver; this.Address = resolver.ConditionFlags; } + + /// + public event ICondition.ConditionChangeDelegate? ConditionChange; - /// - /// A delegate type used with the event. - /// - /// The changed condition. - /// The value the condition is set to. - public delegate void ConditionChangeDelegate(ConditionFlag flag, bool value); + /// + public int MaxEntries => MaxConditionEntries; - /// - /// Event that gets fired when a condition is set. - /// Should only get fired for actual changes, so the previous value will always be !value. - /// - public event ConditionChangeDelegate? ConditionChange; - - /// - /// Gets the condition array base pointer. - /// + /// public IntPtr Address { get; private set; } - /// - /// Check the value of a specific condition/state flag. - /// - /// The condition flag to check. + /// public unsafe bool this[int flag] { get @@ -61,14 +53,11 @@ public sealed partial class Condition : IServiceType } } - /// - public unsafe bool this[ConditionFlag flag] + /// + public bool this[ConditionFlag flag] => this[(int)flag]; - /// - /// Check if any condition flags are set. - /// - /// Whether any single flag is set. + /// public bool Any() { for (var i = 0; i < MaxConditionEntries; i++) @@ -81,6 +70,21 @@ public sealed partial class Condition : IServiceType return false; } + + /// + public bool Any(params ConditionFlag[] flags) + { + foreach (var flag in flags) + { + // this[i] performs range checking, so no need to check here + if (this[flag]) + { + return true; + } + } + + return false; + } [ServiceManager.CallWhenServicesReady] private void ContinueConstruction(Framework framework) diff --git a/Dalamud/Plugin/Services/ICondition.cs b/Dalamud/Plugin/Services/ICondition.cs new file mode 100644 index 000000000..9700cef5a --- /dev/null +++ b/Dalamud/Plugin/Services/ICondition.cs @@ -0,0 +1,54 @@ +using Dalamud.Game.ClientState.Conditions; + +namespace Dalamud.Plugin.Services; + +/// +/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. +/// +public interface ICondition +{ + /// + /// A delegate type used with the event. + /// + /// The changed condition. + /// The value the condition is set to. + public delegate void ConditionChangeDelegate(ConditionFlag flag, bool value); + + /// + /// Event that gets fired when a condition is set. + /// Should only get fired for actual changes, so the previous value will always be !value. + /// + public event ConditionChangeDelegate? ConditionChange; + + /// + /// Gets the current max number of conditions. + /// + public int MaxEntries { get; } + + /// + /// Gets the condition array base pointer. + /// + public nint Address { get; } + + /// + /// Check the value of a specific condition/state flag. + /// + /// The condition flag to check. + public bool this[int flag] { get; } + + /// + public bool this[ConditionFlag flag] => this[(int)flag]; + + /// + /// Check if any condition flags are set. + /// + /// Whether any single flag is set. + public bool Any(); + + /// + /// Check if any provided condition flags are set. + /// + /// Whether any single provided flag is set. + /// The condition flags to check. + public bool Any(params ConditionFlag[] flags); +} From a3787498c9e8e48414c7af684bc2664018436040 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 23:38:28 -0700 Subject: [PATCH 011/585] Add IGameNetwork (#1284) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- Dalamud/Game/Network/GameNetwork.cs | 18 ++++++----------- Dalamud/Plugin/Services/IGameNetwork.cs | 26 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 Dalamud/Plugin/Services/IGameNetwork.cs diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index d1fc0bfba..2b6630c8b 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -5,6 +5,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -16,7 +17,10 @@ namespace Dalamud.Game.Network; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class GameNetwork : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork { private readonly GameNetworkAddressResolver address; private readonly Hook processZonePacketDownHook; @@ -47,16 +51,6 @@ public sealed class GameNetwork : IDisposable, IServiceType this.processZonePacketUpHook = Hook.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour); } - /// - /// The delegate type of a network message event. - /// - /// The pointer to the raw data. - /// The operation ID code. - /// The source actor ID. - /// The taret actor ID. - /// The direction of the packed. - public delegate void OnNetworkMessageDelegate(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void ProcessZonePacketDownDelegate(IntPtr a, uint targetId, IntPtr dataPtr); @@ -66,7 +60,7 @@ public sealed class GameNetwork : IDisposable, IServiceType /// /// Event that is called when a network message is sent/received. /// - public event OnNetworkMessageDelegate NetworkMessage; + public event IGameNetwork.OnNetworkMessageDelegate NetworkMessage; /// /// Dispose of managed and unmanaged resources. diff --git a/Dalamud/Plugin/Services/IGameNetwork.cs b/Dalamud/Plugin/Services/IGameNetwork.cs new file mode 100644 index 000000000..eed79b4af --- /dev/null +++ b/Dalamud/Plugin/Services/IGameNetwork.cs @@ -0,0 +1,26 @@ +using Dalamud.Game.Network; + +namespace Dalamud.Plugin.Services; + +/// +/// This class handles interacting with game network events. +/// +public interface IGameNetwork +{ + // TODO(v9): we shouldn't be passing pointers to the actual data here + + /// + /// The delegate type of a network message event. + /// + /// The pointer to the raw data. + /// The operation ID code. + /// The source actor ID. + /// The taret actor ID. + /// The direction of the packed. + public delegate void OnNetworkMessageDelegate(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction); + + /// + /// Event that is called when a network message is sent/received. + /// + public event OnNetworkMessageDelegate NetworkMessage; +} From c991e1f1d3e9a6310a665f598c908e5fb5cf26f1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 29 Jul 2023 21:09:07 +0200 Subject: [PATCH 012/585] refactor: make IsDataReady internal --- Dalamud/Data/DataManager.cs | 8 +++++--- Dalamud/Plugin/Services/IDataManager.cs | 5 ----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 407a1b0da..791ba2158 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -131,6 +131,11 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager } } + /// + /// Gets a value indicating whether Game Data is ready to be read. + /// + internal bool IsDataReady { get; private set; } + /// public ClientLanguage Language { get; private set; } @@ -147,9 +152,6 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager /// public ExcelModule Excel => this.GameData.Excel; - /// - public bool IsDataReady { get; private set; } - /// public bool HasModifiedGameDataFiles { get; private set; } diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index fa8c5bf43..4eac646ad 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -38,11 +38,6 @@ public interface IDataManager /// public ExcelModule Excel { get; } - /// - /// Gets a value indicating whether Game Data is ready to be read. - /// - public bool IsDataReady { get; } - /// /// Gets a value indicating whether the game data files have been modified by another third-party tool. /// From 0f30b8240c2cd3ba80e83c66b4b30180674c81e1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 1 Aug 2023 18:40:00 +0200 Subject: [PATCH 013/585] feat: append payloads to SeStringBuilder --- Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs index 36bb10a2d..1e3449618 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs @@ -1,3 +1,6 @@ +using System.Collections.Generic; +using System.Linq; + using Dalamud.Game.Text.SeStringHandling.Payloads; namespace Dalamud.Game.Text.SeStringHandling; @@ -30,6 +33,13 @@ public class SeStringBuilder /// The current builder. public SeStringBuilder Append(string text) => this.AddText(text); + /// + /// Append payloads to the builder. + /// + /// A list of payloads. + /// The current builder. + public SeStringBuilder Append(IEnumerable payloads) => this.Append(new SeString(payloads.ToList())); + /// /// Append raw text to the builder. /// From 982755c4a2858e9abbf662e0789884acfa050a8e Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 1 Aug 2023 18:42:56 +0200 Subject: [PATCH 014/585] fix: reset colors in SeString.CreateItemLink --- Dalamud/Game/Text/SeStringHandling/SeString.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 6d0c8b0fb..e0cb67cff 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -211,8 +211,8 @@ public class SeString // arrow goes here new TextPayload(displayName), RawPayload.LinkTerminator, - // sometimes there is another set of uiglow/foreground off payloads here - // might be necessary when including additional text after the item name + UIGlowPayload.UIGlowOff, + UIForegroundPayload.UIForegroundOff, }); payloads.InsertRange(3, TextArrowPayloads); From 3f78df23e3a96f2118ac0801c2748c2641135f49 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 1 Aug 2023 18:44:45 +0200 Subject: [PATCH 015/585] feat: item rarity color in SeString.CreateItemLink --- .../Game/Text/SeStringHandling/SeString.cs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index e0cb67cff..c06cdc6b0 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -171,6 +171,7 @@ public class SeString var data = Service.Get(); var displayName = displayNameOverride; + var rarity = 1; // default: white if (displayName == null) { switch (kind) @@ -178,7 +179,9 @@ public class SeString case ItemPayload.ItemKind.Normal: case ItemPayload.ItemKind.Collectible: case ItemPayload.ItemKind.Hq: - displayName = data.GetExcelSheet()?.GetRow(itemId)?.Name; + var item = data.GetExcelSheet()?.GetRow(itemId); + displayName = item?.Name; + rarity = item.Rarity; break; case ItemPayload.ItemKind.EventItem: displayName = data.GetExcelSheet()?.GetRow(itemId)?.Name; @@ -202,21 +205,19 @@ public class SeString displayName += $" {(char)SeIconChar.Collectible}"; } - // TODO: probably a cleaner way to build these than doing the bulk+insert - var payloads = new List(new Payload[] - { - new UIForegroundPayload(0x0225), - new UIGlowPayload(0x0226), - new ItemPayload(itemId, kind), - // arrow goes here - new TextPayload(displayName), - RawPayload.LinkTerminator, - UIGlowPayload.UIGlowOff, - UIForegroundPayload.UIForegroundOff, - }); - payloads.InsertRange(3, TextArrowPayloads); + var textColor = (ushort)(549 + ((rarity - 1) * 2)); + var textGlowColor = (ushort)(textColor + 1); - return new SeString(payloads); + return new SeStringBuilder() + .Add(new ItemPayload(itemId, kind)) + .Append(TextArrowPayloads) + .AddUiForeground(textColor) + .AddUiGlow(textGlowColor) + .AddText(displayName) + .AddUiGlowOff() + .AddUiForegroundOff() + .Add(RawPayload.LinkTerminator) + .Build(); } /// From 08100ef572c46d148e5bd60f43fbaf2f0a531f89 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 1 Aug 2023 18:57:00 +0200 Subject: [PATCH 016/585] fix: add a full item link to SeStringBuilder This changes the behaviour of `AddItemLink` functions. Previously it just added an `ItemPayload`. Now, it adds a full item link, as one would expect. --- Dalamud/Game/Text/SeStringHandling/SeString.cs | 1 + Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index c06cdc6b0..d7d1784f4 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -208,6 +208,7 @@ public class SeString var textColor = (ushort)(549 + ((rarity - 1) * 2)); var textGlowColor = (ushort)(textColor + 1); + // Note: `SeStringBuilder.AddItemLink` uses this function, so don't call it here! return new SeStringBuilder() .Add(new ItemPayload(itemId, kind)) .Append(TextArrowPayloads) diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs index 1e3449618..5b6a83f61 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs @@ -114,7 +114,7 @@ public class SeStringBuilder /// Override for the item's name. /// The current builder. public SeStringBuilder AddItemLink(uint itemId, bool isHq, string? itemNameOverride = null) => - this.Add(new ItemPayload(itemId, isHq, itemNameOverride)); + this.Append(SeString.CreateItemLink(itemId, isHq, itemNameOverride)); /// /// Add an item link to the builder. @@ -124,7 +124,7 @@ public class SeStringBuilder /// Override for the item's name. /// The current builder. public SeStringBuilder AddItemLink(uint itemId, ItemPayload.ItemKind kind, string? itemNameOverride = null) => - this.Add(new ItemPayload(itemId, kind, itemNameOverride)); + this.Append(SeString.CreateItemLink(itemId, kind, itemNameOverride)); /// /// Add an item link to the builder. From e47bfc7acaca9d6a2b4f0f6d7038f7520237ec8a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 1 Aug 2023 18:58:07 +0200 Subject: [PATCH 017/585] docs: reminder to add a LinkTerminator --- Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs index 5b6a83f61..5d7bcce1d 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs @@ -131,6 +131,7 @@ public class SeStringBuilder /// /// The raw item ID. /// The current builder. + /// To terminate this item link, add a . public SeStringBuilder AddItemLinkRaw(uint rawItemId) => this.Add(ItemPayload.FromRaw(rawItemId)); From dd845a30f0cfb7dc109846ba9bfcda313028264a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 1 Aug 2023 20:40:42 +0200 Subject: [PATCH 018/585] fix: correct order of CreateItemLink payloads --- Dalamud/Game/Text/SeStringHandling/SeString.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index d7d1784f4..207f65287 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -210,10 +210,10 @@ public class SeString // Note: `SeStringBuilder.AddItemLink` uses this function, so don't call it here! return new SeStringBuilder() - .Add(new ItemPayload(itemId, kind)) - .Append(TextArrowPayloads) .AddUiForeground(textColor) .AddUiGlow(textGlowColor) + .Add(new ItemPayload(itemId, kind)) + .Append(TextArrowPayloads) .AddText(displayName) .AddUiGlowOff() .AddUiForegroundOff() From d6555007ce16745b538a5f960a3771c497168e14 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 1 Aug 2023 20:56:11 +0200 Subject: [PATCH 019/585] fix: adjust TextArrowPayloads based on language --- .../Game/Text/SeStringHandling/SeString.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 207f65287..12cc94c3d 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -52,14 +52,27 @@ public class SeString /// with the appropriate glow and coloring. /// /// A list of all the payloads required to insert the link marker. - public static IEnumerable TextArrowPayloads => new List(new Payload[] + public static IEnumerable TextArrowPayloads { - new UIForegroundPayload(0x01F4), - new UIGlowPayload(0x01F5), - new TextPayload($"{(char)SeIconChar.LinkMarker}"), - UIGlowPayload.UIGlowOff, - UIForegroundPayload.UIForegroundOff, - }); + get + { + var clientState = Service.Get(); + var markerSpace = clientState.ClientLanguage switch + { + ClientLanguage.German => " ", + ClientLanguage.French => " ", + _ => string.Empty, + }; + return new List + { + new UIForegroundPayload(500), + new UIGlowPayload(501), + new TextPayload($"{(char)SeIconChar.LinkMarker}{markerSpace}"), + UIGlowPayload.UIGlowOff, + UIForegroundPayload.UIForegroundOff, + }; + } + } /// /// Gets an empty SeString. From 038de41592cb2e3cd0b43f664fb2289a227cc80d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 1 Aug 2023 21:38:33 +0200 Subject: [PATCH 020/585] fix: adjust CoordinateString based on language --- .../Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs index 50945a7ce..667b52e36 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/MapLinkPayload.cs @@ -130,7 +130,13 @@ public class MapLinkPayload : Payload var y = Math.Truncate((this.YCoord + fudge) * 10.0f) / 10.0f; // the formatting and spacing the game uses - return $"( {x:0.0} , {y:0.0} )"; + var clientState = Service.Get(); + return clientState.ClientLanguage switch + { + ClientLanguage.German => $"( {x:0.0}, {y:0.0} )", + ClientLanguage.Japanese => $"({x:0.0}, {y:0.0})", + _ => $"( {x:0.0} , {y:0.0} )", + }; } } From 5b6c90f122da0cf3b7bf3f20d103a2752fbf3e3b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 1 Aug 2023 21:55:51 +0200 Subject: [PATCH 021/585] feat: default ItemKind param for AddItemLink --- Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs index 5d7bcce1d..1fda9f9ae 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs @@ -123,7 +123,7 @@ public class SeStringBuilder /// Kind of item to encode. /// Override for the item's name. /// The current builder. - public SeStringBuilder AddItemLink(uint itemId, ItemPayload.ItemKind kind, string? itemNameOverride = null) => + public SeStringBuilder AddItemLink(uint itemId, ItemPayload.ItemKind kind = ItemPayload.ItemKind.Normal, string? itemNameOverride = null) => this.Append(SeString.CreateItemLink(itemId, kind, itemNameOverride)); /// From a78007b05d92bf0713672ce5dfebc5619b12334d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 3 Aug 2023 00:35:43 +0200 Subject: [PATCH 022/585] fix: null check item rarity --- Dalamud/Game/Text/SeStringHandling/SeString.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 12cc94c3d..2ddb73f12 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -194,7 +194,7 @@ public class SeString case ItemPayload.ItemKind.Hq: var item = data.GetExcelSheet()?.GetRow(itemId); displayName = item?.Name; - rarity = item.Rarity; + rarity = item?.Rarity ?? 1; break; case ItemPayload.ItemKind.EventItem: displayName = data.GetExcelSheet()?.GetRow(itemId)?.Name; From 758ae7c0979ec176a2cc98c6f9cd906d31c723de Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 3 Aug 2023 20:32:31 +0200 Subject: [PATCH 023/585] Remove texture-related IDataManager functions --- Dalamud/Data/DataManager.cs | 155 ------------------ .../Interface/Internal/InterfaceManager.cs | 4 +- Dalamud/Interface/Internal/TextureManager.cs | 24 ++- Dalamud/Plugin/Services/IDataManager.cs | 107 ------------ 4 files changed, 22 insertions(+), 268 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 831b25cbc..8c8a2de29 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -36,9 +36,6 @@ namespace Dalamud.Data; #pragma warning restore SA1015 public sealed class DataManager : IDisposable, IServiceType, IDataManager { - private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; - private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; - private readonly Thread luminaResourceThread; private readonly CancellationTokenSource luminaCancellationTokenSource; @@ -185,158 +182,6 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager public bool FileExists(string path) => this.GameData.FileExists(path); - /// - /// Get a containing the icon with the given ID. - /// - /// The icon ID. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(uint iconId) - => this.GetIcon(this.Language, iconId, false); - - /// - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(uint iconId, bool highResolution) - => this.GetIcon(this.Language, iconId, highResolution); - - /// - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(bool isHq, uint iconId) - { - var type = isHq ? "hq/" : string.Empty; - return this.GetIcon(type, iconId); - } - - /// - /// Get a containing the icon with the given ID, of the given language. - /// - /// The requested language. - /// The icon ID. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId) - => this.GetIcon(iconLanguage, iconId, false); - - /// - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution) - { - var type = iconLanguage switch - { - ClientLanguage.Japanese => "ja/", - ClientLanguage.English => "en/", - ClientLanguage.German => "de/", - ClientLanguage.French => "fr/", - _ => throw new ArgumentOutOfRangeException(nameof(iconLanguage), $"Unknown Language: {iconLanguage}"), - }; - - return this.GetIcon(type, iconId, highResolution); - } - - /// - /// Get a containing the icon with the given ID, of the given type. - /// - /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). - /// The icon ID. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(string? type, uint iconId) - => this.GetIcon(type, iconId, false); - - /// - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(string? type, uint iconId, bool highResolution) - { - var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; - - type ??= string.Empty; - if (type.Length > 0 && !type.EndsWith("/")) - type += "/"; - - var filePath = string.Format(format, iconId / 1000, type, iconId); - var file = this.GetFile(filePath); - - if (type == string.Empty || file != default) - return file; - - // Couldn't get specific type, try for generic version. - filePath = string.Format(format, iconId / 1000, string.Empty, iconId); - file = this.GetFile(filePath); - return file; - } - - /// - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetHqIcon(uint iconId) - => this.GetIcon(true, iconId); - - /// - [Obsolete("Use ITextureProvider instead")] - [return: NotNullIfNotNull(nameof(tex))] - public TextureWrap? GetImGuiTexture(TexFile? tex) - { - if (tex is null) - return null; - - var im = Service.Get(); - var buffer = tex.TextureBuffer; - var bpp = 1 << (((int)tex.Header.Format & (int)TexFile.TextureFormat.BppMask) >> - (int)TexFile.TextureFormat.BppShift); - - var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(tex.Header.Format, false); - if (conversion != TexFile.DxgiFormatConversion.NoConversion || !im.SupportsDxgiFormat((Format)dxgiFormat)) - { - dxgiFormat = (int)Format.B8G8R8A8_UNorm; - buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); - bpp = 32; - } - - var pitch = buffer is BlockCompressionTextureBuffer - ? Math.Max(1, (buffer.Width + 3) / 4) * 2 * bpp - : ((buffer.Width * bpp) + 7) / 8; - return im.LoadImageFromDxgiFormat(buffer.RawData, pitch, buffer.Width, buffer.Height, (Format)dxgiFormat); - } - - /// - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTexture(string path) - => this.GetImGuiTexture(this.GetFile(path)); - - /// - /// Get a containing the icon with the given ID. - /// - /// The icon ID. - /// The containing the icon. - /// TODO(v9): remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution) - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureIcon(uint iconId) - => this.GetImGuiTexture(this.GetIcon(iconId, false)); - - /// - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution) - => this.GetImGuiTexture(this.GetIcon(iconId, highResolution)); - - /// - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId) - => this.GetImGuiTexture(this.GetIcon(isHq, iconId)); - - /// - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId) - => this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId)); - - /// - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureIcon(string type, uint iconId) - => this.GetImGuiTexture(this.GetIcon(type, iconId)); - - /// - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureHqIcon(uint iconId) - => this.GetImGuiTexture(this.GetHqIcon(iconId)); - #endregion /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 841511f55..ad1e514c7 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -325,7 +325,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// The height in pixels. /// Format of the texture. /// A texture, ready to use in ImGui. - public TextureWrap LoadImageFromDxgiFormat(Span data, int pitch, int width, int height, Format dxgiFormat) + public DalamudTextureWrap LoadImageFromDxgiFormat(Span data, int pitch, int width, int height, Format dxgiFormat) { if (this.scene == null) throw new InvalidOperationException("Scene isn't ready."); @@ -360,7 +360,7 @@ internal class InterfaceManager : IDisposable, IServiceType } // no sampler for now because the ImGui implementation we copied doesn't allow for changing it - return new D3DTextureWrap(resView, width, height); + return new DalamudTextureWrap(new D3DTextureWrap(resView, width, height)); } #nullable restore diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 4b2f1f362..de5613eed 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -13,6 +13,8 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using ImGuiScene; using Lumina.Data.Files; +using Lumina.Data.Parsing.Tex.Buffers; +using SharpDX.DXGI; namespace Dalamud.Interface.Internal; @@ -207,10 +209,24 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP if (!this.im.IsReady) throw new InvalidOperationException("Cannot create textures before scene is ready"); - -#pragma warning disable CS0618 - return this.dataManager.GetImGuiTexture(file) as IDalamudTextureWrap; -#pragma warning restore CS0618 + + var buffer = file.TextureBuffer; + var bpp = 1 << (((int)file.Header.Format & (int)TexFile.TextureFormat.BppMask) >> + (int)TexFile.TextureFormat.BppShift); + + var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); + if (conversion != TexFile.DxgiFormatConversion.NoConversion || !im.SupportsDxgiFormat((Format)dxgiFormat)) + { + dxgiFormat = (int)Format.B8G8R8A8_UNorm; + buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); + bpp = 32; + } + + var pitch = buffer is BlockCompressionTextureBuffer + ? Math.Max(1, (buffer.Width + 3) / 4) * 2 * bpp + : ((buffer.Width * bpp) + 7) / 8; + + return this.im.LoadImageFromDxgiFormat(buffer.RawData, pitch, buffer.Width, buffer.Height, (Format)dxgiFormat); } /// diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index a47303ea6..3ae10b0c7 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -81,111 +81,4 @@ public interface IDataManager /// The path inside of the game files. /// True if the file exists. public bool FileExists(string path); - - /// - /// Get a containing the icon with the given ID. - /// - /// The icon ID. - /// Return high resolution version. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(uint iconId, bool highResolution = false); - - /// - /// Get a containing the icon with the given ID, of the given language. - /// - /// The requested language. - /// The icon ID. - /// Return high resolution version. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false); - - /// - /// Get a containing the icon with the given ID, of the given type. - /// - /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). - /// The icon ID. - /// Return high resolution version. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false); - - /// - /// Get a containing the icon with the given ID. - /// - /// The icon ID. - /// Return the high resolution version. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false); - - /// - /// Get a containing the icon with the given ID, of the given quality. - /// - /// A value indicating whether the icon should be HQ. - /// The icon ID. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetIcon(bool isHq, uint iconId); - - /// - /// Get a containing the HQ icon with the given ID. - /// - /// The icon ID. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TexFile? GetHqIcon(uint iconId); - - /// - /// Get the passed as a drawable ImGui TextureWrap. - /// - /// The Lumina . - /// A that can be used to draw the texture. - [Obsolete("Use ITextureProvider instead")] - [return: NotNullIfNotNull(nameof(tex))] - public TextureWrap? GetImGuiTexture(TexFile? tex); - - /// - /// Get the passed texture path as a drawable ImGui TextureWrap. - /// - /// The internal path to the texture. - /// A that can be used to draw the texture. - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTexture(string path); - - /// - /// Get a containing the icon with the given ID, of the given quality. - /// - /// A value indicating whether the icon should be HQ. - /// The icon ID. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId); - - /// - /// Get a containing the icon with the given ID, of the given language. - /// - /// The requested language. - /// The icon ID. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId); - - /// - /// Get a containing the icon with the given ID, of the given type. - /// - /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). - /// The icon ID. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureIcon(string type, uint iconId); - - /// - /// Get a containing the HQ icon with the given ID. - /// - /// The icon ID. - /// The containing the icon. - [Obsolete("Use ITextureProvider instead")] - public TextureWrap? GetImGuiTextureHqIcon(uint iconId); } From 02e1f2502e5031e5c2897c04d931643014c99390 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 4 Aug 2023 19:36:09 +0200 Subject: [PATCH 024/585] refactor: move Dalamud.Interface utils into main assembly, warnings pass --- .editorconfig | 2 +- Dalamud.Interface/Dalamud.Interface.csproj | 17 ---- Dalamud.Interface/ImGuiTable.cs | 41 --------- Dalamud.Interface/InterfaceHelpers.cs | 6 -- Dalamud.sln | 14 --- Dalamud/Dalamud.csproj | 1 - Dalamud/Data/DataManager.cs | 18 ++-- Dalamud/Game/Config/GameConfig.cs | 3 +- Dalamud/Game/Config/GameConfigSection.cs | 13 ++- Dalamud/Game/Gui/GameGui.cs | 1 + Dalamud/GlobalSuppressions.cs | 19 ++++ Dalamud/GlobalUsings.cs | 1 + .../ImGuiComponents.ColorPickerWithPalette.cs | 1 + Dalamud/Interface/DragDrop/DragDropInterop.cs | 4 +- Dalamud/Interface/DragDrop/DragDropManager.cs | 2 + Dalamud/Interface/DragDrop/DragDropTarget.cs | 6 +- .../Interface/DragDrop/IDragDropManager.cs | 13 ++- .../Interface/GameFonts/GameFontManager.cs | 3 +- .../ImGuiFileDialog/FileDialog.UI.cs | 1 + .../Interface/Internal/DalamudInterface.cs | 3 +- .../Interface/Internal/InterfaceManager.cs | 1 + .../Notifications/NotificationManager.cs | 1 + Dalamud/Interface/Internal/TextureManager.cs | 2 +- Dalamud/Interface/Internal/UiDebug.cs | 1 + .../Internal/Windows/BranchSwitcherWindow.cs | 1 + .../Internal/Windows/ChangelogWindow.cs | 1 + .../Internal/Windows/ComponentDemoWindow.cs | 1 + .../Internal/Windows/ConsoleWindow.cs | 1 + .../Internal/Windows/Data/DataWindow.cs | 1 + .../Windows/Data/Widgets/DataShareWidget.cs | 3 +- .../Data/Widgets/FontAwesomeTestWidget.cs | 4 +- .../Data/Widgets/NetworkMonitorWidget.cs | 3 +- .../Windows/Data/Widgets/TargetWidget.cs | 1 + .../Data/Widgets/TaskSchedulerWidget.cs | 1 + .../Windows/Data/Widgets/TexWidget.cs | 4 +- .../Windows/Data/Widgets/ToastWidget.cs | 1 + .../Windows/GamepadModeNotifierWindow.cs | 1 + .../PluginInstaller/PluginInstallerWindow.cs | 3 +- .../PluginInstaller/ProfileManagerWidget.cs | 3 +- .../Internal/Windows/ProfilerWindow.cs | 1 + .../Windows/SelfTest/SelfTestWindow.cs | 1 + .../Internal/Windows/Settings/SettingsTab.cs | 5 +- .../Windows/Settings/SettingsWindow.cs | 3 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 3 +- .../Windows/Settings/Tabs/SettingsTabDtr.cs | 1 + .../Settings/Tabs/SettingsTabExperimental.cs | 1 + .../Windows/Settings/Tabs/SettingsTabLook.cs | 1 + .../Settings/Widgets/ButtonSettingsEntry.cs | 1 + .../Widgets/DevPluginsSettingsEntry.cs | 3 +- .../Settings/Widgets/GapSettingsEntry.cs | 1 + .../Settings/Widgets/HintSettingsEntry.cs | 1 + .../Widgets/LanguageChooserSettingsEntry.cs | 1 + .../Settings/Widgets/SettingsEntry{T}.cs | 3 +- .../Widgets/ThirdRepoSettingsEntry.cs | 3 +- .../Windows/StyleEditor/StyleEditorWindow.cs | 1 + .../Internal/Windows/TitleScreenMenuWindow.cs | 3 +- .../Interface/Utility}/ImGuiClip.cs | 8 +- .../{ => Utility}/ImGuiExtensions.cs | 3 +- .../Interface/{ => Utility}/ImGuiHelpers.cs | 6 +- Dalamud/Interface/Utility/ImGuiTable.cs | 56 ++++++++++++ .../Interface/Utility}/Raii/Color.cs | 17 ++-- .../Interface/Utility}/Raii/EndObjects.cs | 42 +++++---- .../Interface/Utility}/Raii/Font.cs | 14 +-- .../Interface/Utility}/Raii/Id.cs | 16 ++-- .../Interface/Utility}/Raii/Indent.cs | 6 +- .../Interface/Utility}/Raii/Style.cs | 19 ++-- .../Interface/Utility}/Table/Column.cs | 5 +- .../Interface/Utility}/Table/ColumnFlags.cs | 13 +-- .../Interface/Utility}/Table/ColumnSelect.cs | 22 ++--- .../Interface/Utility}/Table/ColumnString.cs | 17 ++-- .../Interface/Utility}/Table/Table.cs | 88 ++++++++++--------- Dalamud/Interface/Windowing/Window.cs | 2 + Dalamud/Plugin/Internal/PluginManager.cs | 17 ++-- Dalamud/Plugin/Services/IGameConfig.cs | 6 +- Dalamud/Plugin/Services/IKeyState.cs | 5 +- .../Utility}/ArrayExtensions.cs | 9 +- Dalamud/Utility/FuzzyMatcher.cs | 68 ++++++++------ .../Utility}/StableInsertionSortExtension.cs | 20 ++++- Dalamud/Utility/Util.cs | 1 + targets/Dalamud.Plugin.targets | 1 - 80 files changed, 394 insertions(+), 303 deletions(-) delete mode 100644 Dalamud.Interface/Dalamud.Interface.csproj delete mode 100644 Dalamud.Interface/ImGuiTable.cs delete mode 100644 Dalamud.Interface/InterfaceHelpers.cs create mode 100644 Dalamud/GlobalUsings.cs rename {Dalamud.Interface => Dalamud/Interface/Utility}/ImGuiClip.cs (97%) rename Dalamud/Interface/{ => Utility}/ImGuiExtensions.cs (98%) rename Dalamud/Interface/{ => Utility}/ImGuiHelpers.cs (99%) create mode 100644 Dalamud/Interface/Utility/ImGuiTable.cs rename {Dalamud.Interface => Dalamud/Interface/Utility}/Raii/Color.cs (83%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Raii/EndObjects.cs (94%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Raii/Font.cs (81%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Raii/Id.cs (82%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Raii/Indent.cs (91%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Raii/Style.cs (95%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Table/Column.cs (92%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Table/ColumnFlags.cs (89%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Table/ColumnSelect.cs (66%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Table/ColumnString.cs (75%) rename {Dalamud.Interface => Dalamud/Interface/Utility}/Table/Table.cs (64%) rename {Dalamud.Interface => Dalamud/Utility}/ArrayExtensions.cs (80%) rename {Dalamud.Interface => Dalamud/Utility}/StableInsertionSortExtension.cs (57%) diff --git a/.editorconfig b/.editorconfig index 829de8f05..0e4f800e0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -35,7 +35,7 @@ dotnet_naming_rule.private_instance_fields_rule.severity = warning dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols dotnet_naming_rule.private_static_fields_rule.severity = warning -dotnet_naming_rule.private_static_fields_rule.style = upper_camel_case_style +dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols dotnet_naming_rule.private_static_readonly_rule.severity = warning dotnet_naming_rule.private_static_readonly_rule.style = upper_camel_case_style diff --git a/Dalamud.Interface/Dalamud.Interface.csproj b/Dalamud.Interface/Dalamud.Interface.csproj deleted file mode 100644 index 1dd8468be..000000000 --- a/Dalamud.Interface/Dalamud.Interface.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net7.0-windows - x64 - x64;AnyCPU - enable - enable - true - Dalamud.Interface - - - - - - - diff --git a/Dalamud.Interface/ImGuiTable.cs b/Dalamud.Interface/ImGuiTable.cs deleted file mode 100644 index 5ea6a2c9a..000000000 --- a/Dalamud.Interface/ImGuiTable.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Dalamud.Interface.Raii; -using ImGuiNET; - -namespace Dalamud.Interface; - -public static class ImGuiTable -{ - // Draw a simple table with the given data using the drawRow action. - // Headers and thus columns and column count are defined by columnTitles. - public static void DrawTable(string label, IEnumerable data, Action drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None, - params string[] columnTitles) - { - if (columnTitles.Length == 0) - return; - - using var table = ImRaii.Table(label, columnTitles.Length, flags); - if (!table) - return; - - foreach (var title in columnTitles) - { - ImGui.TableNextColumn(); - ImGui.TableHeader(title); - } - - foreach (var datum in data) - { - ImGui.TableNextRow(); - drawRow(datum); - } - } - - // Draw a simple table with the given data using the drawRow action inside a collapsing header. - // Headers and thus columns and column count are defined by columnTitles. - public static void DrawTabbedTable(string label, IEnumerable data, Action drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None, - params string[] columnTitles) - { - if (ImGui.CollapsingHeader(label)) - DrawTable($"{label}##Table", data, drawRow, flags, columnTitles); - } -} diff --git a/Dalamud.Interface/InterfaceHelpers.cs b/Dalamud.Interface/InterfaceHelpers.cs deleted file mode 100644 index 26f09bedb..000000000 --- a/Dalamud.Interface/InterfaceHelpers.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Dalamud.Interface; - -public static class InterfaceHelpers -{ - public static float GlobalScale = 1.0f; -} diff --git a/Dalamud.sln b/Dalamud.sln index 20442e52d..443f38496 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -38,8 +38,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.InteropS EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "DalamudCrashHandler\DalamudCrashHandler.vcxproj", "{317A264C-920B-44A1-8A34-F3A6827B0705}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Interface", "Dalamud.Interface\Dalamud.Interface.csproj", "{757C997D-AA58-4241-8299-243C56514917}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -204,18 +202,6 @@ Global {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.Build.0 = Release|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.ActiveCfg = Release|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.Build.0 = Release|x64 - {757C997D-AA58-4241-8299-243C56514917}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Debug|Any CPU.Build.0 = Debug|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Debug|x64.ActiveCfg = Debug|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Debug|x64.Build.0 = Debug|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Debug|x86.ActiveCfg = Debug|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Debug|x86.Build.0 = Debug|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Release|Any CPU.ActiveCfg = Release|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Release|Any CPU.Build.0 = Release|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Release|x64.ActiveCfg = Release|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Release|x64.Build.0 = Release|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Release|x86.ActiveCfg = Release|Any CPU - {757C997D-AA58-4241-8299-243C56514917}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 6e6a01fa9..b147dc961 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -87,7 +87,6 @@ - diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 8c8a2de29..fb167283f 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -1,27 +1,19 @@ -using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; -using Dalamud.Interface.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; -using Dalamud.Utility; using Dalamud.Utility.Timing; -using ImGuiScene; using JetBrains.Annotations; using Lumina; using Lumina.Data; -using Lumina.Data.Files; -using Lumina.Data.Parsing.Tex.Buffers; using Lumina.Excel; using Newtonsoft.Json; using Serilog; -using SharpDX.DXGI; namespace Dalamud.Data; @@ -131,11 +123,6 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager } } - /// - /// Gets a value indicating whether Game Data is ready to be read. - /// - internal bool IsDataReady { get; private set; } - /// public ClientLanguage Language { get; private set; } @@ -155,6 +142,11 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager /// public bool HasModifiedGameDataFiles { get; private set; } + /// + /// Gets a value indicating whether Game Data is ready to be read. + /// + internal bool IsDataReady { get; private set; } + #region Lumina Wrappers /// diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index dfdb8b5d2..49d24c2a5 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -1,5 +1,4 @@ -using System; -using Dalamud.Hooking; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs index 6c87ad3cf..ea79a7fc8 100644 --- a/Dalamud/Game/Config/GameConfigSection.cs +++ b/Dalamud/Game/Config/GameConfigSection.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Diagnostics; using Dalamud.Memory; @@ -18,11 +17,6 @@ public class GameConfigSection private readonly ConcurrentDictionary indexMap = new(); private readonly ConcurrentDictionary enumMap = new(); - /// - /// Event which is fired when a game config option is changed within the section. - /// - public event EventHandler Changed; - /// /// Initializes a new instance of the class. /// @@ -54,6 +48,11 @@ public class GameConfigSection /// Pointer to unmanaged ConfigBase. internal unsafe delegate ConfigBase* GetConfigBaseDelegate(); + /// + /// Event which is fired when a game config option is changed within the section. + /// + public event EventHandler? Changed; + /// /// Gets the number of config entries contained within the section. /// Some entries may be empty with no data. diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 0235bef5a..3954954a3 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -5,6 +5,7 @@ using System.Runtime.InteropServices; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; using Dalamud.Interface; +using Dalamud.Interface.Utility; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; diff --git a/Dalamud/GlobalSuppressions.cs b/Dalamud/GlobalSuppressions.cs index 7426ed5c8..1b869295b 100644 --- a/Dalamud/GlobalSuppressions.cs +++ b/Dalamud/GlobalSuppressions.cs @@ -15,3 +15,22 @@ using System.Diagnostics.CodeAnalysis; [assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1127:Generic type constraints should be on their own line", Justification = "I like this better")] [assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1028:Code should not contain trailing whitespace", Justification = "I don't care anymore")] [assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1633:File should have header", Justification = "We don't do those yet")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1117:ParametersMustBeOnSameLineOrSeparateLines", Justification = "I don't care anymore")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1117:ParametersMustBeOnSameLineOrSeparateLines", Justification = "I don't care anymore")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1407:ArithmeticExpressionsMustDeclarePrecedence", Justification = "I don't care anymore")] +[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1116:SplitParametersMustStartOnLineAfterDeclaration", Justification = "Reviewed.")] + +// ImRAII stuff +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:PartialElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Table")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:PartialElementsMustBeDocumented", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Table")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:ElementsMustBeDocumented", Justification = "Reviewed.", Scope = "type", Target = "Dalamud.Interface.Utility.ImGuiClip")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1601:PartialElementsMustBeDocumented", Justification = "Reviewed.", Scope = "type", Target = "Dalamud.Interface.Utility.ImGuiClip")] +[assembly: SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:CodeMustNotContainMultipleWhitespaceInARow", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] +[assembly: SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:FieldsMustBePrivate", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Table")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:ElementsMustAppearInTheCorrectOrder", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:ElementsMustAppearInTheCorrectOrder", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Table")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Raii")] +[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1202:ElementsMustBeOrderedByAccess", Justification = "Reviewed.", Scope = "namespaceanddescendants", Target = "Dalamud.Interface.Utility.Table")] diff --git a/Dalamud/GlobalUsings.cs b/Dalamud/GlobalUsings.cs new file mode 100644 index 000000000..062a3f981 --- /dev/null +++ b/Dalamud/GlobalUsings.cs @@ -0,0 +1 @@ +global using System; diff --git a/Dalamud/Interface/Components/ImGuiComponents.ColorPickerWithPalette.cs b/Dalamud/Interface/Components/ImGuiComponents.ColorPickerWithPalette.cs index e9db345cb..aa707aecb 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.ColorPickerWithPalette.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.ColorPickerWithPalette.cs @@ -1,5 +1,6 @@ using System.Numerics; +using Dalamud.Interface.Utility; using ImGuiNET; namespace Dalamud.Interface.Components; diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs index 28a2644a5..6edd5642e 100644 --- a/Dalamud/Interface/DragDrop/DragDropInterop.cs +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -34,9 +34,9 @@ internal partial class DragDropManager internal struct POINTL { [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] - public int x; + public int X; [ComAliasName("Microsoft.VisualStudio.OLE.Interop.LONG")] - public int y; + public int Y; } private static class DragDropInterop diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 8336edc11..e8641035f 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -16,7 +16,9 @@ namespace Dalamud.Interface.DragDrop; /// [PluginInterface] [ServiceManager.EarlyLoadedService] +#pragma warning disable SA1015 [ResolveVia] +#pragma warning restore SA1015 internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType { private nint windowHandlePtr = nint.Zero; diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index 5e7166fb3..628f1100c 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -51,7 +51,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget this.Extensions = this.Files.Select(Path.GetExtension).Where(p => !p.IsNullOrEmpty()).Distinct().ToHashSet(); } - Log.Debug("[DragDrop] Entering external Drag and Drop with {KeyState} at {PtX}, {PtY} and with {N} files.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y, this.Files.Count + this.Directories.Count); + Log.Debug("[DragDrop] Entering external Drag and Drop with {KeyState} at {PtX}, {PtY} and with {N} files.", (DragDropInterop.ModifierKeys)grfKeyState, pt.X, pt.Y, this.Files.Count + this.Directories.Count); } /// Invoked every windows update-frame as long as the drag and drop process keeps hovering over an FFXIV-related viewport. @@ -67,7 +67,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget this.lastUpdateFrame = frame; this.lastKeyState = UpdateIo((DragDropInterop.ModifierKeys)grfKeyState, false); pdwEffect &= (uint)DragDropInterop.DropEffects.Copy; - Log.Verbose("[DragDrop] External Drag and Drop with {KeyState} at {PtX}, {PtY}.", (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y); + Log.Verbose("[DragDrop] External Drag and Drop with {KeyState} at {PtX}, {PtY}.", (DragDropInterop.ModifierKeys)grfKeyState, pt.X, pt.Y); } } @@ -101,7 +101,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget pdwEffect = 0; } - Log.Debug("[DragDrop] Dropping {N} files with {KeyState} at {PtX}, {PtY}.", this.Files.Count + this.Directories.Count, (DragDropInterop.ModifierKeys)grfKeyState, pt.x, pt.y); + Log.Debug("[DragDrop] Dropping {N} files with {KeyState} at {PtX}, {PtY}.", this.Files.Count + this.Directories.Count, (DragDropInterop.ModifierKeys)grfKeyState, pt.X, pt.Y); } private static DragDropInterop.ModifierKeys UpdateIo(DragDropInterop.ModifierKeys keys, bool entering) diff --git a/Dalamud/Interface/DragDrop/IDragDropManager.cs b/Dalamud/Interface/DragDrop/IDragDropManager.cs index 736c8af24..a8a0d63b0 100644 --- a/Dalamud/Interface/DragDrop/IDragDropManager.cs +++ b/Dalamud/Interface/DragDrop/IDragDropManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; namespace Dalamud.Interface.DragDrop; @@ -23,20 +22,20 @@ public interface IDragDropManager /// Gets the list of directories currently being dragged from an external application over any of the games viewports. public IReadOnlyList Directories { get; } - /// Create an ImGui drag & drop source that is active only if anything is being dragged from an external source. - /// The label used for the drag & drop payload. + /// Create an ImGui drag and drop source that is active only if anything is being dragged from an external source. + /// The label used for the drag and drop payload. /// A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged. public void CreateImGuiSource(string label, Func validityCheck) => this.CreateImGuiSource(label, validityCheck, _ => false); - /// Create an ImGui drag & drop source that is active only if anything is being dragged from an external source. - /// The label used for the drag & drop payload. + /// Create an ImGui drag and drop source that is active only if anything is being dragged from an external source. + /// The label used for the drag and drop payload. /// A function returning whether the current status is relevant for this source. Checked before creating the source but only if something is being dragged. /// Executes ImGui functions to build a tooltip. Should return true if it creates any tooltip and false otherwise. If multiple sources are active, only the first non-empty tooltip type drawn in a frame will be used. public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder); - /// Create an ImGui drag & drop target on the last ImGui object. - /// The label used for the drag & drop payload. + /// Create an ImGui drag and drop target on the last ImGui object. + /// The label used for the drag and drop payload. /// On success, contains the list of file paths dropped onto the target. /// On success, contains the list of directory paths dropped onto the target. /// True if items were dropped onto the target this frame, false otherwise. diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index ad0e47273..d8130f692 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -9,12 +9,13 @@ using System.Threading.Tasks; using Dalamud.Data; using Dalamud.Game; using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; using Dalamud.Utility.Timing; using ImGuiNET; using Lumina.Data.Files; using Serilog; -using static Dalamud.Interface.ImGuiHelpers; +using static Dalamud.Interface.Utility.ImGuiHelpers; namespace Dalamud.Interface.GameFonts; diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index d3be8da95..0dd1410d5 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Numerics; +using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 479297c20..a7f7e6209 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -21,8 +21,9 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; -using Dalamud.Interface.Raii; using Dalamud.Interface.Style; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Logging.Internal; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index ad1e514c7..794b6c6b3 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -19,6 +19,7 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Style; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Utility; using Dalamud.Utility.Timing; diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs index e941db7a4..9d20d6d3e 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index de5613eed..983ae9963 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -215,7 +215,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP (int)TexFile.TextureFormat.BppShift); var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); - if (conversion != TexFile.DxgiFormatConversion.NoConversion || !im.SupportsDxgiFormat((Format)dxgiFormat)) + if (conversion != TexFile.DxgiFormatConversion.NoConversion || !this.im.SupportsDxgiFormat((Format)dxgiFormat)) { dxgiFormat = (int)Format.B8G8R8A8_UNorm; buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index d1e7a6b78..b1f27828c 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Game.Gui; +using Dalamud.Interface.Utility; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index b599fb58f..05d8d04e8 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Networking.Http; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 05854210e..e61cb400b 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -3,6 +3,7 @@ using System.IO; using System.Numerics; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Utility; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs index 638b30e66..8c5458557 100644 --- a/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ComponentDemoWindow.cs @@ -6,6 +6,7 @@ using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 872fdcd37..bcbad1a21 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -10,6 +10,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 9d8dc1e93..54ff4a5ca 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -5,6 +5,7 @@ using System.Numerics; using Dalamud.Game.Gui; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using ImGuiNET; using Serilog; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index ec7124042..dc18dbd55 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -1,4 +1,5 @@ -using Dalamud.Plugin.Ipc.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Plugin.Ipc.Internal; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 1ed5e9e83..e4284a98e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Numerics; +using Dalamud.Interface.Utility; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 01d0b1759..6f19404ad 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -7,7 +7,8 @@ using System.Text.RegularExpressions; using Dalamud.Data; using Dalamud.Game.Network; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Memory; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index 57fd03300..64ae041ed 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -1,5 +1,6 @@ using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; +using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index 7d91cd154..59ca617f5 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using ImGuiNET; using Serilog; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 5ad5868c3..cc38a58ae 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,8 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; using System.Numerics; +using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; using ImGuiNET; using ImGuiScene; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs index c75230e73..7f020acae 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs @@ -1,6 +1,7 @@ using System.Numerics; using Dalamud.Game.Gui.Toast; +using Dalamud.Interface.Utility; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data; diff --git a/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs index e95c510d3..ff5af1556 100644 --- a/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs +++ b/Dalamud/Interface/Internal/Windows/GamepadModeNotifierWindow.cs @@ -1,6 +1,7 @@ using System.Numerics; using CheapLoc; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 35fa40013..2b0e27673 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -15,8 +15,9 @@ using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Raii; using Dalamud.Interface.Style; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 301e43473..6c17a8522 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -8,7 +8,8 @@ using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Utility; diff --git a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs index 2d0f54912..16f253da9 100644 --- a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Utility.Numerics; using Dalamud.Utility.Timing; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 3e25b6f5a..4a7bb0413 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -6,6 +6,7 @@ using System.Numerics; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs index 16b7749cb..a3ece0d04 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs @@ -1,5 +1,6 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; + +using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Settings; diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 97d9eac5c..4f77c0502 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -5,7 +5,8 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.Settings.Tabs; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; using Dalamud.Utility; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 325d0b8b7..9a7236f2f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -8,7 +8,8 @@ using System.Numerics; using CheapLoc; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs index 85cb8219f..7dd0fa5d1 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabDtr.cs @@ -8,6 +8,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Game.Gui.Dtr; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index 62981f4a2..0a0e2528d 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -6,6 +6,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal; using Dalamud.Utility; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 3e801a8c3..b34a13cc5 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -5,6 +5,7 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; using Serilog; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ButtonSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ButtonSettingsEntry.cs index 9c635fb99..6adddbc82 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ButtonSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ButtonSettingsEntry.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs index 3e73454f3..55deb61bc 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs @@ -11,7 +11,8 @@ using Dalamud.Configuration; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/GapSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/GapSettingsEntry.cs index bc5c2fd0a..1db3c4756 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/GapSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/GapSettingsEntry.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; +using Dalamud.Interface.Utility; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/HintSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/HintSettingsEntry.cs index d1eb43c1f..3edd3ae1d 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/HintSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/HintSettingsEntry.cs @@ -2,6 +2,7 @@ using System.Numerics; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs index 0bb373576..85f8a826f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs @@ -7,6 +7,7 @@ using System.Linq; using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs index 83be6a052..dcbb42089 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/SettingsEntry{T}.cs @@ -7,7 +7,8 @@ using System.Linq; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs index be2e34a57..114de1148 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs @@ -10,7 +10,8 @@ using Dalamud.Configuration; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs index 419361b3b..3a3e871b0 100644 --- a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs +++ b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs @@ -10,6 +10,7 @@ using Dalamud.Data; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Style; +using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using Dalamud.Utility; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 10180f0c3..f11f124cc 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -9,7 +9,8 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using ImGuiNET; using ImGuiScene; diff --git a/Dalamud.Interface/ImGuiClip.cs b/Dalamud/Interface/Utility/ImGuiClip.cs similarity index 97% rename from Dalamud.Interface/ImGuiClip.cs rename to Dalamud/Interface/Utility/ImGuiClip.cs index dc1845a35..e36970885 100644 --- a/Dalamud.Interface/ImGuiClip.cs +++ b/Dalamud/Interface/Utility/ImGuiClip.cs @@ -1,8 +1,11 @@ +using System.Collections.Generic; +using System.Linq; using System.Numerics; -using Dalamud.Interface.Raii; + +using Dalamud.Interface.Utility.Raii; using ImGuiNET; -namespace Dalamud.Interface; +namespace Dalamud.Interface.Utility; public static class ImGuiClip { @@ -132,7 +135,6 @@ public static class ImGuiClip return ~idx; } - // Draw non-random-access data that gets filtered without storing state. // Use GetNecessarySkips first and use its return value for skips. // checkFilter should return true for items that should be displayed and false for those that should be skipped. diff --git a/Dalamud/Interface/ImGuiExtensions.cs b/Dalamud/Interface/Utility/ImGuiExtensions.cs similarity index 98% rename from Dalamud/Interface/ImGuiExtensions.cs rename to Dalamud/Interface/Utility/ImGuiExtensions.cs index be1b99430..21a0d3747 100644 --- a/Dalamud/Interface/ImGuiExtensions.cs +++ b/Dalamud/Interface/Utility/ImGuiExtensions.cs @@ -1,10 +1,9 @@ -using System; using System.Numerics; using System.Text; using ImGuiNET; -namespace Dalamud.Interface; +namespace Dalamud.Interface.Utility; /// /// Class containing various extensions to ImGui, aiding with building custom widgets. diff --git a/Dalamud/Interface/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs similarity index 99% rename from Dalamud/Interface/ImGuiHelpers.cs rename to Dalamud/Interface/Utility/ImGuiHelpers.cs index 2356d90e2..dbb873edf 100644 --- a/Dalamud/Interface/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,15 +1,14 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Numerics; using Dalamud.Game.ClientState.Keys; -using Dalamud.Interface.Raii; +using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; -namespace Dalamud.Interface; +namespace Dalamud.Interface.Utility; /// /// Class containing various helper methods for use with ImGui inside Dalamud. @@ -300,7 +299,6 @@ public static class ImGuiHelpers internal static void NewFrame() { GlobalScale = ImGui.GetIO().FontGlobalScale; - InterfaceHelpers.GlobalScale = GlobalScale; } /// diff --git a/Dalamud/Interface/Utility/ImGuiTable.cs b/Dalamud/Interface/Utility/ImGuiTable.cs new file mode 100644 index 000000000..c74bc0a2f --- /dev/null +++ b/Dalamud/Interface/Utility/ImGuiTable.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; + +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; + +namespace Dalamud.Interface.Utility; + +#pragma warning disable SA1618 // GenericTypeParametersMustBeDocumented +#pragma warning disable SA1611 // ElementParametersMustBeDocumented + +/// +/// Helpers for drawing tables. +/// +public static class ImGuiTable +{ + /// + /// Draw a simple table with the given data using the drawRow action. + /// Headers and thus columns and column count are defined by columnTitles. + /// + public static void DrawTable(string label, IEnumerable data, Action drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None, + params string[] columnTitles) + { + if (columnTitles.Length == 0) + return; + + using var table = ImRaii.Table(label, columnTitles.Length, flags); + if (!table) + return; + + foreach (var title in columnTitles) + { + ImGui.TableNextColumn(); + ImGui.TableHeader(title); + } + + foreach (var datum in data) + { + ImGui.TableNextRow(); + drawRow(datum); + } + } + + /// + /// Draw a simple table with the given data using the drawRow action inside a collapsing header. + /// Headers and thus columns and column count are defined by columnTitles. + /// + public static void DrawTabbedTable(string label, IEnumerable data, Action drawRow, ImGuiTableFlags flags = ImGuiTableFlags.None, + params string[] columnTitles) + { + if (ImGui.CollapsingHeader(label)) + DrawTable($"{label}##Table", data, drawRow, flags, columnTitles); + } +} + +#pragma warning restore SA1611 // ElementParametersMustBeDocumented +#pragma warning restore SA1618 // GenericTypeParametersMustBeDocumented diff --git a/Dalamud.Interface/Raii/Color.cs b/Dalamud/Interface/Utility/Raii/Color.cs similarity index 83% rename from Dalamud.Interface/Raii/Color.cs rename to Dalamud/Interface/Utility/Raii/Color.cs index 388e6e737..3cf93b65c 100644 --- a/Dalamud.Interface/Raii/Color.cs +++ b/Dalamud/Interface/Utility/Raii/Color.cs @@ -1,7 +1,10 @@ +using System.Collections.Generic; +using System.Linq; using System.Numerics; + using ImGuiNET; -namespace Dalamud.Interface.Raii; +namespace Dalamud.Interface.Utility.Raii; // Push an arbitrary amount of colors into an object that are all popped when it is disposed. // If condition is false, no color is pushed. @@ -26,7 +29,7 @@ public static partial class ImRaii public sealed class Color : IDisposable { internal static readonly List<(ImGuiCol, uint)> Stack = new(); - private int _count; + private int count; public Color Push(ImGuiCol idx, uint color, bool condition = true) { @@ -34,7 +37,7 @@ public static partial class ImRaii { Stack.Add((idx, ImGui.GetColorU32(idx))); ImGui.PushStyleColor(idx, color); - ++this._count; + ++this.count; } return this; @@ -46,7 +49,7 @@ public static partial class ImRaii { Stack.Add((idx, ImGui.GetColorU32(idx))); ImGui.PushStyleColor(idx, color); - ++this._count; + ++this.count; } return this; @@ -54,13 +57,13 @@ public static partial class ImRaii public void Pop(int num = 1) { - num = Math.Min(num, this._count); - this._count -= num; + num = Math.Min(num, this.count); + this.count -= num; ImGui.PopStyleColor(num); Stack.RemoveRange(Stack.Count - num, num); } public void Dispose() - => this.Pop(this._count); + => this.Pop(this.count); } } diff --git a/Dalamud.Interface/Raii/EndObjects.cs b/Dalamud/Interface/Utility/Raii/EndObjects.cs similarity index 94% rename from Dalamud.Interface/Raii/EndObjects.cs rename to Dalamud/Interface/Utility/Raii/EndObjects.cs index 032f09621..3f2a016b3 100644 --- a/Dalamud.Interface/Raii/EndObjects.cs +++ b/Dalamud/Interface/Utility/Raii/EndObjects.cs @@ -1,13 +1,14 @@ using System.Numerics; + using ImGuiNET; -namespace Dalamud.Interface.Raii; +namespace Dalamud.Interface.Utility.Raii; // Most ImGui widgets with IDisposable interface that automatically destroys them // when created with using variables. public static partial class ImRaii { - private static int _disabledCount = 0; + private static int disabledCount = 0; public static IEndObject Child(string strId) => new EndUnconditionally(ImGui.EndChild, ImGui.BeginChild(strId)); @@ -120,7 +121,7 @@ public static partial class ImRaii public static IEndObject Disabled() { ImGui.BeginDisabled(); - ++_disabledCount; + ++disabledCount; return DisabledEnd(); } @@ -130,24 +131,24 @@ public static partial class ImRaii return new EndConditionally(Nop, false); ImGui.BeginDisabled(); - ++_disabledCount; + ++disabledCount; return DisabledEnd(); } public static IEndObject Enabled() { - var oldCount = _disabledCount; + var oldCount = disabledCount; if (oldCount == 0) return new EndConditionally(Nop, false); void Restore() { - _disabledCount += oldCount; + disabledCount += oldCount; while (--oldCount >= 0) ImGui.BeginDisabled(); } - for (; _disabledCount > 0; --_disabledCount) + for (; disabledCount > 0; --disabledCount) ImGui.EndDisabled(); return new EndUnconditionally(Restore, true); @@ -156,7 +157,7 @@ public static partial class ImRaii private static IEndObject DisabledEnd() => new EndUnconditionally(() => { - --_disabledCount; + --disabledCount; ImGui.EndDisabled(); }, true); @@ -173,6 +174,11 @@ public static partial class ImRaii return new EndUnconditionally(Widget.EndFramedGroup, true); } */ + + // Used to avoid tree pops when flag for no push is set. + private static void Nop() + { + } // Exported interface for RAII. public interface IEndObject : IDisposable @@ -203,7 +209,9 @@ public static partial class ImRaii private struct EndUnconditionally : IEndObject { private Action EndAction { get; } + public bool Success { get; } + public bool Disposed { get; private set; } public EndUnconditionally(Action endAction, bool success) @@ -226,16 +234,18 @@ public static partial class ImRaii // Use end-function only on success. private struct EndConditionally : IEndObject { - private Action EndAction { get; } - public bool Success { get; } - public bool Disposed { get; private set; } - public EndConditionally(Action endAction, bool success) { this.EndAction = endAction; - this.Success = success; - this.Disposed = false; + this.Success = success; + this.Disposed = false; } + + public bool Success { get; } + + public bool Disposed { get; private set; } + + private Action EndAction { get; } public void Dispose() { @@ -247,8 +257,4 @@ public static partial class ImRaii this.Disposed = true; } } - - // Used to avoid tree pops when flag for no push is set. - private static void Nop() - { } } diff --git a/Dalamud.Interface/Raii/Font.cs b/Dalamud/Interface/Utility/Raii/Font.cs similarity index 81% rename from Dalamud.Interface/Raii/Font.cs rename to Dalamud/Interface/Utility/Raii/Font.cs index cdecf457c..2d11bb071 100644 --- a/Dalamud.Interface/Raii/Font.cs +++ b/Dalamud/Interface/Utility/Raii/Font.cs @@ -1,6 +1,6 @@ using ImGuiNET; -namespace Dalamud.Interface.Raii; +namespace Dalamud.Interface.Utility.Raii; // Push an arbitrary amount of fonts into an object that are all popped when it is disposed. // If condition is false, no font is pushed. @@ -18,10 +18,10 @@ public static partial class ImRaii internal static int FontPushCounter = 0; internal static ImFontPtr DefaultPushed; - private int _count; + private int count; public Font() - => this._count = 0; + => this.count = 0; public Font Push(ImFontPtr font, bool condition = true) { @@ -30,7 +30,7 @@ public static partial class ImRaii if (FontPushCounter++ == 0) DefaultPushed = ImGui.GetFont(); ImGui.PushFont(font); - ++this._count; + ++this.count; } return this; @@ -38,14 +38,14 @@ public static partial class ImRaii public void Pop(int num = 1) { - num = Math.Min(num, this._count); - this._count -= num; + num = Math.Min(num, this.count); + this.count -= num; FontPushCounter -= num; while (num-- > 0) ImGui.PopFont(); } public void Dispose() - => this.Pop(this._count); + => this.Pop(this.count); } } diff --git a/Dalamud.Interface/Raii/Id.cs b/Dalamud/Interface/Utility/Raii/Id.cs similarity index 82% rename from Dalamud.Interface/Raii/Id.cs rename to Dalamud/Interface/Utility/Raii/Id.cs index 1248b92f3..51c6438c4 100644 --- a/Dalamud.Interface/Raii/Id.cs +++ b/Dalamud/Interface/Utility/Raii/Id.cs @@ -1,6 +1,6 @@ using ImGuiNET; -namespace Dalamud.Interface.Raii; +namespace Dalamud.Interface.Utility.Raii; // Push an arbitrary amount of ids into an object that are all popped when it is disposed. // If condition is false, no id is pushed. @@ -17,14 +17,14 @@ public static partial class ImRaii public sealed class Id : IDisposable { - private int _count; + private int count; public Id Push(string id, bool condition = true) { if (condition) { ImGui.PushID(id); - ++this._count; + ++this.count; } return this; @@ -35,7 +35,7 @@ public static partial class ImRaii if (condition) { ImGui.PushID(id); - ++this._count; + ++this.count; } return this; @@ -46,7 +46,7 @@ public static partial class ImRaii if (condition) { ImGui.PushID(id); - ++this._count; + ++this.count; } return this; @@ -54,13 +54,13 @@ public static partial class ImRaii public void Pop(int num = 1) { - num = Math.Min(num, this._count); - this._count -= num; + num = Math.Min(num, this.count); + this.count -= num; while (num-- > 0) ImGui.PopID(); } public void Dispose() - => this.Pop(this._count); + => this.Pop(this.count); } } diff --git a/Dalamud.Interface/Raii/Indent.cs b/Dalamud/Interface/Utility/Raii/Indent.cs similarity index 91% rename from Dalamud.Interface/Raii/Indent.cs rename to Dalamud/Interface/Utility/Raii/Indent.cs index 99eab8783..3c8f0f1da 100644 --- a/Dalamud.Interface/Raii/Indent.cs +++ b/Dalamud/Interface/Utility/Raii/Indent.cs @@ -1,6 +1,6 @@ using ImGuiNET; -namespace Dalamud.Interface.Raii; +namespace Dalamud.Interface.Utility.Raii; public static partial class ImRaii { @@ -19,7 +19,7 @@ public static partial class ImRaii if (condition) { if (scaled) - indent *= InterfaceHelpers.GlobalScale; + indent *= ImGuiHelpers.GlobalScale; IndentInternal(indent); this.Indentation += indent; @@ -43,7 +43,7 @@ public static partial class ImRaii public void Pop(float indent, bool scaled = true) { if (scaled) - indent *= InterfaceHelpers.GlobalScale; + indent *= ImGuiHelpers.GlobalScale; IndentInternal(-indent); this.Indentation -= indent; diff --git a/Dalamud.Interface/Raii/Style.cs b/Dalamud/Interface/Utility/Raii/Style.cs similarity index 95% rename from Dalamud.Interface/Raii/Style.cs rename to Dalamud/Interface/Utility/Raii/Style.cs index 2f1fea538..82f51bf88 100644 --- a/Dalamud.Interface/Raii/Style.cs +++ b/Dalamud/Interface/Utility/Raii/Style.cs @@ -1,7 +1,10 @@ +using System.Collections.Generic; +using System.Linq; using System.Numerics; + using ImGuiNET; -namespace Dalamud.Interface.Raii; +namespace Dalamud.Interface.Utility.Raii; // Push an arbitrary amount of styles into an object that are all popped when it is disposed. // If condition is false, no style is pushed. @@ -17,7 +20,7 @@ public static partial class ImRaii // Push styles that revert all current style changes made temporarily. public static Style DefaultStyle() { - var ret = new Style(); + var ret = new Style(); var reverseStack = Style.Stack.GroupBy(p => p.Item1).Select(p => (p.Key, p.First().Item2)).ToArray(); foreach (var (idx, val) in reverseStack) { @@ -34,7 +37,7 @@ public static partial class ImRaii { internal static readonly List<(ImGuiStyleVar, Vector2)> Stack = new(); - private int _count; + private int count; [System.Diagnostics.Conditional("DEBUG")] private static void CheckStyleIdx(ImGuiStyleVar idx, Type type) @@ -115,7 +118,7 @@ public static partial class ImRaii CheckStyleIdx(idx, typeof(float)); Stack.Add((idx, GetStyle(idx))); ImGui.PushStyleVar(idx, value); - ++this._count; + ++this.count; return this; } @@ -128,20 +131,20 @@ public static partial class ImRaii CheckStyleIdx(idx, typeof(Vector2)); Stack.Add((idx, GetStyle(idx))); ImGui.PushStyleVar(idx, value); - ++this._count; + ++this.count; return this; } public void Pop(int num = 1) { - num = Math.Min(num, this._count); - this._count -= num; + num = Math.Min(num, this.count); + this.count -= num; ImGui.PopStyleVar(num); Stack.RemoveRange(Stack.Count - num, num); } public void Dispose() - => this.Pop(this._count); + => this.Pop(this.count); } } diff --git a/Dalamud.Interface/Table/Column.cs b/Dalamud/Interface/Utility/Table/Column.cs similarity index 92% rename from Dalamud.Interface/Table/Column.cs rename to Dalamud/Interface/Utility/Table/Column.cs index 7460ec189..412ba87dc 100644 --- a/Dalamud.Interface/Table/Column.cs +++ b/Dalamud/Interface/Utility/Table/Column.cs @@ -1,6 +1,6 @@ using ImGuiNET; -namespace Dalamud.Interface.Table; +namespace Dalamud.Interface.Utility.Table; public class Column { @@ -27,7 +27,8 @@ public class Column => 0; public virtual void DrawColumn(TItem item, int idx) - { } + { + } public int CompareInv(TItem lhs, TItem rhs) => this.Compare(rhs, lhs); diff --git a/Dalamud.Interface/Table/ColumnFlags.cs b/Dalamud/Interface/Utility/Table/ColumnFlags.cs similarity index 89% rename from Dalamud.Interface/Table/ColumnFlags.cs rename to Dalamud/Interface/Utility/Table/ColumnFlags.cs index 815ddcf76..24670adfc 100644 --- a/Dalamud.Interface/Table/ColumnFlags.cs +++ b/Dalamud/Interface/Utility/Table/ColumnFlags.cs @@ -1,7 +1,9 @@ -using ImGuiNET; -using ImRaii = Dalamud.Interface.Raii.ImRaii; +using System.Collections.Generic; -namespace Dalamud.Interface.Table; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; + +namespace Dalamud.Interface.Utility.Table; public class ColumnFlags : Column where T : struct, Enum { @@ -17,13 +19,14 @@ public class ColumnFlags : Column where T : struct, Enum => default; protected virtual void SetValue(T value, bool enable) - { } + { + } public override bool DrawFilter() { using var id = ImRaii.PushId(this.FilterLabel); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0); - ImGui.SetNextItemWidth(-Table.ArrowWidth * InterfaceHelpers.GlobalScale); + ImGui.SetNextItemWidth(-Table.ArrowWidth * ImGuiHelpers.GlobalScale); var all = this.FilterValue.HasFlag(this.AllFlags); using var color = ImRaii.PushColor(ImGuiCol.FrameBg, 0x803030A0, !all); using var combo = ImRaii.Combo(string.Empty, this.Label, ImGuiComboFlags.NoArrowButton); diff --git a/Dalamud.Interface/Table/ColumnSelect.cs b/Dalamud/Interface/Utility/Table/ColumnSelect.cs similarity index 66% rename from Dalamud.Interface/Table/ColumnSelect.cs rename to Dalamud/Interface/Utility/Table/ColumnSelect.cs index 5ef276b06..fb463700c 100644 --- a/Dalamud.Interface/Table/ColumnSelect.cs +++ b/Dalamud/Interface/Utility/Table/ColumnSelect.cs @@ -1,7 +1,9 @@ -using ImGuiNET; -using ImRaii = Dalamud.Interface.Raii.ImRaii; +using System.Collections.Generic; -namespace Dalamud.Interface.Table; +using Dalamud.Interface.Utility.Raii; +using ImGuiNET; + +namespace Dalamud.Interface.Utility.Table; public class ColumnSelect : Column where T : struct, Enum, IEquatable { @@ -18,26 +20,26 @@ public class ColumnSelect : Column where T : struct, Enum, IEqu => this.FilterValue = value; public T FilterValue; - protected int Idx = -1; + protected int idx = -1; public override bool DrawFilter() { using var id = ImRaii.PushId(this.FilterLabel); using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0); - ImGui.SetNextItemWidth(-Table.ArrowWidth * InterfaceHelpers.GlobalScale); - using var combo = ImRaii.Combo(string.Empty, this.Idx < 0 ? this.Label : this.Names[this.Idx]); - if(!combo) + ImGui.SetNextItemWidth(-Table.ArrowWidth * ImGuiHelpers.GlobalScale); + using var combo = ImRaii.Combo(string.Empty, this.idx < 0 ? this.Label : this.Names[this.idx]); + if (!combo) return false; var ret = false; for (var i = 0; i < this.Names.Length; ++i) { if (this.FilterValue.Equals(this.Values[i])) - this.Idx = i; - if (!ImGui.Selectable(this.Names[i], this.Idx == i) || this.Idx == i) + this.idx = i; + if (!ImGui.Selectable(this.Names[i], this.idx == i) || this.idx == i) continue; - this.Idx = i; + this.idx = i; this.SetValue(this.Values[i]); ret = true; } diff --git a/Dalamud.Interface/Table/ColumnString.cs b/Dalamud/Interface/Utility/Table/ColumnString.cs similarity index 75% rename from Dalamud.Interface/Table/ColumnString.cs rename to Dalamud/Interface/Utility/Table/ColumnString.cs index dcd43b23c..3f9d2df91 100644 --- a/Dalamud.Interface/Table/ColumnString.cs +++ b/Dalamud/Interface/Utility/Table/ColumnString.cs @@ -1,8 +1,9 @@ using System.Text.RegularExpressions; -using Dalamud.Interface.Raii; + +using Dalamud.Interface.Utility.Raii; using ImGuiNET; -namespace Dalamud.Interface.Table; +namespace Dalamud.Interface.Utility.Table; public class ColumnString : Column { @@ -10,7 +11,7 @@ public class ColumnString : Column => this.Flags &= ~ImGuiTableColumnFlags.NoResize; public string FilterValue = string.Empty; - protected Regex? FilterRegex; + protected Regex? filterRegex; public virtual string ToName(TItem item) => item!.ToString() ?? string.Empty; @@ -22,7 +23,7 @@ public class ColumnString : Column { using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0); - ImGui.SetNextItemWidth(-Table.ArrowWidth * InterfaceHelpers.GlobalScale); + ImGui.SetNextItemWidth(-Table.ArrowWidth * ImGuiHelpers.GlobalScale); var tmp = this.FilterValue; if (!ImGui.InputTextWithHint(this.FilterLabel, this.Label, ref tmp, 256) || tmp == this.FilterValue) return false; @@ -30,11 +31,11 @@ public class ColumnString : Column this.FilterValue = tmp; try { - this.FilterRegex = new Regex(this.FilterValue, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + this.filterRegex = new Regex(this.FilterValue, RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } catch { - this.FilterRegex = null; + this.filterRegex = null; } return true; @@ -46,10 +47,10 @@ public class ColumnString : Column if (this.FilterValue.Length == 0) return true; - return this.FilterRegex?.IsMatch(name) ?? name.Contains(this.FilterValue, StringComparison.OrdinalIgnoreCase); + return this.filterRegex?.IsMatch(name) ?? name.Contains(this.FilterValue, StringComparison.OrdinalIgnoreCase); } - public override void DrawColumn(TItem item, int _) + public override void DrawColumn(TItem item, int idx) { ImGui.TextUnformatted(this.ToName(item)); } diff --git a/Dalamud.Interface/Table/Table.cs b/Dalamud/Interface/Utility/Table/Table.cs similarity index 64% rename from Dalamud.Interface/Table/Table.cs rename to Dalamud/Interface/Utility/Table/Table.cs index 74fb0bc5c..86653e834 100644 --- a/Dalamud.Interface/Table/Table.cs +++ b/Dalamud/Interface/Utility/Table/Table.cs @@ -1,8 +1,12 @@ +using System.Collections.Generic; +using System.Linq; using System.Numerics; -using ImGuiNET; -using ImRaii = Dalamud.Interface.Raii.ImRaii; -namespace Dalamud.Interface.Table; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; +using ImGuiNET; + +namespace Dalamud.Interface.Utility.Table; public static class Table { @@ -11,18 +15,20 @@ public static class Table public class Table { - protected bool FilterDirty = true; - protected bool SortDirty = true; protected readonly ICollection Items; - internal readonly List<(T, int)> FilteredItems; + internal readonly List<(T, int)> FilteredItems; - protected readonly string Label; + protected readonly string Label; protected readonly Column[] Headers; - protected float ItemHeight { get; set; } - public float ExtraHeight { get; set; } = 0; + protected bool filterDirty = true; + protected bool sortDirty = true; - private int _currentIdx = 0; + protected float ItemHeight { get; set; } + + public float ExtraHeight { get; set; } = 0; + + private int currentIdx = 0; protected bool Sortable { @@ -30,7 +36,7 @@ public class Table set => this.Flags = value ? this.Flags | ImGuiTableFlags.Sortable : this.Flags & ~ImGuiTableFlags.Sortable; } - protected int SortIdx = -1; + protected int sortIdx = -1; public ImGuiTableFlags Flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.Sortable @@ -54,10 +60,10 @@ public class Table public Table(string label, ICollection items, params Column[] headers) { - this.Label = label; - this.Items = items; - this.Headers = headers; - this.FilteredItems = new List<(T, int)>(this.Items.Count); + this.Label = label; + this.Items = items; + this.Headers = headers; + this.FilteredItems = new List<(T, int)>(this.Items.Count); this.VisibleColumns = this.Headers.Length; } @@ -73,7 +79,8 @@ public class Table => throw new NotImplementedException(); protected virtual void PreDraw() - { } + { + } private void SortInternal() { @@ -81,29 +88,30 @@ public class Table return; var sortSpecs = ImGui.TableGetSortSpecs(); - this.SortDirty |= sortSpecs.SpecsDirty; + this.sortDirty |= sortSpecs.SpecsDirty; - if (!this.SortDirty) + if (!this.sortDirty) return; - this.SortIdx = sortSpecs.Specs.ColumnIndex; + this.sortIdx = sortSpecs.Specs.ColumnIndex; - if (this.Headers.Length <= this.SortIdx) - this.SortIdx = 0; + if (this.Headers.Length <= this.sortIdx) + this.sortIdx = 0; - if (sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) - this.FilteredItems.StableSort((a, b) => this.Headers[this.SortIdx].Compare(a.Item1, b.Item1)); - else if (sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) - this.FilteredItems.StableSort((a, b) => this.Headers[this.SortIdx].CompareInv(a.Item1, b.Item1)); - else - this.SortIdx = -1; - this.SortDirty = false; - sortSpecs.SpecsDirty = false; + if (sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending) + this.FilteredItems.StableSort((a, b) => this.Headers[this.sortIdx].Compare(a.Item1, b.Item1)); + else if (sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending) + this.FilteredItems.StableSort((a, b) => this.Headers[this.sortIdx].CompareInv(a.Item1, b.Item1)); + else + this.sortIdx = -1; + + this.sortDirty = false; + sortSpecs.SpecsDirty = false; } private void UpdateFilter() { - if (!this.FilterDirty) + if (!this.filterDirty) return; this.FilteredItems.Clear(); @@ -115,20 +123,20 @@ public class Table idx++; } - this.FilterDirty = false; - this.SortDirty = true; + this.filterDirty = false; + this.sortDirty = true; } - private void DrawItem((T, int) pair) + private void DrawItem((T Item, int Index) pair) { - var column = 0; - using var id = ImRaii.PushId(this._currentIdx); - this._currentIdx = pair.Item2; + var column = 0; + using var id = ImRaii.PushId(this.currentIdx); + this.currentIdx = pair.Index; foreach (var header in this.Headers) { id.Push(column++); if (ImGui.TableNextColumn()) - header.DrawColumn(pair.Item1, pair.Item2); + header.DrawColumn(pair.Item, pair.Index); id.Pop(); } } @@ -136,7 +144,7 @@ public class Table private void DrawTableInternal() { using var table = ImRaii.Table("Table", this.Headers.Length, this.Flags, - ImGui.GetContentRegionAvail() - this.ExtraHeight * Vector2.UnitY * InterfaceHelpers.GlobalScale); + ImGui.GetContentRegionAvail() - this.ExtraHeight * Vector2.UnitY * ImGuiHelpers.GlobalScale); if (!table) return; @@ -162,11 +170,11 @@ public class Table ImGui.SameLine(); style.Pop(); if (header.DrawFilter()) - this.FilterDirty = true; + this.filterDirty = true; } this.SortInternal(); - this._currentIdx = 0; + this.currentIdx = 0; ImGuiClip.ClippedDraw(this.FilteredItems, this.DrawItem, this.ItemHeight); } } diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 39c61566b..a339b807d 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -2,6 +2,7 @@ using System.Numerics; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.Utility; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; @@ -223,6 +224,7 @@ public abstract class Window /// /// Draw the window via ImGui. /// + /// Configuration instance used to check if certain window management features should be enabled. internal void DrawInternal(DalamudConfiguration? configuration) { this.PreOpenCheck(); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 887994f30..e91195793 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -53,12 +53,6 @@ namespace Dalamud.Plugin.Internal; #pragma warning restore SA1015 internal partial class PluginManager : IDisposable, IServiceType { - /// - /// The current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded. - /// As of Dalamud 9.x, this always matches the major version number of Dalamud. - /// - public static int DalamudApiLevel => Assembly.GetExecutingAssembly().GetName().Version!.Major; - /// /// Default time to wait between plugin unload and plugin assembly unload. /// @@ -88,6 +82,11 @@ internal partial class PluginManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly HappyHttpClient happyHttpClient = Service.Get(); + static PluginManager() + { + DalamudApiLevel = typeof(PluginManager).Assembly.GetName().Version!.Major; + } + [ServiceManager.ServiceConstructor] private PluginManager() { @@ -149,6 +148,12 @@ internal partial class PluginManager : IDisposable, IServiceType /// public event Action? OnAvailablePluginsChanged; + /// + /// Gets the current Dalamud API level, used to handle breaking changes. Only plugins with this level will be loaded. + /// As of Dalamud 9.x, this always matches the major version number of Dalamud. + /// + public static int DalamudApiLevel { get; private set; } + /// /// Gets a copy of the list of all loaded plugins. /// diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index 98f6160cc..69a611114 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -83,7 +83,7 @@ public interface IGameConfig /// Attempts to get the properties of a String option from the System section. /// /// Option to get the properties of. - /// Details of the option: Default Value + /// Details of the option: Default Value. /// A value representing the success. public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties); @@ -139,7 +139,7 @@ public interface IGameConfig /// Attempts to get the properties of a String option from the UiConfig section. /// /// Option to get the properties of. - /// Details of the option: Default Value + /// Details of the option: Default Value. /// A value representing the success. public bool TryGet(UiConfigOption option, out StringConfigProperties? properties); @@ -195,7 +195,7 @@ public interface IGameConfig /// Attempts to get the properties of a String option from the UiControl section. /// /// Option to get the properties of. - /// Details of the option: Default Value + /// Details of the option: Default Value. /// A value representing the success. public bool TryGet(UiControlOption option, out StringConfigProperties? properties); diff --git a/Dalamud/Plugin/Services/IKeyState.cs b/Dalamud/Plugin/Services/IKeyState.cs index c2bca7347..de78978ca 100644 --- a/Dalamud/Plugin/Services/IKeyState.cs +++ b/Dalamud/Plugin/Services/IKeyState.cs @@ -1,7 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; + using Dalamud.Game.ClientState.Keys; -using PInvoke; namespace Dalamud.Plugin.Services; diff --git a/Dalamud.Interface/ArrayExtensions.cs b/Dalamud/Utility/ArrayExtensions.cs similarity index 80% rename from Dalamud.Interface/ArrayExtensions.cs rename to Dalamud/Utility/ArrayExtensions.cs index 68bf52a29..afb1511e3 100644 --- a/Dalamud.Interface/ArrayExtensions.cs +++ b/Dalamud/Utility/ArrayExtensions.cs @@ -1,7 +1,13 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; -namespace Dalamud.Interface; +namespace Dalamud.Utility; +[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1618:Generic type parameters should be documented", Justification = "Reviewed,")] +[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Reviewed,")] +[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1615:Element return value should be documented", Justification = "Reviewed,")] +[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1611:Element parameters should be documented", Justification = "Reviewed,")] internal static class ArrayExtensions { /// Iterate over enumerables with additional index. @@ -16,7 +22,6 @@ internal static class ArrayExtensions public static IEnumerable WithoutValue(this IEnumerable<(T Value, int Index)> list) => list.Select(x => x.Index); - // Find the index of the first object fulfilling predicate's criteria in the given list. // Returns -1 if no such object is found. public static int IndexOf(this IEnumerable array, Predicate predicate) diff --git a/Dalamud/Utility/FuzzyMatcher.cs b/Dalamud/Utility/FuzzyMatcher.cs index 647c9586d..9ac71d8bb 100644 --- a/Dalamud/Utility/FuzzyMatcher.cs +++ b/Dalamud/Utility/FuzzyMatcher.cs @@ -6,6 +6,9 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +#pragma warning disable SA1600 +#pragma warning disable SA1602 + internal readonly ref struct FuzzyMatcher { private static readonly (int, int)[] EmptySegArray = Array.Empty<(int, int)>(); @@ -13,31 +16,31 @@ internal readonly ref struct FuzzyMatcher private readonly string needleString = string.Empty; private readonly ReadOnlySpan needleSpan = ReadOnlySpan.Empty; private readonly int needleFinalPosition = -1; - private readonly (int start, int end)[] needleSegments = EmptySegArray; + private readonly (int Start, int End)[] needleSegments = EmptySegArray; private readonly MatchMode mode = MatchMode.Simple; public FuzzyMatcher(string term, MatchMode matchMode) { - needleString = term; - needleSpan = needleString.AsSpan(); - needleFinalPosition = needleSpan.Length - 1; - mode = matchMode; + this.needleString = term; + this.needleSpan = this.needleString.AsSpan(); + this.needleFinalPosition = this.needleSpan.Length - 1; + this.mode = matchMode; switch (matchMode) { case MatchMode.FuzzyParts: - needleSegments = FindNeedleSegments(needleSpan); + this.needleSegments = FindNeedleSegments(this.needleSpan); break; case MatchMode.Fuzzy: case MatchMode.Simple: - needleSegments = EmptySegArray; + this.needleSegments = EmptySegArray; break; default: throw new ArgumentOutOfRangeException(nameof(matchMode), matchMode, null); } } - private static (int start, int end)[] FindNeedleSegments(ReadOnlySpan span) + private static (int Start, int End)[] FindNeedleSegments(ReadOnlySpan span) { var segments = new List<(int, int)>(); var wordStart = -1; @@ -66,37 +69,39 @@ internal readonly ref struct FuzzyMatcher return segments.ToArray(); } +#pragma warning disable SA1202 public int Matches(string value) +#pragma warning restore SA1202 { - if (needleFinalPosition < 0) + if (this.needleFinalPosition < 0) { return 0; } - if (mode == MatchMode.Simple) + if (this.mode == MatchMode.Simple) { - return value.Contains(needleString) ? 1 : 0; + return value.Contains(this.needleString) ? 1 : 0; } var haystack = value.AsSpan(); - if (mode == MatchMode.Fuzzy) + if (this.mode == MatchMode.Fuzzy) { - return GetRawScore(haystack, 0, needleFinalPosition); + return this.GetRawScore(haystack, 0, this.needleFinalPosition); } - if (mode == MatchMode.FuzzyParts) + if (this.mode == MatchMode.FuzzyParts) { - if (needleSegments.Length < 2) + if (this.needleSegments.Length < 2) { - return GetRawScore(haystack, 0, needleFinalPosition); + return this.GetRawScore(haystack, 0, this.needleFinalPosition); } var total = 0; - for (var i = 0; i < needleSegments.Length; i++) + for (var i = 0; i < this.needleSegments.Length; i++) { - var (start, end) = needleSegments[i]; - var cur = GetRawScore(haystack, start, end); + var (start, end) = this.needleSegments[i]; + var cur = this.GetRawScore(haystack, start, end); if (cur == 0) { return 0; @@ -116,7 +121,7 @@ internal readonly ref struct FuzzyMatcher var max = 0; for (var i = 0; i < values.Length; i++) { - var cur = Matches(values[i]); + var cur = this.Matches(values[i]); if (cur > max) { max = cur; @@ -128,7 +133,7 @@ internal readonly ref struct FuzzyMatcher private int GetRawScore(ReadOnlySpan haystack, int needleStart, int needleEnd) { - var (startPos, gaps, consecutive, borderMatches, endPos) = FindForward(haystack, needleStart, needleEnd); + var (startPos, gaps, consecutive, borderMatches, endPos) = this.FindForward(haystack, needleStart, needleEnd); if (startPos < 0) { return 0; @@ -140,7 +145,7 @@ internal readonly ref struct FuzzyMatcher // PluginLog.Debug( // $"['{needleString.Substring(needleStart, needleEnd - needleStart + 1)}' in '{haystack}'] fwd: needleSize={needleSize} startPos={startPos} gaps={gaps} consecutive={consecutive} borderMatches={borderMatches} score={score}"); - (startPos, gaps, consecutive, borderMatches) = FindReverse(haystack, endPos, needleStart, needleEnd); + (startPos, gaps, consecutive, borderMatches) = this.FindReverse(haystack, endPos, needleStart, needleEnd); var revScore = CalculateRawScore(needleSize, startPos, gaps, consecutive, borderMatches); // PluginLog.Debug( // $"['{needleString.Substring(needleStart, needleEnd - needleStart + 1)}' in '{haystack}'] rev: needleSize={needleSize} startPos={startPos} gaps={gaps} consecutive={consecutive} borderMatches={borderMatches} score={revScore}"); @@ -149,7 +154,9 @@ internal readonly ref struct FuzzyMatcher } [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable SA1204 private static int CalculateRawScore(int needleSize, int startPos, int gaps, int consecutive, int borderMatches) +#pragma warning restore SA1204 { var score = 100 + needleSize * 3 @@ -162,7 +169,7 @@ internal readonly ref struct FuzzyMatcher return score < 1 ? 1 : score; } - private (int startPos, int gaps, int consecutive, int borderMatches, int haystackIndex) FindForward( + private (int StartPos, int Gaps, int Consecutive, int BorderMatches, int HaystackIndex) FindForward( ReadOnlySpan haystack, int needleStart, int needleEnd) { var needleIndex = needleStart; @@ -175,7 +182,7 @@ internal readonly ref struct FuzzyMatcher for (var haystackIndex = 0; haystackIndex < haystack.Length; haystackIndex++) { - if (haystack[haystackIndex] == needleSpan[needleIndex]) + if (haystack[haystackIndex] == this.needleSpan[needleIndex]) { #if BORDER_MATCHING if (haystackIndex > 0) @@ -217,8 +224,8 @@ internal readonly ref struct FuzzyMatcher return (-1, 0, 0, 0, 0); } - private (int startPos, int gaps, int consecutive, int borderMatches) FindReverse(ReadOnlySpan haystack, - int haystackLastMatchIndex, int needleStart, int needleEnd) + private (int StartPos, int Gaps, int Consecutive, int BorderMatches) FindReverse( + ReadOnlySpan haystack, int haystackLastMatchIndex, int needleStart, int needleEnd) { var needleIndex = needleEnd; var revLastMatchIndex = haystack.Length + 10; @@ -229,7 +236,7 @@ internal readonly ref struct FuzzyMatcher for (var haystackIndex = haystackLastMatchIndex; haystackIndex >= 0; haystackIndex--) { - if (haystack[haystackIndex] == needleSpan[needleIndex]) + if (haystack[haystackIndex] == this.needleSpan[needleIndex]) { #if BORDER_MATCHING if (haystackIndex > 0) @@ -265,9 +272,12 @@ internal readonly ref struct FuzzyMatcher } } -public enum MatchMode +internal enum MatchMode { Simple, Fuzzy, - FuzzyParts + FuzzyParts, } + +#pragma warning restore SA1600 +#pragma warning restore SA1602 diff --git a/Dalamud.Interface/StableInsertionSortExtension.cs b/Dalamud/Utility/StableInsertionSortExtension.cs similarity index 57% rename from Dalamud.Interface/StableInsertionSortExtension.cs rename to Dalamud/Utility/StableInsertionSortExtension.cs index d2884f838..f7c9b43be 100644 --- a/Dalamud.Interface/StableInsertionSortExtension.cs +++ b/Dalamud/Utility/StableInsertionSortExtension.cs @@ -1,9 +1,21 @@ +using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; -namespace Dalamud.Interface; +namespace Dalamud.Utility; +/// +/// Extensions methods providing stable insertion sorts for IList. +/// internal static class StableInsertionSortExtension { + /// + /// Perform a stable sort on a list. + /// + /// The list to sort. + /// Selector to order by. + /// Element type. + /// Selected type. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static void StableSort(this IList list, Func selector) { @@ -13,6 +25,12 @@ internal static class StableInsertionSortExtension list[i] = tmpList[i]; } + /// + /// Perform a stable sort on a list. + /// + /// The list to sort. + /// Comparer to use when comparing items. + /// Element type. [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] public static void StableSort(this IList list, Comparison comparer) { diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 53c570e54..5f2e4d5bf 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -17,6 +17,7 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using Dalamud.Networking.Http; using ImGuiNET; diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets index 4a5f9e97e..2f8e029eb 100644 --- a/targets/Dalamud.Plugin.targets +++ b/targets/Dalamud.Plugin.targets @@ -18,7 +18,6 @@ - From 8ecc00f75ff8f0ba57d5a32c32d683a14d41a303 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 4 Aug 2023 19:37:43 +0200 Subject: [PATCH 025/585] refactor: move data widgets to correct namespace --- Dalamud/Interface/Internal/Windows/Data/DataWindow.cs | 1 + .../Internal/Windows/Data/Widgets/AddonInspectorWidget.cs | 2 +- .../Interface/Internal/Windows/Data/Widgets/AddonWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/AddressesWidget.cs | 3 +-- .../Internal/Windows/Data/Widgets/AetherytesWidget.cs | 2 +- .../Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs | 6 ++---- .../Internal/Windows/Data/Widgets/BuddyListWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/CommandWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/ConditionWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/ConfigurationWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/DataShareWidget.cs | 2 +- .../Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/FateTableWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/FlyTextWidget.cs | 6 ++---- .../Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs | 3 +-- .../Internal/Windows/Data/Widgets/GamepadWidget.cs | 6 ++---- .../Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs | 2 +- .../Interface/Internal/Windows/Data/Widgets/HookWidget.cs | 6 ++---- .../Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs | 6 ++---- .../Internal/Windows/Data/Widgets/KeyStateWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs | 4 +--- .../Internal/Windows/Data/Widgets/ObjectTableWidget.cs | 6 ++---- .../Internal/Windows/Data/Widgets/PartyListWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/PluginIpcWidget.cs | 6 ++---- .../Internal/Windows/Data/Widgets/SeFontTestWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/StartInfoWidget.cs | 2 +- .../Interface/Internal/Windows/Data/Widgets/TargetWidget.cs | 2 +- .../Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs | 5 ++--- .../Interface/Internal/Windows/Data/Widgets/TexWidget.cs | 3 +-- .../Interface/Internal/Windows/Data/Widgets/ToastWidget.cs | 3 +-- .../Internal/Windows/Data/Widgets/UIColorWidget.cs | 3 +-- 32 files changed, 40 insertions(+), 61 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 54ff4a5ca..60024c3d5 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -5,6 +5,7 @@ using System.Numerics; using Dalamud.Game.Gui; using Dalamud.Interface.Components; +using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs index 977037cc5..af2e6dc2a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying addon inspector. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs index b26b7e311..d378dd63d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs @@ -3,7 +3,7 @@ using Dalamud.Memory; using Dalamud.Utility; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying Addon Data. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs index 606fedadd..b998fea08 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs @@ -1,9 +1,8 @@ using System.Collections.Generic; - using Dalamud.Game; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget to display resolved .text sigs. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs index cc4771847..951417456 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs @@ -1,7 +1,7 @@ using Dalamud.Game.ClientState.Aetherytes; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying aetheryte table. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs index df98f99a6..2680b29cc 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs @@ -1,10 +1,8 @@ -using System; -using System.Numerics; - +using System.Numerics; using Dalamud.Memory; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying AtkArrayData. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs index 2aeb9d10d..80d6fdeb9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs @@ -2,7 +2,7 @@ using Dalamud.Utility; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying data about the Buddy List. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs index e415431ba..136b9356f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs @@ -1,7 +1,7 @@ using Dalamud.Game.Command; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying command info. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs index a5224589f..cb6960b62 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs @@ -1,7 +1,7 @@ using Dalamud.Game.ClientState.Conditions; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying current character condition flags. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs index 3922f22b7..9e490ab1f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs @@ -1,7 +1,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Utility; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying configuration info. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index dc18dbd55..a33cc6cda 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -2,7 +2,7 @@ using Dalamud.Plugin.Ipc.Internal; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying plugin data share modules. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs index 6d3a67e1a..f668c4574 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs @@ -2,7 +2,7 @@ using Dalamud.Game.Gui.Dtr; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying dtr test. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs index 779032f1d..be3183cd8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs @@ -1,7 +1,7 @@ using Dalamud.Game.ClientState.Fates; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying the Fate Table. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs index 99c1a3e02..699938e98 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs @@ -1,10 +1,8 @@ -using System; -using System.Numerics; - +using System.Numerics; using Dalamud.Game.Gui.FlyText; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying fly text info. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index e4284a98e..87744c809 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -1,11 +1,10 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; - using Dalamud.Interface.Utility; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget to display FontAwesome Symbols. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs index 1a4408d53..a49fc131a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs @@ -1,9 +1,7 @@ -using System; - -using Dalamud.Game.ClientState.GamePad; +using Dalamud.Game.ClientState.GamePad; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying gamepad info. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs index 02862b33d..1f7770e74 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs @@ -4,7 +4,7 @@ using Dalamud.Game.ClientState.JobGauge.Types; using Dalamud.Utility; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying job gauge data. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index aa565b1e6..b70883c36 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -1,12 +1,10 @@ -using System; -using System.Runtime.InteropServices; - +using System.Runtime.InteropServices; using Dalamud.Hooking; using ImGuiNET; using PInvoke; using Serilog; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying hook information. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 8afce718f..bb0777bc8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -1,10 +1,8 @@ -using System; - -using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying ImGui test. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs index accc48b4b..02f0a2781 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs @@ -2,7 +2,7 @@ using Dalamud.Interface.Colors; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying keyboard state. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 6f19404ad..e37423c33 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -1,10 +1,8 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text.RegularExpressions; - using Dalamud.Data; using Dalamud.Game.Network; using Dalamud.Interface.Utility; @@ -12,7 +10,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Memory; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget to display the current packets. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index dd41315f2..f5dfbb51b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -1,13 +1,11 @@ -using System; -using System.Numerics; - +using System.Numerics; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Gui; using Dalamud.Utility; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget to display the Object Table. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs index c5ac1fb8f..e923947b7 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs @@ -2,7 +2,7 @@ using Dalamud.Utility; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying information about the current party. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs index 9aae9bba3..b22250fe0 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs @@ -1,12 +1,10 @@ -using System; - -using Dalamud.Plugin.Ipc; +using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Internal; using Dalamud.Utility; using ImGuiNET; using Serilog; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for testing plugin IPC systems. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs index a642c439d..feacbbd48 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs @@ -1,7 +1,7 @@ using Dalamud.Game.Text; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying test data for SE Font Symbols. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs index f414e0957..c0735f8cc 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs @@ -2,7 +2,7 @@ using ImGuiNET; using Newtonsoft.Json; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget to display the currently set server opcodes. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs index 656efe388..18979251c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs @@ -1,7 +1,7 @@ using ImGuiNET; using Newtonsoft.Json; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying start info. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index 64ae041ed..8c11b5a6d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -4,7 +4,7 @@ using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying target info. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index 59ca617f5..de9d8dc4f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -1,9 +1,8 @@ // ReSharper disable MethodSupportsCancellation // Using alternative method of cancelling tasks by throwing exceptions. -using System; + using System.Reflection; using System.Threading; using System.Threading.Tasks; - using Dalamud.Game; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; @@ -11,7 +10,7 @@ using Dalamud.Logging.Internal; using ImGuiNET; using Serilog; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying task scheduler test. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index cc38a58ae..a42351f1a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; using System.IO; using System.Numerics; - using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; using ImGuiNET; using ImGuiScene; using Serilog; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying texture test. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs index 7f020acae..7b147da29 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs @@ -1,10 +1,9 @@ using System.Numerics; - using Dalamud.Game.Gui.Toast; using Dalamud.Interface.Utility; using ImGuiNET; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying toast test. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index 1d0ccdce6..2f281538a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -1,10 +1,9 @@ using System.Numerics; - using Dalamud.Data; using ImGuiNET; using Lumina.Excel.GeneratedSheets; -namespace Dalamud.Interface.Internal.Windows.Data; +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying all UI Colors from Lumina. From 458ae57918721b7ebab88fab590f0aa4dbd78a7f Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 4 Aug 2023 21:52:57 +0200 Subject: [PATCH 026/585] refactor: remove Hook.compatHookImpl, make abstract --- Dalamud/Hooking/Hook.cs | 40 +++++++--------------------------------- 1 file changed, 7 insertions(+), 33 deletions(-) diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 558b6bde1..2e785191c 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; @@ -13,7 +12,7 @@ namespace Dalamud.Hooking; /// This class is basically a thin wrapper around the LocalHook type to provide helper functions. /// /// Delegate type to represents a function prototype. This must be the same prototype as original function do. -public class Hook : IDisposable, IDalamudHook where T : Delegate +public abstract class Hook : IDisposable, IDalamudHook where T : Delegate { #pragma warning disable SA1310 // ReSharper disable once InconsistentNaming @@ -24,8 +23,6 @@ public class Hook : IDisposable, IDalamudHook where T : Delegate private readonly IntPtr address; - private readonly Hook? compatHookImpl; - /// /// Initializes a new instance of the class. /// @@ -52,28 +49,19 @@ public class Hook : IDisposable, IDalamudHook where T : Delegate /// Gets a delegate function that can be used to call the actual function as if function is not hooked yet. /// /// Hook is already disposed. - public virtual T Original => this.compatHookImpl != null ? this.compatHookImpl!.Original : throw new NotImplementedException(); + public virtual T Original => throw new NotImplementedException(); /// /// Gets a delegate function that can be used to call the actual function as if function is not hooked yet. /// This can be called even after Dispose. /// public T OriginalDisposeSafe - { - get - { - if (this.compatHookImpl != null) - return this.compatHookImpl!.OriginalDisposeSafe; - if (this.IsDisposed) - return Marshal.GetDelegateForFunctionPointer(this.address); - return this.Original; - } - } + => this.IsDisposed ? Marshal.GetDelegateForFunctionPointer(this.address) : this.Original; /// /// Gets a value indicating whether or not the hook is enabled. /// - public virtual bool IsEnabled => this.compatHookImpl != null ? this.compatHookImpl!.IsEnabled : throw new NotImplementedException(); + public virtual bool IsEnabled => throw new NotImplementedException(); /// /// Gets a value indicating whether or not the hook has been disposed. @@ -81,7 +69,7 @@ public class Hook : IDisposable, IDalamudHook where T : Delegate public bool IsDisposed { get; private set; } /// - public virtual string BackendName => this.compatHookImpl != null ? this.compatHookImpl!.BackendName : throw new NotImplementedException(); + public virtual string BackendName => throw new NotImplementedException(); /// /// Creates a hook by rewriting import table address. @@ -230,32 +218,18 @@ public class Hook : IDisposable, IDalamudHook where T : Delegate if (this.IsDisposed) return; - this.compatHookImpl?.Dispose(); - this.IsDisposed = true; } /// /// Starts intercepting a call to the function. /// - public virtual void Enable() - { - if (this.compatHookImpl != null) - this.compatHookImpl.Enable(); - else - throw new NotImplementedException(); - } + public virtual void Enable() => throw new NotImplementedException(); /// /// Stops intercepting a call to the function. /// - public virtual void Disable() - { - if (this.compatHookImpl != null) - this.compatHookImpl.Disable(); - else - throw new NotImplementedException(); - } + public virtual void Disable() => throw new NotImplementedException(); /// /// Check if this object has been disposed already. From 2bdc4445d469a081ef03ecec1267c2be3736ee98 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 4 Aug 2023 22:43:01 +0200 Subject: [PATCH 027/585] more warnings --- .../Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs | 1 + .../Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs | 1 + Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs | 1 + .../Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs | 1 + Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs | 1 + .../Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs | 1 + .../Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs | 1 + .../Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs | 1 + Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs | 1 + Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs | 1 + Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs | 1 + 11 files changed, 11 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs index b998fea08..a4e98af79 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; + using Dalamud.Game; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs index 2680b29cc..7e4677fca 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs @@ -1,4 +1,5 @@ using System.Numerics; + using Dalamud.Memory; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs index 699938e98..813e17c97 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs @@ -1,4 +1,5 @@ using System.Numerics; + using Dalamud.Game.Gui.FlyText; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 87744c809..52006419b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; + using Dalamud.Interface.Utility; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index b70883c36..141107a76 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; + using Dalamud.Hooking; using ImGuiNET; using PInvoke; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index e37423c33..cb74462e0 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; using System.Text.RegularExpressions; + using Dalamud.Data; using Dalamud.Game.Network; using Dalamud.Interface.Utility; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index f5dfbb51b..cedadb455 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -1,4 +1,5 @@ using System.Numerics; + using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.Gui; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index de9d8dc4f..a8fdc428d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; + using Dalamud.Game; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index a42351f1a..44d4164d1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Numerics; + using Dalamud.Interface.Utility; using Dalamud.Plugin.Services; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs index 7b147da29..c7eab6e8c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs @@ -1,4 +1,5 @@ using System.Numerics; + using Dalamud.Game.Gui.Toast; using Dalamud.Interface.Utility; using ImGuiNET; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index 2f281538a..4f8af514a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -1,4 +1,5 @@ using System.Numerics; + using Dalamud.Data; using ImGuiNET; using Lumina.Excel.GeneratedSheets; From b96ef30c20446d2a6f4a87ee57be4e61957815f7 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 5 Aug 2023 21:36:40 +0200 Subject: [PATCH 028/585] fix: ITextureProvider thread-safety --- Dalamud/Interface/Internal/TextureManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 983ae9963..1648f1961 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -1,4 +1,4 @@ -using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -485,17 +485,17 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDisposable +internal class TextureProviderPluginScoped : ITextureProvider, IServiceType, IDisposable { private readonly TextureManager textureManager; - private readonly List trackedTextures = new(); + private readonly ConcurrentBag trackedTextures = new(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// TextureManager instance. - public TextureManagerPluginScoped(TextureManager textureManager) + public TextureProviderPluginScoped(TextureManager textureManager) { this.textureManager = textureManager; } From e1da238cb580fb030933ced4fb1a4a7e7c2d33a9 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 6 Aug 2023 20:58:55 +0200 Subject: [PATCH 029/585] feat: IHookProvider service, no more static hook creation --- Dalamud/Hooking/Hook.cs | 54 +++++----- Dalamud/Hooking/IDalamudHook.cs | 2 +- .../Internal/HookProviderPluginScoped.cs | 99 +++++++++++++++++++ Dalamud/Plugin/Services/IHookProvider.cs | 97 ++++++++++++++++++ Dalamud/Utility/Signatures/SignatureHelper.cs | 20 ++-- 5 files changed, 238 insertions(+), 34 deletions(-) create mode 100644 Dalamud/Hooking/Internal/HookProviderPluginScoped.cs create mode 100644 Dalamud/Plugin/Services/IHookProvider.cs diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 2e785191c..da65fedc7 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -12,7 +12,7 @@ namespace Dalamud.Hooking; /// This class is basically a thin wrapper around the LocalHook type to provide helper functions. /// /// Delegate type to represents a function prototype. This must be the same prototype as original function do. -public abstract class Hook : IDisposable, IDalamudHook where T : Delegate +public abstract class Hook : IDalamudHook where T : Delegate { #pragma warning disable SA1310 // ReSharper disable once InconsistentNaming @@ -70,6 +70,27 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// public virtual string BackendName => throw new NotImplementedException(); + + /// + /// Remove a hook from the current process. + /// + public virtual void Dispose() + { + if (this.IsDisposed) + return; + + this.IsDisposed = true; + } + + /// + /// Starts intercepting a call to the function. + /// + public virtual void Enable() => throw new NotImplementedException(); + + /// + /// Stops intercepting a call to the function. + /// + public virtual void Disable() => throw new NotImplementedException(); /// /// Creates a hook by rewriting import table address. @@ -77,7 +98,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. - public static unsafe Hook FromFunctionPointerVariable(IntPtr address, T detour) + internal static Hook FromFunctionPointerVariable(IntPtr address, T detour) { return new FunctionPointerVariableHook(address, detour, Assembly.GetCallingAssembly()); } @@ -91,7 +112,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// Hint or ordinal. 0 to unspecify. /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. - public static unsafe Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) + internal static unsafe Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) { module ??= Process.GetCurrentProcess().MainModule; if (module == null) @@ -156,7 +177,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// A name of the exported function name (e.g. send). /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. - public static Hook FromSymbol(string moduleName, string exportName, T detour) + internal static Hook FromSymbol(string moduleName, string exportName, T detour) => FromSymbol(moduleName, exportName, detour, false); /// @@ -169,7 +190,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// Callback function. Delegate must have a same original function prototype. /// Use the MinHook hooking library instead of Reloaded. /// The hook with the supplied parameters. - public static Hook FromSymbol(string moduleName, string exportName, T detour, bool useMinHook) + internal static Hook FromSymbol(string moduleName, string exportName, T detour, bool useMinHook) { if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; @@ -198,7 +219,7 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate /// Callback function. Delegate must have a same original function prototype. /// Use the MinHook hooking library instead of Reloaded. /// The hook with the supplied parameters. - public static Hook FromAddress(IntPtr procAddress, T detour, bool useMinHook = false) + internal static Hook FromAddress(IntPtr procAddress, T detour, bool useMinHook = false) { if (EnvironmentConfiguration.DalamudForceMinHook) useMinHook = true; @@ -210,27 +231,6 @@ public abstract class Hook : IDisposable, IDalamudHook where T : Delegate return new ReloadedHook(procAddress, detour, Assembly.GetCallingAssembly()); } - /// - /// Remove a hook from the current process. - /// - public virtual void Dispose() - { - if (this.IsDisposed) - return; - - this.IsDisposed = true; - } - - /// - /// Starts intercepting a call to the function. - /// - public virtual void Enable() => throw new NotImplementedException(); - - /// - /// Stops intercepting a call to the function. - /// - public virtual void Disable() => throw new NotImplementedException(); - /// /// Check if this object has been disposed already. /// diff --git a/Dalamud/Hooking/IDalamudHook.cs b/Dalamud/Hooking/IDalamudHook.cs index 1104597a1..bd7084d86 100644 --- a/Dalamud/Hooking/IDalamudHook.cs +++ b/Dalamud/Hooking/IDalamudHook.cs @@ -5,7 +5,7 @@ namespace Dalamud.Hooking; /// /// Interface describing a generic hook. /// -public interface IDalamudHook +public interface IDalamudHook : IDisposable { /// /// Gets the address to hook. diff --git a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs new file mode 100644 index 000000000..fa4497799 --- /dev/null +++ b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs @@ -0,0 +1,99 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; + +using Dalamud.Game; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; +using Dalamud.Utility.Signatures; +using Serilog; + +namespace Dalamud.Hooking.Internal; + +/// +/// Plugin-scoped version of a texture manager. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class HookProviderPluginScoped : IHookProvider, IServiceType, IDisposable +{ + private readonly LocalPlugin plugin; + private readonly SigScanner scanner; + + private readonly ConcurrentBag trackedHooks = new(); + + /// + /// Initializes a new instance of the class. + /// + /// Plugin this instance belongs to. + /// SigScanner instance for target module. + public HookProviderPluginScoped(LocalPlugin plugin, SigScanner scanner) + { + this.plugin = plugin; + this.scanner = scanner; + } + + /// + public void InitializeFromAttributes(object self) + { + foreach (var hook in SignatureHelper.Initialise(self)) + this.trackedHooks.Add(hook); + } + + /// + public Hook FromFunctionPointerVariable(IntPtr address, T detour) where T : Delegate + { + var hook = Hook.FromFunctionPointerVariable(address, detour); + this.trackedHooks.Add(hook); + return hook; + } + + /// + public Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate + { + var hook = Hook.FromImport(module, moduleName, functionName, hintOrOrdinal, detour); + this.trackedHooks.Add(hook); + return hook; + } + + /// + public Hook FromSymbol(string moduleName, string exportName, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate + { + var hook = Hook.FromSymbol(moduleName, exportName, detour, backend == IHookProvider.HookBackend.MinHook); + this.trackedHooks.Add(hook); + return hook; + } + + /// + public Hook FromAddress(IntPtr procAddress, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate + { + var hook = Hook.FromAddress(procAddress, detour, backend == IHookProvider.HookBackend.MinHook); + this.trackedHooks.Add(hook); + return hook; + } + + /// + public Hook FromSignature(string signature, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate + => this.FromAddress(this.scanner.ScanText(signature), detour); + + /// + public void Dispose() + { + var notDisposed = this.trackedHooks.Where(x => !x.IsDisposed).ToArray(); + if (notDisposed.Length != 0) + Log.Warning("{PluginName} is leaking {Num} hooks! Make sure that all of them are disposed properly.", this.plugin.InternalName, notDisposed.Length); + + foreach (var hook in notDisposed) + { + hook.Dispose(); + } + + this.trackedHooks.Clear(); + } +} diff --git a/Dalamud/Plugin/Services/IHookProvider.cs b/Dalamud/Plugin/Services/IHookProvider.cs new file mode 100644 index 000000000..dc7d29913 --- /dev/null +++ b/Dalamud/Plugin/Services/IHookProvider.cs @@ -0,0 +1,97 @@ +using System.Diagnostics; + +using Dalamud.Hooking; +using Dalamud.Utility.Signatures; + +namespace Dalamud.Plugin.Services; + +/// +/// Service responsible for the creation of hooks. +/// +public interface IHookProvider +{ + /// + /// Available hooking backends. + /// + public enum HookBackend + { + /// + /// Choose the best backend automatically. + /// + Automatic, + + /// + /// Use Reloaded hooks. + /// + Reloaded, + + /// + /// Use MinHook. + /// You should never have to use this without talking to us first. + /// + MinHook, + } + + /// + /// Initialize members decorated with the . + /// Errors for fallible signatures will be logged. + /// + /// The object to initialise. + public void InitializeFromAttributes(object self); + + /// + /// Creates a hook by rewriting import table address. + /// + /// A memory address to install a hook. + /// Callback function. Delegate must have a same original function prototype. + /// The hook with the supplied parameters. + /// Delegate of detour. + public Hook FromFunctionPointerVariable(IntPtr address, T detour) where T : Delegate; + + /// + /// Creates a hook by rewriting import table address. + /// + /// Module to check for. Current process' main module if null. + /// Name of the DLL, including the extension. + /// Decorated name of the function. + /// Hint or ordinal. 0 to unspecify. + /// Callback function. Delegate must have a same original function prototype. + /// The hook with the supplied parameters. + /// Delegate of detour. + public Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate; + + /// + /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. + /// The hook is not activated until Enable() method is called. + /// Please do not use MinHook unless you have thoroughly troubleshot why Reloaded does not work. + /// + /// A name of the module currently loaded in the memory. (e.g. ws2_32.dll). + /// A name of the exported function name (e.g. send). + /// Callback function. Delegate must have a same original function prototype. + /// Hooking library to use. + /// The hook with the supplied parameters. + /// Delegate of detour. + Hook FromSymbol(string moduleName, string exportName, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; + + /// + /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. + /// The hook is not activated until Enable() method is called. + /// Please do not use MinHook unless you have thoroughly troubleshot why Reloaded does not work. + /// + /// A memory address to install a hook. + /// Callback function. Delegate must have a same original function prototype. + /// Hooking library to use. + /// The hook with the supplied parameters. + /// Delegate of detour. + Hook FromAddress(IntPtr procAddress, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; + + /// + /// Creates a hook from a signature into the Dalamud target module. + /// + /// Signature of function to hook. + /// Callback function. Delegate must have a same original function prototype. + /// Hooking library to use. + /// The hook with the supplied parameters. + /// Delegate of detour. + Hook FromSignature(string signature, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; +} diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index bd99b8515..e133e5453 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; @@ -6,6 +7,7 @@ using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Hooking; using Dalamud.Logging; +using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures.Wrappers; using Serilog; @@ -14,7 +16,7 @@ namespace Dalamud.Utility.Signatures; /// /// A utility class to help reduce signature boilerplate code. /// -public static class SignatureHelper +internal static class SignatureHelper { private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; @@ -24,7 +26,8 @@ public static class SignatureHelper /// /// The object to initialise. /// If warnings should be logged using . - public static void Initialise(object self, bool log = true) + /// Collection of created IDalamudHooks. + internal static IEnumerable Initialise(object self, bool log = true) { var scanner = Service.Get(); var selfType = self.GetType(); @@ -33,6 +36,8 @@ public static class SignatureHelper .Select(field => (field, field.GetCustomAttribute())) .Where(field => field.Item2 != null); + var createdHooks = new List(); + foreach (var (info, sig) in fields) { var wasWrapped = false; @@ -149,15 +154,16 @@ public static class SignatureHelper detour = del; } - var ctor = actualType.GetConstructor(new[] { typeof(IntPtr), hookDelegateType }); - if (ctor == null) + var creator = actualType.GetMethod("FromAddress", BindingFlags.Static | BindingFlags.NonPublic); + if (creator == null) { - Log.Error("Error in SignatureHelper: could not find Hook constructor"); + Log.Error("Error in SignatureHelper: could not find Hook creator"); continue; } - var hook = ctor.Invoke(new object?[] { ptr, detour }); + var hook = creator.Invoke(null, new object?[] { ptr, detour, IHookProvider.HookBackend.Automatic }) as IDalamudHook; info.SetValue(self, hook); + createdHooks.Add(hook); break; } @@ -182,5 +188,7 @@ public static class SignatureHelper } } } + + return createdHooks; } } From fe8ee19175df57334dd2e5aaea4899ea078c3a95 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 6 Aug 2023 21:08:49 +0200 Subject: [PATCH 030/585] fix comment --- Dalamud/Hooking/Internal/HookProviderPluginScoped.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs index fa4497799..0e7ef4c7b 100644 --- a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs @@ -13,7 +13,7 @@ using Serilog; namespace Dalamud.Hooking.Internal; /// -/// Plugin-scoped version of a texture manager. +/// Plugin-scoped version of service used to create hooks. /// [PluginInterface] [InterfaceVersion("1.0")] From 3f764d2e400bd585798f7e4ee58f6d4790573575 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 7 Aug 2023 23:57:31 +0200 Subject: [PATCH 031/585] pass on backend, spelling --- Dalamud/Hooking/Internal/HookProviderPluginScoped.cs | 4 ++-- Dalamud/Utility/Signatures/SignatureHelper.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs index 0e7ef4c7b..6fa700cef 100644 --- a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs @@ -42,7 +42,7 @@ internal class HookProviderPluginScoped : IHookProvider, IServiceType, IDisposab /// public void InitializeFromAttributes(object self) { - foreach (var hook in SignatureHelper.Initialise(self)) + foreach (var hook in SignatureHelper.Initialize(self)) this.trackedHooks.Add(hook); } @@ -80,7 +80,7 @@ internal class HookProviderPluginScoped : IHookProvider, IServiceType, IDisposab /// public Hook FromSignature(string signature, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate - => this.FromAddress(this.scanner.ScanText(signature), detour); + => this.FromAddress(this.scanner.ScanText(signature), detour, backend); /// public void Dispose() diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index e133e5453..1cfd18330 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -27,7 +27,7 @@ internal static class SignatureHelper /// The object to initialise. /// If warnings should be logged using . /// Collection of created IDalamudHooks. - internal static IEnumerable Initialise(object self, bool log = true) + internal static IEnumerable Initialize(object self, bool log = true) { var scanner = Service.Get(); var selfType = self.GetType(); From a6ed6dabe4206b445cad9b0f16d53633287c3bdf Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 10 Aug 2023 02:10:45 +0200 Subject: [PATCH 032/585] refactor: use IEnumerable instead of List in SeString.Append --- Dalamud/Game/Text/SeStringHandling/SeString.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 2ddb73f12..6132d0910 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -421,7 +421,7 @@ public class SeString /// /// The Payloads to append. /// This object. - public SeString Append(List payloads) + public SeString Append(IEnumerable payloads) { this.Payloads.AddRange(payloads); return this; From 54790711ddcd6604f108f31fbd93fd4c3ec15240 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 10 Aug 2023 02:11:59 +0200 Subject: [PATCH 033/585] refactor: append payload list directly in SeStringBuilder --- Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs index 1fda9f9ae..dae9e11a9 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs @@ -38,7 +38,11 @@ public class SeStringBuilder /// /// A list of payloads. /// The current builder. - public SeStringBuilder Append(IEnumerable payloads) => this.Append(new SeString(payloads.ToList())); + public SeStringBuilder Append(IEnumerable payloads) + { + this.BuiltString.Payloads.AddRange(payloads); + return this; + } /// /// Append raw text to the builder. From 78400c8cb6ea95b6cfbdbf362dbfdcc5e4649673 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 16 Aug 2023 02:35:30 +0200 Subject: [PATCH 034/585] Update ClientStructs (#1346) 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 9b2eab0f2..e6d2de724 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9b2eab0f212030c062427b307b96118881d36b99 +Subproject commit e6d2de7240f8cbc6a9a2a7133ad633ebfef6d33e From c0f828f5fa3e793ec9aa6fe0059f78f2b8753095 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 19 Aug 2023 20:18:37 +0200 Subject: [PATCH 035/585] Update ClientStructs (#1349) 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 e6d2de724..81833c4c8 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e6d2de7240f8cbc6a9a2a7133ad633ebfef6d33e +Subproject commit 81833c4c8b79bdcbb21fc6fbfb3d298767368872 From 8cbe7cbb3068bb4d04adc1eb2642aa1cb7376504 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 22 Aug 2023 02:51:40 +0200 Subject: [PATCH 036/585] Update ClientStructs (#1352) 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 81833c4c8..de4c356bf 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 81833c4c8b79bdcbb21fc6fbfb3d298767368872 +Subproject commit de4c356bf27da2e3d596478c0192ce0baa75c125 From a0edd21620f4e3c80616455aca2c6fdd68a30a64 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 22 Aug 2023 09:11:09 -0700 Subject: [PATCH 037/585] build: 7.10.2.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index e2da1a057..be04c5a20 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.10.1.0 + 7.10.2.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 07692753dbeccdbda100451db3117a52563523a6 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 22 Aug 2023 21:49:38 +0200 Subject: [PATCH 038/585] fix: don't draw changelog twice --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index b648a8204..6f7496c72 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2149,6 +2149,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}"); var hasChangelog = !plugin.Manifest.Changelog.IsNullOrEmpty(); + var didDrawChangelogInsideCollapsible = false; if (this.DrawPluginCollapsingHeader(label, plugin, plugin.Manifest, plugin.IsThirdParty, trouble, availablePluginUpdate != default, false, false, plugin.IsOrphaned, () => this.DrawInstalledPluginContextMenu(plugin, testingOptIn), index)) { @@ -2257,6 +2258,7 @@ internal class PluginInstallerWindow : Window, IDisposable { if (ImGui.TreeNode(Locs.PluginBody_CurrentChangeLog(plugin.EffectiveVersion))) { + didDrawChangelogInsideCollapsible = true; this.DrawInstalledPluginChangelog(plugin.Manifest); ImGui.TreePop(); } @@ -2273,7 +2275,7 @@ internal class PluginInstallerWindow : Window, IDisposable } } - if (thisWasUpdated && hasChangelog) + if (thisWasUpdated && hasChangelog && !didDrawChangelogInsideCollapsible) { this.DrawInstalledPluginChangelog(plugin.Manifest); } From c027aacde2020846cfeec141e6426d731c397103 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 22 Aug 2023 22:19:10 +0200 Subject: [PATCH 039/585] feat: add ImGuiComponents.IconButtonWithText() --- .../Components/ImGuiComponents.IconButton.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs index 99e43d68c..05e660b61 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs @@ -1,3 +1,4 @@ +using System; using System.Numerics; using ImGuiNET; @@ -119,4 +120,71 @@ public static partial class ImGuiComponents return button; } + + /// + /// IconButton component to use an icon as a button with color options. + /// + /// Icon to show. + /// Text to show. + /// The default color of the button. + /// The color of the button when active. + /// The color of the button when hovered. + /// Indicator if button is clicked. + public static bool IconButtonWithText(FontAwesomeIcon icon, string text, Vector4? defaultColor = null, Vector4? activeColor = null, Vector4? hoveredColor = null) + { + var numColors = 0; + + if (defaultColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.Button, defaultColor.Value); + numColors++; + } + + if (activeColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.ButtonActive, activeColor.Value); + numColors++; + } + + if (hoveredColor.HasValue) + { + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, hoveredColor.Value); + numColors++; + } + + ImGui.PushID(text); + + ImGui.PushFont(UiBuilder.IconFont); + var iconSize = ImGui.CalcTextSize(icon.ToIconString()); + ImGui.PopFont(); + + var textSize = ImGui.CalcTextSize(text); + var dl = ImGui.GetWindowDrawList(); + var cursor = ImGui.GetCursorScreenPos(); + + var iconPadding = 3 * ImGuiHelpers.GlobalScale; + + // Draw an ImGui button with the icon and text + var buttonWidth = iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding; + var buttonHeight = Math.Max(iconSize.Y, textSize.Y) + (ImGui.GetStyle().FramePadding.Y * 2); + var button = ImGui.Button(string.Empty, new Vector2(buttonWidth, buttonHeight)); + + // Draw the icon on the window drawlist + var iconPos = new Vector2(cursor.X + ImGui.GetStyle().FramePadding.X, cursor.Y + ImGui.GetStyle().FramePadding.Y); + + ImGui.PushFont(UiBuilder.IconFont); + dl.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon.ToIconString()); + ImGui.PopFont(); + + // Draw the text on the window drawlist + var textPos = new Vector2(iconPos.X + iconSize.X + iconPadding, cursor.Y + ImGui.GetStyle().FramePadding.Y); + dl.AddText(textPos, ImGui.GetColorU32(ImGuiCol.Text), text); + + ImGui.PopID(); + + if (numColors > 0) + ImGui.PopStyleColor(numColors); + + return button; + } } From 3272dbb0e2343242338cadb6b57bfc6549f8286c Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 22 Aug 2023 22:20:08 +0200 Subject: [PATCH 040/585] feat: add OpenMainUi event on UiBuilder, respective button in PI --- Dalamud.CorePlugin/PluginImpl.cs | 7 +++ .../PluginInstaller/PluginInstallerWindow.cs | 48 +++++++++++++++++-- Dalamud/Interface/UiBuilder.cs | 18 +++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 9026ea0dd..b858e9a0c 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Utility; +using Serilog; namespace Dalamud.CorePlugin { @@ -66,6 +67,7 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; + this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." }); @@ -143,6 +145,11 @@ namespace Dalamud.CorePlugin // this.window.IsOpen = true; } + private void OnOpenMainUi() + { + Log.Verbose("Opened main UI"); + } + #endif } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 6f7496c72..ad9d77754 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2227,6 +2227,8 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGuiHelpers.SafeTextWrapped($"{command.Key} → {command.Value.HelpMessage}"); } + + ImGuiHelpers.ScaledDummy(3); } } @@ -2573,6 +2575,9 @@ internal class PluginInstallerWindow : Window, IDisposable { // Only if the plugin isn't broken. this.DrawOpenPluginSettingsButton(plugin); + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(5, 0); } if (applicableForProfiles && config.ProfilesEnabled) @@ -2637,10 +2642,39 @@ internal class PluginInstallerWindow : Window, IDisposable private void DrawOpenPluginSettingsButton(LocalPlugin plugin) { - if (plugin.DalamudInterface?.UiBuilder?.HasConfigUi ?? false) + var hasMainUi = plugin.DalamudInterface?.UiBuilder.HasMainUi ?? false; + var hasConfig = plugin.DalamudInterface?.UiBuilder.HasConfigUi ?? false; + if (hasMainUi) { ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.ArrowUpRightFromSquare, Locs.PluginButton_OpenUi)) + { + try + { + plugin.DalamudInterface.UiBuilder.OpenMain(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error during OpenMain(): {plugin.Name}"); + } + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(Locs.PluginButtonToolTip_OpenUi); + } + } + + if (hasConfig) + { + if (hasMainUi) + { + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(5, 0); + } + + ImGui.SameLine(); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Cog, Locs.PluginButton_OpenSettings)) { try { @@ -2648,7 +2682,7 @@ internal class PluginInstallerWindow : Window, IDisposable } catch (Exception ex) { - Log.Error(ex, $"Error during OpenConfigUi: {plugin.Name}"); + Log.Error(ex, $"Error during OpenConfig: {plugin.Name}"); } } @@ -3236,12 +3270,18 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButton_Unload => Loc.Localize("InstallerUnload", "Unload"); public static string PluginButton_SafeMode => Loc.Localize("InstallerSafeModeButton", "Can't change in safe mode"); + + public static string PluginButton_OpenUi => Loc.Localize("InstallerOpenPluginUi", "Open"); + + public static string PluginButton_OpenSettings => Loc.Localize("InstallerOpenPluginSettings", "Settings"); #endregion #region Plugin button tooltips + + public static string PluginButtonToolTip_OpenUi => Loc.Localize("InstallerTooltipOpenUi", "Open this plugin's interface"); - public static string PluginButtonToolTip_OpenConfiguration => Loc.Localize("InstallerOpenConfig", "Open Configuration"); + public static string PluginButtonToolTip_OpenConfiguration => Loc.Localize("InstallerTooltipOpenConfig", "Open this plugin's settings"); public static string PluginButtonToolTip_PickProfiles => Loc.Localize("InstallerPickProfiles", "Pick collections for this plugin"); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index eca0f64a0..b440a0705 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -69,6 +69,11 @@ public sealed class UiBuilder : IDisposable /// Event that is fired when the plugin should open its configuration interface. /// public event Action OpenConfigUi; + + /// + /// Event that is fired when the plugin should open its main interface. + /// + public event Action OpenMainUi; /// /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
@@ -212,6 +217,11 @@ public sealed class UiBuilder : IDisposable ///
internal bool HasConfigUi => this.OpenConfigUi != null; + /// + /// Gets a value indicating whether this UiBuilder has a configuration UI registered. + /// + internal bool HasMainUi => this.OpenMainUi != null; + /// /// Gets or sets the time this plugin took to draw on the last frame. /// @@ -409,6 +419,14 @@ public sealed class UiBuilder : IDisposable { this.OpenConfigUi?.InvokeSafely(); } + + /// + /// Open the registered configuration UI, if it exists. + /// + internal void OpenMain() + { + this.OpenMainUi?.InvokeSafely(); + } /// /// Notify this UiBuilder about plugin UI being hidden. From 122f4a462b293b02c73c1dec954198df8610ad6b Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 22 Aug 2023 22:24:27 +0200 Subject: [PATCH 041/585] build: 7.11.0.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index be04c5a20..af785cf52 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.10.2.0 + 7.11.0.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 84cd209d86ddf62fc6f76165c8132312e73c5281 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 20:48:57 +0200 Subject: [PATCH 042/585] chore: increase size of profiles tutorial --- .../Internal/Windows/PluginInstaller/ProfileManagerWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 301e43473..835a8a60c 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -78,7 +78,7 @@ internal class ProfileManagerWidget private void DrawTutorial(string modalTitle) { var open = true; - ImGui.SetNextWindowSize(new Vector2(450, 350), ImGuiCond.Appearing); + ImGui.SetNextWindowSize(new Vector2(650, 550), ImGuiCond.Appearing); using (var popup = ImRaii.PopupModal(modalTitle, ref open)) { if (popup) From d7f0f5d88844e0778756aad9c9c4a7f9164cad94 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 21:37:33 +0200 Subject: [PATCH 043/585] feat: add ColorHelpers class, various tools to manipulate HSV colors --- Dalamud/Interface/ColorHelpers.cs | 261 ++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 Dalamud/Interface/ColorHelpers.cs diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs new file mode 100644 index 000000000..71f959292 --- /dev/null +++ b/Dalamud/Interface/ColorHelpers.cs @@ -0,0 +1,261 @@ +using System; +using System.Numerics; + +namespace Dalamud.Interface; + +/// +/// Class containing various methods for manipulating colors. +/// +public static class ColorHelpers +{ + /// + /// Pack a vector4 color into a uint for use in ImGui APIs. + /// + /// The color to pack. + /// The packed color. + public static uint RgbaVector4ToUint(Vector4 color) + { + var r = (byte)(color.X * 255); + var g = (byte)(color.Y * 255); + var b = (byte)(color.Z * 255); + var a = (byte)(color.W * 255); + + return (uint)((a << 24) | (b << 16) | (g << 8) | r); + } + + /// + /// Convert a RGBA color in the range of 0.f to 1.f to a uint. + /// + /// The color to pack. + /// The packed color. + public static Vector4 RgbaUintToVector4(uint color) + { + var r = (color & 0x000000FF) / 255f; + var g = ((color & 0x0000FF00) >> 8) / 255f; + var b = ((color & 0x00FF0000) >> 16) / 255f; + var a = ((color & 0xFF000000) >> 24) / 255f; + + return new Vector4(r, g, b, a); + } + + /// + /// Convert a RGBA color in the range of 0.f to 1.f to a HSV color. + /// + /// The color to convert. + /// The color in a HSV representation. + public static HsvaColor RgbaToHsv(Vector4 color) + { + var r = color.X; + var g = color.Y; + var b = color.Z; + + var max = Math.Max(r, Math.Max(g, b)); + var min = Math.Min(r, Math.Min(g, b)); + + var h = max; + var s = max; + var v = max; + + var d = max - min; + s = max == 0 ? 0 : d / max; + + if (max == min) + { + h = 0; // achromatic + } + else + { + if (max == r) + { + h = ((g - b) / d) + (g < b ? 6 : 0); + } + else if (max == g) + { + h = ((b - r) / d) + 2; + } + else if (max == b) + { + h = ((r - g) / d) + 4; + } + + h /= 6; + } + + return new HsvaColor(h, s, v, color.W); + } + + /// + /// Convert a HSV color to a RGBA color in the range of 0.f to 1.f. + /// + /// The color to convert. + /// The RGB color. + public static Vector4 HsvToRgb(HsvaColor hsv) + { + var h = hsv.H; + var s = hsv.S; + var v = hsv.V; + + var r = 0f; + var g = 0f; + var b = 0f; + + var i = (int)Math.Floor(h * 6); + var f = (h * 6) - i; + var p = v * (1 - s); + var q = v * (1 - (f * s)); + var t = v * (1 - ((1 - f) * s)); + + switch (i % 6) + { + case 0: + r = v; + g = t; + b = p; + break; + + case 1: + r = q; + g = v; + b = p; + break; + + case 2: + r = p; + g = v; + b = t; + break; + + case 3: + r = p; + g = q; + b = v; + break; + + case 4: + r = t; + g = p; + b = v; + break; + + case 5: + r = v; + g = p; + b = q; + break; + } + + return new Vector4(r, g, b, hsv.A); + } + + /// + /// Lighten a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The lightened color. + public static Vector4 Lighten(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.V += amount; + return HsvToRgb(hsv); + } + + /// + /// Lighten a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The lightened color. + public static uint Lighten(uint color, float amount) + => RgbaVector4ToUint(Lighten(RgbaUintToVector4(color), amount)); + + /// + /// Darken a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The darkened color. + public static Vector4 Darken(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.V -= amount; + return HsvToRgb(hsv); + } + + /// + /// Darken a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The darkened color. + public static uint Darken(uint color, float amount) + => RgbaVector4ToUint(Darken(RgbaUintToVector4(color), amount)); + + /// + /// Saturate a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The saturated color. + public static Vector4 Saturate(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.S += amount; + return HsvToRgb(hsv); + } + + /// + /// Saturate a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The saturated color. + public static uint Saturate(uint color, float amount) + => RgbaVector4ToUint(Saturate(RgbaUintToVector4(color), amount)); + + /// + /// Desaturate a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The desaturated color. + public static Vector4 Desaturate(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.S -= amount; + return HsvToRgb(hsv); + } + + /// + /// Desaturate a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The desaturated color. + public static uint Desaturate(uint color, float amount) + => RgbaVector4ToUint(Desaturate(RgbaUintToVector4(color), amount)); + + /// + /// Fade a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The faded color. + public static Vector4 Fade(this Vector4 color, float amount) + { + var hsv = RgbaToHsv(color); + hsv.A -= amount; + return HsvToRgb(hsv); + } + + /// + /// Fade a color. + /// + /// The color to lighten. + /// The amount to lighten. + /// The faded color. + public static uint Fade(uint color, float amount) + => RgbaVector4ToUint(Fade(RgbaUintToVector4(color), amount)); + + public record struct HsvaColor(float H, float S, float V, float A); +} From d2e463247cd2c12e48dba36b1943e6776de0fc6f Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 21:40:22 +0200 Subject: [PATCH 044/585] chore: slightly tweak available plugin buttons --- .../PluginInstaller/PluginInstallerWindow.cs | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index ad9d77754..2e2830581 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1437,7 +1437,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.Button($"{buttonText}##{buttonText}testing"); } - this.DrawVisitRepoUrlButton("https://google.com"); + this.DrawVisitRepoUrlButton("https://google.com", true); if (this.testerImages != null) { @@ -1954,6 +1954,7 @@ internal class PluginInstallerWindow : Window, IDisposable } else { + using var color = ImRaii.PushColor(ImGuiCol.Button, ImGuiColors.DalamudRed.Darken(0.3f).Fade(0.4f)); var buttonText = Locs.PluginButton_InstallVersion(versionString); if (ImGui.Button($"{buttonText}##{buttonText}{index}")) { @@ -1961,11 +1962,19 @@ internal class PluginInstallerWindow : Window, IDisposable } } - this.DrawVisitRepoUrlButton(manifest.RepoUrl); + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(10); + ImGui.SameLine(); + + this.DrawVisitRepoUrlButton(manifest.RepoUrl, true); + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(3); + ImGui.SameLine(); if (!manifest.SourceRepo.IsThirdParty && manifest.AcceptsFeedback) { - this.DrawSendFeedbackButton(manifest, false); + this.DrawSendFeedbackButton(manifest, false, true); } ImGuiHelpers.ScaledDummy(5); @@ -2235,12 +2244,12 @@ internal class PluginInstallerWindow : Window, IDisposable // Controls this.DrawPluginControlButton(plugin, availablePluginUpdate); this.DrawDevPluginButtons(plugin); + this.DrawVisitRepoUrlButton(plugin.Manifest.RepoUrl, false); this.DrawDeletePluginButton(plugin); - this.DrawVisitRepoUrlButton(plugin.Manifest.RepoUrl); if (canFeedback) { - this.DrawSendFeedbackButton(plugin.Manifest, plugin.IsTesting); + this.DrawSendFeedbackButton(plugin.Manifest, plugin.IsTesting, false); } if (availablePluginUpdate != default && !plugin.IsDev) @@ -2693,10 +2702,15 @@ internal class PluginInstallerWindow : Window, IDisposable } } - private void DrawSendFeedbackButton(IPluginManifest manifest, bool isTesting) + private void DrawSendFeedbackButton(IPluginManifest manifest, bool isTesting, bool big) { ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Comment)) + + var clicked = big ? + ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Comment, Locs.FeedbackModal_Title) : + ImGuiComponents.IconButton(FontAwesomeIcon.Comment); + + if (clicked) { this.feedbackPlugin = manifest; this.feedbackModalOnNextFrame = true; @@ -2842,12 +2856,16 @@ internal class PluginInstallerWindow : Window, IDisposable } } - private void DrawVisitRepoUrlButton(string? repoUrl) + private void DrawVisitRepoUrlButton(string? repoUrl, bool big) { if (!string.IsNullOrEmpty(repoUrl) && repoUrl.StartsWith("https://")) { ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Globe)) + + var clicked = big ? + ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Globe, "Open website") : + ImGuiComponents.IconButton(FontAwesomeIcon.Globe); + if (clicked) { try { From 3c8e474fe506254a2891839ba8293d1057d58940 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 21:51:46 +0200 Subject: [PATCH 045/585] fix: internal TSM entries must always come first --- .../Internal/Windows/TitleScreenMenuWindow.cs | 17 +++++++++-------- Dalamud/Interface/TitleScreenMenu.cs | 15 +++++++++++++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 10180f0c3..8c835c76a 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -97,16 +97,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable public override void Draw() { var scale = ImGui.GetIO().FontGlobalScale; - - var tsm = Service.Get(); + var entries = Service.Get().Entries + .OrderByDescending(x => x.IsInternal) + .ToList(); switch (this.state) { case State.Show: { - for (var i = 0; i < tsm.Entries.Count; i++) + for (var i = 0; i < entries.Count; i++) { - var entry = tsm.Entries[i]; + var entry = entries[i]; if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing)) { @@ -172,9 +173,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value)) { - for (var i = 0; i < tsm.Entries.Count; i++) + for (var i = 0; i < entries.Count; i++) { - var entry = tsm.Entries[i]; + var entry = entries[i]; var finalPos = (i + 1) * this.shadeTexture.Height * scale; @@ -205,7 +206,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable case State.Hide: { - if (this.DrawEntry(tsm.Entries[0], true, false, true, true, false)) + if (this.DrawEntry(entries[0], true, false, true, true, false)) { this.state = State.Show; } @@ -217,7 +218,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable } } - var srcText = tsm.Entries.Select(e => e.Name).ToHashSet(); + var srcText = entries.Select(e => e.Name).ToHashSet(); var keys = this.specialGlyphRequests.Keys.ToHashSet(); keys.RemoveWhere(x => srcText.Contains(x)); foreach (var key in keys) diff --git a/Dalamud/Interface/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu.cs index 7b3897fdb..c9e1458d6 100644 --- a/Dalamud/Interface/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu.cs @@ -121,7 +121,10 @@ public class TitleScreenMenu : IServiceType lock (this.entries) { - var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered); + var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered) + { + IsInternal = true, + }; this.entries.Add(entry); return entry; } @@ -148,7 +151,10 @@ public class TitleScreenMenu : IServiceType var priority = entriesOfAssembly.Any() ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) : 0; - var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered); + var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered) + { + IsInternal = true, + }; this.entries.Add(entry); return entry; } @@ -192,6 +198,11 @@ public class TitleScreenMenu : IServiceType /// Gets or sets the texture of this entry. /// public TextureWrap Texture { get; set; } + + /// + /// Gets or sets a value indicating whether or not this entry is internal. + /// + internal bool IsInternal { get; set; } /// /// Gets the calling assembly of this entry. From 4c177d32b4bc84260fb41915a40f6950f46d7198 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 23 Aug 2023 22:01:57 +0200 Subject: [PATCH 046/585] fix: ensure that toggle button stays enabled while enabling --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 2e2830581..27bf83dbd 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -108,7 +108,9 @@ internal class PluginInstallerWindow : Window, IDisposable private OperationStatus installStatus = OperationStatus.Idle; private OperationStatus updateStatus = OperationStatus.Idle; + private OperationStatus enableDisableStatus = OperationStatus.Idle; + private Guid enableDisableWorkingPluginId = Guid.Empty; private LoadingIndicatorKind loadingIndicatorKind = LoadingIndicatorKind.Unknown; @@ -2477,6 +2479,10 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.PluginButtonToolTip_UnloadFailed); } + else if (this.enableDisableStatus == OperationStatus.InProgress && this.enableDisableWorkingPluginId == plugin.Manifest.WorkingPluginId) + { + ImGuiComponents.DisabledToggleButton(toggleId, this.loadingIndicatorKind == LoadingIndicatorKind.EnablingSingle); + } else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled) { ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable); @@ -2516,6 +2522,7 @@ internal class PluginInstallerWindow : Window, IDisposable { this.enableDisableStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.DisablingSingle; + this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; Task.Run(async () => { @@ -2536,6 +2543,7 @@ internal class PluginInstallerWindow : Window, IDisposable { this.enableDisableStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; + this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; await applicableProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false); await plugin.LoadAsync(PluginLoadReason.Installer); From 2d528f0515eca59e7e4a58df0ac45d6c3b004e26 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 25 Aug 2023 12:49:32 -0700 Subject: [PATCH 047/585] Update SettingsTabExperimental.cs (#1355) --- .../Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index 62981f4a2..15dab7d94 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -25,7 +25,7 @@ public class SettingsTabExperimental : SettingsTab c => c.DoPluginTest, (v, c) => c.DoPluginTest = v), new HintSettingsEntry( - Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may not have been vetted before being published. Please only enable this if you are aware of the risks."), + Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."), ImGuiColors.DalamudRed), new GapSettingsEntry(5), From 3afe9c41a552568b2f1c390d77e262b3283813f1 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 25 Aug 2023 21:50:20 +0200 Subject: [PATCH 048/585] Update ClientStructs (#1354) 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 de4c356bf..b7ef3eaf0 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit de4c356bf27da2e3d596478c0192ce0baa75c125 +Subproject commit b7ef3eaf02334aad36da26ff25f5f70dee411894 From 1ab198af8bbfcb4fbdcf61ed1bb181071b920c0b Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 26 Aug 2023 19:53:16 +0200 Subject: [PATCH 049/585] Update ClientStructs (#1358) 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 b7ef3eaf0..1e52770d5 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b7ef3eaf02334aad36da26ff25f5f70dee411894 +Subproject commit 1e52770d5367c8237ac78688774d136118994bf7 From f7c2ab528338237ae123ffaa3e3e52daeb7f9184 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 27 Aug 2023 14:46:44 +0200 Subject: [PATCH 050/585] Support Unicode names correctly. --- Dalamud/Interface/DragDrop/DragDropInterop.cs | 2 +- Dalamud/Interface/DragDrop/DragDropTarget.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/DragDrop/DragDropInterop.cs b/Dalamud/Interface/DragDrop/DragDropInterop.cs index 28a2644a5..68418d4b0 100644 --- a/Dalamud/Interface/DragDrop/DragDropInterop.cs +++ b/Dalamud/Interface/DragDrop/DragDropInterop.cs @@ -101,7 +101,7 @@ internal partial class DragDropManager public static extern int RevokeDragDrop(nint hwnd); [DllImport("shell32.dll")] - public static extern int DragQueryFile(IntPtr hDrop, uint iFile, StringBuilder lpszFile, int cch); + public static extern int DragQueryFileW(IntPtr hDrop, uint iFile, [MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpszFile, int cch); } } #pragma warning restore SA1600 // Elements should be documented diff --git a/Dalamud/Interface/DragDrop/DragDropTarget.cs b/Dalamud/Interface/DragDrop/DragDropTarget.cs index 5e7166fb3..01a48173a 100644 --- a/Dalamud/Interface/DragDrop/DragDropTarget.cs +++ b/Dalamud/Interface/DragDrop/DragDropTarget.cs @@ -204,7 +204,7 @@ internal partial class DragDropManager : DragDropManager.IDropTarget try { data.GetData(ref this.formatEtc, out var stgMedium); - var numFiles = DragDropInterop.DragQueryFile(stgMedium.unionmember, uint.MaxValue, new StringBuilder(), 0); + var numFiles = DragDropInterop.DragQueryFileW(stgMedium.unionmember, uint.MaxValue, new StringBuilder(), 0); var files = new string[numFiles]; var sb = new StringBuilder(1024); var directoryCount = 0; @@ -212,11 +212,11 @@ internal partial class DragDropManager : DragDropManager.IDropTarget for (var i = 0u; i < numFiles; ++i) { sb.Clear(); - var ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity); + var ret = DragDropInterop.DragQueryFileW(stgMedium.unionmember, i, sb, sb.Capacity); if (ret >= sb.Capacity) { sb.Capacity = ret + 1; - ret = DragDropInterop.DragQueryFile(stgMedium.unionmember, i, sb, sb.Capacity); + ret = DragDropInterop.DragQueryFileW(stgMedium.unionmember, i, sb, sb.Capacity); } if (ret > 0 && ret < sb.Capacity) From 49bffccf82884eb1c73c4adf79f0eaaaca46c7a9 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:49:06 +0200 Subject: [PATCH 051/585] Update ClientStructs (#1359) 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 1e52770d5..dcc61941b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 1e52770d5367c8237ac78688774d136118994bf7 +Subproject commit dcc61941b2bd73b1aa5badb40276265b80e91a69 From 342e1bc06c60b82bd077519058612eefa754dec3 Mon Sep 17 00:00:00 2001 From: srkizer Date: Tue, 29 Aug 2023 10:04:29 +0900 Subject: [PATCH 052/585] Make TextureManager.GetTexture return value non-nullable (#1342) --- Dalamud/Interface/Internal/TextureManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 1648f1961..b9efb42f3 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -203,7 +203,9 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// /// The texture to obtain a handle to. /// A texture wrap that can be used to render the texture. - public IDalamudTextureWrap? GetTexture(TexFile file) + /// Thrown when the graphics system is not available yet. Relevant for plugins when LoadRequiredState is set to 0 or 1. + /// Thrown when the given is not supported. Most likely is that the file is corrupt. + public IDalamudTextureWrap GetTexture(TexFile file) { ArgumentNullException.ThrowIfNull(file); From efefcd70cfdc8a9e95ff763543efe40cc16a0b0e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 28 Aug 2023 21:56:31 -0700 Subject: [PATCH 053/585] Add AddonLifecycle Service (#1361) Adds new service to manage addon lifecycle events, such as setup/teardown. --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 169 ++++++++++++++++++ .../AddonLifecycleAddressResolver.cs | 27 +++ Dalamud/Plugin/Services/IAddonLifecycle.cs | 50 ++++++ 3 files changed, 246 insertions(+) create mode 100644 Dalamud/Game/AddonLifecycle/AddonLifecycle.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs create mode 100644 Dalamud/Plugin/Services/IAddonLifecycle.cs diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs new file mode 100644 index 000000000..95cb2539c --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -0,0 +1,169 @@ +using System; +using System.Runtime.InteropServices; + +using Dalamud.Hooking; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonLifecycle; + +/// +/// This class provides events for in-game addon lifecycles. +/// +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycle +{ + private static readonly ModuleLog Log = new("AddonLifecycle"); + private readonly AddonLifecycleAddressResolver address; + private readonly Hook onAddonSetupHook; + private readonly Hook onAddonFinalizeHook; + + [ServiceManager.ServiceConstructor] + private AddonLifecycle(SigScanner sigScanner) + { + this.address = new AddonLifecycleAddressResolver(); + this.address.Setup(sigScanner); + + this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); + this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonFinalize); + } + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate nint AddonSetupDelegate(AtkUnitBase* addon); + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + + /// + public event Action? AddonPreSetup; + + /// + public event Action? AddonPostSetup; + + /// + public event Action? AddonPreFinalize; + + /// + public event Action? AddonPostFinalize; + + /// + public void Dispose() + { + this.onAddonSetupHook.Dispose(); + this.onAddonFinalizeHook.Dispose(); + } + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction() + { + this.onAddonSetupHook.Enable(); + this.onAddonFinalizeHook.Enable(); + } + + private nint OnAddonSetup(AtkUnitBase* addon) + { + try + { + this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonSetup pre-setup invoke."); + } + + var result = this.onAddonSetupHook.Original(addon); + + try + { + this.AddonPostSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonSetup post-setup invoke."); + } + + return result; + } + + private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) + { + try + { + this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke."); + } + + this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); + + try + { + this.AddonPostFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonFinalize post-finalize invoke."); + } + } +} + +/// +/// Plugin-scoped version of a AddonLifecycle service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle +{ + [ServiceManager.ServiceDependency] + private readonly AddonLifecycle addonLifecycleService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + public AddonLifecyclePluginScoped() + { + this.addonLifecycleService.AddonPreSetup += this.AddonPreSetupForward; + this.addonLifecycleService.AddonPostSetup += this.AddonPostSetupForward; + this.addonLifecycleService.AddonPreFinalize += this.AddonPreFinalizeForward; + this.addonLifecycleService.AddonPostFinalize += this.AddonPostFinalizeForward; + } + + /// + public event Action? AddonPreSetup; + + /// + public event Action? AddonPostSetup; + + /// + public event Action? AddonPreFinalize; + + /// + public event Action? AddonPostFinalize; + + /// + public void Dispose() + { + this.addonLifecycleService.AddonPreSetup -= this.AddonPreSetupForward; + this.addonLifecycleService.AddonPostSetup -= this.AddonPostSetupForward; + this.addonLifecycleService.AddonPreFinalize -= this.AddonPreFinalizeForward; + this.addonLifecycleService.AddonPostFinalize -= this.AddonPostFinalizeForward; + } + + private void AddonPreSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPreSetup?.Invoke(args); + + private void AddonPostSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPostSetup?.Invoke(args); + + private void AddonPreFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPreFinalize?.Invoke(args); + + private void AddonPostFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPostFinalize?.Invoke(args); +} diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs new file mode 100644 index 000000000..ba7b723ec --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Game.AddonLifecycle; + +/// +/// AddonLifecycleService memory address resolver. +/// +internal class AddonLifecycleAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the addon setup hook invoked by the atkunitmanager. + /// + public nint AddonSetup { get; private set; } + + /// + /// 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(SigScanner sig) + { + this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); + this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); + } +} diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs new file mode 100644 index 000000000..7b90cf0cd --- /dev/null +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -0,0 +1,50 @@ +using System; + +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Plugin.Services; + +/// +/// This class provides events for in-game addon lifecycles. +/// +public interface IAddonLifecycle +{ + /// + /// Event that fires before an addon is being setup. + /// + public event Action AddonPreSetup; + + /// + /// Event that fires after an addon is done being setup. + /// + public event Action AddonPostSetup; + + /// + /// Event that fires before an addon is being finalized. + /// + public event Action AddonPreFinalize; + + /// + /// Event that fires after an addon is done being finalized. + /// + public event Action AddonPostFinalize; + + /// + /// Addon argument data for use in event subscribers. + /// + public unsafe class AddonArgs + { + private string? addonName; + + /// + /// Gets the name of the addon this args referrers to. + /// + public string AddonName => this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + required public nint Addon { get; init; } + } +} From cfef50af0c9ac5a64e1b08ff706b03ae793def67 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:01:53 -0700 Subject: [PATCH 054/585] [AddonLifecycle] Fixes --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 33 ++++++------------- Dalamud/Plugin/Services/IAddonLifecycle.cs | 7 +--- 2 files changed, 11 insertions(+), 29 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 95cb2539c..c3ec038ed 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; using Dalamud.Hooking; using Dalamud.IoC; @@ -29,13 +28,11 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl this.address.Setup(sigScanner); this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); - this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonFinalize); + this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); } - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate nint AddonSetupDelegate(AtkUnitBase* addon); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); /// @@ -47,9 +44,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl /// public event Action? AddonPreFinalize; - /// - public event Action? AddonPostFinalize; - /// public void Dispose() { @@ -66,6 +60,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl private nint OnAddonSetup(AtkUnitBase* addon) { + if (addon is null) + return this.onAddonSetupHook.Original(addon); + try { this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); @@ -91,6 +88,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { + if (atkUnitBase is null) + { + this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); + return; + } + try { this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); @@ -101,15 +104,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl } this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); - - try - { - this.AddonPostFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonFinalize post-finalize invoke."); - } } } @@ -135,7 +129,6 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif this.addonLifecycleService.AddonPreSetup += this.AddonPreSetupForward; this.addonLifecycleService.AddonPostSetup += this.AddonPostSetupForward; this.addonLifecycleService.AddonPreFinalize += this.AddonPreFinalizeForward; - this.addonLifecycleService.AddonPostFinalize += this.AddonPostFinalizeForward; } /// @@ -147,16 +140,12 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif /// public event Action? AddonPreFinalize; - /// - public event Action? AddonPostFinalize; - /// public void Dispose() { this.addonLifecycleService.AddonPreSetup -= this.AddonPreSetupForward; this.addonLifecycleService.AddonPostSetup -= this.AddonPostSetupForward; this.addonLifecycleService.AddonPreFinalize -= this.AddonPreFinalizeForward; - this.addonLifecycleService.AddonPostFinalize -= this.AddonPostFinalizeForward; } private void AddonPreSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPreSetup?.Invoke(args); @@ -164,6 +153,4 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif private void AddonPostSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPostSetup?.Invoke(args); private void AddonPreFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPreFinalize?.Invoke(args); - - private void AddonPostFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPostFinalize?.Invoke(args); } diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 7b90cf0cd..43b9fef0a 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -25,11 +25,6 @@ public interface IAddonLifecycle ///
public event Action AddonPreFinalize; - /// - /// Event that fires after an addon is done being finalized. - /// - public event Action AddonPostFinalize; - /// /// Addon argument data for use in event subscribers. /// @@ -40,7 +35,7 @@ public interface IAddonLifecycle /// /// Gets the name of the addon this args referrers to. /// - public string AddonName => this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); /// /// Gets the pointer to the addons AtkUnitBase. From 46fbb94b1ddc428c181d78ea6004118f9ea5403b Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 29 Aug 2023 14:05:31 -0700 Subject: [PATCH 055/585] [AddonLifecycle] Also check specific addon for null --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index c3ec038ed..72d1c25ff 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -88,7 +88,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { - if (atkUnitBase is null) + if (atkUnitBase is null || atkUnitBase[0] is null) { this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); return; From 8a267e51bf588e687cffab49285f65a53be1c6ca Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 30 Aug 2023 23:53:24 +0200 Subject: [PATCH 056/585] feat: improve custom repo disclaimer a bit --- .../Internal/DalamudConfiguration.cs | 5 ++ .../Windows/Settings/SettingsEntry.cs | 8 +++ .../Internal/Windows/Settings/SettingsTab.cs | 5 +- .../Widgets/ThirdRepoSettingsEntry.cs | 61 ++++++++++++++++++- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index ac410527c..2d0a08942 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -100,6 +100,11 @@ internal sealed class DalamudConfiguration : IServiceType /// public List ThirdRepoList { get; set; } = new(); + /// + /// Gets or sets a value indicating whether or not a disclaimer regarding third-party repos has been dismissed. + /// + public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null; + /// /// Gets or sets a list of hidden plugins. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs index 5e1dc7884..1e57d716e 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs @@ -42,6 +42,14 @@ public abstract class SettingsEntry ///
public abstract void Draw(); + /// + /// Function to be called when the tab is opened. + /// + public virtual void OnOpen() + { + // ignored + } + /// /// Function to be called when the tab is closed. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs index 16b7749cb..49a8935df 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsTab.cs @@ -16,7 +16,10 @@ public abstract class SettingsTab : IDisposable public virtual void OnOpen() { - // ignored + foreach (var settingsEntry in this.Entries) + { + settingsEntry.OnOpen(); + } } public virtual void OnClose() diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs index be2e34a57..6e476792b 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Raii; using Dalamud.Plugin.Internal; +using Dalamud.Utility; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Settings.Widgets; @@ -23,7 +24,13 @@ public class ThirdRepoSettingsEntry : SettingsEntry private bool thirdRepoListChanged; private string thirdRepoTempUrl = string.Empty; private string thirdRepoAddError = string.Empty; + private DateTime timeSinceOpened; + public override void OnOpen() + { + this.timeSinceOpened = DateTime.Now; + } + public override void OnClose() { this.thirdRepoList = @@ -51,6 +58,8 @@ public class ThirdRepoSettingsEntry : SettingsEntry public override void Draw() { + var config = Service.Get(); + using var id = ImRaii.PushId("thirdRepo"); ImGui.TextUnformatted(Loc.Localize("DalamudSettingsCustomRepo", "Custom Plugin Repositories")); if (this.thirdRepoListChanged) @@ -61,13 +70,59 @@ public class ThirdRepoSettingsEntry : SettingsEntry ImGui.TextUnformatted(Loc.Localize("DalamudSettingsChanged", "(Changed)")); } } - + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingCustomRepoHint", "Add custom plugin repositories.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for third-party plugins and repositories.")); + + ImGuiHelpers.ScaledDummy(2); + + config.ThirdRepoSpeedbumpDismissed ??= config.ThirdRepoList.Any(x => x.IsEnabled); + var disclaimerDismissed = config.ThirdRepoSpeedbumpDismissed.Value; + + ImGui.PushFont(InterfaceManager.IconFont); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle.ToIconString()); + ImGui.PopFont(); + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(2); + ImGui.SameLine(); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarningReadThis", "READ THIS FIRST!")); + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(2); + ImGui.SameLine(); + ImGui.PushFont(InterfaceManager.IconFont); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle.ToIconString()); + ImGui.PopFont(); + + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for custom plugins and repositories.")); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning5", "If someone told you to copy/paste something here, it's very possible that you are being scammed or taken advantage of.")); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning2", "Plugins have full control over your PC, like any other program, and may cause harm or crashes.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning4", "They can delete your character, upload your family photos and burn down your house.")); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning4", "They can delete your character, steal your FC or Discord account, and burn down your house.")); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning3", "Please make absolutely sure that you only install third-party plugins from developers you trust.")); + if (!disclaimerDismissed) + { + const int speedbumpTime = 15; + var elapsed = DateTime.Now - this.timeSinceOpened; + if (elapsed < TimeSpan.FromSeconds(speedbumpTime)) + { + ImGui.BeginDisabled(); + ImGui.Button( + Loc.Localize("DalamudSettingCustomRepoWarningPleaseWait", "Please wait {0} seconds...").Format(speedbumpTime - elapsed.Seconds)); + ImGui.EndDisabled(); + } + else + { + if (ImGui.Button(Loc.Localize("DalamudSettingCustomRepoWarningIReadIt", "Ok, I have read and understood this warning"))) + { + config.ThirdRepoSpeedbumpDismissed = true; + config.QueueSave(); + } + } + } + + ImGuiHelpers.ScaledDummy(2); + + using var disabled = ImRaii.Disabled(!disclaimerDismissed); + ImGuiHelpers.ScaledDummy(5); ImGui.Columns(4); From c095f99cd1378db36a31972ac870f13b354ec9a9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 1 Sep 2023 21:53:34 -0700 Subject: [PATCH 057/585] Add AddonEventManager --- .../AddonEventManager/AddonEventManager.cs | 207 ++++++++++++++++++ .../AddonEventManagerAddressResolver.cs | 21 ++ .../Game/AddonEventManager/AddonEventType.cs | 132 +++++++++++ Dalamud/Plugin/Services/IAddonEventManager.cs | 36 +++ 4 files changed, 396 insertions(+) create mode 100644 Dalamud/Game/AddonEventManager/AddonEventManager.cs create mode 100644 Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs create mode 100644 Dalamud/Game/AddonEventManager/AddonEventType.cs create mode 100644 Dalamud/Plugin/Services/IAddonEventManager.cs diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs new file mode 100644 index 000000000..8a0f16d1e --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; + +using Dalamud.Hooking; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonEventManager; + +/// +/// Service provider for addon event management. +/// +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal unsafe class AddonEventManager : IDisposable, IServiceType +{ + // The starting value for param key ranges. + // ascii `DD` 0x4444 chosen for the key start, this just has to be larger than anything vanilla makes. + private const uint ParamKeyStart = 0x44440000; + + // The range each plugin is allowed to use. + // 65,536 per plugin should be reasonable. + private const uint ParamKeyPluginRange = 0x10000; + + // The maximum range allowed to be given to a plugin. + // 20,560 maximum plugins should be reasonable. + // 202,113,024 maximum event handlers should be reasonable. + private const uint ParamKeyMax = 0x50500000; + + private static readonly ModuleLog Log = new("AddonEventManager"); + private readonly AddonEventManagerAddressResolver address; + private readonly Hook onGlobalEventHook; + private readonly Dictionary eventHandlers; + + private uint currentPluginParamStart = ParamKeyStart; + + [ServiceManager.ServiceConstructor] + private AddonEventManager(SigScanner sigScanner) + { + this.address = new AddonEventManagerAddressResolver(); + this.address.Setup(sigScanner); + + this.eventHandlers = new Dictionary(); + + this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler); + } + + private delegate nint GlobalEventHandlerDetour(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown); + + /// + public void Dispose() + { + this.onGlobalEventHook.Dispose(); + } + + /// + /// Get the start value for a new plugin register. + /// + /// A unique starting range for event handlers. + /// Throws when attempting to register too many event handlers. + public uint GetPluginParamStart() + { + if (this.currentPluginParamStart >= ParamKeyMax) + { + throw new Exception("Maximum number of event handlers reached."); + } + + var result = this.currentPluginParamStart; + + this.currentPluginParamStart += ParamKeyPluginRange; + return result; + } + + /// + /// Adds a event handler to be triggered when the specified id is received. + /// + /// Unique id for this event handler. + /// The event handler to be called. + public void AddEvent(uint eventId, IAddonEventManager.AddonEventHandler handler) => this.eventHandlers.Add(eventId, handler); + + /// + /// Removes the event handler with the specified id. + /// + /// Event id to unregister. + public void RemoveEvent(uint eventId) => this.eventHandlers.Remove(eventId); + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction() + { + this.onGlobalEventHook.Enable(); + } + + private nint GlobalEventHandler(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown) + { + try + { + if (this.eventHandlers.TryGetValue(eventParam, out var handler)) + { + try + { + handler?.Invoke((AddonEventType)eventType, (nint)atkUnitBase, (nint)eventData[0]); + return nint.Zero; + } + catch (Exception exception) + { + Log.Error(exception, "Exception in GlobalEventHandler custom event invoke."); + } + } + } + catch (Exception e) + { + Log.Error(e, "Exception in GlobalEventHandler attempting to retrieve event handler."); + } + + return this.onGlobalEventHook!.Original(atkUnitBase, eventType, eventParam, eventData, unknown); + } +} + +/// +/// Plugin-scoped version of a AddonEventManager service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager +{ + private static readonly ModuleLog Log = new("AddonEventManager"); + + [ServiceManager.ServiceDependency] + private readonly AddonEventManager baseEventManager = Service.Get(); + + private readonly uint paramKeyStartRange; + private readonly List activeParamKeys; + + /// + /// Initializes a new instance of the class. + /// + public AddonEventManagerPluginScoped() + { + this.paramKeyStartRange = this.baseEventManager.GetPluginParamStart(); + this.activeParamKeys = new List(); + } + + /// + public void Dispose() + { + foreach (var activeKey in this.activeParamKeys) + { + this.baseEventManager.RemoveEvent(activeKey); + } + } + + /// + public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + { + if (eventId < 0x10000) + { + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + var eventListener = (AtkEventListener*)atkUnitBase; + var uniqueId = eventId + this.paramKeyStartRange; + + if (!this.activeParamKeys.Contains(uniqueId)) + { + node->AddEvent(type, uniqueId, eventListener, node, true); + this.baseEventManager.AddEvent(uniqueId, eventHandler); + + this.activeParamKeys.Add(uniqueId); + } + else + { + Log.Warning($"Attempted to register already registered eventId: {eventId}"); + } + } + else + { + Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10000}"); + } + } + + /// + public void RemoveEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType) + { + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + var eventListener = (AtkEventListener*)atkUnitBase; + var uniqueId = eventId + this.paramKeyStartRange; + + if (this.activeParamKeys.Contains(uniqueId)) + { + node->RemoveEvent(type, uniqueId, eventListener, true); + this.baseEventManager.RemoveEvent(uniqueId); + + this.activeParamKeys.Remove(uniqueId); + } + else + { + Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); + } + } +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs new file mode 100644 index 000000000..8dcf81580 --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs @@ -0,0 +1,21 @@ +namespace Dalamud.Game.AddonEventManager; + +/// +/// AddonEventManager memory address resolver. +/// +internal class AddonEventManagerAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the global atkevent handler + /// + public nint GlobalEventHandler { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(SigScanner scanner) + { + this.GlobalEventHandler = scanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2"); + } +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventType.cs b/Dalamud/Game/AddonEventManager/AddonEventType.cs new file mode 100644 index 000000000..eef9763ff --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventType.cs @@ -0,0 +1,132 @@ +namespace Dalamud.Game.AddonEventManager; + +/// +/// Reimplementation of AtkEventType. +/// +public enum AddonEventType : byte +{ + /// + /// Mouse Down. + /// + MouseDown = 3, + + /// + /// Mouse Up. + /// + MouseUp = 4, + + /// + /// Mouse Move. + /// + MouseMove = 5, + + /// + /// Mouse Over. + /// + MouseOver = 6, + + /// + /// Mouse Out. + /// + MouseOut = 7, + + /// + /// Mouse Click. + /// + MouseClick = 9, + + /// + /// Input Received. + /// + InputReceived = 12, + + /// + /// Focus Start. + /// + FocusStart = 18, + + /// + /// Focus Stop. + /// + FocusStop = 19, + + /// + /// Button Press, sent on MouseDown on Button. + /// + ButtonPress = 23, + + /// + /// Button Release, sent on MouseUp and MouseOut. + /// + ButtonRelease = 24, + + /// + /// Button Click, sent on MouseUp and MouseClick on button. + /// + ButtonClick = 25, + + /// + /// List Item RollOver. + /// + ListItemRollOver = 33, + + /// + /// List Item Roll Out. + /// + ListItemRollOut = 34, + + /// + /// List Item Toggle. + /// + ListItemToggle = 35, + + /// + /// Drag Drop Roll Over. + /// + DragDropRollOver = 52, + + /// + /// Drag Drop Roll Out. + /// + DragDropRollOut = 53, + + /// + /// Drag Drop Unknown. + /// + DragDropUnk54 = 54, + + /// + /// Drag Drop Unknown. + /// + DragDropUnk55 = 55, + + /// + /// Icon Text Roll Over. + /// + IconTextRollOver = 56, + + /// + /// Icon Text Roll Out. + /// + IconTextRollOut = 57, + + /// + /// Icon Text Click. + /// + IconTextClick = 58, + + /// + /// Window Roll Over. + /// + WindowRollOver = 67, + + /// + /// Window Roll Out. + /// + WindowRollOut = 68, + + /// + /// Window Change Scale. + /// + WindowChangeScale = 69, +} diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs new file mode 100644 index 000000000..8e2b1f67b --- /dev/null +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -0,0 +1,36 @@ +using Dalamud.Game.AddonEventManager; + +namespace Dalamud.Plugin.Services; + +/// +/// Service provider for addon event management. +/// +public interface IAddonEventManager +{ + /// + /// Delegate to be called when an event is received. + /// + /// Event type for this event handler. + /// The parent addon for this event handler. + /// The specific node that will trigger this event handler. + public delegate void AddonEventHandler(AddonEventType atkEventType, nint atkUnitBase, nint atkResNode); + + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// Unique Id for this event, maximum 0x10000. + /// The parent addon for this event. + /// The node that will trigger this event. + /// The event type for this event. + /// The handler to call when event is triggered. + void AddEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler); + + /// + /// Unregisters an event handler with the specified event id and event type. + /// + /// The Unique Id for this event. + /// The parent addon for this event. + /// The node for this event. + /// The event type for this event. + void RemoveEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType); +} From 7ac37a579b740541fc36b7f9d4131634c823b1ea Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 1 Sep 2023 23:33:10 -0700 Subject: [PATCH 058/585] [AddonEventManager] Add null check --- Dalamud/Game/AddonEventManager/AddonEventManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 8a0f16d1e..77111421f 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -97,7 +97,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType { try { - if (this.eventHandlers.TryGetValue(eventParam, out var handler)) + if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) { try { From ad06b5f05486d92be0d2ae28f95c05244159bd84 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 00:06:54 -0700 Subject: [PATCH 059/585] [AddonEventManager] Add Cursor Control --- .../Game/AddonEventManager/AddonCursorType.cs | 97 +++++++++++++++++++ .../AddonEventManager/AddonEventManager.cs | 84 +++++++++++++++- .../AddonEventManagerAddressResolver.cs | 8 +- Dalamud/Plugin/Services/IAddonEventManager.cs | 11 +++ 4 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 Dalamud/Game/AddonEventManager/AddonCursorType.cs diff --git a/Dalamud/Game/AddonEventManager/AddonCursorType.cs b/Dalamud/Game/AddonEventManager/AddonCursorType.cs new file mode 100644 index 000000000..8ba3a901b --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonCursorType.cs @@ -0,0 +1,97 @@ +namespace Dalamud.Game.AddonEventManager; + +/// +/// Reimplementation of CursorType. +/// +public enum AddonCursorType +{ + /// + /// Arrow. + /// + Arrow, + + /// + /// Boot. + /// + Boot, + + /// + /// Search. + /// + Search, + + /// + /// Chat Pointer. + /// + ChatPointer, + + /// + /// Interact. + /// + Interact, + + /// + /// Attack. + /// + Attack, + + /// + /// Hand. + /// + Hand, + + /// + /// Resizeable Left-Right. + /// + ResizeWE, + + /// + /// Resizeable Up-Down. + /// + ResizeNS, + + /// + /// Resizeable. + /// + ResizeNWSR, + + /// + /// Resizeable 4-way. + /// + ResizeNESW, + + /// + /// Clickable. + /// + Clickable, + + /// + /// Text Input. + /// + TextInput, + + /// + /// Text Click. + /// + TextClick, + + /// + /// Grab. + /// + Grab, + + /// + /// Chat Bubble. + /// + ChatBubble, + + /// + /// No Access. + /// + NoAccess, + + /// + /// Hidden. + /// + Hidden, +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 77111421f..ede59a9a9 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -6,6 +6,7 @@ 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.AddonEventManager; @@ -32,9 +33,13 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType private static readonly ModuleLog Log = new("AddonEventManager"); private readonly AddonEventManagerAddressResolver address; - private readonly Hook onGlobalEventHook; + private readonly Hook onGlobalEventHook; + private readonly Hook onUpdateCursor; private readonly Dictionary eventHandlers; + private AddonCursorType currentCursor; + private bool cursorSet; + private uint currentPluginParamStart = ParamKeyStart; [ServiceManager.ServiceConstructor] @@ -44,16 +49,21 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address.Setup(sigScanner); this.eventHandlers = new Dictionary(); + this.currentCursor = AddonCursorType.Arrow; - this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler); + this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler); + this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); } - private delegate nint GlobalEventHandlerDetour(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown); + private delegate nint GlobalEventHandlerDelegate(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown); + + private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); /// public void Dispose() { this.onGlobalEventHook.Dispose(); + this.onUpdateCursor.Dispose(); } /// @@ -86,11 +96,31 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// /// Event id to unregister. public void RemoveEvent(uint eventId) => this.eventHandlers.Remove(eventId); + + /// + /// Sets the game cursor. + /// + /// Cursor type to set. + public void SetCursor(AddonCursorType cursor) + { + this.currentCursor = cursor; + this.cursorSet = true; + } + + /// + /// Resets and un-forces custom cursor. + /// + public void ResetCursor() + { + this.currentCursor = AddonCursorType.Arrow; + this.cursorSet = false; + } [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { this.onGlobalEventHook.Enable(); + this.onUpdateCursor.Enable(); } private nint GlobalEventHandler(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown) @@ -117,6 +147,31 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType return this.onGlobalEventHook!.Original(atkUnitBase, eventType, eventParam, eventData, unknown); } + + private nint UpdateCursorDetour(RaptureAtkModule* module) + { + try + { + var atkStage = AtkStage.GetSingleton(); + + if (this.cursorSet && atkStage is not null) + { + var cursor = (AddonCursorType)atkStage->AtkCursor.Type; + if (cursor != this.currentCursor) + { + AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.currentCursor, 1); + } + + return nint.Zero; + } + } + catch (Exception e) + { + Log.Error(e, "Exception in UpdateCursorDetour."); + } + + return this.onUpdateCursor!.Original(module); + } } /// @@ -137,6 +192,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, private readonly uint paramKeyStartRange; private readonly List activeParamKeys; + private bool isForcingCursor; /// /// Initializes a new instance of the class. @@ -154,6 +210,12 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, { this.baseEventManager.RemoveEvent(activeKey); } + + // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. + if (this.isForcingCursor) + { + this.baseEventManager.ResetCursor(); + } } /// @@ -204,4 +266,20 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); } } + + /// + public void SetCursor(AddonCursorType cursor) + { + this.isForcingCursor = true; + + this.baseEventManager.SetCursor(cursor); + } + + /// + public void ResetCursor() + { + this.isForcingCursor = false; + + this.baseEventManager.ResetCursor(); + } } diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs index 8dcf81580..5cfa51149 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs @@ -6,9 +6,14 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver { /// - /// Gets the address of the global atkevent handler + /// Gets the address of the global AtkEvent handler. /// public nint GlobalEventHandler { get; private set; } + + /// + /// Gets the address of the AtkModule UpdateCursor method. + /// + public nint UpdateCursor { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -17,5 +22,6 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver protected override void Setup64Bit(SigScanner scanner) { this.GlobalEventHandler = scanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2"); + this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); } } diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index 8e2b1f67b..f052ed607 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -33,4 +33,15 @@ public interface IAddonEventManager /// The node for this event. /// The event type for this event. void RemoveEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType); + + /// + /// Force the game cursor to be the specified cursor. + /// + /// Which cursor to use. + void SetCursor(AddonCursorType cursor); + + /// + /// Un-forces the game cursor. + /// + void ResetCursor(); } From 712a492c89b919ad38c4f293da0308a974d74e54 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 09:41:21 -0700 Subject: [PATCH 060/585] [AddonEventController] Use custom AtkEventListener --- .../AddonEventManager/AddonEventListener.cs | 87 +++++++++++++++ .../AddonEventManager/AddonEventManager.cs | 103 +++++++++--------- .../AddonEventManagerAddressResolver.cs | 6 - 3 files changed, 141 insertions(+), 55 deletions(-) create mode 100644 Dalamud/Game/AddonEventManager/AddonEventListener.cs diff --git a/Dalamud/Game/AddonEventManager/AddonEventListener.cs b/Dalamud/Game/AddonEventManager/AddonEventListener.cs new file mode 100644 index 000000000..cb0aa1502 --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventListener.cs @@ -0,0 +1,87 @@ +using System; +using System.Runtime.InteropServices; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonEventManager; + +/// +/// Event listener class for managing custom events. +/// +// Custom event handler tech provided by Pohky, implemented by MidoriKami +internal unsafe class AddonEventListener : IDisposable +{ + private ReceiveEventDelegate? receiveEventDelegate; + + private AtkEventListener* eventListener; + + /// + /// Initializes a new instance of the class. + /// + /// The managed handler to send events to. + public AddonEventListener(ReceiveEventDelegate eventHandler) + { + this.receiveEventDelegate = eventHandler; + + this.eventListener = (AtkEventListener*)Marshal.AllocHGlobal(sizeof(AtkEventListener)); + this.eventListener->vtbl = (void*)Marshal.AllocHGlobal(sizeof(void*) * 3); + this.eventListener->vfunc[0] = (delegate* unmanaged)&NullSub; + this.eventListener->vfunc[1] = (delegate* unmanaged)&NullSub; + this.eventListener->vfunc[2] = (void*)Marshal.GetFunctionPointerForDelegate(this.receiveEventDelegate); + } + + /// + /// Delegate for receiving custom events. + /// + /// Pointer to the event listener. + /// Event type. + /// Unique Id for this event. + /// Event Data. + /// Unknown Parameter. + public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown); + + /// + public void Dispose() + { + if (this.eventListener is null) return; + + Marshal.FreeHGlobal((nint)this.eventListener->vtbl); + Marshal.FreeHGlobal((nint)this.eventListener); + + this.eventListener = null; + this.receiveEventDelegate = null; + } + + /// + /// Register an event to this event handler. + /// + /// Addon that triggers this event. + /// Node to attach event to. + /// Event type to trigger this event. + /// Unique id for this event. + public void RegisterEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param) + { + if (node is null) return; + + node->AddEvent(eventType, param, this.eventListener, (AtkResNode*)addon, false); + } + + /// + /// Unregister an event from this event handler. + /// + /// Node to remove the event from. + /// Event type that this event is for. + /// Unique id for this event. + public void UnregisterEvent(AtkResNode* node, AtkEventType eventType, uint param) + { + if (node is null) return; + + node->RemoveEvent(eventType, param, this.eventListener, false); + } + + [UnmanagedCallersOnly] + private static void NullSub() + { + /* do nothing */ + } +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index ede59a9a9..c6e61fe5a 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -19,23 +19,22 @@ namespace Dalamud.Game.AddonEventManager; internal unsafe class AddonEventManager : IDisposable, IServiceType { // The starting value for param key ranges. - // ascii `DD` 0x4444 chosen for the key start, this just has to be larger than anything vanilla makes. - private const uint ParamKeyStart = 0x44440000; + private const uint ParamKeyStart = 0x0000_0000; // The range each plugin is allowed to use. - // 65,536 per plugin should be reasonable. - private const uint ParamKeyPluginRange = 0x10000; + // 1,048,576 per plugin should be reasonable. + private const uint ParamKeyPluginRange = 0x10_0000; // The maximum range allowed to be given to a plugin. - // 20,560 maximum plugins should be reasonable. - // 202,113,024 maximum event handlers should be reasonable. - private const uint ParamKeyMax = 0x50500000; + // 1,048,576 maximum plugins should be reasonable. + private const uint ParamKeyMax = 0xFFF0_0000; private static readonly ModuleLog Log = new("AddonEventManager"); + private readonly AddonEventManagerAddressResolver address; - private readonly Hook onGlobalEventHook; private readonly Hook onUpdateCursor; private readonly Dictionary eventHandlers; + private readonly AddonEventListener eventListener; private AddonCursorType currentCursor; private bool cursorSet; @@ -48,21 +47,20 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); + this.eventListener = new AddonEventListener(this.OnCustomEvent); + this.eventHandlers = new Dictionary(); this.currentCursor = AddonCursorType.Arrow; - this.onGlobalEventHook = Hook.FromAddress(this.address.GlobalEventHandler, this.GlobalEventHandler); this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); } - private delegate nint GlobalEventHandlerDelegate(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown); - private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); /// public void Dispose() { - this.onGlobalEventHook.Dispose(); + this.eventListener.Dispose(); this.onUpdateCursor.Dispose(); } @@ -85,17 +83,39 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType } /// - /// Adds a event handler to be triggered when the specified id is received. + /// Attaches an event to a node. /// - /// Unique id for this event handler. + /// Addon that contains the node. + /// The node that will trigger the event. + /// The event type to trigger on. + /// The unique id for this event. /// The event handler to be called. - public void AddEvent(uint eventId, IAddonEventManager.AddonEventHandler handler) => this.eventHandlers.Add(eventId, handler); + public void AddEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param, IAddonEventManager.AddonEventHandler handler) + { + this.eventListener.RegisterEvent(addon, node, eventType, param); + this.eventHandlers.TryAdd(param, handler); + } /// - /// Removes the event handler with the specified id. + /// Detaches an event from a node. /// - /// Event id to unregister. - public void RemoveEvent(uint eventId) => this.eventHandlers.Remove(eventId); + /// The node to remove the event from. + /// The event type to remove. + /// The unique id of the event to remove. + public void RemoveEvent(AtkResNode* node, AtkEventType eventType, uint param) + { + this.eventListener.UnregisterEvent(node, eventType, param); + this.eventHandlers.Remove(param); + } + + /// + /// Removes a delegate from the managed event handlers. + /// + /// Unique id of the delegate to remove. + public void RemoveHandler(uint param) + { + this.eventHandlers.Remove(param); + } /// /// Sets the game cursor. @@ -119,33 +139,23 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { - this.onGlobalEventHook.Enable(); this.onUpdateCursor.Enable(); } - private nint GlobalEventHandler(AtkUnitBase* atkUnitBase, AtkEventType eventType, uint eventParam, AtkResNode** eventData, nint unknown) + private void OnCustomEvent(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown) { - try + if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) { - if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) + try { - try - { - handler?.Invoke((AddonEventType)eventType, (nint)atkUnitBase, (nint)eventData[0]); - return nint.Zero; - } - catch (Exception exception) - { - Log.Error(exception, "Exception in GlobalEventHandler custom event invoke."); - } + // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler + handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); + } + catch (Exception exception) + { + Log.Error(exception, "Exception in OnCustomEvent custom event invoke."); } } - catch (Exception e) - { - Log.Error(e, "Exception in GlobalEventHandler attempting to retrieve event handler."); - } - - return this.onGlobalEventHook!.Original(atkUnitBase, eventType, eventParam, eventData, unknown); } private nint UpdateCursorDetour(RaptureAtkModule* module) @@ -193,7 +203,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, private readonly uint paramKeyStartRange; private readonly List activeParamKeys; private bool isForcingCursor; - + /// /// Initializes a new instance of the class. /// @@ -208,7 +218,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, { foreach (var activeKey in this.activeParamKeys) { - this.baseEventManager.RemoveEvent(activeKey); + this.baseEventManager.RemoveHandler(activeKey); } // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. @@ -221,18 +231,16 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, /// public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) { - if (eventId < 0x10000) + if (eventId < 0x10_000) { var type = (AtkEventType)eventType; var node = (AtkResNode*)atkResNode; - var eventListener = (AtkEventListener*)atkUnitBase; + var addon = (AtkUnitBase*)atkUnitBase; var uniqueId = eventId + this.paramKeyStartRange; if (!this.activeParamKeys.Contains(uniqueId)) { - node->AddEvent(type, uniqueId, eventListener, node, true); - this.baseEventManager.AddEvent(uniqueId, eventHandler); - + this.baseEventManager.AddEvent(addon, node, type, uniqueId, eventHandler); this.activeParamKeys.Add(uniqueId); } else @@ -242,7 +250,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, } else { - Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10000}"); + Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10_000}"); } } @@ -251,14 +259,11 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, { var type = (AtkEventType)eventType; var node = (AtkResNode*)atkResNode; - var eventListener = (AtkEventListener*)atkUnitBase; var uniqueId = eventId + this.paramKeyStartRange; if (this.activeParamKeys.Contains(uniqueId)) { - node->RemoveEvent(type, uniqueId, eventListener, true); - this.baseEventManager.RemoveEvent(uniqueId); - + this.baseEventManager.RemoveEvent(node, type, uniqueId); this.activeParamKeys.Remove(uniqueId); } else diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs index 5cfa51149..ba1c07db8 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs @@ -5,11 +5,6 @@ /// internal class AddonEventManagerAddressResolver : BaseAddressResolver { - /// - /// Gets the address of the global AtkEvent handler. - /// - public nint GlobalEventHandler { get; private set; } - /// /// Gets the address of the AtkModule UpdateCursor method. /// @@ -21,7 +16,6 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver /// The signature scanner to facilitate setup. protected override void Setup64Bit(SigScanner scanner) { - this.GlobalEventHandler = scanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2"); this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); } } From 22a381b8746bfc58d14a84a815eabd030ac03f12 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 09:49:21 -0700 Subject: [PATCH 061/585] [AddonEventManager] Reserve the first range for Dalamud internal use --- Dalamud/Game/AddonEventManager/AddonEventManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index c6e61fe5a..b88c64253 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -19,7 +19,8 @@ namespace Dalamud.Game.AddonEventManager; internal unsafe class AddonEventManager : IDisposable, IServiceType { // The starting value for param key ranges. - private const uint ParamKeyStart = 0x0000_0000; + // Reserve the first 0x10_000 for dalamud internal use. + private const uint ParamKeyStart = 0x0010_0000; // The range each plugin is allowed to use. // 1,048,576 per plugin should be reasonable. From 26e138c7834011f385fa9c0bd4cd66fbad55f247 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 11:30:31 -0700 Subject: [PATCH 062/585] [AddonEventManager] Give each plugin their own event listener. --- .../AddonEventManager/AddonEventManager.cs | 180 +++++------------- 1 file changed, 44 insertions(+), 136 deletions(-) diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index b88c64253..69dc27f3b 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -18,29 +18,12 @@ namespace Dalamud.Game.AddonEventManager; [ServiceManager.EarlyLoadedService] internal unsafe class AddonEventManager : IDisposable, IServiceType { - // The starting value for param key ranges. - // Reserve the first 0x10_000 for dalamud internal use. - private const uint ParamKeyStart = 0x0010_0000; - - // The range each plugin is allowed to use. - // 1,048,576 per plugin should be reasonable. - private const uint ParamKeyPluginRange = 0x10_0000; - - // The maximum range allowed to be given to a plugin. - // 1,048,576 maximum plugins should be reasonable. - private const uint ParamKeyMax = 0xFFF0_0000; - private static readonly ModuleLog Log = new("AddonEventManager"); private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; - private readonly Dictionary eventHandlers; - private readonly AddonEventListener eventListener; - private AddonCursorType currentCursor; - private bool cursorSet; - - private uint currentPluginParamStart = ParamKeyStart; + private AddonCursorType? cursorOverride; [ServiceManager.ServiceConstructor] private AddonEventManager(SigScanner sigScanner) @@ -48,10 +31,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); - this.eventListener = new AddonEventListener(this.OnCustomEvent); - - this.eventHandlers = new Dictionary(); - this.currentCursor = AddonCursorType.Arrow; + this.cursorOverride = null; this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); } @@ -61,116 +41,38 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// public void Dispose() { - this.eventListener.Dispose(); this.onUpdateCursor.Dispose(); } - /// - /// Get the start value for a new plugin register. - /// - /// A unique starting range for event handlers. - /// Throws when attempting to register too many event handlers. - public uint GetPluginParamStart() - { - if (this.currentPluginParamStart >= ParamKeyMax) - { - throw new Exception("Maximum number of event handlers reached."); - } - - var result = this.currentPluginParamStart; - - this.currentPluginParamStart += ParamKeyPluginRange; - return result; - } - - /// - /// Attaches an event to a node. - /// - /// Addon that contains the node. - /// The node that will trigger the event. - /// The event type to trigger on. - /// The unique id for this event. - /// The event handler to be called. - public void AddEvent(AtkUnitBase* addon, AtkResNode* node, AtkEventType eventType, uint param, IAddonEventManager.AddonEventHandler handler) - { - this.eventListener.RegisterEvent(addon, node, eventType, param); - this.eventHandlers.TryAdd(param, handler); - } - - /// - /// Detaches an event from a node. - /// - /// The node to remove the event from. - /// The event type to remove. - /// The unique id of the event to remove. - public void RemoveEvent(AtkResNode* node, AtkEventType eventType, uint param) - { - this.eventListener.UnregisterEvent(node, eventType, param); - this.eventHandlers.Remove(param); - } - - /// - /// Removes a delegate from the managed event handlers. - /// - /// Unique id of the delegate to remove. - public void RemoveHandler(uint param) - { - this.eventHandlers.Remove(param); - } - /// /// Sets the game cursor. /// /// Cursor type to set. - public void SetCursor(AddonCursorType cursor) - { - this.currentCursor = cursor; - this.cursorSet = true; - } + public void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; /// /// Resets and un-forces custom cursor. /// - public void ResetCursor() - { - this.currentCursor = AddonCursorType.Arrow; - this.cursorSet = false; - } - + public void ResetCursor() => this.cursorOverride = null; + [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { this.onUpdateCursor.Enable(); } - private void OnCustomEvent(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown) - { - if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) - { - try - { - // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler - handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); - } - catch (Exception exception) - { - Log.Error(exception, "Exception in OnCustomEvent custom event invoke."); - } - } - } - private nint UpdateCursorDetour(RaptureAtkModule* module) { try { var atkStage = AtkStage.GetSingleton(); - if (this.cursorSet && atkStage is not null) + if (this.cursorOverride is not null && atkStage is not null) { var cursor = (AddonCursorType)atkStage->AtkCursor.Type; - if (cursor != this.currentCursor) + if (cursor != this.cursorOverride) { - AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.currentCursor, 1); + AtkStage.GetSingleton()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); } return nint.Zero; @@ -200,9 +102,10 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, [ServiceManager.ServiceDependency] private readonly AddonEventManager baseEventManager = Service.Get(); - - private readonly uint paramKeyStartRange; - private readonly List activeParamKeys; + + private readonly AddonEventListener eventListener; + private readonly Dictionary eventHandlers; + private bool isForcingCursor; /// @@ -210,62 +113,51 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, /// public AddonEventManagerPluginScoped() { - this.paramKeyStartRange = this.baseEventManager.GetPluginParamStart(); - this.activeParamKeys = new List(); + this.eventHandlers = new Dictionary(); + this.eventListener = new AddonEventListener(this.PluginAddonEventHandler); } - + /// public void Dispose() { - foreach (var activeKey in this.activeParamKeys) - { - this.baseEventManager.RemoveHandler(activeKey); - } - // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. if (this.isForcingCursor) { this.baseEventManager.ResetCursor(); } + + this.eventListener.Dispose(); + this.eventHandlers.Clear(); } /// public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) { - if (eventId < 0x10_000) + if (!this.eventHandlers.ContainsKey(eventId)) { var type = (AtkEventType)eventType; var node = (AtkResNode*)atkResNode; var addon = (AtkUnitBase*)atkUnitBase; - var uniqueId = eventId + this.paramKeyStartRange; - if (!this.activeParamKeys.Contains(uniqueId)) - { - this.baseEventManager.AddEvent(addon, node, type, uniqueId, eventHandler); - this.activeParamKeys.Add(uniqueId); - } - else - { - Log.Warning($"Attempted to register already registered eventId: {eventId}"); - } + this.eventHandlers.Add(eventId, eventHandler); + this.eventListener.RegisterEvent(addon, node, type, eventId); } else { - Log.Warning($"Attempted to register eventId out of range: {eventId}\nMaximum value: {0x10_000}"); + Log.Warning($"Attempted to register already registered eventId: {eventId}"); } } /// public void RemoveEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType) { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - var uniqueId = eventId + this.paramKeyStartRange; - - if (this.activeParamKeys.Contains(uniqueId)) + if (this.eventHandlers.ContainsKey(eventId)) { - this.baseEventManager.RemoveEvent(node, type, uniqueId); - this.activeParamKeys.Remove(uniqueId); + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + + this.eventListener.UnregisterEvent(node, type, eventId); + this.eventHandlers.Remove(eventId); } else { @@ -288,4 +180,20 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, this.baseEventManager.ResetCursor(); } + + private void PluginAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) + { + if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) + { + try + { + // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler + handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); + } + catch (Exception exception) + { + Log.Error(exception, "Exception in PluginAddonEventHandler custom event invoke."); + } + } + } } From 627a41f2363ee44956cd891950b8953cc1ff69c5 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:08:14 -0700 Subject: [PATCH 063/585] [AddonEventManager] Remove AtkUnitBase from remove event, it's not used to unregister events. --- Dalamud/Plugin/Services/IAddonEventManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index f052ed607..dbbfd784b 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -29,10 +29,9 @@ public interface IAddonEventManager /// Unregisters an event handler with the specified event id and event type. /// /// The Unique Id for this event. - /// The parent addon for this event. /// The node for this event. /// The event type for this event. - void RemoveEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType); + void RemoveEvent(uint eventId, nint atkResNode, AddonEventType eventType); /// /// Force the game cursor to be the specified cursor. From ce4392e1093557aebeb5dfeb0a8c4c0148e18b78 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 2 Sep 2023 13:56:44 -0700 Subject: [PATCH 064/585] [AddonEventManager] Add Dalamud specific EventListener for internal use --- .../AddonEventManager/AddonEventManager.cs | 72 ++++++++++++++++--- 1 file changed, 63 insertions(+), 9 deletions(-) diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 69dc27f3b..4718d4800 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -16,13 +16,16 @@ namespace Dalamud.Game.AddonEventManager; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonEventManager : IDisposable, IServiceType +internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEventManager { private static readonly ModuleLog Log = new("AddonEventManager"); private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; + private readonly AddonEventListener eventListener; + private readonly Dictionary eventHandlers; + private AddonCursorType? cursorOverride; [ServiceManager.ServiceConstructor] @@ -31,28 +34,63 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); + this.eventHandlers = new Dictionary(); + this.eventListener = new AddonEventListener(this.DalamudAddonEventHandler); + this.cursorOverride = null; this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); } private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); + + /// + public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + { + if (!this.eventHandlers.ContainsKey(eventId)) + { + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + var addon = (AtkUnitBase*)atkUnitBase; + + this.eventHandlers.Add(eventId, eventHandler); + this.eventListener.RegisterEvent(addon, node, type, eventId); + } + else + { + Log.Warning($"Attempted to register already registered eventId: {eventId}"); + } + } + /// + public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType) + { + if (this.eventHandlers.ContainsKey(eventId)) + { + var type = (AtkEventType)eventType; + var node = (AtkResNode*)atkResNode; + + this.eventListener.UnregisterEvent(node, type, eventId); + this.eventHandlers.Remove(eventId); + } + else + { + Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); + } + } + /// public void Dispose() { this.onUpdateCursor.Dispose(); + this.eventListener.Dispose(); + this.eventHandlers.Clear(); } - /// - /// Sets the game cursor. - /// - /// Cursor type to set. + /// public void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; - /// - /// Resets and un-forces custom cursor. - /// + /// public void ResetCursor() => this.cursorOverride = null; [ServiceManager.CallWhenServicesReady] @@ -85,6 +123,22 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType return this.onUpdateCursor!.Original(module); } + + private void DalamudAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) + { + if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) + { + try + { + // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler + handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); + } + catch (Exception exception) + { + Log.Error(exception, "Exception in DalamudAddonEventHandler custom event invoke."); + } + } + } } /// @@ -149,7 +203,7 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, } /// - public void RemoveEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType) + public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType) { if (this.eventHandlers.ContainsKey(eventId)) { From 2439bcccbd0b202d079290abd2ed0df8af7d7b09 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 4 Sep 2023 23:45:17 -0700 Subject: [PATCH 065/585] Add Tooltips, and OnClick actions to DtrBarEntries --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 209 +++++++++++++++--- Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs | 29 +++ Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 10 + 3 files changed, 218 insertions(+), 30 deletions(-) create mode 100644 Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index dd1e7aa30..4d2a005ae 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -3,13 +3,19 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; +using Dalamud.Game.AddonEventManager; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Memory; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; -using Serilog; + +using DalamudAddonEventManager = Dalamud.Game.AddonEventManager.AddonEventManager; namespace Dalamud.Game.Gui.Dtr; @@ -25,7 +31,12 @@ namespace Dalamud.Game.Gui.Dtr; public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; + private const uint MouseOverEventIdOffset = 10000; + private const uint MouseOutEventIdOffset = 20000; + private const uint MouseClickEventIdOffset = 30000; + private static readonly ModuleLog Log = new("DtrBar"); + [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -35,12 +46,24 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - private List entries = new(); + [ServiceManager.ServiceDependency] + private readonly DalamudAddonEventManager uiEventManager = Service.Get(); + + private readonly DtrBarAddressResolver address; + private readonly List entries = new(); + private readonly Hook onAddonDrawHook; + private readonly Hook onAddonRequestedUpdateHook; private uint runningNodeIds = BaseNodeId; [ServiceManager.ServiceConstructor] - private DtrBar() + private DtrBar(SigScanner sigScanner) { + this.address = new DtrBarAddressResolver(); + this.address.Setup(sigScanner); + + this.onAddonDrawHook = Hook.FromAddress(this.address.AtkUnitBaseDraw, this.OnAddonDrawDetour); + this.onAddonRequestedUpdateHook = Hook.FromAddress(this.address.AddonRequestedUpdate, this.OnAddonRequestedUpdateDetour); + this.framework.Update += this.Update; this.configuration.DtrOrder ??= new List(); @@ -48,6 +71,10 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.configuration.QueueSave(); } + private delegate void AddonDrawDelegate(AtkUnitBase* addon); + + private delegate void AddonRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + /// public DtrBarEntry Get(string title, SeString? text = null) { @@ -70,6 +97,9 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar /// void IDisposable.Dispose() { + this.onAddonDrawHook.Dispose(); + this.onAddonRequestedUpdateHook.Dispose(); + foreach (var entry in this.entries) this.RemoveNode(entry.TextNode); @@ -130,6 +160,13 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar return xPos.CompareTo(yPos); }); } + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction() + { + this.onAddonDrawHook.Enable(); + this.onAddonRequestedUpdateHook.Enable(); + } private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); @@ -148,7 +185,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (!this.CheckForDalamudNodes()) this.RecreateNodes(); - var collisionNode = dtr->UldManager.NodeList[1]; + var collisionNode = dtr->GetNodeById(17); if (collisionNode == null) return; // If we are drawing backwards, we should start from the right side of the collision node. That is, @@ -157,28 +194,24 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar ? collisionNode->X + collisionNode->Width : collisionNode->X; - for (var i = 0; i < this.entries.Count; i++) + foreach (var data in this.entries) { - var data = this.entries[i]; var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown; - if (data.Dirty && data.Added && data.Text != null && data.TextNode != null) + if (data is { Dirty: true, Added: true, Text: not null, TextNode: not null }) { var node = data.TextNode; - node->SetText(data.Text?.Encode()); + node->SetText(data.Text.Encode()); ushort w = 0, h = 0; - if (isHide) + if (!isHide) { - node->AtkResNode.ToggleVisibility(false); - } - else - { - node->AtkResNode.ToggleVisibility(true); node->GetTextDrawSize(&w, &h, node->NodeText.StringPtr); node->AtkResNode.SetWidth(w); } + node->AtkResNode.ToggleVisibility(!isHide); + data.Dirty = false; } @@ -202,8 +235,62 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); } } + } + } - this.entries[i] = data; + // This hooks all AtkUnitBase.Draw calls, then checks for our specific addon name. + // AddonDtr doesn't implement it's own Draw method, would need to replace vtable entry to be more efficient. + private void OnAddonDrawDetour(AtkUnitBase* addon) + { + this.onAddonDrawHook!.Original(addon); + + try + { + if (MemoryHelper.ReadString((nint)addon->Name, 0x20) is not "_DTR") return; + + this.UpdateNodePositions(addon); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonDraw."); + } + } + + private void UpdateNodePositions(AtkUnitBase* addon) + { + var targetSize = (ushort)this.CalculateTotalSize(); + addon->RootNode->SetWidth(targetSize); + + // If we grow to the right, we need to left-justify the original elements. + // else if we grow to the left, the game right-justifies it for us. + if (this.configuration.DtrSwapDirection) + { + var sizeOffset = addon->GetNodeById(17)->GetX(); + + var node = addon->RootNode->ChildNode; + while (node is not null) + { + if (node->NodeID < 1000 && node->IsVisible) + { + node->SetX(node->GetX() - sizeOffset); + } + + node = node->PrevSiblingNode; + } + } + } + + private void OnAddonRequestedUpdateDetour(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + this.onAddonRequestedUpdateHook.Original(addon, numberArrayData, stringArrayData); + + try + { + this.UpdateNodePositions(addon); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRequestedUpdate."); } } @@ -235,11 +322,37 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } + // Calculates the total width the dtr bar should be + private float CalculateTotalSize() + { + var addon = this.GetDtr(); + if (addon is null || addon->RootNode is null || addon->UldManager.NodeList is null) return 0; + + var totalSize = 0.0f; + + foreach (var index in Enumerable.Range(0, addon->UldManager.NodeListCount)) + { + var node = addon->UldManager.NodeList[index]; + + // Node 17 is the default CollisionNode that fits over the existing elements + if (node->NodeID is 17) totalSize += node->Width; + + // Node > 1000, are our custom nodes + if (node->NodeID is > 1000) totalSize += node->Width + this.configuration.DtrSpacing; + } + + return totalSize; + } + private bool AddNode(AtkTextNode* node) { var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; + this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); + this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); + this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); + var lastChild = dtr->RootNode->ChildNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; Log.Debug($"Found last sibling: {(ulong)lastChild:X}"); @@ -251,6 +364,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar Log.Debug("Set last sibling of DTR and updated child count"); dtr->UldManager.UpdateDrawNodeList(); + dtr->UpdateCollisionNodeList(false); Log.Debug("Updated node draw list"); return true; } @@ -260,6 +374,10 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; + this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)node, AddonEventType.MouseOver); + this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)node, AddonEventType.MouseOut); + this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)node, AddonEventType.MouseClick); + var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; @@ -272,25 +390,23 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar dtr->RootNode->ChildCount = (ushort)(dtr->RootNode->ChildCount - 1); Log.Debug("Set last sibling of DTR and updated child count"); dtr->UldManager.UpdateDrawNodeList(); + dtr->UpdateCollisionNodeList(false); Log.Debug("Updated node draw list"); return true; } private AtkTextNode* MakeNode(uint nodeId) { - var newTextNode = (AtkTextNode*)IMemorySpace.GetUISpace()->Malloc((ulong)sizeof(AtkTextNode), 8); + var newTextNode = IMemorySpace.GetUISpace()->Create(); if (newTextNode == null) { - Log.Debug("Failed to allocate memory for text node"); + Log.Debug("Failed to allocate memory for AtkTextNode"); return null; } - IMemorySpace.Memset(newTextNode, 0, (ulong)sizeof(AtkTextNode)); - newTextNode->Ctor(); - newTextNode->AtkResNode.NodeID = nodeId; newTextNode->AtkResNode.Type = NodeType.Text; - newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop; + newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents; newTextNode->AtkResNode.DrawFlags = 12; newTextNode->AtkResNode.SetWidth(22); newTextNode->AtkResNode.SetHeight(22); @@ -304,16 +420,49 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar newTextNode->SetText(" "); - newTextNode->TextColor.R = 255; - newTextNode->TextColor.G = 255; - newTextNode->TextColor.B = 255; - newTextNode->TextColor.A = 255; - - newTextNode->EdgeColor.R = 142; - newTextNode->EdgeColor.G = 106; - newTextNode->EdgeColor.B = 12; - newTextNode->EdgeColor.A = 255; + newTextNode->TextColor = new ByteColor { R = 255, G = 255, B = 255, A = 255 }; + newTextNode->EdgeColor = new ByteColor { R = 142, G = 106, B = 12, A = 255 }; return newTextNode; } + + private void DtrEventHandler(AddonEventType atkEventType, IntPtr atkUnitBase, IntPtr atkResNode) + { + var addon = (AtkUnitBase*)atkUnitBase; + var node = (AtkResNode*)atkResNode; + + if (this.entries.FirstOrDefault(entry => entry.TextNode == node) is not { } dtrBarEntry) return; + + if (dtrBarEntry is { Tooltip: not null }) + { + switch (atkEventType) + { + case AddonEventType.MouseOver: + AtkStage.GetSingleton()->TooltipManager.ShowTooltip(addon->ID, node, dtrBarEntry.Tooltip.Encode()); + break; + + case AddonEventType.MouseOut: + AtkStage.GetSingleton()->TooltipManager.HideTooltip(addon->ID); + break; + } + } + + if (dtrBarEntry is { OnClick: not null }) + { + switch (atkEventType) + { + case AddonEventType.MouseOver: + this.uiEventManager.SetCursor(AddonCursorType.Clickable); + break; + + case AddonEventType.MouseOut: + this.uiEventManager.ResetCursor(); + break; + + case AddonEventType.MouseClick: + dtrBarEntry.OnClick.Invoke(); + break; + } + } + } } diff --git a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs new file mode 100644 index 000000000..1e6fd09cd --- /dev/null +++ b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs @@ -0,0 +1,29 @@ +namespace Dalamud.Game.Gui.Dtr; + +/// +/// DtrBar memory address resolver. +/// +public class DtrBarAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the AtkUnitBaseDraw method. + /// This is the base handler for all addons. + /// We will use this here because _DTR does not have a overloaded handler, so we must use the base handler. + /// + public nint AtkUnitBaseDraw { get; private set; } + + /// + /// Gets the address of the DTRRequestUpdate method. + /// + public nint AddonRequestedUpdate { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(SigScanner scanner) + { + this.AtkUnitBaseDraw = scanner.ScanText("48 83 EC 28 F6 81 ?? ?? ?? ?? ?? 4C 8B C1"); + this.AddonRequestedUpdate = scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B BA ?? ?? ?? ?? 48 8B F1 49 8B 98 ?? ?? ?? ?? 33 D2"); + } +} diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index c5bdb7e85..f04e1427d 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -41,6 +41,16 @@ public sealed unsafe class DtrBarEntry : IDisposable this.Dirty = true; } } + + /// + /// Gets or sets a tooltip to be shown when the user mouses over the dtr entry. + /// + public SeString? Tooltip { get; set; } + + /// + /// Gets or sets a action to be invoked when the user clicks on the dtr entry. + /// + public Action? OnClick { get; set; } /// /// Gets or sets a value indicating whether this entry is visible. From 153f7c45bf1475ccc7dcf58e88a83eb1bfc0558e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 5 Sep 2023 00:46:45 -0700 Subject: [PATCH 066/585] Fix non-reversed resizing logic --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 4d2a005ae..b1679c296 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -249,6 +249,21 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (MemoryHelper.ReadString((nint)addon->Name, 0x20) is not "_DTR") return; this.UpdateNodePositions(addon); + + if (!this.configuration.DtrSwapDirection) + { + var targetSize = (ushort)this.CalculateTotalSize(); + var sizeDelta = targetSize - addon->RootNode->Width; + + if (addon->RootNode->Width != targetSize) + { + addon->RootNode->SetWidth(targetSize); + addon->SetX((short)(addon->GetX() - sizeDelta)); + + // force a RequestedUpdate immediately to force the game to right-justify it immediately. + this.onAddonRequestedUpdateHook.Original(addon, AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); + } + } } catch (Exception e) { @@ -258,13 +273,12 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private void UpdateNodePositions(AtkUnitBase* addon) { - var targetSize = (ushort)this.CalculateTotalSize(); - addon->RootNode->SetWidth(targetSize); - // If we grow to the right, we need to left-justify the original elements. // else if we grow to the left, the game right-justifies it for us. if (this.configuration.DtrSwapDirection) { + var targetSize = (ushort)this.CalculateTotalSize(); + addon->RootNode->SetWidth(targetSize); var sizeOffset = addon->GetNodeById(17)->GetX(); var node = addon->RootNode->ChildNode; @@ -338,7 +352,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (node->NodeID is 17) totalSize += node->Width; // Node > 1000, are our custom nodes - if (node->NodeID is > 1000) totalSize += node->Width + this.configuration.DtrSpacing; + if (node->NodeID is > 1000 && node->IsVisible) totalSize += node->Width + this.configuration.DtrSpacing; } return totalSize; From 166669597dea8e3d9735d457f06fe4056b257d9d Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:03:37 -0700 Subject: [PATCH 067/585] [DtrBar] Probably fix concurrency issues --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index b1679c296..8b021bc7a 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -50,6 +51,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private readonly DalamudAddonEventManager uiEventManager = Service.Get(); private readonly DtrBarAddressResolver address; + private readonly ConcurrentBag newEntries = new(); private readonly List entries = new(); private readonly Hook onAddonDrawHook; private readonly Hook onAddonRequestedUpdateHook; @@ -78,18 +80,17 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar /// public DtrBarEntry Get(string title, SeString? text = null) { - if (this.entries.Any(x => x.Title == title)) + if (this.entries.Any(x => x.Title == title) || this.newEntries.Any(x => x.Title == title)) throw new ArgumentException("An entry with the same title already exists."); - var node = this.MakeNode(++this.runningNodeIds); - var entry = new DtrBarEntry(title, node); + var entry = new DtrBarEntry(title, null); entry.Text = text; // Add the entry to the end of the order list, if it's not there already. if (!this.configuration.DtrOrder!.Contains(title)) this.configuration.DtrOrder!.Add(title); - this.entries.Add(entry); - this.ApplySort(); + + this.newEntries.Add(entry); return entry; } @@ -173,6 +174,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private void Update(Framework unused) { this.HandleRemovedNodes(); + this.HandleAddedNodes(); var dtr = this.GetDtr(); if (dtr == null) return; @@ -238,6 +240,21 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } + private void HandleAddedNodes() + { + if (this.newEntries.Any()) + { + foreach (var newEntry in this.newEntries) + { + newEntry.TextNode = this.MakeNode(++this.runningNodeIds); + this.entries.Add(newEntry); + } + + this.newEntries.Clear(); + this.ApplySort(); + } + } + // This hooks all AtkUnitBase.Draw calls, then checks for our specific addon name. // AddonDtr doesn't implement it's own Draw method, would need to replace vtable entry to be more efficient. private void OnAddonDrawDetour(AtkUnitBase* addon) From 692113958b3481aa75ce7d727d2ae158e143d619 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:35:08 -0700 Subject: [PATCH 068/585] Scope DTRBar --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 59 ++++++++++++++++++++++++++++-- Dalamud/Plugin/Services/IDtrBar.cs | 6 +++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 8b021bc7a..2ff99a450 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -26,9 +26,6 @@ namespace Dalamud.Game.Gui.Dtr; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; @@ -94,6 +91,15 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar return entry; } + + /// + public void Remove(string title) + { + if (this.entries.FirstOrDefault(entry => entry.Title == title) is { } dtrBarEntry) + { + dtrBarEntry.Remove(); + } + } /// void IDisposable.Dispose() @@ -497,3 +503,50 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } } + +/// +/// Plugin-scoped version of a AddonEventManager service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar +{ + [ServiceManager.ServiceDependency] + private readonly DtrBar dtrBarService = Service.Get(); + + private readonly Dictionary pluginEntries = new(); + + /// + public void Dispose() + { + foreach (var entry in this.pluginEntries) + { + entry.Value.Remove(); + } + + this.pluginEntries.Clear(); + } + + /// + public DtrBarEntry Get(string title, SeString? text = null) + { + // If we already have a known entry for this plugin, return it. + if (this.pluginEntries.TryGetValue(title, out var existingEntry)) return existingEntry; + + return this.pluginEntries[title] = this.dtrBarService.Get(title, text); + } + + /// + public void Remove(string title) + { + if (this.pluginEntries.TryGetValue(title, out var existingEntry)) + { + existingEntry.Remove(); + this.pluginEntries.Remove(title); + } + } +} diff --git a/Dalamud/Plugin/Services/IDtrBar.cs b/Dalamud/Plugin/Services/IDtrBar.cs index 6c2b8ad1e..a5a750cf6 100644 --- a/Dalamud/Plugin/Services/IDtrBar.cs +++ b/Dalamud/Plugin/Services/IDtrBar.cs @@ -19,4 +19,10 @@ public interface IDtrBar /// The entry object used to update, hide and remove the entry. /// Thrown when an entry with the specified title exists. public DtrBarEntry Get(string title, SeString? text = null); + + /// + /// Removes a DTR bar entry from the system. + /// + /// Title of the entry to remove. + public void Remove(string title); } From 4dabd0713171613d1fef4ece7015dd2879decc76 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 6 Sep 2023 13:08:04 -0700 Subject: [PATCH 069/585] Prototype, untested --- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 63 ++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 185 ++++++++++++++---- .../AddonLifecycleEventListener.cs | 38 ++++ Dalamud/Hooking/Internal/CallHook.cs | 83 ++++++++ Dalamud/Plugin/Services/IAddonLifecycle.cs | 70 ++++++- 5 files changed, 393 insertions(+), 46 deletions(-) create mode 100644 Dalamud/Game/AddonLifecycle/AddonEvent.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs create mode 100644 Dalamud/Hooking/Internal/CallHook.cs diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs new file mode 100644 index 000000000..bf5ee75cf --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -0,0 +1,63 @@ +namespace Dalamud.Game.AddonLifecycle; + +/// +/// Enumeration for available AddonLifecycle events +/// +public enum AddonEvent +{ + /// + /// Event that is fired before an addon begins it's setup process. + /// + PreSetup, + + /// + /// Event that is fired after an addon has completed it's setup process. + /// + PostSetup, + + // // Events not implemented yet. + // /// + // /// Event that is fired right before an addon is set to shown. + // /// + // PreShow, + // + // /// + // /// Event that is fired after an addon has been shown. + // /// + // PostShow, + // + // /// + // /// Event that is fired right before an addon is set to hidden. + // /// + // PreHide, + // + // /// + // /// Event that is fired after an addon has been hidden. + // /// + // PostHide, + // + // /// + // /// Event that is fired before an addon begins update. + // /// + // PreUpdate, + // + // /// + // /// Event that is fired after an addon has completed update. + // /// + // PostUpdate, + // + // /// + // /// Event that is fired before an addon begins draw. + // /// + // PreDraw, + // + // /// + // /// Event that is fired after an addon has completed draw. + // /// + // PostDraw, + + /// + /// Event that is fired before an addon is finalized. + /// + PreFinalize, +} diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 72d1c25ff..d915bbd00 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; using Dalamud.Hooking; using Dalamud.IoC; @@ -14,49 +17,99 @@ namespace Dalamud.Game.AddonLifecycle; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycle +internal unsafe class AddonLifecycle : IDisposable, IServiceType { private static readonly ModuleLog Log = new("AddonLifecycle"); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private readonly AddonLifecycleAddressResolver address; private readonly Hook onAddonSetupHook; private readonly Hook onAddonFinalizeHook; + + private readonly ConcurrentBag newEventListeners = new(); + private readonly ConcurrentBag removeEventListeners = new(); + private readonly List eventListeners = new(); [ServiceManager.ServiceConstructor] private AddonLifecycle(SigScanner sigScanner) { this.address = new AddonLifecycleAddressResolver(); this.address.Setup(sigScanner); + + this.framework.Update += this.OnFrameworkUpdate; this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); this.onAddonFinalizeHook = Hook.FromAddress(this.address.AddonFinalize, this.OnAddonFinalize); } - + private delegate nint AddonSetupDelegate(AtkUnitBase* addon); private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); - - /// - public event Action? AddonPreSetup; - - /// - public event Action? AddonPostSetup; - - /// - public event Action? AddonPreFinalize; - + /// public void Dispose() { + this.framework.Update -= this.OnFrameworkUpdate; + this.onAddonSetupHook.Dispose(); this.onAddonFinalizeHook.Dispose(); } + /// + /// Register a listener for the target event and addon. + /// + /// The listener to register. + internal void RegisterListener(AddonLifecycleEventListener listener) + { + this.newEventListeners.Add(listener); + } + + /// + /// Unregisters the listener from events. + /// + /// The listener to unregister. + internal void UnregisterListener(AddonLifecycleEventListener listener) + { + this.removeEventListeners.Add(listener); + } + + // Used to prevent concurrency issues if plugins try to register during iteration of listeners. + private void OnFrameworkUpdate(Framework unused) + { + if (this.newEventListeners.Any()) + { + this.eventListeners.AddRange(this.newEventListeners); + this.newEventListeners.Clear(); + } + + if (this.removeEventListeners.Any()) + { + foreach (var toRemoveListener in this.removeEventListeners) + { + this.eventListeners.Remove(toRemoveListener); + } + + this.removeEventListeners.Clear(); + } + } + [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { this.onAddonSetupHook.Enable(); this.onAddonFinalizeHook.Enable(); } + + private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) + { + // Match on string.empty for listeners that want events for all addons. + foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) + { + listener.FunctionDelegate.Invoke(eventType, args); + } + } private nint OnAddonSetup(AtkUnitBase* addon) { @@ -65,7 +118,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl try { - this.AddonPreSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -76,7 +129,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl try { - this.AddonPostSetup?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -96,7 +149,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl try { - this.AddonPreFinalize?.Invoke(new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + this.InvokeListeners(AddonEvent.PreFinalize, new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); } catch (Exception e) { @@ -118,39 +171,93 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType, IAddonLifecycl #pragma warning restore SA1015 internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle { + private static readonly ModuleLog Log = new("AddonLifecycle:PluginScoped"); + [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); - - /// - /// Initializes a new instance of the class. - /// - public AddonLifecyclePluginScoped() - { - this.addonLifecycleService.AddonPreSetup += this.AddonPreSetupForward; - this.addonLifecycleService.AddonPostSetup += this.AddonPostSetupForward; - this.addonLifecycleService.AddonPreFinalize += this.AddonPreFinalizeForward; - } - /// - public event Action? AddonPreSetup; - - /// - public event Action? AddonPostSetup; - - /// - public event Action? AddonPreFinalize; + private readonly List eventListeners = new(); /// public void Dispose() { - this.addonLifecycleService.AddonPreSetup -= this.AddonPreSetupForward; - this.addonLifecycleService.AddonPostSetup -= this.AddonPostSetupForward; - this.addonLifecycleService.AddonPreFinalize -= this.AddonPreFinalizeForward; + foreach (var listener in this.eventListeners) + { + this.addonLifecycleService.UnregisterListener(listener); + } } - private void AddonPreSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPreSetup?.Invoke(args); + /// + public void RegisterListener(AddonEvent eventType, IEnumerable addonNames, IAddonLifecycle.AddonEventDelegate handler) + { + foreach (var addonName in addonNames) + { + this.RegisterListener(eventType, addonName, handler); + } + } - private void AddonPostSetupForward(IAddonLifecycle.AddonArgs args) => this.AddonPostSetup?.Invoke(args); + /// + public void RegisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate handler) + { + var listener = new AddonLifecycleEventListener(eventType, addonName, handler); + this.eventListeners.Add(listener); + this.addonLifecycleService.RegisterListener(listener); + } + + /// + public void RegisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate handler) + { + this.RegisterListener(eventType, string.Empty, handler); + } + + /// + public void UnregisterListener(AddonEvent eventType, IEnumerable addonNames, IAddonLifecycle.AddonEventDelegate? handler = null) + { + foreach (var addonName in addonNames) + { + this.UnregisterListener(eventType, addonName, handler); + } + } - private void AddonPreFinalizeForward(IAddonLifecycle.AddonArgs args) => this.AddonPreFinalize?.Invoke(args); + /// + public void UnregisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate? handler = null) + { + // This style is simpler to read imo. If the handler is null we want all entries, + // if they specified a handler then only the specific entries with that handler. + var targetListeners = this.eventListeners + .Where(entry => entry.EventType == eventType) + .Where(entry => entry.AddonName == addonName) + .Where(entry => handler is null || entry.FunctionDelegate == handler); + + foreach (var listener in targetListeners) + { + this.addonLifecycleService.UnregisterListener(listener); + this.eventListeners.Remove(listener); + } + } + + /// + public void UnregisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate? handler = null) + { + this.UnregisterListener(eventType, string.Empty, handler); + } + + /// + public void UnregisterListener(IAddonLifecycle.AddonEventDelegate handler, params IAddonLifecycle.AddonEventDelegate[] handlers) + { + foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handler)) + { + this.addonLifecycleService.UnregisterListener(listener); + this.eventListeners.Remove(listener); + } + + foreach (var handlerParma in handlers) + { + foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handlerParma)) + { + this.addonLifecycleService.UnregisterListener(listener); + this.eventListeners.Remove(listener); + } + } + } } diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs new file mode 100644 index 000000000..0f088362d --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs @@ -0,0 +1,38 @@ +using Dalamud.Plugin.Services; + +namespace Dalamud.Game.AddonLifecycle; + +/// +/// This class is a helper for tracking and invoking listener delegates. +/// +internal class AddonLifecycleEventListener +{ + /// + /// Initializes a new instance of the class. + /// + /// Event type to listen for. + /// Addon name to listen for. + /// Delegate to invoke. + internal AddonLifecycleEventListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate functionDelegate) + { + this.EventType = eventType; + this.AddonName = addonName; + this.FunctionDelegate = functionDelegate; + } + + /// + /// Gets the name of the addon this listener is looking for. + /// string.Empty if it wants to be called for any addon. + /// + public string AddonName { get; init; } + + /// + /// Gets the event type this listener is looking for. + /// + public AddonEvent EventType { get; init; } + + /// + /// Gets the delegate this listener invokes. + /// + public IAddonLifecycle.AddonEventDelegate FunctionDelegate { get; init; } +} diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs new file mode 100644 index 000000000..0f8c681c2 --- /dev/null +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -0,0 +1,83 @@ +using System; +using System.Runtime.InteropServices; + +using Reloaded.Hooks.Definitions; + +namespace Dalamud.Hooking.Internal; + +/// +/// Hooking class for callsite hooking. This hook does not have capabilities of calling the original function. +/// The intended use is replacing virtual function calls where you are able to manually invoke the original call using the delegate arguments. +/// +/// Delegate signature for this hook. +internal class CallHook : 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 CallHook(nint address, T detour) + { + this.detour = detour; + + 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 or not 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/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 43b9fef0a..1e318ae79 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -1,5 +1,7 @@ -using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Dalamud.Game.AddonLifecycle; using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -11,19 +13,73 @@ namespace Dalamud.Plugin.Services; public interface IAddonLifecycle { /// - /// Event that fires before an addon is being setup. + /// Delegate for receiving addon lifecycle event messages. /// - public event Action AddonPreSetup; + /// The event type that triggered the message. + /// Information about what addon triggered the message. + public delegate void AddonEventDelegate(AddonEvent eventType, AddonArgs addonInfo); /// - /// Event that fires after an addon is done being setup. + /// Register a listener that will trigger on the specified event and any of the specified addons. /// - public event Action AddonPostSetup; + /// Event type to trigger on. + /// Addon names that will trigger the handler to be invoked. + /// The handler to invoke. + void RegisterListener(AddonEvent eventType, IEnumerable addonNames, AddonEventDelegate handler); /// - /// Event that fires before an addon is being finalized. + /// Register a listener that will trigger on the specified event only for the specified addon. /// - public event Action AddonPreFinalize; + /// Event type to trigger on. + /// 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. + /// + /// + /// If a specific handler is not provided, all handlers for the event type and addon names will be unregistered. + /// + /// Event type to deregister. + /// 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. + /// + /// + /// If a specific handler is not provided, all handlers for the event type and addons will be unregistered. + /// + /// Event type to deregister. + /// 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 . + ///
+ /// + /// If a specific handler is not provided, all handlers for the event type and addons will be unregistered. + /// + /// Event type to deregister. + /// Optional specific handler to remove. + void UnregisterListener(AddonEvent eventType, [Optional] AddonEventDelegate handler); + + /// + /// Unregister all events that use the specified handlers. + /// + /// Event handler to remove. + /// Additional handlers to remove. + void UnregisterListener(AddonEventDelegate handler, params AddonEventDelegate[] handlers); /// /// Addon argument data for use in event subscribers. From 9176342ad5b4978b1f3849758d8093ee3768cbd9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 6 Sep 2023 16:00:37 -0700 Subject: [PATCH 070/585] Add AddonDraw, AddonUpdate --- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 67 ++++++----------- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 74 +++++++++++++++++-- .../AddonLifecycleAddressResolver.cs | 16 +++- 3 files changed, 106 insertions(+), 51 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs index bf5ee75cf..0125d1337 100644 --- a/Dalamud/Game/AddonLifecycle/AddonEvent.cs +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -1,7 +1,7 @@ namespace Dalamud.Game.AddonLifecycle; /// -/// Enumeration for available AddonLifecycle events +/// Enumeration for available AddonLifecycle events. /// public enum AddonEvent { @@ -9,53 +9,32 @@ public enum AddonEvent /// Event that is fired before an addon begins it's setup process. /// PreSetup, - + /// /// Event that is fired after an addon has completed it's setup process. /// PostSetup, - - // // Events not implemented yet. - // /// - // /// Event that is fired right before an addon is set to shown. - // /// - // PreShow, - // - // /// - // /// Event that is fired after an addon has been shown. - // /// - // PostShow, - // - // /// - // /// Event that is fired right before an addon is set to hidden. - // /// - // PreHide, - // - // /// - // /// Event that is fired after an addon has been hidden. - // /// - // PostHide, - // - // /// - // /// Event that is fired before an addon begins update. - // /// - // PreUpdate, - // - // /// - // /// Event that is fired after an addon has completed update. - // /// - // PostUpdate, - // - // /// - // /// Event that is fired before an addon begins draw. - // /// - // PreDraw, - // - // /// - // /// Event that is fired after an addon has completed draw. - // /// - // PostDraw, - + + /// + /// Event that is fired before an addon begins update. + /// + PreUpdate, + + /// + /// Event that is fired after an addon has completed update. + /// + PostUpdate, + + /// + /// Event that is fired before an addon begins draw. + /// + PreDraw, + + /// + /// Event that is fired after an addon has completed draw. + /// + PostDraw, + /// /// Event that is fired before an addon is finalized. /// diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index d915bbd00..3a8644c4c 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Hooking; +using Dalamud.Hooking.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -27,6 +28,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly AddonLifecycleAddressResolver address; private readonly Hook onAddonSetupHook; private readonly Hook onAddonFinalizeHook; + private readonly CallHook onAddonDrawHook; + private readonly CallHook onAddonUpdateHook; private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); @@ -42,12 +45,18 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonSetupHook = Hook.FromAddress(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); } - + private delegate nint AddonSetupDelegate(AtkUnitBase* addon); private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + private delegate void AddonDrawDelegate(AtkUnitBase* addon); + + private delegate void AddonUpdateDelegate(AtkUnitBase* addon); + /// public void Dispose() { @@ -55,6 +64,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonSetupHook.Dispose(); this.onAddonFinalizeHook.Dispose(); + this.onAddonDrawHook.Dispose(); + this.onAddonUpdateHook.Dispose(); } /// @@ -100,6 +111,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { this.onAddonSetupHook.Enable(); this.onAddonFinalizeHook.Enable(); + this.onAddonDrawHook.Enable(); + this.onAddonUpdateHook.Enable(); } private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) @@ -158,6 +171,56 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); } + + private void OnAddonDraw(AtkUnitBase* addon) + { + if (addon is null) return; + + try + { + this.InvokeListeners(AddonEvent.PreDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); + } + + ((delegate* unmanaged)addon->AtkEventListener.vfunc[42])(addon); + + try + { + this.InvokeListeners(AddonEvent.PostDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonDraw post-draw invoke."); + } + } + + private void OnAddonUpdate(AtkUnitBase* addon) + { + if (addon is null) return; + + try + { + this.InvokeListeners(AddonEvent.PreUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonUpdate pre-update invoke."); + } + + ((delegate* unmanaged)addon->AtkEventListener.vfunc[41])(addon); + + try + { + this.InvokeListeners(AddonEvent.PostUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); + } + } } /// @@ -175,7 +238,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); - + private readonly List eventListeners = new(); /// @@ -227,7 +290,8 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif var targetListeners = this.eventListeners .Where(entry => entry.EventType == eventType) .Where(entry => entry.AddonName == addonName) - .Where(entry => handler is null || entry.FunctionDelegate == handler); + .Where(entry => handler is null || entry.FunctionDelegate == handler) + .ToArray(); // Make a copy so we don't mutate this list while removing entries. foreach (var listener in targetListeners) { @@ -245,7 +309,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif /// public void UnregisterListener(IAddonLifecycle.AddonEventDelegate handler, params IAddonLifecycle.AddonEventDelegate[] handlers) { - foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handler)) + foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handler).ToArray()) { this.addonLifecycleService.UnregisterListener(listener); this.eventListeners.Remove(listener); @@ -253,7 +317,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif foreach (var handlerParma in handlers) { - foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handlerParma)) + foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handlerParma).ToArray()) { this.addonLifecycleService.UnregisterListener(listener); this.eventListeners.Remove(listener); diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index ba7b723ec..0300667a9 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -6,14 +6,24 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver { /// - /// Gets the address of the addon setup hook invoked by the atkunitmanager. + /// Gets the address of the addon setup hook invoked by the AtkUnitManager. /// public nint AddonSetup { get; private set; } /// - /// Gets the address of the addon finalize hook invoked by the atkunitmanager. + /// 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; } /// /// Scan for and setup any configured address pointers. @@ -23,5 +33,7 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver { this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); + this.AddonDraw = sig.ScanText("48 8B 01 FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); + this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); } } From 8cb76a7438ee668a413f1cbb604f0c2c045379c0 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 6 Sep 2023 20:40:46 -0700 Subject: [PATCH 071/585] Fix incorrect AtkUnitBase.Update function delegate definition --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 14 +++++--------- .../AddonLifecycleAddressResolver.cs | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 3a8644c4c..a3e9f4be8 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -55,7 +55,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate void AddonDrawDelegate(AtkUnitBase* addon); - private delegate void AddonUpdateDelegate(AtkUnitBase* addon); + private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta); /// public void Dispose() @@ -174,8 +174,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonDraw(AtkUnitBase* addon) { - if (addon is null) return; - try { this.InvokeListeners(AddonEvent.PreDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); @@ -185,7 +183,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); } - ((delegate* unmanaged)addon->AtkEventListener.vfunc[42])(addon); + addon->Draw(); try { @@ -197,10 +195,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - private void OnAddonUpdate(AtkUnitBase* addon) + private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - if (addon is null) return; - try { this.InvokeListeners(AddonEvent.PreUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); @@ -209,8 +205,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonUpdate pre-update invoke."); } - - ((delegate* unmanaged)addon->AtkEventListener.vfunc[41])(addon); + + addon->Update(delta); try { diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index 0300667a9..688476d82 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -33,7 +33,7 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver { this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); - this.AddonDraw = sig.ScanText("48 8B 01 FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); + this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); } } From 7a03458696756dd2baa4219850deccbf0e4f4ff0 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 7 Sep 2023 18:43:04 +0200 Subject: [PATCH 072/585] Update ClientStructs (#1363) 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 dcc61941b..ada62e7ae 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit dcc61941b2bd73b1aa5badb40276265b80e91a69 +Subproject commit ada62e7ae60de220d1f950b03ddb8d66e9e10daf From a12d9df9a20645c454d4b49c727fd432e8beba61 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:07:55 -0700 Subject: [PATCH 073/585] Chat Payload Remove Logspam (#1368) --- Dalamud/Game/Text/SeStringHandling/Payload.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs index dbd70a58e..117606a7a 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs @@ -206,9 +206,9 @@ public abstract partial class Payload case SeStringChunkType.Icon: payload = new IconPayload(); break; - + default: - Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType); + // Log.Verbose("Unhandled SeStringChunkType: {0}", chunkType); break; } @@ -307,6 +307,11 @@ public abstract partial class Payload /// protected enum SeStringChunkType { + /// + /// See the . + /// + NewLine = 0x10, + /// /// See the class. /// @@ -317,11 +322,6 @@ public abstract partial class Payload /// EmphasisItalic = 0x1A, - /// - /// See the . - /// - NewLine = 0x10, - /// /// See the class. /// From 633894364d850569c2270b26690a62011fd6e5b9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 7 Sep 2023 10:12:19 -0700 Subject: [PATCH 074/585] Add Targets to TargetManager (#1364) --- Dalamud/Game/ClientState/Objects/TargetManager.cs | 14 ++++++++++++++ .../Internal/Windows/Data/Widgets/TargetWidget.cs | 6 ++++++ Dalamud/Plugin/Services/ITargetManager.cs | 12 ++++++++++++ 3 files changed, 32 insertions(+) diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index ff1bdc5ba..00bcaac7d 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -70,6 +70,20 @@ public sealed unsafe class TargetManager : IServiceType, ITargetManager set => this.SetSoftTarget(value); } + /// + public GameObject? GPoseTarget + { + get => this.objectTable.CreateObjectReference((IntPtr)Struct->GPoseTarget); + set => Struct->GPoseTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; + } + + /// + public GameObject? MouseOverNameplateTarget + { + get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverNameplateTarget); + set => Struct->MouseOverNameplateTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; + } + private FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem*)this.Address; /// diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index 57fd03300..f33e67f70 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -63,6 +63,12 @@ internal class TargetWidget : IDataWindowWidget if (targetMgr.SoftTarget != null) Util.PrintGameObject(targetMgr.SoftTarget, "SoftTarget", this.resolveGameData); + + if (targetMgr.GPoseTarget != null) + Util.PrintGameObject(targetMgr.GPoseTarget, "GPoseTarget", this.resolveGameData); + + if (targetMgr.MouseOverNameplateTarget != null) + Util.PrintGameObject(targetMgr.MouseOverNameplateTarget, "MouseOverNameplateTarget", this.resolveGameData); if (ImGui.Button("Clear CT")) targetMgr.Target = null; diff --git a/Dalamud/Plugin/Services/ITargetManager.cs b/Dalamud/Plugin/Services/ITargetManager.cs index 108b1ca03..99a9d8dfb 100644 --- a/Dalamud/Plugin/Services/ITargetManager.cs +++ b/Dalamud/Plugin/Services/ITargetManager.cs @@ -41,4 +41,16 @@ public interface ITargetManager /// Set to null to clear the target. /// public GameObject? SoftTarget { get; set; } + + /// + /// Gets or sets the gpose target. + /// Set to null to clear the target. + /// + public GameObject? GPoseTarget { get; set; } + + /// + /// Gets or sets the mouseover nameplate target. + /// Set to null to clear the target. + /// + public GameObject? MouseOverNameplateTarget { get; set; } } From 37cb1f5dd075a47bd819625e8dc6011254ddf3d7 Mon Sep 17 00:00:00 2001 From: Kurochi51 Date: Thu, 7 Sep 2023 20:33:10 +0300 Subject: [PATCH 075/585] Fade to black during credits animation (#1356) --- Dalamud/Interface/Internal/DalamudInterface.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 479297c20..a8a769070 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -524,7 +524,8 @@ internal class DalamudInterface : IDisposable, IServiceType private void DrawCreditsDarkeningAnimation() { - using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f); + using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding | ImGuiStyleVar.WindowBorderSize, 0f); + using var color = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 0)); ImGui.SetNextWindowPos(new Vector2(0, 0)); ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size); From 1dbf93e428fdc4a5d324b281678b4c5befe179d7 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 7 Sep 2023 10:33:46 -0700 Subject: [PATCH 076/585] Add SpecialPluginSource to public API (#1357) --- Dalamud/Plugin/DalamudPluginInterface.cs | 6 +++--- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- .../Types/Manifest/LocalPluginManifest.cs | 14 +------------- .../Types/Manifest/SpecialPluginSource.cs | 17 +++++++++++++++++ 4 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 Dalamud/Plugin/Internal/Types/Manifest/SpecialPluginSource.cs diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 2b58c21cc..7d788726d 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -56,7 +56,7 @@ public sealed class DalamudPluginInterface : IDisposable this.configs = Service.Get().PluginConfigs; this.Reason = reason; - this.SourceRepository = this.IsDev ? LocalPluginManifest.FlagDevPlugin : plugin.Manifest.InstalledFromUrl; + this.SourceRepository = this.IsDev ? SpecialPluginSource.DevPlugin : plugin.Manifest.InstalledFromUrl; this.IsTesting = plugin.IsTesting; this.LoadTime = DateTime.Now; @@ -118,8 +118,8 @@ public sealed class DalamudPluginInterface : IDisposable /// Gets the repository from which this plugin was installed. /// /// If a plugin was installed from the official/main repository, this will return the value of - /// . Developer plugins will return the value of - /// . + /// . Developer plugins will return the value of + /// . /// public string SourceRepository { get; } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 58e122c3e..e66f226ea 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -873,7 +873,7 @@ internal partial class PluginManager : IDisposable, IServiceType } // Document the url the plugin was installed from - manifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty ? repoManifest.SourceRepo.PluginMasterUrl : LocalPluginManifest.FlagMainRepo; + manifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty ? repoManifest.SourceRepo.PluginMasterUrl : SpecialPluginSource.MainRepo; manifest.Save(manifestFile, "installation"); diff --git a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs index 8afbe1aea..b7fe6d062 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/LocalPluginManifest.cs @@ -13,18 +13,6 @@ namespace Dalamud.Plugin.Internal.Types.Manifest; ///
internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest { - /// - /// Flag indicating that a plugin was installed from the official repo. - /// - [JsonIgnore] - public const string FlagMainRepo = "OFFICIAL"; - - /// - /// Flag indicating that a plugin is a dev plugin.. - /// - [JsonIgnore] - public const string FlagDevPlugin = "DEVPLUGIN"; - /// /// Gets or sets a value indicating whether the plugin is disabled and should not be loaded. /// This value supersedes the ".disabled" file functionality and should not be included in the plugin master. @@ -51,7 +39,7 @@ internal record LocalPluginManifest : PluginManifest, ILocalPluginManifest /// Gets a value indicating whether this manifest is associated with a plugin that was installed from a third party /// repo. Unless the manifest has been manually modified, this is determined by the InstalledFromUrl being null. /// - public bool IsThirdParty => !this.InstalledFromUrl.IsNullOrEmpty() && this.InstalledFromUrl != FlagMainRepo; + public bool IsThirdParty => !this.InstalledFromUrl.IsNullOrEmpty() && this.InstalledFromUrl != SpecialPluginSource.MainRepo; /// /// Gets the effective version of this plugin. diff --git a/Dalamud/Plugin/Internal/Types/Manifest/SpecialPluginSource.cs b/Dalamud/Plugin/Internal/Types/Manifest/SpecialPluginSource.cs new file mode 100644 index 000000000..d6508019d --- /dev/null +++ b/Dalamud/Plugin/Internal/Types/Manifest/SpecialPluginSource.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Plugin.Internal.Types.Manifest; + +/// +/// A fake enum representing "special" sources for plugins. +/// +public static class SpecialPluginSource +{ + /// + /// Indication that this plugin came from the official Dalamud repository. + /// + public const string MainRepo = "OFFICIAL"; + + /// + /// Indication that this plugin is loaded as a dev plugin. See also . + /// + public const string DevPlugin = "DEVPLUGIN"; +} From 8c51bbf0f8140913a88deee072134200a06540c5 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 7 Sep 2023 10:58:41 -0700 Subject: [PATCH 077/585] Add Scoped Plugin Log Service (#1341) Adds a new `IPluginLog` service to Dalamud, which provides scoped logging on a per-plugin basis. This improves log performance for plugins, and paves the way for per-plugin log levels. * Plugins must opt in to enable verbose logging by setting `IPluginLog.MinimumLogLevel` to `LogEventLevel.Verbose`. This option is automatically enabled for dev plugins and is currently not persisted. * All release plugins will default to `Debug` as their lowest allowed log level. * This setting does not override the global log level set in Dalamud. --- Dalamud.CorePlugin/PluginImpl.cs | 3 +- Dalamud/Logging/PluginLog.cs | 69 +----------- Dalamud/Logging/ScopedPluginLogService.cs | 130 ++++++++++++++++++++++ Dalamud/Plugin/Services/IPluginLog.cs | 128 +++++++++++++++++++++ 4 files changed, 263 insertions(+), 67 deletions(-) create mode 100644 Dalamud/Logging/ScopedPluginLogService.cs create mode 100644 Dalamud/Plugin/Services/IPluginLog.cs diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index b858e9a0c..2f76a1087 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -6,6 +6,7 @@ using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Logging; using Dalamud.Plugin; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -56,7 +57,7 @@ namespace Dalamud.CorePlugin /// /// Dalamud plugin interface. /// Logging service. - public PluginImpl(DalamudPluginInterface pluginInterface, PluginLog log) + public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log) { try { diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index acbd663e7..b2f2a5065 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -1,9 +1,6 @@ using System; using System.Reflection; -using Dalamud.IoC; -using Dalamud.IoC.Internal; -using Dalamud.Plugin.Internal.Types; using Serilog; using Serilog.Events; @@ -12,29 +9,9 @@ namespace Dalamud.Logging; /// /// Class offering various static methods to allow for logging in plugins. /// -[PluginInterface] -[InterfaceVersion("1.0")] -[ServiceManager.ScopedService] -public class PluginLog : IServiceType, IDisposable +public static class PluginLog { - private readonly LocalPlugin plugin; - - /// - /// Initializes a new instance of the class. - /// Do not use this ctor, inject PluginLog instead. - /// - /// The plugin this service is scoped for. - internal PluginLog(LocalPlugin plugin) - { - this.plugin = plugin; - } - - /// - /// Gets or sets a prefix appended to log messages. - /// - public string? LogPrefix { get; set; } = null; - - #region Legacy static "Log" prefixed Serilog style methods + #region "Log" prefixed Serilog style methods /// /// Log a templated message to the in-game debug log. @@ -157,7 +134,7 @@ public class PluginLog : IServiceType, IDisposable #endregion - #region Legacy static Serilog style methods + #region Serilog style methods /// /// Log a templated verbose message to the in-game debug log. @@ -277,25 +254,6 @@ public class PluginLog : IServiceType, IDisposable public static void LogRaw(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values) => WriteLog(Assembly.GetCallingAssembly().GetName().Name, level, messageTemplate, exception, values); - /// - void IDisposable.Dispose() - { - // ignored - } - - #region New instanced methods - - /// - /// Log some information. - /// - /// The message. - internal void Information(string message) - { - Serilog.Log.Information($"[{this.plugin.InternalName}] {this.LogPrefix} {message}"); - } - - #endregion - private static ILogger GetPluginLogger(string? pluginName) { return Serilog.Log.ForContext("SourceContext", pluginName ?? string.Empty); @@ -314,24 +272,3 @@ public class PluginLog : IServiceType, IDisposable values); } } - -/// -/// Class offering logging services, for a specific type. -/// -/// The type to log for. -[PluginInterface] -[InterfaceVersion("1.0")] -[ServiceManager.ScopedService] -public class PluginLog : PluginLog -{ - /// - /// Initializes a new instance of the class. - /// Do not use this ctor, inject PluginLog instead. - /// - /// The plugin this service is scoped for. - internal PluginLog(LocalPlugin plugin) - : base(plugin) - { - this.LogPrefix = typeof(T).Name; - } -} diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs new file mode 100644 index 000000000..8c502fcf0 --- /dev/null +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -0,0 +1,130 @@ +using System; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; +using Serilog; +using Serilog.Core; +using Serilog.Events; + +namespace Dalamud.Logging; + +/// +/// Implementation of . +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable +{ + private readonly LocalPlugin localPlugin; + + private readonly LoggingLevelSwitch levelSwitch; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin that owns this service. + internal ScopedPluginLogService(LocalPlugin localPlugin) + { + this.localPlugin = localPlugin; + + this.levelSwitch = new LoggingLevelSwitch(this.GetDefaultLevel()); + + var loggerConfiguration = new LoggerConfiguration() + .Enrich.WithProperty("Dalamud.PluginName", localPlugin.InternalName) + .MinimumLevel.ControlledBy(this.levelSwitch) + .WriteTo.Logger(Log.Logger); + + this.Logger = loggerConfiguration.CreateLogger(); + } + + /// + public LogEventLevel MinimumLogLevel + { + get => this.levelSwitch.MinimumLevel; + set => this.levelSwitch.MinimumLevel = value; + } + + /// + public ILogger Logger { get; } + + /// + public void Dispose() + { + GC.SuppressFinalize(this); + } + + /// + public void Fatal(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Fatal, null, messageTemplate, values); + + /// + public void Fatal(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Fatal, exception, messageTemplate, values); + + /// + public void Error(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Error, null, messageTemplate, values); + + /// + public void Error(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Error, exception, messageTemplate, values); + + /// + public void Warning(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Warning, null, messageTemplate, values); + + /// + public void Warning(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Warning, exception, messageTemplate, values); + + /// + public void Information(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Information, null, messageTemplate, values); + + /// + public void Information(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Information, exception, messageTemplate, values); + + /// + public void Debug(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Debug, null, messageTemplate, values); + + /// + public void Debug(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Debug, exception, messageTemplate, values); + + /// + public void Verbose(string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Verbose, null, messageTemplate, values); + + /// + public void Verbose(Exception? exception, string messageTemplate, params object[] values) => + this.Write(LogEventLevel.Verbose, exception, messageTemplate, values); + + /// + public void Write(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values) + { + this.Logger.Write( + level, + exception: exception, + messageTemplate: $"[{this.localPlugin.InternalName}] {messageTemplate}", + values); + } + + /// + /// Gets the default log level for this plugin. + /// + /// A log level. + private LogEventLevel GetDefaultLevel() + { + // TODO: Add some way to save log levels to a config. Or let plugins handle it? + + return this.localPlugin.IsDev ? LogEventLevel.Verbose : LogEventLevel.Debug; + } +} diff --git a/Dalamud/Plugin/Services/IPluginLog.cs b/Dalamud/Plugin/Services/IPluginLog.cs new file mode 100644 index 000000000..87876f36f --- /dev/null +++ b/Dalamud/Plugin/Services/IPluginLog.cs @@ -0,0 +1,128 @@ +using System; + +using Serilog; +using Serilog.Events; + +namespace Dalamud.Plugin.Services; + +/// +/// An opinionated service to handle logging for plugins. +/// +public interface IPluginLog +{ + /// + /// Gets or sets the minimum log level that will be recorded from this plugin to Dalamud's logs. This may be set + /// by either the plugin or by Dalamud itself. + /// + /// + /// Defaults to for downloaded plugins, and + /// for dev plugins. + /// + LogEventLevel MinimumLogLevel { get; set; } + + /// + /// Gets an instance of the Serilog for advanced use cases. The provided logger will handle + /// tagging all log messages with the appropriate context variables and properties. + /// + /// + /// Not currently part of public API - will be added after some formatter work has been completed. + /// + internal ILogger Logger { get; } + + /// + /// Log a message to the Dalamud log for this plugin. This log level should be + /// used primarily for unrecoverable errors or critical faults in a plugin. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Fatal(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Fatal(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log a message to the Dalamud log for this plugin. This log level should be + /// used for recoverable errors or faults that impact plugin functionality. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Error(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Error(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log a message to the Dalamud log for this plugin. This log level should be + /// used for user error, potential problems, or high-importance messages that should be logged. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Warning(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Warning(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log an message to the Dalamud log for this plugin. This log level + /// should be used for general plugin operations and other relevant information to track a plugin's behavior. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Information(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Information(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log a message to the Dalamud log for this plugin. This log level should be + /// used for messages or information that aid with debugging or tracing a plugin's operations, but should not be + /// recorded unless requested. + /// + /// + /// By default, this log level is below the default log level of Dalamud. Messages logged at this level will not be + /// recorded unless the global log level is specifically set to Debug or lower. If information should be generally + /// or easily accessible for support purposes without the user taking additional action, consider using the + /// Information level instead. Developers should not use this log level where it can be triggered on a + /// per-frame basis. + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Debug(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Debug(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Log a message to the Dalamud log for this plugin. This log level is + /// intended almost primarily for development purposes and detailed tracing of a plugin's operations. Verbose logs + /// should not be used to expose information useful for support purposes. + /// + /// + /// By default, this log level is below the default log level of Dalamud. Messages logged at this level will not be + /// recorded unless the global log level is specifically set to Verbose. Release plugins must also set the + /// to to use this level, and should only do so + /// upon specific user request (e.g. a "Enable Troubleshooting Logs" button). + /// + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Verbose(string messageTemplate, params object[] values); + + /// + /// An (optional) exception that should be recorded alongside this event. + void Verbose(Exception? exception, string messageTemplate, params object[] values); + + /// + /// Write a raw log event to the plugin's log. Used for interoperability with other log systems, as well as + /// advanced use cases. + /// + /// The log level for this event. + /// An (optional) exception that should be recorded alongside this event. + /// Message template describing the event. + /// Objects positionally formatted into the message template. + void Write(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values); +} From 6a0401646ff99ea845fb16e7f48a7e3e1c7314dd Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 7 Sep 2023 11:47:35 -0700 Subject: [PATCH 078/585] Add AddonOnRequestedUpdate --- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 10 ++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 31 +++++++++++++++++++ .../AddonLifecycleAddressResolver.cs | 6 ++++ 3 files changed, 47 insertions(+) diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs index 0125d1337..30121d162 100644 --- a/Dalamud/Game/AddonLifecycle/AddonEvent.cs +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -39,4 +39,14 @@ public enum AddonEvent /// Event that is fired before an addon is finalized. /// PreFinalize, + + /// + /// Event that is fired before an addon begins a requested update. + /// + PreRequestedUpdate, + + /// + /// Event that is fired after an addon finishes a requested update. + /// + PostRequestedUpdate, } diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index a3e9f4be8..9bc792f46 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -30,6 +30,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly Hook onAddonFinalizeHook; private readonly CallHook onAddonDrawHook; private readonly CallHook onAddonUpdateHook; + // private readonly CallHook onAddonRequestedUpdateHook; // See Note in Ctor private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); @@ -47,6 +48,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType 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); + + // todo: reenable this. WARNING: This hook overwrites a system that SimpleTweaks uses, causing SimpleTweaks to report exceptions. + // this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); } private delegate nint AddonSetupDelegate(AtkUnitBase* addon); @@ -57,6 +61,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta); + private delegate void AddonOnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + /// public void Dispose() { @@ -66,6 +72,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Dispose(); this.onAddonDrawHook.Dispose(); this.onAddonUpdateHook.Dispose(); + // this.onAddonRequestedUpdateHook.Dispose(); // See Note in Ctor } /// @@ -113,6 +120,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Enable(); this.onAddonDrawHook.Enable(); this.onAddonUpdateHook.Enable(); + // this.onAddonRequestedUpdateHook.Enable(); // See Note in Ctor } private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) @@ -217,6 +225,29 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); } } + + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + { + try + { + this.InvokeListeners(AddonEvent.PreRequestedUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke."); + } + + addon->OnUpdate(numberArrayData, stringArrayData); + + try + { + this.InvokeListeners(AddonEvent.PostRequestedUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke."); + } + } } /// diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index 688476d82..557cedc34 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -24,6 +24,11 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver /// 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. @@ -35,5 +40,6 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); + this.AddonOnRequestedUpdate = sig.ScanText("FF 90 90 01 00 00 48 8B 5C 24 30 48 83 C4 20"); } } From e54b09a3c5c132ac2989cbf682bf4a35b82d0064 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 7 Sep 2023 12:24:45 -0700 Subject: [PATCH 079/585] Add Addon.OnRefresh --- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 10 ++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 31 ++++++++++++++++++- .../AddonLifecycleAddressResolver.cs | 6 ++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs index 30121d162..faef30c88 100644 --- a/Dalamud/Game/AddonLifecycle/AddonEvent.cs +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -49,4 +49,14 @@ public enum AddonEvent /// Event that is fired after an addon finishes a requested update. /// PostRequestedUpdate, + + /// + /// Event that is fired before an addon begins a refresh. + /// + PreRefresh, + + /// + /// Event that is fired after an addon has finished a refresh. + /// + PostRefresh, } diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 9bc792f46..d6da18dd5 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -30,6 +30,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly Hook onAddonFinalizeHook; private readonly CallHook onAddonDrawHook; private readonly CallHook onAddonUpdateHook; + private readonly Hook onAddonRefreshHook; // private readonly CallHook onAddonRequestedUpdateHook; // See Note in Ctor private readonly ConcurrentBag newEventListeners = new(); @@ -48,6 +49,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType 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(this.address.AddonOnRefresh, this.OnAddonRefresh); // todo: reenable this. WARNING: This hook overwrites a system that SimpleTweaks uses, causing SimpleTweaks to report exceptions. // this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); @@ -61,7 +63,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate void AddonUpdateDelegate(AtkUnitBase* addon, float delta); - private delegate void AddonOnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + private delegate void AddonOnRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); + + private delegate void AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); /// public void Dispose() @@ -72,6 +76,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Dispose(); this.onAddonDrawHook.Dispose(); this.onAddonUpdateHook.Dispose(); + this.onAddonRefreshHook.Dispose(); // this.onAddonRequestedUpdateHook.Dispose(); // See Note in Ctor } @@ -120,6 +125,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Enable(); this.onAddonDrawHook.Enable(); this.onAddonUpdateHook.Enable(); + this.onAddonRefreshHook.Enable(); // this.onAddonRequestedUpdateHook.Enable(); // See Note in Ctor } @@ -226,6 +232,29 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } + private void OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) + { + try + { + this.InvokeListeners(AddonEvent.PreRefresh, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke."); + } + + this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values); + + try + { + this.InvokeListeners(AddonEvent.PostRefresh, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); + } + } + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { try diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index 557cedc34..079e09c80 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -29,6 +29,11 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver /// Gets the address of the addon onRequestedUpdate hook invoked by virtual function call. /// public nint AddonOnRequestedUpdate { get; private set; } + + /// + /// Gets the address of AtkUnitManager_vf10 which triggers addon onRefresh. + /// + public nint AddonOnRefresh { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -41,5 +46,6 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); this.AddonOnRequestedUpdate = sig.ScanText("FF 90 90 01 00 00 48 8B 5C 24 30 48 83 C4 20"); + this.AddonOnRefresh = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 41 8B F8 48 8B DA"); } } From 371b8a9dccb6324114c763d83f11e70ea60fb1df Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 00:16:25 -0700 Subject: [PATCH 080/585] Restore AddonOnRequestedUpdate --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index d6da18dd5..321c60a15 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -31,7 +31,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly CallHook onAddonDrawHook; private readonly CallHook onAddonUpdateHook; private readonly Hook onAddonRefreshHook; - // private readonly CallHook onAddonRequestedUpdateHook; // See Note in Ctor + private readonly CallHook onAddonRequestedUpdateHook; private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); @@ -50,9 +50,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonDrawHook = new CallHook(this.address.AddonDraw, this.OnAddonDraw); this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); this.onAddonRefreshHook = Hook.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh); - - // todo: reenable this. WARNING: This hook overwrites a system that SimpleTweaks uses, causing SimpleTweaks to report exceptions. - // this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); + this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); } private delegate nint AddonSetupDelegate(AtkUnitBase* addon); @@ -77,7 +75,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonDrawHook.Dispose(); this.onAddonUpdateHook.Dispose(); this.onAddonRefreshHook.Dispose(); - // this.onAddonRequestedUpdateHook.Dispose(); // See Note in Ctor + this.onAddonRequestedUpdateHook.Dispose(); } /// @@ -126,7 +124,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonDrawHook.Enable(); this.onAddonUpdateHook.Enable(); this.onAddonRefreshHook.Enable(); - // this.onAddonRequestedUpdateHook.Enable(); // See Note in Ctor + this.onAddonRequestedUpdateHook.Enable(); } private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) From e914f339906b8656211d7efd4da678e416e93d35 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 01:20:29 -0700 Subject: [PATCH 081/585] Remove redundant null checking, and unused module log. There's no reasonable way the args passed in will be null. There's tons of null checking that square does before passing the pointers to these functions. If they are null, then the game has likely already crashed. --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 321c60a15..014033312 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -138,9 +138,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private nint OnAddonSetup(AtkUnitBase* addon) { - if (addon is null) - return this.onAddonSetupHook.Original(addon); - try { this.InvokeListeners(AddonEvent.PreSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); @@ -166,12 +163,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { - if (atkUnitBase is null || atkUnitBase[0] is null) - { - this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); - return; - } - try { this.InvokeListeners(AddonEvent.PreFinalize, new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); @@ -288,8 +279,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType #pragma warning restore SA1015 internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle { - private static readonly ModuleLog Log = new("AddonLifecycle:PluginScoped"); - [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); From 967c79fdd318acba110772fea546e820b9c0fe2c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 02:33:27 -0700 Subject: [PATCH 082/585] Improve logic for Unregistering Listeners --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 014033312..c0094d11a 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -21,10 +21,10 @@ namespace Dalamud.Game.AddonLifecycle; internal unsafe class AddonLifecycle : IDisposable, IServiceType { private static readonly ModuleLog Log = new("AddonLifecycle"); - + [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - + private readonly AddonLifecycleAddressResolver address; private readonly Hook onAddonSetupHook; private readonly Hook onAddonFinalizeHook; @@ -36,13 +36,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); private readonly List eventListeners = new(); - + [ServiceManager.ServiceConstructor] private AddonLifecycle(SigScanner sigScanner) { this.address = new AddonLifecycleAddressResolver(); this.address.Setup(sigScanner); - + this.framework.Update += this.OnFrameworkUpdate; this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); @@ -69,7 +69,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType public void Dispose() { this.framework.Update -= this.OnFrameworkUpdate; - + this.onAddonSetupHook.Dispose(); this.onAddonFinalizeHook.Dispose(); this.onAddonDrawHook.Dispose(); @@ -77,7 +77,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRefreshHook.Dispose(); this.onAddonRequestedUpdateHook.Dispose(); } - + /// /// Register a listener for the target event and addon. /// @@ -95,7 +95,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { this.removeEventListeners.Add(listener); } - + // Used to prevent concurrency issues if plugins try to register during iteration of listeners. private void OnFrameworkUpdate(Framework unused) { @@ -111,11 +111,11 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { this.eventListeners.Remove(toRemoveListener); } - + this.removeEventListeners.Clear(); } } - + [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() { @@ -126,7 +126,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRefreshHook.Enable(); this.onAddonRequestedUpdateHook.Enable(); } - + private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) { // Match on string.empty for listeners that want events for all addons. @@ -174,7 +174,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); } - + private void OnAddonDraw(AtkUnitBase* addon) { try @@ -185,7 +185,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); } - + addon->Draw(); try @@ -197,7 +197,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonDraw post-draw invoke."); } } - + private void OnAddonUpdate(AtkUnitBase* addon, float delta) { try @@ -220,7 +220,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); } } - + private void OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) { try @@ -243,7 +243,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); } } - + private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { try @@ -283,7 +283,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif private readonly AddonLifecycle addonLifecycleService = Service.Get(); private readonly List eventListeners = new(); - + /// public void Dispose() { @@ -301,7 +301,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif this.RegisterListener(eventType, addonName, handler); } } - + /// public void RegisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate handler) { @@ -324,31 +324,27 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif this.UnregisterListener(eventType, addonName, handler); } } - + /// public void UnregisterListener(AddonEvent eventType, string addonName, IAddonLifecycle.AddonEventDelegate? handler = null) { - // This style is simpler to read imo. If the handler is null we want all entries, - // if they specified a handler then only the specific entries with that handler. - var targetListeners = this.eventListeners - .Where(entry => entry.EventType == eventType) - .Where(entry => entry.AddonName == addonName) - .Where(entry => handler is null || entry.FunctionDelegate == handler) - .ToArray(); // Make a copy so we don't mutate this list while removing entries. - - foreach (var listener in targetListeners) + this.eventListeners.RemoveAll(entry => { - this.addonLifecycleService.UnregisterListener(listener); - this.eventListeners.Remove(listener); - } + if (entry.EventType != eventType) return false; + if (entry.AddonName != addonName) return false; + if (handler is not null && entry.FunctionDelegate != handler) return false; + + this.addonLifecycleService.UnregisterListener(entry); + return true; + }); } - + /// public void UnregisterListener(AddonEvent eventType, IAddonLifecycle.AddonEventDelegate? handler = null) { this.UnregisterListener(eventType, string.Empty, handler); } - + /// public void UnregisterListener(IAddonLifecycle.AddonEventDelegate handler, params IAddonLifecycle.AddonEventDelegate[] handlers) { From 40b875c8e93b796eb9104233263bf6bd790afc6d Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 08:37:05 -0700 Subject: [PATCH 083/585] Remove inconsistent reference to `third-party` (#1372) --- .../Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs index 6e476792b..2fac28070 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs @@ -96,7 +96,7 @@ public class ThirdRepoSettingsEntry : SettingsEntry ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning5", "If someone told you to copy/paste something here, it's very possible that you are being scammed or taken advantage of.")); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning2", "Plugins have full control over your PC, like any other program, and may cause harm or crashes.")); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning4", "They can delete your character, steal your FC or Discord account, and burn down your house.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning3", "Please make absolutely sure that you only install third-party plugins from developers you trust.")); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning3", "Please make absolutely sure that you only install plugins from developers you trust.")); if (!disclaimerDismissed) { From 1b4bee3d1302a4a779dd6efa396f0ad1cf8008ae Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 8 Sep 2023 21:07:03 -0700 Subject: [PATCH 084/585] Remove array copy of handlers. --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 20 ++++++++----------- Dalamud/Plugin/Services/IAddonLifecycle.cs | 5 ++--- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index c0094d11a..c0e817f0d 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -346,21 +346,17 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif } /// - public void UnregisterListener(IAddonLifecycle.AddonEventDelegate handler, params IAddonLifecycle.AddonEventDelegate[] handlers) + public void UnregisterListener(params IAddonLifecycle.AddonEventDelegate[] handlers) { - foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handler).ToArray()) + foreach (var handler in handlers) { - this.addonLifecycleService.UnregisterListener(listener); - this.eventListeners.Remove(listener); - } - - foreach (var handlerParma in handlers) - { - foreach (var listener in this.eventListeners.Where(entry => entry.FunctionDelegate == handlerParma).ToArray()) + this.eventListeners.RemoveAll(entry => { - this.addonLifecycleService.UnregisterListener(listener); - this.eventListeners.Remove(listener); - } + if (entry.FunctionDelegate != handler) return false; + + this.addonLifecycleService.UnregisterListener(entry); + return true; + }); } } } diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 1e318ae79..cbb3d7c24 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -77,9 +77,8 @@ public interface IAddonLifecycle /// /// Unregister all events that use the specified handlers. /// - /// Event handler to remove. - /// Additional handlers to remove. - void UnregisterListener(AddonEventDelegate handler, params AddonEventDelegate[] handlers); + /// Handlers to remove. + void UnregisterListener(params AddonEventDelegate[] handlers); /// /// Addon argument data for use in event subscribers. From 764e0a81b7caf732846c2363b48fa68e642fe3d3 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 9 Sep 2023 15:10:52 -0700 Subject: [PATCH 085/585] Fix log filtering with IPluginLog - Rename `PluginLog`'s property to `Dalamud.PluginName` to match what `IPluginLog` is doing. - Change `ModuleLog` to use `Dalamud.ModuleName` as its context property. - Update the Console window to handle both changes. - Add the ability to filter to only Dalamud module log messages. --- .../Internal/Windows/ConsoleWindow.cs | 34 +++++++++++++------ Dalamud/Logging/Internal/ModuleLog.cs | 11 +++--- Dalamud/Logging/PluginLog.cs | 2 +- 3 files changed, 31 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 872fdcd37..3303a2280 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -180,17 +180,20 @@ internal class ConsoleWindow : Window, IDisposable } // Filter by specific plugin(s) - var pluginInternalNames = Service.Get().InstalledPlugins + var sourceNames = Service.Get().InstalledPlugins .Select(p => p.Manifest.InternalName) - .OrderBy(s => s).ToList(); + .OrderBy(s => s) + .Prepend("DalamudInternal") + .ToList(); + var sourcePreviewVal = this.sourceFilters.Count switch { - 0 => "All plugins...", - 1 => "1 plugin...", - _ => $"{this.sourceFilters.Count} plugins...", + 0 => "All sources...", + 1 => "1 source...", + _ => $"{this.sourceFilters.Count} sources...", }; - var sourceSelectables = pluginInternalNames.Union(this.sourceFilters).ToList(); - if (ImGui.BeginCombo("Plugins", sourcePreviewVal)) + var sourceSelectables = sourceNames.Union(this.sourceFilters).ToList(); + if (ImGui.BeginCombo("Sources", sourcePreviewVal)) { foreach (var selectable in sourceSelectables) { @@ -443,7 +446,8 @@ internal class ConsoleWindow : Window, IDisposable // TODO: Improve this, add partial completion // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484 - var candidates = Service.Get().Commands.Where(x => x.Key.Contains("/" + words[0])).ToList(); + var candidates = Service.Get().Commands.Where(x => x.Key.Contains("/" + words[0])) + .ToList(); if (candidates.Count > 0) { ptr.DeleteChars(0, ptr.BufTextLen); @@ -499,9 +503,13 @@ internal class ConsoleWindow : Window, IDisposable TimeStamp = logEvent.Timestamp, HasException = logEvent.Exception != null, }; - - if (logEvent.Properties.TryGetValue("SourceContext", out var sourceProp) && - sourceProp is ScalarValue { Value: string value }) + + if (logEvent.Properties.ContainsKey("Dalamud.ModuleName")) + { + entry.Source = "DalamudInternal"; + } + else if (logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) && + sourceProp is ScalarValue { Value: string value }) { entry.Source = value; } @@ -579,6 +587,10 @@ internal class ConsoleWindow : Window, IDisposable public bool IsMultiline { get; set; } + /// + /// Gets or sets the system responsible for generating this log entry. Generally will be a plugin's + /// InternalName. + /// public string? Source { get; set; } public bool HasException { get; set; } diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index c6c66e81a..2fb735640 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -12,6 +12,10 @@ public class ModuleLog { private readonly string moduleName; private readonly ILogger moduleLogger; + + // FIXME (v9): Deprecate this class in favor of using contextualized ILoggers with proper formatting. + // We can keep this class around as a Serilog helper, but ModuleLog should no longer be a returned + // type, instead returning a (prepared) ILogger appropriately. /// /// Initializes a new instance of the class. @@ -20,10 +24,8 @@ public class ModuleLog /// The module name. public ModuleLog(string? moduleName) { - // FIXME: Should be namespaced better, e.g. `Dalamud.PluginLoader`, but that becomes a relatively large - // change. this.moduleName = moduleName ?? "DalamudInternal"; - this.moduleLogger = Log.ForContext("SourceContext", this.moduleName); + this.moduleLogger = Log.ForContext("Dalamud.ModuleName", this.moduleName); } /// @@ -128,7 +130,8 @@ public class ModuleLog public void Fatal(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); - private void WriteLog(LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) + private void WriteLog( + LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) { // FIXME: Eventually, the `pluginName` tag should be removed from here and moved over to the actual log // formatter. diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index b2f2a5065..3ac98f15a 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -256,7 +256,7 @@ public static class PluginLog private static ILogger GetPluginLogger(string? pluginName) { - return Serilog.Log.ForContext("SourceContext", pluginName ?? string.Empty); + return Serilog.Log.ForContext("Dalamud.PluginName", pluginName ?? string.Empty); } private static void WriteLog(string? pluginName, LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) From f1c8201f1b027f0e35400f1899e53bd0dd8ffe07 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:49:04 -0700 Subject: [PATCH 086/585] Use vfunc call instead of hook --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 2ff99a450..ae01d4886 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -284,7 +284,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar addon->SetX((short)(addon->GetX() - sizeDelta)); // force a RequestedUpdate immediately to force the game to right-justify it immediately. - this.onAddonRequestedUpdateHook.Original(addon, AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); + addon->OnUpdate(AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); } } } From c9a5c7c4c53d3af8280b5ff809f18c1ba3f3e82c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 9 Sep 2023 20:21:26 -0700 Subject: [PATCH 087/585] Add AddonLifecycle to Self-Test --- Dalamud/Hooking/Internal/CallHook.cs | 10 +- .../AgingSteps/AddonLifecycleAgingStep.cs | 133 ++++++++++++++++++ .../Windows/SelfTest/SelfTestWindow.cs | 1 + 3 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs index 0f8c681c2..2bef59c86 100644 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -6,8 +6,14 @@ using Reloaded.Hooks.Definitions; namespace Dalamud.Hooking.Internal; /// -/// Hooking class for callsite hooking. This hook does not have capabilities of calling the original function. -/// The intended use is replacing virtual function calls where you are able to manually invoke the original call using the delegate arguments. +/// 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 : IDisposable where T : Delegate diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs new file mode 100644 index 000000000..9dcaec558 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; + +using Dalamud.Game.AddonLifecycle; +using Dalamud.Plugin.Services; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; + +/// +/// Test setup AddonLifecycle Service. +/// +internal class AddonLifecycleAgingStep : IAgingStep +{ + private readonly List listeners; + + private AddonLifecycle? service; + private TestStep currentStep = TestStep.CharacterRefresh; + private bool listenersRegistered; + + /// + /// Initializes a new instance of the class. + /// + public AddonLifecycleAgingStep() + { + this.listeners = new List + { + new(AddonEvent.PostSetup, "Character", this.PostSetup), + new(AddonEvent.PostUpdate, "Character", this.PostUpdate), + new(AddonEvent.PostDraw, "Character", this.PostDraw), + new(AddonEvent.PostRefresh, "Character", this.PostRefresh), + new(AddonEvent.PostRequestedUpdate, "Character", this.PostRequestedUpdate), + new(AddonEvent.PreFinalize, "Character", this.PreFinalize), + }; + } + + private enum TestStep + { + CharacterRefresh, + CharacterSetup, + CharacterRequestedUpdate, + CharacterUpdate, + CharacterDraw, + CharacterFinalize, + Complete, + } + + /// + public string Name => "Test AddonLifecycle"; + + /// + public SelfTestStepResult RunStep() + { + this.service ??= Service.Get(); + if (this.service is null) return SelfTestStepResult.Fail; + + if (!this.listenersRegistered) + { + foreach (var listener in this.listeners) + { + this.service.RegisterListener(listener); + } + + this.listenersRegistered = true; + } + + switch (this.currentStep) + { + case TestStep.CharacterRefresh: + ImGui.Text("Open Character Window."); + break; + + case TestStep.CharacterSetup: + ImGui.Text("Open Character Window."); + break; + + case TestStep.CharacterRequestedUpdate: + ImGui.Text("Change tabs, or un-equip/equip gear."); + break; + + case TestStep.CharacterFinalize: + ImGui.Text("Close Character Window."); + break; + + case TestStep.CharacterUpdate: + case TestStep.CharacterDraw: + case TestStep.Complete: + default: + // Nothing to report to tester. + break; + } + + return this.currentStep is TestStep.Complete ? SelfTestStepResult.Pass : SelfTestStepResult.Waiting; + } + + /// + public void CleanUp() + { + foreach (var listener in this.listeners) + { + this.service?.UnregisterListener(listener); + } + } + + private void PostSetup(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterSetup) this.currentStep++; + } + + private void PostUpdate(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterUpdate) this.currentStep++; + } + + private void PostDraw(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterDraw) this.currentStep++; + } + + private void PostRefresh(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterRefresh) this.currentStep++; + } + + private void PostRequestedUpdate(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterRequestedUpdate) this.currentStep++; + } + + private void PreFinalize(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + { + if (this.currentStep is TestStep.CharacterFinalize) this.currentStep++; + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 3e25b6f5a..68d197208 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -39,6 +39,7 @@ internal class SelfTestWindow : Window new ChatAgingStep(), new HoverAgingStep(), new LuminaAgingStep(), + new AddonLifecycleAgingStep(), new PartyFinderAgingStep(), new HandledExceptionAgingStep(), new DutyStateAgingStep(), From 385c4b7a8b01b01514a1e47042d58d752f1ab147 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 13:19:44 -0700 Subject: [PATCH 088/585] Add IFramework (#1286) --- Dalamud/Game/Framework.cs | 106 ++++-------------- Dalamud/Plugin/Services/IFramework.cs | 126 ++++++++++++++++++++++ Dalamud/Utility/EventHandlerExtensions.cs | 5 +- 3 files changed, 150 insertions(+), 87 deletions(-) create mode 100644 Dalamud/Plugin/Services/IFramework.cs diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index b3083e913..2b77bf400 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -12,6 +12,7 @@ using Dalamud.Game.Gui.Toast; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -23,7 +24,10 @@ namespace Dalamud.Game; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class Framework : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed class Framework : IDisposable, IServiceType, IFramework { private static readonly Stopwatch StatsStopwatch = new(); @@ -57,12 +61,6 @@ public sealed class Framework : IDisposable, IServiceType this.destroyHook = Hook.FromAddress(this.Address.DestroyAddress, this.HandleFrameworkDestroy); } - /// - /// A delegate type used with the event. - /// - /// The Framework instance. - public delegate void OnUpdateDelegate(Framework framework); - /// /// A delegate type used during the native Framework::destroy. /// @@ -81,10 +79,8 @@ public sealed class Framework : IDisposable, IServiceType private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate - /// - /// Event that gets fired every time the game framework updates. - /// - public event OnUpdateDelegate Update; + /// + public event IFramework.OnUpdateDelegate Update; /// /// Gets or sets a value indicating whether the collection of stats is enabled. @@ -96,34 +92,22 @@ public sealed class Framework : IDisposable, IServiceType /// public static Dictionary> StatsHistory { get; } = new(); - /// - /// Gets a raw pointer to the instance of Client::Framework. - /// + /// public FrameworkAddressResolver Address { get; } - /// - /// Gets the last time that the Framework Update event was triggered. - /// + /// public DateTime LastUpdate { get; private set; } = DateTime.MinValue; - /// - /// Gets the last time in UTC that the Framework Update event was triggered. - /// + /// public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue; - /// - /// Gets the delta between the last Framework Update and the currently executing one. - /// + /// public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; - /// - /// Gets a value indicating whether currently executing code is running in the game's framework update thread. - /// + /// public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread; - /// - /// Gets a value indicating whether game Framework is unloading. - /// + /// public bool IsFrameworkUnloading { get; internal set; } /// @@ -131,20 +115,11 @@ public sealed class Framework : IDisposable, IServiceType /// internal bool DispatchUpdateEvents { get; set; } = true; - /// - /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. - /// - /// Return type. - /// Function to call. - /// Task representing the pending or already completed function. + /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func); - /// - /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. - /// - /// Function to call. - /// Task representing the pending or already completed function. + /// public Task RunOnFrameworkThread(Action action) { if (this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading) @@ -165,32 +140,15 @@ public sealed class Framework : IDisposable, IServiceType } } - /// - /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. - /// - /// Return type. - /// Function to call. - /// Task representing the pending or already completed function. + /// public Task RunOnFrameworkThread(Func> func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func); - /// - /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. - /// - /// Function to call. - /// Task representing the pending or already completed function. + /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func); - /// - /// Run given function in upcoming Framework.Tick call. - /// - /// Return type. - /// Function to call. - /// Wait for given timespan before calling this function. - /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. - /// Cancellation token which will prevent the execution of this function if wait conditions are not met. - /// Task representing the pending function. + /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) @@ -219,14 +177,7 @@ public sealed class Framework : IDisposable, IServiceType return tcs.Task; } - /// - /// Run given function in upcoming Framework.Tick call. - /// - /// Function to call. - /// Wait for given timespan before calling this function. - /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. - /// Cancellation token which will prevent the execution of this function if wait conditions are not met. - /// Task representing the pending function. + /// public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) @@ -255,15 +206,7 @@ public sealed class Framework : IDisposable, IServiceType return tcs.Task; } - /// - /// Run given function in upcoming Framework.Tick call. - /// - /// Return type. - /// Function to call. - /// Wait for given timespan before calling this function. - /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. - /// Cancellation token which will prevent the execution of this function if wait conditions are not met. - /// Task representing the pending function. + /// public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) @@ -292,14 +235,7 @@ public sealed class Framework : IDisposable, IServiceType return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap(); } - /// - /// Run given function in upcoming Framework.Tick call. - /// - /// Function to call. - /// Wait for given timespan before calling this function. - /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. - /// Cancellation token which will prevent the execution of this function if wait conditions are not met. - /// Task representing the pending function. + /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { if (this.IsFrameworkUnloading) diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs new file mode 100644 index 000000000..69c21bca4 --- /dev/null +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -0,0 +1,126 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Game; + +namespace Dalamud.Plugin.Services; + +/// +/// This class represents the Framework of the native game client and grants access to various subsystems. +/// +public interface IFramework +{ + /// + /// A delegate type used with the event. + /// + /// The Framework instance. + public delegate void OnUpdateDelegate(Framework framework); + + /// + /// Event that gets fired every time the game framework updates. + /// + public event OnUpdateDelegate Update; + + /// + /// Gets a raw pointer to the instance of Client::Framework. + /// + public FrameworkAddressResolver Address { get; } + + /// + /// Gets the last time that the Framework Update event was triggered. + /// + public DateTime LastUpdate { get; } + + /// + /// Gets the last time in UTC that the Framework Update event was triggered. + /// + public DateTime LastUpdateUTC { get; } + + /// + /// Gets the delta between the last Framework Update and the currently executing one. + /// + public TimeSpan UpdateDelta { get; } + + /// + /// Gets a value indicating whether currently executing code is running in the game's framework update thread. + /// + public bool IsInFrameworkUpdateThread { get; } + + /// + /// Gets a value indicating whether game Framework is unloading. + /// + public bool IsFrameworkUnloading { get; } + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func func); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Action action); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func> func); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func func); + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); +} diff --git a/Dalamud/Utility/EventHandlerExtensions.cs b/Dalamud/Utility/EventHandlerExtensions.cs index bce815a7b..eefd245bb 100644 --- a/Dalamud/Utility/EventHandlerExtensions.cs +++ b/Dalamud/Utility/EventHandlerExtensions.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using Dalamud.Game; +using Dalamud.Plugin.Services; using Serilog; using static Dalamud.Game.Framework; @@ -72,12 +73,12 @@ internal static class EventHandlerExtensions /// /// The OnUpdateDelegate in question. /// Framework to be passed on to OnUpdateDelegate. - public static void InvokeSafely(this OnUpdateDelegate updateDelegate, Framework framework) + public static void InvokeSafely(this IFramework.OnUpdateDelegate updateDelegate, Framework framework) { if (updateDelegate == null) return; - foreach (var action in updateDelegate.GetInvocationList().Cast()) + foreach (var action in updateDelegate.GetInvocationList().Cast()) { HandleInvoke(() => action(framework)); } From 275ec72ab769cfcbf85c09d2c693254219a3c8ad Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 10 Sep 2023 14:56:28 -0700 Subject: [PATCH 089/585] Re-Add Lost Import - No idea how, but the import for ImGuiHelpers was lost. --- Dalamud/Interface/Components/ImGuiComponents.IconButton.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs index 05e660b61..1c484d423 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs @@ -1,6 +1,7 @@ using System; using System.Numerics; +using Dalamud.Interface.Utility; using ImGuiNET; namespace Dalamud.Interface.Components; From d378fe1dfcb0255eaa03d0818d7025ce4cbaeb26 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 15:20:44 -0700 Subject: [PATCH 090/585] Add IPartyFinderGui (v9) (#1279) --- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 52 +++++++++++++------ Dalamud/Plugin/Services/IPartyFinderGui.cs | 23 ++++++++ 2 files changed, 59 insertions(+), 16 deletions(-) create mode 100644 Dalamud/Plugin/Services/IPartyFinderGui.cs diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 6427f2a54..85c6a4a39 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -6,6 +6,7 @@ using Dalamud.Game.Gui.PartyFinder.Types; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.Gui.PartyFinder; @@ -13,10 +14,9 @@ namespace Dalamud.Game.Gui.PartyFinder; /// /// This class handles interacting with the native PartyFinder window. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class PartyFinderGui : IDisposable, IServiceType +internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGui { private readonly PartyFinderAddressResolver address; private readonly IntPtr memory; @@ -35,25 +35,14 @@ public sealed class PartyFinderGui : IDisposable, IServiceType this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize); - this.receiveListingHook = Hook.FromAddress(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour)); + this.receiveListingHook = Hook.FromAddress(this.address.ReceiveListing, this.HandleReceiveListingDetour); } - /// - /// Event type fired each time the game receives an individual Party Finder listing. - /// Cannot modify listings but can hide them. - /// - /// The listings received. - /// Additional arguments passed by the game. - public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void ReceiveListingDelegate(IntPtr managerPtr, IntPtr data); - /// - /// Event fired each time the game receives an individual Party Finder listing. - /// Cannot modify listings but can hide them. - /// - public event PartyFinderListingEventDelegate ReceiveListing; + /// + public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing; /// /// Dispose of managed and unmanaged resources. @@ -138,3 +127,34 @@ public sealed class PartyFinderGui : IDisposable, IServiceType } } } + +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFinderGui +{ + [ServiceManager.ServiceDependency] + private readonly PartyFinderGui partyFinderGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal PartyFinderGuiPluginScoped() + { + this.partyFinderGuiService.ReceiveListing += this.ReceiveListingForward; + } + + /// + public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing; + + /// + public void Dispose() + { + this.partyFinderGuiService.ReceiveListing -= this.ReceiveListingForward; + } + + private void ReceiveListingForward(PartyFinderListing listing, PartyFinderListingEventArgs args) => this.ReceiveListing?.Invoke(listing, args); +} diff --git a/Dalamud/Plugin/Services/IPartyFinderGui.cs b/Dalamud/Plugin/Services/IPartyFinderGui.cs new file mode 100644 index 000000000..f656963db --- /dev/null +++ b/Dalamud/Plugin/Services/IPartyFinderGui.cs @@ -0,0 +1,23 @@ +using Dalamud.Game.Gui.PartyFinder.Types; + +namespace Dalamud.Plugin.Services; + +/// +/// This class handles interacting with the native PartyFinder window. +/// +public interface IPartyFinderGui +{ + /// + /// Event type fired each time the game receives an individual Party Finder listing. + /// Cannot modify listings but can hide them. + /// + /// The listings received. + /// Additional arguments passed by the game. + public delegate void PartyFinderListingEventDelegate(PartyFinderListing listing, PartyFinderListingEventArgs args); + + /// + /// Event fired each time the game receives an individual Party Finder listing. + /// Cannot modify listings but can hide them. + /// + public event PartyFinderListingEventDelegate ReceiveListing; +} From 617c2bdb9c752ee067eacbabf5827dc2731b06e5 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 16:18:10 -0700 Subject: [PATCH 091/585] Add IFlyTextGui (v9) (#1278) --- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 92 +++++++++++++------------- Dalamud/Plugin/Services/IFlyTextGui.cs | 55 +++++++++++++++ 2 files changed, 101 insertions(+), 46 deletions(-) create mode 100644 Dalamud/Plugin/Services/IFlyTextGui.cs diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index f2222a7cd..3c04c744a 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -7,6 +7,7 @@ using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Memory; +using Dalamud.Plugin.Services; using Serilog; namespace Dalamud.Game.Gui.FlyText; @@ -14,10 +15,9 @@ namespace Dalamud.Game.Gui.FlyText; /// /// This class facilitates interacting with and creating native in-game "fly text". /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class FlyTextGui : IDisposable, IServiceType +internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui { /// /// The native function responsible for adding fly text to the UI. See . @@ -39,32 +39,6 @@ public sealed class FlyTextGui : IDisposable, IServiceType this.createFlyTextHook = Hook.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour); } - /// - /// The delegate defining the type for the FlyText event. - /// - /// The FlyTextKind. See . - /// Value1 passed to the native flytext function. - /// Value2 passed to the native flytext function. Seems unused. - /// Text1 passed to the native flytext function. - /// Text2 passed to the native flytext function. - /// Color passed to the native flytext function. Changes flytext color. - /// Icon ID passed to the native flytext function. Only displays with select FlyTextKind. - /// Damage Type Icon ID passed to the native flytext function. Displayed next to damage values to denote damage type. - /// The vertical offset to place the flytext at. 0 is default. Negative values result - /// in text appearing higher on the screen. This does not change where the element begins to fade. - /// Whether this flytext has been handled. If a subscriber sets this to true, the FlyText will not appear. - public delegate void OnFlyTextCreatedDelegate( - ref FlyTextKind kind, - ref int val1, - ref int val2, - ref SeString text1, - ref SeString text2, - ref uint color, - ref uint icon, - ref uint damageTypeIcon, - ref float yOffset, - ref bool handled); - /// /// Private delegate for the native CreateFlyText function's hook. /// @@ -95,12 +69,8 @@ public sealed class FlyTextGui : IDisposable, IServiceType uint offsetStrMax, int unknown); - /// - /// The FlyText event that can be subscribed to. - /// - public event OnFlyTextCreatedDelegate? FlyTextCreated; - - private Dalamud Dalamud { get; } + /// + public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated; private FlyTextGuiAddressResolver Address { get; } @@ -112,18 +82,7 @@ public sealed class FlyTextGui : IDisposable, IServiceType this.createFlyTextHook.Dispose(); } - /// - /// Displays a fly text in-game on the local player. - /// - /// The FlyTextKind. See . - /// The index of the actor to place flytext on. Indexing unknown. 1 places flytext on local player. - /// Value1 passed to the native flytext function. - /// Value2 passed to the native flytext function. Seems unused. - /// Text1 passed to the native flytext function. - /// Text2 passed to the native flytext function. - /// Color passed to the native flytext function. Changes flytext color. - /// Icon ID passed to the native flytext function. Only displays with select FlyTextKind. - /// Damage Type Icon ID passed to the native flytext function. Displayed next to damage values to denote damage type. + /// public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon) { // Known valid flytext region within the atk arrays @@ -318,3 +277,44 @@ public sealed class FlyTextGui : IDisposable, IServiceType return retVal; } } + +/// +/// Plugin scoped version of FlyTextGui. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui +{ + [ServiceManager.ServiceDependency] + private readonly FlyTextGui flyTextGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal FlyTextGuiPluginScoped() + { + this.flyTextGuiService.FlyTextCreated += this.FlyTextCreatedForward; + } + + /// + public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated; + + /// + public void Dispose() + { + this.flyTextGuiService.FlyTextCreated -= this.FlyTextCreatedForward; + } + + /// + public void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon) + { + this.flyTextGuiService.AddFlyText(kind, actorIndex, val1, val2, text1, text2, color, icon, damageTypeIcon); + } + + private void FlyTextCreatedForward(ref FlyTextKind kind, ref int val1, ref int val2, ref SeString text1, ref SeString text2, ref uint color, ref uint icon, ref uint damageTypeIcon, ref float yOffset, ref bool handled) + => this.FlyTextCreated?.Invoke(ref kind, ref val1, ref val2, ref text1, ref text2, ref color, ref icon, ref damageTypeIcon, ref yOffset, ref handled); +} diff --git a/Dalamud/Plugin/Services/IFlyTextGui.cs b/Dalamud/Plugin/Services/IFlyTextGui.cs new file mode 100644 index 000000000..04fae351d --- /dev/null +++ b/Dalamud/Plugin/Services/IFlyTextGui.cs @@ -0,0 +1,55 @@ +using Dalamud.Game.Gui.FlyText; +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Plugin.Services; + +/// +/// This class facilitates interacting with and creating native in-game "fly text". +/// +public interface IFlyTextGui +{ + /// + /// The delegate defining the type for the FlyText event. + /// + /// The FlyTextKind. See . + /// Value1 passed to the native flytext function. + /// Value2 passed to the native flytext function. Seems unused. + /// Text1 passed to the native flytext function. + /// Text2 passed to the native flytext function. + /// Color passed to the native flytext function. Changes flytext color. + /// Icon ID passed to the native flytext function. Only displays with select FlyTextKind. + /// Damage Type Icon ID passed to the native flytext function. Displayed next to damage values to denote damage type. + /// The vertical offset to place the flytext at. 0 is default. Negative values result + /// in text appearing higher on the screen. This does not change where the element begins to fade. + /// Whether this flytext has been handled. If a subscriber sets this to true, the FlyText will not appear. + public delegate void OnFlyTextCreatedDelegate( + ref FlyTextKind kind, + ref int val1, + ref int val2, + ref SeString text1, + ref SeString text2, + ref uint color, + ref uint icon, + ref uint damageTypeIcon, + ref float yOffset, + ref bool handled); + + /// + /// The FlyText event that can be subscribed to. + /// + public event OnFlyTextCreatedDelegate? FlyTextCreated; + + /// + /// Displays a fly text in-game on the local player. + /// + /// The FlyTextKind. See . + /// The index of the actor to place flytext on. Indexing unknown. 1 places flytext on local player. + /// Value1 passed to the native flytext function. + /// Value2 passed to the native flytext function. Seems unused. + /// Text1 passed to the native flytext function. + /// Text2 passed to the native flytext function. + /// Color passed to the native flytext function. Changes flytext color. + /// Icon ID passed to the native flytext function. Only displays with select FlyTextKind. + /// Damage Type Icon ID passed to the native flytext function. Displayed next to damage values to denote damage type. + public void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon); +} From 0f3b9eab8cd52d0e42a269f3f4d32caefaca7824 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 16:24:47 -0700 Subject: [PATCH 092/585] Add IToastGui (v9) (#1280) --- Dalamud/Game/Gui/Toast/ToastGui.cs | 183 ++++++++++++++------------- Dalamud/Plugin/Services/IToastGui.cs | 88 +++++++++++++ 2 files changed, 186 insertions(+), 85 deletions(-) create mode 100644 Dalamud/Plugin/Services/IToastGui.cs diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index e65fa1444..93126710b 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Text; @@ -6,16 +5,16 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; namespace Dalamud.Game.Gui.Toast; /// /// This class facilitates interacting with and creating native toast windows. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed partial class ToastGui : IDisposable, IServiceType +internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui { private const uint QuestToastCheckmarkMagic = 60081; @@ -39,38 +38,11 @@ public sealed partial class ToastGui : IDisposable, IServiceType this.address = new ToastGuiAddressResolver(); this.address.Setup(sigScanner); - this.showNormalToastHook = Hook.FromAddress(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour)); - this.showQuestToastHook = Hook.FromAddress(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour)); - this.showErrorToastHook = Hook.FromAddress(this.address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour)); + this.showNormalToastHook = Hook.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour); + this.showQuestToastHook = Hook.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour); + this.showErrorToastHook = Hook.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour); } - #region Event delegates - - /// - /// A delegate type used when a normal toast window appears. - /// - /// The message displayed. - /// Assorted toast options. - /// Whether the toast has been handled or should be propagated. - public delegate void OnNormalToastDelegate(ref SeString message, ref ToastOptions options, ref bool isHandled); - - /// - /// A delegate type used when a quest toast window appears. - /// - /// The message displayed. - /// Assorted toast options. - /// Whether the toast has been handled or should be propagated. - public delegate void OnQuestToastDelegate(ref SeString message, ref QuestToastOptions options, ref bool isHandled); - - /// - /// A delegate type used when an error toast window appears. - /// - /// The message displayed. - /// Whether the toast has been handled or should be propagated. - public delegate void OnErrorToastDelegate(ref SeString message, ref bool isHandled); - - #endregion - #region Marshal delegates private delegate IntPtr ShowNormalToastDelegate(IntPtr manager, IntPtr text, int layer, byte isTop, byte isFast, int logMessageId); @@ -82,21 +54,15 @@ public sealed partial class ToastGui : IDisposable, IServiceType #endregion #region Events + + /// + public event IToastGui.OnNormalToastDelegate? Toast; - /// - /// Event that will be fired when a toast is sent by the game or a plugin. - /// - public event OnNormalToastDelegate Toast; + /// + public event IToastGui.OnQuestToastDelegate? QuestToast; - /// - /// Event that will be fired when a quest toast is sent by the game or a plugin. - /// - public event OnQuestToastDelegate QuestToast; - - /// - /// Event that will be fired when an error toast is sent by the game or a plugin. - /// - public event OnErrorToastDelegate ErrorToast; + /// + public event IToastGui.OnErrorToastDelegate? ErrorToast; #endregion @@ -172,31 +138,23 @@ public sealed partial class ToastGui : IDisposable, IServiceType /// /// Handles normal toasts. /// -public sealed partial class ToastGui +internal sealed partial class ToastGui { - /// - /// Show a toast message with the given content. - /// - /// The message to be shown. - /// Options for the toast. - public void ShowNormal(string message, ToastOptions options = null) + /// + public void ShowNormal(string message, ToastOptions? options = null) { options ??= new ToastOptions(); this.normalQueue.Enqueue((Encoding.UTF8.GetBytes(message), options)); } - - /// - /// Show a toast message with the given content. - /// - /// The message to be shown. - /// Options for the toast. - public void ShowNormal(SeString message, ToastOptions options = null) + + /// + public void ShowNormal(SeString message, ToastOptions? options = null) { options ??= new ToastOptions(); this.normalQueue.Enqueue((message.Encode(), options)); } - private void ShowNormal(byte[] bytes, ToastOptions options = null) + private void ShowNormal(byte[] bytes, ToastOptions? options = null) { options ??= new ToastOptions(); @@ -255,31 +213,23 @@ public sealed partial class ToastGui /// /// Handles quest toasts. /// -public sealed partial class ToastGui +internal sealed partial class ToastGui { - /// - /// Show a quest toast message with the given content. - /// - /// The message to be shown. - /// Options for the toast. - public void ShowQuest(string message, QuestToastOptions options = null) + /// + public void ShowQuest(string message, QuestToastOptions? options = null) { options ??= new QuestToastOptions(); this.questQueue.Enqueue((Encoding.UTF8.GetBytes(message), options)); } - - /// - /// Show a quest toast message with the given content. - /// - /// The message to be shown. - /// Options for the toast. - public void ShowQuest(SeString message, QuestToastOptions options = null) + + /// + public void ShowQuest(SeString message, QuestToastOptions? options = null) { options ??= new QuestToastOptions(); this.questQueue.Enqueue((message.Encode(), options)); } - private void ShowQuest(byte[] bytes, QuestToastOptions options = null) + private void ShowQuest(byte[] bytes, QuestToastOptions? options = null) { options ??= new QuestToastOptions(); @@ -365,21 +315,15 @@ public sealed partial class ToastGui /// /// Handles error toasts. /// -public sealed partial class ToastGui +internal sealed partial class ToastGui { - /// - /// Show an error toast message with the given content. - /// - /// The message to be shown. + /// public void ShowError(string message) { this.errorQueue.Enqueue(Encoding.UTF8.GetBytes(message)); } - /// - /// Show an error toast message with the given content. - /// - /// The message to be shown. + /// public void ShowError(SeString message) { this.errorQueue.Enqueue(message.Encode()); @@ -433,3 +377,72 @@ public sealed partial class ToastGui } } } + +/// +/// Plugin scoped version of ToastGui. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui +{ + [ServiceManager.ServiceDependency] + private readonly ToastGui toastGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal ToastGuiPluginScoped() + { + this.toastGuiService.Toast += this.ToastForward; + this.toastGuiService.QuestToast += this.QuestToastForward; + this.toastGuiService.ErrorToast += this.ErrorToastForward; + } + + /// + public event IToastGui.OnNormalToastDelegate? Toast; + + /// + public event IToastGui.OnQuestToastDelegate? QuestToast; + + /// + public event IToastGui.OnErrorToastDelegate? ErrorToast; + + /// + public void Dispose() + { + this.toastGuiService.Toast -= this.ToastForward; + this.toastGuiService.QuestToast -= this.QuestToastForward; + this.toastGuiService.ErrorToast -= this.ErrorToastForward; + } + + /// + public void ShowNormal(string message, ToastOptions? options = null) => this.toastGuiService.ShowNormal(message, options); + + /// + public void ShowNormal(SeString message, ToastOptions? options = null) => this.toastGuiService.ShowNormal(message, options); + + /// + public void ShowQuest(string message, QuestToastOptions? options = null) => this.toastGuiService.ShowQuest(message, options); + + /// + public void ShowQuest(SeString message, QuestToastOptions? options = null) => this.toastGuiService.ShowQuest(message, options); + + /// + public void ShowError(string message) => this.toastGuiService.ShowError(message); + + /// + public void ShowError(SeString message) => this.toastGuiService.ShowError(message); + + private void ToastForward(ref SeString message, ref ToastOptions options, ref bool isHandled) + => this.Toast?.Invoke(ref message, ref options, ref isHandled); + + private void QuestToastForward(ref SeString message, ref QuestToastOptions options, ref bool isHandled) + => this.QuestToast?.Invoke(ref message, ref options, ref isHandled); + + private void ErrorToastForward(ref SeString message, ref bool isHandled) + => this.ErrorToast?.Invoke(ref message, ref isHandled); +} diff --git a/Dalamud/Plugin/Services/IToastGui.cs b/Dalamud/Plugin/Services/IToastGui.cs new file mode 100644 index 000000000..ef83e95ac --- /dev/null +++ b/Dalamud/Plugin/Services/IToastGui.cs @@ -0,0 +1,88 @@ +using Dalamud.Game.Gui.Toast; +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Plugin.Services; + +/// +/// This class facilitates interacting with and creating native toast windows. +/// +public interface IToastGui +{ + /// + /// A delegate type used when a normal toast window appears. + /// + /// The message displayed. + /// Assorted toast options. + /// Whether the toast has been handled or should be propagated. + public delegate void OnNormalToastDelegate(ref SeString message, ref ToastOptions options, ref bool isHandled); + + /// + /// A delegate type used when a quest toast window appears. + /// + /// The message displayed. + /// Assorted toast options. + /// Whether the toast has been handled or should be propagated. + public delegate void OnQuestToastDelegate(ref SeString message, ref QuestToastOptions options, ref bool isHandled); + + /// + /// A delegate type used when an error toast window appears. + /// + /// The message displayed. + /// Whether the toast has been handled or should be propagated. + public delegate void OnErrorToastDelegate(ref SeString message, ref bool isHandled); + + /// + /// Event that will be fired when a toast is sent by the game or a plugin. + /// + public event OnNormalToastDelegate Toast; + + /// + /// Event that will be fired when a quest toast is sent by the game or a plugin. + /// + public event OnQuestToastDelegate QuestToast; + + /// + /// Event that will be fired when an error toast is sent by the game or a plugin. + /// + public event OnErrorToastDelegate ErrorToast; + + /// + /// Show a toast message with the given content. + /// + /// The message to be shown. + /// Options for the toast. + public void ShowNormal(string message, ToastOptions? options = null); + + /// + /// Show a toast message with the given content. + /// + /// The message to be shown. + /// Options for the toast. + public void ShowNormal(SeString message, ToastOptions? options = null); + + /// + /// Show a quest toast message with the given content. + /// + /// The message to be shown. + /// Options for the toast. + public void ShowQuest(string message, QuestToastOptions? options = null); + + /// + /// Show a quest toast message with the given content. + /// + /// The message to be shown. + /// Options for the toast. + public void ShowQuest(SeString message, QuestToastOptions? options = null); + + /// + /// Show an error toast message with the given content. + /// + /// The message to be shown. + public void ShowError(string message); + + /// + /// Show an error toast message with the given content. + /// + /// The message to be shown. + public void ShowError(SeString message); +} From ca58a1bf4fa4852b6ea77a1f09088e57439c9842 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 19:26:28 -0700 Subject: [PATCH 093/585] Move AddonArgs to it's own file --- Dalamud/Game/AddonLifecycle/AddonArgs.cs | 22 +++++++++++++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 24 +++++++++---------- Dalamud/Plugin/Services/IAddonLifecycle.cs | 20 ---------------- 3 files changed, 34 insertions(+), 32 deletions(-) create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgs.cs diff --git a/Dalamud/Game/AddonLifecycle/AddonArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgs.cs new file mode 100644 index 000000000..50c995abb --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgs.cs @@ -0,0 +1,22 @@ +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonLifecycle; + +/// +/// Addon argument data for use in event subscribers. +/// +public unsafe class AddonArgs +{ + private string? addonName; + + /// + /// Gets the name of the addon this args referrers to. + /// + public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + required public nint Addon { get; init; } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index c0e817f0d..5fc1c7d2b 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -127,7 +127,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRequestedUpdateHook.Enable(); } - private void InvokeListeners(AddonEvent eventType, IAddonLifecycle.AddonArgs args) + private void InvokeListeners(AddonEvent eventType, AddonArgs args) { // Match on string.empty for listeners that want events for all addons. foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) @@ -140,7 +140,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreSetup, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -151,7 +151,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostSetup, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostSetup, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -165,7 +165,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreFinalize, new IAddonLifecycle.AddonArgs { Addon = (nint)atkUnitBase[0] }); + this.InvokeListeners(AddonEvent.PreFinalize, new AddonArgs { Addon = (nint)atkUnitBase[0] }); } catch (Exception e) { @@ -179,7 +179,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreDraw, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -190,7 +190,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostDraw, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostDraw, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -202,7 +202,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreUpdate, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -213,7 +213,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostUpdate, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -225,7 +225,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreRefresh, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreRefresh, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -236,7 +236,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostRefresh, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostRefresh, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -248,7 +248,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreRequestedUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -259,7 +259,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostRequestedUpdate, new IAddonLifecycle.AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonArgs { Addon = (nint)addon }); } catch (Exception e) { diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index cbb3d7c24..1dc792660 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -2,8 +2,6 @@ using System.Runtime.InteropServices; using Dalamud.Game.AddonLifecycle; -using Dalamud.Memory; -using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Plugin.Services; @@ -79,22 +77,4 @@ public interface IAddonLifecycle /// /// Handlers to remove. void UnregisterListener(params AddonEventDelegate[] handlers); - - /// - /// Addon argument data for use in event subscribers. - /// - public unsafe class AddonArgs - { - private string? addonName; - - /// - /// Gets the name of the addon this args referrers to. - /// - public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); - - /// - /// Gets the pointer to the addons AtkUnitBase. - /// - required public nint Addon { get; init; } - } } From 9c0ca2769c774c02a0d510ba36d953e609d93888 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 10 Sep 2023 20:01:49 -0700 Subject: [PATCH 094/585] Fix Aging Steps --- .../SelfTest/AgingSteps/AddonLifecycleAgingStep.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs index 9dcaec558..3a1cb0e77 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Dalamud.Game.AddonLifecycle; -using Dalamud.Plugin.Services; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; @@ -101,32 +100,32 @@ internal class AddonLifecycleAgingStep : IAgingStep } } - private void PostSetup(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostSetup(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterSetup) this.currentStep++; } - private void PostUpdate(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostUpdate(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterUpdate) this.currentStep++; } - private void PostDraw(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostDraw(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterDraw) this.currentStep++; } - private void PostRefresh(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostRefresh(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterRefresh) this.currentStep++; } - private void PostRequestedUpdate(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PostRequestedUpdate(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterRequestedUpdate) this.currentStep++; } - private void PreFinalize(AddonEvent eventType, IAddonLifecycle.AddonArgs addonInfo) + private void PreFinalize(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterFinalize) this.currentStep++; } From 4870428bac6c90bd498e11796f53c358967df551 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Mon, 11 Sep 2023 21:32:56 -0700 Subject: [PATCH 095/585] Bump Lumina to 3.11.0 --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 2 +- Dalamud/Dalamud.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 25919af07..3938d0c80 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,7 +27,7 @@ - + diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index b147dc961..98b515642 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -67,7 +67,7 @@ - + From 130a57f850660992c12ca426cc0510f0b7f253f8 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 12 Sep 2023 07:02:32 +0200 Subject: [PATCH 096/585] Update ClientStructs (#1371) 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 ada62e7ae..7279a8f3c 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit ada62e7ae60de220d1f950b03ddb8d66e9e10daf +Subproject commit 7279a8f3ca6b79490184b05532af509781a89415 From af632175647debf7d469ba2f709ffe1e186e0e10 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:06:40 -0700 Subject: [PATCH 097/585] Add ITitleScreenMenu and Scoped Service. (#1379) --- .../Internal/Windows/TitleScreenMenuWindow.cs | 3 +- .../{ => TitleScreenMenu}/TitleScreenMenu.cs | 168 ++++++------------ .../TitleScreenMenu/TitleScreenMenuEntry.cs | 94 ++++++++++ Dalamud/Plugin/Services/ITitleScreenMenu.cs | 44 +++++ 4 files changed, 195 insertions(+), 114 deletions(-) rename Dalamud/Interface/{ => TitleScreenMenu}/TitleScreenMenu.cs (50%) create mode 100644 Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs create mode 100644 Dalamud/Plugin/Services/ITitleScreenMenu.cs diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index e92c80846..e3cf78296 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -230,7 +229,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable } private bool DrawEntry( - TitleScreenMenu.TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) + TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) { InterfaceManager.SpecialGlyphRequest fontHandle; if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) diff --git a/Dalamud/Interface/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs similarity index 50% rename from Dalamud/Interface/TitleScreenMenu.cs rename to Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs index c9e1458d6..3123ffbb8 100644 --- a/Dalamud/Interface/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Reflection; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using ImGuiScene; namespace Dalamud.Interface; @@ -12,10 +12,9 @@ namespace Dalamud.Interface; /// /// Class responsible for managing elements in the title screen menu. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public class TitleScreenMenu : IServiceType +internal class TitleScreenMenu : IServiceType, ITitleScreenMenu { /// /// Gets the texture size needed for title screen menu logos. @@ -29,19 +28,10 @@ public class TitleScreenMenu : IServiceType { } - /// - /// Gets the list of entries in the title screen menu. - /// + /// public IReadOnlyList Entries => this.entries; - /// - /// Adds a new entry to the title screen menu. - /// - /// The text to show. - /// The texture to show. - /// The action to execute when the option is selected. - /// A object that can be used to manage the entry. - /// Thrown when the texture provided does not match the required resolution(64x64). + /// public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered) { if (texture.Height != TextureSize || texture.Width != TextureSize) @@ -64,15 +54,7 @@ public class TitleScreenMenu : IServiceType } } - /// - /// Adds a new entry to the title screen menu. - /// - /// Priority of the entry. - /// The text to show. - /// The texture to show. - /// The action to execute when the option is selected. - /// A object that can be used to manage the entry. - /// Thrown when the texture provided does not match the required resolution(64x64). + /// public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered) { if (texture.Height != TextureSize || texture.Width != TextureSize) @@ -91,10 +73,7 @@ public class TitleScreenMenu : IServiceType } } - /// - /// Remove an entry from the title screen menu. - /// - /// The entry to remove. + /// public void RemoveEntry(TitleScreenMenuEntry entry) { lock (this.entries) @@ -159,93 +138,58 @@ public class TitleScreenMenu : IServiceType return entry; } } +} - /// - /// Class representing an entry in the title screen menu. - /// - public class TitleScreenMenuEntry : IComparable +/// +/// Plugin-scoped version of a TitleScreenMenu service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleScreenMenu +{ + [ServiceManager.ServiceDependency] + private readonly TitleScreenMenu titleScreenMenuService = Service.Get(); + + private readonly List pluginEntries = new(); + + /// + public IReadOnlyList? Entries => this.titleScreenMenuService.Entries; + + /// + public void Dispose() { - private readonly Action onTriggered; - - /// - /// Initializes a new instance of the class. - /// - /// The calling assembly. - /// The priority of this entry. - /// The text to show. - /// The texture to show. - /// The action to execute when the option is selected. - internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, TextureWrap texture, Action onTriggered) + foreach (var entry in this.pluginEntries) { - this.CallingAssembly = callingAssembly; - this.Priority = priority; - this.Name = text; - this.Texture = texture; - this.onTriggered = onTriggered; - } - - /// - /// Gets the priority of this entry. - /// - public ulong Priority { get; init; } - - /// - /// Gets or sets the name of this entry. - /// - public string Name { get; set; } - - /// - /// Gets or sets the texture of this entry. - /// - public TextureWrap Texture { get; set; } - - /// - /// Gets or sets a value indicating whether or not this entry is internal. - /// - internal bool IsInternal { get; set; } - - /// - /// Gets the calling assembly of this entry. - /// - internal Assembly? CallingAssembly { get; init; } - - /// - /// Gets the internal ID of this entry. - /// - internal Guid Id { get; init; } = Guid.NewGuid(); - - /// - public int CompareTo(TitleScreenMenuEntry? other) - { - if (other == null) - return 1; - if (this.CallingAssembly != other.CallingAssembly) - { - if (this.CallingAssembly == null && other.CallingAssembly == null) - return 0; - if (this.CallingAssembly == null && other.CallingAssembly != null) - return -1; - if (this.CallingAssembly != null && other.CallingAssembly == null) - return 1; - return string.Compare( - this.CallingAssembly!.FullName!, - other.CallingAssembly!.FullName!, - StringComparison.CurrentCultureIgnoreCase); - } - - if (this.Priority != other.Priority) - return this.Priority.CompareTo(other.Priority); - if (this.Name != other.Name) - return string.Compare(this.Name, other.Name, StringComparison.InvariantCultureIgnoreCase); - return string.Compare(this.Name, other.Name, StringComparison.InvariantCulture); - } - - /// - /// Trigger the action associated with this entry. - /// - internal void Trigger() - { - this.onTriggered(); + this.titleScreenMenuService.RemoveEntry(entry); } } + + /// + public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered) + { + var entry = this.titleScreenMenuService.AddEntry(text, texture, onTriggered); + this.pluginEntries.Add(entry); + + return entry; + } + + /// + public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered) + { + var entry = this.titleScreenMenuService.AddEntry(priority, text, texture, onTriggered); + this.pluginEntries.Add(entry); + + return entry; + } + + /// + public void RemoveEntry(TitleScreenMenuEntry entry) + { + this.pluginEntries.Remove(entry); + this.titleScreenMenuService.RemoveEntry(entry); + } } diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs new file mode 100644 index 000000000..18acc4f47 --- /dev/null +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs @@ -0,0 +1,94 @@ +using System.Reflection; + +using ImGuiScene; + +namespace Dalamud.Interface; + +/// +/// Class representing an entry in the title screen menu. +/// +public class TitleScreenMenuEntry : IComparable +{ + private readonly Action onTriggered; + + /// + /// Initializes a new instance of the class. + /// + /// The calling assembly. + /// The priority of this entry. + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, TextureWrap texture, Action onTriggered) + { + this.CallingAssembly = callingAssembly; + this.Priority = priority; + this.Name = text; + this.Texture = texture; + this.onTriggered = onTriggered; + } + + /// + /// Gets the priority of this entry. + /// + public ulong Priority { get; init; } + + /// + /// Gets or sets the name of this entry. + /// + public string Name { get; set; } + + /// + /// Gets or sets the texture of this entry. + /// + public TextureWrap Texture { get; set; } + + /// + /// Gets or sets a value indicating whether or not this entry is internal. + /// + internal bool IsInternal { get; set; } + + /// + /// Gets the calling assembly of this entry. + /// + internal Assembly? CallingAssembly { get; init; } + + /// + /// Gets the internal ID of this entry. + /// + internal Guid Id { get; init; } = Guid.NewGuid(); + + /// + public int CompareTo(TitleScreenMenuEntry? other) + { + if (other == null) + return 1; + if (this.CallingAssembly != other.CallingAssembly) + { + if (this.CallingAssembly == null && other.CallingAssembly == null) + return 0; + if (this.CallingAssembly == null && other.CallingAssembly != null) + return -1; + if (this.CallingAssembly != null && other.CallingAssembly == null) + return 1; + return string.Compare( + this.CallingAssembly!.FullName!, + other.CallingAssembly!.FullName!, + StringComparison.CurrentCultureIgnoreCase); + } + + if (this.Priority != other.Priority) + return this.Priority.CompareTo(other.Priority); + if (this.Name != other.Name) + return string.Compare(this.Name, other.Name, StringComparison.InvariantCultureIgnoreCase); + return 0; + } + + /// + /// Trigger the action associated with this entry. + /// + internal void Trigger() + { + this.onTriggered(); + } +} diff --git a/Dalamud/Plugin/Services/ITitleScreenMenu.cs b/Dalamud/Plugin/Services/ITitleScreenMenu.cs new file mode 100644 index 000000000..2094dc435 --- /dev/null +++ b/Dalamud/Plugin/Services/ITitleScreenMenu.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +using Dalamud.Interface; +using ImGuiScene; + +namespace Dalamud.Plugin.Services; + +/// +/// Interface for class responsible for managing elements in the title screen menu. +/// +public interface ITitleScreenMenu +{ + /// + /// Gets the list of entries in the title screen menu. + /// + public IReadOnlyList Entries { get; } + + /// + /// Adds a new entry to the title screen menu. + /// + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + /// A object that can be used to manage the entry. + /// Thrown when the texture provided does not match the required resolution(64x64). + public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered); + + /// + /// Adds a new entry to the title screen menu. + /// + /// Priority of the entry. + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + /// A object that can be used to manage the entry. + /// Thrown when the texture provided does not match the required resolution(64x64). + public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered); + + /// + /// Remove an entry from the title screen menu. + /// + /// The entry to remove. + public void RemoveEntry(TitleScreenMenuEntry entry); +} From 181ec6b95623b6cf15c3b7063785c7976065bc4a Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:07:46 -0700 Subject: [PATCH 098/585] Add GameNetworkPluginScoped (#1380) --- Dalamud/Game/Network/GameNetwork.cs | 52 +++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 2b6630c8b..f56fd3996 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; @@ -14,13 +13,9 @@ namespace Dalamud.Game.Network; /// /// This class handles interacting with game network events. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork +internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork { private readonly GameNetworkAddressResolver address; private readonly Hook processZonePacketDownHook; @@ -57,14 +52,10 @@ public sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate byte ProcessZonePacketUpDelegate(IntPtr a1, IntPtr dataPtr, IntPtr a3, byte a4); - /// - /// Event that is called when a network message is sent/received. - /// - public event IGameNetwork.OnNetworkMessageDelegate NetworkMessage; + /// + public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; - /// - /// Dispose of managed and unmanaged resources. - /// + /// void IDisposable.Dispose() { this.processZonePacketDownHook.Dispose(); @@ -148,3 +139,38 @@ public sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork return this.processZonePacketUpHook.Original(a1, dataPtr, a3, a4); } } + +/// +/// Plugin-scoped version of a AddonLifecycle service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork +{ + [ServiceManager.ServiceDependency] + private readonly GameNetwork gameNetworkService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal GameNetworkPluginScoped() + { + this.gameNetworkService.NetworkMessage += this.NetworkMessageForward; + } + + /// + public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; + + /// + public void Dispose() + { + this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward; + } + + private void NetworkMessageForward(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) + => this.NetworkMessage?.Invoke(dataPtr, opCode, sourceActorId, targetActorId, direction); +} From abf7c243e428166bcfd9b9dc71b28a3d24412c32 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:09:01 -0700 Subject: [PATCH 099/585] Add GameGuiPluginScoped (#1381) --- Dalamud/Game/Gui/GameGui.cs | 143 +++++++++++++++++++++++++++++------- 1 file changed, 117 insertions(+), 26 deletions(-) diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 3954954a3..078c624e8 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -1,13 +1,12 @@ -using System; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; -using Dalamud.Interface; using Dalamud.Interface.Utility; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel; @@ -16,7 +15,6 @@ using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; -using Serilog; using SharpDX; using Vector2 = System.Numerics.Vector2; @@ -27,14 +25,12 @@ namespace Dalamud.Game.Gui; /// /// A class handling many aspects of the in-game UI. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui +internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui { + private static readonly ModuleLog Log = new("GameGui"); + private readonly GameGuiAddressResolver address; private readonly GetMatrixSingletonDelegate getMatrixSingleton; @@ -48,8 +44,8 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui private readonly Hook toggleUiHideHook; private readonly Hook utf8StringFromSequenceHook; - private GetUIMapObjectDelegate getUIMapObject; - private OpenMapWithFlagDelegate openMapWithFlag; + private GetUIMapObjectDelegate? getUIMapObject; + private OpenMapWithFlagDelegate? openMapWithFlag; [ServiceManager.ServiceConstructor] private GameGui(SigScanner sigScanner) @@ -116,16 +112,16 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui private delegate char HandleImmDelegate(IntPtr framework, char a2, byte a3); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, byte unknownByte); + private delegate IntPtr ToggleUiHideDelegate(IntPtr thisPtr, bool uiVisible); /// - public event EventHandler UiHideToggled; + public event EventHandler? UiHideToggled; /// - public event EventHandler HoveredItemChanged; + public event EventHandler? HoveredItemChanged; /// - public event EventHandler HoveredActionChanged; + public event EventHandler? HoveredActionChanged; /// public bool GameUiHidden { get; private set; } @@ -147,7 +143,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui return false; } - this.getUIMapObject = this.address.GetVirtualFunction(uiModule, 0, 8); + this.getUIMapObject ??= this.address.GetVirtualFunction(uiModule, 0, 8); var uiMapObjectPtr = this.getUIMapObject(uiModule); @@ -157,7 +153,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui return false; } - this.openMapWithFlag = this.address.GetVirtualFunction(uiMapObjectPtr, 0, 63); + this.openMapWithFlag ??= this.address.GetVirtualFunction(uiMapObjectPtr, 0, 63); var mapLinkString = mapLink.DataString; @@ -217,14 +213,13 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui // Read current ViewProjectionMatrix plus game window size var viewProjectionMatrix = default(Matrix); - float width, height; var rawMatrix = (float*)(matrixSingleton + 0x1b4).ToPointer(); for (var i = 0; i < 16; i++, rawMatrix++) viewProjectionMatrix[i] = *rawMatrix; - width = *rawMatrix; - height = *(rawMatrix + 1); + var width = *rawMatrix; + var height = *(rawMatrix + 1); viewProjectionMatrix.Invert(); @@ -414,7 +409,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui this.HoveredItemChanged?.InvokeSafely(this, itemId); - Log.Verbose("HoverItemId:{0} this:{1}", itemId, hoverState.ToInt64().ToString("X")); + Log.Verbose($"HoverItemId:{itemId} this:{hoverState.ToInt64()}"); } return retVal; @@ -456,7 +451,7 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui this.HoveredAction.ActionID = (uint)Marshal.ReadInt32(hoverState, 0x3C); this.HoveredActionChanged?.InvokeSafely(this, this.HoveredAction); - Log.Verbose("HoverActionId: {0}/{1} this:{2}", actionKind, actionId, hoverState.ToInt64().ToString("X")); + Log.Verbose($"HoverActionId: {actionKind}/{actionId} this:{hoverState.ToInt64():X}"); } private IntPtr HandleActionOutDetour(IntPtr agentActionDetail, IntPtr a2, IntPtr a3, int a4) @@ -489,16 +484,15 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui return retVal; } - private IntPtr ToggleUiHideDetour(IntPtr thisPtr, byte unknownByte) + private IntPtr ToggleUiHideDetour(IntPtr thisPtr, bool uiVisible) { - // TODO(goat): We should read this from memory directly, instead of relying on catching every toggle. - this.GameUiHidden = !this.GameUiHidden; + this.GameUiHidden = !RaptureAtkModule.Instance()->IsUiVisible; this.UiHideToggled?.InvokeSafely(this, this.GameUiHidden); Log.Debug("UiHide toggled: {0}", this.GameUiHidden); - return this.toggleUiHideHook.Original(thisPtr, unknownByte); + return this.toggleUiHideHook.Original(thisPtr, uiVisible); } private char HandleImmDetour(IntPtr framework, char a2, byte a3) @@ -514,8 +508,105 @@ public sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui if (sourcePtr != null) this.utf8StringFromSequenceHook.Original(thisPtr, sourcePtr, sourceLen); else - thisPtr->Ctor(); // this is in clientstructs but you could do it manually too + thisPtr->Ctor(); // this is in ClientStructs but you could do it manually too return thisPtr; // this function shouldn't need to return but the original asm moves this into rax before returning so be safe? } } + +/// +/// Plugin-scoped version of a AddonLifecycle service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui +{ + [ServiceManager.ServiceDependency] + private readonly GameGui gameGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal GameGuiPluginScoped() + { + this.gameGuiService.UiHideToggled += this.UiHideToggledForward; + this.gameGuiService.HoveredItemChanged += this.HoveredItemForward; + this.gameGuiService.HoveredActionChanged += this.HoveredActionForward; + } + + /// + public event EventHandler? UiHideToggled; + + /// + public event EventHandler? HoveredItemChanged; + + /// + public event EventHandler? HoveredActionChanged; + + /// + public bool GameUiHidden => this.gameGuiService.GameUiHidden; + + /// + public ulong HoveredItem + { + get => this.gameGuiService.HoveredItem; + set => this.gameGuiService.HoveredItem = value; + } + + /// + public HoveredAction HoveredAction => this.gameGuiService.HoveredAction; + + /// + public void Dispose() + { + this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; + this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; + this.gameGuiService.HoveredActionChanged -= this.HoveredActionForward; + } + + /// + public bool OpenMapWithMapLink(MapLinkPayload mapLink) + => this.gameGuiService.OpenMapWithMapLink(mapLink); + + /// + public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos) + => this.gameGuiService.WorldToScreen(worldPos, out screenPos); + + /// + public bool WorldToScreen(Vector3 worldPos, out Vector2 screenPos, out bool inView) + => this.gameGuiService.WorldToScreen(worldPos, out screenPos, out inView); + + /// + public bool ScreenToWorld(Vector2 screenPos, out Vector3 worldPos, float rayDistance = 100000) + => this.gameGuiService.ScreenToWorld(screenPos, out worldPos, rayDistance); + + /// + public IntPtr GetUIModule() + => this.gameGuiService.GetUIModule(); + + /// + public IntPtr GetAddonByName(string name, int index = 1) + => this.gameGuiService.GetAddonByName(name, index); + + /// + public IntPtr FindAgentInterface(string addonName) + => this.gameGuiService.FindAgentInterface(addonName); + + /// + public unsafe IntPtr FindAgentInterface(void* addon) + => this.gameGuiService.FindAgentInterface(addon); + + /// + public IntPtr FindAgentInterface(IntPtr addonPtr) + => this.gameGuiService.FindAgentInterface(addonPtr); + + private void UiHideToggledForward(object sender, bool toggled) => this.UiHideToggled?.Invoke(sender, toggled); + + private void HoveredItemForward(object sender, ulong itemId) => this.HoveredItemChanged?.Invoke(sender, itemId); + + private void HoveredActionForward(object sender, HoveredAction hoverAction) => this.HoveredActionChanged?.Invoke(sender, hoverAction); +} From d96175ed16920f36e5851df736016657f11a9cda Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:10:36 -0700 Subject: [PATCH 100/585] Add DutyStatePluginScoped (#1382) --- Dalamud/Game/DutyState/DutyState.cs | 95 ++++++++++++++----- .../DutyState/DutyStateAddressResolver.cs | 2 - Dalamud/Plugin/Services/IDutyState.cs | 4 +- 3 files changed, 74 insertions(+), 27 deletions(-) diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 49fc874e3..2f117a492 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -1,25 +1,19 @@ -using System; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; -using Dalamud.Utility; namespace Dalamud.Game.DutyState; /// /// This class represents the state of the currently occupied duty. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public unsafe class DutyState : IDisposable, IServiceType, IDutyState +internal unsafe class DutyState : IDisposable, IServiceType, IDutyState { private readonly DutyStateAddressResolver address; private readonly Hook contentDirectorNetworkMessageHook; @@ -49,16 +43,16 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState private delegate byte SetupContentDirectNetworkMessageDelegate(IntPtr a1, IntPtr a2, ushort* a3); /// - public event EventHandler DutyStarted; + public event EventHandler? DutyStarted; /// - public event EventHandler DutyWiped; + public event EventHandler? DutyWiped; /// - public event EventHandler DutyRecommenced; + public event EventHandler? DutyRecommenced; /// - public event EventHandler DutyCompleted; + public event EventHandler? DutyCompleted; /// public bool IsDutyStarted { get; private set; } @@ -66,7 +60,7 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState private bool CompletedThisTerritory { get; set; } /// - void IDisposable.Dispose() + public void Dispose() { this.contentDirectorNetworkMessageHook.Dispose(); this.framework.Update -= this.FrameworkOnUpdateEvent; @@ -92,33 +86,33 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState // Duty Commenced case 0x4000_0001: this.IsDutyStarted = true; - this.DutyStarted.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyStarted?.Invoke(this, this.clientState.TerritoryType); break; // Party Wipe case 0x4000_0005: this.IsDutyStarted = false; - this.DutyWiped.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyWiped?.Invoke(this, this.clientState.TerritoryType); break; // Duty Recommence case 0x4000_0006: this.IsDutyStarted = true; - this.DutyRecommenced.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyRecommenced?.Invoke(this, this.clientState.TerritoryType); break; // Duty Completed Flytext Shown case 0x4000_0002 when !this.CompletedThisTerritory: this.IsDutyStarted = false; this.CompletedThisTerritory = true; - this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType); break; // Duty Completed case 0x4000_0003 when !this.CompletedThisTerritory: this.IsDutyStarted = false; this.CompletedThisTerritory = true; - this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType); + this.DutyCompleted?.Invoke(this, this.clientState.TerritoryType); break; } } @@ -161,11 +155,68 @@ public unsafe class DutyState : IDisposable, IServiceType, IDutyState } private bool IsBoundByDuty() + => this.condition.Any(ConditionFlag.BoundByDuty, + ConditionFlag.BoundByDuty56, + ConditionFlag.BoundByDuty95); + + private bool IsInCombat() + => this.condition.Any(ConditionFlag.InCombat); +} + +/// +/// Plugin scoped version of DutyState. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState +{ + [ServiceManager.ServiceDependency] + private readonly DutyState dutyStateService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal DutyStatePluginScoped() { - return this.condition[ConditionFlag.BoundByDuty] || - this.condition[ConditionFlag.BoundByDuty56] || - this.condition[ConditionFlag.BoundByDuty95]; + this.dutyStateService.DutyStarted += this.DutyStartedForward; + this.dutyStateService.DutyWiped += this.DutyWipedForward; + this.dutyStateService.DutyRecommenced += this.DutyRecommencedForward; + this.dutyStateService.DutyCompleted += this.DutyCompletedForward; } - private bool IsInCombat() => this.condition[ConditionFlag.InCombat]; + /// + public event EventHandler? DutyStarted; + + /// + public event EventHandler? DutyWiped; + + /// + public event EventHandler? DutyRecommenced; + + /// + public event EventHandler? DutyCompleted; + + /// + public bool IsDutyStarted => this.dutyStateService.IsDutyStarted; + + /// + public void Dispose() + { + this.dutyStateService.DutyStarted -= this.DutyStartedForward; + this.dutyStateService.DutyWiped -= this.DutyWipedForward; + this.dutyStateService.DutyRecommenced -= this.DutyRecommencedForward; + this.dutyStateService.DutyCompleted -= this.DutyCompletedForward; + } + + private void DutyStartedForward(object sender, ushort territoryId) => this.DutyStarted?.Invoke(sender, territoryId); + + private void DutyWipedForward(object sender, ushort territoryId) => this.DutyWiped?.Invoke(sender, territoryId); + + private void DutyRecommencedForward(object sender, ushort territoryId) => this.DutyRecommenced?.Invoke(sender, territoryId); + + private void DutyCompletedForward(object sender, ushort territoryId) => this.DutyCompleted?.Invoke(sender, territoryId); } diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs index 801e5ef55..436883dc2 100644 --- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs +++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs @@ -1,5 +1,3 @@ -using System; - namespace Dalamud.Game.DutyState; /// diff --git a/Dalamud/Plugin/Services/IDutyState.cs b/Dalamud/Plugin/Services/IDutyState.cs index a2331364c..3d49f68cb 100644 --- a/Dalamud/Plugin/Services/IDutyState.cs +++ b/Dalamud/Plugin/Services/IDutyState.cs @@ -1,6 +1,4 @@ -using System; - -namespace Dalamud.Plugin.Services; +namespace Dalamud.Plugin.Services; /// /// This class represents the state of the currently occupied duty. From e3d688141c21daaa3d8b8f04bb7140edf83b956c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:20:05 -0700 Subject: [PATCH 101/585] Add ConditionPluginScoped (#1385) --- .../Game/ClientState/Conditions/Condition.cs | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index b72c91c74..585b762bf 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -10,13 +10,9 @@ namespace Dalamud.Game.ClientState.Conditions; /// /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public sealed partial class Condition : IServiceType, ICondition +internal sealed partial class Condition : IServiceType, 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. @@ -122,7 +118,7 @@ public sealed partial class Condition : IServiceType, ICondition /// /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. /// -public sealed partial class Condition : IDisposable +internal sealed partial class Condition : IDisposable { private bool isDisposed; @@ -156,3 +152,52 @@ public sealed partial class Condition : IDisposable this.isDisposed = true; } } + +/// +/// Plugin-scoped version of a Condition service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition +{ + [ServiceManager.ServiceDependency] + private readonly Condition conditionService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal ConditionPluginScoped() + { + this.conditionService.ConditionChange += this.ConditionChangedForward; + } + + /// + public event ICondition.ConditionChangeDelegate? ConditionChange; + + /// + public int MaxEntries => this.conditionService.MaxEntries; + + /// + public IntPtr Address => this.conditionService.Address; + + /// + public bool this[int flag] => this.conditionService[flag]; + + /// + public void Dispose() + { + this.conditionService.ConditionChange -= this.ConditionChangedForward; + } + + /// + public bool Any() => this.conditionService.Any(); + + /// + public bool Any(params ConditionFlag[] flags) => this.conditionService.Any(flags); + + private void ConditionChangedForward(ConditionFlag flag, bool value) => this.ConditionChange?.Invoke(flag, value); +} From 3e613cffd0b032f68d8bcca4564731bc29eea691 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 12 Sep 2023 07:20:20 +0200 Subject: [PATCH 102/585] Update ClientStructs (#1371) (#1387) 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 ada62e7ae..7279a8f3c 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit ada62e7ae60de220d1f950b03ddb8d66e9e10daf +Subproject commit 7279a8f3ca6b79490184b05532af509781a89415 From 5ad464f2a83ff2126d906154b8cf6861efad74ae Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:53:00 +0200 Subject: [PATCH 103/585] Update ClientStructs (#1388) 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 7279a8f3c..a593cb163 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7279a8f3ca6b79490184b05532af509781a89415 +Subproject commit a593cb163e1c5d33b27d34df4d1ccc57d1e67643 From fd518f8e3f6123835733e6714f5840eae108bacf Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sun, 20 Aug 2023 16:14:42 -0700 Subject: [PATCH 104/585] fix: Don't check for Wine Registry anymore - Renames `IsLinux` to `IsWine` to better reflect that this will return true for macOS as well. - Fixes a bug caused by misbehaving apps wanting to be helpful to Linux users - Also makes Wine checking far more resilient in cases where XL_WINEONLINUX isn't set. --- Dalamud/Utility/Util.cs | 42 ++++++++++++++++++++--------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 5f2e4d5bf..63fc9faee 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -8,6 +8,7 @@ using System.Net.Http; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Text; using Dalamud.Configuration.Internal; @@ -22,7 +23,6 @@ using Dalamud.Logging.Internal; using Dalamud.Networking.Http; using ImGuiNET; using Lumina.Excel.GeneratedSheets; -using Microsoft.Win32; using Serilog; namespace Dalamud.Utility; @@ -491,32 +491,30 @@ public static class Util } /// - /// Heuristically determine if Dalamud is running on Linux/WINE. + /// Determine if Dalamud is currently running within a Wine context (e.g. either on macOS or Linux). This method + /// will not return information about the host operating system. /// - /// Whether or not Dalamud is running on Linux/WINE. - public static bool IsLinux() + /// Returns true if Wine is detected, false otherwise. + public static bool IsWine() { - bool Check1() - { - return EnvironmentConfiguration.XlWineOnLinux; - } + if (EnvironmentConfiguration.XlWineOnLinux) return true; - bool Check2() - { - var hModule = NativeFunctions.GetModuleHandleW("ntdll.dll"); - var proc1 = NativeFunctions.GetProcAddress(hModule, "wine_get_version"); - var proc2 = NativeFunctions.GetProcAddress(hModule, "wine_get_build_id"); + var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); - return proc1 != IntPtr.Zero || proc2 != IntPtr.Zero; - } + // Test to see if any Wine specific exports exist. If they do, then we are running on Wine. + // The exports "wine_get_version", "wine_get_build_id", and "wine_get_host_version" will tend to be hidden + // by most Linux users (else FFXIV will want a macOS license), so we will additionally check some lesser-known + // exports as well. + return AnyProcExists( + ntdll, + "wine_get_version", + "wine_get_build_id", + "wine_get_host_version", + "wine_server_call", + "wine_unix_to_nt_file_name"); - bool Check3() - { - return Registry.CurrentUser.OpenSubKey(@"Software\Wine") != null || - Registry.LocalMachine.OpenSubKey(@"Software\Wine") != null; - } - - return Check1() || Check2() || Check3(); + bool AnyProcExists(nint handle, params string[] procs) => + procs.Any(p => NativeFunctions.GetProcAddress(handle, p) != nint.Zero); } /// From 2acba2b81f93cdd6dd766c9140389c9ec12ded08 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 12 Sep 2023 17:06:22 -0700 Subject: [PATCH 105/585] Update Dalamud.Boot to use extra import checks - Ports extra import checks to Dalamud Boot - Renames is_running_on_linux to is_running_on_wine - Update relevant logging string for VEH --- Dalamud.Boot/dllmain.cpp | 4 ++-- Dalamud.Boot/utils.cpp | 6 +++++- Dalamud.Boot/utils.h | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 9a741a47f..94f1c7d0f 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -133,8 +133,8 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { // ============================== VEH ======================================== // logging::I("Initializing VEH..."); - if (utils::is_running_on_linux()) { - logging::I("=> VEH was disabled, running on linux"); + if (utils::is_running_on_wine()) { + logging::I("=> VEH was disabled, running on wine"); } else if (g_startInfo.BootVehEnabled) { if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory)) logging::I("=> Done!"); diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index 79205eb8d..b45795045 100644 --- a/Dalamud.Boot/utils.cpp +++ b/Dalamud.Boot/utils.cpp @@ -578,7 +578,7 @@ std::vector utils::get_env_list(const wchar_t* pcszName) { return res; } -bool utils::is_running_on_linux() { +bool utils::is_running_on_wine() { if (get_env(L"XL_WINEONLINUX")) return true; HMODULE hntdll = GetModuleHandleW(L"ntdll.dll"); @@ -588,6 +588,10 @@ bool utils::is_running_on_linux() { return true; if (GetProcAddress(hntdll, "wine_get_host_version")) return true; + if (GetProcAddress(hntdll, "wine_server_call")) + return true; + if (GetProcAddress(hntdll, "wine_unix_to_nt_file_name")) + return true; return false; } diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h index 5d5c90dde..5e3caa4d6 100644 --- a/Dalamud.Boot/utils.h +++ b/Dalamud.Boot/utils.h @@ -264,7 +264,7 @@ namespace utils { return get_env_list(unicode::convert(pcszName).c_str()); } - bool is_running_on_linux(); + bool is_running_on_wine(); std::filesystem::path get_module_path(HMODULE hModule); From 0fca2c80c30457f9634e1d12be437ae51dc02b49 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 12 Sep 2023 18:14:54 -0700 Subject: [PATCH 106/585] Fix a build error, oops --- Dalamud/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 1975505a8..7ad794e42 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -166,7 +166,7 @@ public sealed class EntryPoint // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12 | SecurityProtocolType.Tls; - if (!Util.IsLinux()) + if (!Util.IsWine()) InitSymbolHandler(info); var dalamud = new Dalamud(info, configuration, mainThreadContinueEvent); From a7aacb15e4603a367e2f980578271a9a639d8852 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 12 Sep 2023 18:22:20 -0700 Subject: [PATCH 107/585] Add XL_PLATFORM env var, Util.GetHostPlatform() - New env var XL_PLATFORM allows launcher to inform Dalamud of the current platform (one of Windows, macOS, or Linux). - New method Util.GetHostPlatform() provides a best guess for the current platform. This method will respect XL_PLATFORM if set, but will otherwise resort to heuristic checks. --- Dalamud/Utility/Util.cs | 68 +++++++++++++++++++++++++++++++++++------ 1 file changed, 58 insertions(+), 10 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 63fc9faee..5be4e4fc4 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -1,10 +1,8 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; -using System.Net.Http; using System.Numerics; using System.Reflection; using System.Runtime.CompilerServices; @@ -16,11 +14,10 @@ using Dalamud.Data; using Dalamud.Game; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; -using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; -using Dalamud.Networking.Http; +using Dalamud.Memory; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Serilog; @@ -38,7 +35,7 @@ public static class Util private static ulong moduleStartAddr; private static ulong moduleEndAddr; - + /// /// Gets the assembly version of Dalamud. /// @@ -498,6 +495,7 @@ public static class Util public static bool IsWine() { if (EnvironmentConfiguration.XlWineOnLinux) return true; + if (Environment.GetEnvironmentVariable("XL_PLATFORM") is not null and not "Windows") return true; var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); @@ -506,17 +504,46 @@ public static class Util // by most Linux users (else FFXIV will want a macOS license), so we will additionally check some lesser-known // exports as well. return AnyProcExists( - ntdll, - "wine_get_version", - "wine_get_build_id", + ntdll, + "wine_get_version", + "wine_get_build_id", "wine_get_host_version", - "wine_server_call", + "wine_server_call", "wine_unix_to_nt_file_name"); bool AnyProcExists(nint handle, params string[] procs) => procs.Any(p => NativeFunctions.GetProcAddress(handle, p) != nint.Zero); } + /// + /// Gets the best guess for the current host's platform based on the XL_PLATFORM environment variable or + /// heuristics. + /// + /// + /// This method is not perfectly reliable if XL_PLATFORM is unset. For example, macOS users running with + /// exports hidden will be marked as Linux users. Better heuristic checks for macOS are needed in order to fix this. + /// + /// Returns the that Dalamud is currently running on. + public static OSPlatform GetHostPlatform() + { + switch (Environment.GetEnvironmentVariable("XL_PLATFORM")) + { + case "Windows": return OSPlatform.Windows; + case "MacOS": return OSPlatform.OSX; + case "Linux": return OSPlatform.Linux; + } + + if (IsWine()) + { + GetWineHostVersion(out var platform, out _); + if (platform == "Darwin") return OSPlatform.OSX; // only happens on macOS without export hides (mac license) + + return OSPlatform.Linux; + } + + return OSPlatform.Windows; + } + /// /// Heuristically determine if the Windows version is higher than Windows 11's first build. /// @@ -683,7 +710,7 @@ public static class Util ImGui.SetClipboardText(actor.Address.ToInt64().ToString("X")); } } - + private static unsafe void ShowValue(ulong addr, IEnumerable path, Type type, object value) { if (type.IsPointer) @@ -738,4 +765,25 @@ public static class Util } } } + + private static unsafe bool GetWineHostVersion(out string? platform, out string? version) + { + platform = null; + version = null; + + var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); + var methodPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version"); + + if (methodPtr == nint.Zero) return false; + + var methodDelegate = (delegate* unmanaged[Fastcall])methodPtr; + methodDelegate(out var platformPtr, out var versionPtr); + + if (platformPtr == null) return false; + + platform = MemoryHelper.ReadStringNullTerminated((nint)platformPtr); + if (versionPtr != null) version = MemoryHelper.ReadStringNullTerminated((nint)versionPtr); + + return true; + } } From 8911d4ebc2e2134661360de3c2e0e26b6e202c0c Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Tue, 12 Sep 2023 18:33:41 -0700 Subject: [PATCH 108/585] Re-add SourceContext property check Technically an API breakage, some plugins are passing this for logging. Slated for removal in API 9, however. --- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 3303a2280..0febc0fc4 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -504,14 +504,16 @@ internal class ConsoleWindow : Window, IDisposable HasException = logEvent.Exception != null, }; + // TODO (v9): Remove SourceContext property check. if (logEvent.Properties.ContainsKey("Dalamud.ModuleName")) { entry.Source = "DalamudInternal"; } - else if (logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) && - sourceProp is ScalarValue { Value: string value }) + else if ((logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) || + logEvent.Properties.TryGetValue("SourceContext", out sourceProp)) && + sourceProp is ScalarValue { Value: string sourceValue }) { - entry.Source = value; + entry.Source = sourceValue; } this.logText.Add(entry); From 088cf8c3922022d5fb576f05b876d7263032226f Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 13 Sep 2023 00:23:17 -0700 Subject: [PATCH 109/585] Remove fancy Darwin checking code --- Dalamud/Utility/Util.cs | 38 +++++++------------------------------- 1 file changed, 7 insertions(+), 31 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 5be4e4fc4..78edda3dd 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -520,8 +520,8 @@ public static class Util /// heuristics. /// /// - /// This method is not perfectly reliable if XL_PLATFORM is unset. For example, macOS users running with - /// exports hidden will be marked as Linux users. Better heuristic checks for macOS are needed in order to fix this. + /// macOS users running without XL_PLATFORM being set will be reported as Linux users. Due to the way our + /// Wines work, there isn't a great (consistent) way to split the two apart if we're not told. /// /// Returns the that Dalamud is currently running on. public static OSPlatform GetHostPlatform() @@ -532,16 +532,13 @@ public static class Util case "MacOS": return OSPlatform.OSX; case "Linux": return OSPlatform.Linux; } - - if (IsWine()) - { - GetWineHostVersion(out var platform, out _); - if (platform == "Darwin") return OSPlatform.OSX; // only happens on macOS without export hides (mac license) - return OSPlatform.Linux; - } + // n.b. we had some fancy code here to check if the Wine host version returned "Darwin" but apparently + // *all* our Wines report Darwin if exports aren't hidden. As such, it is effectively impossible (without some + // (very cursed and inaccurate heuristics) to determine if we're on macOS or Linux unless we're explicitly told + // by our launcher. See commit a7aacb15e4603a367e2f980578271a9a639d8852 for the old check. - return OSPlatform.Windows; + return IsWine() ? OSPlatform.Linux : OSPlatform.Windows; } /// @@ -765,25 +762,4 @@ public static class Util } } } - - private static unsafe bool GetWineHostVersion(out string? platform, out string? version) - { - platform = null; - version = null; - - var ntdll = NativeFunctions.GetModuleHandleW("ntdll.dll"); - var methodPtr = NativeFunctions.GetProcAddress(ntdll, "wine_get_host_version"); - - if (methodPtr == nint.Zero) return false; - - var methodDelegate = (delegate* unmanaged[Fastcall])methodPtr; - methodDelegate(out var platformPtr, out var versionPtr); - - if (platformPtr == null) return false; - - platform = MemoryHelper.ReadStringNullTerminated((nint)platformPtr); - if (versionPtr != null) version = MemoryHelper.ReadStringNullTerminated((nint)versionPtr); - - return true; - } } From 6eba1415f546e38d5fab31d8f71a71dc379bf254 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 24 Jun 2023 22:18:41 -0700 Subject: [PATCH 110/585] Add IChatGui --- Dalamud/Game/Gui/ChatGui.cs | 112 ++++++-------------------- Dalamud/Plugin/Services/IChatGui.cs | 119 ++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 89 deletions(-) create mode 100644 Dalamud/Plugin/Services/IChatGui.cs diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 93185caf9..d8154b41d 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -22,7 +23,10 @@ namespace Dalamud.Game.Gui; [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public sealed class ChatGui : IDisposable, IServiceType +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +public sealed class ChatGui : IDisposable, IServiceType, IChatGui { private readonly ChatGuiAddressResolver address; @@ -51,45 +55,7 @@ public sealed class ChatGui : IDisposable, IServiceType this.populateItemLinkHook = Hook.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); this.interactableLinkClickedHook = Hook.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); } - - /// - /// A delegate type used with the event. - /// - /// The type of chat. - /// The sender ID. - /// The sender name. - /// The message sent. - /// A value indicating whether the message was handled or should be propagated. - public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); - - /// - /// A delegate type used with the event. - /// - /// The type of chat. - /// The sender ID. - /// The sender name. - /// The message sent. - /// A value indicating whether the message was handled or should be propagated. - public delegate void OnCheckMessageHandledDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); - - /// - /// A delegate type used with the event. - /// - /// The type of chat. - /// The sender ID. - /// The sender name. - /// The message sent. - public delegate void OnMessageHandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message); - - /// - /// A delegate type used with the event. - /// - /// The type of chat. - /// The sender ID. - /// The sender name. - /// The message sent. - public delegate void OnMessageUnhandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message); - + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr PrintMessageDelegate(IntPtr manager, XivChatType chatType, IntPtr senderName, IntPtr message, uint senderId, IntPtr parameter); @@ -99,34 +65,22 @@ public sealed class ChatGui : IDisposable, IServiceType [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr); - /// - /// Event that will be fired when a chat message is sent to chat by the game. - /// - public event OnMessageDelegate ChatMessage; + /// + public event IChatGui.OnMessageDelegate ChatMessage; - /// - /// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true. - /// - public event OnCheckMessageHandledDelegate CheckMessageHandled; + /// + public event IChatGui.OnCheckMessageHandledDelegate CheckMessageHandled; - /// - /// Event that will be fired when a chat message is handled by Dalamud or a Plugin. - /// - public event OnMessageHandledDelegate ChatMessageHandled; + /// + public event IChatGui.OnMessageHandledDelegate ChatMessageHandled; - /// - /// Event that will be fired when a chat message is not handled by Dalamud or a Plugin. - /// - public event OnMessageUnhandledDelegate ChatMessageUnhandled; + /// + public event IChatGui.OnMessageUnhandledDelegate ChatMessageUnhandled; - /// - /// Gets the ID of the last linked item. - /// + /// public int LastLinkedItemId { get; private set; } - /// - /// Gets the flags of the last linked item. - /// + /// public byte LastLinkedItemFlags { get; private set; } /// @@ -139,21 +93,13 @@ public sealed class ChatGui : IDisposable, IServiceType this.interactableLinkClickedHook.Dispose(); } - /// - /// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void PrintChat(XivChatEntry chat) { this.chatQueue.Enqueue(chat); } - /// - /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void Print(string message) { // Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message); @@ -164,11 +110,7 @@ public sealed class ChatGui : IDisposable, IServiceType }); } - /// - /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void Print(SeString message) { // Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue); @@ -179,11 +121,7 @@ public sealed class ChatGui : IDisposable, IServiceType }); } - /// - /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to - /// the queue, later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void PrintError(string message) { // Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message); @@ -194,11 +132,7 @@ public sealed class ChatGui : IDisposable, IServiceType }); } - /// - /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to - /// the queue, later to be processed when UpdateQueue() is called. - /// - /// A message to send. + /// public void PrintError(SeString message) { // Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue); @@ -330,7 +264,7 @@ public sealed class ChatGui : IDisposable, IServiceType { try { - var messageHandledDelegate = @delegate as OnCheckMessageHandledDelegate; + var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate; messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) @@ -346,7 +280,7 @@ public sealed class ChatGui : IDisposable, IServiceType { try { - var messageHandledDelegate = @delegate as OnMessageDelegate; + var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate; messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs new file mode 100644 index 000000000..1d71bb886 --- /dev/null +++ b/Dalamud/Plugin/Services/IChatGui.cs @@ -0,0 +1,119 @@ +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Plugin.Services; + +/// +/// This class handles interacting with the native chat UI. +/// +public interface IChatGui +{ + /// + /// A delegate type used with the event. + /// + /// The type of chat. + /// The sender ID. + /// The sender name. + /// The message sent. + /// A value indicating whether the message was handled or should be propagated. + public delegate void OnMessageDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); + + /// + /// A delegate type used with the event. + /// + /// The type of chat. + /// The sender ID. + /// The sender name. + /// The message sent. + /// A value indicating whether the message was handled or should be propagated. + public delegate void OnCheckMessageHandledDelegate(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled); + + /// + /// A delegate type used with the event. + /// + /// The type of chat. + /// The sender ID. + /// The sender name. + /// The message sent. + public delegate void OnMessageHandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message); + + /// + /// A delegate type used with the event. + /// + /// The type of chat. + /// The sender ID. + /// The sender name. + /// The message sent. + public delegate void OnMessageUnhandledDelegate(XivChatType type, uint senderId, SeString sender, SeString message); + + /// + /// Event that will be fired when a chat message is sent to chat by the game. + /// + public event OnMessageDelegate ChatMessage; + + /// + /// Event that allows you to stop messages from appearing in chat by setting the isHandled parameter to true. + /// + public event OnCheckMessageHandledDelegate CheckMessageHandled; + + /// + /// Event that will be fired when a chat message is handled by Dalamud or a Plugin. + /// + public event OnMessageHandledDelegate ChatMessageHandled; + + /// + /// Event that will be fired when a chat message is not handled by Dalamud or a Plugin. + /// + public event OnMessageUnhandledDelegate ChatMessageUnhandled; + + /// + /// Gets the ID of the last linked item. + /// + public int LastLinkedItemId { get; } + + /// + /// Gets the flags of the last linked item. + /// + public byte LastLinkedItemFlags { get; } + + /// + /// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue, + /// later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void PrintChat(XivChatEntry chat); + + /// + /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, + /// later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void Print(string message); + + /// + /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, + /// later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void Print(SeString message); + + /// + /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to + /// the queue, later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void PrintError(string message); + + /// + /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to + /// the queue, later to be processed when UpdateQueue() is called. + /// + /// A message to send. + public void PrintError(SeString message); + + /// + /// Process a chat queue. + /// + public void UpdateQueue(); +} From 7080087e9db6469b7ff824fab28d6e7cca6822a4 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 13 Sep 2023 19:06:28 -0700 Subject: [PATCH 111/585] Scope ChatGui --- Dalamud/Game/ChatHandlers.cs | 4 +- Dalamud/Game/Gui/ChatGui.cs | 224 +++++++++++++++++------ Dalamud/Plugin/Internal/PluginManager.cs | 4 +- Dalamud/Plugin/Services/IChatGui.cs | 38 ++-- 4 files changed, 186 insertions(+), 84 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index ed69b7bbe..1d82e5f9c 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -264,7 +264,7 @@ public class ChatHandlers : IServiceType if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion)) { - chatGui.PrintChat(new XivChatEntry + chatGui.Print(new XivChatEntry { Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."), Type = XivChatType.Notice, @@ -321,7 +321,7 @@ public class ChatHandlers : IServiceType } else { - chatGui.PrintChat(new XivChatEntry + chatGui.Print(new XivChatEntry { Message = new SeString(new List() { diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index d8154b41d..2fbeb404e 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -20,13 +20,9 @@ namespace Dalamud.Game.Gui; /// /// This class handles interacting with the native chat UI. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -public sealed class ChatGui : IDisposable, IServiceType, IChatGui +internal sealed class ChatGui : IDisposable, IServiceType, IChatGui { private readonly ChatGuiAddressResolver address; @@ -66,16 +62,16 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui private delegate void InteractableLinkClickedDelegate(IntPtr managerPtr, IntPtr messagePtr); /// - public event IChatGui.OnMessageDelegate ChatMessage; + public event IChatGui.OnMessageDelegate? ChatMessage; /// - public event IChatGui.OnCheckMessageHandledDelegate CheckMessageHandled; + public event IChatGui.OnCheckMessageHandledDelegate? CheckMessageHandled; /// - public event IChatGui.OnMessageHandledDelegate ChatMessageHandled; + public event IChatGui.OnMessageHandledDelegate? ChatMessageHandled; /// - public event IChatGui.OnMessageUnhandledDelegate ChatMessageUnhandled; + public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; /// public int LastLinkedItemId { get; private set; } @@ -94,55 +90,35 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui } /// - public void PrintChat(XivChatEntry chat) + public void Print(XivChatEntry chat) { this.chatQueue.Enqueue(chat); } - + /// - public void Print(string message) + public void Print(string message, string? messageTag = null, ushort? tagColor = null) { - // Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message); - this.PrintChat(new XivChatEntry - { - Message = message, - Type = this.configuration.GeneralChatType, - }); + this.PrintTagged(message, this.configuration.GeneralChatType, messageTag, tagColor); } - + /// - public void Print(SeString message) + public void Print(SeString message, string? messageTag = null, ushort? tagColor = null) { - // Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue); - this.PrintChat(new XivChatEntry - { - Message = message, - Type = this.configuration.GeneralChatType, - }); + this.PrintTagged(message, this.configuration.GeneralChatType, messageTag, tagColor); } - + /// - public void PrintError(string message) + public void PrintError(string message, string? messageTag = null, ushort? tagColor = null) { - // Log.Verbose("[CHATGUI PRINT REGULAR ERROR]{0}", message); - this.PrintChat(new XivChatEntry - { - Message = message, - Type = XivChatType.Urgent, - }); + this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor); } - + /// - public void PrintError(SeString message) + public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null) { - // Log.Verbose("[CHATGUI PRINT SESTRING ERROR]{0}", message.TextValue); - this.PrintChat(new XivChatEntry - { - Message = message, - Type = XivChatType.Urgent, - }); + this.PrintTagged(message, XivChatType.Urgent, messageTag, tagColor); } - + /// /// Process a chat queue. /// @@ -176,7 +152,7 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui /// A payload for handling. internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action commandAction) { - var payload = new DalamudLinkPayload() { Plugin = pluginName, CommandId = commandId }; + var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId }; this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction); return payload; } @@ -200,20 +176,63 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui /// The ID of the command to be removed. internal void RemoveChatLinkHandler(string pluginName, uint commandId) { - if (this.dalamudLinkHandlers.ContainsKey((pluginName, commandId))) - { - this.dalamudLinkHandlers.Remove((pluginName, commandId)); - } + this.dalamudLinkHandlers.Remove((pluginName, commandId)); } [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(GameGui gameGui, LibcFunction libcFunction) + private void ContinueConstruction() { this.printMessageHook.Enable(); this.populateItemLinkHook.Enable(); this.interactableLinkClickedHook.Enable(); } + private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color) + { + var builder = new SeStringBuilder(); + + if (!tag.IsNullOrEmpty()) + { + if (color is not null) + { + builder.AddUiForeground($"[{tag}] ", color.Value); + } + else + { + builder.AddText($"[{tag}] "); + } + } + + this.Print(new XivChatEntry + { + Message = builder.AddText(message).Build(), + Type = channel, + }); + } + + private void PrintTagged(SeString message, XivChatType channel, string? tag, ushort? color) + { + var builder = new SeStringBuilder(); + + if (!tag.IsNullOrEmpty()) + { + if (color is not null) + { + builder.AddUiForeground($"[{tag}] ", color.Value); + } + else + { + builder.AddText($"[{tag}] "); + } + } + + this.Print(new XivChatEntry + { + Message = builder.Build().Append(message), + Type = channel, + }); + } + private void HandlePopulateItemLinkDetour(IntPtr linkObjectPtr, IntPtr itemInfoPtr) { try @@ -232,7 +251,7 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui } } - private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chattype, IntPtr pSenderName, IntPtr pMessage, uint senderid, IntPtr parameter) + private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chatType, IntPtr pSenderName, IntPtr pMessage, uint senderId, IntPtr parameter) { var retVal = IntPtr.Zero; @@ -259,13 +278,13 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui // Call events var isHandled = false; - var invocationList = this.CheckMessageHandled.GetInvocationList(); + var invocationList = this.CheckMessageHandled!.GetInvocationList(); foreach (var @delegate in invocationList) { try { var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate; - messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); + messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) { @@ -275,13 +294,13 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui if (!isHandled) { - invocationList = this.ChatMessage.GetInvocationList(); + invocationList = this.ChatMessage!.GetInvocationList(); foreach (var @delegate in invocationList) { try { var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate; - messageHandledDelegate!.Invoke(chattype, senderid, ref parsedSender, ref parsedMessage, ref isHandled); + messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) { @@ -324,12 +343,12 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui // Print the original chat if it's handled. if (isHandled) { - this.ChatMessageHandled?.Invoke(chattype, senderid, parsedSender, parsedMessage); + this.ChatMessageHandled?.Invoke(chatType, senderId, parsedSender, parsedMessage); } else { - retVal = this.printMessageHook.Original(manager, chattype, senderPtr, messagePtr, senderid, parameter); - this.ChatMessageUnhandled?.Invoke(chattype, senderid, parsedSender, parsedMessage); + retVal = this.printMessageHook.Original(manager, chatType, senderPtr, messagePtr, senderId, parameter); + this.ChatMessageUnhandled?.Invoke(chatType, senderId, parsedSender, parsedMessage); } if (this.baseAddress == IntPtr.Zero) @@ -341,7 +360,7 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui catch (Exception ex) { Log.Error(ex, "Exception on OnChatMessage hook."); - retVal = this.printMessageHook.Original(manager, chattype, pSenderName, pMessage, senderid, parameter); + retVal = this.printMessageHook.Original(manager, chatType, pSenderName, pMessage, senderId, parameter); } return retVal; @@ -373,10 +392,10 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui var linkPayload = payloads[0]; if (linkPayload is DalamudLinkPayload link) { - if (this.dalamudLinkHandlers.ContainsKey((link.Plugin, link.CommandId))) + if (this.dalamudLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value)) { Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}"); - this.dalamudLinkHandlers[(link.Plugin, link.CommandId)].Invoke(link.CommandId, new SeString(payloads)); + value.Invoke(link.CommandId, new SeString(payloads)); } else { @@ -390,3 +409,88 @@ public sealed class ChatGui : IDisposable, IServiceType, IChatGui } } } + +/// +/// Plugin scoped version of ChatGui. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui +{ + [ServiceManager.ServiceDependency] + private readonly ChatGui chatGuiService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal ChatGuiPluginScoped() + { + this.chatGuiService.ChatMessage += this.OnMessageForward; + this.chatGuiService.CheckMessageHandled += this.OnCheckMessageForward; + this.chatGuiService.ChatMessageHandled += this.OnMessageHandledForward; + this.chatGuiService.ChatMessageUnhandled += this.OnMessageUnhandledForward; + } + + /// + public event IChatGui.OnMessageDelegate? ChatMessage; + + /// + public event IChatGui.OnCheckMessageHandledDelegate? CheckMessageHandled; + + /// + public event IChatGui.OnMessageHandledDelegate? ChatMessageHandled; + + /// + public event IChatGui.OnMessageUnhandledDelegate? ChatMessageUnhandled; + + /// + public int LastLinkedItemId => this.chatGuiService.LastLinkedItemId; + + /// + public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags; + + /// + public void Dispose() + { + this.chatGuiService.ChatMessage -= this.OnMessageForward; + this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward; + this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward; + this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward; + } + + /// + public void Print(XivChatEntry chat) + => this.chatGuiService.Print(chat); + + /// + public void Print(string message, string? messageTag = null, ushort? tagColor = null) + => this.chatGuiService.Print(message, messageTag, tagColor); + + /// + public void Print(SeString message, string? messageTag = null, ushort? tagColor = null) + => this.chatGuiService.Print(message, messageTag, tagColor); + + /// + public void PrintError(string message, string? messageTag = null, ushort? tagColor = null) + => this.chatGuiService.PrintError(message, messageTag, tagColor); + + /// + public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null) + => this.chatGuiService.PrintError(message, messageTag, tagColor); + + private void OnMessageForward(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) + => this.ChatMessage?.Invoke(type, senderId, ref sender, ref message, ref isHandled); + + private void OnCheckMessageForward(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) + => this.CheckMessageHandled?.Invoke(type, senderId, ref sender, ref message, ref isHandled); + + private void OnMessageHandledForward(XivChatType type, uint senderId, SeString sender, SeString message) + => this.ChatMessageHandled?.Invoke(type, senderId, sender, message); + + private void OnMessageUnhandledForward(XivChatType type, uint senderId, SeString sender, SeString message) + => this.ChatMessageUnhandled?.Invoke(type, senderId, sender, message); +} diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index b160038d7..691d5f729 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -285,7 +285,7 @@ internal partial class PluginManager : IDisposable, IServiceType if (updateMetadata is { Count: > 0 }) { - chatGui.PrintChat(new XivChatEntry + chatGui.Print(new XivChatEntry { Message = new SeString(new List() { @@ -308,7 +308,7 @@ internal partial class PluginManager : IDisposable, IServiceType } else { - chatGui.PrintChat(new XivChatEntry + chatGui.Print(new XivChatEntry { Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version), Type = XivChatType.Urgent, diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs index 1d71bb886..bafdabbb5 100644 --- a/Dalamud/Plugin/Services/IChatGui.cs +++ b/Dalamud/Plugin/Services/IChatGui.cs @@ -78,42 +78,40 @@ public interface IChatGui public byte LastLinkedItemFlags { get; } /// - /// Queue a chat message. While method is named as PrintChat, it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void PrintChat(XivChatEntry chat); + public void Print(XivChatEntry chat); /// - /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void Print(string message); + /// String to prepend message with "[messageTag] ". + /// Color to display the message tag with. + public void Print(string message, string? messageTag = null, ushort? tagColor = null); /// - /// Queue a chat message. While method is named as PrintChat (it calls it internally), it only add a entry to the queue, - /// later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void Print(SeString message); + /// String to prepend message with "[messageTag] ". + /// Color to display the message tag with. + public void Print(SeString message, string? messageTag = null, ushort? tagColor = null); /// - /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to - /// the queue, later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void PrintError(string message); + /// String to prepend message with "[messageTag] ". + /// Color to display the message tag with. + public void PrintError(string message, string? messageTag = null, ushort? tagColor = null); /// - /// Queue an error chat message. While method is named as PrintChat (it calls it internally), it only add a entry to - /// the queue, later to be processed when UpdateQueue() is called. + /// Queue a chat message. Dalamud will send queued messages on the next framework event. /// /// A message to send. - public void PrintError(SeString message); - - /// - /// Process a chat queue. - /// - public void UpdateQueue(); + /// String to prepend message with "[messageTag] ". + /// Color to display the message tag with. + public void PrintError(SeString message, string? messageTag = null, ushort? tagColor = null); } From 947cd79bcae588ed30c0ea36b62b5e61ecace971 Mon Sep 17 00:00:00 2001 From: kal <35899782+kalilistic@users.noreply.github.com> Date: Sat, 16 Sep 2023 20:54:29 -0400 Subject: [PATCH 112/585] chore: remove remaining obsolete icons --- .../Interface/FontAwesome/FontAwesomeIcon.cs | 62 +++---------------- 1 file changed, 10 insertions(+), 52 deletions(-) diff --git a/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs b/Dalamud/Interface/FontAwesome/FontAwesomeIcon.cs index 3d21ea86c..f88d7f8f0 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.3.0 +// Font-Awesome Version: 6.4.2 // //------------------------------------------------------------------------------ @@ -19,12 +19,6 @@ public enum FontAwesomeIcon /// None = 0, - /// - /// The Font Awesome "acquisitionsincorporated" icon unicode character. - /// - [Obsolete] - AcquisitionsIncorporated = 0xF6AF, - /// /// The Font Awesome "rectangle-ad" icon unicode character. /// @@ -43,7 +37,7 @@ public enum FontAwesomeIcon /// The Font Awesome "address-card" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "address card", "about", "contact", "id", "identification", "postcard", "profile", "registration" })] - [FontAwesomeCategoriesAttribute(new[] { "Business", "Communication", "Users + People" })] + [FontAwesomeCategoriesAttribute(new[] { "Accessibility", "Alphabet", "Business", "Communication", "Users + People" })] AddressCard = 0xF2BB, /// @@ -53,12 +47,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Charts + Diagrams", "Design", "Editing", "Photos + Images", "Shapes" })] Adjust = 0xF042, - /// - /// The Font Awesome "adobe" icon unicode character. - /// - [Obsolete] - Adobe = 0xF778, - /// /// The Font Awesome "spray-can-sparkles" icon unicode character. /// @@ -884,7 +872,7 @@ public enum FontAwesomeIcon /// The Font Awesome "binoculars" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "binoculars", "glasses", "magnify", "scenic", "spyglass", "view" })] - [FontAwesomeCategoriesAttribute(new[] { "Camping", "Maps", "Nature" })] + [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Camping", "Maps", "Nature" })] Binoculars = 0xF1E5, /// @@ -1359,7 +1347,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "bullseye" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "bullseye", "archery", "goal", "objective", "target" })] + [FontAwesomeSearchTerms(new[] { "bullseye", "archery", "goal", "objective", "strategy", "target" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Marketing", "Toggle" })] Bullseye = 0xF140, @@ -2202,7 +2190,7 @@ public enum FontAwesomeIcon /// The Font Awesome "gear" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "cog", "cogwheel", "gear", "mechanical", "settings", "sprocket", "tool", "wheel" })] - [FontAwesomeCategoriesAttribute(new[] { "Spinners" })] + [FontAwesomeCategoriesAttribute(new[] { "Coding", "Editing", "Spinners" })] Cog = 0xF013, /// @@ -3423,14 +3411,14 @@ public enum FontAwesomeIcon /// /// The Font Awesome "flask" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "flask", "beaker", "experimental", "labs", "science" })] + [FontAwesomeSearchTerms(new[] { "flask", "beaker", "chemicals", "experiment", "experimental", "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", "ampule", "chemistry", "lab", "laboratory", "test", "test tube" })] + [FontAwesomeSearchTerms(new[] { "flask vial", " beaker", " chemicals", " experiment", " experimental", " labs", " liquid", " science", " vial", "ampule", "chemistry", "lab", "laboratory", "potion", "test", "test tube" })] [FontAwesomeCategoriesAttribute(new[] { "Humanitarian", "Medical + Health", "Science" })] FlaskVial = 0xE4F3, @@ -5088,7 +5076,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "lightbulb" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "lightbulb", "bulb", "comic", "electric", "energy", "idea", "inspiration", "light", "light bulb" })] + [FontAwesomeSearchTerms(new[] { "lightbulb", " comic", " electric", " idea", " innovation", " inspiration", " light", " light bulb", " bulb", "bulb", "comic", "electric", "energy", "idea", "inspiration", "mechanical" })] [FontAwesomeCategoriesAttribute(new[] { "Energy", "Household", "Maps", "Marketing" })] Lightbulb = 0xF0EB, @@ -5270,7 +5258,7 @@ public enum FontAwesomeIcon /// /// The Font Awesome "magnifying-glass-chart" icon unicode character. /// - [FontAwesomeSearchTerms(new[] { "magnifying glass chart", "analysis", "chart" })] + [FontAwesomeSearchTerms(new[] { "magnifying glass chart", " data", " graph", " intelligence", "analysis", "chart", "market" })] [FontAwesomeCategoriesAttribute(new[] { "Business", "Humanitarian", "Marketing" })] MagnifyingGlassChart = 0xE522, @@ -5484,12 +5472,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Astronomy", "Weather" })] Meteor = 0xF753, - /// - /// The Font Awesome "microblog" icon unicode character. - /// - [Obsolete] - Microblog = 0xF91A, - /// /// The Font Awesome "microchip" icon unicode character. /// @@ -5676,7 +5658,7 @@ public enum FontAwesomeIcon /// The Font Awesome "monument" icon unicode character. /// [FontAwesomeSearchTerms(new[] { "monument", "building", "historic", "landmark", "memorable" })] - [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Travel + Hotel" })] + [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Maps", "Travel + Hotel" })] Monument = 0xF5A6, /// @@ -6043,12 +6025,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Business", "Design", "Editing" })] PenNib = 0xF5AD, - /// - /// The Font Awesome "pennyarcade" icon unicode character. - /// - [Obsolete] - PennyArcade = 0xF704, - /// /// The Font Awesome "square-pen" icon unicode character. /// @@ -6415,12 +6391,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Files", "Film + Video", "Photos + Images", "Social" })] PhotoVideo = 0xF87C, - /// - /// The Font Awesome "piedpipersquare" icon unicode character. - /// - [Obsolete] - PiedPiperSquare = 0xF91E, - /// /// The Font Awesome "piggy-bank" icon unicode character. /// @@ -8720,12 +8690,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Buildings", "Humanitarian", "Travel + Hotel" })] TreeCity = 0xE587, - /// - /// The Font Awesome "tripadvisor" icon unicode character. - /// - [Obsolete] - Tripadvisor = 0xF262, - /// /// The Font Awesome "trophy" icon unicode character. /// @@ -8887,12 +8851,6 @@ public enum FontAwesomeIcon [FontAwesomeCategoriesAttribute(new[] { "Arrows", "Media Playback" })] UndoAlt = 0xF2EA, - /// - /// The Font Awesome "unity" icon unicode character. - /// - [Obsolete] - Unity = 0xF949, - /// /// The Font Awesome "universal-access" icon unicode character. /// From 85bb5229d9c4b8bc84d1bc1fbd85b574ba59cc25 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 16 Sep 2023 18:00:08 -0700 Subject: [PATCH 113/585] Update ClientStructs - Fix build errors caused by obsoletes --- Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs | 4 +--- .../Game/ClientState/Objects/SubKinds/PlayerCharacter.cs | 4 +--- Dalamud/Game/ClientState/Objects/Types/Character.cs | 7 +++---- lib/FFXIVClientStructs | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs b/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs index 59f32e33d..add7a7f9f 100644 --- a/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs +++ b/Dalamud/Game/ClientState/Objects/SubKinds/BattleNpc.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Game.ClientState.Objects.Enums; namespace Dalamud.Game.ClientState.Objects.Types; @@ -25,5 +23,5 @@ public unsafe class BattleNpc : BattleChara public BattleNpcSubKind BattleNpcKind => (BattleNpcSubKind)this.Struct->Character.GameObject.SubKind; /// - public override ulong TargetObjectId => this.Struct->Character.TargetObjectID; + public override ulong TargetObjectId => this.Struct->Character.TargetId; } diff --git a/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs b/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs index 7fc9c0079..9de11e3ec 100644 --- a/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs +++ b/Dalamud/Game/ClientState/Objects/SubKinds/PlayerCharacter.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Resolvers; @@ -33,5 +31,5 @@ public unsafe class PlayerCharacter : BattleChara /// /// Gets the target actor ID of the PlayerCharacter. /// - public override ulong TargetObjectId => this.Struct->Character.PlayerTargetObjectID; + public override ulong TargetObjectId => this.Struct->Character.LookTargetId; } diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index ee8418362..a1eb52edc 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Resolvers; using Dalamud.Game.Text.SeStringHandling; @@ -87,7 +85,7 @@ public unsafe class Character : GameObject /// /// Gets the target object ID of the character. /// - public override ulong TargetObjectId => this.Struct->TargetObjectID; + public override ulong TargetObjectId => this.Struct->TargetId; /// /// Gets the name ID of the character. @@ -115,5 +113,6 @@ public unsafe class Character : GameObject /// /// Gets the underlying structure. /// - protected internal new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address; + protected internal new FFXIVClientStructs.FFXIV.Client.Game.Character.Character* Struct => + (FFXIVClientStructs.FFXIV.Client.Game.Character.Character*)this.Address; } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 7279a8f3c..06e3ca233 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7279a8f3ca6b79490184b05532af509781a89415 +Subproject commit 06e3ca2336031ba86ef95d022a2af722e5d00a7e From a9a0980372c3cecc405daa952c5cb2da9420b064 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 16 Sep 2023 18:07:19 -0700 Subject: [PATCH 114/585] Fix random build warnings. --- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 5 ++- Dalamud/Interface/ColorHelpers.cs | 39 ++++++++++++------- Dalamud/Plugin/Services/IPluginLog.cs | 6 +-- 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 85c6a4a39..e3ea74f2d 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -1,6 +1,4 @@ -using System; using System.Runtime.InteropServices; - using Dalamud.Game.Gui.PartyFinder.Internal; using Dalamud.Game.Gui.PartyFinder.Types; using Dalamud.Hooking; @@ -128,6 +126,9 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu } } +/// +/// A scoped variant of the PartyFinderGui service. +/// [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.ScopedService] diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs index 71f959292..ad9eedaa9 100644 --- a/Dalamud/Interface/ColorHelpers.cs +++ b/Dalamud/Interface/ColorHelpers.cs @@ -1,4 +1,4 @@ -using System; +using System.Diagnostics.CodeAnalysis; using System.Numerics; namespace Dalamud.Interface; @@ -8,6 +8,17 @@ namespace Dalamud.Interface; /// public static class ColorHelpers { + /// + /// A struct representing a color using HSVA coordinates. + /// + /// The hue represented by this struct. + /// The saturation represented by this struct. + /// The value represented by this struct. + /// The alpha represented by this struct. + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", + Justification = "I don't like it.")] + public record struct HsvaColor(float H, float S, float V, float A); + /// /// Pack a vector4 color into a uint for use in ImGui APIs. /// @@ -22,7 +33,7 @@ public static class ColorHelpers return (uint)((a << 24) | (b << 16) | (g << 8) | r); } - + /// /// Convert a RGBA color in the range of 0.f to 1.f to a uint. /// @@ -37,7 +48,7 @@ public static class ColorHelpers return new Vector4(r, g, b, a); } - + /// /// Convert a RGBA color in the range of 0.f to 1.f to a HSV color. /// @@ -146,7 +157,7 @@ public static class ColorHelpers return new Vector4(r, g, b, hsv.A); } - + /// /// Lighten a color. /// @@ -159,7 +170,7 @@ public static class ColorHelpers hsv.V += amount; return HsvToRgb(hsv); } - + /// /// Lighten a color. /// @@ -168,7 +179,7 @@ public static class ColorHelpers /// The lightened color. public static uint Lighten(uint color, float amount) => RgbaVector4ToUint(Lighten(RgbaUintToVector4(color), amount)); - + /// /// Darken a color. /// @@ -181,7 +192,7 @@ public static class ColorHelpers hsv.V -= amount; return HsvToRgb(hsv); } - + /// /// Darken a color. /// @@ -190,7 +201,7 @@ public static class ColorHelpers /// The darkened color. public static uint Darken(uint color, float amount) => RgbaVector4ToUint(Darken(RgbaUintToVector4(color), amount)); - + /// /// Saturate a color. /// @@ -203,7 +214,7 @@ public static class ColorHelpers hsv.S += amount; return HsvToRgb(hsv); } - + /// /// Saturate a color. /// @@ -212,7 +223,7 @@ public static class ColorHelpers /// The saturated color. public static uint Saturate(uint color, float amount) => RgbaVector4ToUint(Saturate(RgbaUintToVector4(color), amount)); - + /// /// Desaturate a color. /// @@ -225,7 +236,7 @@ public static class ColorHelpers hsv.S -= amount; return HsvToRgb(hsv); } - + /// /// Desaturate a color. /// @@ -234,7 +245,7 @@ public static class ColorHelpers /// The desaturated color. public static uint Desaturate(uint color, float amount) => RgbaVector4ToUint(Desaturate(RgbaUintToVector4(color), amount)); - + /// /// Fade a color. /// @@ -247,7 +258,7 @@ public static class ColorHelpers hsv.A -= amount; return HsvToRgb(hsv); } - + /// /// Fade a color. /// @@ -256,6 +267,4 @@ public static class ColorHelpers /// The faded color. public static uint Fade(uint color, float amount) => RgbaVector4ToUint(Fade(RgbaUintToVector4(color), amount)); - - public record struct HsvaColor(float H, float S, float V, float A); } diff --git a/Dalamud/Plugin/Services/IPluginLog.cs b/Dalamud/Plugin/Services/IPluginLog.cs index 87876f36f..62f9e8728 100644 --- a/Dalamud/Plugin/Services/IPluginLog.cs +++ b/Dalamud/Plugin/Services/IPluginLog.cs @@ -1,8 +1,8 @@ -using System; - -using Serilog; +using Serilog; using Serilog.Events; +#pragma warning disable CS1573 // See https://github.com/dotnet/roslyn/issues/40325 + namespace Dalamud.Plugin.Services; /// From 57ae2264e3461ba9d4367527fc39586391619d4e Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Sat, 16 Sep 2023 18:11:06 -0700 Subject: [PATCH 115/585] Fix SA1502 errors on autoformat - Braces in `{ }` style cause SA1502 to complain. --- .editorconfig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0e4f800e0..0ae30cf95 100644 --- a/.editorconfig +++ b/.editorconfig @@ -57,12 +57,12 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly -dotnet_style_parentheses_in_arithmetic_binary_operators =always_for_clarity:suggestion -dotnet_style_parentheses_in_other_binary_operators =always_for_clarity:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion dotnet_style_predefined_type_for_member_access = true:suggestion dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion -dotnet_style_parentheses_in_other_operators=always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = always_for_clarity:silent dotnet_style_object_initializer = false dotnet_style_qualification_for_event = true:suggestion dotnet_style_qualification_for_field = true:suggestion @@ -78,7 +78,7 @@ csharp_space_before_comma = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_comma = true csharp_space_after_cast = false -csharp_space_around_binary_operators = before_and_after +csharp_space_around_binary_operators = before_and_after csharp_space_between_method_declaration_name_and_open_parenthesis = false csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = none @@ -101,7 +101,7 @@ resharper_braces_for_ifelse = required_for_multiline resharper_can_use_global_alias = false resharper_csharp_align_multiline_parameter = true resharper_csharp_align_multiple_declaration = true -resharper_csharp_empty_block_style = together_same_line +resharper_csharp_empty_block_style = multiline resharper_csharp_int_align_comments = true resharper_csharp_new_line_before_while = true resharper_csharp_wrap_after_declaration_lpar = true @@ -133,13 +133,13 @@ resharper_suggest_var_or_type_built_in_types_highlighting = hint resharper_suggest_var_or_type_elsewhere_highlighting = hint resharper_suggest_var_or_type_simple_types_highlighting = hint resharper_unused_auto_property_accessor_global_highlighting = none -csharp_style_deconstructed_variable_declaration=true:silent +csharp_style_deconstructed_variable_declaration = true:silent [*.{appxmanifest,asax,ascx,aspx,axaml,axml,build,c,c++,cc,cginc,compute,config,cp,cpp,cs,cshtml,csproj,css,cu,cuh,cxx,dbml,discomap,dtd,h,hh,hlsl,hlsli,hlslinc,hpp,htm,html,hxx,inc,inl,ino,ipp,js,json,jsproj,jsx,lsproj,master,mpp,mq4,mq5,mqh,njsproj,nuspec,paml,proj,props,proto,razor,resjson,resw,resx,skin,StyleCop,targets,tasks,tpp,ts,tsx,usf,ush,vb,vbproj,xaml,xamlx,xml,xoml,xsd}] indent_style = space indent_size = 4 tab_width = 4 -dotnet_style_parentheses_in_other_operators=always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = always_for_clarity:silent [*.{yaml,yml}] indent_style = space From 08fd4434eaae56d7dae7b6dbdbf7263eb5b0ded0 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 17 Sep 2023 05:41:56 -0700 Subject: [PATCH 116/585] Dalamud Console Window UI-Rework (#1390) Co-authored-by: Kaz Wolfe --- .../Internal/Windows/ConsoleWindow.cs | 584 ++++++++++++------ 1 file changed, 388 insertions(+), 196 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 0febc0fc4..4bd41c025 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Text; +using System.Text.RegularExpressions; using Dalamud.Configuration.Internal; using Dalamud.Game.Command; @@ -13,6 +14,7 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; +using Dalamud.Utility; using ImGuiNET; using Serilog; using Serilog.Events; @@ -27,26 +29,26 @@ internal class ConsoleWindow : Window, IDisposable private readonly List logText = new(); private readonly object renderLock = new(); - private readonly string[] logLevelStrings = new[] { "Verbose", "Debug", "Information", "Warning", "Error", "Fatal" }; - - private List filteredLogText = new(); - private bool autoScroll; - private bool openAtStartup; + private readonly List history = new(); + private readonly List pluginFilters = new(); private bool? lastCmdSuccess; private string commandText = string.Empty; - private string textFilter = string.Empty; - private int levelFilter; - private List sourceFilters = new(); - private bool filterShowUncaughtExceptions = false; - private bool isFiltered = false; + private string selectedSource = "DalamudInternal"; + + private bool filterShowUncaughtExceptions; + private bool showFilterToolbar; + private bool clearLog; + private bool copyLog; + private bool copyMode; + private bool killGameArmed; + private bool autoScroll; + private bool autoOpen; private int historyPos; - private List history = new(); - - private bool killGameArmed = false; + private int copyStart = -1; /// /// Initializes a new instance of the class. @@ -57,16 +59,22 @@ internal class ConsoleWindow : Window, IDisposable var configuration = Service.Get(); this.autoScroll = configuration.LogAutoScroll; - this.openAtStartup = configuration.LogOpenAtStartup; + this.autoOpen = configuration.LogOpenAtStartup; SerilogEventSink.Instance.LogLine += this.OnLogLine; this.Size = new Vector2(500, 400); this.SizeCondition = ImGuiCond.FirstUseEver; + this.SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(600.0f, 200.0f), + MaximumSize = new Vector2(9999.0f, 9999.0f), + }; + this.RespectCloseHotkey = false; } - private List LogEntries => this.isFiltered ? this.filteredLogText : this.logText; + private List FilteredLogEntries { get; set; } = new(); /// public override void OnOpen() @@ -91,10 +99,20 @@ internal class ConsoleWindow : Window, IDisposable lock (this.renderLock) { this.logText.Clear(); - this.filteredLogText.Clear(); + this.FilteredLogEntries.Clear(); + this.clearLog = false; } } + /// + /// Copies the entire log contents to clipboard. + /// + public void CopyLog() + { + ImGui.LogToClipboard(); + this.copyLog = false; + } + /// /// Add a single log line to the display. /// @@ -122,160 +140,15 @@ internal class ConsoleWindow : Window, IDisposable /// public override void Draw() { - // Options menu - if (ImGui.BeginPopup("Options")) - { - var configuration = Service.Get(); + this.DrawOptionsToolbar(); - if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) - { - configuration.LogAutoScroll = this.autoScroll; - configuration.QueueSave(); - } - - if (ImGui.Checkbox("Open at startup", ref this.openAtStartup)) - { - configuration.LogOpenAtStartup = this.openAtStartup; - configuration.QueueSave(); - } - - var prevLevel = (int)EntryPoint.LogLevelSwitch.MinimumLevel; - if (ImGui.Combo("Log Level", ref prevLevel, Enum.GetValues(typeof(LogEventLevel)).Cast().Select(x => x.ToString()).ToArray(), 6)) - { - EntryPoint.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel; - configuration.LogLevel = (LogEventLevel)prevLevel; - configuration.QueueSave(); - } - - ImGui.EndPopup(); - } - - // Filter menu - if (ImGui.BeginPopup("Filters")) - { - if (ImGui.Checkbox("Enabled", ref this.isFiltered)) - { - this.Refilter(); - } - - if (ImGui.InputTextWithHint("##filterText", "Text Filter", ref this.textFilter, 255, ImGuiInputTextFlags.EnterReturnsTrue)) - { - this.Refilter(); - } - - ImGui.TextColored(ImGuiColors.DalamudGrey, "Enter to confirm."); - - if (ImGui.BeginCombo("Levels", this.levelFilter == 0 ? "All Levels..." : "Selected Levels...")) - { - for (var i = 0; i < this.logLevelStrings.Length; i++) - { - if (ImGui.Selectable(this.logLevelStrings[i], ((this.levelFilter >> i) & 1) == 1)) - { - this.levelFilter ^= 1 << i; - this.Refilter(); - } - } - - ImGui.EndCombo(); - } - - // Filter by specific plugin(s) - var sourceNames = Service.Get().InstalledPlugins - .Select(p => p.Manifest.InternalName) - .OrderBy(s => s) - .Prepend("DalamudInternal") - .ToList(); - - var sourcePreviewVal = this.sourceFilters.Count switch - { - 0 => "All sources...", - 1 => "1 source...", - _ => $"{this.sourceFilters.Count} sources...", - }; - var sourceSelectables = sourceNames.Union(this.sourceFilters).ToList(); - if (ImGui.BeginCombo("Sources", sourcePreviewVal)) - { - foreach (var selectable in sourceSelectables) - { - if (ImGui.Selectable(selectable, this.sourceFilters.Contains(selectable))) - { - if (!this.sourceFilters.Contains(selectable)) - { - this.sourceFilters.Add(selectable); - } - else - { - this.sourceFilters.Remove(selectable); - } - - this.Refilter(); - } - } - - ImGui.EndCombo(); - } - - if (ImGui.Checkbox("Always Show Uncaught Exceptions", ref this.filterShowUncaughtExceptions)) - { - this.Refilter(); - } - - ImGui.EndPopup(); - } - - ImGui.SameLine(); - - if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) - ImGui.OpenPopup("Options"); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Options"); - - ImGui.SameLine(); - if (ImGuiComponents.IconButton(FontAwesomeIcon.Search)) - ImGui.OpenPopup("Filters"); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Filters"); - - ImGui.SameLine(); - var clear = ImGuiComponents.IconButton(FontAwesomeIcon.Trash); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Clear Log"); - - ImGui.SameLine(); - var copy = ImGuiComponents.IconButton(FontAwesomeIcon.Copy); - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Copy Log"); - - ImGui.SameLine(); - if (this.killGameArmed) - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.Flushed)) - Process.GetCurrentProcess().Kill(); - } - else - { - if (ImGuiComponents.IconButton(FontAwesomeIcon.Skull)) - this.killGameArmed = true; - } - - if (ImGui.IsItemHovered()) - ImGui.SetTooltip("Kill game"); + this.DrawFilterToolbar(); ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); - if (clear) - { - this.Clear(); - } + if (this.clearLog) this.Clear(); - if (copy) - { - ImGui.LogToClipboard(); - } + if (this.copyLog) this.CopyLog(); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); @@ -291,27 +164,40 @@ internal class ConsoleWindow : Window, IDisposable var childDrawList = ImGui.GetWindowDrawList(); var childSize = ImGui.GetWindowSize(); - var cursorDiv = ImGuiHelpers.GlobalScale * 92; + var cursorDiv = ImGuiHelpers.GlobalScale * 93; var cursorLogLevel = ImGuiHelpers.GlobalScale * 100; var cursorLogLine = ImGuiHelpers.GlobalScale * 135; lock (this.renderLock) { - clipper.Begin(this.LogEntries.Count); + clipper.Begin(this.FilteredLogEntries.Count); while (clipper.Step()) { for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { - var line = this.LogEntries[i]; + var line = this.FilteredLogEntries[i]; - if (!line.IsMultiline && !copy) + if (!line.IsMultiline && !this.copyLog) ImGui.Separator(); + + if (line.SelectedForCopy) + { + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGrey); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, ImGuiColors.ParsedGrey); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, ImGuiColors.ParsedGrey); + } + else + { + ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level)); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level)); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level)); + } - ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level)); - ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level)); - ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level)); + ImGui.Selectable("###console_null", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns); - ImGui.Selectable("###consolenull", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns); + // This must be after ImGui.Selectable, it uses ImGui.IsItem... functions + this.HandleCopyMode(i, line); + ImGui.SameLine(); ImGui.PopStyleColor(3); @@ -366,12 +252,12 @@ internal class ConsoleWindow : Window, IDisposable } } - ImGui.SetNextItemWidth(ImGui.GetWindowSize().X - 80); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (80.0f * ImGuiHelpers.GlobalScale) - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale)); var getFocus = false; unsafe { - if (ImGui.InputText("##commandbox", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback)) + if (ImGui.InputText("##command_box", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback)) { this.ProcessCommand(); getFocus = true; @@ -387,16 +273,279 @@ internal class ConsoleWindow : Window, IDisposable if (hadColor) ImGui.PopStyleColor(); - if (ImGui.Button("Send")) + if (ImGui.Button("Send", ImGuiHelpers.ScaledVector2(80.0f, 23.0f))) { this.ProcessCommand(); } } + + private void HandleCopyMode(int i, LogEntry line) + { + var selectionChanged = false; + + // If copyStart is -1, it means a drag has not been started yet, let's start one, and select the starting spot. + if (this.copyMode && this.copyStart == -1 && ImGui.IsItemClicked()) + { + this.copyStart = i; + line.SelectedForCopy = !line.SelectedForCopy; + + selectionChanged = true; + } + + // Update the selected range when dragging over entries + if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + { + if (!line.SelectedForCopy) + { + foreach (var index in Enumerable.Range(0, this.FilteredLogEntries.Count)) + { + if (this.copyStart < i) + { + this.FilteredLogEntries[index].SelectedForCopy = index >= this.copyStart && index <= i; + } + else + { + this.FilteredLogEntries[index].SelectedForCopy = index >= i && index <= this.copyStart; + } + } + + selectionChanged = true; + } + } + + // Finish the drag, we should have already marked all dragged entries as selected by now. + if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + this.copyStart = -1; + } + + if (selectionChanged) + { + var allSelectedLines = this.FilteredLogEntries + .Where(entry => entry.SelectedForCopy) + .Select(entry => $"{line.TimeStamp:HH:mm:ss.fff} {this.GetTextForLogEventLevel(entry.Level)} | {entry.Line}"); + + ImGui.SetClipboardText(string.Join("\n", allSelectedLines)); + } + } + + private void DrawOptionsToolbar() + { + var configuration = Service.Get(); + + ImGui.PushItemWidth(150.0f * ImGuiHelpers.GlobalScale); + if (ImGui.BeginCombo("##log_level", $"{EntryPoint.LogLevelSwitch.MinimumLevel}+")) + { + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), value == EntryPoint.LogLevelSwitch.MinimumLevel)) + { + EntryPoint.LogLevelSwitch.MinimumLevel = value; + configuration.LogLevel = value; + configuration.QueueSave(); + this.Refilter(); + } + } + + ImGui.EndCombo(); + } + + ImGui.SameLine(); + + this.autoScroll = configuration.LogAutoScroll; + if (this.DrawToggleButtonWithTooltip("auto_scroll", "Auto-scroll", FontAwesomeIcon.Sync, ref this.autoScroll)) + { + configuration.LogAutoScroll = !configuration.LogAutoScroll; + configuration.QueueSave(); + } + + ImGui.SameLine(); + + this.autoOpen = configuration.LogOpenAtStartup; + if (this.DrawToggleButtonWithTooltip("auto_open", "Open at startup", FontAwesomeIcon.WindowRestore, ref this.autoOpen)) + { + configuration.LogOpenAtStartup = !configuration.LogOpenAtStartup; + configuration.QueueSave(); + } + + ImGui.SameLine(); + + if (this.DrawToggleButtonWithTooltip("show_filters", "Show filter toolbar", FontAwesomeIcon.Search, ref this.showFilterToolbar)) + { + this.showFilterToolbar = !this.showFilterToolbar; + } + + ImGui.SameLine(); + + if (this.DrawToggleButtonWithTooltip("show_uncaught_exceptions", "Show uncaught exception while filtering", FontAwesomeIcon.Bug, ref this.filterShowUncaughtExceptions)) + { + this.filterShowUncaughtExceptions = !this.filterShowUncaughtExceptions; + } + + ImGui.SameLine(); + + if (ImGuiComponents.IconButton("clear_log", FontAwesomeIcon.Trash)) + { + this.clearLog = true; + } + + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Clear Log"); + + ImGui.SameLine(); + + if (this.DrawToggleButtonWithTooltip("copy_mode", "Enable Copy Mode\nRight-click to copy entire log", FontAwesomeIcon.Copy, ref this.copyMode)) + { + this.copyMode = !this.copyMode; + + if (!this.copyMode) + { + foreach (var entry in this.FilteredLogEntries) + { + entry.SelectedForCopy = false; + } + } + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) this.copyLog = true; + + ImGui.SameLine(); + if (this.killGameArmed) + { + if (ImGuiComponents.IconButton(FontAwesomeIcon.ExclamationTriangle)) + Process.GetCurrentProcess().Kill(); + } + else + { + if (ImGuiComponents.IconButton(FontAwesomeIcon.Stop)) + this.killGameArmed = true; + } + + if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game"); + + ImGui.SameLine(); + ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - (200.0f * ImGuiHelpers.GlobalScale)); + ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale); + if (ImGui.InputTextWithHint("##global_filter", "regex global filter", ref this.textFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)) + { + this.Refilter(); + } + + if (ImGui.IsItemDeactivatedAfterEdit()) + { + this.Refilter(); + } + } + + private void DrawFilterToolbar() + { + if (!this.showFilterToolbar) return; + + PluginFilterEntry? removalEntry = null; + if (ImGui.BeginTable("plugin_filter_entries", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV)) + { + ImGui.TableSetupColumn("##remove_button", ImGuiTableColumnFlags.WidthFixed, 25.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##source_name", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##log_level", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##filter_text", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton("add_entry", FontAwesomeIcon.Plus)) + { + if (this.pluginFilters.All(entry => entry.Source != this.selectedSource)) + { + this.pluginFilters.Add(new PluginFilterEntry + { + Source = this.selectedSource, + Filter = string.Empty, + Level = LogEventLevel.Debug, + }); + } + + this.Refilter(); + } + + ImGui.TableNextColumn(); + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.BeginCombo("##Sources", this.selectedSource)) + { + var sourceNames = Service.Get().InstalledPlugins + .Select(p => p.Manifest.InternalName) + .OrderBy(s => s) + .Prepend("DalamudInternal") + .ToList(); + + foreach (var selectable in sourceNames) + { + if (ImGui.Selectable(selectable, this.selectedSource == selectable)) + { + this.selectedSource = selectable; + } + } + + ImGui.EndCombo(); + } + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + + foreach (var entry in this.pluginFilters) + { + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton($"remove{entry.Source}", FontAwesomeIcon.Trash)) + { + removalEntry = entry; + } + + ImGui.TableNextColumn(); + ImGui.Text(entry.Source); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.BeginCombo($"##levels{entry.Source}", $"{entry.Level}+")) + { + foreach (var value in Enum.GetValues()) + { + if (ImGui.Selectable(value.ToString(), value == entry.Level)) + { + entry.Level = value; + this.Refilter(); + } + } + + ImGui.EndCombo(); + } + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var entryFilter = entry.Filter; + if (ImGui.InputTextWithHint($"##filter{entry.Source}", $"{entry.Source} regex filter", ref entryFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)) + { + entry.Filter = entryFilter; + this.Refilter(); + } + + if (ImGui.IsItemDeactivatedAfterEdit()) this.Refilter(); + } + + ImGui.EndTable(); + } + + if (removalEntry is { } toRemove) + { + this.pluginFilters.Remove(toRemove); + this.Refilter(); + } + } private void ProcessCommand() { try { + if (this.commandText is['/', ..]) + { + this.commandText = this.commandText[1..]; + } + this.historyPos = -1; for (var i = this.history.Count - 1; i >= 0; i--) { @@ -409,7 +558,7 @@ internal class ConsoleWindow : Window, IDisposable this.history.Add(this.commandText); - if (this.commandText == "clear" || this.commandText == "cls") + if (this.commandText is "clear" or "cls") { this.Clear(); return; @@ -446,7 +595,8 @@ internal class ConsoleWindow : Window, IDisposable // TODO: Improve this, add partial completion // https://github.com/ocornut/imgui/blob/master/imgui_demo.cpp#L6443-L6484 - var candidates = Service.Get().Commands.Where(x => x.Key.Contains("/" + words[0])) + var candidates = Service.Get().Commands + .Where(x => x.Key.Contains("/" + words[0])) .ToList(); if (candidates.Count > 0) { @@ -455,6 +605,7 @@ internal class ConsoleWindow : Window, IDisposable } break; + case ImGuiInputTextFlags.CallbackHistory: var prevPos = this.historyPos; @@ -503,7 +654,7 @@ internal class ConsoleWindow : Window, IDisposable TimeStamp = logEvent.Timestamp, HasException = logEvent.Exception != null, }; - + // TODO (v9): Remove SourceContext property check. if (logEvent.Properties.ContainsKey("Dalamud.ModuleName")) { @@ -518,37 +669,49 @@ internal class ConsoleWindow : Window, IDisposable this.logText.Add(entry); - if (!this.isFiltered) - return; - if (this.IsFilterApplicable(entry)) - this.filteredLogText.Add(entry); + this.FilteredLogEntries.Add(entry); } private bool IsFilterApplicable(LogEntry entry) { - if (this.levelFilter > 0 && ((this.levelFilter >> (int)entry.Level) & 1) == 0) + // If this entry is below a newly set minimum level, fail it + if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level) return false; - + // Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught) // After log levels because uncaught exceptions should *never* fall below Error. if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null) return true; - if (this.sourceFilters.Count > 0 && !this.sourceFilters.Contains(entry.Source)) - return false; + // If we have a global filter, check that first + if (!this.textFilter.IsNullOrEmpty()) + { + // Someone will definitely try to just text filter a source without using the actual filters, should allow that. + var matchesSource = entry.Source is not null && Regex.IsMatch(entry.Source, this.textFilter, RegexOptions.IgnoreCase); + var matchesContent = Regex.IsMatch(entry.Line, this.textFilter, RegexOptions.IgnoreCase); - if (!string.IsNullOrEmpty(this.textFilter) && !entry.Line.Contains(this.textFilter)) - return false; + return matchesSource || matchesContent; + } - return true; + // If this entry has a filter, check the filter + if (this.pluginFilters.FirstOrDefault(filter => string.Equals(filter.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase)) is { } filterEntry) + { + var allowedLevel = filterEntry.Level <= entry.Level; + var matchesContent = filterEntry.Filter.IsNullOrEmpty() || Regex.IsMatch(entry.Line, filterEntry.Filter, RegexOptions.IgnoreCase); + + return allowedLevel && matchesContent; + } + + // else we couldn't find a filter for this entry, if we have any filters, we need to block this entry. + return !this.pluginFilters.Any(); } private void Refilter() { lock (this.renderLock) { - this.filteredLogText = this.logText.Where(this.IsFilterApplicable).ToList(); + this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList(); } } @@ -579,22 +742,51 @@ internal class ConsoleWindow : Window, IDisposable this.HandleLogLine(logEvent.Line, logEvent.LogEvent); } + private bool DrawToggleButtonWithTooltip(string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState) + { + var result = false; + + var buttonEnabled = enabledState; + if (buttonEnabled) ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.HealerGreen with { W = 0.25f }); + if (ImGuiComponents.IconButton(buttonId, icon)) + { + result = true; + } + + if (ImGui.IsItemHovered()) ImGui.SetTooltip(tooltip); + + if (buttonEnabled) ImGui.PopStyleColor(); + + return result; + } + private class LogEntry { - public string Line { get; set; } + public string Line { get; init; } = string.Empty; - public LogEventLevel Level { get; set; } + public LogEventLevel Level { get; init; } - public DateTimeOffset TimeStamp { get; set; } + public DateTimeOffset TimeStamp { get; init; } - public bool IsMultiline { get; set; } + public bool IsMultiline { get; init; } /// /// Gets or sets the system responsible for generating this log entry. Generally will be a plugin's /// InternalName. /// public string? Source { get; set; } + + public bool SelectedForCopy { get; set; } - public bool HasException { get; set; } + public bool HasException { get; init; } + } + + private class PluginFilterEntry + { + public string Source { get; init; } = string.Empty; + + public string Filter { get; set; } = string.Empty; + + public LogEventLevel Level { get; set; } } } From 2c23e6fdb33db0dad89a3a93b02073f19d77169f Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sun, 17 Sep 2023 06:56:49 -0700 Subject: [PATCH 117/585] [v9] Move GPose check to ClientState (#1378) --- Dalamud/Game/ClientState/ClientState.cs | 3 +++ Dalamud/Interface/UiBuilder.cs | 18 +++--------------- Dalamud/Plugin/Services/IClientState.cs | 5 +++++ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index fed0ec3c4..6817523af 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -102,6 +102,9 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState /// public bool IsPvPExcludingDen { get; private set; } + /// + public bool IsGPosing => GameMain.IsInGPose(); + /// /// Gets client state address resolver. /// diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index b440a0705..95ee28f56 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; +using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; @@ -179,20 +180,6 @@ public sealed class UiBuilder : IDisposable } } - /// - /// Gets a value indicating whether or not gpose is active. - /// - public bool GposeActive - { - get - { - var condition = Service.GetNullable(); - if (condition == null) - return false; - return condition[ConditionFlag.WatchingCutscene]; - } - } - /// /// Gets a value indicating whether this plugin should modify the game's interface at this time. /// @@ -448,6 +435,7 @@ public sealed class UiBuilder : IDisposable { this.hitchDetector.Start(); + var clientState = Service.Get(); var configuration = Service.Get(); var gameGui = Service.GetNullable(); if (gameGui == null) @@ -457,7 +445,7 @@ public sealed class UiBuilder : IDisposable !(this.DisableUserUiHide || this.DisableAutomaticUiHide)) || (this.CutsceneActive && configuration.ToggleUiHideDuringCutscenes && !(this.DisableCutsceneUiHide || this.DisableAutomaticUiHide)) || - (this.GposeActive && configuration.ToggleUiHideDuringGpose && + (clientState.IsGPosing && configuration.ToggleUiHideDuringGpose && !(this.DisableGposeUiHide || this.DisableAutomaticUiHide))) { if (!this.lastFrameUiHideState) diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index d66db9cc9..881cad841 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -73,4 +73,9 @@ public interface IClientState /// Gets a value indicating whether or not the user is playing PvP, excluding the Wolves' Den. /// public bool IsPvPExcludingDen { get; } + + /// + /// Gets a value indicating whether the client is currently in Group Pose (GPose) mode. + /// + public bool IsGPosing { get; } } From 452cf7813f70c71e9365f640273f46154491bb97 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 17 Sep 2023 07:01:11 -0700 Subject: [PATCH 118/585] Data Window ui rework (#1376) --- .../Internal/Windows/Data/DataKindEnum.cs | 163 --------------- .../Internal/Windows/Data/DataWindow.cs | 186 +++++++++++------- .../Windows/Data/IDataWindowWidget.cs | 21 +- .../Data/Widgets/AddonInspectorWidget.cs | 5 +- .../Windows/Data/Widgets/AddonWidget.cs | 5 +- .../Windows/Data/Widgets/AddressesWidget.cs | 5 +- .../Windows/Data/Widgets/AetherytesWidget.cs | 7 +- .../Data/Widgets/AtkArrayDataBrowserWidget.cs | 7 +- .../Windows/Data/Widgets/BuddyListWidget.cs | 7 +- .../Windows/Data/Widgets/CommandWidget.cs | 5 +- .../Windows/Data/Widgets/ConditionWidget.cs | 7 +- .../Data/Widgets/ConfigurationWidget.cs | 5 +- .../Windows/Data/Widgets/DataShareWidget.cs | 5 +- .../Windows/Data/Widgets/DtrBarWidget.cs | 5 +- .../Windows/Data/Widgets/FateTableWidget.cs | 5 +- .../Windows/Data/Widgets/FlyTextWidget.cs | 5 +- .../Data/Widgets/FontAwesomeTestWidget.cs | 5 +- .../Windows/Data/Widgets/GamepadWidget.cs | 5 +- .../Windows/Data/Widgets/GaugeWidget.cs | 5 +- .../Windows/Data/Widgets/HookWidget.cs | 5 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 5 +- .../Windows/Data/Widgets/KeyStateWidget.cs | 5 +- .../Data/Widgets/NetworkMonitorWidget.cs | 5 +- .../Windows/Data/Widgets/ObjectTableWidget.cs | 5 +- .../Windows/Data/Widgets/PartyListWidget.cs | 5 +- .../Windows/Data/Widgets/PluginIpcWidget.cs | 5 +- .../Windows/Data/Widgets/SeFontTestWidget.cs | 5 +- .../Data/Widgets/ServerOpcodeWidget.cs | 5 +- .../Windows/Data/Widgets/StartInfoWidget.cs | 5 +- .../Windows/Data/Widgets/TargetWidget.cs | 5 +- .../Data/Widgets/TaskSchedulerWidget.cs | 5 +- .../Windows/Data/Widgets/TexWidget.cs | 5 +- .../Windows/Data/Widgets/ToastWidget.cs | 5 +- .../Windows/Data/Widgets/UIColorWidget.cs | 5 +- 34 files changed, 257 insertions(+), 276 deletions(-) delete mode 100644 Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs b/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs deleted file mode 100644 index d7c4eb095..000000000 --- a/Dalamud/Interface/Internal/Windows/Data/DataKindEnum.cs +++ /dev/null @@ -1,163 +0,0 @@ -// ReSharper disable InconsistentNaming // Naming is suppressed so we can replace '_' with ' ' -namespace Dalamud.Interface.Internal.Windows; - -/// -/// Enum representing a DataKind for the Data Window. -/// -internal enum DataKind -{ - /// - /// Server Opcode Display. - /// - Server_OpCode, - - /// - /// Address. - /// - Address, - - /// - /// Object Table. - /// - Object_Table, - - /// - /// Fate Table. - /// - Fate_Table, - - /// - /// SE Font Test. - /// - SE_Font_Test, - - /// - /// FontAwesome Test. - /// - FontAwesome_Test, - - /// - /// Party List. - /// - Party_List, - - /// - /// Buddy List. - /// - Buddy_List, - - /// - /// Plugin IPC Test. - /// - Plugin_IPC, - - /// - /// Player Condition. - /// - Condition, - - /// - /// Gauge. - /// - Gauge, - - /// - /// Command. - /// - Command, - - /// - /// Addon. - /// - Addon, - - /// - /// Addon Inspector. - /// - Addon_Inspector, - - /// - /// AtkArrayData Browser. - /// - AtkArrayData_Browser, - - /// - /// StartInfo. - /// - StartInfo, - - /// - /// Target. - /// - Target, - - /// - /// Toast. - /// - Toast, - - /// - /// Fly Text. - /// - FlyText, - - /// - /// ImGui. - /// - ImGui, - - /// - /// Tex. - /// - Tex, - - /// - /// KeyState. - /// - KeyState, - - /// - /// GamePad. - /// - Gamepad, - - /// - /// Configuration. - /// - Configuration, - - /// - /// Task Scheduler. - /// - TaskSched, - - /// - /// Hook. - /// - Hook, - - /// - /// Aetherytes. - /// - Aetherytes, - - /// - /// DTR Bar. - /// - Dtr_Bar, - - /// - /// UIColor. - /// - UIColor, - - /// - /// Data Share. - /// - Data_Share, - - /// - /// Network Monitor. - /// - Network_Monitor, -} diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 9d8dc1e93..05d5bff3f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Numerics; @@ -51,26 +50,24 @@ internal class DataWindow : Window new NetworkMonitorWidget(), }; - private readonly Dictionary dataKindNames = new(); + private readonly IOrderedEnumerable orderedModules; private bool isExcept; - private DataKind currentKind; - + private bool selectionCollapsed; + private IDataWindowWidget currentWidget; + /// /// Initializes a new instance of the class. /// public DataWindow() - : base("Dalamud Data") + : base("Dalamud Data", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { - this.Size = new Vector2(500, 500); + this.Size = new Vector2(400, 300); this.SizeCondition = ImGuiCond.FirstUseEver; this.RespectCloseHotkey = false; - - foreach (var dataKind in Enum.GetValues()) - { - this.dataKindNames[dataKind] = dataKind.ToString().Replace("_", " "); - } + this.orderedModules = this.modules.OrderBy(module => module.DisplayName); + this.currentWidget = this.orderedModules.First(); this.Load(); } @@ -94,24 +91,9 @@ internal class DataWindow : Window if (string.IsNullOrEmpty(dataKind)) return; - dataKind = dataKind switch + if (this.modules.FirstOrDefault(module => module.IsWidgetCommand(dataKind)) is { } targetModule) { - "ai" => "Addon Inspector", - "at" => "Object Table", // Actor Table - "ot" => "Object Table", - "uic" => "UIColor", - _ => dataKind, - }; - - dataKind = dataKind.Replace(" ", string.Empty).ToLower(); - - var matched = Enum - .GetValues() - .FirstOrDefault(kind => Enum.GetName(kind)?.Replace("_", string.Empty).ToLower() == dataKind); - - if (matched != default) - { - this.currentKind = matched; + this.currentWidget = targetModule; } else { @@ -124,59 +106,113 @@ internal class DataWindow : Window /// public override void Draw() { - if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync)) this.Load(); - if (ImGui.IsItemHovered()) ImGui.SetTooltip("Force Reload"); - ImGui.SameLine(); - var copy = ImGuiComponents.IconButton("copyAll", FontAwesomeIcon.ClipboardList); - if (ImGui.IsItemHovered()) ImGui.SetTooltip("Copy All"); - ImGui.SameLine(); - - ImGui.SetNextItemWidth(275.0f * ImGuiHelpers.GlobalScale); - if (ImGui.BeginCombo("Data Kind", this.dataKindNames[this.currentKind])) + // Only draw the widget contents if the selection pane is collapsed. + if (this.selectionCollapsed) { - foreach (var module in this.modules.OrderBy(module => this.dataKindNames[module.DataKind])) + this.DrawContents(); + return; + } + + if (ImGui.BeginTable("XlData_Table", 2, ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.Resizable)) + { + ImGui.TableSetupColumn("##SelectionColumn", ImGuiTableColumnFlags.WidthFixed, 200.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##ContentsColumn", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + this.DrawSelection(); + + ImGui.TableNextColumn(); + this.DrawContents(); + + ImGui.EndTable(); + } + } + + private void DrawSelection() + { + if (ImGui.BeginChild("XlData_SelectionPane", ImGui.GetContentRegionAvail())) + { + if (ImGui.BeginListBox("WidgetSelectionListbox", ImGui.GetContentRegionAvail())) { - if (ImGui.Selectable(this.dataKindNames[module.DataKind], this.currentKind == module.DataKind)) + foreach (var widget in this.orderedModules) { - this.currentKind = module.DataKind; + if (ImGui.Selectable(widget.DisplayName, this.currentWidget == widget)) + { + this.currentWidget = widget; + } + } + + ImGui.EndListBox(); + } + } + + ImGui.EndChild(); + } + + private void DrawContents() + { + if (ImGui.BeginChild("XlData_ContentsPane", ImGui.GetContentRegionAvail())) + { + if (ImGuiComponents.IconButton("collapse-expand", this.selectionCollapsed ? FontAwesomeIcon.ArrowRight : FontAwesomeIcon.ArrowLeft)) + { + this.selectionCollapsed = !this.selectionCollapsed; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip($"{(this.selectionCollapsed ? "Expand" : "Collapse")} selection pane"); + } + + ImGui.SameLine(); + + if (ImGuiComponents.IconButton("forceReload", FontAwesomeIcon.Sync)) + { + this.Load(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Force Reload"); + } + + ImGui.SameLine(); + + var copy = ImGuiComponents.IconButton("copyAll", FontAwesomeIcon.ClipboardList); + + ImGuiHelpers.ScaledDummy(10.0f); + + if (ImGui.BeginChild("XlData_WidgetContents", ImGui.GetContentRegionAvail())) + { + if (copy) + ImGui.LogToClipboard(); + + try + { + if (this.currentWidget is { Ready: true }) + { + this.currentWidget.Draw(); + } + else + { + ImGui.TextUnformatted("Data not ready."); + } + + this.isExcept = false; + } + catch (Exception ex) + { + if (!this.isExcept) + { + Log.Error(ex, "Could not draw data"); + } + + this.isExcept = true; + + ImGui.TextUnformatted(ex.ToString()); } } - - ImGui.EndCombo(); - } - - ImGuiHelpers.ScaledDummy(10.0f); - ImGui.BeginChild("scrolling", Vector2.Zero, false, ImGuiWindowFlags.HorizontalScrollbar); - - if (copy) - ImGui.LogToClipboard(); - - try - { - var selectedWidget = this.modules.FirstOrDefault(dataWindowWidget => dataWindowWidget.DataKind == this.currentKind); - - if (selectedWidget is { Ready: true }) - { - selectedWidget.Draw(); - } - else - { - ImGui.TextUnformatted("Data not ready."); - } - - this.isExcept = false; - } - catch (Exception ex) - { - if (!this.isExcept) - { - Log.Error(ex, "Could not draw data"); - } - - this.isExcept = true; - - ImGui.TextUnformatted(ex.ToString()); + ImGui.EndChild(); } ImGui.EndChild(); diff --git a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs index ebbdfff83..0e12e4c51 100644 --- a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs @@ -1,4 +1,7 @@ -namespace Dalamud.Interface.Internal.Windows; +using System; +using System.Linq; + +namespace Dalamud.Interface.Internal.Windows; /// /// Class representing a date window entry. @@ -6,9 +9,14 @@ internal interface IDataWindowWidget { /// - /// Gets the Data Kind for this data window module. + /// Gets the command strings that can be used to open the data window directly to this module. /// - DataKind DataKind { get; init; } + string[]? CommandShortcuts { get; init; } + + /// + /// Gets the display name for this module. + /// + string DisplayName { get; init; } /// /// Gets or sets a value indicating whether this data window module is ready. @@ -24,4 +32,11 @@ internal interface IDataWindowWidget /// Draws this data window module. /// void Draw(); + + /// + /// Helper method to check if this widget should be activated by the input command. + /// + /// The command being run. + /// true if this module should be activated by the input command. + bool IsWidgetCommand(string command) => this.CommandShortcuts?.Any(shortcut => string.Equals(shortcut, command, StringComparison.InvariantCultureIgnoreCase)) ?? false; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs index 977037cc5..b39dfcc4f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonInspectorWidget.cs @@ -8,7 +8,10 @@ internal class AddonInspectorWidget : IDataWindowWidget private UiDebug? addonInspector; /// - public DataKind DataKind { get; init; } = DataKind.Addon_Inspector; + public string[]? CommandShortcuts { get; init; } = { "ai", "addoninspector" }; + + /// + public string DisplayName { get; init; } = "Addon Inspector"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs index b26b7e311..8d11e6285 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonWidget.cs @@ -15,7 +15,10 @@ internal unsafe class AddonWidget : IDataWindowWidget private nint findAgentInterfacePtr; /// - public DataKind DataKind { get; init; } = DataKind.Addon; + public string DisplayName { get; init; } = "Addon"; + + /// + public string[]? CommandShortcuts { get; init; } /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs index 606fedadd..2beea905e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs @@ -14,7 +14,10 @@ internal class AddressesWidget : IDataWindowWidget private nint sigResult = nint.Zero; /// - public DataKind DataKind { get; init; } = DataKind.Address; + public string[]? CommandShortcuts { get; init; } = { "address" }; + + /// + public string DisplayName { get; init; } = "Addresses"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs index cc4771847..fd4a76544 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AetherytesWidget.cs @@ -9,10 +9,13 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class AetherytesWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Aetherytes; + public bool Ready { get; set; } /// - public bool Ready { get; set; } + public string[]? CommandShortcuts { get; init; } = { "aetherytes" }; + + /// + public string DisplayName { get; init; } = "Aetherytes"; /// public void Load() diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs index df98f99a6..9aac779b3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AtkArrayDataBrowserWidget.cs @@ -12,10 +12,13 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal unsafe class AtkArrayDataBrowserWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.AtkArrayData_Browser; + public bool Ready { get; set; } /// - public bool Ready { get; set; } + public string[]? CommandShortcuts { get; init; } = { "atkarray" }; + + /// + public string DisplayName { get; init; } = "Atk Array Data"; /// public void Load() diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs index 2aeb9d10d..664b4205e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/BuddyListWidget.cs @@ -12,10 +12,13 @@ internal class BuddyListWidget : IDataWindowWidget private bool resolveGameData; /// - public DataKind DataKind { get; init; } = DataKind.Buddy_List; + public bool Ready { get; set; } /// - public bool Ready { get; set; } + public string[]? CommandShortcuts { get; init; } = { "buddy", "buddylist" }; + + /// + public string DisplayName { get; init; } = "Buddy List"; /// public void Load() diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs index e415431ba..c7f6564d1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class CommandWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Command; + public string[]? CommandShortcuts { get; init; } = { "command" }; + + /// + public string DisplayName { get; init; } = "Command"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs index a5224589f..be19d7ae2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConditionWidget.cs @@ -9,10 +9,13 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class ConditionWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Condition; + public bool Ready { get; set; } /// - public bool Ready { get; set; } + public string[]? CommandShortcuts { get; init; } = { "condition" }; + + /// + public string DisplayName { get; init; } = "Condition"; /// public void Load() diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs index 3922f22b7..5b85eb814 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ConfigurationWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class ConfigurationWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Configuration; + public string[]? CommandShortcuts { get; init; } = { "config", "configuration" }; + + /// + public string DisplayName { get; init; } = "Configuration"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index ec7124042..d89be3357 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class DataShareWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Data_Share; + public string[]? CommandShortcuts { get; init; } = { "datashare" }; + + /// + public string DisplayName { get; init; } = "Data Share"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs index 6d3a67e1a..125d6dbbf 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DtrBarWidget.cs @@ -14,7 +14,10 @@ internal class DtrBarWidget : IDataWindowWidget private DtrBarEntry? dtrTest3; /// - public DataKind DataKind { get; init; } = DataKind.Dtr_Bar; + public string[]? CommandShortcuts { get; init; } = { "dtr", "dtrbar" }; + + /// + public string DisplayName { get; init; } = "DTR Bar"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs index 779032f1d..ca77f089f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FateTableWidget.cs @@ -11,7 +11,10 @@ internal class FateTableWidget : IDataWindowWidget private bool resolveGameData; /// - public DataKind DataKind { get; init; } = DataKind.Fate_Table; + public string[]? CommandShortcuts { get; init; } = { "fate", "fatetable" }; + + /// + public string DisplayName { get; init; } = "Fate Table"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs index 99c1a3e02..ff937996e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs @@ -22,7 +22,10 @@ internal class FlyTextWidget : IDataWindowWidget private Vector4 flyColor = new(1, 0, 0, 1); /// - public DataKind DataKind { get; init; } = DataKind.FlyText; + public string[]? CommandShortcuts { get; init; } = { "flytext" }; + + /// + public string DisplayName { get; init; } = "Fly Text"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 1ed5e9e83..036ea7000 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -20,7 +20,10 @@ internal class FontAwesomeTestWidget : IDataWindowWidget private bool iconSearchChanged = true; /// - public DataKind DataKind { get; init; } = DataKind.FontAwesome_Test; + public string[]? CommandShortcuts { get; init; } = { "fa", "fatest", "fontawesome" }; + + /// + public string DisplayName { get; init; } = "Font Awesome Test"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs index 1a4408d53..b20e0132e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamepadWidget.cs @@ -11,7 +11,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class GamepadWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Gamepad; + public string[]? CommandShortcuts { get; init; } = { "gamepad", "controller" }; + + /// + public string DisplayName { get; init; } = "Gamepad"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs index 02862b33d..dee7999ee 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GaugeWidget.cs @@ -12,7 +12,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class GaugeWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.Gauge; + public string[]? CommandShortcuts { get; init; } = { "gauge", "jobgauge", "job" }; + + /// + public string DisplayName { get; init; } = "Job Gauge"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index aa565b1e6..d5c566e52 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -23,8 +23,11 @@ internal class HookWidget : IDataWindowWidget NativeFunctions.MessageBoxType type); /// - public DataKind DataKind { get; init; } = DataKind.Hook; + public string DisplayName { get; init; } = "Hook"; + /// + public string[]? CommandShortcuts { get; init; } = { "hook" }; + /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 8afce718f..311004f2d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -12,7 +12,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class ImGuiWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.ImGui; + public string[]? CommandShortcuts { get; init; } = { "imgui" }; + + /// + public string DisplayName { get; init; } = "ImGui"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs index accc48b4b..ce072abc4 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/KeyStateWidget.cs @@ -10,7 +10,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class KeyStateWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.KeyState; + public string[]? CommandShortcuts { get; init; } = { "keystate" }; + + /// + public string DisplayName { get; init; } = "KeyState"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 01d0b1759..d1c0150dc 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -54,7 +54,10 @@ internal class NetworkMonitorWidget : IDataWindowWidget } /// - public DataKind DataKind { get; init; } = DataKind.Network_Monitor; + public string[]? CommandShortcuts { get; init; } = { "network", "netmon", "networkmonitor" }; + + /// + public string DisplayName { get; init; } = "Network Monitor"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs index dd41315f2..42ce3ced6 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ObjectTableWidget.cs @@ -19,8 +19,11 @@ internal class ObjectTableWidget : IDataWindowWidget private float maxCharaDrawDistance = 20.0f; /// - public DataKind DataKind { get; init; } = DataKind.Object_Table; + public string[]? CommandShortcuts { get; init; } = { "ot", "objecttable" }; + /// + public string DisplayName { get; init; } = "Object Table"; + /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs index c5ac1fb8f..369fd7620 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PartyListWidget.cs @@ -12,7 +12,10 @@ internal class PartyListWidget : IDataWindowWidget private bool resolveGameData; /// - public DataKind DataKind { get; init; } = DataKind.Party_List; + public string[]? CommandShortcuts { get; init; } = { "partylist", "party" }; + + /// + public string DisplayName { get; init; } = "Party List"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs index 9aae9bba3..b89ead526 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/PluginIpcWidget.cs @@ -19,7 +19,10 @@ internal class PluginIpcWidget : IDataWindowWidget private string callGateResponse = string.Empty; /// - public DataKind DataKind { get; init; } = DataKind.Plugin_IPC; + public string[]? CommandShortcuts { get; init; } = { "ipc" }; + + /// + public string DisplayName { get; init; } = "Plugin IPC"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs index a642c439d..89dc5735a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeFontTestWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class SeFontTestWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.SE_Font_Test; + public string[]? CommandShortcuts { get; init; } = { "sefont", "sefonttest" }; + + /// + public string DisplayName { get; init; } = "SeFont Test"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs index f414e0957..6adf02b3d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs @@ -12,7 +12,10 @@ internal class ServerOpcodeWidget : IDataWindowWidget private string? serverOpString; /// - public DataKind DataKind { get; init; } = DataKind.Server_OpCode; + public string[]? CommandShortcuts { get; init; } = { "opcode", "serveropcode" }; + + /// + public string DisplayName { get; init; } = "Server Opcode"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs index 656efe388..e635b55e0 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs @@ -9,7 +9,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class StartInfoWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.StartInfo; + public string[]? CommandShortcuts { get; init; } = { "startinfo" }; + + /// + public string DisplayName { get; init; } = "Start Info"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs index f33e67f70..39c6d5b18 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TargetWidget.cs @@ -13,7 +13,10 @@ internal class TargetWidget : IDataWindowWidget private bool resolveGameData; /// - public DataKind DataKind { get; init; } = DataKind.Target; + public string[]? CommandShortcuts { get; init; } = { "target" }; + + /// + public string DisplayName { get; init; } = "Target"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index 7d91cd154..35d449443 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -20,7 +20,10 @@ internal class TaskSchedulerWidget : IDataWindowWidget private CancellationTokenSource taskSchedulerCancelSource = new(); /// - public DataKind DataKind { get; init; } = DataKind.TaskSched; + public string[]? CommandShortcuts { get; init; } = { "tasksched", "taskscheduler" }; + + /// + public string DisplayName { get; init; } = "Task Scheduler"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 5ad5868c3..9c1f93b0b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -28,7 +28,10 @@ internal class TexWidget : IDataWindowWidget private Vector2 inputTexScale = Vector2.Zero; /// - public DataKind DataKind { get; init; } = DataKind.Tex; + public string[]? CommandShortcuts { get; init; } = { "tex", "texture" }; + + /// + public string DisplayName { get; init; } = "Tex"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs index c75230e73..336312e87 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ToastWidget.cs @@ -19,7 +19,10 @@ internal class ToastWidget : IDataWindowWidget private bool questToastCheckmark; /// - public DataKind DataKind { get; init; } = DataKind.Toast; + public string[]? CommandShortcuts { get; init; } = { "toast" }; + + /// + public string DisplayName { get; init; } = "Toast"; /// public bool Ready { get; set; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs index 1d0ccdce6..d2d480fff 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/UIColorWidget.cs @@ -12,7 +12,10 @@ namespace Dalamud.Interface.Internal.Windows.Data; internal class UIColorWidget : IDataWindowWidget { /// - public DataKind DataKind { get; init; } = DataKind.UIColor; + public string[]? CommandShortcuts { get; init; } = { "uicolor" }; + + /// + public string DisplayName { get; init; } = "UIColor"; /// public bool Ready { get; set; } From 428e1afefd8e7e9ace99eef506f009f82bb93213 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 20:11:34 +0200 Subject: [PATCH 119/585] fix warnings --- Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index e3ea74f2d..41a8ba56a 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; + using Dalamud.Game.Gui.PartyFinder.Internal; using Dalamud.Game.Gui.PartyFinder.Types; using Dalamud.Hooking; From ab9b7e1602192b0cfdf394d782dd1bd128ea3f9d Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 20:35:40 +0200 Subject: [PATCH 120/585] chore: add data widget for servicecontainer status, remove serveropcode data widget --- .../Internal/Windows/Data/DataWindow.cs | 2 +- .../Data/Widgets/ServerOpcodeWidget.cs | 40 ------------- .../Windows/Data/Widgets/ServicesWidget.cs | 59 +++++++++++++++++++ Dalamud/IoC/Internal/ServiceContainer.cs | 10 ++++ 4 files changed, 70 insertions(+), 41 deletions(-) delete mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 4c446cacd..1363c6abe 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -19,7 +19,7 @@ internal class DataWindow : Window { private readonly IDataWindowWidget[] modules = { - new ServerOpcodeWidget(), + new ServicesWidget(), new AddressesWidget(), new ObjectTableWidget(), new FateTableWidget(), diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs deleted file mode 100644 index b23f3961e..000000000 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServerOpcodeWidget.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Dalamud.Data; -using ImGuiNET; -using Newtonsoft.Json; - -namespace Dalamud.Interface.Internal.Windows.Data.Widgets; - -/// -/// Widget to display the currently set server opcodes. -/// -internal class ServerOpcodeWidget : IDataWindowWidget -{ - private string? serverOpString; - - /// - public string[]? CommandShortcuts { get; init; } = { "opcode", "serveropcode" }; - - /// - public string DisplayName { get; init; } = "Server Opcode"; - - /// - public bool Ready { get; set; } - - /// - public void Load() - { - var dataManager = Service.Get(); - - if (dataManager.IsDataReady) - { - this.serverOpString = JsonConvert.SerializeObject(dataManager.ServerOpCodes, Formatting.Indented); - this.Ready = true; - } - } - - /// - public void Draw() - { - ImGui.TextUnformatted(this.serverOpString ?? "serverOpString not initialized"); - } -} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs new file mode 100644 index 000000000..49f3c1b90 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -0,0 +1,59 @@ +using System.Linq; + +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.IoC.Internal; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget for displaying start info. +/// +internal class ServicesWidget : IDataWindowWidget +{ + /// + public string[]? CommandShortcuts { get; init; } = { "services" }; + + /// + public string DisplayName { get; init; } = "Service Container"; + + /// + public bool Ready { get; set; } + + /// + public void Load() + { + this.Ready = true; + } + + /// + public void Draw() + { + var container = Service.Get(); + + foreach (var instance in container.Instances) + { + var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key); + var isPublic = instance.Key.IsPublic; + + ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})"); + + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) + { + ImGui.Text(hasInterface + ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" + : "\t => NO INTERFACE!!!"); + } + + if (isPublic) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.Text("\t => PUBLIC!!!"); + } + + ImGuiHelpers.ScaledDummy(2); + } + } +} diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index db748303e..a82440029 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -29,6 +29,16 @@ internal class ServiceContainer : IServiceProvider, IServiceType public ServiceContainer() { } + + /// + /// Gets a dictionary of all registered instances. + /// + public IReadOnlyDictionary Instances => this.instances; + + /// + /// Gets a dictionary mapping interfaces to their implementations. + /// + public IReadOnlyDictionary InterfaceToTypeMap => this.interfaceToTypeMap; /// /// Register a singleton object of any type into the current IOC container. From 5809cf5d7c30926f57e16c17466169897919d945 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 21:09:00 +0200 Subject: [PATCH 121/585] chore: make all services with interfaces internal --- Dalamud/Data/DataManager.cs | 2 +- Dalamud/Game/BaseAddressResolver.cs | 2 +- .../Game/ClientState/Aetherytes/AetheryteList.cs | 4 ++-- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 4 ++-- Dalamud/Game/ClientState/ClientState.cs | 5 ++--- .../ClientState/ClientStateAddressResolver.cs | 2 +- Dalamud/Game/ClientState/Conditions/Condition.cs | 4 +--- Dalamud/Game/ClientState/Fates/FateTable.cs | 4 ++-- Dalamud/Game/ClientState/GamePad/GamepadState.cs | 2 +- Dalamud/Game/ClientState/JobGauge/JobGauges.cs | 2 +- Dalamud/Game/ClientState/Keys/KeyState.cs | 2 +- Dalamud/Game/ClientState/Objects/ObjectTable.cs | 4 ++-- Dalamud/Game/ClientState/Objects/TargetManager.cs | 2 +- Dalamud/Game/ClientState/Party/PartyList.cs | 4 ++-- Dalamud/Game/Command/CommandManager.cs | 2 +- Dalamud/Game/Config/GameConfig.cs | 2 +- Dalamud/Game/Config/GameConfigAddressResolver.cs | 2 +- Dalamud/Game/DutyState/DutyState.cs | 2 +- .../Game/DutyState/DutyStateAddressResolver.cs | 2 +- Dalamud/Game/Framework.cs | 15 +++++++-------- Dalamud/Game/FrameworkAddressResolver.cs | 2 +- Dalamud/Game/GameLifecycle.cs | 2 +- Dalamud/Game/Gui/ChatGuiAddressResolver.cs | 4 +--- Dalamud/Game/Gui/Dtr/DtrBar.cs | 7 +++---- .../Game/Gui/FlyText/FlyTextGuiAddressResolver.cs | 4 +--- .../Gui/PartyFinder/PartyFinderAddressResolver.cs | 4 +--- .../Gui/PartyFinder/Types/JobFlagsExtensions.cs | 6 +++--- Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs | 4 +--- .../Game/Internal/DXGI/SwapChainSigResolver.cs | 3 +-- .../Game/Internal/DXGI/SwapChainVtableResolver.cs | 3 +-- Dalamud/Game/Libc/LibcFunction.cs | 2 +- Dalamud/Game/Libc/LibcFunctionAddressResolver.cs | 2 +- .../Game/Network/GameNetworkAddressResolver.cs | 4 +--- Dalamud/Game/SigScanner.cs | 2 +- Dalamud/Game/Text/SeStringHandling/Payload.cs | 14 ++++++++------ Dalamud/Interface/Internal/TextureManager.cs | 2 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 3 ++- Dalamud/Logging/Internal/TaskTracker.cs | 4 ++-- .../Internal/Profiles/ProfileCommandHandler.cs | 6 +++--- Dalamud/Plugin/Services/IFramework.cs | 7 +------ 40 files changed, 67 insertions(+), 86 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index fb167283f..809726684 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -26,7 +26,7 @@ namespace Dalamud.Data; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class DataManager : IDisposable, IServiceType, IDataManager +internal sealed class DataManager : IDisposable, IServiceType, IDataManager { private readonly Thread luminaResourceThread; private readonly CancellationTokenSource luminaCancellationTokenSource; diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index 24e7dffe8..9935aac7b 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game; /// /// Base memory address resolver. /// -public abstract class BaseAddressResolver +internal abstract class BaseAddressResolver { /// /// Gets a list of memory addresses that were found, to list in /xldata. diff --git a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs index 17b468d70..e6af6e1df 100644 --- a/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs +++ b/Dalamud/Game/ClientState/Aetherytes/AetheryteList.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.ClientState.Aetherytes; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList +internal sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList { [ServiceManager.ServiceDependency] private readonly ClientState clientState = Service.Get(); @@ -78,7 +78,7 @@ public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList /// /// This collection represents the list of available Aetherytes in the Teleport window. /// -public sealed partial class AetheryteList +internal sealed partial class AetheryteList { /// public int Count => this.Length; diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index dc2cb9fae..489e75bc3 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -20,7 +20,7 @@ namespace Dalamud.Game.ClientState.Buddy; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed partial class BuddyList : IServiceType, IBuddyList +internal sealed partial class BuddyList : IServiceType, IBuddyList { private const uint InvalidObjectID = 0xE0000000; @@ -147,7 +147,7 @@ public sealed partial class BuddyList : IServiceType, IBuddyList /// /// This collection represents the buddies present in your squadron or trust party. /// -public sealed partial class BuddyList +internal sealed partial class BuddyList { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 6817523af..cef802c81 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.InteropServices; using Dalamud.Data; @@ -25,7 +24,7 @@ namespace Dalamud.Game.ClientState; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class ClientState : IDisposable, IServiceType, IClientState +internal sealed class ClientState : IDisposable, IServiceType, IClientState { private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; @@ -141,7 +140,7 @@ public sealed class ClientState : IDisposable, IServiceType, IClientState this.CfPop?.InvokeSafely(this, e); } - private void FrameworkOnOnUpdateEvent(Framework framework1) + private void FrameworkOnOnUpdateEvent(IFramework framework1) { var condition = Service.GetNullable(); var gameGui = Service.GetNullable(); diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 369e620be..305dda454 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.ClientState; /// /// Client state memory address resolver. /// -public sealed class ClientStateAddressResolver : BaseAddressResolver +internal sealed class ClientStateAddressResolver : BaseAddressResolver { // Static offsets diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 585b762bf..0f8523e9b 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -92,7 +90,7 @@ internal sealed partial class Condition : IServiceType, ICondition framework.Update += this.FrameworkUpdate; } - private void FrameworkUpdate(Framework framework) + private void FrameworkUpdate(IFramework framework) { for (var i = 0; i < MaxConditionEntries; i++) { diff --git a/Dalamud/Game/ClientState/Fates/FateTable.cs b/Dalamud/Game/ClientState/Fates/FateTable.cs index 53196d5df..e9400842f 100644 --- a/Dalamud/Game/ClientState/Fates/FateTable.cs +++ b/Dalamud/Game/ClientState/Fates/FateTable.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.ClientState.Fates; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed partial class FateTable : IServiceType, IFateTable +internal sealed partial class FateTable : IServiceType, IFateTable { private readonly ClientStateAddressResolver address; @@ -110,7 +110,7 @@ public sealed partial class FateTable : IServiceType, IFateTable /// /// This collection represents the currently available Fate events. /// -public sealed partial class FateTable +internal sealed partial class FateTable { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index bc5744047..8acb6ada5 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.GamePad; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public unsafe class GamepadState : IDisposable, IServiceType, IGamepadState +internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState { private readonly Hook? gamepadPoll; diff --git a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs index 683f5c61f..74e22ddbe 100644 --- a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs +++ b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.ClientState.JobGauge; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class JobGauges : IServiceType, IJobGauges +internal class JobGauges : IServiceType, IJobGauges { private Dictionary cache = new(); diff --git a/Dalamud/Game/ClientState/Keys/KeyState.cs b/Dalamud/Game/ClientState/Keys/KeyState.cs index ba5cd06d9..03c5d59b9 100644 --- a/Dalamud/Game/ClientState/Keys/KeyState.cs +++ b/Dalamud/Game/ClientState/Keys/KeyState.cs @@ -28,7 +28,7 @@ namespace Dalamud.Game.ClientState.Keys; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class KeyState : IServiceType, IKeyState +internal class KeyState : IServiceType, IKeyState { // The array is accessed in a way that this limit doesn't appear to exist // but there is other state data past this point, and keys beyond here aren't diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index 16cf7c277..c6320ccbb 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.Objects; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed partial class ObjectTable : IServiceType, IObjectTable +internal sealed partial class ObjectTable : IServiceType, IObjectTable { private const int ObjectTableLength = 596; @@ -109,7 +109,7 @@ public sealed partial class ObjectTable : IServiceType, IObjectTable /// /// This collection represents the currently spawned FFXIV game objects. /// -public sealed partial class ObjectTable +internal sealed partial class ObjectTable { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index 00bcaac7d..a821ba806 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -16,7 +16,7 @@ namespace Dalamud.Game.ClientState.Objects; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed unsafe class TargetManager : IServiceType, ITargetManager +internal sealed unsafe class TargetManager : IServiceType, ITargetManager { [ServiceManager.ServiceDependency] private readonly ClientState clientState = Service.Get(); diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 529b57b6f..946c73245 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.ClientState.Party; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed unsafe partial class PartyList : IServiceType, IPartyList +internal sealed unsafe partial class PartyList : IServiceType, IPartyList { private const int GroupLength = 8; private const int AllianceLength = 20; @@ -130,7 +130,7 @@ public sealed unsafe partial class PartyList : IServiceType, IPartyList /// /// This collection represents the party members present in your party or alliance. /// -public sealed partial class PartyList +internal sealed partial class PartyList { /// int IReadOnlyCollection.Count => this.Length; diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 63a1a3d09..6a8651b41 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game.Command; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class CommandManager : IServiceType, IDisposable, ICommandManager +internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager { private readonly ConcurrentDictionary commandMap = new(); private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled); diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 49d24c2a5..b77b9c4af 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -17,7 +17,7 @@ namespace Dalamud.Game.Config; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class GameConfig : IServiceType, IGameConfig, IDisposable +internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable { private readonly GameConfigAddressResolver address = new(); private Hook? configChangeHook; diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs index 6a207807a..674ee4764 100644 --- a/Dalamud/Game/Config/GameConfigAddressResolver.cs +++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs @@ -3,7 +3,7 @@ /// /// Game config system address resolver. /// -public sealed class GameConfigAddressResolver : BaseAddressResolver +internal sealed class GameConfigAddressResolver : BaseAddressResolver { /// /// Gets the address of the method called when any config option is changed. diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 2f117a492..34940dee0 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -135,7 +135,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState /// Joining a duty in progress, or disconnecting and reconnecting will cause the player to miss the event. /// /// Framework reference. - private void FrameworkOnUpdateEvent(Framework framework1) + private void FrameworkOnUpdateEvent(IFramework framework1) { // If the duty hasn't been started, and has not been completed yet this territory if (!this.IsDutyStarted && !this.CompletedThisTerritory) diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs index 436883dc2..772af79a8 100644 --- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs +++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs @@ -3,7 +3,7 @@ namespace Dalamud.Game.DutyState; /// /// Duty state memory address resolver. /// -public class DutyStateAddressResolver : BaseAddressResolver +internal class DutyStateAddressResolver : BaseAddressResolver { /// /// Gets the address of the method which is called when the client receives a content director update. diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 2b77bf400..08b97edbc 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -27,7 +27,7 @@ namespace Dalamud.Game; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class Framework : IDisposable, IServiceType, IFramework +internal sealed class Framework : IDisposable, IServiceType, IFramework { private static readonly Stopwatch StatsStopwatch = new(); @@ -39,6 +39,8 @@ public sealed class Framework : IDisposable, IServiceType, IFramework private readonly Hook updateHook; private readonly Hook destroyHook; + private readonly FrameworkAddressResolver addressResolver; + [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); @@ -54,11 +56,11 @@ public sealed class Framework : IDisposable, IServiceType, IFramework this.lifecycle = lifecycle; this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch); - this.Address = new FrameworkAddressResolver(); - this.Address.Setup(sigScanner); + this.addressResolver = new FrameworkAddressResolver(); + this.addressResolver.Setup(sigScanner); - this.updateHook = Hook.FromAddress(this.Address.TickAddress, this.HandleFrameworkUpdate); - this.destroyHook = Hook.FromAddress(this.Address.DestroyAddress, this.HandleFrameworkDestroy); + this.updateHook = Hook.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate); + this.destroyHook = Hook.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy); } /// @@ -92,9 +94,6 @@ public sealed class Framework : IDisposable, IServiceType, IFramework /// public static Dictionary> StatsHistory { get; } = new(); - /// - public FrameworkAddressResolver Address { get; } - /// public DateTime LastUpdate { get; private set; } = DateTime.MinValue; diff --git a/Dalamud/Game/FrameworkAddressResolver.cs b/Dalamud/Game/FrameworkAddressResolver.cs index 36915d7a9..c47469a01 100644 --- a/Dalamud/Game/FrameworkAddressResolver.cs +++ b/Dalamud/Game/FrameworkAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game; /// /// The address resolver for the class. /// -public sealed class FrameworkAddressResolver : BaseAddressResolver +internal sealed class FrameworkAddressResolver : BaseAddressResolver { /// /// Gets the address for the function that is called once the Framework is destroyed. diff --git a/Dalamud/Game/GameLifecycle.cs b/Dalamud/Game/GameLifecycle.cs index 5c1acc989..4192d055b 100644 --- a/Dalamud/Game/GameLifecycle.cs +++ b/Dalamud/Game/GameLifecycle.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class GameLifecycle : IServiceType, IGameLifecycle +internal class GameLifecycle : IServiceType, IGameLifecycle { private readonly CancellationTokenSource dalamudUnloadCts = new(); private readonly CancellationTokenSource gameShutdownCts = new(); diff --git a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs index 4686d5725..494e0b3ed 100644 --- a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Gui; /// /// The address resolver for the class. /// -public sealed class ChatGuiAddressResolver : BaseAddressResolver +internal sealed class ChatGuiAddressResolver : BaseAddressResolver { /// /// Gets the address of the native PrintMessage method. diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index dd1e7aa30..c126825d5 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; @@ -22,7 +21,7 @@ namespace Dalamud.Game.Gui.Dtr; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar +internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; @@ -133,7 +132,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); - private void Update(Framework unused) + private void Update(IFramework unused) { this.HandleRemovedNodes(); diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs b/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs index 588177032..677d92e57 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Gui.FlyText; /// /// An address resolver for the class. /// -public class FlyTextGuiAddressResolver : BaseAddressResolver +internal class FlyTextGuiAddressResolver : BaseAddressResolver { /// /// Gets the address of the native AddFlyText method, which occurs diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs index aa9d28cb1..c12721358 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Gui.PartyFinder; /// /// The address resolver for the class. /// -public class PartyFinderAddressResolver : BaseAddressResolver +internal class PartyFinderAddressResolver : BaseAddressResolver { /// /// Gets the address of the native ReceiveListing method. diff --git a/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs b/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs index c7630acfa..46e83b972 100644 --- a/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs +++ b/Dalamud/Game/Gui/PartyFinder/Types/JobFlagsExtensions.cs @@ -1,4 +1,4 @@ -using Dalamud.Data; +using Dalamud.Plugin.Services; using Lumina.Excel.GeneratedSheets; namespace Dalamud.Game.Gui.PartyFinder.Types; @@ -14,7 +14,7 @@ public static class JobFlagsExtensions /// A JobFlags enum member. /// A DataManager to get the ClassJob from. /// A ClassJob if found or null if not. - public static ClassJob ClassJob(this JobFlags job, DataManager data) + public static ClassJob? ClassJob(this JobFlags job, IDataManager data) { var jobs = data.GetExcelSheet(); @@ -52,6 +52,6 @@ public static class JobFlagsExtensions _ => null, }; - return row == null ? null : jobs.GetRow((uint)row); + return row == null ? null : jobs?.GetRow((uint)row); } } diff --git a/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs b/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs index 4f935b465..ae5426023 100644 --- a/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Gui.Toast; /// /// An address resolver for the class. /// -public class ToastGuiAddressResolver : BaseAddressResolver +internal class ToastGuiAddressResolver : BaseAddressResolver { /// /// Gets the address of the native ShowNormalToast method. diff --git a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs index ad79dff9f..a2fc08646 100644 --- a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs +++ b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics; using System.Linq; @@ -10,7 +9,7 @@ namespace Dalamud.Game.Internal.DXGI; /// The address resolver for native D3D11 methods to facilitate displaying the Dalamud UI. /// [Obsolete("This has been deprecated in favor of the VTable resolver.")] -public sealed class SwapChainSigResolver : BaseAddressResolver, ISwapChainAddressResolver +internal sealed class SwapChainSigResolver : BaseAddressResolver, ISwapChainAddressResolver { /// public IntPtr Present { get; set; } diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs index 603324175..50aae26ed 100644 --- a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs +++ b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; @@ -15,7 +14,7 @@ namespace Dalamud.Game.Internal.DXGI; /// /// If the normal signature based method of resolution fails, this is the backup. /// -public class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver +internal class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressResolver { /// public IntPtr Present { get; set; } diff --git a/Dalamud/Game/Libc/LibcFunction.cs b/Dalamud/Game/Libc/LibcFunction.cs index 7dfc26b3b..b0bd4950c 100644 --- a/Dalamud/Game/Libc/LibcFunction.cs +++ b/Dalamud/Game/Libc/LibcFunction.cs @@ -17,7 +17,7 @@ namespace Dalamud.Game.Libc; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public sealed class LibcFunction : IServiceType, ILibcFunction +internal sealed class LibcFunction : IServiceType, ILibcFunction { private readonly LibcFunctionAddressResolver address; private readonly StdStringFromCStringDelegate stdStringCtorCString; diff --git a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs index 89b721a87..4c3b7cdf8 100644 --- a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs +++ b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs @@ -5,7 +5,7 @@ namespace Dalamud.Game.Libc; /// /// The address resolver for the class. /// -public sealed class LibcFunctionAddressResolver : BaseAddressResolver +internal sealed class LibcFunctionAddressResolver : BaseAddressResolver { private delegate IntPtr StringFromCString(); diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs index c698ee813..fa6af8c93 100644 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ b/Dalamud/Game/Network/GameNetworkAddressResolver.cs @@ -1,11 +1,9 @@ -using System; - namespace Dalamud.Game.Network; /// /// The address resolver for the class. /// -public sealed class GameNetworkAddressResolver : BaseAddressResolver +internal sealed class GameNetworkAddressResolver : BaseAddressResolver { /// /// Gets the address of the ProcessZonePacketDown method. diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index b5fe0b5b3..ace4654be 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -25,7 +25,7 @@ namespace Dalamud.Game; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class SigScanner : IDisposable, IServiceType, ISigScanner +internal class SigScanner : IDisposable, IServiceType, ISigScanner { private readonly FileInfo? cacheFile; diff --git a/Dalamud/Game/Text/SeStringHandling/Payload.cs b/Dalamud/Game/Text/SeStringHandling/Payload.cs index 117606a7a..ff7332f12 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payload.cs @@ -5,6 +5,7 @@ using System.IO; using Dalamud.Data; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Plugin.Services; using Newtonsoft.Json; using Serilog; @@ -27,12 +28,6 @@ public abstract partial class Payload // To force-invalidate it, Dirty can be set to true private byte[] encodedData; - /// - /// Gets the Lumina instance to use for any necessary data lookups. - /// - [JsonIgnore] - public DataManager DataResolver => Service.Get(); - /// /// Gets the type of this payload. /// @@ -43,6 +38,13 @@ public abstract partial class Payload /// public bool Dirty { get; protected set; } = true; + /// + /// Gets the Lumina instance to use for any necessary data lookups. + /// + [JsonIgnore] + // TODO: We should refactor this. It should not be possible to get IDataManager through here. + protected IDataManager DataResolver => Service.Get(); + /// /// Decodes a binary representation of a payload into its corresponding nice object payload. /// diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index b397182ef..78af0ebb7 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -442,7 +442,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP } } - private void FrameworkOnUpdate(Framework fw) + private void FrameworkOnUpdate(IFramework fw) { lock (this.activeTextures) { diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index e3cf78296..20d260704 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Services; using ImGuiNET; using ImGuiScene; @@ -358,7 +359,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable return isHover; } - private void FrameworkOnUpdate(Framework framework) + private void FrameworkOnUpdate(IFramework framework) { var clientState = Service.Get(); this.IsOpen = !clientState.IsLoggedIn; diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index a8729893f..b65f0efa7 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -6,6 +5,7 @@ using System.Reflection; using System.Threading.Tasks; using Dalamud.Game; +using Dalamud.Plugin.Services; namespace Dalamud.Logging.Internal; @@ -141,7 +141,7 @@ internal class TaskTracker : IDisposable, IServiceType return true; } - private void FrameworkOnUpdate(Framework framework) + private void FrameworkOnUpdate(IFramework framework) { UpdateData(); } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs index 8ea55856c..7001e4d7b 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -7,6 +6,7 @@ using CheapLoc; using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Game.Gui; +using Dalamud.Plugin.Services; using Dalamud.Utility; using Serilog; @@ -78,7 +78,7 @@ internal class ProfileCommandHandler : IServiceType, IDisposable this.framework.Update += this.FrameworkOnUpdate; } - private void FrameworkOnUpdate(Framework framework1) + private void FrameworkOnUpdate(IFramework framework1) { if (this.profileManager.IsBusy) return; diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs index 69c21bca4..334577b92 100644 --- a/Dalamud/Plugin/Services/IFramework.cs +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -15,18 +15,13 @@ public interface IFramework /// A delegate type used with the event. /// /// The Framework instance. - public delegate void OnUpdateDelegate(Framework framework); + public delegate void OnUpdateDelegate(IFramework framework); /// /// Event that gets fired every time the game framework updates. /// public event OnUpdateDelegate Update; - /// - /// Gets a raw pointer to the instance of Client::Framework. - /// - public FrameworkAddressResolver Address { get; } - /// /// Gets the last time that the Framework Update event was triggered. /// From 00fa1dc4f83beea0373aa15ddcaef16a9bf88547 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 23:02:42 +0200 Subject: [PATCH 122/585] chore: remove ChatHandlers public service --- Dalamud/Game/ChatHandlers.cs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 1d82e5f9c..896d296fc 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -25,10 +25,8 @@ namespace Dalamud.Game; /// /// Chat events and public helper functions. /// -[PluginInterface] -[InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -public class ChatHandlers : IServiceType +internal class ChatHandlers : IServiceType { // private static readonly Dictionary UnicodeToDiscordEmojiDict = new() // { @@ -134,22 +132,6 @@ public class ChatHandlers : IServiceType /// public bool IsAutoUpdateComplete { get; private set; } - /// - /// Convert a TextPayload to SeString and wrap in italics payloads. - /// - /// Text to convert. - /// SeString payload of italicized text. - public static SeString MakeItalics(string text) - => MakeItalics(new TextPayload(text)); - - /// - /// Convert a TextPayload to SeString and wrap in italics payloads. - /// - /// Text to convert. - /// SeString payload of italicized text. - public static SeString MakeItalics(TextPayload text) - => new(EmphasisItalicPayload.ItalicsOn, text, EmphasisItalicPayload.ItalicsOff); - private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) { var textVal = message.TextValue; From 3d94d07f56b0d959b10ad1f5ab9c025e1ade8c69 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 23:13:16 +0200 Subject: [PATCH 123/585] chore: remove opcodes from public API --- Dalamud/Plugin/Services/IDataManager.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index 3ae10b0c7..4977b65b3 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -19,17 +19,7 @@ public interface IDataManager /// Gets the current game client language. /// public ClientLanguage Language { get; } - - /// - /// Gets the OpCodes sent by the server to the client. - /// - public ReadOnlyDictionary ServerOpCodes { get; } - - /// - /// Gets the OpCodes sent by the client to the server. - /// - public ReadOnlyDictionary ClientOpCodes { get; } - + /// /// Gets a object which gives access to any excel/game data. /// From 9d4a2fad3bd1fff50cd1bfd6d7f44fdb13df0695 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 17 Sep 2023 23:39:09 +0200 Subject: [PATCH 124/585] fix dtr errors after merge --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 3 ++- Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 4e9584f27..42f06443f 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; diff --git a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs index 1e6fd09cd..744d926f0 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs @@ -3,7 +3,7 @@ /// /// DtrBar memory address resolver. /// -public class DtrBarAddressResolver : BaseAddressResolver +internal class DtrBarAddressResolver : BaseAddressResolver { /// /// Gets the address of the AtkUnitBaseDraw method. From 4989e2b69b8ce23dbe01b8a6786267e6a0ed6ea2 Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Mon, 18 Sep 2023 03:55:43 +0200 Subject: [PATCH 125/585] refactor: rename fly text kind members --- Dalamud/Game/Gui/FlyText/FlyTextKind.cs | 144 +++++++++++++----------- 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs index 68650fb5c..3727fd0f8 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs @@ -1,57 +1,58 @@ namespace Dalamud.Game.Gui.FlyText; /// -/// Enum of FlyTextKind values. Members suffixed with -/// a number seem to be a duplicate, or perform duplicate behavior. +/// Enum of FlyTextKind values. /// public enum FlyTextKind : int { /// /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// Used for autos and incoming DoTs. /// - AutoAttack = 0, + AutoAttackOrDot = 0, /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// Does a bounce effect on appearance. /// - DirectHit = 1, + AutoAttackOrDotDh = 1, /// /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. /// Does a bigger bounce effect on appearance. /// - CriticalHit = 2, + AutoAttackOrDotCrit = 2, /// - /// Val1 in even larger serif font with 2 exclamations, Text2 in - /// sans-serif as subtitle. Does a large bounce effect on appearance. - /// Does not scroll up or down the screen. + /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle. + /// Does a large bounce effect on appearance. Does not scroll up or down the screen. /// - CriticalDirectHit = 3, + AutoAttackOrDotCritDh = 3, /// - /// AutoAttack with sans-serif Text1 to the left of the Val1. + /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. /// - NamedAttack = 4, + Damage = 4, /// - /// DirectHit with sans-serif Text1 to the left of the Val1. + /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. + /// Does a bounce effect on appearance. /// - NamedDirectHit = 5, + DamageDh = 5, /// - /// CriticalHit with sans-serif Text1 to the left of the Val1. + /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. + /// Does a bigger bounce effect on appearance. /// - NamedCriticalHit = 6, + DamageCrit = 6, /// - /// CriticalDirectHit with sans-serif Text1 to the left of the Val1. + /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. + /// Does a large bounce effect on appearance. Does not scroll up or down the screen. /// - NamedCriticalDirectHit = 7, + DamageCritDh = 7, /// + /// The text changes to DODGE under certain circumstances. /// All caps, serif MISS. /// Miss = 8, @@ -74,12 +75,12 @@ public enum FlyTextKind : int /// /// Icon next to sans-serif Text1. /// - NamedIcon = 12, + Buff = 12, /// - /// Icon next to sans-serif Text1 (2). + /// Icon next to sans-serif Text1. /// - NamedIcon2 = 13, + Debuff = 13, /// /// Serif Val1 with all caps condensed font EXP with Text2 in sans-serif as subtitle. @@ -94,42 +95,44 @@ public enum FlyTextKind : int /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// - NamedMp = 16, + MpDrain = 16, /// + /// Currently not used by the game. /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// NamedTp = 17, /// - /// AutoAttack with sans-serif Text1 to the left of the Val1 (2). + /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. /// - NamedAttack2 = 18, + Healing = 18, /// - /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (2). + /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// - NamedMp2 = 19, + MpRegen = 19, /// - /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (2). + /// Currently not used by the game. + /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// NamedTp2 = 20, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font EP with Text2 in sans-serif as subtitle. /// - NamedEp = 21, + EpRegen = 21, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font CP with Text2 in sans-serif as subtitle. /// - NamedCp = 22, + CpRegen = 22, /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font GP with Text2 in sans-serif as subtitle. /// - NamedGp = 23, + GpRegen = 23, /// /// Displays nothing. @@ -149,57 +152,59 @@ public enum FlyTextKind : int Interrupted = 26, /// - /// AutoAttack with no Text2. + /// Val1 in serif font. /// - AutoAttackNoText = 27, + CraftingProgress = 27, /// - /// AutoAttack with no Text2 (2). + /// Val1 in serif font. /// - AutoAttackNoText2 = 28, + CraftingQuality = 28, /// - /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance (2). + /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. Does a bigger bounce effect on appearance. /// - CriticalHit2 = 29, + CraftingQualityCrit = 29, /// - /// AutoAttack with no Text2 (3). + /// Currently not used by the game. + /// Val1 in serif font. /// AutoAttackNoText3 = 30, /// /// CriticalHit with sans-serif Text1 to the left of the Val1 (2). /// - NamedCriticalHit2 = 31, + HealingCrit = 31, /// - /// Same as NamedCriticalHit with a green (cannot change) MP in condensed font to the right of Val1. + /// Currently not used by the game. + /// Same as DamageCrit with a MP in condensed font to the right of Val1. /// Does a jiggle effect to the right on appearance. /// NamedCriticalHitWithMp = 32, /// - /// Same as NamedCriticalHit with a yellow (cannot change) TP in condensed font to the right of Val1. + /// Currently not used by the game. + /// Same as DamageCrit with a TP in condensed font to the right of Val1. /// Does a jiggle effect to the right on appearance. /// NamedCriticalHitWithTp = 33, /// - /// Same as NamedIcon with sans-serif "has no effect!" to the right. + /// Icon next to sans-serif Text1 with sans-serif "has no effect!" to the right. /// - NamedIconHasNoEffect = 34, + DebuffNoEffect = 34, /// - /// Same as NamedIcon but Text1 is slightly faded. Used for buff expiration. + /// Icon next to sans-serif slightly faded Text1. /// - NamedIconFaded = 35, + BuffFading = 35, /// - /// Same as NamedIcon but Text1 is slightly faded (2). - /// Used for buff expiration. + /// Icon next to sans-serif slightly faded Text1. /// - NamedIconFaded2 = 36, + DebuffFading = 36, /// /// Text1 in sans-serif font. @@ -207,9 +212,9 @@ public enum FlyTextKind : int Named = 37, /// - /// Same as NamedIcon with sans-serif "(fully resisted)" to the right. + /// Icon next to sans-serif Text1 with sans-serif "(fully resisted)" to the right. /// - NamedIconFullyResisted = 38, + DebuffResisted = 38, /// /// All caps serif 'INCAPACITATED!'. @@ -219,32 +224,34 @@ public enum FlyTextKind : int /// /// Text1 with sans-serif "(fully resisted)" to the right. /// - NamedFullyResisted = 40, + FullyResisted = 40, /// /// Text1 with sans-serif "has no effect!" to the right. /// - NamedHasNoEffect = 41, + HasNoEffect = 41, /// - /// AutoAttack with sans-serif Text1 to the left of the Val1 (3). + /// Val1 in serif font, Text2 in sans-serif as subtitle with sans-serif Text1 to the left of the Val1. /// - NamedAttack3 = 42, + HpDrain = 42, /// - /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle (3). + /// Currently not used by the game. + /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// NamedMp3 = 43, /// - /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle (3). + /// Currently not used by the game. + /// Sans-serif Text1 next to serif Val1 with all caps condensed font TP with Text2 in sans-serif as subtitle. /// NamedTp3 = 44, /// - /// Same as NamedIcon with serif "INVULNERABLE!" beneath the Text1. + /// Icon next to sans-serif Text1 with serif "INVULNERABLE!" beneath the Text1. /// - NamedIconInvulnerable = 45, + DebuffInvulnerable = 45, /// /// All caps serif RESIST. @@ -252,20 +259,20 @@ public enum FlyTextKind : int Resist = 46, /// - /// Same as NamedIcon but places the given icon in the item icon outline. + /// Icon with an item icon outline next to sans-serif Text1. /// - NamedIconWithItemOutline = 47, + LootedItem = 47, /// - /// AutoAttack with no Text2 (4). + /// Val1 in serif font. /// - AutoAttackNoText4 = 48, + Collectability = 48, /// - /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (3). + /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. /// Does a bigger bounce effect on appearance. /// - CriticalHit3 = 49, + CollectabilityCrit = 49, /// /// All caps serif REFLECT. @@ -278,20 +285,21 @@ public enum FlyTextKind : int Reflected = 51, /// - /// Val1 in serif font, Text2 in sans-serif as subtitle (2). + /// Val1 in serif font, Text2 in sans-serif as subtitle. /// Does a bounce effect on appearance. /// - DirectHit2 = 52, + CraftingQualityDh = 52, /// - /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle (4). + /// Currently not used by the game. + /// Val1 in larger serif font with exclamation, with Text2 in sans-serif as subtitle. /// Does a bigger bounce effect on appearance. /// CriticalHit4 = 53, /// - /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle (2). + /// Val1 in even larger serif font with 2 exclamations, Text2 in sans-serif as subtitle. /// Does a large bounce effect on appearance. Does not scroll up or down the screen. /// - CriticalDirectHit2 = 54, + CraftingQualityCritDh = 54, } From 5a3196e5f89d6675ee91ecbf00e95f7336dd40be Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:21:01 -0700 Subject: [PATCH 126/585] [AddonLifecycle] Fix incorrect delegate signature (#1401) --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 5fc1c7d2b..c416b6d1f 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -63,7 +63,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate void AddonOnRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); - private delegate void AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); + private delegate byte AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); /// public void Dispose() @@ -221,7 +221,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - private void OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) + private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) { try { @@ -232,7 +232,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke."); } - this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values); + var result = this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values); try { @@ -242,6 +242,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); } + + return result; } private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) From af39add38e3f93761f0774fd488245613af0c331 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 18 Sep 2023 09:18:01 +0200 Subject: [PATCH 127/585] [v9] Update ClientStructs and fix build (#1398) * Update ClientStructs * Use IFramework in AddonLifecycle * Fix for ClientStructs breaking changes --- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 4 ++-- Dalamud/Interface/Internal/UiDebug.cs | 10 +++++----- lib/FFXIVClientStructs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 5fc1c7d2b..5a1070dfb 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -97,7 +97,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } // Used to prevent concurrency issues if plugins try to register during iteration of listeners. - private void OnFrameworkUpdate(Framework unused) + private void OnFrameworkUpdate(IFramework unused) { if (this.newEventListeners.Any()) { diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index b1f27828c..524759f4a 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -1,5 +1,6 @@ using System; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.Game; @@ -416,7 +417,7 @@ internal unsafe class UiDebug $"MultiplyRGB: {node->MultiplyRed} {node->MultiplyGreen} {node->MultiplyBlue}"); } - private bool DrawUnitListHeader(int index, uint count, ulong ptr, bool highlight) + private bool DrawUnitListHeader(int index, ushort count, ulong ptr, bool highlight) { ImGui.PushStyleColor(ImGuiCol.Text, highlight ? 0xFFAAAA00 : 0xFFFFFFFF); if (!string.IsNullOrEmpty(this.searchInput) && !this.doingSearch) @@ -455,8 +456,6 @@ internal unsafe class UiDebug this.selectedInList[i] = false; var unitManager = &unitManagers[i]; - var unitBaseArray = &unitManager->AtkUnitEntries; - var headerOpen = true; if (!searching) @@ -468,7 +467,7 @@ internal unsafe class UiDebug for (var j = 0; j < unitManager->Count && headerOpen; j++) { - var unitBase = unitBaseArray[j]; + var unitBase = *(AtkUnitBase**)Unsafe.AsPointer(ref unitManager->EntriesSpan[j]); if (this.selectedUnitBase != null && unitBase == this.selectedUnitBase) { this.selectedInList[i] = true; @@ -513,7 +512,8 @@ internal unsafe class UiDebug { for (var j = 0; j < unitManager->Count; j++) { - if (this.selectedUnitBase == null || unitBaseArray[j] != this.selectedUnitBase) continue; + var unitBase = *(AtkUnitBase**)Unsafe.AsPointer(ref unitManager->EntriesSpan[j]); + if (this.selectedUnitBase == null || unitBase != this.selectedUnitBase) continue; this.selectedInList[i] = true; foundSelected = true; } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 06e3ca233..fd5ba8a27 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 06e3ca2336031ba86ef95d022a2af722e5d00a7e +Subproject commit fd5ba8a27ec911a69eeb93ceb0202091279dfceb From 7a0f58a1063adef5223ca5e8a364cffdfd45d19e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 00:19:31 -0700 Subject: [PATCH 128/585] Remove PluginInterface from base Dtr service (#1399) --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 42f06443f..5467e207f 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -22,12 +22,8 @@ namespace Dalamud.Game.Gui.Dtr; /// /// Class used to interface with the server info bar. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; From 3e8be33a5cfb916bf28633521fe228b6e570a609 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:45:42 -0700 Subject: [PATCH 129/585] Add CommandManagerPluginScoped --- Dalamud/Game/Command/CommandManager.cs | 76 ++++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 6a8651b41..56630a5ad 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -17,12 +16,8 @@ namespace Dalamud.Game.Command; /// /// This class manages registered in-game slash commands. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager { private readonly ConcurrentDictionary commandMap = new(); @@ -84,7 +79,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage // => command: 0-12 (12 chars) // => argument: 13-17 (4 chars) // => content.IndexOf(' ') == 12 - command = content.Substring(0, separatorPosition); + command = content[..separatorPosition]; var argStart = separatorPosition + 1; argument = content[argStart..]; @@ -162,3 +157,72 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage } } } + +/// +/// Plugin-scoped version of a AddonLifecycle service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandManager +{ + [ServiceManager.ServiceDependency] + private readonly CommandManager commandManagerService = Service.Get(); + + private readonly List pluginRegisteredCommands = new(); + + /// + public ReadOnlyDictionary Commands => this.commandManagerService.Commands; + + /// + public void Dispose() + { + foreach (var command in this.pluginRegisteredCommands) + { + this.commandManagerService.RemoveHandler(command); + } + + this.pluginRegisteredCommands.Clear(); + } + + /// + public bool ProcessCommand(string content) + => this.commandManagerService.ProcessCommand(content); + + /// + public void DispatchCommand(string command, string argument, CommandInfo info) + => this.commandManagerService.DispatchCommand(command, argument, info); + + /// + public bool AddHandler(string command, CommandInfo info) + { + if (!this.pluginRegisteredCommands.Contains(command)) + { + if (this.commandManagerService.AddHandler(command, info)) + { + this.pluginRegisteredCommands.Add(command); + return true; + } + } + + return false; + } + + /// + public bool RemoveHandler(string command) + { + if (this.pluginRegisteredCommands.Contains(command)) + { + if (this.commandManagerService.RemoveHandler(command)) + { + this.pluginRegisteredCommands.Remove(command); + return true; + } + } + + return false; + } +} From 34617cf377b7805b0fa60296fcfccb3af73c6ca1 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:50:05 -0700 Subject: [PATCH 130/585] Add error logging --- Dalamud/Game/Command/CommandManager.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 56630a5ad..118d210e2 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -8,8 +8,8 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using Serilog; namespace Dalamud.Game.Command; @@ -20,6 +20,8 @@ namespace Dalamud.Game.Command; [ServiceManager.BlockingEarlyLoadedService] internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager { + private static readonly ModuleLog Log = new("Command"); + private readonly ConcurrentDictionary commandMap = new(); private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled); private readonly Regex commandRegexJp = new(@"^そのコマンドはありません。: (?.+)$", RegexOptions.Compiled); @@ -169,6 +171,8 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage #pragma warning restore SA1015 internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandManager { + private static readonly ModuleLog Log = new("Command"); + [ServiceManager.ServiceDependency] private readonly CommandManager commandManagerService = Service.Get(); @@ -207,6 +211,10 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM return true; } } + else + { + Log.Error($"Command {command} is already registered."); + } return false; } @@ -222,6 +230,10 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM return true; } } + else + { + Log.Error($"Command {command} not found."); + } return false; } From 5d0694918572281ba0ac2a00915691406bf5f2ef Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 12:00:49 -0700 Subject: [PATCH 131/585] Set CommandInfo InternalName when adding to CommandManager via PluginScopedService --- Dalamud/Game/Command/CommandInfo.cs | 1 - Dalamud/Game/Command/CommandManager.cs | 12 ++++++++++++ .../Windows/PluginInstaller/PluginInstallerWindow.cs | 4 +--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Command/CommandInfo.cs b/Dalamud/Game/Command/CommandInfo.cs index 9b559599a..bc0250a66 100644 --- a/Dalamud/Game/Command/CommandInfo.cs +++ b/Dalamud/Game/Command/CommandInfo.cs @@ -15,7 +15,6 @@ public sealed class CommandInfo public CommandInfo(HandlerDelegate handler) { this.Handler = handler; - this.LoaderAssemblyName = Assembly.GetCallingAssembly()?.GetName()?.Name; } /// diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 118d210e2..218b89676 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -9,6 +9,7 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; namespace Dalamud.Game.Command; @@ -177,7 +178,17 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM private readonly CommandManager commandManagerService = Service.Get(); private readonly List pluginRegisteredCommands = new(); + private readonly LocalPlugin pluginInfo; + /// + /// Initializes a new instance of the class. + /// + /// Info for the plugin that requests this service. + public CommandManagerPluginScoped(LocalPlugin localPlugin) + { + this.pluginInfo = localPlugin; + } + /// public ReadOnlyDictionary Commands => this.commandManagerService.Commands; @@ -205,6 +216,7 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM { if (!this.pluginRegisteredCommands.Contains(command)) { + info.LoaderAssemblyName = this.pluginInfo.InternalName; if (this.commandManagerService.AddHandler(command, info)) { this.pluginRegisteredCommands.Add(command); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index e5d3c147b..dcbdced28 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -15,7 +15,6 @@ using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -2227,8 +2226,7 @@ internal class PluginInstallerWindow : Window, IDisposable { var commands = commandManager.Commands .Where(cInfo => - cInfo.Value != null && - cInfo.Value.ShowInHelp && + cInfo.Value is { ShowInHelp: true } && cInfo.Value.LoaderAssemblyName == plugin.Manifest.InternalName) .ToArray(); From dde1dd2f552fdfc85ac3940643706749645700d9 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 18 Sep 2023 14:16:45 -0700 Subject: [PATCH 132/585] Add Clear button to NetworkMonitorWidget --- .../Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index 8212c2e95..e7bce0b84 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -98,6 +98,11 @@ internal class NetworkMonitorWidget : IDataWindowWidget this.trackedPackets = Math.Clamp(this.trackedPackets, 1, 512); } + if (ImGui.Button("Clear Stored Packets")) + { + this.packets.Clear(); + } + this.DrawFilterInput(); this.DrawNegativeFilterInput(); From 84c5982c258c4bf714278aa4cf6cdac3182e5676 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 19 Sep 2023 15:17:15 +0900 Subject: [PATCH 133/585] Use axis12 glyph ranges when loading noto12 as Dalamud default font --- .../Interface/GameFonts/GameFontManager.cs | 36 +++++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 29 +++++++-------- 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index ad0e47273..3a1ab737e 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -171,6 +171,42 @@ internal class GameFontManager : IServiceType fontPtr.BuildLookupTable(); } + /// + /// Create a glyph range for use with ImGui AddFont. + /// + /// Font family and size. + /// Merge two ranges into one if distance is below the value specified in this parameter. + /// Glyph ranges. + public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8) + { + var fdt = this.fdts[(int)family]!; + var ranges = new List(fdt.Glyphs.Count) + { + checked((ushort)fdt.Glyphs[0].CharInt), + checked((ushort)fdt.Glyphs[0].CharInt), + }; + + foreach (var glyph in fdt.Glyphs.Skip(1)) + { + var c32 = glyph.CharInt; + if (c32 >= 0x10000) + break; + + var c16 = unchecked((ushort)c32); + if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) + { + ranges[^1] = c16; + } + else if (ranges[^1] + 1 < c16) + { + ranges.Add(c16); + ranges.Add(c16); + } + } + + return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned); + } + /// /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 841511f55..559207ed6 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -790,10 +790,10 @@ internal class InterfaceManager : IDisposable, IServiceType } else { - var japaneseRangeHandle = GCHandle.Alloc(GlyphRangesJapanese.GlyphRanges, GCHandleType.Pinned); - garbageList.Add(japaneseRangeHandle); + var rangeHandle = gameFontManager.ToGlyphRanges(GameFontFamilyAndSize.Axis12); + garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = japaneseRangeHandle.AddrOfPinnedObject(); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); fontConfig.PixelSnapH = true; DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); this.loadedFontInfo[DefaultFont] = fontInfo; @@ -850,22 +850,19 @@ internal class InterfaceManager : IDisposable, IServiceType foreach (var (fontSize, requests) in extraFontRequests) { - List> codepointRanges = new(); - codepointRanges.Add(Tuple.Create(Fallback1Codepoint, Fallback1Codepoint)); - codepointRanges.Add(Tuple.Create(Fallback2Codepoint, Fallback2Codepoint)); - - // ImGui default ellipsis characters - codepointRanges.Add(Tuple.Create(0x2026, 0x2026)); - codepointRanges.Add(Tuple.Create(0x0085, 0x0085)); + List<(ushort, ushort)> codepointRanges = new(4 + requests.Sum(x => x.CodepointRanges.Count)) + { + new(Fallback1Codepoint, Fallback1Codepoint), + new(Fallback2Codepoint, Fallback2Codepoint), + // ImGui default ellipsis characters + new(0x2026, 0x2026), + new(0x0085, 0x0085), + }; foreach (var request in requests) - { - foreach (var range in request.CodepointRanges) - codepointRanges.Add(range); - } - - codepointRanges.Sort((x, y) => (x.Item1 == y.Item1 ? (x.Item2 < y.Item2 ? -1 : (x.Item2 == y.Item2 ? 0 : 1)) : (x.Item1 < y.Item1 ? -1 : 1))); + codepointRanges.AddRange(request.CodepointRanges.Select(x => (From: x.Item1, To: x.Item2))); + codepointRanges.Sort(); List flattenedRanges = new(); foreach (var range in codepointRanges) { From a61e181bde092988c5fac6c984b2dead307179db Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 19 Sep 2023 19:28:26 +0200 Subject: [PATCH 134/585] fix warning in DataManager --- Dalamud/Data/DataManager.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 809726684..f1f98229a 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -126,10 +126,14 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager /// public ClientLanguage Language { get; private set; } - /// + /// + /// Gets a list of server opcodes. + /// public ReadOnlyDictionary ServerOpCodes { get; private set; } - - /// + + /// + /// Gets a list of client opcodes. + /// [UsedImplicitly] public ReadOnlyDictionary ClientOpCodes { get; private set; } From f7ae34281f530343421b40aac61270ee997f2ada Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 19 Sep 2023 14:11:23 -0700 Subject: [PATCH 135/585] Update License to AGPL 3.0 or Later (#1373) --- Dalamud/Dalamud.csproj | 1 + LICENSE | 671 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 625 insertions(+), 47 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index af785cf52..bcc06d435 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -13,6 +13,7 @@ $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) + AGPL-3.0-or-later diff --git a/LICENSE b/LICENSE index 946b95d8d..0ad25db4b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,84 +1,661 @@ -AFFERO GENERAL PUBLIC LICENSE -Version 1, March 2002

Copyright © 2002 Affero Inc.
510 Third Street - Suite 225, San Francisco, CA 94107, USA -This license is a modified version of the GNU General Public License copyright (C) 1989, 1991 Free Software Foundation, Inc. made with their permission. Section 2(d) has been added to cover use of software over a computer network. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -Preamble + Preamble -The licenses for most software are designed to take away your freedom to share and change it. By contrast, the Affero General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This Public License applies to most of Affero's software and to any other program whose authors commit to using it. (Some other Affero software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. -When we speak of free software, we are referring to freedom, not price. This General Public License is designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. -To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. -For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. -We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. -Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. -Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. -The precise terms and conditions for copying, distribution and modification follow. + The precise terms and conditions for copying, distribution and +modification follow. -TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + TERMS AND CONDITIONS -0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this Affero General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + 0. Definitions. -Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + "This License" refers to version 3 of the GNU Affero General Public License. -1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. -You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. -2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
 -a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
 -b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
 -c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
 -d) If the Program as you received it is intended to interact with users through a computer network and if, in the version you received, any user interacting with the Program was given the opportunity to request transmission to that user of the Program's complete source code, you must not remove that facility from your modified version of the Program or work based on the Program, and must offer an equivalent opportunity for all users interacting with your Program through a computer network to request immediate transmission by HTTP of the complete source code of your modified version or other derivative work. + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. -These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + A "covered work" means either the unmodified Program or a work based +on the Program. -Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. -In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. -3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
 -a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
 -b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
 -c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. -The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + 1. Source Code. -If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. -4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. -5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. -6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. -7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. -If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + The Corresponding Source for a work in source code form is that +same work. -It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + 2. Basic Permissions. -This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. -8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. -9. Affero Inc. may publish revised and/or new versions of the Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. -Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by Affero, Inc. If the Program does not specify a version number of this License, you may choose any version ever published by Affero, Inc. + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. -You may also choose to redistribute modified versions of this program under any version of the Free Software Foundation's GNU General Public License version 3 or higher, so long as that version of the GNU GPL includes terms and conditions substantially equivalent to those of this license. + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. -10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by Affero, Inc., write to us; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. -NO WARRANTY + 4. Conveying Verbatim Copies. -11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. -12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. From 979a5463ca14d60f558c61f06872a067c126fba9 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 19 Sep 2023 23:14:50 +0200 Subject: [PATCH 136/585] fix: dev plugins always need to retain their WorkingPluginId, even throughout reloads --- .../Internal/DevPluginSettings.cs | 7 ++++++ .../Plugin/Internal/Types/LocalDevPlugin.cs | 22 ++++++++++++++++++ Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 23 ++++++++++++++----- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs index 939b03eca..cfe8ba411 100644 --- a/Dalamud/Configuration/Internal/DevPluginSettings.cs +++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs @@ -1,3 +1,5 @@ +using System; + namespace Dalamud.Configuration.Internal; /// @@ -14,4 +16,9 @@ internal sealed class DevPluginSettings /// Gets or sets a value indicating whether this plugin should automatically reload on file change. /// public bool AutomaticReloading { get; set; } = false; + + /// + /// Gets or sets an ID uniquely identifying this specific instance of a devPlugin. + /// + public Guid WorkingPluginId { get; set; } = Guid.Empty; } diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 98784ce64..580d5c161 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -40,6 +41,22 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings(); configuration.QueueSave(); } + + // Legacy dev plugins might not have this! + if (this.devSettings.WorkingPluginId == Guid.Empty) + { + this.devSettings.WorkingPluginId = Guid.NewGuid(); + Log.Verbose("{InternalName} was assigned new devPlugin GUID {Guid}", this.InternalName, this.devSettings.WorkingPluginId); + configuration.QueueSave(); + } + + // If the ID in the manifest is wrong, force the good one + if (this.DevImposedWorkingPluginId != this.manifest.WorkingPluginId) + { + Debug.Assert(this.DevImposedWorkingPluginId != Guid.Empty, "Empty guid for devPlugin"); + this.manifest.WorkingPluginId = this.DevImposedWorkingPluginId; + this.SaveManifest("dev imposed working plugin id"); + } if (this.AutomaticReload) { @@ -76,6 +93,11 @@ internal class LocalDevPlugin : LocalPlugin, IDisposable } } } + + /// + /// Gets an ID uniquely identifying this specific instance of a devPlugin. + /// + public Guid DevImposedWorkingPluginId => this.devSettings.WorkingPluginId; /// public new void Dispose() diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 115ab0f8d..f7306b5a7 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -26,6 +26,13 @@ namespace Dalamud.Plugin.Internal.Types; /// internal class LocalPlugin : IDisposable { + /// + /// The underlying manifest for this plugin. + /// +#pragma warning disable SA1401 + protected LocalPluginManifest manifest; +#pragma warning restore SA1401 + private static readonly ModuleLog Log = new("LOCALPLUGIN"); private readonly FileInfo manifestFile; @@ -39,8 +46,6 @@ internal class LocalPlugin : IDisposable private Type? pluginType; private IDalamudPlugin? instance; - private LocalPluginManifest manifest; - /// /// Initializes a new instance of the class. /// @@ -659,9 +664,11 @@ internal class LocalPlugin : IDisposable var manifestPath = LocalPluginManifest.GetManifestFile(this.DllFile); if (manifestPath.Exists) { - // var isDisabled = this.IsDisabled; // saving the internal state because it could have been deleted + // Save some state that we do actually want to carry over + var guid = this.manifest.WorkingPluginId; + this.manifest = LocalPluginManifest.Load(manifestPath) ?? throw new Exception("Could not reload manifest."); - // this.manifest.Disabled = isDisabled; + this.manifest.WorkingPluginId = guid; this.SaveManifest("dev reload"); } @@ -686,6 +693,12 @@ internal class LocalPlugin : IDisposable }); } + /// + /// Save this plugin manifest. + /// + /// Why it should be saved. + protected void SaveManifest(string reason) => this.manifest.Save(this.manifestFile, reason); + private static void SetupLoaderConfig(LoaderConfig config) { config.IsUnloadable = true; @@ -694,6 +707,4 @@ internal class LocalPlugin : IDisposable config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName()); config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); } - - private void SaveManifest(string reason) => this.manifest.Save(this.manifestFile, reason); } From 674f02136ba9c669425f4c59ae722c653076a53e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:16:55 -0700 Subject: [PATCH 137/585] [AddonEventManager] Properly track and cleanup events. Also replaced DTR hooks with AddonLifecycle events. --- .../Game/AddonEventManager/AddonEventEntry.cs | 54 ++++ .../AddonEventManager/AddonEventListener.cs | 5 + .../AddonEventManager/AddonEventManager.cs | 258 +++++++++--------- .../PluginEventController.cs | 182 ++++++++++++ Dalamud/Game/Gui/Dtr/DtrBar.cs | 117 +++----- Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs | 29 -- Dalamud/Plugin/Services/IAddonEventManager.cs | 4 +- 7 files changed, 421 insertions(+), 228 deletions(-) create mode 100644 Dalamud/Game/AddonEventManager/AddonEventEntry.cs create mode 100644 Dalamud/Game/AddonEventManager/PluginEventController.cs delete mode 100644 Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs diff --git a/Dalamud/Game/AddonEventManager/AddonEventEntry.cs b/Dalamud/Game/AddonEventManager/AddonEventEntry.cs new file mode 100644 index 000000000..22b4756c1 --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventEntry.cs @@ -0,0 +1,54 @@ +using Dalamud.Memory; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonEventManager; + +/// +/// This class represents a registered event that a plugin registers with a native ui node. +/// Contains all necessary information to track and clean up events automatically. +/// +internal unsafe class AddonEventEntry +{ + /// + /// Name of an invalid addon. + /// + public const string InvalidAddonName = "NullAddon"; + + private string? addonName; + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + required public nint Addon { get; init; } + + /// + /// Gets the name of the addon this args referrers to. + /// + public string AddonName => this.Addon == nint.Zero ? InvalidAddonName : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + + /// + /// Gets the pointer to the event source. + /// + required public nint Node { get; init; } + + /// + /// Gets the handler that gets called when this event is triggered. + /// + required public IAddonEventManager.AddonEventHandler Handler { get; init; } + + /// + /// Gets the unique id for this event. + /// + required public uint ParamKey { get; init; } + + /// + /// Gets the event type for this event. + /// + required public AddonEventType EventType { get; init; } + + /// + /// Gets the formatted log string for this AddonEventEntry. + /// + internal string LogString => $"ParamKey: {this.ParamKey}, Addon: {this.AddonName}, Event: {this.EventType}"; +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventListener.cs b/Dalamud/Game/AddonEventManager/AddonEventListener.cs index cb0aa1502..8f724f890 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventListener.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventListener.cs @@ -39,6 +39,11 @@ internal unsafe class AddonEventListener : IDisposable /// Event Data. /// Unknown Parameter. public delegate void ReceiveEventDelegate(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, nint unknown); + + /// + /// Gets the address of this listener. + /// + public nint Address => (nint)this.eventListener; /// public void Dispose() diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 4718d4800..0aa4612c1 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.AddonLifecycle; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -16,15 +19,24 @@ namespace Dalamud.Game.AddonEventManager; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEventManager +internal unsafe class AddonEventManager : IDisposable, IServiceType { + /// + /// PluginName for Dalamud Internal use. + /// + public const string DalamudInternalKey = "Dalamud.Internal"; + private static readonly ModuleLog Log = new("AddonEventManager"); + [ServiceManager.ServiceDependency] + private readonly AddonLifecycle.AddonLifecycle addonLifecycle = Service.Get(); + + private readonly AddonLifecycleEventListener finalizeEventListener; + private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; - private readonly AddonEventListener eventListener; - private readonly Dictionary eventHandlers; + private readonly List pluginEventControllers; private AddonCursorType? cursorOverride; @@ -34,64 +46,109 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); - this.eventHandlers = new Dictionary(); - this.eventListener = new AddonEventListener(this.DalamudAddonEventHandler); + this.pluginEventControllers = new List + { + new(DalamudInternalKey), // Create entry for Dalamud's Internal Use. + }; this.cursorOverride = null; this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); + + this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize); + this.addonLifecycle.RegisterListener(this.finalizeEventListener); } private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); - /// - public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) - { - if (!this.eventHandlers.ContainsKey(eventId)) - { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - var addon = (AtkUnitBase*)atkUnitBase; - - this.eventHandlers.Add(eventId, eventHandler); - this.eventListener.RegisterEvent(addon, node, type, eventId); - } - else - { - Log.Warning($"Attempted to register already registered eventId: {eventId}"); - } - } - - /// - public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType) - { - if (this.eventHandlers.ContainsKey(eventId)) - { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - - this.eventListener.UnregisterEvent(node, type, eventId); - this.eventHandlers.Remove(eventId); - } - else - { - Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); - } - } - /// public void Dispose() { this.onUpdateCursor.Dispose(); - this.eventListener.Dispose(); - this.eventHandlers.Clear(); + + foreach (var pluginEventController in this.pluginEventControllers) + { + pluginEventController.Dispose(); + } + + this.addonLifecycle.UnregisterListener(this.finalizeEventListener); + } + + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// Unique ID for this plugin. + /// Unique Id for this event, maximum 0x10000. + /// The parent addon for this event. + /// The node that will trigger this event. + /// The event type for this event. + /// The handler to call when event is triggered. + internal void AddEvent(string pluginId, uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + { + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) + { + eventController.AddEvent(eventId, atkUnitBase, atkResNode, eventType, eventHandler); + } + else + { + Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); + } + } + + /// + /// Unregisters an event handler with the specified event id and event type. + /// + /// Unique ID for this plugin. + /// The Unique Id for this event. + internal void RemoveEvent(string pluginId, uint eventId) + { + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) + { + eventController.RemoveEvent(eventId); + } + else + { + Log.Verbose($"Unable to locate controller for {pluginId}. No event was removed."); + } } - /// - public void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; + /// + /// Force the game cursor to be the specified cursor. + /// + /// Which cursor to use. + internal void SetCursor(AddonCursorType cursor) => this.cursorOverride = cursor; - /// - public void ResetCursor() => this.cursorOverride = null; + /// + /// Un-forces the game cursor. + /// + internal void ResetCursor() => this.cursorOverride = null; + + /// + /// Adds a new managed event controller if one doesn't already exist for this pluginId. + /// + /// Unique ID for this plugin. + internal void AddPluginEventController(string pluginId) + { + if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId)) + { + Log.Verbose($"Creating new PluginEventController for: {pluginId}"); + this.pluginEventControllers.Add(new PluginEventController(pluginId)); + } + } + + /// + /// Removes an existing managed event controller for the specified plugin. + /// + /// Unique ID for this plugin. + internal void RemovePluginEventController(string pluginId) + { + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller) + { + Log.Verbose($"Removing PluginEventController for: {pluginId}"); + this.pluginEventControllers.Remove(controller); + controller.Dispose(); + } + } [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() @@ -99,6 +156,22 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent this.onUpdateCursor.Enable(); } + /// + /// When an addon finalizes, check it for any registered events, and unregister them. + /// + /// Event type that triggered this call. + /// Addon that triggered this call. + private void OnAddonFinalize(AddonEvent eventType, AddonArgs addonInfo) + { + // It shouldn't be possible for this event to be anything other than PreFinalize. + if (eventType != AddonEvent.PreFinalize) return; + + foreach (var pluginList in this.pluginEventControllers) + { + pluginList.RemoveForAddon(addonInfo.AddonName); + } + } + private nint UpdateCursorDetour(RaptureAtkModule* module) { try @@ -123,22 +196,6 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent return this.onUpdateCursor!.Original(module); } - - private void DalamudAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) - { - if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) - { - try - { - // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler - handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); - } - catch (Exception exception) - { - Log.Error(exception, "Exception in DalamudAddonEventHandler custom event invoke."); - } - } - } } /// @@ -150,25 +207,24 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType, IAddonEvent #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager +internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager { - private static readonly ModuleLog Log = new("AddonEventManager"); - [ServiceManager.ServiceDependency] - private readonly AddonEventManager baseEventManager = Service.Get(); + private readonly AddonEventManager eventManagerService = Service.Get(); - private readonly AddonEventListener eventListener; - private readonly Dictionary eventHandlers; + private readonly LocalPlugin plugin; private bool isForcingCursor; /// /// Initializes a new instance of the class. /// - public AddonEventManagerPluginScoped() + /// Plugin info for the plugin that requested this service. + public AddonEventManagerPluginScoped(LocalPlugin plugin) { - this.eventHandlers = new Dictionary(); - this.eventListener = new AddonEventListener(this.PluginAddonEventHandler); + this.plugin = plugin; + + this.eventManagerService.AddPluginEventController(plugin.Manifest.WorkingPluginId.ToString()); } /// @@ -177,54 +233,26 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. if (this.isForcingCursor) { - this.baseEventManager.ResetCursor(); + this.eventManagerService.ResetCursor(); } - this.eventListener.Dispose(); - this.eventHandlers.Clear(); + this.eventManagerService.RemovePluginEventController(this.plugin.Manifest.WorkingPluginId.ToString()); } /// - public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) - { - if (!this.eventHandlers.ContainsKey(eventId)) - { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - var addon = (AtkUnitBase*)atkUnitBase; + public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + => this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventId, atkUnitBase, atkResNode, eventType, eventHandler); - this.eventHandlers.Add(eventId, eventHandler); - this.eventListener.RegisterEvent(addon, node, type, eventId); - } - else - { - Log.Warning($"Attempted to register already registered eventId: {eventId}"); - } - } - /// - public void RemoveEvent(uint eventId, IntPtr atkResNode, AddonEventType eventType) - { - if (this.eventHandlers.ContainsKey(eventId)) - { - var type = (AtkEventType)eventType; - var node = (AtkResNode*)atkResNode; - - this.eventListener.UnregisterEvent(node, type, eventId); - this.eventHandlers.Remove(eventId); - } - else - { - Log.Warning($"Attempted to unregister already unregistered eventId: {eventId}"); - } - } + public void RemoveEvent(uint eventId) + => this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventId); /// public void SetCursor(AddonCursorType cursor) { this.isForcingCursor = true; - this.baseEventManager.SetCursor(cursor); + this.eventManagerService.SetCursor(cursor); } /// @@ -232,22 +260,6 @@ internal unsafe class AddonEventManagerPluginScoped : IDisposable, IServiceType, { this.isForcingCursor = false; - this.baseEventManager.ResetCursor(); - } - - private void PluginAddonEventHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) - { - if (this.eventHandlers.TryGetValue(eventParam, out var handler) && eventData is not null) - { - try - { - // We passed the AtkUnitBase into the EventData.Node field from our AddonEventHandler - handler?.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); - } - catch (Exception exception) - { - Log.Error(exception, "Exception in PluginAddonEventHandler custom event invoke."); - } - } + this.eventManagerService.ResetCursor(); } } diff --git a/Dalamud/Game/AddonEventManager/PluginEventController.cs b/Dalamud/Game/AddonEventManager/PluginEventController.cs new file mode 100644 index 000000000..2b9ba8e89 --- /dev/null +++ b/Dalamud/Game/AddonEventManager/PluginEventController.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Game.Gui; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.AddonEventManager; + +/// +/// Class to manage creating and cleaning up events per-plugin. +/// +internal unsafe class PluginEventController : IDisposable +{ + private static readonly ModuleLog Log = new("AddonEventManager"); + + /// + /// Initializes a new instance of the class. + /// + /// The Unique ID for this plugin. + public PluginEventController(string pluginId) + { + this.PluginId = pluginId; + + this.EventListener = new AddonEventListener(this.PluginEventListHandler); + } + + /// + /// Gets the unique ID for this PluginEventList. + /// + public string PluginId { get; init; } + + private AddonEventListener EventListener { get; init; } + + private List Events { get; } = new(); + + /// + /// Adds a tracked event. + /// + /// Unique ID of the event to add. + /// The Parent addon for the event. + /// The Node for the event. + /// The Event Type. + /// The delegate to call when invoking this event. + public void AddEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler) + { + var node = (AtkResNode*)atkResNode; + var addon = (AtkUnitBase*)atkUnitBase; + var eventType = (AtkEventType)atkEventType; + + var eventEntry = new AddonEventEntry + { + Addon = atkUnitBase, + Handler = handler, + Node = atkResNode, + EventType = atkEventType, + ParamKey = eventId, + }; + + Log.Verbose($"Adding Event: {eventEntry.LogString}"); + this.EventListener.RegisterEvent(addon, node, eventType, eventId); + this.Events.Add(eventEntry); + } + + /// + /// Removes a tracked event, also attempts to un-attach the event from native. + /// + /// Unique ID of the event to remove. + public void RemoveEvent(uint eventId) + { + if (this.Events.FirstOrDefault(registeredEvent => registeredEvent.ParamKey == eventId) is not { } targetEvent) return; + + Log.Verbose($"Removing Event: {targetEvent.LogString}"); + this.TryRemoveEventFromNative(targetEvent); + this.Events.Remove(targetEvent); + } + + /// + /// Removes all events attached to the specified addon. + /// + /// Addon name to remove events from. + public void RemoveForAddon(string addonName) + { + foreach (var registeredEvent in this.Events.Where(entry => entry.AddonName == addonName).ToList()) + { + Log.Verbose($"Addon: {addonName} is Finalizing, removing event: {registeredEvent.LogString}"); + this.RemoveEvent(registeredEvent.ParamKey); + } + } + + /// + public void Dispose() + { + foreach (var registeredEvent in this.Events.ToList()) + { + this.RemoveEvent(registeredEvent.ParamKey); + } + + this.EventListener.Dispose(); + } + + /// + /// Attempts to remove a tracked event from native UI. + /// This method performs several safety checks to only remove events from a still active addon. + /// If any of these checks fail, it likely means the native UI already cleaned up the event, and we don't have to worry about them. + /// + /// Event entry to remove. + private void TryRemoveEventFromNative(AddonEventEntry eventEntry) + { + // Is the eventEntry addon valid? + if (eventEntry.AddonName is AddonEventEntry.InvalidAddonName) return; + + // Is an addon with the same name active? + var currentAddonPointer = Service.Get().GetAddonByName(eventEntry.AddonName); + if (currentAddonPointer == nint.Zero) return; + + // Is our stored addon pointer the same as the active addon pointer? + if (currentAddonPointer != eventEntry.Addon) return; + + // Does this addon contain the node this event is for? (by address) + var atkUnitBase = (AtkUnitBase*)currentAddonPointer; + var nodeFound = false; + foreach (var index in Enumerable.Range(0, atkUnitBase->UldManager.NodeListCount)) + { + var node = atkUnitBase->UldManager.NodeList[index]; + + // If this node matches our node, then we know our node is still valid. + if (node is not null && (nint)node == eventEntry.Node) + { + nodeFound = true; + } + } + + // If we didn't find the node, we can't remove the event. + if (!nodeFound) return; + + // Does the node have a registered event matching the parameters we have? + var atkResNode = (AtkResNode*)eventEntry.Node; + var eventType = (AtkEventType)eventEntry.EventType; + var currentEvent = atkResNode->AtkEventManager.Event; + var eventFound = false; + while (currentEvent is not null) + { + var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey; + var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address; + var eventTypeMatches = currentEvent->Type == eventType; + + if (paramKeyMatches && eventListenerAddressMatches && eventTypeMatches) + { + eventFound = true; + break; + } + + // Move to the next event. + currentEvent = currentEvent->NextEvent; + } + + // If we didn't find the event, we can't remove the event. + if (!eventFound) return; + + // We have a valid addon, valid node, valid event, and valid key. + this.EventListener.UnregisterEvent(atkResNode, eventType, eventEntry.ParamKey); + } + + private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventData, IntPtr unknown) + { + try + { + if (eventData is null) return; + if (this.Events.FirstOrDefault(handler => handler.ParamKey == eventParam) is not { } eventInfo) return; + + // We stored the AtkUnitBase* in EventData->Node, and EventData->Target contains the node that triggered the event. + eventInfo.Handler.Invoke((AddonEventType)eventType, (nint)eventData->Node, (nint)eventData->Target); + } + catch (Exception exception) + { + Log.Error(exception, "Exception in PluginEventList custom event invoke."); + } + } +} diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index ae01d4886..0298dd203 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -5,19 +5,16 @@ using System.Linq; using Dalamud.Configuration.Internal; using Dalamud.Game.AddonEventManager; +using Dalamud.Game.AddonLifecycle; using Dalamud.Game.Text.SeStringHandling; -using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; -using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Graphics; using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; -using DalamudAddonEventManager = Dalamud.Game.AddonEventManager.AddonEventManager; - namespace Dalamud.Game.Gui.Dtr; /// @@ -45,23 +42,27 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private readonly DalamudConfiguration configuration = Service.Get(); [ServiceManager.ServiceDependency] - private readonly DalamudAddonEventManager uiEventManager = Service.Get(); + private readonly AddonEventManager.AddonEventManager uiEventManager = Service.Get(); - private readonly DtrBarAddressResolver address; + [ServiceManager.ServiceDependency] + private readonly AddonLifecycle.AddonLifecycle addonLifecycle = Service.Get(); + + private readonly AddonLifecycleEventListener dtrPostDrawListener; + private readonly AddonLifecycleEventListener dtrPostRequestedUpdateListener; + private readonly ConcurrentBag newEntries = new(); private readonly List entries = new(); - private readonly Hook onAddonDrawHook; - private readonly Hook onAddonRequestedUpdateHook; + private uint runningNodeIds = BaseNodeId; [ServiceManager.ServiceConstructor] - private DtrBar(SigScanner sigScanner) + private DtrBar() { - this.address = new DtrBarAddressResolver(); - this.address.Setup(sigScanner); + this.dtrPostDrawListener = new AddonLifecycleEventListener(AddonEvent.PostDraw, "_DTR", this.OnDtrPostDraw); + this.dtrPostRequestedUpdateListener = new AddonLifecycleEventListener(AddonEvent.PostRequestedUpdate, "_DTR", this.OnAddonRequestedUpdateDetour); - this.onAddonDrawHook = Hook.FromAddress(this.address.AtkUnitBaseDraw, this.OnAddonDrawDetour); - this.onAddonRequestedUpdateHook = Hook.FromAddress(this.address.AddonRequestedUpdate, this.OnAddonRequestedUpdateDetour); + this.addonLifecycle.RegisterListener(this.dtrPostDrawListener); + this.addonLifecycle.RegisterListener(this.dtrPostRequestedUpdateListener); this.framework.Update += this.Update; @@ -70,10 +71,6 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.configuration.QueueSave(); } - private delegate void AddonDrawDelegate(AtkUnitBase* addon); - - private delegate void AddonRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); - /// public DtrBarEntry Get(string title, SeString? text = null) { @@ -104,8 +101,8 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar /// void IDisposable.Dispose() { - this.onAddonDrawHook.Dispose(); - this.onAddonRequestedUpdateHook.Dispose(); + this.addonLifecycle.UnregisterListener(this.dtrPostDrawListener); + this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener); foreach (var entry in this.entries) this.RemoveNode(entry.TextNode); @@ -167,13 +164,6 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar return xPos.CompareTo(yPos); }); } - - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.onAddonDrawHook.Enable(); - this.onAddonRequestedUpdateHook.Enable(); - } private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR").ToPointer(); @@ -260,37 +250,26 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.ApplySort(); } } - - // This hooks all AtkUnitBase.Draw calls, then checks for our specific addon name. - // AddonDtr doesn't implement it's own Draw method, would need to replace vtable entry to be more efficient. - private void OnAddonDrawDetour(AtkUnitBase* addon) + + private void OnDtrPostDraw(AddonEvent eventType, AddonArgs addonInfo) { - this.onAddonDrawHook!.Original(addon); + var addon = (AtkUnitBase*)addonInfo.Addon; - try - { - if (MemoryHelper.ReadString((nint)addon->Name, 0x20) is not "_DTR") return; - - this.UpdateNodePositions(addon); + this.UpdateNodePositions(addon); - if (!this.configuration.DtrSwapDirection) - { - var targetSize = (ushort)this.CalculateTotalSize(); - var sizeDelta = targetSize - addon->RootNode->Width; - - if (addon->RootNode->Width != targetSize) - { - addon->RootNode->SetWidth(targetSize); - addon->SetX((short)(addon->GetX() - sizeDelta)); - - // force a RequestedUpdate immediately to force the game to right-justify it immediately. - addon->OnUpdate(AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); - } - } - } - catch (Exception e) + if (!this.configuration.DtrSwapDirection) { - Log.Error(e, "Exception in OnAddonDraw."); + var targetSize = (ushort)this.CalculateTotalSize(); + var sizeDelta = targetSize - addon->RootNode->Width; + + if (addon->RootNode->Width != targetSize) + { + addon->RootNode->SetWidth(targetSize); + addon->SetX((short)(addon->GetX() - sizeDelta)); + + // force a RequestedUpdate immediately to force the game to right-justify it immediately. + addon->OnUpdate(AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); + } } } @@ -317,18 +296,11 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } - private void OnAddonRequestedUpdateDetour(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) + private void OnAddonRequestedUpdateDetour(AddonEvent eventType, AddonArgs addonInfo) { - this.onAddonRequestedUpdateHook.Original(addon, numberArrayData, stringArrayData); - - try - { - this.UpdateNodePositions(addon); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRequestedUpdate."); - } + var addon = (AtkUnitBase*)addonInfo.Addon; + + this.UpdateNodePositions(addon); } /// @@ -386,9 +358,9 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; - this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); - this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); - this.uiEventManager.AddEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); + this.uiEventManager.AddEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); + this.uiEventManager.AddEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); + this.uiEventManager.AddEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); var lastChild = dtr->RootNode->ChildNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; @@ -406,14 +378,14 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar return true; } - private bool RemoveNode(AtkTextNode* node) + private void RemoveNode(AtkTextNode* node) { var dtr = this.GetDtr(); - if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; + if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)node, AddonEventType.MouseOver); - this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)node, AddonEventType.MouseOut); - this.uiEventManager.RemoveEvent(node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)node, AddonEventType.MouseClick); + this.uiEventManager.RemoveEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset); + this.uiEventManager.RemoveEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset); + this.uiEventManager.RemoveEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset); var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; @@ -429,7 +401,6 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar dtr->UldManager.UpdateDrawNodeList(); dtr->UpdateCollisionNodeList(false); Log.Debug("Updated node draw list"); - return true; } private AtkTextNode* MakeNode(uint nodeId) diff --git a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs b/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs deleted file mode 100644 index 1e6fd09cd..000000000 --- a/Dalamud/Game/Gui/Dtr/DtrBarAddressResolver.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace Dalamud.Game.Gui.Dtr; - -/// -/// DtrBar memory address resolver. -/// -public class DtrBarAddressResolver : BaseAddressResolver -{ - /// - /// Gets the address of the AtkUnitBaseDraw method. - /// This is the base handler for all addons. - /// We will use this here because _DTR does not have a overloaded handler, so we must use the base handler. - /// - public nint AtkUnitBaseDraw { get; private set; } - - /// - /// Gets the address of the DTRRequestUpdate method. - /// - public nint AddonRequestedUpdate { get; private set; } - - /// - /// Scan for and setup any configured address pointers. - /// - /// The signature scanner to facilitate setup. - protected override void Setup64Bit(SigScanner scanner) - { - this.AtkUnitBaseDraw = scanner.ScanText("48 83 EC 28 F6 81 ?? ?? ?? ?? ?? 4C 8B C1"); - this.AddonRequestedUpdate = scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B BA ?? ?? ?? ?? 48 8B F1 49 8B 98 ?? ?? ?? ?? 33 D2"); - } -} diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index dbbfd784b..aa7e71478 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -29,9 +29,7 @@ public interface IAddonEventManager /// Unregisters an event handler with the specified event id and event type. /// /// The Unique Id for this event. - /// The node for this event. - /// The event type for this event. - void RemoveEvent(uint eventId, nint atkResNode, AddonEventType eventType); + void RemoveEvent(uint eventId); /// /// Force the game cursor to be the specified cursor. From f48c6d499b765285c297c05906c762bd19e5ee1d Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 19 Sep 2023 22:08:06 -0700 Subject: [PATCH 138/585] [AddonEventManager] Cleanup logging --- .../Game/AddonEventManager/PluginEventController.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/AddonEventManager/PluginEventController.cs b/Dalamud/Game/AddonEventManager/PluginEventController.cs index 2b9ba8e89..42424c6ec 100644 --- a/Dalamud/Game/AddonEventManager/PluginEventController.cs +++ b/Dalamud/Game/AddonEventManager/PluginEventController.cs @@ -83,10 +83,14 @@ internal unsafe class PluginEventController : IDisposable /// Addon name to remove events from. public void RemoveForAddon(string addonName) { - foreach (var registeredEvent in this.Events.Where(entry => entry.AddonName == addonName).ToList()) + if (this.Events.Where(entry => entry.AddonName == addonName).ToList() is { Count: not 0 } events) { - Log.Verbose($"Addon: {addonName} is Finalizing, removing event: {registeredEvent.LogString}"); - this.RemoveEvent(registeredEvent.ParamKey); + Log.Verbose($"Addon: {addonName} is Finalizing, removing {events.Count} events."); + + foreach (var registeredEvent in events) + { + this.RemoveEvent(registeredEvent.ParamKey); + } } } From c305c01dfdc8482a6c5abf4fdf5b290f55d57529 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:43:45 -0700 Subject: [PATCH 139/585] Nullify Scoped Service Delegates --- Dalamud/Game/ClientState/Conditions/Condition.cs | 2 ++ Dalamud/Game/DutyState/DutyState.cs | 5 +++++ Dalamud/Game/Gui/ChatGui.cs | 5 +++++ Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 2 ++ Dalamud/Game/Gui/GameGui.cs | 4 ++++ Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs | 2 ++ Dalamud/Game/Gui/Toast/ToastGui.cs | 4 ++++ Dalamud/Game/Network/GameNetwork.cs | 2 ++ Dalamud/Utility/Util.cs | 2 +- 9 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 0f8523e9b..2db47ea4d 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -189,6 +189,8 @@ internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition public void Dispose() { this.conditionService.ConditionChange -= this.ConditionChangedForward; + + this.ConditionChange = null; } /// diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 34940dee0..c52ceff0f 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -210,6 +210,11 @@ internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState this.dutyStateService.DutyWiped -= this.DutyWipedForward; this.dutyStateService.DutyRecommenced -= this.DutyRecommencedForward; this.dutyStateService.DutyCompleted -= this.DutyCompletedForward; + + this.DutyStarted = null; + this.DutyWiped = null; + this.DutyRecommenced = null; + this.DutyCompleted = null; } private void DutyStartedForward(object sender, ushort territoryId) => this.DutyStarted?.Invoke(sender, territoryId); diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 2fbeb404e..55c919ab5 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -460,6 +460,11 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward; this.chatGuiService.ChatMessageHandled -= this.OnMessageHandledForward; this.chatGuiService.ChatMessageUnhandled -= this.OnMessageUnhandledForward; + + this.ChatMessage = null; + this.CheckMessageHandled = null; + this.ChatMessageHandled = null; + this.ChatMessageUnhandled = null; } /// diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 3c04c744a..64de4b2dd 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -307,6 +307,8 @@ internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui public void Dispose() { this.flyTextGuiService.FlyTextCreated -= this.FlyTextCreatedForward; + + this.FlyTextCreated = null; } /// diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 078c624e8..349d2a424 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -566,6 +566,10 @@ internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; this.gameGuiService.HoveredActionChanged -= this.HoveredActionForward; + + this.UiHideToggled = null; + this.HoveredItemChanged = null; + this.HoveredActionChanged = null; } /// diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 41a8ba56a..4bd93cdf0 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -156,6 +156,8 @@ internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFin public void Dispose() { this.partyFinderGuiService.ReceiveListing -= this.ReceiveListingForward; + + this.ReceiveListing = null; } private void ReceiveListingForward(PartyFinderListing listing, PartyFinderListingEventArgs args) => this.ReceiveListing?.Invoke(listing, args); diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 93126710b..9624e3e72 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -417,6 +417,10 @@ internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui this.toastGuiService.Toast -= this.ToastForward; this.toastGuiService.QuestToast -= this.QuestToastForward; this.toastGuiService.ErrorToast -= this.ErrorToastForward; + + this.Toast = null; + this.QuestToast = null; + this.ErrorToast = null; } /// diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index f56fd3996..7c900ece4 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -169,6 +169,8 @@ internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork public void Dispose() { this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward; + + this.NetworkMessage = null; } private void NetworkMessageForward(nint dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 78edda3dd..c386ee23e 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -651,7 +651,7 @@ public static class Util /// /// The path of the file to write to. /// The text to write. - internal static void WriteAllTextSafe(string path, string text) + public static void WriteAllTextSafe(string path, string text) { var tmpPath = path + ".tmp"; if (File.Exists(tmpPath)) From 2b2a027fb08aca6e7400b2cf5afbb886a8f9ac76 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 19 Sep 2023 23:44:56 -0700 Subject: [PATCH 140/585] Move WriteAllTextSafe to correct location --- Dalamud/Utility/Util.cs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index c386ee23e..8ca87b691 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -609,7 +609,23 @@ public static class Util } } } + + /// + /// Overwrite text in a file by first writing it to a temporary file, and then + /// moving that file to the path specified. + /// + /// The path of the file to write to. + /// The text to write. + public static void WriteAllTextSafe(string path, string text) + { + var tmpPath = path + ".tmp"; + if (File.Exists(tmpPath)) + File.Delete(tmpPath); + File.WriteAllText(tmpPath, text); + File.Move(tmpPath, path, true); + } + /// /// Dispose this object. /// @@ -645,22 +661,6 @@ public static class Util } } - /// - /// Overwrite text in a file by first writing it to a temporary file, and then - /// moving that file to the path specified. - /// - /// The path of the file to write to. - /// The text to write. - public static void WriteAllTextSafe(string path, string text) - { - var tmpPath = path + ".tmp"; - if (File.Exists(tmpPath)) - File.Delete(tmpPath); - - File.WriteAllText(tmpPath, text); - File.Move(tmpPath, path, true); - } - /// /// Gets a random, inoffensive, human-friendly string. /// From 6f40449ab3abe57c5bfe278bf9994fe088e50071 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:46:16 -0700 Subject: [PATCH 141/585] Adjust Namespaces --- Dalamud/Game/AddonEventManager/AddonCursorType.cs | 2 +- Dalamud/Game/AddonEventManager/AddonEventEntry.cs | 2 +- .../Game/AddonEventManager/AddonEventListener.cs | 2 +- Dalamud/Game/AddonEventManager/AddonEventManager.cs | 5 ++--- .../AddonEventManagerAddressResolver.cs | 2 +- Dalamud/Game/AddonEventManager/AddonEventType.cs | 2 +- .../Game/AddonEventManager/PluginEventController.cs | 2 +- Dalamud/Game/AddonLifecycle/AddonArgs.cs | 2 +- Dalamud/Game/AddonLifecycle/AddonEvent.cs | 2 +- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 2 +- .../AddonLifecycle/AddonLifecycleAddressResolver.cs | 2 +- .../AddonLifecycle/AddonLifecycleEventListener.cs | 2 +- Dalamud/Game/Gui/Dtr/DtrBar.cs | 13 +++++++++---- Dalamud/Plugin/Services/IAddonEventManager.cs | 2 +- Dalamud/Plugin/Services/IAddonLifecycle.cs | 2 +- 15 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Dalamud/Game/AddonEventManager/AddonCursorType.cs b/Dalamud/Game/AddonEventManager/AddonCursorType.cs index 8ba3a901b..57d58c60c 100644 --- a/Dalamud/Game/AddonEventManager/AddonCursorType.cs +++ b/Dalamud/Game/AddonEventManager/AddonCursorType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// Reimplementation of CursorType. diff --git a/Dalamud/Game/AddonEventManager/AddonEventEntry.cs b/Dalamud/Game/AddonEventManager/AddonEventEntry.cs index 22b4756c1..83f1a724c 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventEntry.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventEntry.cs @@ -2,7 +2,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// This class represents a registered event that a plugin registers with a native ui node. diff --git a/Dalamud/Game/AddonEventManager/AddonEventListener.cs b/Dalamud/Game/AddonEventManager/AddonEventListener.cs index 8f724f890..6f7c55c4c 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventListener.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventListener.cs @@ -3,7 +3,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// Event listener class for managing custom events. diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 0aa4612c1..89554074a 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; -using Dalamud.Game.AddonLifecycle; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; @@ -12,7 +11,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// Service provider for addon event management. @@ -29,7 +28,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType private static readonly ModuleLog Log = new("AddonEventManager"); [ServiceManager.ServiceDependency] - private readonly AddonLifecycle.AddonLifecycle addonLifecycle = Service.Get(); + private readonly AddonLifecycle addonLifecycle = Service.Get(); private readonly AddonLifecycleEventListener finalizeEventListener; diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs index ba1c07db8..71a6093bb 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// AddonEventManager memory address resolver. diff --git a/Dalamud/Game/AddonEventManager/AddonEventType.cs b/Dalamud/Game/AddonEventManager/AddonEventType.cs index eef9763ff..74f35c257 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventType.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// Reimplementation of AtkEventType. diff --git a/Dalamud/Game/AddonEventManager/PluginEventController.cs b/Dalamud/Game/AddonEventManager/PluginEventController.cs index 42424c6ec..852d78128 100644 --- a/Dalamud/Game/AddonEventManager/PluginEventController.cs +++ b/Dalamud/Game/AddonEventManager/PluginEventController.cs @@ -7,7 +7,7 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.AddonEventManager; +namespace Dalamud.Game.Addon; /// /// Class to manage creating and cleaning up events per-plugin. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgs.cs index 50c995abb..4ae306817 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgs.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgs.cs @@ -1,7 +1,7 @@ using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.AddonLifecycle; +namespace Dalamud.Game.Addon; /// /// Addon argument data for use in event subscribers. diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/AddonLifecycle/AddonEvent.cs index faef30c88..cfc83fb8a 100644 --- a/Dalamud/Game/AddonLifecycle/AddonEvent.cs +++ b/Dalamud/Game/AddonLifecycle/AddonEvent.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonLifecycle; +namespace Dalamud.Game.Addon; /// /// Enumeration for available AddonLifecycle events. diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index c416b6d1f..68233eeb8 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -11,7 +11,7 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.AddonLifecycle; +namespace Dalamud.Game.Addon; /// /// This class provides events for in-game addon lifecycles. diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index 079e09c80..d68fee9ed 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.AddonLifecycle; +namespace Dalamud.Game.Addon; /// /// AddonLifecycleService memory address resolver. diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs index 0f088362d..12ccf5e8f 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin.Services; -namespace Dalamud.Game.AddonLifecycle; +namespace Dalamud.Game.Addon; /// /// This class is a helper for tracking and invoking listener delegates. diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 0298dd203..860750dc7 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -4,8 +4,7 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; -using Dalamud.Game.AddonEventManager; -using Dalamud.Game.AddonLifecycle; +using Dalamud.Game.Addon; using Dalamud.Game.Text.SeStringHandling; using Dalamud.IoC; using Dalamud.IoC.Internal; @@ -42,10 +41,10 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private readonly DalamudConfiguration configuration = Service.Get(); [ServiceManager.ServiceDependency] - private readonly AddonEventManager.AddonEventManager uiEventManager = Service.Get(); + private readonly AddonEventManager uiEventManager = Service.Get(); [ServiceManager.ServiceDependency] - private readonly AddonLifecycle.AddonLifecycle addonLifecycle = Service.Get(); + private readonly AddonLifecycle addonLifecycle = Service.Get(); private readonly AddonLifecycleEventListener dtrPostDrawListener; private readonly AddonLifecycleEventListener dtrPostRequestedUpdateListener; @@ -361,6 +360,9 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.uiEventManager.AddEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); this.uiEventManager.AddEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); this.uiEventManager.AddEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); + this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); + this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); + this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); var lastChild = dtr->RootNode->ChildNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; @@ -386,6 +388,9 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.uiEventManager.RemoveEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset); this.uiEventManager.RemoveEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset); this.uiEventManager.RemoveEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset); + this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset); + this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset); + this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset); var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index aa7e71478..f3588f469 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.AddonEventManager; +using Dalamud.Game.Addon; namespace Dalamud.Plugin.Services; diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 1dc792660..e455754a1 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; -using Dalamud.Game.AddonLifecycle; +using Dalamud.Game.Addon; namespace Dalamud.Plugin.Services; From b32a3b93856106a723c5f1943bf3254c99877d38 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:46:38 -0700 Subject: [PATCH 142/585] Fix missed namespace --- .../Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs index 3a1cb0e77..a9948430f 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -1,6 +1,6 @@ using System.Collections.Generic; -using Dalamud.Game.AddonLifecycle; +using Dalamud.Game.Addon; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; From d836a3e55631d6af4489740cbf18c64350064420 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 20 Sep 2023 19:14:22 +0200 Subject: [PATCH 143/585] Catch exceptions in Window.Draw --- Dalamud/Interface/Windowing/Window.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 39c61566b..73a14db79 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -1,9 +1,11 @@ +using System; using System.Numerics; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; +using Serilog; namespace Dalamud.Interface.Windowing; @@ -284,7 +286,14 @@ public abstract class Window if (this.ShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, this.Flags) : ImGui.Begin(this.WindowName, this.Flags)) { // Draw the actual window contents - this.Draw(); + try + { + this.Draw(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error during Draw(): {this.WindowName}"); + } } if (wasFocused) From cec382dfed1fa68f5ee3bb61acd4b0ea309d616f Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 20 Sep 2023 19:53:45 +0200 Subject: [PATCH 144/585] Switch to ModuleLog in Window --- Dalamud/Interface/Windowing/Window.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 73a14db79..277fb46c8 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -3,9 +3,9 @@ using System.Numerics; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.Logging.Internal; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; -using Serilog; namespace Dalamud.Interface.Windowing; @@ -14,6 +14,8 @@ namespace Dalamud.Interface.Windowing; /// public abstract class Window { + private static readonly ModuleLog Log = new("WindowSystem"); + private static bool wasEscPressedLastFrame = false; private bool internalLastIsOpen = false; From 5f86496360c117933c949d41885f994c5d591ed4 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 20 Sep 2023 10:55:45 -0700 Subject: [PATCH 145/585] Add missing renames --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 860750dc7..f27698e54 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -357,9 +357,6 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; - this.uiEventManager.AddEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); - this.uiEventManager.AddEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); - this.uiEventManager.AddEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); @@ -385,9 +382,6 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - this.uiEventManager.RemoveEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset); - this.uiEventManager.RemoveEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset); - this.uiEventManager.RemoveEvent(AddonEventManager.AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset); this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset); this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset); this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset); From b26550eea9a15027db05e8f716c1b1e548ed2816 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 20 Sep 2023 10:56:07 -0700 Subject: [PATCH 146/585] Fix not applying spacing when set to grow from the right --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index f27698e54..48e04e6a3 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -223,7 +223,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (this.configuration.DtrSwapDirection) { - data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); + data.TextNode->AtkResNode.SetPositionFloat(runningXPos + this.configuration.DtrSpacing, 2); runningXPos += elementWidth; } else From 48e60a7a5d3f460a6e8de0c0b138337bf898b491 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 20 Sep 2023 11:22:58 -0700 Subject: [PATCH 147/585] Fix hidden nodes preventing native tooltips from propogating --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 48e04e6a3..8882088ef 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -232,6 +232,11 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); } } + else + { + // If we want the node hidden, shift it up, to prevent collision conflicts + data.TextNode->AtkResNode.SetY(-collisionNode->Height); + } } } From dae105d157b9c4e9ad4f1b1d5c9ef397be3e44d7 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:19:06 -0700 Subject: [PATCH 148/585] Account for UiScale when moving nodes --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 8882088ef..880bc0625 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -235,7 +235,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar else { // If we want the node hidden, shift it up, to prevent collision conflicts - data.TextNode->AtkResNode.SetY(-collisionNode->Height); + data.TextNode->AtkResNode.SetY(-collisionNode->Height * dtr->RootNode->ScaleX); } } } @@ -264,7 +264,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar if (!this.configuration.DtrSwapDirection) { var targetSize = (ushort)this.CalculateTotalSize(); - var sizeDelta = targetSize - addon->RootNode->Width; + var sizeDelta = MathF.Round((targetSize - addon->RootNode->Width) * addon->RootNode->ScaleX); if (addon->RootNode->Width != targetSize) { From b79241902fc8b42e88b64bdb5d451a41787f2832 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 20 Sep 2023 22:41:07 -0700 Subject: [PATCH 149/585] Use multi-platform strategy for Targets - Intelligently choose the default `DalamudLibPath` depending on the building system's OS. - Allow overrides with ``$DALAMUD_HOME` env var. --- targets/Dalamud.Plugin.Bootstrap.targets | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/targets/Dalamud.Plugin.Bootstrap.targets b/targets/Dalamud.Plugin.Bootstrap.targets index c30a5acba..db4bf6cd7 100644 --- a/targets/Dalamud.Plugin.Bootstrap.targets +++ b/targets/Dalamud.Plugin.Bootstrap.targets @@ -1,11 +1,10 @@ - $(appdata)\XIVLauncher\addon\Hooks\dev\ - - - - $(DALAMUD_HOME)/ + $(appdata)\XIVLauncher\addon\Hooks\dev\ + $(HOME)/.xlcore/dalamud/Hooks/dev/ + $(HOME)/Library/Application Support/XIV on Mac/dalamud/Hooks/dev/ + $(DALAMUD_HOME)/ From 1abaeef5ab183efb225cda4a62a252936f484555 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 18:54:02 +0200 Subject: [PATCH 150/585] feat: use WorkingPluginId as identifier for plugins to load from profiles --- .../PluginInstaller/PluginInstallerWindow.cs | 24 ++++----- .../PluginInstaller/ProfileManagerWidget.cs | 16 +++--- Dalamud/Plugin/Internal/PluginManager.cs | 47 +++++++++------- Dalamud/Plugin/Internal/Profiles/Profile.cs | 54 +++++++++++++------ .../Internal/Profiles/ProfileManager.cs | 42 ++++++++++----- .../Plugin/Internal/Profiles/ProfileModel.cs | 36 +++++++++++-- .../Internal/Profiles/ProfileModelV1.cs | 2 + .../Internal/Profiles/ProfilePluginEntry.cs | 5 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- 9 files changed, 155 insertions(+), 73 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index dcbdced28..163e62b78 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2381,10 +2381,10 @@ internal class PluginInstallerWindow : Window, IDisposable var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev; var profilesThatWantThisPlugin = profileManager.Profiles - .Where(x => x.WantsPlugin(plugin.InternalName) != null) + .Where(x => x.WantsPlugin(plugin.Manifest.WorkingPluginId) != null) .ToArray(); var isInSingleProfile = profilesThatWantThisPlugin.Length == 1; - var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName); + var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.WorkingPluginId); // Disable everything if the updater is running or another plugin is operating var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress; @@ -2419,17 +2419,17 @@ internal class PluginInstallerWindow : Window, IDisposable foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile)) { - var inProfile = profile.WantsPlugin(plugin.Manifest.InternalName) != null; + var inProfile = profile.WantsPlugin(plugin.Manifest.WorkingPluginId) != null; if (ImGui.Checkbox($"###profilePick{profile.Guid}{plugin.Manifest.InternalName}", ref inProfile)) { if (inProfile) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd); } else { - Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName)) + Task.Run(() => profile.RemoveAsync(plugin.Manifest.WorkingPluginId)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotRemove); } } @@ -2449,11 +2449,11 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, plugin.IsLoaded, false)) + Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.IsLoaded, false)) .GetAwaiter().GetResult(); foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName))) { - Task.Run(() => profile.RemoveAsync(plugin.Manifest.InternalName, false)) + Task.Run(() => profile.RemoveAsync(plugin.Manifest.WorkingPluginId, false)) .GetAwaiter().GetResult(); } @@ -2527,7 +2527,7 @@ internal class PluginInstallerWindow : Window, IDisposable { await plugin.UnloadAsync(); await applicableProfile.AddOrUpdateAsync( - plugin.Manifest.InternalName, false, false); + plugin.Manifest.WorkingPluginId, false, false); notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); }).ContinueWith(t => @@ -2544,7 +2544,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; - await applicableProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); await plugin.LoadAsync(PluginLoadReason.Installer); notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success); @@ -2565,7 +2565,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (shouldUpdate) { // We need to update the profile right here, because PM will not enable the plugin otherwise - await applicableProfile.AddOrUpdateAsync(plugin.InternalName, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); await this.UpdateSinglePlugin(availableUpdate); } else @@ -2739,7 +2739,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (localPlugin is LocalDevPlugin plugin) { var isInDefaultProfile = - Service.Get().IsInDefaultProfile(localPlugin.Manifest.InternalName); + Service.Get().IsInDefaultProfile(localPlugin.Manifest.WorkingPluginId); // https://colorswall.com/palette/2868/ var greenColor = new Vector4(0x5C, 0xB8, 0x5C, 0xFF) / 0xFF; @@ -3083,7 +3083,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.pluginListAvailable.Sort((p1, p2) => p1.Name.CompareTo(p2.Name)); var profman = Service.Get(); - this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.InternalName).CompareTo(profman.IsInDefaultProfile(p2.InternalName))); + this.pluginListInstalled.Sort((p1, p2) => profman.IsInDefaultProfile(p1.Manifest.WorkingPluginId).CompareTo(profman.IsInDefaultProfile(p2.Manifest.WorkingPluginId))); break; default: throw new InvalidEnumArgumentException("Unknown plugin sort type."); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 039877158..2be074f84 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -229,7 +229,7 @@ internal class ProfileManagerWidget if (ImGuiComponents.IconButton($"###exportButton{profile.Guid}", FontAwesomeIcon.FileExport)) { - ImGui.SetClipboardText(profile.Model.Serialize()); + ImGui.SetClipboardText(profile.Model.SerializeForShare()); Service.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success); } @@ -300,7 +300,7 @@ internal class ProfileManagerWidget if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}")) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } } @@ -327,7 +327,7 @@ internal class ProfileManagerWidget if (ImGuiComponents.IconButton(FontAwesomeIcon.FileExport)) { - ImGui.SetClipboardText(profile.Model.Serialize()); + ImGui.SetClipboardText(profile.Model.SerializeForShare()); Service.Get().AddNotification(Locs.CopyToClipboardNotification, type: NotificationType.Success); } @@ -400,7 +400,7 @@ internal class ProfileManagerWidget if (pluginListChild) { var pluginLineHeight = 32 * ImGuiHelpers.GlobalScale; - string? wantRemovePluginInternalName = null; + Guid? wantRemovePluginGuid = null; using var syncScope = profile.GetSyncScope(); foreach (var plugin in profile.Plugins.ToArray()) @@ -467,7 +467,7 @@ internal class ProfileManagerWidget var enabled = plugin.IsEnabled; if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.InternalName, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.WorkingPluginId, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } @@ -477,17 +477,17 @@ internal class ProfileManagerWidget if (ImGuiComponents.IconButton($"###removePlugin{plugin.InternalName}", FontAwesomeIcon.Trash)) { - wantRemovePluginInternalName = plugin.InternalName; + wantRemovePluginGuid = plugin.WorkingPluginId; } if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.RemovePlugin); } - if (wantRemovePluginInternalName != null) + if (wantRemovePluginGuid != null) { // TODO: handle error - Task.Run(() => profile.RemoveAsync(wantRemovePluginInternalName, false)) + Task.Run(() => profile.RemoveAsync(wantRemovePluginGuid.Value, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotRemove); } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 691d5f729..f782b4129 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1290,13 +1290,27 @@ internal partial class PluginManager : IDisposable, IServiceType if (isDev) { Log.Information($"Loading dev plugin {name}"); - var devPlugin = new LocalDevPlugin(dllFile, manifest); + plugin = new LocalDevPlugin(dllFile, manifest); + } + else + { + Log.Information($"Loading plugin {name}"); + plugin = new LocalPlugin(dllFile, manifest); + } + + // Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here. + if (plugin.Manifest.WorkingPluginId == Guid.Empty) + throw new Exception("Plugin should have a WorkingPluginId at this point"); + this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId); + + // Now, if this is a devPlugin, figure out if we want to load it + if (isDev) + { + var devPlugin = (LocalDevPlugin)plugin; loadPlugin &= !isBoot; - var probablyInternalNameForThisPurpose = manifest?.InternalName ?? dllFile.Name; - var wantsInDefaultProfile = - this.profileManager.DefaultProfile.WantsPlugin(probablyInternalNameForThisPurpose); + this.profileManager.DefaultProfile.WantsPlugin(plugin.Manifest.WorkingPluginId); if (wantsInDefaultProfile == null) { // We don't know about this plugin, so we don't want to do anything here. @@ -1305,46 +1319,41 @@ internal partial class PluginManager : IDisposable, IServiceType else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) { // We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled. - Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot) { // We wanted this plugin, and StartOnBoot is on. That means we actually do want it. - Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, true, false); + Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); loadPlugin = !doNotLoad; } else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot) { // We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore. - Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot) { // We didn't want this plugin, and StartOnBoot is off. We don't want it. - Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", probablyInternalNameForThisPurpose); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(probablyInternalNameForThisPurpose, false, false); + Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", plugin.Manifest.InternalName); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); loadPlugin = false; } plugin = devPlugin; } - else - { - Log.Information($"Loading plugin {name}"); - plugin = new LocalPlugin(dllFile, manifest); - } #pragma warning disable CS0618 var defaultState = manifest?.Disabled != true && loadPlugin; #pragma warning restore CS0618 - + // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.InternalName, defaultState); + var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, defaultState); if (loadPlugin) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index ac46d9153..657cde534 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -102,7 +102,7 @@ internal class Profile /// Gets all plugins declared in this profile. /// public IEnumerable Plugins => - this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.IsEnabled)); + this.modelV1.Plugins.Select(x => new ProfilePluginEntry(x.InternalName, x.WorkingPluginId, x.IsEnabled)); /// /// Gets this profile's underlying model. @@ -144,11 +144,11 @@ internal class Profile /// /// The internal name of the plugin. /// Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled. - public bool? WantsPlugin(string internalName) + public bool? WantsPlugin(Guid workingPluginId) { lock (this) { - var entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + var entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); return entry?.IsEnabled; } } @@ -161,13 +161,13 @@ internal class Profile /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. - public async Task AddOrUpdateAsync(string internalName, bool state, bool apply = true) + public async Task AddOrUpdateAsync(Guid workingPluginId, bool state, bool apply = true) { - Debug.Assert(!internalName.IsNullOrEmpty(), "!internalName.IsNullOrEmpty()"); - + Debug.Assert(workingPluginId != Guid.Empty, "Trying to add plugin with empty guid"); + lock (this) { - var existing = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + var existing = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); if (existing != null) { existing.IsEnabled = state; @@ -176,16 +176,16 @@ internal class Profile { this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin { - InternalName = internalName, + WorkingPluginId = workingPluginId, IsEnabled = state, }); } } // We need to remove this plugin from the default profile, if it declares it. - if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(internalName) != null) + if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(workingPluginId) != null) { - await this.manager.DefaultProfile.RemoveAsync(internalName, false); + await this.manager.DefaultProfile.RemoveAsync(workingPluginId, false); } Service.Get().QueueSave(); @@ -201,25 +201,25 @@ internal class Profile /// The internal name of the plugin. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. - public async Task RemoveAsync(string internalName, bool apply = true) + public async Task RemoveAsync(Guid workingPluginId, bool apply = true) { ProfileModelV1.ProfileModelV1Plugin entry; lock (this) { - entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); + entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); if (entry == null) - throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\""); + throw new ArgumentException($"No plugin \"{workingPluginId}\" in profile \"{this.Guid}\""); if (!this.modelV1.Plugins.Remove(entry)) throw new Exception("Couldn't remove plugin from model collection"); } // We need to add this plugin back to the default profile, if we were the last profile to have it. - if (!this.manager.IsInAnyProfile(internalName)) + if (!this.manager.IsInAnyProfile(workingPluginId)) { if (!this.IsDefaultProfile) { - await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, this.IsEnabled && entry.IsEnabled, false); + await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, this.IsEnabled && entry.IsEnabled, false); } else { @@ -233,6 +233,30 @@ internal class Profile await this.manager.ApplyAllWantStatesAsync(); } + /// + /// This function tries to migrate all plugins with this internalName which do not have + /// a GUID to the specified GUID. + /// This is best-effort and will probably work well for anyone that only uses regular plugins. + /// + /// InternalName of the plugin to migrate. + /// Guid to use. + public void MigrateProfilesToGuidsForPlugin(string internalName, Guid newGuid) + { + lock (this) + { + foreach (var plugin in this.modelV1.Plugins) + { + if (plugin.InternalName == internalName && plugin.WorkingPluginId == Guid.Empty) + { + plugin.WorkingPluginId = newGuid; + Log.Information("Migrated profile {Profile} plugin {Name} to guid {Guid}", this, internalName, newGuid); + } + } + } + + Service.Get().QueueSave(); + } + /// public override string ToString() => $"{this.Guid} ({this.Name})"; } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 46b572c1a..1d14ade4b 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -73,7 +73,7 @@ internal class ProfileManager : IServiceType /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. - public async Task GetWantStateAsync(string internalName, bool defaultState, bool addIfNotDeclared = true) + public async Task GetWantStateAsync(Guid workingPluginId, bool defaultState, bool addIfNotDeclared = true) { var want = false; var wasInAnyProfile = false; @@ -82,7 +82,7 @@ internal class ProfileManager : IServiceType { foreach (var profile in this.profiles) { - var state = profile.WantsPlugin(internalName); + var state = profile.WantsPlugin(workingPluginId); if (state.HasValue) { want = want || (profile.IsEnabled && state.Value); @@ -93,8 +93,8 @@ internal class ProfileManager : IServiceType if (!wasInAnyProfile && addIfNotDeclared) { - Log.Warning("{Name} was not in any profile, adding to default with {Default}", internalName, defaultState); - await this.DefaultProfile.AddOrUpdateAsync(internalName, defaultState, false); + Log.Warning("{Guid} was not in any profile, adding to default with {Default}", workingPluginId, defaultState); + await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, defaultState, false); return defaultState; } @@ -107,10 +107,10 @@ internal class ProfileManager : IServiceType /// /// The internal name of the plugin. /// Whether or not the plugin is in any profile. - public bool IsInAnyProfile(string internalName) + public bool IsInAnyProfile(Guid workingPluginId) { lock (this.profiles) - return this.profiles.Any(x => x.WantsPlugin(internalName) != null); + return this.profiles.Any(x => x.WantsPlugin(workingPluginId) != null); } /// @@ -119,8 +119,8 @@ internal class ProfileManager : IServiceType /// /// The internal name of the plugin. /// Whether or not the plugin is in the default profile. - public bool IsInDefaultProfile(string internalName) - => this.DefaultProfile.WantsPlugin(internalName) != null; + public bool IsInDefaultProfile(Guid workingPluginId) + => this.DefaultProfile.WantsPlugin(workingPluginId) != null; /// /// Add a new profile. @@ -151,7 +151,7 @@ internal class ProfileManager : IServiceType /// The newly cloned profile. public Profile CloneProfile(Profile toClone) { - var newProfile = this.ImportProfile(toClone.Model.Serialize()); + var newProfile = this.ImportProfile(toClone.Model.SerializeForShare()); if (newProfile == null) throw new Exception("New profile was null while cloning"); @@ -196,13 +196,13 @@ internal class ProfileManager : IServiceType this.isBusy = true; Log.Information("Getting want states..."); - List wantActive; + List wantActive; lock (this.profiles) { wantActive = this.profiles .Where(x => x.IsEnabled) .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled) - .Select(plugin => plugin.InternalName)) + .Select(plugin => plugin.WorkingPluginId)) .Distinct().ToList(); } @@ -218,7 +218,7 @@ internal class ProfileManager : IServiceType var pm = Service.Get(); foreach (var installedPlugin in pm.InstalledPlugins) { - var wantThis = wantActive.Contains(installedPlugin.Manifest.InternalName); + var wantThis = wantActive.Contains(installedPlugin.Manifest.WorkingPluginId); switch (wantThis) { case true when !installedPlugin.IsLoaded: @@ -267,7 +267,7 @@ internal class ProfileManager : IServiceType // We need to remove all plugins from the profile first, so that they are re-added to the default profile if needed foreach (var plugin in profile.Plugins.ToArray()) { - await profile.RemoveAsync(plugin.InternalName, false); + await profile.RemoveAsync(plugin.WorkingPluginId, false); } if (!this.config.SavedProfiles!.Remove(profile.Model)) @@ -279,6 +279,22 @@ internal class ProfileManager : IServiceType this.config.QueueSave(); } + /// + /// This function tries to migrate all plugins with this internalName which do not have + /// a GUID to the specified GUID. + /// This is best-effort and will probably work well for anyone that only uses regular plugins. + /// + /// InternalName of the plugin to migrate. + /// Guid to use. + public void MigrateProfilesToGuidsForPlugin(string internalName, Guid newGuid) + { + lock (this.profiles) + { + foreach (var profile in this.profiles) + profile.MigrateProfilesToGuidsForPlugin(internalName, newGuid); + } + } + private string GenerateUniqueProfileName(string startingWith) { if (this.profiles.All(x => x.Name != startingWith)) diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs index bf2a9c2c9..d77cab443 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs @@ -1,7 +1,9 @@ using System; - +using System.Collections.Generic; +using System.Reflection; using Dalamud.Utility; using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; namespace Dalamud.Plugin.Internal.Profiles; @@ -39,11 +41,11 @@ public abstract class ProfileModel } /// - /// Serialize this model into a string usable for sharing. + /// Serialize this model into a string usable for sharing, without including GUIDs. /// /// The serialized representation of the model. /// Thrown when an unsupported model is serialized. - public string Serialize() + public string SerializeForShare() { string prefix; switch (this) @@ -55,6 +57,32 @@ public abstract class ProfileModel throw new ArgumentOutOfRangeException(); } - return prefix + Convert.ToBase64String(Util.CompressString(JsonConvert.SerializeObject(this))); + // HACK: Just filter the ID for now, we should split the sharing + saving model + var serialized = JsonConvert.SerializeObject(this, new JsonSerializerSettings() + { ContractResolver = new IgnorePropertiesResolver(new[] { "WorkingPluginId" }) }); + + return prefix + Convert.ToBase64String(Util.CompressString(serialized)); + } + + // Short helper class to ignore some properties from serialization + private class IgnorePropertiesResolver : DefaultContractResolver + { + private readonly HashSet ignoreProps; + + public IgnorePropertiesResolver(IEnumerable propNamesToIgnore) + { + this.ignoreProps = new HashSet(propNamesToIgnore); + } + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var property = base.CreateProperty(member, memberSerialization); + if (this.ignoreProps.Contains(property.PropertyName)) + { + property.ShouldSerialize = _ => false; + } + + return property; + } } } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index 2a851d234..1b224c8dc 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -46,6 +46,8 @@ public class ProfileModelV1 : ProfileModel /// Gets or sets the internal name of the plugin. /// public string? InternalName { get; set; } + + public Guid WorkingPluginId { get; set; } /// /// Gets or sets a value indicating whether or not this entry is enabled. diff --git a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs index 0a6f5140b..2c10def99 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs @@ -10,9 +10,10 @@ internal class ProfilePluginEntry /// /// The internal name of the plugin. /// A value indicating whether or not this entry is enabled. - public ProfilePluginEntry(string internalName, bool state) + public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state) { this.InternalName = internalName; + this.WorkingPluginId = workingPluginId; this.IsEnabled = state; } @@ -20,6 +21,8 @@ internal class ProfilePluginEntry /// Gets the internal name of the plugin. /// public string InternalName { get; } + + public Guid WorkingPluginId { get; set; } /// /// Gets a value indicating whether or not this entry is enabled. diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index f7306b5a7..8abfd2f9f 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -235,7 +235,7 @@ internal class LocalPlugin : IDisposable /// INCLUDES the default profile. /// public bool IsWantedByAnyProfile => - Service.Get().GetWantStateAsync(this.manifest.InternalName, false, false).GetAwaiter().GetResult(); + Service.Get().GetWantStateAsync(this.manifest.WorkingPluginId, false, false).GetAwaiter().GetResult(); /// /// Gets a value indicating whether this plugin's API level is out of date. From a9f6d6d104080148068c2b422968135ea439ce47 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:04:08 +0200 Subject: [PATCH 151/585] chore: ModuleLog fmt objects may be nullable --- Dalamud/Logging/Internal/ModuleLog.cs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index 2fb735640..baa2708ac 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -33,7 +33,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Verbose(string messageTemplate, params object[] values) + public void Verbose(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, null, values); /// @@ -42,7 +42,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Verbose(Exception exception, string messageTemplate, params object[] values) + public void Verbose(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values); /// @@ -50,7 +50,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Debug(string messageTemplate, params object[] values) + public void Debug(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, null, values); /// @@ -59,7 +59,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Debug(Exception exception, string messageTemplate, params object[] values) + public void Debug(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values); /// @@ -67,7 +67,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Information(string messageTemplate, params object[] values) + public void Information(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, null, values); /// @@ -76,7 +76,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Information(Exception exception, string messageTemplate, params object[] values) + public void Information(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values); /// @@ -84,7 +84,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Warning(string messageTemplate, params object[] values) + public void Warning(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, null, values); /// @@ -93,7 +93,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Warning(Exception exception, string messageTemplate, params object[] values) + public void Warning(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values); /// @@ -101,7 +101,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Error(string messageTemplate, params object[] values) + public void Error(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, null, values); /// @@ -110,7 +110,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Error(Exception? exception, string messageTemplate, params object[] values) + public void Error(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values); /// @@ -118,7 +118,7 @@ public class ModuleLog /// /// The message template. /// Values to log. - public void Fatal(string messageTemplate, params object[] values) + public void Fatal(string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, null, values); /// @@ -127,11 +127,11 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Fatal(Exception exception, string messageTemplate, params object[] values) + public void Fatal(Exception exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); private void WriteLog( - LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) + LogEventLevel level, string messageTemplate, Exception? exception = null, params object?[] values) { // FIXME: Eventually, the `pluginName` tag should be removed from here and moved over to the actual log // formatter. From 6e54c085fa32a14a11699f6cea86b114f81c8394 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:08:58 +0200 Subject: [PATCH 152/585] fix: find matching plugins when importing a profile --- Dalamud/Plugin/Internal/PluginManager.cs | 3 +++ Dalamud/Plugin/Internal/Profiles/Profile.cs | 4 ++++ .../Internal/Profiles/ProfileManager.cs | 20 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index f782b4129..37dab0f03 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1299,6 +1299,9 @@ internal partial class PluginManager : IDisposable, IServiceType } // Perform a migration from InternalName to GUIDs. The plugin should definitely have a GUID here. + // This will also happen if you are installing a plugin with the installer, and that's intended! + // It means that, if you have a profile which has unsatisfied plugins, installing a matching plugin will + // enter it into the profiles it can match. if (plugin.Manifest.WorkingPluginId == Guid.Empty) throw new Exception("Plugin should have a WorkingPluginId at this point"); this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId); diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 657cde534..b9c90235a 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -246,6 +246,10 @@ internal class Profile { foreach (var plugin in this.modelV1.Plugins) { + // TODO: What should happen if a profile has a GUID locked in, but the plugin + // is not installed anymore? That probably means that the user uninstalled the plugin + // and is now reinstalling it. We should still satisfy that and update the ID. + if (plugin.InternalName == internalName && plugin.WorkingPluginId == Guid.Empty) { plugin.WorkingPluginId = newGuid; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 1d14ade4b..d8f091e9f 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -172,7 +172,27 @@ internal class ProfileManager : IServiceType newModel.Guid = Guid.NewGuid(); newModel.Name = this.GenerateUniqueProfileName(newModel.Name.IsNullOrEmpty() ? "Unknown Collection" : newModel.Name); if (newModel is ProfileModelV1 modelV1) + { + // Disable it modelV1.IsEnabled = false; + + // Try to find matching plugins for all plugins in the profile + var pm = Service.Get(); + foreach (var plugin in modelV1.Plugins) + { + var installedPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName); + if (installedPlugin != null) + { + Log.Information("Satisfying plugin {InternalName} for profile {Name} with {Guid}", plugin.InternalName, newModel.Name, installedPlugin.Manifest.WorkingPluginId); + plugin.WorkingPluginId = installedPlugin.Manifest.WorkingPluginId; + } + else + { + Log.Warning("Couldn't find plugin {InternalName} for profile {Name}", plugin.InternalName, newModel.Name); + plugin.WorkingPluginId = Guid.Empty; + } + } + } this.config.SavedProfiles!.Add(newModel); this.config.QueueSave(); From 8b85139e6198ad22257aa1938d866c8e79a262a7 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:16:54 +0200 Subject: [PATCH 153/585] chore: prevent plugins from being installed twice for now if an assignment is missing --- .../PluginInstaller/ProfileManagerWidget.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 2be074f84..7c9026505 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -406,7 +406,7 @@ internal class ProfileManagerWidget foreach (var plugin in profile.Plugins.ToArray()) { didAny = true; - var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == plugin.InternalName); + var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == plugin.WorkingPluginId); var btnOffset = 2; if (pmPlugin != null) @@ -437,26 +437,33 @@ internal class ProfileManagerWidget ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); ImGui.TextUnformatted(text); - - var available = + + var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == plugin.InternalName); + var installable = pm.AvailablePlugins.FirstOrDefault( x => x.InternalName == plugin.InternalName && !x.SourceRepo.IsThirdParty); - if (available != null) + + if (firstAvailableInstalled != null) + { + // TODO + ImGui.Text("GOAT WAS TOO LAZY TO IMPLEMENT THIS"); + } + else if (installable != null) { ImGui.SameLine(); ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * 2) - 2); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); btnOffset = 3; - if (ImGuiComponents.IconButton($"###installMissingPlugin{available.InternalName}", FontAwesomeIcon.Download)) + if (ImGuiComponents.IconButton($"###installMissingPlugin{installable.InternalName}", FontAwesomeIcon.Download)) { - this.installer.StartInstall(available, false); + this.installer.StartInstall(installable, false); } if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.InstallPlugin); } - + ImGui.SetCursorPos(before); } From a85c6315d4da311b5f9368eeb58358ac530e8f24 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 19:24:30 +0200 Subject: [PATCH 154/585] warnings --- Dalamud/Plugin/Internal/Profiles/Profile.cs | 6 +++--- Dalamud/Plugin/Internal/Profiles/ProfileManager.cs | 6 +++--- Dalamud/Plugin/Internal/Profiles/ProfileModel.cs | 4 ++-- Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs | 3 +++ Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs | 4 ++++ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index b9c90235a..d20b5c6bc 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -142,7 +142,7 @@ internal class Profile /// /// Check if this profile contains a specific plugin, and if it is enabled. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Null if this profile does not declare the plugin, true if the profile declares the plugin and wants it enabled, false if the profile declares the plugin and does not want it enabled. public bool? WantsPlugin(Guid workingPluginId) { @@ -157,7 +157,7 @@ internal class Profile /// Add a plugin to this profile with the desired state, or change the state of a plugin in this profile. /// This will block until all states have been applied. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. @@ -198,7 +198,7 @@ internal class Profile /// Remove a plugin from this profile. /// This will block until all states have been applied. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. public async Task RemoveAsync(Guid workingPluginId, bool apply = true) diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index d8f091e9f..6b51f7535 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -69,7 +69,7 @@ internal class ProfileManager : IServiceType /// /// Check if any enabled profile wants a specific plugin enabled. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. @@ -105,7 +105,7 @@ internal class ProfileManager : IServiceType /// /// Check whether a plugin is declared in any profile. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the plugin is in any profile. public bool IsInAnyProfile(Guid workingPluginId) { @@ -117,7 +117,7 @@ internal class ProfileManager : IServiceType /// Check whether a plugin is only in the default profile. /// A plugin can never be in the default profile if it is in any other profile. /// - /// The internal name of the plugin. + /// The ID of the plugin. /// Whether or not the plugin is in the default profile. public bool IsInDefaultProfile(Guid workingPluginId) => this.DefaultProfile.WantsPlugin(workingPluginId) != null; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs index d77cab443..e3d9e2955 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModel.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Reflection; + using Dalamud.Utility; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index 1b224c8dc..99da4263b 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -47,6 +47,9 @@ public class ProfileModelV1 : ProfileModel /// public string? InternalName { get; set; } + /// + /// Gets or sets an ID uniquely identifying this specific instance of a plugin. + /// public Guid WorkingPluginId { get; set; } /// diff --git a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs index 2c10def99..7909981bc 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs @@ -9,6 +9,7 @@ internal class ProfilePluginEntry /// Initializes a new instance of the class. /// /// The internal name of the plugin. + /// The ID of the plugin. /// A value indicating whether or not this entry is enabled. public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state) { @@ -22,6 +23,9 @@ internal class ProfilePluginEntry /// public string InternalName { get; } + /// + /// Gets or sets an ID uniquely identifying this specific instance of a plugin. + /// public Guid WorkingPluginId { get; set; } /// From f072a6fc40f710dbb30bbfe6b99360714053ee32 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:47:06 +0200 Subject: [PATCH 155/585] Update Dalamud/Hooking/Internal/HookProviderPluginScoped.cs Co-authored-by: Haselnussbomber --- Dalamud/Hooking/Internal/HookProviderPluginScoped.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs index 0e7ef4c7b..0878bce28 100644 --- a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs @@ -80,7 +80,7 @@ internal class HookProviderPluginScoped : IHookProvider, IServiceType, IDisposab /// public Hook FromSignature(string signature, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate - => this.FromAddress(this.scanner.ScanText(signature), detour); + => this.FromAddress(this.scanner.ScanText(signature), detour, backend); /// public void Dispose() From 173e9a3144d2ab93e9ee190da3fcb6694c6e84ec Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 22:07:09 +0200 Subject: [PATCH 156/585] IHookProvider => IGameInteropProvider --- ...d.cs => GameInteropProviderPluginScoped.cs} | 18 +++++++++--------- ...HookProvider.cs => IGameInteropProvider.cs} | 2 +- Dalamud/Utility/Signatures/SignatureHelper.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) rename Dalamud/Hooking/Internal/{HookProviderPluginScoped.cs => GameInteropProviderPluginScoped.cs} (78%) rename Dalamud/Plugin/Services/{IHookProvider.cs => IGameInteropProvider.cs} (99%) diff --git a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs similarity index 78% rename from Dalamud/Hooking/Internal/HookProviderPluginScoped.cs rename to Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index 6fa700cef..d6fc34b10 100644 --- a/Dalamud/Hooking/Internal/HookProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -19,9 +19,9 @@ namespace Dalamud.Hooking.Internal; [InterfaceVersion("1.0")] [ServiceManager.ScopedService] #pragma warning disable SA1015 -[ResolveVia] +[ResolveVia] #pragma warning restore SA1015 -internal class HookProviderPluginScoped : IHookProvider, IServiceType, IDisposable +internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceType, IDisposable { private readonly LocalPlugin plugin; private readonly SigScanner scanner; @@ -29,11 +29,11 @@ internal class HookProviderPluginScoped : IHookProvider, IServiceType, IDisposab private readonly ConcurrentBag trackedHooks = new(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Plugin this instance belongs to. /// SigScanner instance for target module. - public HookProviderPluginScoped(LocalPlugin plugin, SigScanner scanner) + public GameInteropProviderPluginScoped(LocalPlugin plugin, SigScanner scanner) { this.plugin = plugin; this.scanner = scanner; @@ -63,23 +63,23 @@ internal class HookProviderPluginScoped : IHookProvider, IServiceType, IDisposab } /// - public Hook FromSymbol(string moduleName, string exportName, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate + public Hook FromSymbol(string moduleName, string exportName, T detour, IGameInteropProvider.HookBackend backend = IGameInteropProvider.HookBackend.Automatic) where T : Delegate { - var hook = Hook.FromSymbol(moduleName, exportName, detour, backend == IHookProvider.HookBackend.MinHook); + var hook = Hook.FromSymbol(moduleName, exportName, detour, backend == IGameInteropProvider.HookBackend.MinHook); this.trackedHooks.Add(hook); return hook; } /// - public Hook FromAddress(IntPtr procAddress, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate + public Hook FromAddress(IntPtr procAddress, T detour, IGameInteropProvider.HookBackend backend = IGameInteropProvider.HookBackend.Automatic) where T : Delegate { - var hook = Hook.FromAddress(procAddress, detour, backend == IHookProvider.HookBackend.MinHook); + var hook = Hook.FromAddress(procAddress, detour, backend == IGameInteropProvider.HookBackend.MinHook); this.trackedHooks.Add(hook); return hook; } /// - public Hook FromSignature(string signature, T detour, IHookProvider.HookBackend backend = IHookProvider.HookBackend.Automatic) where T : Delegate + public Hook FromSignature(string signature, T detour, IGameInteropProvider.HookBackend backend = IGameInteropProvider.HookBackend.Automatic) where T : Delegate => this.FromAddress(this.scanner.ScanText(signature), detour, backend); /// diff --git a/Dalamud/Plugin/Services/IHookProvider.cs b/Dalamud/Plugin/Services/IGameInteropProvider.cs similarity index 99% rename from Dalamud/Plugin/Services/IHookProvider.cs rename to Dalamud/Plugin/Services/IGameInteropProvider.cs index dc7d29913..9451a60eb 100644 --- a/Dalamud/Plugin/Services/IHookProvider.cs +++ b/Dalamud/Plugin/Services/IGameInteropProvider.cs @@ -8,7 +8,7 @@ namespace Dalamud.Plugin.Services; /// /// Service responsible for the creation of hooks. /// -public interface IHookProvider +public interface IGameInteropProvider { /// /// Available hooking backends. diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index 1cfd18330..f011f8121 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -161,7 +161,7 @@ internal static class SignatureHelper continue; } - var hook = creator.Invoke(null, new object?[] { ptr, detour, IHookProvider.HookBackend.Automatic }) as IDalamudHook; + var hook = creator.Invoke(null, new object?[] { ptr, detour, IGameInteropProvider.HookBackend.Automatic }) as IDalamudHook; info.SetValue(self, hook); createdHooks.Add(hook); From e31234ffeccb863b8a24940bf568d6a1906b2c9f Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 22:09:38 +0200 Subject: [PATCH 157/585] prefix methods with Hook to improve clarity --- .../Internal/GameInteropProviderPluginScoped.cs | 12 ++++++------ Dalamud/Plugin/Services/IGameInteropProvider.cs | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index d6fc34b10..96172e5b2 100644 --- a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -47,7 +47,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT } /// - public Hook FromFunctionPointerVariable(IntPtr address, T detour) where T : Delegate + public Hook HookFromFunctionPointerVariable(IntPtr address, T detour) where T : Delegate { var hook = Hook.FromFunctionPointerVariable(address, detour); this.trackedHooks.Add(hook); @@ -55,7 +55,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT } /// - public Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate + public Hook HookFromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate { var hook = Hook.FromImport(module, moduleName, functionName, hintOrOrdinal, detour); this.trackedHooks.Add(hook); @@ -63,7 +63,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT } /// - public Hook FromSymbol(string moduleName, string exportName, T detour, IGameInteropProvider.HookBackend backend = IGameInteropProvider.HookBackend.Automatic) where T : Delegate + public Hook HookFromSymbol(string moduleName, string exportName, T detour, IGameInteropProvider.HookBackend backend = IGameInteropProvider.HookBackend.Automatic) where T : Delegate { var hook = Hook.FromSymbol(moduleName, exportName, detour, backend == IGameInteropProvider.HookBackend.MinHook); this.trackedHooks.Add(hook); @@ -71,7 +71,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT } /// - public Hook FromAddress(IntPtr procAddress, T detour, IGameInteropProvider.HookBackend backend = IGameInteropProvider.HookBackend.Automatic) where T : Delegate + public Hook HookFromAddress(IntPtr procAddress, T detour, IGameInteropProvider.HookBackend backend = IGameInteropProvider.HookBackend.Automatic) where T : Delegate { var hook = Hook.FromAddress(procAddress, detour, backend == IGameInteropProvider.HookBackend.MinHook); this.trackedHooks.Add(hook); @@ -79,8 +79,8 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT } /// - public Hook FromSignature(string signature, T detour, IGameInteropProvider.HookBackend backend = IGameInteropProvider.HookBackend.Automatic) where T : Delegate - => this.FromAddress(this.scanner.ScanText(signature), detour, backend); + public Hook HookFromSignature(string signature, T detour, IGameInteropProvider.HookBackend backend = IGameInteropProvider.HookBackend.Automatic) where T : Delegate + => this.HookFromAddress(this.scanner.ScanText(signature), detour, backend); /// public void Dispose() diff --git a/Dalamud/Plugin/Services/IGameInteropProvider.cs b/Dalamud/Plugin/Services/IGameInteropProvider.cs index 9451a60eb..186663a9e 100644 --- a/Dalamud/Plugin/Services/IGameInteropProvider.cs +++ b/Dalamud/Plugin/Services/IGameInteropProvider.cs @@ -46,7 +46,7 @@ public interface IGameInteropProvider /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. /// Delegate of detour. - public Hook FromFunctionPointerVariable(IntPtr address, T detour) where T : Delegate; + public Hook HookFromFunctionPointerVariable(IntPtr address, T detour) where T : Delegate; /// /// Creates a hook by rewriting import table address. @@ -58,7 +58,7 @@ public interface IGameInteropProvider /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. /// Delegate of detour. - public Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate; + public Hook HookFromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) where T : Delegate; /// /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. @@ -71,7 +71,7 @@ public interface IGameInteropProvider /// Hooking library to use. /// The hook with the supplied parameters. /// Delegate of detour. - Hook FromSymbol(string moduleName, string exportName, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; + Hook HookFromSymbol(string moduleName, string exportName, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; /// /// Creates a hook. Hooking address is inferred by calling to GetProcAddress() function. @@ -83,7 +83,7 @@ public interface IGameInteropProvider /// Hooking library to use. /// The hook with the supplied parameters. /// Delegate of detour. - Hook FromAddress(IntPtr procAddress, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; + Hook HookFromAddress(IntPtr procAddress, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; /// /// Creates a hook from a signature into the Dalamud target module. @@ -93,5 +93,5 @@ public interface IGameInteropProvider /// Hooking library to use. /// The hook with the supplied parameters. /// Delegate of detour. - Hook FromSignature(string signature, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; + Hook HookFromSignature(string signature, T detour, HookBackend backend = HookBackend.Automatic) where T : Delegate; } From eb2a5f36f9184ce9cb2f4c5e008898c2690794e2 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 21 Sep 2023 22:11:18 +0200 Subject: [PATCH 158/585] fix spelling inconsistency --- Dalamud/Plugin/Services/IGameInteropProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Services/IGameInteropProvider.cs b/Dalamud/Plugin/Services/IGameInteropProvider.cs index 186663a9e..29f42a655 100644 --- a/Dalamud/Plugin/Services/IGameInteropProvider.cs +++ b/Dalamud/Plugin/Services/IGameInteropProvider.cs @@ -36,7 +36,7 @@ public interface IGameInteropProvider /// Initialize members decorated with the . /// Errors for fallible signatures will be logged. /// - /// The object to initialise. + /// The object to initialize. public void InitializeFromAttributes(object self); /// From 0636a03e41bab46300544bfc00f6643648db1ef8 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Sep 2023 15:26:08 -0700 Subject: [PATCH 159/585] Include argument data in event information. --- .../AddonEventManager/AddonEventManager.cs | 2 +- .../AddonArgTypes/AddonDrawArgs.cs | 13 ++++++ .../AddonArgTypes/AddonFinalizeArgs.cs | 13 ++++++ .../AddonArgTypes/AddonRefreshArgs.cs | 23 ++++++++++ .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 23 ++++++++++ .../AddonArgTypes/AddonSetupArgs.cs | 13 ++++++ .../AddonArgTypes/AddonUpdateArgs.cs | 18 ++++++++ Dalamud/Game/AddonLifecycle/AddonArgs.cs | 22 --------- Dalamud/Game/AddonLifecycle/AddonArgsType.cs | 37 +++++++++++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 45 ++++++++++++++----- Dalamud/Game/AddonLifecycle/IAddonArgs.cs | 25 +++++++++++ Dalamud/Game/Gui/Dtr/DtrBar.cs | 4 +- .../AgingSteps/AddonLifecycleAgingStep.cs | 12 ++--- Dalamud/Plugin/Services/IAddonLifecycle.cs | 4 +- 14 files changed, 209 insertions(+), 45 deletions(-) create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs delete mode 100644 Dalamud/Game/AddonLifecycle/AddonArgs.cs create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgsType.cs create mode 100644 Dalamud/Game/AddonLifecycle/IAddonArgs.cs diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 89554074a..730a7a404 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -160,7 +160,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// /// Event type that triggered this call. /// Addon that triggered this call. - private void OnAddonFinalize(AddonEvent eventType, AddonArgs addonInfo) + private void OnAddonFinalize(AddonEvent eventType, IAddonArgs addonInfo) { // It shouldn't be possible for this event to be anything other than PreFinalize. if (eventType != AddonEvent.PreFinalize) return; diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs new file mode 100644 index 000000000..614a7ac2a --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -0,0 +1,13 @@ +namespace Dalamud.Game.Addon.AddonArgTypes; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonDrawArgs : IAddonArgs +{ + /// + public nint Addon { get; init; } + + /// + public AddonArgsType Type => AddonArgsType.Draw; +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs new file mode 100644 index 000000000..aa31fb051 --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -0,0 +1,13 @@ +namespace Dalamud.Game.Addon.AddonArgTypes; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonFinalizeArgs : IAddonArgs +{ + /// + public nint Addon { get; init; } + + /// + public AddonArgsType Type => AddonArgsType.Finalize; +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs new file mode 100644 index 000000000..ab4f37c3c --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -0,0 +1,23 @@ +namespace Dalamud.Game.Addon.AddonArgTypes; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonRefreshArgs : IAddonArgs +{ + /// + public nint Addon { get; init; } + + /// + public AddonArgsType Type => AddonArgsType.Refresh; + + /// + /// Gets the number of AtkValues. + /// + public uint AtkValueCount { get; init; } + + /// + /// Gets the address of the AtkValue array. + /// + public nint AtkValues { get; init; } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs new file mode 100644 index 000000000..dfd0dac5e --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -0,0 +1,23 @@ +namespace Dalamud.Game.Addon.AddonArgTypes; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonRequestedUpdateArgs : IAddonArgs +{ + /// + public nint Addon { get; init; } + + /// + public AddonArgsType Type => AddonArgsType.RequestedUpdate; + + /// + /// Gets the NumberArrayData** for this event. + /// + public nint NumberArrayData { get; init; } + + /// + /// Gets the StringArrayData** for this event. + /// + public nint StringArrayData { get; init; } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs new file mode 100644 index 000000000..a73d11ae2 --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -0,0 +1,13 @@ +namespace Dalamud.Game.Addon.AddonArgTypes; + +/// +/// Addon argument data for Setup events. +/// +public class AddonSetupArgs : IAddonArgs +{ + /// + public nint Addon { get; init; } + + /// + public AddonArgsType Type => AddonArgsType.Setup; +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs new file mode 100644 index 000000000..ede588001 --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Addon.AddonArgTypes; + +/// +/// Addon argument data for Finalize events. +/// +public class AddonUpdateArgs : IAddonArgs +{ + /// + public nint Addon { get; init; } + + /// + public AddonArgsType Type => AddonArgsType.Update; + + /// + /// Gets the time since the last update. + /// + public float TimeDelta { get; init; } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgs.cs deleted file mode 100644 index 4ae306817..000000000 --- a/Dalamud/Game/AddonLifecycle/AddonArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Dalamud.Memory; -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace Dalamud.Game.Addon; - -/// -/// Addon argument data for use in event subscribers. -/// -public unsafe class AddonArgs -{ - private string? addonName; - - /// - /// Gets the name of the addon this args referrers to. - /// - public string AddonName => this.Addon == nint.Zero ? "NullAddon" : this.addonName ??= MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); - - /// - /// Gets the pointer to the addons AtkUnitBase. - /// - required public nint Addon { get; init; } -} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgsType.cs b/Dalamud/Game/AddonLifecycle/AddonArgsType.cs new file mode 100644 index 000000000..ac325229d --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgsType.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Game.Addon; + +/// +/// Enumeration for available AddonLifecycle arg data +/// +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, +} diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 68233eeb8..7f4a4de95 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -3,6 +3,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Dalamud.Game.Addon.AddonArgTypes; using Dalamud.Hooking; using Dalamud.Hooking.Internal; using Dalamud.IoC; @@ -127,7 +128,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRequestedUpdateHook.Enable(); } - private void InvokeListeners(AddonEvent eventType, AddonArgs args) + private void InvokeListeners(AddonEvent eventType, IAddonArgs args) { // Match on string.empty for listeners that want events for all addons. foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) @@ -140,7 +141,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreSetup, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -151,7 +152,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostSetup, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -165,7 +166,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreFinalize, new AddonArgs { Addon = (nint)atkUnitBase[0] }); + this.InvokeListeners(AddonEvent.PreFinalize, new AddonFinalizeArgs { Addon = (nint)atkUnitBase[0] }); } catch (Exception e) { @@ -179,7 +180,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreDraw, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreDraw, new AddonDrawArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -190,7 +191,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostDraw, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostDraw, new AddonDrawArgs { Addon = (nint)addon }); } catch (Exception e) { @@ -202,7 +203,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreUpdate, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); } catch (Exception e) { @@ -213,7 +214,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostUpdate, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); } catch (Exception e) { @@ -225,7 +226,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreRefresh, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreRefresh, new AddonRefreshArgs + { + Addon = (nint)addon, + AtkValueCount = valueCount, + AtkValues = (nint)values, + }); } catch (Exception e) { @@ -236,7 +242,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostRefresh, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostRefresh, new AddonRefreshArgs + { + Addon = (nint)addon, + AtkValueCount = valueCount, + AtkValues = (nint)values, + }); } catch (Exception e) { @@ -250,7 +261,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonRequestedUpdateArgs + { + Addon = (nint)addon, + NumberArrayData = (nint)numberArrayData, + StringArrayData = (nint)stringArrayData, + }); } catch (Exception e) { @@ -261,7 +277,12 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonRequestedUpdateArgs + { + Addon = (nint)addon, + NumberArrayData = (nint)numberArrayData, + StringArrayData = (nint)stringArrayData, + }); } catch (Exception e) { diff --git a/Dalamud/Game/AddonLifecycle/IAddonArgs.cs b/Dalamud/Game/AddonLifecycle/IAddonArgs.cs new file mode 100644 index 000000000..ba77a2c6d --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/IAddonArgs.cs @@ -0,0 +1,25 @@ +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon; + +/// +/// Interface representing the argument data for AddonLifecycle events. +/// +public unsafe interface IAddonArgs +{ + /// + /// Gets the name of the addon this args referrers to. + /// + string AddonName => this.Addon == nint.Zero ? "NullAddon" : MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + nint Addon { get; init; } + + /// + /// Gets the type of these args. + /// + AddonArgsType Type { get; } +} diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 880bc0625..6b74e47cd 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -255,7 +255,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } - private void OnDtrPostDraw(AddonEvent eventType, AddonArgs addonInfo) + private void OnDtrPostDraw(AddonEvent eventType, IAddonArgs addonInfo) { var addon = (AtkUnitBase*)addonInfo.Addon; @@ -300,7 +300,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } - private void OnAddonRequestedUpdateDetour(AddonEvent eventType, AddonArgs addonInfo) + private void OnAddonRequestedUpdateDetour(AddonEvent eventType, IAddonArgs addonInfo) { var addon = (AtkUnitBase*)addonInfo.Addon; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs index a9948430f..0821e62de 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -100,32 +100,32 @@ internal class AddonLifecycleAgingStep : IAgingStep } } - private void PostSetup(AddonEvent eventType, AddonArgs addonInfo) + private void PostSetup(AddonEvent eventType, IAddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterSetup) this.currentStep++; } - private void PostUpdate(AddonEvent eventType, AddonArgs addonInfo) + private void PostUpdate(AddonEvent eventType, IAddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterUpdate) this.currentStep++; } - private void PostDraw(AddonEvent eventType, AddonArgs addonInfo) + private void PostDraw(AddonEvent eventType, IAddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterDraw) this.currentStep++; } - private void PostRefresh(AddonEvent eventType, AddonArgs addonInfo) + private void PostRefresh(AddonEvent eventType, IAddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterRefresh) this.currentStep++; } - private void PostRequestedUpdate(AddonEvent eventType, AddonArgs addonInfo) + private void PostRequestedUpdate(AddonEvent eventType, IAddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterRequestedUpdate) this.currentStep++; } - private void PreFinalize(AddonEvent eventType, AddonArgs addonInfo) + private void PreFinalize(AddonEvent eventType, IAddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterFinalize) this.currentStep++; } diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index e455754a1..5290395ab 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -14,8 +14,8 @@ public interface IAddonLifecycle /// Delegate for receiving addon lifecycle event messages. /// /// The event type that triggered the message. - /// Information about what addon triggered the message. - public delegate void AddonEventDelegate(AddonEvent eventType, AddonArgs addonInfo); + /// Information about what addon triggered the message. + public delegate void AddonEventDelegate(AddonEvent eventType, IAddonArgs args); /// /// Register a listener that will trigger on the specified event and any of the specified addons. From bd81d230972402d992ced89888aaa5d959ebf700 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Sep 2023 17:05:27 -0700 Subject: [PATCH 160/585] Use CallHook for AddonSetup --- .../AddonArgTypes/AddonSetupArgs.cs | 10 +++++++ Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 26 ++++++++++++------- .../AddonLifecycleAddressResolver.cs | 2 +- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs index a73d11ae2..4b467deb8 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -10,4 +10,14 @@ public class AddonSetupArgs : IAddonArgs /// public AddonArgsType Type => AddonArgsType.Setup; + + /// + /// Gets the number of AtkValues. + /// + public uint AtkValueCount { get; init; } + + /// + /// Gets the address of the AtkValue array. + /// + public nint AtkValues { get; init; } } diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 7f4a4de95..75b5b3753 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -27,7 +27,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly Framework framework = Service.Get(); private readonly AddonLifecycleAddressResolver address; - private readonly Hook onAddonSetupHook; + private readonly CallHook onAddonSetupHook; private readonly Hook onAddonFinalizeHook; private readonly CallHook onAddonDrawHook; private readonly CallHook onAddonUpdateHook; @@ -46,7 +46,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.framework.Update += this.OnFrameworkUpdate; - this.onAddonSetupHook = Hook.FromAddress(this.address.AddonSetup, this.OnAddonSetup); + this.onAddonSetupHook = new CallHook(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); @@ -54,7 +54,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); } - private delegate nint AddonSetupDelegate(AtkUnitBase* addon); + private delegate void AddonSetupDelegate(AtkUnitBase* addon, uint valueCount, AtkValue* values); private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); @@ -137,29 +137,37 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - private nint OnAddonSetup(AtkUnitBase* addon) + private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { try { - this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs() + { + Addon = (nint)addon, + AtkValueCount = valueCount, + AtkValues = (nint)values, + }); } catch (Exception e) { Log.Error(e, "Exception in OnAddonSetup pre-setup invoke."); } - var result = this.onAddonSetupHook.Original(addon); + addon->OnSetup(valueCount, values); try { - this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs { Addon = (nint)addon }); + this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs() + { + Addon = (nint)addon, + AtkValueCount = valueCount, + AtkValues = (nint)values, + }); } catch (Exception e) { Log.Error(e, "Exception in OnAddonSetup post-setup invoke."); } - - return result; } private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs index d68fee9ed..16fd54832 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs @@ -41,7 +41,7 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver /// The signature scanner to facilitate setup. protected override void Setup64Bit(SigScanner sig) { - this.AddonSetup = sig.ScanText("E8 ?? ?? ?? ?? 8B 83 ?? ?? ?? ?? C1 E8 14"); + this.AddonSetup = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 93 ?? ?? ?? ?? 80 8B"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); From 3b5995e6abc855ad18ff95a6fd1495058976a5d8 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:46:44 -0700 Subject: [PATCH 161/585] Fix DTR Null Reference on first login --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 880bc0625..64c3e16b3 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -172,7 +172,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar this.HandleAddedNodes(); var dtr = this.GetDtr(); - if (dtr == null) return; + if (dtr == null || dtr->RootNode == null || dtr->RootNode->ChildNode == null) return; // The collision node on the DTR element is always the width of its content if (dtr->UldManager.NodeList == null) return; From 26838d9f5c9dba482c39ba06a75683e66837fd8c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:47:49 -0700 Subject: [PATCH 162/585] Auto generate paramkeys and return handles to events. --- .../Game/AddonEventManager/AddonEventEntry.cs | 11 ++++- .../AddonEventManager/AddonEventHandle.cs | 21 +++++++++ .../AddonEventManager/AddonEventManager.cs | 27 ++++++------ .../AddonEventManager/IAddonEventHandle.cs | 29 ++++++++++++ .../PluginEventController.cs | 44 ++++++++++++++----- Dalamud/Game/Gui/Dtr/DtrBar.cs | 27 +++++++----- Dalamud/Plugin/Services/IAddonEventManager.cs | 8 ++-- 7 files changed, 127 insertions(+), 40 deletions(-) create mode 100644 Dalamud/Game/AddonEventManager/AddonEventHandle.cs create mode 100644 Dalamud/Game/AddonEventManager/IAddonEventHandle.cs diff --git a/Dalamud/Game/AddonEventManager/AddonEventEntry.cs b/Dalamud/Game/AddonEventManager/AddonEventEntry.cs index 83f1a724c..48c3feb24 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventEntry.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventEntry.cs @@ -1,4 +1,6 @@ -using Dalamud.Memory; +using System; + +using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -46,9 +48,14 @@ internal unsafe class AddonEventEntry /// Gets the event type for this event. /// required public AddonEventType EventType { get; init; } + + /// + /// Gets the event handle for this event. + /// + required internal IAddonEventHandle Handle { get; init; } /// /// Gets the formatted log string for this AddonEventEntry. /// - internal string LogString => $"ParamKey: {this.ParamKey}, Addon: {this.AddonName}, Event: {this.EventType}"; + internal string LogString => $"ParamKey: {this.ParamKey}, Addon: {this.AddonName}, Event: {this.EventType}, GUID: {this.Handle.EventGuid}"; } diff --git a/Dalamud/Game/AddonEventManager/AddonEventHandle.cs b/Dalamud/Game/AddonEventManager/AddonEventHandle.cs new file mode 100644 index 000000000..48abba9a0 --- /dev/null +++ b/Dalamud/Game/AddonEventManager/AddonEventHandle.cs @@ -0,0 +1,21 @@ +using System; + +namespace Dalamud.Game.Addon; + +/// +/// Class that represents a addon event handle. +/// +public class AddonEventHandle : IAddonEventHandle +{ + /// + public uint ParamKey { get; init; } + + /// + public string AddonName { get; init; } = "NullAddon"; + + /// + public AddonEventType EventType { get; init; } + + /// + public Guid EventGuid { get; init; } +} diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 89554074a..dfc037e23 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -77,33 +77,32 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// Registers an event handler for the specified addon, node, and type. /// /// Unique ID for this plugin. - /// Unique Id for this event, maximum 0x10000. /// The parent addon for this event. /// The node that will trigger this event. /// The event type for this event. /// The handler to call when event is triggered. - internal void AddEvent(string pluginId, uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + /// IAddonEventHandle used to remove the event. + internal IAddonEventHandle? AddEvent(string pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) { if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) { - eventController.AddEvent(eventId, atkUnitBase, atkResNode, eventType, eventHandler); - } - else - { - Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); + return eventController.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler); } + + Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); + return null; } /// /// Unregisters an event handler with the specified event id and event type. /// /// Unique ID for this plugin. - /// The Unique Id for this event. - internal void RemoveEvent(string pluginId, uint eventId) + /// The Unique Id for this event. + internal void RemoveEvent(string pluginId, IAddonEventHandle eventHandle) { if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) { - eventController.RemoveEvent(eventId); + eventController.RemoveEvent(eventHandle); } else { @@ -239,12 +238,12 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon } /// - public void AddEvent(uint eventId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) - => this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventId, atkUnitBase, atkResNode, eventType, eventHandler); + public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + => this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId.ToString(), atkUnitBase, atkResNode, eventType, eventHandler); /// - public void RemoveEvent(uint eventId) - => this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventId); + public void RemoveEvent(IAddonEventHandle eventHandle) + => this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventHandle); /// public void SetCursor(AddonCursorType cursor) diff --git a/Dalamud/Game/AddonEventManager/IAddonEventHandle.cs b/Dalamud/Game/AddonEventManager/IAddonEventHandle.cs new file mode 100644 index 000000000..3b2c5c3ae --- /dev/null +++ b/Dalamud/Game/AddonEventManager/IAddonEventHandle.cs @@ -0,0 +1,29 @@ +using System; + +namespace Dalamud.Game.Addon; + +/// +/// Interface representing the data used for managing AddonEvents. +/// +public interface IAddonEventHandle +{ + /// + /// Gets the param key associated with this event. + /// + public uint ParamKey { get; init; } + + /// + /// Gets the name of the addon that this event was attached to. + /// + public string AddonName { get; init; } + + /// + /// Gets the event type associated with this handle. + /// + public AddonEventType EventType { get; init; } + + /// + /// Gets the unique ID for this handle. + /// + public Guid EventGuid { get; init; } +} diff --git a/Dalamud/Game/AddonEventManager/PluginEventController.cs b/Dalamud/Game/AddonEventManager/PluginEventController.cs index 852d78128..b66bbc99e 100644 --- a/Dalamud/Game/AddonEventManager/PluginEventController.cs +++ b/Dalamud/Game/AddonEventManager/PluginEventController.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Game.Gui; using Dalamud.Logging.Internal; +using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -39,17 +40,27 @@ internal unsafe class PluginEventController : IDisposable /// /// Adds a tracked event. /// - /// Unique ID of the event to add. /// The Parent addon for the event. /// The Node for the event. /// The Event Type. /// The delegate to call when invoking this event. - public void AddEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler) + /// IAddonEventHandle used to remove the event. + public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler) { var node = (AtkResNode*)atkResNode; var addon = (AtkUnitBase*)atkUnitBase; var eventType = (AtkEventType)atkEventType; - + var eventId = this.GetNextParamKey(); + var eventGuid = Guid.NewGuid(); + + var eventHandle = new AddonEventHandle + { + AddonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name), + ParamKey = eventId, + EventType = atkEventType, + EventGuid = eventGuid, + }; + var eventEntry = new AddonEventEntry { Addon = atkUnitBase, @@ -57,22 +68,25 @@ internal unsafe class PluginEventController : IDisposable Node = atkResNode, EventType = atkEventType, ParamKey = eventId, + Handle = eventHandle, }; - Log.Verbose($"Adding Event: {eventEntry.LogString}"); + Log.Verbose($"Adding Event. {eventEntry.LogString}"); this.EventListener.RegisterEvent(addon, node, eventType, eventId); this.Events.Add(eventEntry); + + return eventHandle; } /// /// Removes a tracked event, also attempts to un-attach the event from native. /// - /// Unique ID of the event to remove. - public void RemoveEvent(uint eventId) + /// Unique ID of the event to remove. + public void RemoveEvent(IAddonEventHandle handle) { - if (this.Events.FirstOrDefault(registeredEvent => registeredEvent.ParamKey == eventId) is not { } targetEvent) return; + if (this.Events.FirstOrDefault(registeredEvent => registeredEvent.Handle == handle) is not { } targetEvent) return; - Log.Verbose($"Removing Event: {targetEvent.LogString}"); + Log.Verbose($"Removing Event. {targetEvent.LogString}"); this.TryRemoveEventFromNative(targetEvent); this.Events.Remove(targetEvent); } @@ -89,7 +103,7 @@ internal unsafe class PluginEventController : IDisposable foreach (var registeredEvent in events) { - this.RemoveEvent(registeredEvent.ParamKey); + this.RemoveEvent(registeredEvent.Handle); } } } @@ -99,11 +113,21 @@ internal unsafe class PluginEventController : IDisposable { foreach (var registeredEvent in this.Events.ToList()) { - this.RemoveEvent(registeredEvent.ParamKey); + this.RemoveEvent(registeredEvent.Handle); } this.EventListener.Dispose(); } + + private uint GetNextParamKey() + { + for (var i = 0u; i < uint.MaxValue; ++i) + { + if (this.Events.All(registeredEvent => registeredEvent.ParamKey != i)) return i; + } + + throw new OverflowException($"uint.MaxValue number of ParamKeys used for {this.PluginId}"); + } /// /// Attempts to remove a tracked event from native UI. diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 64c3e16b3..390f58b1e 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -25,9 +25,6 @@ namespace Dalamud.Game.Gui.Dtr; public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { private const uint BaseNodeId = 1000; - private const uint MouseOverEventIdOffset = 10000; - private const uint MouseOutEventIdOffset = 20000; - private const uint MouseClickEventIdOffset = 30000; private static readonly ModuleLog Log = new("DtrBar"); @@ -51,6 +48,8 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private readonly ConcurrentBag newEntries = new(); private readonly List entries = new(); + + private readonly Dictionary> eventHandles = new(); private uint runningNodeIds = BaseNodeId; @@ -328,6 +327,11 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private void RecreateNodes() { this.runningNodeIds = BaseNodeId; + if (this.entries.Any()) + { + this.eventHandles.Clear(); + } + foreach (var entry in this.entries) { entry.TextNode = this.MakeNode(++this.runningNodeIds); @@ -362,10 +366,14 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; - this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler); - this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler); - this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler); - + this.eventHandles.TryAdd(node->AtkResNode.NodeID, new List()); + this.eventHandles[node->AtkResNode.NodeID].AddRange(new List + { + this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOver, this.DtrEventHandler), + this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseOut, this.DtrEventHandler), + this.uiEventManager.AddEvent(AddonEventManager.DalamudInternalKey, (nint)dtr, (nint)node, AddonEventType.MouseClick, this.DtrEventHandler), + }); + var lastChild = dtr->RootNode->ChildNode; while (lastChild->PrevSiblingNode != null) lastChild = lastChild->PrevSiblingNode; Log.Debug($"Found last sibling: {(ulong)lastChild:X}"); @@ -387,9 +395,8 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return; - this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOverEventIdOffset); - this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseOutEventIdOffset); - this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, node->AtkResNode.NodeID + MouseClickEventIdOffset); + this.eventHandles[node->AtkResNode.NodeID].ForEach(handle => this.uiEventManager.RemoveEvent(AddonEventManager.DalamudInternalKey, handle)); + this.eventHandles[node->AtkResNode.NodeID].Clear(); var tmpPrevNode = node->AtkResNode.PrevSiblingNode; var tmpNextNode = node->AtkResNode.NextSiblingNode; diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index f3588f469..52f836b4f 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -18,18 +18,18 @@ public interface IAddonEventManager /// /// Registers an event handler for the specified addon, node, and type. /// - /// Unique Id for this event, maximum 0x10000. /// The parent addon for this event. /// The node that will trigger this event. /// The event type for this event. /// The handler to call when event is triggered. - void AddEvent(uint eventId, nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler); + /// IAddonEventHandle used to remove the event. Null if no event was added. + IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler); /// /// Unregisters an event handler with the specified event id and event type. /// - /// The Unique Id for this event. - void RemoveEvent(uint eventId); + /// Unique handle identifying this event. + void RemoveEvent(IAddonEventHandle eventHandle); /// /// Force the game cursor to be the specified cursor. From b742abe77fd0bde0d5cf5657ec28759989bb370c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:55:16 -0700 Subject: [PATCH 163/585] Add ClientStatePluginScoped (#1384) * Add ClientStatePluginScoped * Restore InvokeSafely * Add InvokeSafely for basic Action types. * Set delegates to null to prevent leaking memory * Resolve Merge --- Dalamud/Game/ClientState/ClientState.cs | 137 +++++++++++++++--- Dalamud/Game/DutyState/DutyState.cs | 2 +- .../Game/Network/Internal/NetworkHandlers.cs | 6 +- .../AgingSteps/EnterTerritoryAgingStep.cs | 4 +- .../AgingSteps/LoginEventAgingStep.cs | 2 +- .../AgingSteps/LogoutEventAgingStep.cs | 2 +- Dalamud/Plugin/Services/IClientState.cs | 8 +- Dalamud/Utility/EventHandlerExtensions.cs | 41 +++++- 8 files changed, 165 insertions(+), 37 deletions(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index cef802c81..baf6f6634 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -8,24 +8,25 @@ using Dalamud.Game.Network.Internal; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; -using Serilog; +using Lumina.Excel.GeneratedSheets; + +using Action = System.Action; namespace Dalamud.Game.ClientState; /// /// This class represents the state of the game client at the time of access. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 internal sealed class ClientState : IDisposable, IServiceType, IClientState { + private static readonly ModuleLog Log = new("ClientState"); + private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; private readonly Hook setupTerritoryTypeHook; @@ -37,7 +38,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState private readonly NetworkHandlers networkHandlers = Service.Get(); private bool lastConditionNone = true; - private bool lastFramePvP = false; + private bool lastFramePvP; [ServiceManager.ServiceConstructor] private ClientState(SigScanner sigScanner, DalamudStartInfo startInfo, GameLifecycle lifecycle) @@ -63,22 +64,22 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType); /// - public event EventHandler TerritoryChanged; + public event Action? TerritoryChanged; /// - public event EventHandler Login; + public event Action? Login; /// - public event EventHandler Logout; + public event Action? Logout; /// - public event Action EnterPvP; + public event Action? EnterPvP; /// - public event Action LeavePvP; + public event Action? LeavePvP; /// - public event EventHandler CfPop; + public event Action? CfPop; /// public ClientLanguage ClientLanguage { get; } @@ -128,16 +129,16 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType) { this.TerritoryType = terriType; - this.TerritoryChanged?.InvokeSafely(this, terriType); + this.TerritoryChanged?.InvokeSafely(terriType); Log.Debug("TerritoryType changed: {0}", terriType); return this.setupTerritoryTypeHook.Original(manager, terriType); } - private void NetworkHandlersOnCfPop(object sender, Lumina.Excel.GeneratedSheets.ContentFinderCondition e) + private void NetworkHandlersOnCfPop(ContentFinderCondition e) { - this.CfPop?.InvokeSafely(this, e); + this.CfPop?.InvokeSafely(e); } private void FrameworkOnOnUpdateEvent(IFramework framework1) @@ -149,12 +150,12 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState if (condition == null || gameGui == null || data == null) return; - if (condition.Any() && this.lastConditionNone == true && this.LocalPlayer != null) + if (condition.Any() && this.lastConditionNone && this.LocalPlayer != null) { Log.Debug("Is login"); this.lastConditionNone = false; this.IsLoggedIn = true; - this.Login?.InvokeSafely(this, null); + this.Login?.InvokeSafely(); gameGui.ResetUiHideState(); this.lifecycle.ResetLogout(); @@ -165,7 +166,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState Log.Debug("Is logout"); this.lastConditionNone = true; this.IsLoggedIn = false; - this.Logout?.InvokeSafely(this, null); + this.Logout?.InvokeSafely(); gameGui.ResetUiHideState(); this.lifecycle.SetLogout(); @@ -189,3 +190,103 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState } } } + +/// +/// Plugin-scoped version of a GameConfig service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState +{ + [ServiceManager.ServiceDependency] + private readonly ClientState clientStateService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal ClientStatePluginScoped() + { + this.clientStateService.TerritoryChanged += this.TerritoryChangedForward; + this.clientStateService.Login += this.LoginForward; + this.clientStateService.Logout += this.LogoutForward; + this.clientStateService.EnterPvP += this.EnterPvPForward; + this.clientStateService.LeavePvP += this.ExitPvPForward; + this.clientStateService.CfPop += this.ContentFinderPopForward; + } + + /// + public event Action? TerritoryChanged; + + /// + public event Action? Login; + + /// + public event Action? Logout; + + /// + public event Action? EnterPvP; + + /// + public event Action? LeavePvP; + + /// + public event Action? CfPop; + + /// + public ClientLanguage ClientLanguage => this.clientStateService.ClientLanguage; + + /// + public ushort TerritoryType => this.clientStateService.TerritoryType; + + /// + public PlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer; + + /// + public ulong LocalContentId => this.clientStateService.LocalContentId; + + /// + public bool IsLoggedIn => this.clientStateService.IsLoggedIn; + + /// + public bool IsPvP => this.clientStateService.IsPvP; + + /// + public bool IsPvPExcludingDen => this.clientStateService.IsPvPExcludingDen; + + /// + public bool IsGPosing => this.clientStateService.IsGPosing; + + /// + public void Dispose() + { + this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; + this.clientStateService.Login -= this.LoginForward; + this.clientStateService.Logout -= this.LogoutForward; + this.clientStateService.EnterPvP -= this.EnterPvPForward; + this.clientStateService.LeavePvP -= this.ExitPvPForward; + this.clientStateService.CfPop -= this.ContentFinderPopForward; + + this.TerritoryChanged = null; + this.Login = null; + this.Logout = null; + this.EnterPvP = null; + this.LeavePvP = null; + this.CfPop = null; + } + + private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId); + + private void LoginForward() => this.Login?.Invoke(); + + private void LogoutForward() => this.Logout?.Invoke(); + + private void EnterPvPForward() => this.EnterPvP?.Invoke(); + + private void ExitPvPForward() => this.LeavePvP?.Invoke(); + + private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc); +} diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index c52ceff0f..3890a1f8b 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -120,7 +120,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState return this.contentDirectorNetworkMessageHook.Original(a1, a2, a3); } - private void TerritoryOnChangedEvent(object? sender, ushort e) + private void TerritoryOnChangedEvent(ushort territoryId) { if (this.IsDutyStarted) { diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 1ccf6c6d5..77bf99c1b 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -44,7 +44,7 @@ internal class NetworkHandlers : IDisposable, IServiceType private NetworkHandlers(GameNetwork gameNetwork) { this.uploader = new UniversalisMarketBoardUploader(); - this.CfPop = (_, _) => { }; + this.CfPop = _ => { }; this.messages = Observable.Create(observer => { @@ -75,7 +75,7 @@ internal class NetworkHandlers : IDisposable, IServiceType /// /// Event which gets fired when a duty is ready. /// - public event EventHandler CfPop; + public event Action CfPop; /// /// Disposes of managed and unmanaged resources. @@ -430,7 +430,7 @@ internal class NetworkHandlers : IDisposable, IServiceType Service.GetNullable()?.Print($"Duty pop: {cfcName}"); } - this.CfPop.InvokeSafely(this, cfCondition); + this.CfPop.InvokeSafely(cfCondition); }).ContinueWith( task => Log.Error(task.Exception, "CfPop.Invoke failed"), TaskContinuationOptions.OnlyOnFaulted); diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/EnterTerritoryAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/EnterTerritoryAgingStep.cs index d301cb1ff..4f5c758d6 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/EnterTerritoryAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/EnterTerritoryAgingStep.cs @@ -59,9 +59,9 @@ internal class EnterTerritoryAgingStep : IAgingStep this.subscribed = false; } - private void ClientStateOnTerritoryChanged(object sender, ushort e) + private void ClientStateOnTerritoryChanged(ushort territoryId) { - if (e == this.territory) + if (territoryId == this.territory) { this.hasPassed = true; } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LoginEventAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LoginEventAgingStep.cs index c1dba389f..23b0b903a 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LoginEventAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LoginEventAgingStep.cs @@ -51,7 +51,7 @@ internal class LoginEventAgingStep : IAgingStep } } - private void ClientStateOnOnLogin(object sender, EventArgs e) + private void ClientStateOnOnLogin() { this.hasPassed = true; } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LogoutEventAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LogoutEventAgingStep.cs index 060c0bcc8..c4c6ebfce 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LogoutEventAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LogoutEventAgingStep.cs @@ -51,7 +51,7 @@ internal class LogoutEventAgingStep : IAgingStep } } - private void ClientStateOnOnLogout(object sender, EventArgs e) + private void ClientStateOnOnLogout() { this.hasPassed = true; } diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 881cad841..652a6c888 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -12,17 +12,17 @@ public interface IClientState /// /// Event that gets fired when the current Territory changes. /// - public event EventHandler TerritoryChanged; + public event Action TerritoryChanged; /// /// Event that fires when a character is logging in, and the local character object is available. /// - public event EventHandler Login; + public event Action Login; /// /// Event that fires when a character is logging out. /// - public event EventHandler Logout; + public event Action Logout; /// /// Event that fires when a character is entering PvP. @@ -37,7 +37,7 @@ public interface IClientState /// /// Event that gets fired when a duty is ready. /// - public event EventHandler CfPop; + public event Action CfPop; /// /// Gets the language of the client. diff --git a/Dalamud/Utility/EventHandlerExtensions.cs b/Dalamud/Utility/EventHandlerExtensions.cs index eefd245bb..d05ad6ea5 100644 --- a/Dalamud/Utility/EventHandlerExtensions.cs +++ b/Dalamud/Utility/EventHandlerExtensions.cs @@ -1,12 +1,9 @@ -using System; using System.Linq; using Dalamud.Game; using Dalamud.Plugin.Services; using Serilog; -using static Dalamud.Game.Framework; - namespace Dalamud.Utility; /// @@ -21,7 +18,7 @@ internal static class EventHandlerExtensions /// The EventHandler in question. /// Default sender for Invoke equivalent. /// Default EventArgs for Invoke equivalent. - public static void InvokeSafely(this EventHandler eh, object sender, EventArgs e) + public static void InvokeSafely(this EventHandler? eh, object sender, EventArgs e) { if (eh == null) return; @@ -40,7 +37,7 @@ internal static class EventHandlerExtensions /// Default sender for Invoke equivalent. /// Default EventArgs for Invoke equivalent. /// Type of EventArgs. - public static void InvokeSafely(this EventHandler eh, object sender, T e) + public static void InvokeSafely(this EventHandler? eh, object sender, T e) { if (eh == null) return; @@ -56,7 +53,7 @@ internal static class EventHandlerExtensions /// of a thrown Exception inside of an invocation. /// /// The Action in question. - public static void InvokeSafely(this Action act) + public static void InvokeSafely(this Action? act) { if (act == null) return; @@ -67,13 +64,31 @@ internal static class EventHandlerExtensions } } + /// + /// Replacement for Invoke() on event Actions to catch exceptions that stop event propagation in case + /// of a thrown Exception inside of an invocation. + /// + /// The Action in question. + /// Templated argument for Action. + /// Type of Action args. + public static void InvokeSafely(this Action? act, T argument) + { + if (act == null) + return; + + foreach (var action in act.GetInvocationList().Cast>()) + { + HandleInvoke(action, argument); + } + } + /// /// Replacement for Invoke() on OnUpdateDelegate to catch exceptions that stop event propagation in case /// of a thrown Exception inside of an invocation. /// /// The OnUpdateDelegate in question. /// Framework to be passed on to OnUpdateDelegate. - public static void InvokeSafely(this IFramework.OnUpdateDelegate updateDelegate, Framework framework) + public static void InvokeSafely(this IFramework.OnUpdateDelegate? updateDelegate, Framework framework) { if (updateDelegate == null) return; @@ -95,4 +110,16 @@ internal static class EventHandlerExtensions Log.Error(ex, "Exception during raise of {handler}", act.Method); } } + + private static void HandleInvoke(Action act, T argument) + { + try + { + act(argument); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", act.Method); + } + } } From 43abb12710e1193f6e3ba898a932b3583826baba Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:55:56 -0700 Subject: [PATCH 164/585] Add GameConfigPluginScoped (#1383) * Add GameConfigPluginScoped * Proposed Resolution to sub-object events * Nullify delegates to prevent memory leaks --- Dalamud/Game/Config/GameConfig.cs | 224 ++++++++++++++++++++++- Dalamud/Game/Config/GameConfigSection.cs | 2 +- Dalamud/Plugin/Services/IGameConfig.cs | 17 +- 3 files changed, 235 insertions(+), 8 deletions(-) diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index b77b9c4af..831c1157b 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -12,11 +12,7 @@ namespace Dalamud.Game.Config; /// This class represents the game's configuration. /// [InterfaceVersion("1.0")] -[PluginInterface] [ServiceManager.EarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable { private readonly GameConfigAddressResolver address = new(); @@ -36,15 +32,30 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable this.address.Setup(sigScanner); this.configChangeHook = Hook.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged); - this.configChangeHook?.Enable(); + this.configChangeHook.Enable(); }); } private unsafe delegate nint ConfigChangeDelegate(ConfigBase* configBase, ConfigEntry* configEntry); /// - public event EventHandler Changed; + public event EventHandler? Changed; + + /// + /// Unused internally, used as a proxy for System.Changed via GameConfigPluginScoped + /// + public event EventHandler? SystemChanged; + /// + /// Unused internally, used as a proxy for UiConfig.Changed via GameConfigPluginScoped + /// + public event EventHandler? UiConfigChanged; + + /// + /// Unused internally, used as a proxy for UiControl.Changed via GameConfigPluginScoped + /// + public event EventHandler? UiControlChanged; + /// public GameConfigSection System { get; private set; } @@ -192,3 +203,204 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable return returnValue; } } + +/// +/// Plugin-scoped version of a GameConfig service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig +{ + [ServiceManager.ServiceDependency] + private readonly GameConfig gameConfigService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal GameConfigPluginScoped() + { + this.gameConfigService.Changed += this.ConfigChangedForward; + this.gameConfigService.System.Changed += this.SystemConfigChangedForward; + this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward; + this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward; + } + + /// + public event EventHandler? Changed; + + /// + public event EventHandler? SystemChanged; + + /// + public event EventHandler? UiConfigChanged; + + /// + public event EventHandler? UiControlChanged; + + /// + public GameConfigSection System => this.gameConfigService.System; + + /// + public GameConfigSection UiConfig => this.gameConfigService.UiConfig; + + /// + public GameConfigSection UiControl => this.gameConfigService.UiControl; + + /// + public void Dispose() + { + this.gameConfigService.Changed -= this.ConfigChangedForward; + this.gameConfigService.System.Changed -= this.SystemConfigChangedForward; + this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward; + this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward; + + this.Changed = null; + this.SystemChanged = null; + this.UiConfigChanged = null; + this.UiControlChanged = null; + } + + /// + public bool TryGet(SystemConfigOption option, out bool value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(SystemConfigOption option, out uint value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(SystemConfigOption option, out float value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(SystemConfigOption option, out string value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(SystemConfigOption option, out UIntConfigProperties? properties) + => this.gameConfigService.TryGet(option, out properties); + + /// + public bool TryGet(SystemConfigOption option, out FloatConfigProperties? properties) + => this.gameConfigService.TryGet(option, out properties); + + /// + public bool TryGet(SystemConfigOption option, out StringConfigProperties? properties) + => this.gameConfigService.TryGet(option, out properties); + + /// + public bool TryGet(UiConfigOption option, out bool value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(UiConfigOption option, out uint value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(UiConfigOption option, out float value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(UiConfigOption option, out string value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(UiConfigOption option, out UIntConfigProperties? properties) + => this.gameConfigService.TryGet(option, out properties); + + /// + public bool TryGet(UiConfigOption option, out FloatConfigProperties? properties) + => this.gameConfigService.TryGet(option, out properties); + + /// + public bool TryGet(UiConfigOption option, out StringConfigProperties? properties) + => this.gameConfigService.TryGet(option, out properties); + + /// + public bool TryGet(UiControlOption option, out bool value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(UiControlOption option, out uint value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(UiControlOption option, out float value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(UiControlOption option, out string value) + => this.gameConfigService.TryGet(option, out value); + + /// + public bool TryGet(UiControlOption option, out UIntConfigProperties? properties) + => this.gameConfigService.TryGet(option, out properties); + + /// + public bool TryGet(UiControlOption option, out FloatConfigProperties? properties) + => this.gameConfigService.TryGet(option, out properties); + + /// + public bool TryGet(UiControlOption option, out StringConfigProperties? properties) + => this.gameConfigService.TryGet(option, out properties); + + /// + public void Set(SystemConfigOption option, bool value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(SystemConfigOption option, uint value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(SystemConfigOption option, float value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(SystemConfigOption option, string value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(UiConfigOption option, bool value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(UiConfigOption option, uint value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(UiConfigOption option, float value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(UiConfigOption option, string value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(UiControlOption option, bool value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(UiControlOption option, uint value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(UiControlOption option, float value) + => this.gameConfigService.Set(option, value); + + /// + public void Set(UiControlOption option, string value) + => this.gameConfigService.Set(option, value); + + private void ConfigChangedForward(object sender, ConfigChangeEvent data) => this.Changed?.Invoke(sender, data); + + private void SystemConfigChangedForward(object sender, ConfigChangeEvent data) => this.SystemChanged?.Invoke(sender, data); + + private void UiConfigConfigChangedForward(object sender, ConfigChangeEvent data) => this.UiConfigChanged?.Invoke(sender, data); + + private void UiControlConfigChangedForward(object sender, ConfigChangeEvent data) => this.UiControlChanged?.Invoke(sender, data); +} diff --git a/Dalamud/Game/Config/GameConfigSection.cs b/Dalamud/Game/Config/GameConfigSection.cs index ea79a7fc8..31e4a0b3f 100644 --- a/Dalamud/Game/Config/GameConfigSection.cs +++ b/Dalamud/Game/Config/GameConfigSection.cs @@ -51,7 +51,7 @@ public class GameConfigSection /// /// Event which is fired when a game config option is changed within the section. /// - public event EventHandler? Changed; + internal event EventHandler? Changed; /// /// Gets the number of config entries contained within the section. diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index 69a611114..8e9b48d83 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -12,10 +12,25 @@ namespace Dalamud.Plugin.Services; public interface IGameConfig { /// - /// Event which is fired when a game config option is changed. + /// Event which is fired when any game config option is changed. /// public event EventHandler Changed; + /// + /// Event which is fired when a system config option is changed. + /// + public event EventHandler SystemChanged; + + /// + /// Event which is fired when a UiConfig option is changed. + /// + public event EventHandler UiConfigChanged; + + /// + /// Event which is fired when a UiControl config option is changed. + /// + public event EventHandler UiControlChanged; + /// /// Gets the collection of config options that persist between characters. /// From 9181e1119564f21fbcbcf4265fbe1d74fe8f7470 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Sep 2023 23:59:04 -0700 Subject: [PATCH 165/585] Remove Obsoletes --- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 12 -- .../Game/ClientState/GamePad/GamepadState.cs | 48 -------- .../Game/ClientState/Objects/TargetManager.cs | 110 +----------------- .../Internal/DXGI/SwapChainSigResolver.cs | 34 ------ .../Interface/ImGuiFileDialog/FileDialog.cs | 10 -- Dalamud/Interface/Windowing/WindowSystem.cs | 8 -- 6 files changed, 5 insertions(+), 217 deletions(-) delete mode 100644 Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index 489e75bc3..5d0098187 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -55,18 +55,6 @@ internal sealed partial class BuddyList : IServiceType, IBuddyList } } - /// - /// Gets a value indicating whether the local player's companion is present. - /// - [Obsolete("Use CompanionBuddy != null", false)] - public bool CompanionBuddyPresent => this.CompanionBuddy != null; - - /// - /// Gets a value indicating whether the local player's pet is present. - /// - [Obsolete("Use PetBuddy != null", false)] - public bool PetBuddyPresent => this.PetBuddy != null; - /// public BuddyMember? CompanionBuddy { diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index 8acb6ada5..b03db6df2 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -55,54 +55,6 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState public Vector2 RightStick => new(this.rightStickX, this.rightStickY); - /// - /// Gets the state of the left analogue stick in the left direction between 0 (not tilted) and 1 (max tilt). - /// - [Obsolete("Use IGamepadState.LeftStick.X", false)] - public float LeftStickLeft => this.leftStickX < 0 ? -this.leftStickX / 100f : 0; - - /// - /// Gets the state of the left analogue stick in the right direction between 0 (not tilted) and 1 (max tilt). - /// - [Obsolete("Use IGamepadState.LeftStick.X", false)] - public float LeftStickRight => this.leftStickX > 0 ? this.leftStickX / 100f : 0; - - /// - /// Gets the state of the left analogue stick in the up direction between 0 (not tilted) and 1 (max tilt). - /// - [Obsolete("Use IGamepadState.LeftStick.Y", false)] - public float LeftStickUp => this.leftStickY > 0 ? this.leftStickY / 100f : 0; - - /// - /// Gets the state of the left analogue stick in the down direction between 0 (not tilted) and 1 (max tilt). - /// - [Obsolete("Use IGamepadState.LeftStick.Y", false)] - public float LeftStickDown => this.leftStickY < 0 ? -this.leftStickY / 100f : 0; - - /// - /// Gets the state of the right analogue stick in the left direction between 0 (not tilted) and 1 (max tilt). - /// - [Obsolete("Use IGamepadState.RightStick.X", false)] - public float RightStickLeft => this.rightStickX < 0 ? -this.rightStickX / 100f : 0; - - /// - /// Gets the state of the right analogue stick in the right direction between 0 (not tilted) and 1 (max tilt). - /// - [Obsolete("Use IGamepadState.RightStick.X", false)] - public float RightStickRight => this.rightStickX > 0 ? this.rightStickX / 100f : 0; - - /// - /// Gets the state of the right analogue stick in the up direction between 0 (not tilted) and 1 (max tilt). - /// - [Obsolete("Use IGamepadState.RightStick.Y", false)] - public float RightStickUp => this.rightStickY > 0 ? this.rightStickY / 100f : 0; - - /// - /// Gets the state of the right analogue stick in the down direction between 0 (not tilted) and 1 (max tilt). - /// - [Obsolete("Use IGamepadState.RightStick.Y", false)] - public float RightStickDown => this.rightStickY < 0 ? -this.rightStickY / 100f : 0; - /// /// Gets buttons pressed bitmask, set once when the button is pressed. See for the mapping. /// diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index a821ba806..fcb242c1e 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -39,35 +39,35 @@ internal sealed unsafe class TargetManager : IServiceType, ITargetManager public GameObject? Target { get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target); - set => this.SetTarget(value); + set => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } /// public GameObject? MouseOverTarget { get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget); - set => this.SetMouseOverTarget(value); + set => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } /// public GameObject? FocusTarget { get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget); - set => this.SetFocusTarget(value); + set => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } /// public GameObject? PreviousTarget { get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget); - set => this.SetPreviousTarget(value); + set => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } /// public GameObject? SoftTarget { get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget); - set => this.SetSoftTarget(value); + set => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)value?.Address; } /// @@ -85,104 +85,4 @@ internal sealed unsafe class TargetManager : IServiceType, ITargetManager } private FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem* Struct => (FFXIVClientStructs.FFXIV.Client.Game.Control.TargetSystem*)this.Address; - - /// - /// Sets the current target. - /// - /// Actor to target. - [Obsolete("Use Target Property", false)] - public void SetTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero); - - /// - /// Sets the mouseover target. - /// - /// Actor to target. - [Obsolete("Use MouseOverTarget Property", false)] - public void SetMouseOverTarget(GameObject? actor) => this.SetMouseOverTarget(actor?.Address ?? IntPtr.Zero); - - /// - /// Sets the focus target. - /// - /// Actor to target. - [Obsolete("Use FocusTarget Property", false)] - public void SetFocusTarget(GameObject? actor) => this.SetFocusTarget(actor?.Address ?? IntPtr.Zero); - - /// - /// Sets the previous target. - /// - /// Actor to target. - [Obsolete("Use PreviousTarget Property", false)] - public void SetPreviousTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero); - - /// - /// Sets the soft target. - /// - /// Actor to target. - [Obsolete("Use SoftTarget Property", false)] - public void SetSoftTarget(GameObject? actor) => this.SetTarget(actor?.Address ?? IntPtr.Zero); - - /// - /// Sets the current target. - /// - /// Actor (address) to target. - [Obsolete("Use Target Property", false)] - public void SetTarget(IntPtr actorAddress) => Struct->Target = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; - - /// - /// Sets the mouseover target. - /// - /// Actor (address) to target. - [Obsolete("Use MouseOverTarget Property", false)] - public void SetMouseOverTarget(IntPtr actorAddress) => Struct->MouseOverTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; - - /// - /// Sets the focus target. - /// - /// Actor (address) to target. - [Obsolete("Use FocusTarget Property", false)] - public void SetFocusTarget(IntPtr actorAddress) => Struct->FocusTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; - - /// - /// Sets the previous target. - /// - /// Actor (address) to target. - [Obsolete("Use PreviousTarget Property", false)] - public void SetPreviousTarget(IntPtr actorAddress) => Struct->PreviousTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; - - /// - /// Sets the soft target. - /// - /// Actor (address) to target. - [Obsolete("Use SoftTarget Property", false)] - public void SetSoftTarget(IntPtr actorAddress) => Struct->SoftTarget = (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)actorAddress; - - /// - /// Clears the current target. - /// - [Obsolete("Use Target Property", false)] - public void ClearTarget() => this.SetTarget(IntPtr.Zero); - - /// - /// Clears the mouseover target. - /// - [Obsolete("Use MouseOverTarget Property", false)] - public void ClearMouseOverTarget() => this.SetMouseOverTarget(IntPtr.Zero); - - /// - /// Clears the focus target. - /// - [Obsolete("Use FocusTarget Property", false)] - public void ClearFocusTarget() => this.SetFocusTarget(IntPtr.Zero); - - /// - /// Clears the previous target. - /// - [Obsolete("Use PreviousTarget Property", false)] - public void ClearPreviousTarget() => this.SetPreviousTarget(IntPtr.Zero); - - /// - /// Clears the soft target. - /// - [Obsolete("Use SoftTarget Property", false)] - public void ClearSoftTarget() => this.SetSoftTarget(IntPtr.Zero); } diff --git a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs deleted file mode 100644 index a2fc08646..000000000 --- a/Dalamud/Game/Internal/DXGI/SwapChainSigResolver.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Diagnostics; -using System.Linq; - -using Serilog; - -namespace Dalamud.Game.Internal.DXGI; - -/// -/// The address resolver for native D3D11 methods to facilitate displaying the Dalamud UI. -/// -[Obsolete("This has been deprecated in favor of the VTable resolver.")] -internal sealed class SwapChainSigResolver : BaseAddressResolver, ISwapChainAddressResolver -{ - /// - public IntPtr Present { get; set; } - - /// - public IntPtr ResizeBuffers { get; set; } - - /// - protected override void Setup64Bit(SigScanner sig) - { - var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "dxgi.dll"); - - Log.Debug($"Found DXGI: 0x{module.BaseAddress.ToInt64():X}"); - - var scanner = new SigScanner(module); - - // This(code after the function head - offset of it) was picked to avoid running into issues with other hooks being installed into this function. - this.Present = scanner.ScanModule("41 8B F0 8B FA 89 54 24 ?? 48 8B D9 48 89 4D ?? C6 44 24 ?? 00") - 0x37; - - this.ResizeBuffers = scanner.ScanModule("48 8B C4 55 41 54 41 55 41 56 41 57 48 8D 68 B1 48 81 EC ?? ?? ?? ?? 48 C7 45 ?? ?? ?? ?? ?? 48 89 58 10 48 89 70 18 48 89 78 20 45 8B F9 45 8B E0 44 8B EA 48 8B F9 8B 45 7F 89 44 24 30 8B 75 77 89 74 24 28 44 89 4C 24"); - } -} diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs index aec5e9af4..411f203cc 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.cs @@ -123,16 +123,6 @@ public partial class FileDialog return this.isOk; } - /// - /// Gets the result of the selection. - /// - /// The result of the selection (file or folder path). If multiple entries were selected, they are separated with commas. - [Obsolete("Use GetResults() instead.", true)] - public string GetResult() - { - return string.Join(',', this.GetResults()); - } - /// /// Gets the result of the selection. /// diff --git a/Dalamud/Interface/Windowing/WindowSystem.cs b/Dalamud/Interface/Windowing/WindowSystem.cs index 8e12d8f68..3e2a95a8d 100644 --- a/Dalamud/Interface/Windowing/WindowSystem.cs +++ b/Dalamud/Interface/Windowing/WindowSystem.cs @@ -94,14 +94,6 @@ public class WindowSystem /// public void RemoveAllWindows() => this.windows.Clear(); - /// - /// Get a window by name. - /// - /// The name of the . - /// The object with matching name or null. - [Obsolete("WindowSystem does not own your window - you should store a reference to it and use that instead.")] - public Window? GetWindow(string windowName) => this.windows.FirstOrDefault(w => w.WindowName == windowName); - /// /// Draw all registered windows using ImGui. /// From 64caf77a32d46482a3ad1d7e8f5530a7421edb23 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 22 Sep 2023 14:27:13 +0200 Subject: [PATCH 166/585] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index fd5ba8a27..a1ddff097 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit fd5ba8a27ec911a69eeb93ceb0202091279dfceb +Subproject commit a1ddff0974729a2e984d8cc1dc007eff19bd74ab From fd3bd6dc5b9c9db01360016e0b553b50505cb176 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 22 Sep 2023 12:17:54 -0700 Subject: [PATCH 167/585] Use abstract class instead of interface --- .../AddonEventManager/AddonEventManager.cs | 2 +- .../AddonLifecycle/AddonArgTypes/AddonArgs.cs | 46 +++++++++++++++++++ .../AddonArgTypes/AddonDrawArgs.cs | 9 ++-- .../AddonArgTypes/AddonFinalizeArgs.cs | 9 ++-- .../AddonArgTypes/AddonRefreshArgs.cs | 17 ++++--- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 9 ++-- .../AddonArgTypes/AddonSetupArgs.cs | 18 +++++--- .../AddonArgTypes/AddonUpdateArgs.cs | 9 ++-- Dalamud/Game/AddonLifecycle/AddonArgsType.cs | 2 +- Dalamud/Game/AddonLifecycle/AddonLifecycle.cs | 7 ++- Dalamud/Game/AddonLifecycle/IAddonArgs.cs | 25 ---------- Dalamud/Game/Gui/Dtr/DtrBar.cs | 4 +- .../AgingSteps/AddonLifecycleAgingStep.cs | 12 ++--- Dalamud/Plugin/Services/IAddonLifecycle.cs | 2 +- 14 files changed, 95 insertions(+), 76 deletions(-) create mode 100644 Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs delete mode 100644 Dalamud/Game/AddonLifecycle/IAddonArgs.cs diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/AddonEventManager/AddonEventManager.cs index 730a7a404..89554074a 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/AddonEventManager/AddonEventManager.cs @@ -160,7 +160,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// /// Event type that triggered this call. /// Addon that triggered this call. - private void OnAddonFinalize(AddonEvent eventType, IAddonArgs addonInfo) + private void OnAddonFinalize(AddonEvent eventType, AddonArgs addonInfo) { // It shouldn't be possible for this event to be anything other than PreFinalize. if (eventType != AddonEvent.PreFinalize) return; diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs new file mode 100644 index 000000000..949d3fde9 --- /dev/null +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs @@ -0,0 +1,46 @@ +using Dalamud.Memory; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon; + +/// +/// Base class for AddonLifecycle AddonArgTypes. +/// +public abstract unsafe class AddonArgs +{ + /// + /// Constant string representing the name of an addon that is invalid. + /// + public const string InvalidAddon = "NullAddon"; + + private string? addonName; + + /// + /// Gets the name of the addon this args referrers to. + /// + public string AddonName => this.GetAddonName(); + + /// + /// Gets the pointer to the addons AtkUnitBase. + /// + public nint Addon { get; init; } + + /// + /// 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 == nint.Zero) return InvalidAddon; + + var addonPointer = (AtkUnitBase*)this.Addon; + if (addonPointer->Name is null) return InvalidAddon; + + return this.addonName ??= MemoryHelper.ReadString((nint)addonPointer->Name, 0x20); + } +} diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs index 614a7ac2a..d93001d1c 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -1,13 +1,10 @@ -namespace Dalamud.Game.Addon.AddonArgTypes; +namespace Dalamud.Game.Addon; /// /// Addon argument data for Finalize events. /// -public class AddonDrawArgs : IAddonArgs +public class AddonDrawArgs : AddonArgs { /// - public nint Addon { get; init; } - - /// - public AddonArgsType Type => AddonArgsType.Draw; + public override AddonArgsType Type => AddonArgsType.Draw; } diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs index aa31fb051..ed7aa1b3c 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -1,13 +1,10 @@ -namespace Dalamud.Game.Addon.AddonArgTypes; +namespace Dalamud.Game.Addon; /// /// Addon argument data for Finalize events. /// -public class AddonFinalizeArgs : IAddonArgs +public class AddonFinalizeArgs : AddonArgs { /// - public nint Addon { get; init; } - - /// - public AddonArgsType Type => AddonArgsType.Finalize; + public override AddonArgsType Type => AddonArgsType.Finalize; } diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs index ab4f37c3c..60ccaf8ea 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,15 +1,15 @@ -namespace Dalamud.Game.Addon.AddonArgTypes; +using System; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon; /// /// Addon argument data for Finalize events. /// -public class AddonRefreshArgs : IAddonArgs +public class AddonRefreshArgs : AddonArgs { /// - public nint Addon { get; init; } - - /// - public AddonArgsType Type => AddonArgsType.Refresh; + public override AddonArgsType Type => AddonArgsType.Refresh; /// /// Gets the number of AtkValues. @@ -20,4 +20,9 @@ public class AddonRefreshArgs : IAddonArgs /// Gets the address of the AtkValue array. /// public nint AtkValues { get; init; } + + /// + /// Gets the AtkValues in the form of a span. + /// + public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); } diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index dfd0dac5e..a31369aaf 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -1,15 +1,12 @@ -namespace Dalamud.Game.Addon.AddonArgTypes; +namespace Dalamud.Game.Addon; /// /// Addon argument data for Finalize events. /// -public class AddonRequestedUpdateArgs : IAddonArgs +public class AddonRequestedUpdateArgs : AddonArgs { /// - public nint Addon { get; init; } - - /// - public AddonArgsType Type => AddonArgsType.RequestedUpdate; + public override AddonArgsType Type => AddonArgsType.RequestedUpdate; /// /// Gets the NumberArrayData** for this event. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs index 4b467deb8..17c87967a 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,15 +1,16 @@ -namespace Dalamud.Game.Addon.AddonArgTypes; +using System; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Addon; /// /// Addon argument data for Setup events. /// -public class AddonSetupArgs : IAddonArgs +public class AddonSetupArgs : AddonArgs { /// - public nint Addon { get; init; } - - /// - public AddonArgsType Type => AddonArgsType.Setup; + public override AddonArgsType Type => AddonArgsType.Setup; /// /// Gets the number of AtkValues. @@ -20,4 +21,9 @@ public class AddonSetupArgs : IAddonArgs /// Gets the address of the AtkValue array. /// public nint AtkValues { get; init; } + + /// + /// Gets the AtkValues in the form of a span. + /// + public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); } diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs index ede588001..993883d77 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -1,15 +1,12 @@ -namespace Dalamud.Game.Addon.AddonArgTypes; +namespace Dalamud.Game.Addon; /// /// Addon argument data for Finalize events. /// -public class AddonUpdateArgs : IAddonArgs +public class AddonUpdateArgs : AddonArgs { /// - public nint Addon { get; init; } - - /// - public AddonArgsType Type => AddonArgsType.Update; + public override AddonArgsType Type => AddonArgsType.Update; /// /// Gets the time since the last update. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgsType.cs b/Dalamud/Game/AddonLifecycle/AddonArgsType.cs index ac325229d..8a07d445b 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgsType.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgsType.cs @@ -1,7 +1,7 @@ namespace Dalamud.Game.Addon; /// -/// Enumeration for available AddonLifecycle arg data +/// Enumeration for available AddonLifecycle arg data. /// public enum AddonArgsType { diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs index 75b5b3753..17afbaeac 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; -using Dalamud.Game.Addon.AddonArgTypes; using Dalamud.Hooking; using Dalamud.Hooking.Internal; using Dalamud.IoC; @@ -128,7 +127,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRequestedUpdateHook.Enable(); } - private void InvokeListeners(AddonEvent eventType, IAddonArgs args) + private void InvokeListeners(AddonEvent eventType, AddonArgs args) { // Match on string.empty for listeners that want events for all addons. foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) @@ -141,7 +140,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs() + this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs { Addon = (nint)addon, AtkValueCount = valueCount, @@ -157,7 +156,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { - this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs() + this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs { Addon = (nint)addon, AtkValueCount = valueCount, diff --git a/Dalamud/Game/AddonLifecycle/IAddonArgs.cs b/Dalamud/Game/AddonLifecycle/IAddonArgs.cs deleted file mode 100644 index ba77a2c6d..000000000 --- a/Dalamud/Game/AddonLifecycle/IAddonArgs.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Dalamud.Memory; -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace Dalamud.Game.Addon; - -/// -/// Interface representing the argument data for AddonLifecycle events. -/// -public unsafe interface IAddonArgs -{ - /// - /// Gets the name of the addon this args referrers to. - /// - string AddonName => this.Addon == nint.Zero ? "NullAddon" : MemoryHelper.ReadString((nint)((AtkUnitBase*)this.Addon)->Name, 0x20); - - /// - /// Gets the pointer to the addons AtkUnitBase. - /// - nint Addon { get; init; } - - /// - /// Gets the type of these args. - /// - AddonArgsType Type { get; } -} diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 6b74e47cd..880bc0625 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -255,7 +255,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } - private void OnDtrPostDraw(AddonEvent eventType, IAddonArgs addonInfo) + private void OnDtrPostDraw(AddonEvent eventType, AddonArgs addonInfo) { var addon = (AtkUnitBase*)addonInfo.Addon; @@ -300,7 +300,7 @@ public sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } - private void OnAddonRequestedUpdateDetour(AddonEvent eventType, IAddonArgs addonInfo) + private void OnAddonRequestedUpdateDetour(AddonEvent eventType, AddonArgs addonInfo) { var addon = (AtkUnitBase*)addonInfo.Addon; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs index 0821e62de..a9948430f 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -100,32 +100,32 @@ internal class AddonLifecycleAgingStep : IAgingStep } } - private void PostSetup(AddonEvent eventType, IAddonArgs addonInfo) + private void PostSetup(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterSetup) this.currentStep++; } - private void PostUpdate(AddonEvent eventType, IAddonArgs addonInfo) + private void PostUpdate(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterUpdate) this.currentStep++; } - private void PostDraw(AddonEvent eventType, IAddonArgs addonInfo) + private void PostDraw(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterDraw) this.currentStep++; } - private void PostRefresh(AddonEvent eventType, IAddonArgs addonInfo) + private void PostRefresh(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterRefresh) this.currentStep++; } - private void PostRequestedUpdate(AddonEvent eventType, IAddonArgs addonInfo) + private void PostRequestedUpdate(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterRequestedUpdate) this.currentStep++; } - private void PreFinalize(AddonEvent eventType, IAddonArgs addonInfo) + private void PreFinalize(AddonEvent eventType, AddonArgs addonInfo) { if (this.currentStep is TestStep.CharacterFinalize) this.currentStep++; } diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 5290395ab..e89c57931 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -15,7 +15,7 @@ public interface IAddonLifecycle /// /// The event type that triggered the message. /// Information about what addon triggered the message. - public delegate void AddonEventDelegate(AddonEvent eventType, IAddonArgs args); + public delegate void AddonEventDelegate(AddonEvent eventType, AddonArgs args); /// /// Register a listener that will trigger on the specified event and any of the specified addons. From 4f8de2e20592f9d3b0c87b38bcfb5626e62475ec Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Fri, 22 Sep 2023 17:01:10 -0700 Subject: [PATCH 168/585] Obsolete (static) PluginLog for future removal - Mark PluginLog as obsoleted and pending removal, encouraging users to switch to IPluginLog. - Remove internal references to PluginLog. --- Dalamud.CorePlugin/PluginImpl.cs | 11 ++++++----- Dalamud/Logging/PluginLog.cs | 8 ++++++-- Dalamud/Utility/Signatures/SignatureHelper.cs | 9 +++------ 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 2f76a1087..e2b373d42 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -1,10 +1,8 @@ using System; using System.IO; - using Dalamud.Configuration.Internal; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -52,6 +50,8 @@ namespace Dalamud.CorePlugin private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin"); private Localization localization; + private IPluginLog pluginLog; + /// /// Initializes a new instance of the class. /// @@ -63,6 +63,7 @@ namespace Dalamud.CorePlugin { // this.InitLoc(); this.Interface = pluginInterface; + this.pluginLog = log; this.windowSystem.AddWindow(new PluginWindow()); @@ -76,7 +77,7 @@ namespace Dalamud.CorePlugin } catch (Exception ex) { - PluginLog.Error(ex, "kaboom"); + log.Error(ex, "kaboom"); } } @@ -130,13 +131,13 @@ namespace Dalamud.CorePlugin } catch (Exception ex) { - PluginLog.Error(ex, "Boom"); + this.pluginLog.Error(ex, "Boom"); } } private void OnCommand(string command, string args) { - PluginLog.Information("Command called!"); + this.pluginLog.Information("Command called!"); // this.window.IsOpen = true; } diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index 3ac98f15a..c3fe0c808 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -1,6 +1,5 @@ -using System; using System.Reflection; - +using Dalamud.Plugin.Services; using Serilog; using Serilog.Events; @@ -9,6 +8,11 @@ namespace Dalamud.Logging; /// /// Class offering various static methods to allow for logging in plugins. /// +/// +/// PluginLog has been obsoleted and replaced by the service. Developers are encouraged to +/// move over as soon as reasonably possible for performance reasons. +/// +[Obsolete("Static PluginLog will be removed in API 10. Developers should use IPluginLog.")] public static class PluginLog { #region "Log" prefixed Serilog style methods diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index bd99b8515..20b45e7fb 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -1,11 +1,8 @@ -using System; -using System.Linq; +using System.Linq; using System.Reflection; using System.Runtime.InteropServices; - using Dalamud.Game; using Dalamud.Hooking; -using Dalamud.Logging; using Dalamud.Utility.Signatures.Wrappers; using Serilog; @@ -23,7 +20,7 @@ public static class SignatureHelper /// . /// /// The object to initialise. - /// If warnings should be logged using . + /// If warnings should be logged. public static void Initialise(object self, bool log = true) { var scanner = Service.Get(); @@ -61,7 +58,7 @@ public static class SignatureHelper : message; if (fallible) { - PluginLog.Warning(errorMsg); + Log.Warning(errorMsg); } else { From 3618a510d0d56b70c0cd83a02297d321949d5e49 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Fri, 22 Sep 2023 19:17:00 -0700 Subject: [PATCH 169/585] Make Custom Repo warning orange - Easier on the eyes, allegedly. --- .../Widgets/ThirdRepoSettingsEntry.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs index 617cbb045..afebeaedb 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs @@ -1,10 +1,8 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Threading.Tasks; - using CheapLoc; using Dalamud.Configuration; using Dalamud.Configuration.Internal; @@ -80,25 +78,28 @@ public class ThirdRepoSettingsEntry : SettingsEntry var disclaimerDismissed = config.ThirdRepoSpeedbumpDismissed.Value; ImGui.PushFont(InterfaceManager.IconFont); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle.ToIconString()); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudOrange); + ImGuiHelpers.SafeTextWrapped(FontAwesomeIcon.ExclamationTriangle.ToIconString()); ImGui.PopFont(); ImGui.SameLine(); ImGuiHelpers.ScaledDummy(2); ImGui.SameLine(); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarningReadThis", "READ THIS FIRST!")); + ImGuiHelpers.SafeTextWrapped(Loc.Localize("DalamudSettingCustomRepoWarningReadThis", "READ THIS FIRST!")); ImGui.SameLine(); ImGuiHelpers.ScaledDummy(2); ImGui.SameLine(); ImGui.PushFont(InterfaceManager.IconFont); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle.ToIconString()); + ImGuiHelpers.SafeTextWrapped(FontAwesomeIcon.ExclamationTriangle.ToIconString()); ImGui.PopFont(); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for custom plugins and repositories.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning5", "If someone told you to copy/paste something here, it's very possible that you are being scammed or taken advantage of.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning2", "Plugins have full control over your PC, like any other program, and may cause harm or crashes.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning4", "They can delete your character, steal your FC or Discord account, and burn down your house.")); - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, Loc.Localize("DalamudSettingCustomRepoWarning3", "Please make absolutely sure that you only install plugins from developers you trust.")); + ImGuiHelpers.SafeTextWrapped(Loc.Localize("DalamudSettingCustomRepoWarning", "We cannot take any responsibility for custom plugins and repositories.")); + ImGuiHelpers.SafeTextWrapped(Loc.Localize("DalamudSettingCustomRepoWarning5", "If someone told you to copy/paste something here, it's very possible that you are being scammed or taken advantage of.")); + ImGuiHelpers.SafeTextWrapped(Loc.Localize("DalamudSettingCustomRepoWarning2", "Plugins have full control over your PC, like any other program, and may cause harm or crashes.")); + ImGuiHelpers.SafeTextWrapped(Loc.Localize("DalamudSettingCustomRepoWarning4", "They can delete your character, steal your FC or Discord account, and burn down your house.")); + ImGuiHelpers.SafeTextWrapped(Loc.Localize("DalamudSettingCustomRepoWarning3", "Please make absolutely sure that you only install plugins from developers you trust.")); + ImGui.PopStyleColor(); + if (!disclaimerDismissed) { const int speedbumpTime = 15; From 74375ec414290ce98d730ec9f94100af859d7c96 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 22 Sep 2023 20:19:27 -0700 Subject: [PATCH 170/585] Add more ColorHelpers --- Dalamud/Interface/ColorHelpers.cs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs index 71f959292..0239ad754 100644 --- a/Dalamud/Interface/ColorHelpers.cs +++ b/Dalamud/Interface/ColorHelpers.cs @@ -1,4 +1,5 @@ using System; +using System.Drawing; using System.Numerics; namespace Dalamud.Interface; @@ -256,6 +257,34 @@ public static class ColorHelpers /// The faded color. public static uint Fade(uint color, float amount) => RgbaVector4ToUint(Fade(RgbaUintToVector4(color), amount)); - + + /// + /// Convert a KnownColor to a RGBA vector with values between 0.0f and 1.0f + /// + /// Known Color to convert. + /// RGBA Vector with values between 0.0f and 1.0f. + public static Vector4 Vector(this KnownColor knownColor) + { + var rgbColor = Color.FromKnownColor(knownColor); + return new Vector4(rgbColor.R, rgbColor.G, rgbColor.B, rgbColor.A) / 255.0f; + } + + /// + /// Normalizes a Vector4 with RGBA 255 color values to values between 0.0f and 1.0f + /// If values are out of RGBA 255 range, the original value is returned. + /// + /// The color vector to convert. + /// A vector with values between 0.0f and 1.0f. + public static Vector4 NormalizeToUnitRange(this Vector4 color) => color switch + { + // If any components are out of range, return original value. + { W: > 255.0f or < 0.0f } or { X: > 255.0f or < 0.0f } or { Y: > 255.0f or < 0.0f } or { Z: > 255.0f or < 0.0f } => color, + + // If all components are already unit range, return original value. + { W: >= 0.0f and <= 1.0f, X: >= 0.0f and <= 1.0f, Y: >= 0.0f and <= 1.0f, Z: >= 0.0f and <= 1.0f } => color, + + _ => color / 255.0f, + }; + public record struct HsvaColor(float H, float S, float V, float A); } From c6c28c6e3f8eff1652a2027673907235105ba37d Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 22 Sep 2023 21:12:44 -0700 Subject: [PATCH 171/585] Change default name so auto generate stops complaining about improper casing. --- Dalamud/Plugin/Services/IAddonLifecycle.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index e89c57931..2bc41a366 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -13,9 +13,9 @@ public interface IAddonLifecycle /// /// Delegate for receiving addon lifecycle event messages. /// - /// The event type that triggered the message. + /// The event type that triggered the message. /// Information about what addon triggered the message. - public delegate void AddonEventDelegate(AddonEvent eventType, AddonArgs args); + public delegate void AddonEventDelegate(AddonEvent type, AddonArgs args); /// /// Register a listener that will trigger on the specified event and any of the specified addons. From d29422bc507582f1cc031880937f36b567ec8792 Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Fri, 22 Sep 2023 23:22:59 -0700 Subject: [PATCH 172/585] Add IPluginLog#Info - Shorthand for information log lines, because typing out `Information` is too much. --- Dalamud/Logging/ScopedPluginLogService.cs | 12 +++++++++--- Dalamud/Plugin/Services/IPluginLog.cs | 6 ++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs index 8c502fcf0..d6bb1f82d 100644 --- a/Dalamud/Logging/ScopedPluginLogService.cs +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -1,6 +1,4 @@ -using System; - -using Dalamud.IoC; +using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; @@ -90,6 +88,14 @@ public class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable /// public void Information(Exception? exception, string messageTemplate, params object[] values) => this.Write(LogEventLevel.Information, exception, messageTemplate, values); + + /// + public void Info(string messageTemplate, params object[] values) => + this.Information(messageTemplate, values); + + /// + public void Info(Exception? exception, string messageTemplate, params object[] values) => + this.Information(exception, messageTemplate, values); /// public void Debug(string messageTemplate, params object[] values) => diff --git a/Dalamud/Plugin/Services/IPluginLog.cs b/Dalamud/Plugin/Services/IPluginLog.cs index 62f9e8728..d16e985af 100644 --- a/Dalamud/Plugin/Services/IPluginLog.cs +++ b/Dalamud/Plugin/Services/IPluginLog.cs @@ -76,6 +76,12 @@ public interface IPluginLog /// /// An (optional) exception that should be recorded alongside this event. void Information(Exception? exception, string messageTemplate, params object[] values); + + /// + void Info(string messageTemplate, params object[] values); + + /// + void Info(Exception? exception, string messageTemplate, params object[] values); /// /// Log a message to the Dalamud log for this plugin. This log level should be From 1304c54effb42535dc1a0b78b9b0049ffa9c0ec6 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 23 Sep 2023 10:39:29 +0200 Subject: [PATCH 173/585] docs fixed --- Dalamud/Plugin/Services/IGameInteropProvider.cs | 4 +++- Dalamud/Utility/Signatures/SignatureHelper.cs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud/Plugin/Services/IGameInteropProvider.cs b/Dalamud/Plugin/Services/IGameInteropProvider.cs index 29f42a655..217e08445 100644 --- a/Dalamud/Plugin/Services/IGameInteropProvider.cs +++ b/Dalamud/Plugin/Services/IGameInteropProvider.cs @@ -34,13 +34,15 @@ public interface IGameInteropProvider /// /// Initialize members decorated with the . + /// Initialize any delegate members decorated with the . + /// Fill out any IntPtr members decorated with the with the resolved address. /// Errors for fallible signatures will be logged. /// /// The object to initialize. public void InitializeFromAttributes(object self); /// - /// Creates a hook by rewriting import table address. + /// Creates a hook by replacing the original address with an address pointing to a newly created jump to the detour. /// /// A memory address to install a hook. /// Callback function. Delegate must have a same original function prototype. diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index f011f8121..cabe672ce 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -21,10 +21,10 @@ internal static class SignatureHelper private const BindingFlags Flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; /// - /// Initialises an object's fields and properties that are annotated with a + /// Initializes an object's fields and properties that are annotated with a /// . /// - /// The object to initialise. + /// The object to initialize. /// If warnings should be logged using . /// Collection of created IDalamudHooks. internal static IEnumerable Initialize(object self, bool log = true) From c55b93d3c2bd6fe3b71919d5c873028fc6de7adf Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 23 Sep 2023 11:17:53 +0200 Subject: [PATCH 174/585] chore: remove IDalamudPlugin.Name --- Dalamud.CorePlugin/PluginImpl.cs | 9 ++------- Dalamud/Plugin/IDalamudPlugin.cs | 4 ---- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 7 ------- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index e2b373d42..5ed672f2d 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -1,5 +1,6 @@ using System; using System.IO; + using Dalamud.Configuration.Internal; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; @@ -37,9 +38,6 @@ namespace Dalamud.CorePlugin { } - /// - public string Name => "Dalamud.CorePlugin"; - /// public void Dispose() { @@ -71,7 +69,7 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; - Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." }); + Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = "Access the plugin." }); log.Information("CorePlugin ctor!"); } @@ -81,9 +79,6 @@ namespace Dalamud.CorePlugin } } - /// - public string Name => "Dalamud.CorePlugin"; - /// /// Gets the plugin interface. /// diff --git a/Dalamud/Plugin/IDalamudPlugin.cs b/Dalamud/Plugin/IDalamudPlugin.cs index c752df3d6..b48d55d1c 100644 --- a/Dalamud/Plugin/IDalamudPlugin.cs +++ b/Dalamud/Plugin/IDalamudPlugin.cs @@ -7,8 +7,4 @@ namespace Dalamud.Plugin; /// public interface IDalamudPlugin : IDisposable { - /// - /// Gets the name of the plugin. - /// - string Name { get; } } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index f7306b5a7..80d6edfd3 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -505,13 +505,6 @@ internal class LocalPlugin : IDisposable return; } - // In-case the manifest name was a placeholder. Can occur when no manifest was included. - if (this.manifest.Name.IsNullOrEmpty() && !this.IsDev) - { - this.manifest.Name = this.instance.Name; - this.manifest.Save(this.manifestFile, "manifest name null or empty"); - } - this.State = PluginState.Loaded; Log.Information($"Finished loading {this.DllFile.Name}"); } From 2119d08926548591eeb80dacec7011235a0a723c Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 23 Sep 2023 11:18:20 +0200 Subject: [PATCH 175/585] fix warnings --- .../Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs | 1 + Dalamud/Logging/PluginLog.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs index afebeaedb..1d6aab1bd 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/ThirdRepoSettingsEntry.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Threading.Tasks; + using CheapLoc; using Dalamud.Configuration; using Dalamud.Configuration.Internal; diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index c3fe0c808..decf10b4c 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -1,4 +1,5 @@ using System.Reflection; + using Dalamud.Plugin.Services; using Serilog; using Serilog.Events; From 6fbcd0e0e4d7715494d4c2d319524a3362682d45 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 23 Sep 2023 13:05:19 +0200 Subject: [PATCH 176/585] chore: don't use ImGuiScene.TextureWrap for any external API, IDalamudTextureWrap does not inherit from ImGuiScene.TextureWrap any longer --- .../AddonArgTypes/AddonRefreshArgs.cs | 1 - Dalamud/Interface/ColorHelpers.cs | 2 +- .../Interface/Internal/DalamudInterface.cs | 4 +- .../Interface/Internal/DalamudTextureWrap.cs | 20 ++++-- .../Interface/Internal/InterfaceManager.cs | 6 +- Dalamud/Interface/Internal/TextureManager.cs | 6 +- .../Internal/Windows/ChangelogWindow.cs | 2 +- .../Windows/Data/Widgets/TexWidget.cs | 4 +- .../Internal/Windows/PluginImageCache.cs | 66 +++++++++---------- .../PluginInstaller/PluginInstallerWindow.cs | 10 +-- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 2 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 2 +- .../TitleScreenMenu/TitleScreenMenu.cs | 13 ++-- .../TitleScreenMenu/TitleScreenMenuEntry.cs | 6 +- Dalamud/Interface/UiBuilder.cs | 12 ++-- Dalamud/Interface/UldWrapper.cs | 8 +-- Dalamud/Plugin/Services/ITitleScreenMenu.cs | 5 +- 17 files changed, 91 insertions(+), 78 deletions(-) diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs index 60ccaf8ea..6376c16b0 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,4 +1,3 @@ -using System; using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon; diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs index dd9ab08f7..b2b489004 100644 --- a/Dalamud/Interface/ColorHelpers.cs +++ b/Dalamud/Interface/ColorHelpers.cs @@ -270,7 +270,7 @@ public static class ColorHelpers => RgbaVector4ToUint(Fade(RgbaUintToVector4(color), amount)); /// - /// Convert a KnownColor to a RGBA vector with values between 0.0f and 1.0f + /// Convert a KnownColor to a RGBA vector with values between 0.0f and 1.0f. /// /// Known Color to convert. /// RGBA Vector with values between 0.0f and 1.0f. diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 4de73adc7..cfaae485a 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -66,8 +66,8 @@ internal class DalamudInterface : IDisposable, IServiceType private readonly BranchSwitcherWindow branchSwitcherWindow; private readonly HitchSettingsWindow hitchSettingsWindow; - private readonly TextureWrap logoTexture; - private readonly TextureWrap tsmLogoTexture; + private readonly IDalamudTextureWrap logoTexture; + private readonly IDalamudTextureWrap tsmLogoTexture; private bool isCreditsDarkening = false; private OutCubic creditsDarkeningAnimation = new(TimeSpan.FromSeconds(10)); diff --git a/Dalamud/Interface/Internal/DalamudTextureWrap.cs b/Dalamud/Interface/Internal/DalamudTextureWrap.cs index 039873f1f..036686c29 100644 --- a/Dalamud/Interface/Internal/DalamudTextureWrap.cs +++ b/Dalamud/Interface/Internal/DalamudTextureWrap.cs @@ -1,6 +1,4 @@ -using System; - -using ImGuiScene; +using ImGuiScene; namespace Dalamud.Interface.Internal; @@ -8,8 +6,22 @@ namespace Dalamud.Interface.Internal; /// Base TextureWrap interface for all Dalamud-owned texture wraps. /// Used to avoid referencing ImGuiScene. /// -public interface IDalamudTextureWrap : TextureWrap +public interface IDalamudTextureWrap : IDisposable { + /// + /// Gets a texture handle suitable for direct use with ImGui functions. + /// + IntPtr ImGuiHandle { get; } + + /// + /// Gets the width of the texture. + /// + int Width { get; } + + /// + /// Gets the height of the texture. + /// + int Height { get; } } /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6a3256a7f..be6ca3528 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -241,7 +241,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// The filepath to load. /// A texture, ready to use in ImGui. - public TextureWrap? LoadImage(string filePath) + public IDalamudTextureWrap? LoadImage(string filePath) { if (this.scene == null) throw new InvalidOperationException("Scene isn't ready."); @@ -264,7 +264,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// The data to load. /// A texture, ready to use in ImGui. - public TextureWrap? LoadImage(byte[] imageData) + public IDalamudTextureWrap? LoadImage(byte[] imageData) { if (this.scene == null) throw new InvalidOperationException("Scene isn't ready."); @@ -290,7 +290,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// The height in pixels. /// The number of channels. /// A texture, ready to use in ImGui. - public TextureWrap? LoadImageRaw(byte[] imageData, int width, int height, int numChannels) + public IDalamudTextureWrap? LoadImageRaw(byte[] imageData, int width, int height, int numChannels) { if (this.scene == null) throw new InvalidOperationException("Scene isn't ready."); diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 78af0ebb7..ce08e6cc7 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -43,7 +43,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP private readonly Dictionary activeTextures = new(); - private TextureWrap? fallbackTextureWrap; + private IDalamudTextureWrap? fallbackTextureWrap; /// /// Initializes a new instance of the class. @@ -319,7 +319,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP // Substitute the path here for loading, instead of when getting the respective TextureInfo path = this.GetSubstitutedPath(path); - TextureWrap? wrap; + IDalamudTextureWrap? wrap; try { // We want to load this from the disk, probably, if the path has a root @@ -495,7 +495,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// /// Gets or sets the actual texture wrap. May be unpopulated. /// - public TextureWrap? Wrap { get; set; } + public IDalamudTextureWrap? Wrap { get; set; } /// /// Gets or sets the time the texture was last accessed. diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index e61cb400b..61010ce0c 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -36,7 +36,7 @@ Thanks and have fun!"; private readonly string assemblyVersion = Util.AssemblyVersion; - private readonly TextureWrap logoTexture; + private readonly IDalamudTextureWrap logoTexture; /// /// Initializes a new instance of the class. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 9f7f69ca2..0cbc401e7 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -15,7 +15,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class TexWidget : IDataWindowWidget { - private readonly List addedTextures = new(); + private readonly List addedTextures = new(); private string iconId = "18"; private bool hiRes = true; @@ -104,7 +104,7 @@ internal class TexWidget : IDataWindowWidget ImGuiHelpers.ScaledDummy(10); - TextureWrap? toRemove = null; + IDalamudTextureWrap? toRemove = null; for (var i = 0; i < this.addedTextures.Count; i++) { if (ImGui.CollapsingHeader($"Tex #{i}")) diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 766f80b23..c334cd4bd 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -59,24 +59,24 @@ internal class PluginImageCache : IDisposable, IServiceType private readonly Task downloadTask; private readonly Task loadTask; - private readonly ConcurrentDictionary pluginIconMap = new(); - private readonly ConcurrentDictionary pluginImagesMap = new(); + private readonly ConcurrentDictionary pluginIconMap = new(); + private readonly ConcurrentDictionary pluginImagesMap = new(); - private readonly Task emptyTextureTask; - private readonly Task disabledIconTask; - private readonly Task outdatedInstallableIconTask; - private readonly Task defaultIconTask; - private readonly Task troubleIconTask; - private readonly Task updateIconTask; - private readonly Task installedIconTask; - private readonly Task thirdIconTask; - private readonly Task thirdInstalledIconTask; - private readonly Task corePluginIconTask; + private readonly Task emptyTextureTask; + private readonly Task disabledIconTask; + private readonly Task outdatedInstallableIconTask; + private readonly Task defaultIconTask; + private readonly Task troubleIconTask; + private readonly Task updateIconTask; + private readonly Task installedIconTask; + private readonly Task thirdIconTask; + private readonly Task thirdInstalledIconTask; + private readonly Task corePluginIconTask; [ServiceManager.ServiceConstructor] private PluginImageCache(Dalamud dalamud) { - Task? TaskWrapIfNonNull(TextureWrap? tw) => tw == null ? null : Task.FromResult(tw!); + Task? TaskWrapIfNonNull(IDalamudTextureWrap? tw) => tw == null ? null : Task.FromResult(tw!); var imwst = Task.Run(() => this.imWithScene); this.emptyTextureTask = imwst.ContinueWith(task => task.Result.Manager.LoadImageRaw(new byte[64], 8, 8, 4)!); @@ -99,70 +99,70 @@ internal class PluginImageCache : IDisposable, IServiceType /// /// Gets the fallback empty texture. /// - public TextureWrap EmptyTexture => this.emptyTextureTask.IsCompleted - ? this.emptyTextureTask.Result - : this.emptyTextureTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap EmptyTexture => this.emptyTextureTask.IsCompleted + ? this.emptyTextureTask.Result + : this.emptyTextureTask.GetAwaiter().GetResult(); /// /// Gets the disabled plugin icon. /// - public TextureWrap DisabledIcon => this.disabledIconTask.IsCompleted + public IDalamudTextureWrap DisabledIcon => this.disabledIconTask.IsCompleted ? this.disabledIconTask.Result : this.disabledIconTask.GetAwaiter().GetResult(); /// /// Gets the outdated installable plugin icon. /// - public TextureWrap OutdatedInstallableIcon => this.outdatedInstallableIconTask.IsCompleted + public IDalamudTextureWrap OutdatedInstallableIcon => this.outdatedInstallableIconTask.IsCompleted ? this.outdatedInstallableIconTask.Result : this.outdatedInstallableIconTask.GetAwaiter().GetResult(); /// /// Gets the default plugin icon. /// - public TextureWrap DefaultIcon => this.defaultIconTask.IsCompleted + public IDalamudTextureWrap DefaultIcon => this.defaultIconTask.IsCompleted ? this.defaultIconTask.Result : this.defaultIconTask.GetAwaiter().GetResult(); /// /// Gets the plugin trouble icon overlay. /// - public TextureWrap TroubleIcon => this.troubleIconTask.IsCompleted + public IDalamudTextureWrap TroubleIcon => this.troubleIconTask.IsCompleted ? this.troubleIconTask.Result : this.troubleIconTask.GetAwaiter().GetResult(); /// /// Gets the plugin update icon overlay. /// - public TextureWrap UpdateIcon => this.updateIconTask.IsCompleted + public IDalamudTextureWrap UpdateIcon => this.updateIconTask.IsCompleted ? this.updateIconTask.Result : this.updateIconTask.GetAwaiter().GetResult(); /// /// Gets the plugin installed icon overlay. /// - public TextureWrap InstalledIcon => this.installedIconTask.IsCompleted + public IDalamudTextureWrap InstalledIcon => this.installedIconTask.IsCompleted ? this.installedIconTask.Result : this.installedIconTask.GetAwaiter().GetResult(); /// /// Gets the third party plugin icon overlay. /// - public TextureWrap ThirdIcon => this.thirdIconTask.IsCompleted + public IDalamudTextureWrap ThirdIcon => this.thirdIconTask.IsCompleted ? this.thirdIconTask.Result : this.thirdIconTask.GetAwaiter().GetResult(); /// /// Gets the installed third party plugin icon overlay. /// - public TextureWrap ThirdInstalledIcon => this.thirdInstalledIconTask.IsCompleted + public IDalamudTextureWrap ThirdInstalledIcon => this.thirdInstalledIconTask.IsCompleted ? this.thirdInstalledIconTask.Result : this.thirdInstalledIconTask.GetAwaiter().GetResult(); /// /// Gets the core plugin icon. /// - public TextureWrap CorePluginIcon => this.corePluginIconTask.IsCompleted + public IDalamudTextureWrap CorePluginIcon => this.corePluginIconTask.IsCompleted ? this.corePluginIconTask.Result : this.corePluginIconTask.GetAwaiter().GetResult(); @@ -233,7 +233,7 @@ internal class PluginImageCache : IDisposable, IServiceType /// If the plugin was third party sourced. /// Cached image textures, or an empty array. /// True if an entry exists, may be null if currently downloading. - public bool TryGetIcon(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out TextureWrap? iconTexture) + public bool TryGetIcon(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out IDalamudTextureWrap? iconTexture) { iconTexture = null; @@ -275,16 +275,16 @@ internal class PluginImageCache : IDisposable, IServiceType /// If the plugin was third party sourced. /// Cached image textures, or an empty array. /// True if the image array exists, may be empty if currently downloading. - public bool TryGetImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out TextureWrap?[] imageTextures) + public bool TryGetImages(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, out IDalamudTextureWrap?[] imageTextures) { if (!this.pluginImagesMap.TryAdd(manifest.InternalName, null)) { var found = this.pluginImagesMap[manifest.InternalName]; - imageTextures = found ?? Array.Empty(); + imageTextures = found ?? Array.Empty(); return true; } - var target = new TextureWrap?[5]; + var target = new IDalamudTextureWrap?[5]; this.pluginImagesMap[manifest.InternalName] = target; imageTextures = target; @@ -304,7 +304,7 @@ internal class PluginImageCache : IDisposable, IServiceType return false; } - private async Task TryLoadImage( + private async Task TryLoadImage( byte[]? bytes, string name, string? loc, @@ -319,7 +319,7 @@ internal class PluginImageCache : IDisposable, IServiceType var interfaceManager = this.imWithScene.Manager; var framework = await Service.GetAsync(); - TextureWrap? image; + IDalamudTextureWrap? image; // FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread. try { @@ -492,7 +492,7 @@ internal class PluginImageCache : IDisposable, IServiceType Log.Debug("Plugin image loader has shutdown"); } - private async Task DownloadPluginIconAsync(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame) + private async Task DownloadPluginIconAsync(LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame) { if (plugin is { IsDev: true }) { @@ -559,7 +559,7 @@ internal class PluginImageCache : IDisposable, IServiceType return icon; } - private async Task DownloadPluginImagesAsync(TextureWrap?[] pluginImages, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame) + private async Task DownloadPluginImagesAsync(IDalamudTextureWrap?[] pluginImages, LocalPlugin? plugin, IPluginManifest manifest, bool isThirdParty, ulong requestedFrame) { if (plugin is { IsDev: true }) { diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index dcbdced28..6e2ad862c 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -62,8 +62,8 @@ internal class PluginInstallerWindow : Window, IDisposable private string[] testerImagePaths = new string[5]; private string testerIconPath = string.Empty; - private TextureWrap?[] testerImages; - private TextureWrap? testerIcon; + private IDalamudTextureWrap?[] testerImages; + private IDalamudTextureWrap? testerIcon; private bool testerError = false; private bool testerUpdateAvailable = false; @@ -1525,7 +1525,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGuiHelpers.ScaledDummy(20); - static void CheckImageSize(TextureWrap? image, int maxWidth, int maxHeight, bool requireSquare) + static void CheckImageSize(IDalamudTextureWrap? image, int maxWidth, int maxHeight, bool requireSquare) { if (image == null) return; @@ -1570,7 +1570,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.testerIcon = im.LoadImage(this.testerIconPath); } - this.testerImages = new TextureWrap[this.testerImagePaths.Length]; + this.testerImages = new IDalamudTextureWrap[this.testerImagePaths.Length]; for (var i = 0; i < this.testerImagePaths.Length; i++) { @@ -1822,7 +1822,7 @@ internal class PluginInstallerWindow : Window, IDisposable var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos(); if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize)) { - TextureWrap icon; + IDalamudTextureWrap icon; if (log is PluginChangelogEntry pluginLog) { icon = this.imageCache.DefaultIcon; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 9a7236f2f..ec9833b78 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -171,7 +171,7 @@ Dalamud is licensed under AGPL v3 or later. Contribute at: https://github.com/goatcorp/Dalamud "; - private readonly TextureWrap logoTexture; + private readonly IDalamudTextureWrap logoTexture; private readonly Stopwatch creditsThrottler; private string creditsText; diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 20d260704..e77a3db4e 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -25,7 +25,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable private const float TargetFontSizePt = 18f; private const float TargetFontSizePx = TargetFontSizePt * 4 / 3; - private readonly TextureWrap shadeTexture; + private readonly IDalamudTextureWrap shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs index 3123ffbb8..6665bbafb 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Reflection; +using Dalamud.Interface.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -32,7 +33,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu public IReadOnlyList Entries => this.entries; /// - public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered) + public TitleScreenMenuEntry AddEntry(string text, IDalamudTextureWrap texture, Action onTriggered) { if (texture.Height != TextureSize || texture.Width != TextureSize) { @@ -55,7 +56,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu } /// - public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered) + public TitleScreenMenuEntry AddEntry(ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered) { if (texture.Height != TextureSize || texture.Width != TextureSize) { @@ -91,7 +92,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu /// The action to execute when the option is selected. /// A object that can be used to manage the entry. /// Thrown when the texture provided does not match the required resolution(64x64). - internal TitleScreenMenuEntry AddEntryCore(ulong priority, string text, TextureWrap texture, Action onTriggered) + internal TitleScreenMenuEntry AddEntryCore(ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered) { if (texture.Height != TextureSize || texture.Width != TextureSize) { @@ -117,7 +118,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu /// The action to execute when the option is selected. /// A object that can be used to manage the entry. /// Thrown when the texture provided does not match the required resolution(64x64). - internal TitleScreenMenuEntry AddEntryCore(string text, TextureWrap texture, Action onTriggered) + internal TitleScreenMenuEntry AddEntryCore(string text, IDalamudTextureWrap texture, Action onTriggered) { if (texture.Height != TextureSize || texture.Width != TextureSize) { @@ -169,7 +170,7 @@ internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleSc } /// - public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered) + public TitleScreenMenuEntry AddEntry(string text, IDalamudTextureWrap texture, Action onTriggered) { var entry = this.titleScreenMenuService.AddEntry(text, texture, onTriggered); this.pluginEntries.Add(entry); @@ -178,7 +179,7 @@ internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleSc } /// - public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered) + public TitleScreenMenuEntry AddEntry(ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered) { var entry = this.titleScreenMenuService.AddEntry(priority, text, texture, onTriggered); this.pluginEntries.Add(entry); diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs index 18acc4f47..76382ace2 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs @@ -1,6 +1,6 @@ using System.Reflection; -using ImGuiScene; +using Dalamud.Interface.Internal; namespace Dalamud.Interface; @@ -19,7 +19,7 @@ public class TitleScreenMenuEntry : IComparable /// The text to show. /// The texture to show. /// The action to execute when the option is selected. - internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, TextureWrap texture, Action onTriggered) + internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered) { this.CallingAssembly = callingAssembly; this.Priority = priority; @@ -41,7 +41,7 @@ public class TitleScreenMenuEntry : IComparable /// /// Gets or sets the texture of this entry. /// - public TextureWrap Texture { get; set; } + public IDalamudTextureWrap Texture { get; set; } /// /// Gets or sets a value indicating whether or not this entry is internal. diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 95ee28f56..dd2e5bad3 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -235,7 +235,7 @@ public sealed class UiBuilder : IDisposable /// /// The full filepath to the image. /// A object wrapping the created image. Use inside ImGui.Image(). - public TextureWrap LoadImage(string filePath) + public IDalamudTextureWrap LoadImage(string filePath) => this.InterfaceManagerWithScene?.LoadImage(filePath) ?? throw new InvalidOperationException("Load failed."); @@ -244,7 +244,7 @@ public sealed class UiBuilder : IDisposable /// /// A byte array containing the raw image data. /// A object wrapping the created image. Use inside ImGui.Image(). - public TextureWrap LoadImage(byte[] imageData) + public IDalamudTextureWrap LoadImage(byte[] imageData) => this.InterfaceManagerWithScene?.LoadImage(imageData) ?? throw new InvalidOperationException("Load failed."); @@ -256,7 +256,7 @@ public sealed class UiBuilder : IDisposable /// The height of the image contained in . /// The number of channels (bytes per pixel) of the image contained in . This should usually be 4. /// A object wrapping the created image. Use inside ImGui.Image(). - public TextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) + public IDalamudTextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) => this.InterfaceManagerWithScene?.LoadImageRaw(imageData, width, height, numChannels) ?? throw new InvalidOperationException("Load failed."); @@ -273,7 +273,7 @@ public sealed class UiBuilder : IDisposable /// /// The full filepath to the image. /// A object wrapping the created image. Use inside ImGui.Image(). - public Task LoadImageAsync(string filePath) => Task.Run( + public Task LoadImageAsync(string filePath) => Task.Run( async () => (await this.InterfaceManagerWithSceneAsync).LoadImage(filePath) ?? throw new InvalidOperationException("Load failed.")); @@ -283,7 +283,7 @@ public sealed class UiBuilder : IDisposable /// /// A byte array containing the raw image data. /// A object wrapping the created image. Use inside ImGui.Image(). - public Task LoadImageAsync(byte[] imageData) => Task.Run( + public Task LoadImageAsync(byte[] imageData) => Task.Run( async () => (await this.InterfaceManagerWithSceneAsync).LoadImage(imageData) ?? throw new InvalidOperationException("Load failed.")); @@ -296,7 +296,7 @@ public sealed class UiBuilder : IDisposable /// The height of the image contained in . /// The number of channels (bytes per pixel) of the image contained in . This should usually be 4. /// A object wrapping the created image. Use inside ImGui.Image(). - public Task LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => Task.Run( + public Task LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => Task.Run( async () => (await this.InterfaceManagerWithSceneAsync).LoadImageRaw(imageData, width, height, numChannels) ?? throw new InvalidOperationException("Load failed.")); diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index d41256fa2..e78546ed9 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -1,8 +1,8 @@ -using System; using System.Collections.Generic; using System.Linq; using Dalamud.Data; +using Dalamud.Interface.Internal; using Dalamud.Utility; using ImGuiScene; using Lumina.Data.Files; @@ -38,7 +38,7 @@ public class UldWrapper : IDisposable /// The path of the requested texture. /// The index of the desired icon. /// A TextureWrap containing the requested part if it exists and null otherwise. - public TextureWrap? LoadTexturePart(string texturePath, int part) + public IDalamudTextureWrap? LoadTexturePart(string texturePath, int part) { if (!this.Valid) { @@ -67,7 +67,7 @@ public class UldWrapper : IDisposable this.Uld = null; } - private TextureWrap? CreateTexture(uint id, int width, int height, bool hd, byte[] rgbaData, int partIdx) + private IDalamudTextureWrap? CreateTexture(uint id, int width, int height, bool hd, byte[] rgbaData, int partIdx) { var idx = 0; UldRoot.PartData? partData = null; @@ -105,7 +105,7 @@ public class UldWrapper : IDisposable return this.CopyRect(width, height, rgbaData, d); } - private TextureWrap? CopyRect(int width, int height, byte[] rgbaData, UldRoot.PartData part) + private IDalamudTextureWrap? CopyRect(int width, int height, byte[] rgbaData, UldRoot.PartData part) { if (part.V + part.W > width || part.U + part.H > height) { diff --git a/Dalamud/Plugin/Services/ITitleScreenMenu.cs b/Dalamud/Plugin/Services/ITitleScreenMenu.cs index 2094dc435..b4af06e71 100644 --- a/Dalamud/Plugin/Services/ITitleScreenMenu.cs +++ b/Dalamud/Plugin/Services/ITitleScreenMenu.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using Dalamud.Interface; +using Dalamud.Interface.Internal; using ImGuiScene; namespace Dalamud.Plugin.Services; @@ -23,7 +24,7 @@ public interface ITitleScreenMenu /// The action to execute when the option is selected. /// A object that can be used to manage the entry. /// Thrown when the texture provided does not match the required resolution(64x64). - public TitleScreenMenuEntry AddEntry(string text, TextureWrap texture, Action onTriggered); + public TitleScreenMenuEntry AddEntry(string text, IDalamudTextureWrap texture, Action onTriggered); /// /// Adds a new entry to the title screen menu. @@ -34,7 +35,7 @@ public interface ITitleScreenMenu /// The action to execute when the option is selected. /// A object that can be used to manage the entry. /// Thrown when the texture provided does not match the required resolution(64x64). - public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered); + public TitleScreenMenuEntry AddEntry(ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered); /// /// Remove an entry from the title screen menu. From c767971a364fae3e3a724abf21bfad07f1366488 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 23 Sep 2023 13:09:43 +0200 Subject: [PATCH 177/585] move around new Addon services a bit to match folder structure --- Dalamud/Dalamud.csproj | 4 ++++ .../{AddonEventManager => Addon/Events}/AddonCursorType.cs | 2 +- .../{AddonEventManager => Addon/Events}/AddonEventEntry.cs | 6 ++---- .../Events}/AddonEventHandle.cs | 4 +--- .../Events}/AddonEventListener.cs | 3 +-- .../Events}/AddonEventManager.cs | 7 ++++--- .../Events}/AddonEventManagerAddressResolver.cs | 2 +- .../{AddonEventManager => Addon/Events}/AddonEventType.cs | 2 +- .../Events}/IAddonEventHandle.cs | 4 +--- .../Events}/PluginEventController.cs | 5 ++--- .../Lifecycle}/AddonArgTypes/AddonArgs.cs | 2 +- .../Lifecycle}/AddonArgTypes/AddonDrawArgs.cs | 2 +- .../Lifecycle}/AddonArgTypes/AddonFinalizeArgs.cs | 2 +- .../Lifecycle}/AddonArgTypes/AddonRefreshArgs.cs | 2 +- .../Lifecycle}/AddonArgTypes/AddonRequestedUpdateArgs.cs | 2 +- .../Lifecycle}/AddonArgTypes/AddonSetupArgs.cs | 6 ++---- .../Lifecycle}/AddonArgTypes/AddonUpdateArgs.cs | 2 +- .../{AddonLifecycle => Addon/Lifecycle}/AddonArgsType.cs | 2 +- .../Game/{AddonLifecycle => Addon/Lifecycle}/AddonEvent.cs | 2 +- .../{AddonLifecycle => Addon/Lifecycle}/AddonLifecycle.cs | 4 ++-- .../Lifecycle}/AddonLifecycleAddressResolver.cs | 2 +- .../Lifecycle}/AddonLifecycleEventListener.cs | 2 +- Dalamud/Game/Gui/Dtr/DtrBar.cs | 3 +++ .../Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs | 2 ++ Dalamud/Plugin/Services/IAddonEventManager.cs | 1 + Dalamud/Plugin/Services/IAddonLifecycle.cs | 2 ++ 26 files changed, 40 insertions(+), 37 deletions(-) rename Dalamud/Game/{AddonEventManager => Addon/Events}/AddonCursorType.cs (97%) rename Dalamud/Game/{AddonEventManager => Addon/Events}/AddonEventEntry.cs (96%) rename Dalamud/Game/{AddonEventManager => Addon/Events}/AddonEventHandle.cs (90%) rename Dalamud/Game/{AddonEventManager => Addon/Events}/AddonEventListener.cs (98%) rename Dalamud/Game/{AddonEventManager => Addon/Events}/AddonEventManager.cs (98%) rename Dalamud/Game/{AddonEventManager => Addon/Events}/AddonEventManagerAddressResolver.cs (94%) rename Dalamud/Game/{AddonEventManager => Addon/Events}/AddonEventType.cs (98%) rename Dalamud/Game/{AddonEventManager => Addon/Events}/IAddonEventHandle.cs (93%) rename Dalamud/Game/{AddonEventManager => Addon/Events}/PluginEventController.cs (98%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonArgTypes/AddonArgs.cs (96%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonArgTypes/AddonDrawArgs.cs (77%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonArgTypes/AddonFinalizeArgs.cs (79%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonArgTypes/AddonRefreshArgs.cs (92%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonArgTypes/AddonRequestedUpdateArgs.cs (89%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonArgTypes/AddonSetupArgs.cs (86%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonArgTypes/AddonUpdateArgs.cs (86%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonArgsType.cs (94%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonEvent.cs (97%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonLifecycle.cs (99%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonLifecycleAddressResolver.cs (97%) rename Dalamud/Game/{AddonLifecycle => Addon/Lifecycle}/AddonLifecycleEventListener.cs (97%) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index dd84c42e5..5093fbfe9 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -105,6 +105,10 @@ + + + + diff --git a/Dalamud/Game/AddonEventManager/AddonCursorType.cs b/Dalamud/Game/Addon/Events/AddonCursorType.cs similarity index 97% rename from Dalamud/Game/AddonEventManager/AddonCursorType.cs rename to Dalamud/Game/Addon/Events/AddonCursorType.cs index 57d58c60c..83a81582c 100644 --- a/Dalamud/Game/AddonEventManager/AddonCursorType.cs +++ b/Dalamud/Game/Addon/Events/AddonCursorType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Events; /// /// Reimplementation of CursorType. diff --git a/Dalamud/Game/AddonEventManager/AddonEventEntry.cs b/Dalamud/Game/Addon/Events/AddonEventEntry.cs similarity index 96% rename from Dalamud/Game/AddonEventManager/AddonEventEntry.cs rename to Dalamud/Game/Addon/Events/AddonEventEntry.cs index 48c3feb24..a7430acf0 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventEntry.cs +++ b/Dalamud/Game/Addon/Events/AddonEventEntry.cs @@ -1,10 +1,8 @@ -using System; - -using Dalamud.Memory; +using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Events; /// /// This class represents a registered event that a plugin registers with a native ui node. diff --git a/Dalamud/Game/AddonEventManager/AddonEventHandle.cs b/Dalamud/Game/Addon/Events/AddonEventHandle.cs similarity index 90% rename from Dalamud/Game/AddonEventManager/AddonEventHandle.cs rename to Dalamud/Game/Addon/Events/AddonEventHandle.cs index 48abba9a0..fb0e2886c 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventHandle.cs +++ b/Dalamud/Game/Addon/Events/AddonEventHandle.cs @@ -1,6 +1,4 @@ -using System; - -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Events; /// /// Class that represents a addon event handle. diff --git a/Dalamud/Game/AddonEventManager/AddonEventListener.cs b/Dalamud/Game/Addon/Events/AddonEventListener.cs similarity index 98% rename from Dalamud/Game/AddonEventManager/AddonEventListener.cs rename to Dalamud/Game/Addon/Events/AddonEventListener.cs index 6f7c55c4c..ceac38108 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventListener.cs +++ b/Dalamud/Game/Addon/Events/AddonEventListener.cs @@ -1,9 +1,8 @@ -using System; using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Events; /// /// Event listener class for managing custom events. diff --git a/Dalamud/Game/AddonEventManager/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs similarity index 98% rename from Dalamud/Game/AddonEventManager/AddonEventManager.cs rename to Dalamud/Game/Addon/Events/AddonEventManager.cs index dfc037e23..8ec77b10d 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -1,7 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; @@ -11,7 +12,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Events; /// /// Service provider for addon event management. diff --git a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs similarity index 94% rename from Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs rename to Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs index 71a6093bb..1405446fe 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Events; /// /// AddonEventManager memory address resolver. diff --git a/Dalamud/Game/AddonEventManager/AddonEventType.cs b/Dalamud/Game/Addon/Events/AddonEventType.cs similarity index 98% rename from Dalamud/Game/AddonEventManager/AddonEventType.cs rename to Dalamud/Game/Addon/Events/AddonEventType.cs index 74f35c257..2c6c96334 100644 --- a/Dalamud/Game/AddonEventManager/AddonEventType.cs +++ b/Dalamud/Game/Addon/Events/AddonEventType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Events; /// /// Reimplementation of AtkEventType. diff --git a/Dalamud/Game/AddonEventManager/IAddonEventHandle.cs b/Dalamud/Game/Addon/Events/IAddonEventHandle.cs similarity index 93% rename from Dalamud/Game/AddonEventManager/IAddonEventHandle.cs rename to Dalamud/Game/Addon/Events/IAddonEventHandle.cs index 3b2c5c3ae..f9272c92a 100644 --- a/Dalamud/Game/AddonEventManager/IAddonEventHandle.cs +++ b/Dalamud/Game/Addon/Events/IAddonEventHandle.cs @@ -1,6 +1,4 @@ -using System; - -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Events; /// /// Interface representing the data used for managing AddonEvents. diff --git a/Dalamud/Game/AddonEventManager/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs similarity index 98% rename from Dalamud/Game/AddonEventManager/PluginEventController.cs rename to Dalamud/Game/Addon/Events/PluginEventController.cs index b66bbc99e..7847dd482 100644 --- a/Dalamud/Game/AddonEventManager/PluginEventController.cs +++ b/Dalamud/Game/Addon/Events/PluginEventController.cs @@ -1,5 +1,4 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Dalamud.Game.Gui; @@ -8,7 +7,7 @@ using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Events; /// /// Class to manage creating and cleaning up events per-plugin. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs similarity index 96% rename from Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs rename to Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 949d3fde9..334542c71 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -1,7 +1,7 @@ using Dalamud.Memory; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Base class for AddonLifecycle AddonArgTypes. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs similarity index 77% rename from Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs rename to Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index d93001d1c..6bb72f567 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Finalize events. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs similarity index 79% rename from Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs rename to Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index ed7aa1b3c..782943955 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Finalize events. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs similarity index 92% rename from Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs rename to Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index 6376c16b0..a50dc68f6 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -1,6 +1,6 @@ using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Finalize events. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs similarity index 89% rename from Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs rename to Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index a31369aaf..e73d11e23 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Finalize events. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs similarity index 86% rename from Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs rename to Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index 17c87967a..df2ec26be 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -1,8 +1,6 @@ -using System; +using FFXIVClientStructs.FFXIV.Component.GUI; -using FFXIVClientStructs.FFXIV.Component.GUI; - -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Setup events. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs similarity index 86% rename from Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs rename to Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index 993883d77..6870746db 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Finalize events. diff --git a/Dalamud/Game/AddonLifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs similarity index 94% rename from Dalamud/Game/AddonLifecycle/AddonArgsType.cs rename to Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 8a07d445b..11f73a4de 100644 --- a/Dalamud/Game/AddonLifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle; /// /// Enumeration for available AddonLifecycle arg data. diff --git a/Dalamud/Game/AddonLifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs similarity index 97% rename from Dalamud/Game/AddonLifecycle/AddonEvent.cs rename to Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index cfc83fb8a..75a77482d 100644 --- a/Dalamud/Game/AddonLifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle; /// /// Enumeration for available AddonLifecycle events. diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs similarity index 99% rename from Dalamud/Game/AddonLifecycle/AddonLifecycle.cs rename to Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 8de98abcc..f1ee69f2b 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,8 +1,8 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; using Dalamud.Hooking.Internal; using Dalamud.IoC; @@ -11,7 +11,7 @@ using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle; /// /// This class provides events for in-game addon lifecycles. diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs similarity index 97% rename from Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs rename to Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 16fd54832..7690db50d 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle; /// /// AddonLifecycleService memory address resolver. diff --git a/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs similarity index 97% rename from Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs rename to Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs index 12ccf5e8f..6464a1edd 100644 --- a/Dalamud/Game/AddonLifecycle/AddonLifecycleEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleEventListener.cs @@ -1,6 +1,6 @@ using Dalamud.Plugin.Services; -namespace Dalamud.Game.Addon; +namespace Dalamud.Game.Addon.Lifecycle; /// /// This class is a helper for tracking and invoking listener delegates. diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index e8e6ca6c5..06d37e7ec 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -4,6 +4,9 @@ using System.Linq; using Dalamud.Configuration.Internal; using Dalamud.Game.Addon; +using Dalamud.Game.Addon.Events; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Text.SeStringHandling; using Dalamud.IoC; using Dalamud.IoC.Internal; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs index a9948430f..b2229e4e4 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/AddonLifecycleAgingStep.cs @@ -1,6 +1,8 @@ using System.Collections.Generic; using Dalamud.Game.Addon; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index 52f836b4f..e696bbaae 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -1,4 +1,5 @@ using Dalamud.Game.Addon; +using Dalamud.Game.Addon.Events; namespace Dalamud.Plugin.Services; diff --git a/Dalamud/Plugin/Services/IAddonLifecycle.cs b/Dalamud/Plugin/Services/IAddonLifecycle.cs index 2bc41a366..6f44349d5 100644 --- a/Dalamud/Plugin/Services/IAddonLifecycle.cs +++ b/Dalamud/Plugin/Services/IAddonLifecycle.cs @@ -2,6 +2,8 @@ using System.Runtime.InteropServices; using Dalamud.Game.Addon; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; namespace Dalamud.Plugin.Services; From 6295f047aec8a54e9ebe20ab4352aa0a18fc4e06 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 23 Sep 2023 07:55:17 -0700 Subject: [PATCH 178/585] Add Size Vector to IDalamudTextureWrap --- Dalamud/Interface/Internal/DalamudTextureWrap.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudTextureWrap.cs b/Dalamud/Interface/Internal/DalamudTextureWrap.cs index 036686c29..9737d9f7b 100644 --- a/Dalamud/Interface/Internal/DalamudTextureWrap.cs +++ b/Dalamud/Interface/Internal/DalamudTextureWrap.cs @@ -1,4 +1,6 @@ -using ImGuiScene; +using System.Numerics; + +using ImGuiScene; namespace Dalamud.Interface.Internal; @@ -22,6 +24,11 @@ public interface IDalamudTextureWrap : IDisposable /// Gets the height of the texture. /// int Height { get; } + + /// + /// Gets the size vector of the texture using Width, Height. + /// + Vector2 Size => new(this.Width, this.Height); } /// From 64f76ec69f26383e4a0af9b3567caab954d143ff Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 23 Sep 2023 17:20:19 +0200 Subject: [PATCH 179/585] Fix SignatureHelper Hooks --- Dalamud/Utility/Signatures/SignatureHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index 9186b1fc0..e2c9926a8 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -161,7 +161,7 @@ internal static class SignatureHelper continue; } - var hook = creator.Invoke(null, new object?[] { ptr, detour, IGameInteropProvider.HookBackend.Automatic }) as IDalamudHook; + var hook = creator.Invoke(null, new object?[] { ptr, detour, false }) as IDalamudHook; info.SetValue(self, hook); createdHooks.Add(hook); From 8527e035f11afbd963706232512b18876348700f Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 23 Sep 2023 17:37:41 +0200 Subject: [PATCH 180/585] chore: remove refcounting, keepalive logic from TextureManager, remove scoped service Makes this whole thing a lot simpler to use and easier to understand. --- Dalamud/Interface/Internal/TextureManager.cs | 193 +++---------------- Dalamud/Plugin/Services/ITextureProvider.cs | 6 +- 2 files changed, 25 insertions(+), 174 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index ce08e6cc7..542299656 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -1,8 +1,6 @@ -using System.Collections.Concurrent; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.IO; -using System.Linq; using System.Numerics; using Dalamud.Data; @@ -11,13 +9,14 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using ImGuiScene; using Lumina.Data.Files; using Lumina.Data.Parsing.Tex.Buffers; using SharpDX.DXGI; namespace Dalamud.Interface.Internal; +// TODO API10: Remove keepAlive from public APIs + /// /// Service responsible for loading and disposing ImGui texture wraps. /// @@ -25,9 +24,10 @@ namespace Dalamud.Interface.Internal; [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] #pragma warning disable SA1015 +[ResolveVia] [ResolveVia] #pragma warning restore SA1015 -internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionProvider +internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITextureSubstitutionProvider { private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; @@ -78,16 +78,16 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// If null, default to the game's current language. /// /// - /// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set. + /// Not used. This parameter is ignored. /// /// /// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used /// to render the icon. /// - public TextureManagerTextureWrap? GetIcon(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false) + public IDalamudTextureWrap? GetIcon(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false) { var path = this.GetIconPath(iconId, flags, language); - return path == null ? null : this.CreateWrap(path, keepAlive); + return path == null ? null : this.CreateWrap(path); } /// @@ -171,16 +171,16 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// You may only specify paths in the game's VFS. /// /// The path to the texture in the game's VFS. - /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set. + /// Not used. This parameter is ignored. /// Null, if the icon does not exist, or a texture wrap that can be used to render the texture. - public TextureManagerTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) + public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) { ArgumentException.ThrowIfNullOrEmpty(path); if (Path.IsPathRooted(path)) throw new ArgumentException("Use GetTextureFromFile() to load textures directly from a file.", nameof(path)); - return !this.dataManager.FileExists(path) ? null : this.CreateWrap(path, keepAlive); + return !this.dataManager.FileExists(path) ? null : this.CreateWrap(path); } /// @@ -190,12 +190,12 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// This API can load .png and .tex files. /// /// The FileInfo describing the image or texture file. - /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set. + /// Not used. This parameter is ignored. /// Null, if the file does not exist, or a texture wrap that can be used to render the texture. - public TextureManagerTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) + public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) { ArgumentNullException.ThrowIfNull(file); - return !file.Exists ? null : this.CreateWrap(file.FullName, keepAlive); + return !file.Exists ? null : this.CreateWrap(file.FullName); } /// @@ -307,8 +307,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP throw new Exception("null info in activeTextures"); } - if (info.KeepAliveCount == 0) - info.LastAccess = DateTime.UtcNow; + info.LastAccess = DateTime.UtcNow; if (info is { Wrap: not null }) return info; @@ -384,33 +383,6 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP return info; } - /// - /// Notify the system about an instance of a texture wrap being disposed. - /// If required conditions are met, the texture will be unloaded at the next update. - /// - /// The path to the texture. - /// Whether or not this handle was created in keep-alive mode. - internal void NotifyTextureDisposed(string path, bool keepAlive) - { - lock (this.activeTextures) - { - if (!this.activeTextures.TryGetValue(path, out var info)) - { - Log.Warning("Disposing texture that didn't exist: {Path}", path); - return; - } - - info.RefCount--; - - if (keepAlive) - info.KeepAliveCount--; - - // Clean it up by the next update. If it's re-requested in-between, we don't reload it. - if (info.RefCount <= 0) - info.LastAccess = default; - } - } - private static string FormatIconPath(uint iconId, string? type, bool highResolution) { var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; @@ -422,23 +394,15 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP return string.Format(format, iconId / 1000, type, iconId); } - private TextureManagerTextureWrap? CreateWrap(string path, bool keepAlive) + private TextureManagerTextureWrap? CreateWrap(string path) { lock (this.activeTextures) { // This will create the texture. // That's fine, it's probably used immediately and this will let the plugin catch load errors. var info = this.GetInfo(path, rethrow: true); - - // We need to increase the refcounts here while locking the collection! - // Otherwise, if this is loaded from a task, cleanup might already try to delete it - // before it can be increased. - info.RefCount++; - - if (keepAlive) - info.KeepAliveCount++; - return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this); + return new TextureManagerTextureWrap(path, info.Extents, this); } } @@ -450,19 +414,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP foreach (var texInfo in this.activeTextures) { - if (texInfo.Value.RefCount == 0) - { - Log.Verbose("Evicting {Path} since no refs", texInfo.Key); - - Debug.Assert(texInfo.Value.KeepAliveCount == 0, "texInfo.Value.KeepAliveCount == 0"); - - texInfo.Value.Wrap?.Dispose(); - texInfo.Value.Wrap = null; - toRemove.Add(texInfo.Key); - continue; - } - - if (texInfo.Value.KeepAliveCount > 0 || texInfo.Value.Wrap == null) + if (texInfo.Value.Wrap == null) continue; if (DateTime.UtcNow - texInfo.Value.LastAccess > TimeSpan.FromMilliseconds(MillisecondsEvictionTime)) @@ -470,6 +422,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP Log.Verbose("Evicting {Path} since too old", texInfo.Key); texInfo.Value.Wrap.Dispose(); texInfo.Value.Wrap = null; + toRemove.Add(texInfo.Key); } } @@ -501,16 +454,6 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// Gets or sets the time the texture was last accessed. /// public DateTime LastAccess { get; set; } - - /// - /// Gets or sets the number of active holders of this texture. - /// - public uint RefCount { get; set; } - - /// - /// Gets or sets the number of active holders that want this texture to stay alive forever. - /// - public uint KeepAliveCount { get; set; } /// /// Gets or sets the extents of the texture. @@ -519,90 +462,6 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP } } -/// -/// Plugin-scoped version of a texture manager. -/// -[PluginInterface] -[InterfaceVersion("1.0")] -[ServiceManager.ScopedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -internal class TextureProviderPluginScoped : ITextureProvider, IServiceType, IDisposable -{ - private readonly TextureManager textureManager; - - private readonly ConcurrentBag trackedTextures = new(); - - /// - /// Initializes a new instance of the class. - /// - /// TextureManager instance. - public TextureProviderPluginScoped(TextureManager textureManager) - { - this.textureManager = textureManager; - } - - /// - public IDalamudTextureWrap? GetIcon( - uint iconId, - ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.ItemHighQuality, - ClientLanguage? language = null, - bool keepAlive = false) - { - var wrap = this.textureManager.GetIcon(iconId, flags, language, keepAlive); - if (wrap == null) - return null; - - this.trackedTextures.Add(wrap); - return wrap; - } - - /// - public string? GetIconPath(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null) - => this.textureManager.GetIconPath(iconId, flags, language); - - /// - public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) - { - ArgumentException.ThrowIfNullOrEmpty(path); - - var wrap = this.textureManager.GetTextureFromGame(path, keepAlive); - if (wrap == null) - return null; - - this.trackedTextures.Add(wrap); - return wrap; - } - - /// - public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive) - { - ArgumentNullException.ThrowIfNull(file); - - var wrap = this.textureManager.GetTextureFromFile(file, keepAlive); - if (wrap == null) - return null; - - this.trackedTextures.Add(wrap); - return wrap; - } - - /// - public IDalamudTextureWrap? GetTexture(TexFile file) - => this.textureManager.GetTexture(file); - - /// - public void Dispose() - { - // Dispose all leaked textures - foreach (var textureWrap in this.trackedTextures.Where(x => !x.IsDisposed)) - { - textureWrap.Dispose(); - } - } -} - /// /// Wrap. /// @@ -610,19 +469,16 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap { private readonly TextureManager manager; private readonly string path; - private readonly bool keepAlive; /// /// Initializes a new instance of the class. /// /// The path to the texture. /// The extents of the texture. - /// Keep alive or not. /// Manager that we obtained this from. - internal TextureManagerTextureWrap(string path, Vector2 extents, bool keepAlive, TextureManager manager) + internal TextureManagerTextureWrap(string path, Vector2 extents, TextureManager manager) { this.path = path; - this.keepAlive = keepAlive; this.manager = manager; this.Width = (int)extents.X; this.Height = (int)extents.Y; @@ -648,12 +504,7 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap /// public void Dispose() { - lock (this) - { - if (!this.IsDisposed) - this.manager.NotifyTextureDisposed(this.path, this.keepAlive); - - this.IsDisposed = true; - } + this.IsDisposed = true; + // This is a no-op. The manager cleans up textures that are not being drawn. } } diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 091b2ed67..f91d4ee8e 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -44,7 +44,7 @@ public interface ITextureProvider /// If null, default to the game's current language. /// /// - /// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set. + /// Not used. This parameter is ignored. /// /// /// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used @@ -72,7 +72,7 @@ public interface ITextureProvider /// You may only specify paths in the game's VFS. /// /// The path to the texture in the game's VFS. - /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set. + /// Not used. This parameter is ignored. /// Null, if the icon does not exist, or a texture wrap that can be used to render the texture. public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false); @@ -83,7 +83,7 @@ public interface ITextureProvider /// This API can load .png and .tex files. /// /// The FileInfo describing the image or texture file. - /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set. + /// Not used. This parameter is ignored. /// Null, if the file does not exist, or a texture wrap that can be used to render the texture. public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false); From acb81deb9c073829ff22c5c895096c3d1f079750 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 23 Sep 2023 17:45:50 +0200 Subject: [PATCH 181/585] make sure that access is completely atomic --- Dalamud/Interface/Internal/TextureManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 542299656..7c773bd36 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -305,12 +305,12 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe if (info == null) throw new Exception("null info in activeTextures"); - } - - info.LastAccess = DateTime.UtcNow; + + info.LastAccess = DateTime.UtcNow; - if (info is { Wrap: not null }) - return info; + if (info is { Wrap: not null }) + return info; + } if (!this.im.IsReady) throw new InvalidOperationException("Cannot create textures before scene is ready"); From ebabb7bd049e8e428bfdc476811f723ca7882ded Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 24 Sep 2023 01:40:56 +0200 Subject: [PATCH 182/585] chore: make SigScanner public, have separate service TargetSigScanner that resolves via ISigScanner (closes #1426) --- Dalamud/Dalamud.cs | 2 +- .../Game/Addon/Events/AddonEventManager.cs | 2 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/BaseAddressResolver.cs | 9 ------ Dalamud/Game/ClientState/ClientState.cs | 2 +- Dalamud/Game/ClientState/Keys/KeyState.cs | 2 +- Dalamud/Game/Config/GameConfig.cs | 2 +- Dalamud/Game/DutyState/DutyState.cs | 2 +- Dalamud/Game/Framework.cs | 2 +- Dalamud/Game/Gui/ChatGui.cs | 2 +- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 2 +- Dalamud/Game/Gui/GameGui.cs | 2 +- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 2 +- Dalamud/Game/Gui/Toast/ToastGui.cs | 2 +- Dalamud/Game/Internal/AntiDebug.cs | 2 +- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 2 +- Dalamud/Game/Libc/LibcFunction.cs | 2 +- Dalamud/Game/Network/GameNetwork.cs | 2 +- Dalamud/Game/SigScanner.cs | 7 +---- Dalamud/Game/TargetSigScanner.cs | 28 +++++++++++++++++++ .../GameInteropProviderPluginScoped.cs | 2 +- .../Interface/Internal/InterfaceManager.cs | 2 +- .../Windows/Data/Widgets/AddressesWidget.cs | 2 +- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- Dalamud/ServiceManager.cs | 8 +++--- Dalamud/Utility/Signatures/SignatureHelper.cs | 2 +- 26 files changed, 55 insertions(+), 41 deletions(-) create mode 100644 Dalamud/Game/TargetSigScanner.cs diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index c38594771..a9d822f55 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -167,7 +167,7 @@ internal sealed class Dalamud : IServiceType internal void ReplaceExceptionHandler() { var releaseSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??"; - var releaseFilter = Service.Get().ScanText(releaseSig); + var releaseFilter = Service.Get().ScanText(releaseSig); Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}"); var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter); diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 8ec77b10d..a91f5437c 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -41,7 +41,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType private AddonCursorType? cursorOverride; [ServiceManager.ServiceConstructor] - private AddonEventManager(SigScanner sigScanner) + private AddonEventManager(TargetSigScanner sigScanner) { this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index f1ee69f2b..d4e45688d 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,7 +38,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly List eventListeners = new(); [ServiceManager.ServiceConstructor] - private AddonLifecycle(SigScanner sigScanner) + private AddonLifecycle(TargetSigScanner sigScanner) { this.address = new AddonLifecycleAddressResolver(); this.address.Setup(sigScanner); diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index 9935aac7b..cd1ef8fd2 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -22,15 +22,6 @@ internal abstract class BaseAddressResolver /// protected bool IsResolved { get; set; } - /// - /// Setup the resolver, calling the appropriate method based on the process architecture, - /// using the default SigScanner. - /// - /// For plugins. Not intended to be called from Dalamud Service{T} constructors. - /// - [UsedImplicitly] - public void Setup() => this.Setup(Service.Get()); - /// /// Setup the resolver, calling the appropriate method based on the process architecture. /// diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index baf6f6634..ccb87ff0e 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -41,7 +41,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState private bool lastFramePvP; [ServiceManager.ServiceConstructor] - private ClientState(SigScanner sigScanner, DalamudStartInfo startInfo, GameLifecycle lifecycle) + private ClientState(TargetSigScanner sigScanner, DalamudStartInfo startInfo, GameLifecycle lifecycle) { this.lifecycle = lifecycle; this.address = new ClientStateAddressResolver(); diff --git a/Dalamud/Game/ClientState/Keys/KeyState.cs b/Dalamud/Game/ClientState/Keys/KeyState.cs index 03c5d59b9..76bee51bf 100644 --- a/Dalamud/Game/ClientState/Keys/KeyState.cs +++ b/Dalamud/Game/ClientState/Keys/KeyState.cs @@ -39,7 +39,7 @@ internal class KeyState : IServiceType, IKeyState private VirtualKey[]? validVirtualKeyCache; [ServiceManager.ServiceConstructor] - private KeyState(SigScanner sigScanner, ClientState clientState) + private KeyState(TargetSigScanner sigScanner, ClientState clientState) { var moduleBaseAddress = sigScanner.Module.BaseAddress; var addressResolver = clientState.AddressResolver; diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 831c1157b..ea988525c 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -19,7 +19,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable private Hook? configChangeHook; [ServiceManager.ServiceConstructor] - private unsafe GameConfig(Framework framework, SigScanner sigScanner) + private unsafe GameConfig(Framework framework, TargetSigScanner sigScanner) { framework.RunOnTick(() => { diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 3890a1f8b..6dda95a66 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -28,7 +28,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState private readonly ClientState.ClientState clientState = Service.Get(); [ServiceManager.ServiceConstructor] - private DutyState(SigScanner sigScanner) + private DutyState(TargetSigScanner sigScanner) { this.address = new DutyStateAddressResolver(); this.address.Setup(sigScanner); diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 08b97edbc..22343fd8e 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -51,7 +51,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework private Thread? frameworkUpdateThread; [ServiceManager.ServiceConstructor] - private Framework(SigScanner sigScanner, GameLifecycle lifecycle) + private Framework(TargetSigScanner sigScanner, GameLifecycle lifecycle) { this.lifecycle = lifecycle; this.hitchDetector = new HitchDetector("FrameworkUpdate", this.configuration.FrameworkUpdateHitch); diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 55c919ab5..5bf6232fa 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -42,7 +42,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui private IntPtr baseAddress = IntPtr.Zero; [ServiceManager.ServiceConstructor] - private ChatGui(SigScanner sigScanner) + private ChatGui(TargetSigScanner sigScanner) { this.address = new ChatGuiAddressResolver(); this.address.Setup(sigScanner); diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 64de4b2dd..3da8dc2a9 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -30,7 +30,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui private readonly Hook createFlyTextHook; [ServiceManager.ServiceConstructor] - private FlyTextGui(SigScanner sigScanner) + private FlyTextGui(TargetSigScanner sigScanner) { this.Address = new FlyTextGuiAddressResolver(); this.Address.Setup(sigScanner); diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 349d2a424..9796effc5 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -48,7 +48,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui private OpenMapWithFlagDelegate? openMapWithFlag; [ServiceManager.ServiceConstructor] - private GameGui(SigScanner sigScanner) + private GameGui(TargetSigScanner sigScanner) { this.address = new GameGuiAddressResolver(); this.address.Setup(sigScanner); diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 4bd93cdf0..61c0f62e4 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -27,7 +27,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu /// /// Sig scanner to use. [ServiceManager.ServiceConstructor] - private PartyFinderGui(SigScanner sigScanner) + private PartyFinderGui(TargetSigScanner sigScanner) { this.address = new PartyFinderAddressResolver(); this.address.Setup(sigScanner); diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 9624e3e72..362edb3be 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -33,7 +33,7 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui /// /// Sig scanner to use. [ServiceManager.ServiceConstructor] - private ToastGui(SigScanner sigScanner) + private ToastGui(TargetSigScanner sigScanner) { this.address = new ToastGuiAddressResolver(); this.address.Setup(sigScanner); diff --git a/Dalamud/Game/Internal/AntiDebug.cs b/Dalamud/Game/Internal/AntiDebug.cs index ba482ef48..2f4ec28c0 100644 --- a/Dalamud/Game/Internal/AntiDebug.cs +++ b/Dalamud/Game/Internal/AntiDebug.cs @@ -19,7 +19,7 @@ internal sealed partial class AntiDebug : IServiceType private IntPtr debugCheckAddress; [ServiceManager.ServiceConstructor] - private AntiDebug(SigScanner sigScanner) + private AntiDebug(TargetSigScanner sigScanner) { try { diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 60e61b2f7..b45b35c4d 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -41,7 +41,7 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private readonly string locDalamudSettings; [ServiceManager.ServiceConstructor] - private DalamudAtkTweaks(SigScanner sigScanner) + private DalamudAtkTweaks(TargetSigScanner sigScanner) { var openSystemMenuAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 32 C0 4C 8B AC 24 ?? ?? ?? ?? 48 8B 8D ?? ?? ?? ??"); diff --git a/Dalamud/Game/Libc/LibcFunction.cs b/Dalamud/Game/Libc/LibcFunction.cs index b0bd4950c..f1cd07080 100644 --- a/Dalamud/Game/Libc/LibcFunction.cs +++ b/Dalamud/Game/Libc/LibcFunction.cs @@ -24,7 +24,7 @@ internal sealed class LibcFunction : IServiceType, ILibcFunction private readonly StdStringDeallocateDelegate stdStringDeallocate; [ServiceManager.ServiceConstructor] - private LibcFunction(SigScanner sigScanner) + private LibcFunction(TargetSigScanner sigScanner) { this.address = new LibcFunctionAddressResolver(); this.address.Setup(sigScanner); diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 7c900ece4..9ea3e491e 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -30,7 +30,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork private IntPtr baseAddress; [ServiceManager.ServiceConstructor] - private GameNetwork(SigScanner sigScanner) + private GameNetwork(TargetSigScanner sigScanner) { this.hitchDetectorUp = new HitchDetector("GameNetworkUp", this.configuration.GameNetworkUpHitch); this.hitchDetectorDown = new HitchDetector("GameNetworkDown", this.configuration.GameNetworkDownHitch); diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index ace4654be..fe2d9083e 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -20,12 +20,7 @@ namespace Dalamud.Game; /// /// A SigScanner facilitates searching for memory signatures in a given ProcessModule. /// -[PluginInterface] -[InterfaceVersion("1.0")] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 -internal class SigScanner : IDisposable, IServiceType, ISigScanner +public class SigScanner : IDisposable, ISigScanner { private readonly FileInfo? cacheFile; diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs new file mode 100644 index 000000000..0360f95cc --- /dev/null +++ b/Dalamud/Game/TargetSigScanner.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; +using System.IO; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; + +namespace Dalamud.Game; + +/// +/// A SigScanner facilitates searching for memory signatures in a given ProcessModule. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class TargetSigScanner : SigScanner, IServiceType +{ + /// + /// Initializes a new instance of the class. + /// + /// Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. + /// File used to cached signatures. + public TargetSigScanner(bool doCopy = false, FileInfo? cacheFile = null) + : base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile) + { + } +} diff --git a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index 96172e5b2..59f2d2684 100644 --- a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -33,7 +33,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT /// /// Plugin this instance belongs to. /// SigScanner instance for target module. - public GameInteropProviderPluginScoped(LocalPlugin plugin, SigScanner scanner) + public GameInteropProviderPluginScoped(LocalPlugin plugin, TargetSigScanner scanner) { this.plugin = plugin; this.scanner = scanner; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index be6ca3528..d00f33180 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1055,7 +1055,7 @@ internal class InterfaceManager : IDisposable, IServiceType } [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(SigScanner sigScanner, Framework framework) + private void ContinueConstruction(TargetSigScanner sigScanner, Framework framework) { this.address.Setup(sigScanner); framework.RunOnFrameworkThread(() => diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs index 0955c1183..dfa6f173d 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddressesWidget.cs @@ -36,7 +36,7 @@ internal class AddressesWidget : IDataWindowWidget { try { - var sigScanner = Service.Get(); + var sigScanner = Service.Get(); this.sigResult = sigScanner.ScanText(this.inputSig); } catch (KeyNotFoundException) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 691d5f729..f91d4cd56 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -623,7 +623,7 @@ internal partial class PluginManager : IDisposable, IServiceType Log.Error(e, "Failed to load at least one plugin"); } - var sigScanner = await Service.GetAsync().ConfigureAwait(false); + var sigScanner = await Service.GetAsync().ConfigureAwait(false); this.PluginsReady = true; this.NotifyinstalledPluginsListChanged(); sigScanner.Save(); diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index d1c1002bd..ecb58d48b 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -105,15 +105,15 @@ internal static class ServiceManager Service.Provide(new ServiceContainer()); LoadedServices.Add(typeof(ServiceContainer)); - Service.Provide( - new SigScanner( + Service.Provide( + new TargetSigScanner( true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json")))); - LoadedServices.Add(typeof(SigScanner)); + LoadedServices.Add(typeof(TargetSigScanner)); } using (Timings.Start("CS Resolver Init")) { - FFXIVClientStructs.Interop.Resolver.GetInstance.SetupSearchSpace(Service.Get().SearchBase, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json"))); + FFXIVClientStructs.Interop.Resolver.GetInstance.SetupSearchSpace(Service.Get().SearchBase, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json"))); FFXIVClientStructs.Interop.Resolver.GetInstance.Resolve(); } } diff --git a/Dalamud/Utility/Signatures/SignatureHelper.cs b/Dalamud/Utility/Signatures/SignatureHelper.cs index e2c9926a8..51f59bba2 100755 --- a/Dalamud/Utility/Signatures/SignatureHelper.cs +++ b/Dalamud/Utility/Signatures/SignatureHelper.cs @@ -29,7 +29,7 @@ internal static class SignatureHelper /// Collection of created IDalamudHooks. internal static IEnumerable Initialize(object self, bool log = true) { - var scanner = Service.Get(); + var scanner = Service.Get(); var selfType = self.GetType(); var fields = selfType.GetFields(Flags).Select(field => (IFieldOrPropertyInfo)new FieldInfoWrapper(field)) .Concat(selfType.GetProperties(Flags).Select(prop => new PropertyInfoWrapper(prop))) From f96ab7aa90773bbc5da5ce4288c5a243954fd1ce Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 24 Sep 2023 01:45:46 +0200 Subject: [PATCH 183/585] fix warnings --- Dalamud/Game/Config/GameConfig.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index ea988525c..ae3205abc 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -41,6 +41,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable /// public event EventHandler? Changed; +#pragma warning disable 67 /// /// Unused internally, used as a proxy for System.Changed via GameConfigPluginScoped /// @@ -55,6 +56,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable /// Unused internally, used as a proxy for UiControl.Changed via GameConfigPluginScoped /// public event EventHandler? UiControlChanged; +#pragma warning restore 67 /// public GameConfigSection System { get; private set; } From 34c05adeb13e4bf8d51d39064f476479735cc1fb Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 23 Sep 2023 19:58:21 -0700 Subject: [PATCH 184/585] Remove IPluginLog#Logger for now (#1428) - Causes issues with mocking. --- Dalamud/Plugin/Services/IPluginLog.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Dalamud/Plugin/Services/IPluginLog.cs b/Dalamud/Plugin/Services/IPluginLog.cs index d16e985af..aac321092 100644 --- a/Dalamud/Plugin/Services/IPluginLog.cs +++ b/Dalamud/Plugin/Services/IPluginLog.cs @@ -20,15 +20,6 @@ public interface IPluginLog /// LogEventLevel MinimumLogLevel { get; set; } - /// - /// Gets an instance of the Serilog for advanced use cases. The provided logger will handle - /// tagging all log messages with the appropriate context variables and properties. - /// - /// - /// Not currently part of public API - will be added after some formatter work has been completed. - /// - internal ILogger Logger { get; } - /// /// Log a message to the Dalamud log for this plugin. This log level should be /// used primarily for unrecoverable errors or critical faults in a plugin. From 6a3e4906f3156a77948cf08b165de1e27415908a Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 23 Sep 2023 23:53:25 -0700 Subject: [PATCH 185/585] Fix bug, and simplify logic --- Dalamud/Game/Gui/GameGui.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 9796effc5..a1a17436e 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -484,15 +484,16 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui return retVal; } - private IntPtr ToggleUiHideDetour(IntPtr thisPtr, bool uiVisible) + private IntPtr ToggleUiHideDetour(IntPtr thisPtr, bool unknownByte) { - this.GameUiHidden = !RaptureAtkModule.Instance()->IsUiVisible; + var result = this.toggleUiHideHook.Original(thisPtr, unknownByte); + this.GameUiHidden = !RaptureAtkModule.Instance()->IsUiVisible; this.UiHideToggled?.InvokeSafely(this, this.GameUiHidden); Log.Debug("UiHide toggled: {0}", this.GameUiHidden); - return this.toggleUiHideHook.Original(thisPtr, uiVisible); + return result; } private char HandleImmDetour(IntPtr framework, char a2, byte a3) From 02daff2543334c36239eb89502b229d4d98bb9a3 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 25 Sep 2023 21:48:35 -0700 Subject: [PATCH 186/585] Add clipped draw for drawing rows with multiple items per row --- Dalamud/Interface/Utility/ImGuiClip.cs | 50 ++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/Dalamud/Interface/Utility/ImGuiClip.cs b/Dalamud/Interface/Utility/ImGuiClip.cs index e36970885..fafd026f0 100644 --- a/Dalamud/Interface/Utility/ImGuiClip.cs +++ b/Dalamud/Interface/Utility/ImGuiClip.cs @@ -59,6 +59,56 @@ public static class ImGuiClip clipper.Destroy(); } + /// + /// Draws the enumerable data with number of items per line. + /// + /// Enumerable containing data to draw. + /// The function to draw a single item. + /// How many items to draw per line. + /// How tall each line is. + /// The type of data to draw. + public static void ClippedDraw(IReadOnlyList data, Action draw, int itemsPerLine, float lineHeight) + { + ImGuiListClipperPtr clipper; + unsafe + { + clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + } + + var maxRows = (int)MathF.Ceiling((float)data.Count / itemsPerLine); + + clipper.Begin(maxRows, lineHeight); + while (clipper.Step()) + { + for (var actualRow = clipper.DisplayStart; actualRow < clipper.DisplayEnd; actualRow++) + { + if (actualRow >= maxRows) + return; + + if (actualRow < 0) + continue; + + var itemsForRow = data + .Skip(actualRow * itemsPerLine) + .Take(itemsPerLine); + + var currentIndex = 0; + foreach (var item in itemsForRow) + { + if (currentIndex++ != 0 && currentIndex < itemsPerLine + 1) + { + ImGui.SameLine(); + } + + draw(item); + } + } + } + + clipper.End(); + clipper.Destroy(); + } + // Draw a clipped random-access collection of consistent height lineHeight. // Uses ImGuiListClipper and thus handles start- and end-dummies itself, but acts on type and index. public static void ClippedDraw(IReadOnlyList data, Action draw, float lineHeight) From 3e3f0e632b9f3c576684a01b824be115de72d05d Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:50:26 -0700 Subject: [PATCH 187/585] Fix Notification Window not showing with multimonitor mode enabled. --- Dalamud/Interface/Internal/Notifications/NotificationManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs index 9d20d6d3e..67ad3ee8f 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs @@ -106,7 +106,7 @@ internal class NotificationManager : IServiceType ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowBgAlpha(opacity); - ImGui.SetNextWindowPos(new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One); + ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One); ImGui.Begin(windowName, NotifyToastFlags); ImGui.PushTextWrapPos(viewportSize.X / 3.0f); From 125034155b01fe34a59d065fc3a8670dc647ec3f Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 27 Sep 2023 22:10:21 +0200 Subject: [PATCH 188/585] feat: first pass at ReliableFileStorage service --- .../Internal/DalamudConfiguration.cs | 35 +- Dalamud/Dalamud.cs | 6 +- Dalamud/Dalamud.csproj | 1 + Dalamud/EntryPoint.cs | 6 +- Dalamud/ServiceManager.cs | 7 +- Dalamud/Storage/ReliableFileStorage.cs | 337 ++++++++++++++++++ 6 files changed, 379 insertions(+), 13 deletions(-) create mode 100644 Dalamud/Storage/ReliableFileStorage.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 2d0a08942..55bf82496 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -7,6 +7,7 @@ using System.Linq; using Dalamud.Game.Text; using Dalamud.Interface.Style; using Dalamud.Plugin.Internal.Profiles; +using Dalamud.Storage; using Dalamud.Utility; using Newtonsoft.Json; using Serilog; @@ -18,7 +19,7 @@ namespace Dalamud.Configuration.Internal; /// Class containing Dalamud settings. /// [Serializable] -internal sealed class DalamudConfiguration : IServiceType +internal sealed class DalamudConfiguration : IServiceType, IDisposable { private static readonly JsonSerializerSettings SerializerSettings = new() { @@ -422,23 +423,33 @@ internal sealed class DalamudConfiguration : IServiceType /// /// Load a configuration from the provided path. /// - /// The path to load the configuration file from. + /// Path to read from. + /// File storage. /// The deserialized configuration file. - public static DalamudConfiguration Load(string path) + public static DalamudConfiguration Load(string path, ReliableFileStorage fs) { DalamudConfiguration deserialized = null; + try { - deserialized = JsonConvert.DeserializeObject(File.ReadAllText(path), SerializerSettings); + fs.ReadAllText(path, text => + { + deserialized = + JsonConvert.DeserializeObject(text, SerializerSettings); + }); } - catch (Exception ex) + catch (FileNotFoundException) { - Log.Warning(ex, "Failed to load DalamudConfiguration at {0}", path); + // ignored + } + catch (Exception e) + { + Log.Error(e, "Could not load DalamudConfiguration at {Path}, creating new", path); } deserialized ??= new DalamudConfiguration(); deserialized.configPath = path; - + return deserialized; } @@ -457,6 +468,13 @@ internal sealed class DalamudConfiguration : IServiceType { this.Save(); } + + /// + public void Dispose() + { + // Make sure that we save, if a save is queued while we are shutting down + this.Update(); + } /// /// Save the file, if needed. Only needs to be done once a frame. @@ -476,7 +494,8 @@ internal sealed class DalamudConfiguration : IServiceType { ThreadSafety.AssertMainThread(); - Util.WriteAllTextSafe(this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); + Service.Get().WriteAllText( + this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); this.DalamudConfigurationSaved?.Invoke(this); } } diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index a9d822f55..2187f0da2 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -12,6 +12,7 @@ using Dalamud.Game; using Dalamud.Game.Gui.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; +using Dalamud.Storage; using Dalamud.Utility; using PInvoke; using Serilog; @@ -40,14 +41,15 @@ internal sealed class Dalamud : IServiceType /// Initializes a new instance of the class. /// /// DalamudStartInfo instance. + /// ReliableFileStorage instance. /// The Dalamud configuration. /// Event used to signal the main thread to continue. - public Dalamud(DalamudStartInfo info, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent) + public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent) { this.unloadSignal = new ManualResetEvent(false); this.unloadSignal.Reset(); - ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration); + ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, fs, configuration); if (!configuration.IsResumeGameAfterPluginLoad) { diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 5093fbfe9..7ae97e1a6 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -77,6 +77,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 7ad794e42..6b53ee3a6 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -10,6 +10,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Logging.Internal; using Dalamud.Logging.Retention; using Dalamud.Plugin.Internal; +using Dalamud.Storage; using Dalamud.Support; using Dalamud.Utility; using Newtonsoft.Json; @@ -137,7 +138,8 @@ public sealed class EntryPoint SerilogEventSink.Instance.LogLine += SerilogOnLogLine; // Load configuration first to get some early persistent state, like log level - var configuration = DalamudConfiguration.Load(info.ConfigurationPath!); + var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!); + var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs); // Set the appropriate logging level from the configuration if (!configuration.LogSynchronously) @@ -169,7 +171,7 @@ public sealed class EntryPoint if (!Util.IsWine()) InitSymbolHandler(info); - var dalamud = new Dalamud(info, configuration, mainThreadContinueEvent); + var dalamud = new Dalamud(info, fs, configuration, mainThreadContinueEvent); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash} [{CsVersion}]", Util.GetGitHash(), Util.GetGitHashClientStructs(), FFXIVClientStructs.Interop.Resolver.Version); dalamud.WaitForUnload(); diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index ecb58d48b..38dc7534f 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -11,6 +11,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Storage; using Dalamud.Utility.Timing; using JetBrains.Annotations; @@ -83,8 +84,9 @@ internal static class ServiceManager /// /// Instance of . /// Instance of . + /// Instance of /// Instance of . - public static void InitializeProvidedServicesAndClientStructs(Dalamud dalamud, DalamudStartInfo startInfo, DalamudConfiguration configuration) + public static void InitializeProvidedServicesAndClientStructs(Dalamud dalamud, DalamudStartInfo startInfo, ReliableFileStorage fs, DalamudConfiguration configuration) { // Initialize the process information. var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs")); @@ -98,6 +100,9 @@ internal static class ServiceManager Service.Provide(startInfo); LoadedServices.Add(typeof(DalamudStartInfo)); + + Service.Provide(fs); + LoadedServices.Add(typeof(ReliableFileStorage)); Service.Provide(configuration); LoadedServices.Add(typeof(DalamudConfiguration)); diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs new file mode 100644 index 000000000..14ab59143 --- /dev/null +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -0,0 +1,337 @@ +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +using Dalamud.Logging.Internal; +using PInvoke; +using SQLite; + +namespace Dalamud.Storage; + +/* + * TODO: A file that is read frequently, but written very rarely, might not have offline changes by users persisted + * into the backup database, since it is only written to the backup database when it is written to the filesystem. + */ + +/// +/// A service that provides a reliable file storage. +/// Implements a VFS that writes files to the disk, and additionally keeps files in a SQLite database +/// for journaling/backup purposes. +/// Consumers can choose to receive a backup if they think that the file is corrupt. +/// +/// +/// This is not an early-loaded service, as it is needed before they are initialized. +/// +public class ReliableFileStorage : IServiceType, IDisposable +{ + private static readonly ModuleLog Log = new("VFS"); + + private SQLiteConnection db; + + /// + /// Initializes a new instance of the class. + /// + /// Path to the VFS. + [ServiceManager.ServiceConstructor] + public ReliableFileStorage(string vfsDbPath) + { + var databasePath = Path.Combine(vfsDbPath, "dalamudVfs.db"); + + Log.Verbose("Initializing VFS database at {Path}", databasePath); + this.db = new SQLiteConnection(databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.FullMutex); + this.db.CreateTable(); + } + + /// + /// Check if a file exists. + /// This will return true if the file does not exist on the filesystem, but in the transparent backup. + /// You must then use this instance to read the file to ensure consistency. + /// + /// The path to check. + /// The container to check in. + /// True if the file exists. + public bool Exists(string path, Guid containerId = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + if (File.Exists(path)) + return true; + + // If the file doesn't actually exist on the FS, but it does in the DB, we can say YES and read operations will read from the DB instead + var normalizedPath = NormalizePath(path); + var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); + return file != null; + } + + /// + /// Write all text to a file. + /// + /// Path to write to. + /// The contents of the file. + /// Container to write to. + public void WriteAllText(string path, string? contents, Guid containerId = default) + => this.WriteAllText(path, contents, Encoding.UTF8, containerId); + + /// + /// Write all text to a file. + /// + /// Path to write to. + /// The contents of the file. + /// The encoding to write with. + /// Container to write to. + public void WriteAllText(string path, string? contents, Encoding encoding, Guid containerId = default) + { + var bytes = encoding.GetBytes(contents ?? string.Empty); + this.WriteAllBytes(path, bytes, containerId); + } + + /// + /// Write all bytes to a file. + /// + /// Path to write to. + /// The contents of the file. + /// Container to write to. + public void WriteAllBytes(string path, byte[] bytes, Guid containerId = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + var normalizedPath = NormalizePath(path); + var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); + if (file == null) + { + file = new DbFile + { + ContainerId = containerId, + Path = normalizedPath, + Data = bytes, + }; + this.db.Insert(file); + } + else + { + file.Data = bytes; + this.db.Update(file); + } + + WriteFileReliably(path, bytes); + } + + /// + /// Read all text from a file. + /// If the file does not exist on the filesystem, a read is attempted from the backup. The backup is not + /// automatically written back to disk, however. + /// + /// The path to read from. + /// Whether or not the backup of the file should take priority. + /// The container to read from. + /// All text stored in this file. + /// Thrown if the file does not exist on the filesystem or in the backup. + public string ReadAllText(string path, bool forceBackup = false, Guid containerId = default) + => this.ReadAllText(path, Encoding.UTF8, forceBackup, containerId); + + /// + /// Read all text from a file. + /// If the file does not exist on the filesystem, a read is attempted from the backup. The backup is not + /// automatically written back to disk, however. + /// + /// The path to read from. + /// The encoding to read with. + /// Whether or not the backup of the file should take priority. + /// The container to read from. + /// All text stored in this file. + /// Thrown if the file does not exist on the filesystem or in the backup. + public string ReadAllText(string path, Encoding encoding, bool forceBackup = false, Guid containerId = default) + { + var bytes = this.ReadAllBytes(path, forceBackup, containerId); + return encoding.GetString(bytes); + } + + /// + /// Read all text from a file, and automatically try again with the backup if the file does not exist or + /// the function throws an exception. If the backup read also throws an exception, + /// or the file does not exist in the backup, a is thrown. + /// + /// The path to read from. + /// Lambda that reads the file. Throw here to automatically attempt a read from the backup. + /// The container to read from. + /// Thrown if the file does not exist on the filesystem or in the backup. + /// Thrown here if the file and the backup fail their read. + public void ReadAllText(string path, Action reader, Guid containerId = default) + => this.ReadAllText(path, Encoding.UTF8, reader, containerId); + + /// + /// Read all text from a file, and automatically try again with the backup if the file does not exist or + /// the function throws an exception. If the backup read also throws an exception, + /// or the file does not exist in the backup, a is thrown. + /// + /// The path to read from. + /// The encoding to read with. + /// Lambda that reads the file. Throw here to automatically attempt a read from the backup. + /// The container to read from. + /// Thrown if the file does not exist on the filesystem or in the backup. + /// Thrown here if the file and the backup fail their read. + public void ReadAllText(string path, Encoding encoding, Action reader, Guid containerId = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + // TODO: We are technically reading one time too many here, if the file does not exist on the FS, ReadAllText + // fails over to the backup, and then the backup fails to read in the lambda. We should do something about that, + // but it's not a big deal. Would be nice if ReadAllText could indicate if it did fail over. + + // 1.) Try without using the backup + try + { + var text = this.ReadAllText(path, encoding, false, containerId); + reader(text); + return; + } + catch (FileNotFoundException) + { + // We can't do anything about this. + throw; + } + catch (Exception ex) + { + Log.Verbose(ex, "First chance read from {Path} failed, trying backup", path); + } + + // 2.) Try using the backup + try + { + var text = this.ReadAllText(path, encoding, true, containerId); + reader(text); + } + catch (Exception ex) + { + Log.Error(ex, "Second chance read from {Path} failed, giving up", path); + throw new FileReadException(ex); + } + } + + /// + /// Read all bytes from a file. + /// If the file does not exist on the filesystem, a read is attempted from the backup. The backup is not + /// automatically written back to disk, however. + /// + /// The path to read from. + /// Whether or not the backup of the file should take priority. + /// The container to read from. + /// All bytes stored in this file. + /// Thrown if the file does not exist on the filesystem or in the backup. + public byte[] ReadAllBytes(string path, bool forceBackup = false, Guid containerId = default) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + if (forceBackup) + { + var normalizedPath = NormalizePath(path); + var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); + if (file == null) + throw new FileNotFoundException(); + + return file.Data; + } + + // If the file doesn't exist, immediately check the backup db + if (!File.Exists(path)) + return this.ReadAllBytes(path, true, containerId); + + try + { + return File.ReadAllBytes(path); + } + catch (Exception e) + { + Log.Error(e, "Failed to read file from disk, falling back to database"); + return this.ReadAllBytes(path, true, containerId); + } + } + + /// + public void Dispose() + { + this.db.Dispose(); + } + + private static void WriteFileReliably(string path, byte[] bytes) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + // Open the temp file + var tempPath = path + ".tmp"; + + using var tempFile = Kernel32 + .CreateFile(tempPath.AsSpan(), + new Kernel32.ACCESS_MASK(Kernel32.FileAccess.FILE_GENERIC_READ | Kernel32.FileAccess.FILE_GENERIC_WRITE), + Kernel32.FileShare.None, + null, + Kernel32.CreationDisposition.CREATE_ALWAYS, + Kernel32.CreateFileFlags.FILE_ATTRIBUTE_NORMAL, + Kernel32.SafeObjectHandle.Null); + + if (tempFile.IsInvalid) + throw new Win32Exception(); + + // Write the data + var bytesWritten = Kernel32.WriteFile(tempFile, new ArraySegment(bytes)); + if (bytesWritten != bytes.Length) + throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); + + if (!Kernel32.FlushFileBuffers(tempFile)) + throw new Win32Exception(); + + tempFile.Close(); + + if (!MoveFileEx(tempPath, path, MoveFileFlags.MovefileReplaceExisting | MoveFileFlags.MovefileWriteThrough)) + throw new Win32Exception(); + } + + /// + /// Replace possible non-portable parts of a path with portable versions. + /// + /// The path to normalize. + /// The normalized path. + private static string NormalizePath(string path) + { + // Replace users folder + var usersFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + path = path.Replace(usersFolder, "%USERPROFILE%"); + + return path; + } + + [Flags] +#pragma warning disable SA1201 + private enum MoveFileFlags +#pragma warning restore SA1201 + { + MovefileReplaceExisting = 0x00000001, + MovefileWriteThrough = 0x00000008, + } + + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] + private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, + MoveFileFlags dwFlags); + + private class DbFile + { + [PrimaryKey] + [AutoIncrement] + public int Id { get; set; } + + public Guid ContainerId { get; set; } + + public string Path { get; set; } = null!; + + public byte[] Data { get; set; } = null!; + } +} + +public class FileReadException : Exception +{ + public FileReadException(Exception inner) + : base("Failed to read file", inner) + { + } +} From 1d8b579b040aebdf296c911c8e1d2d1f74f2444a Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 27 Sep 2023 22:33:58 +0200 Subject: [PATCH 189/585] feat: also use reliable storage for plugin configs --- .../Internal/DalamudConfiguration.cs | 4 + Dalamud/Configuration/PluginConfigurations.cs | 26 +++++-- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- Dalamud/Storage/ReliableFileStorage.cs | 51 +------------ Dalamud/Utility/Util.cs | 73 +++++++++++++++++-- 5 files changed, 94 insertions(+), 62 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 55bf82496..63494931c 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -436,6 +436,10 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable { deserialized = JsonConvert.DeserializeObject(text, SerializerSettings); + + // If this reads as null, the file was empty, that's no good + if (deserialized == null) + throw new Exception("Read config was null."); }); } catch (FileNotFoundException) diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index 957a7c99e..d1f926b0d 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -1,6 +1,6 @@ using System.IO; -using Dalamud.Utility; +using Dalamud.Storage; using Newtonsoft.Json; namespace Dalamud.Configuration; @@ -33,22 +33,36 @@ public sealed class PluginConfigurations /// Plugin name. public void Save(IPluginConfiguration config, string pluginName) { - Util.WriteAllTextSafe(this.GetConfigFile(pluginName).FullName, SerializeConfig(config)); + Service.Get() + .WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config)); } /// /// Load plugin configuration. /// /// Plugin name. + /// WorkingPluginID of the plugin. /// Plugin configuration. - public IPluginConfiguration? Load(string pluginName) + public IPluginConfiguration? Load(string pluginName, Guid workingPluginId) { var path = this.GetConfigFile(pluginName); - if (!path.Exists) - return null; + IPluginConfiguration? config = null; + try + { + Service.Get().ReadAllText(path.FullName, text => + { + config = DeserializeConfig(text); + if (config == null) + throw new Exception("Read config was null."); + }, workingPluginId); + } + catch (FileNotFoundException) + { + // ignored + } - return DeserializeConfig(File.ReadAllText(path.FullName)); + return config; } /// diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 6fdf875e5..0f5b4297c 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -370,7 +370,7 @@ public sealed class DalamudPluginInterface : IDisposable } // this shouldn't be a thing, I think, but just in case - return this.configs.Load(this.plugin.InternalName); + return this.configs.Load(this.plugin.InternalName, this.plugin.Manifest.WorkingPluginId); } /// diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 14ab59143..32fba9aef 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices; using System.Text; using Dalamud.Logging.Internal; +using Dalamud.Utility; using PInvoke; using SQLite; @@ -32,7 +33,6 @@ public class ReliableFileStorage : IServiceType, IDisposable /// Initializes a new instance of the class. /// /// Path to the VFS. - [ServiceManager.ServiceConstructor] public ReliableFileStorage(string vfsDbPath) { var databasePath = Path.Combine(vfsDbPath, "dalamudVfs.db"); @@ -113,7 +113,7 @@ public class ReliableFileStorage : IServiceType, IDisposable this.db.Update(file); } - WriteFileReliably(path, bytes); + Util.WriteAllBytesSafe(path, bytes); } /// @@ -252,39 +252,6 @@ public class ReliableFileStorage : IServiceType, IDisposable { this.db.Dispose(); } - - private static void WriteFileReliably(string path, byte[] bytes) - { - ArgumentException.ThrowIfNullOrEmpty(path); - - // Open the temp file - var tempPath = path + ".tmp"; - - using var tempFile = Kernel32 - .CreateFile(tempPath.AsSpan(), - new Kernel32.ACCESS_MASK(Kernel32.FileAccess.FILE_GENERIC_READ | Kernel32.FileAccess.FILE_GENERIC_WRITE), - Kernel32.FileShare.None, - null, - Kernel32.CreationDisposition.CREATE_ALWAYS, - Kernel32.CreateFileFlags.FILE_ATTRIBUTE_NORMAL, - Kernel32.SafeObjectHandle.Null); - - if (tempFile.IsInvalid) - throw new Win32Exception(); - - // Write the data - var bytesWritten = Kernel32.WriteFile(tempFile, new ArraySegment(bytes)); - if (bytesWritten != bytes.Length) - throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); - - if (!Kernel32.FlushFileBuffers(tempFile)) - throw new Win32Exception(); - - tempFile.Close(); - - if (!MoveFileEx(tempPath, path, MoveFileFlags.MovefileReplaceExisting | MoveFileFlags.MovefileWriteThrough)) - throw new Win32Exception(); - } /// /// Replace possible non-portable parts of a path with portable versions. @@ -299,20 +266,6 @@ public class ReliableFileStorage : IServiceType, IDisposable return path; } - - [Flags] -#pragma warning disable SA1201 - private enum MoveFileFlags -#pragma warning restore SA1201 - { - MovefileReplaceExisting = 0x00000001, - MovefileWriteThrough = 0x00000008, - } - - [return: MarshalAs(UnmanagedType.Bool)] - [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] - private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, - MoveFileFlags dwFlags); private class DbFile { diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 8ca87b691..36918abd2 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -20,6 +20,7 @@ using Dalamud.Logging.Internal; using Dalamud.Memory; using ImGuiNET; using Lumina.Excel.GeneratedSheets; +using PInvoke; using Serilog; namespace Dalamud.Utility; @@ -609,7 +610,7 @@ public static class Util } } } - + /// /// Overwrite text in a file by first writing it to a temporary file, and then /// moving that file to the path specified. @@ -618,12 +619,58 @@ public static class Util /// The text to write. public static void WriteAllTextSafe(string path, string text) { - var tmpPath = path + ".tmp"; - if (File.Exists(tmpPath)) - File.Delete(tmpPath); + WriteAllTextSafe(path, text, Encoding.UTF8); + } + + /// + /// Overwrite text in a file by first writing it to a temporary file, and then + /// moving that file to the path specified. + /// + /// The path of the file to write to. + /// The text to write. + /// Encoding to use. + public static void WriteAllTextSafe(string path, string text, Encoding encoding) + { + WriteAllBytesSafe(path, encoding.GetBytes(text)); + } + + /// + /// Overwrite data in a file by first writing it to a temporary file, and then + /// moving that file to the path specified. + /// + /// The path of the file to write to. + /// The data to write. + public static void WriteAllBytesSafe(string path, byte[] bytes) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + // Open the temp file + var tempPath = path + ".tmp"; - File.WriteAllText(tmpPath, text); - File.Move(tmpPath, path, true); + using var tempFile = Kernel32 + .CreateFile(tempPath.AsSpan(), + new Kernel32.ACCESS_MASK(Kernel32.FileAccess.FILE_GENERIC_READ | Kernel32.FileAccess.FILE_GENERIC_WRITE), + Kernel32.FileShare.None, + null, + Kernel32.CreationDisposition.CREATE_ALWAYS, + Kernel32.CreateFileFlags.FILE_ATTRIBUTE_NORMAL, + Kernel32.SafeObjectHandle.Null); + + if (tempFile.IsInvalid) + throw new Win32Exception(); + + // Write the data + var bytesWritten = Kernel32.WriteFile(tempFile, new ArraySegment(bytes)); + if (bytesWritten != bytes.Length) + throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); + + if (!Kernel32.FlushFileBuffers(tempFile)) + throw new Win32Exception(); + + tempFile.Close(); + + if (!MoveFileEx(tempPath, path, MoveFileFlags.MovefileReplaceExisting | MoveFileFlags.MovefileWriteThrough)) + throw new Win32Exception(); } /// @@ -762,4 +809,18 @@ public static class Util } } } + + [Flags] +#pragma warning disable SA1201 + private enum MoveFileFlags +#pragma warning restore SA1201 + { + MovefileReplaceExisting = 0x00000001, + MovefileWriteThrough = 0x00000008, + } + + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] + private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, + MoveFileFlags dwFlags); } From 63764cb669277fa79b097885657b85f3ff548035 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 27 Sep 2023 22:41:25 +0200 Subject: [PATCH 190/585] chore: move exception to separate file --- Dalamud/ServiceManager.cs | 2 +- Dalamud/Storage/FileReadException.cs | 16 ++++++++++++++++ Dalamud/Storage/ReliableFileStorage.cs | 8 -------- 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 Dalamud/Storage/FileReadException.cs diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 38dc7534f..f2ff864c3 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -84,7 +84,7 @@ internal static class ServiceManager /// /// Instance of . /// Instance of . - /// Instance of + /// Instance of . /// Instance of . public static void InitializeProvidedServicesAndClientStructs(Dalamud dalamud, DalamudStartInfo startInfo, ReliableFileStorage fs, DalamudConfiguration configuration) { diff --git a/Dalamud/Storage/FileReadException.cs b/Dalamud/Storage/FileReadException.cs new file mode 100644 index 000000000..09f7ff4fb --- /dev/null +++ b/Dalamud/Storage/FileReadException.cs @@ -0,0 +1,16 @@ +namespace Dalamud.Storage; + +/// +/// Thrown if all read operations fail. +/// +public class FileReadException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// Inner error that caused this exception. + internal FileReadException(Exception inner) + : base("Failed to read file", inner) + { + } +} diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 32fba9aef..43a32bf29 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -280,11 +280,3 @@ public class ReliableFileStorage : IServiceType, IDisposable public byte[] Data { get; set; } = null!; } } - -public class FileReadException : Exception -{ - public FileReadException(Exception inner) - : base("Failed to read file", inner) - { - } -} From f027b684eda6bc498a149a1ed39196834524da6f Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 27 Sep 2023 23:27:51 +0200 Subject: [PATCH 191/585] fix: specify WorkingPluginId when saving --- Dalamud/Configuration/PluginConfigurations.cs | 5 +++-- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dalamud/Configuration/PluginConfigurations.cs b/Dalamud/Configuration/PluginConfigurations.cs index d1f926b0d..de5e071c1 100644 --- a/Dalamud/Configuration/PluginConfigurations.cs +++ b/Dalamud/Configuration/PluginConfigurations.cs @@ -31,10 +31,11 @@ public sealed class PluginConfigurations /// /// Plugin configuration. /// Plugin name. - public void Save(IPluginConfiguration config, string pluginName) + /// WorkingPluginId of the plugin. + public void Save(IPluginConfiguration config, string pluginName, Guid workingPluginId) { Service.Get() - .WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config)); + .WriteAllText(this.GetConfigFile(pluginName).FullName, SerializeConfig(config), workingPluginId); } /// diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 0f5b4297c..004b7196c 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -343,7 +343,7 @@ public sealed class DalamudPluginInterface : IDisposable if (currentConfig == null) return; - this.configs.Save(currentConfig, this.plugin.InternalName); + this.configs.Save(currentConfig, this.plugin.InternalName, this.plugin.Manifest.WorkingPluginId); } /// From c1fd08cc932097ee9cf298e2cba34399fe197862 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 28 Sep 2023 00:43:25 +0200 Subject: [PATCH 192/585] feat: wrap writes in a transaction --- Dalamud/Storage/ReliableFileStorage.cs | 35 ++++++++++++++------------ 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 43a32bf29..fb75f3abd 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -95,25 +95,28 @@ public class ReliableFileStorage : IServiceType, IDisposable { ArgumentException.ThrowIfNullOrEmpty(path); - var normalizedPath = NormalizePath(path); - var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); - if (file == null) + this.db.RunInTransaction(() => { - file = new DbFile + var normalizedPath = NormalizePath(path); + var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); + if (file == null) { - ContainerId = containerId, - Path = normalizedPath, - Data = bytes, - }; - this.db.Insert(file); - } - else - { - file.Data = bytes; - this.db.Update(file); - } + file = new DbFile + { + ContainerId = containerId, + Path = normalizedPath, + Data = bytes, + }; + this.db.Insert(file); + } + else + { + file.Data = bytes; + this.db.Update(file); + } - Util.WriteAllBytesSafe(path, bytes); + Util.WriteAllBytesSafe(path, bytes); + }); } /// From 416bb42f5b91c7785f54ab13fa170d1422e47f0b Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 29 Sep 2023 18:37:45 +0200 Subject: [PATCH 193/585] feat: handle db load failures gracefully --- Dalamud/Storage/ReliableFileStorage.cs | 47 +++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index fb75f3abd..fec461cc3 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -27,7 +27,7 @@ public class ReliableFileStorage : IServiceType, IDisposable { private static readonly ModuleLog Log = new("VFS"); - private SQLiteConnection db; + private SQLiteConnection? db; /// /// Initializes a new instance of the class. @@ -38,8 +38,27 @@ public class ReliableFileStorage : IServiceType, IDisposable var databasePath = Path.Combine(vfsDbPath, "dalamudVfs.db"); Log.Verbose("Initializing VFS database at {Path}", databasePath); - this.db = new SQLiteConnection(databasePath, SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.FullMutex); - this.db.CreateTable(); + + try + { + this.SetupDb(vfsDbPath); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load VFS database, starting fresh"); + + try + { + if (File.Exists(vfsDbPath)) + File.Delete(vfsDbPath); + } + catch (Exception) + { + // ignored + } + + this.SetupDb(vfsDbPath); + } } /// @@ -56,6 +75,9 @@ public class ReliableFileStorage : IServiceType, IDisposable if (File.Exists(path)) return true; + + if (this.db == null) + return false; // If the file doesn't actually exist on the FS, but it does in the DB, we can say YES and read operations will read from the DB instead var normalizedPath = NormalizePath(path); @@ -94,6 +116,12 @@ public class ReliableFileStorage : IServiceType, IDisposable public void WriteAllBytes(string path, byte[] bytes, Guid containerId = default) { ArgumentException.ThrowIfNullOrEmpty(path); + + if (this.db == null) + { + Util.WriteAllBytesSafe(path, bytes); + return; + } this.db.RunInTransaction(() => { @@ -227,6 +255,10 @@ public class ReliableFileStorage : IServiceType, IDisposable if (forceBackup) { + // If the db failed to load, act as if the file does not exist + if (this.db == null) + throw new FileNotFoundException("Backup database was not available"); + var normalizedPath = NormalizePath(path); var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); if (file == null) @@ -253,7 +285,7 @@ public class ReliableFileStorage : IServiceType, IDisposable /// public void Dispose() { - this.db.Dispose(); + this.db?.Dispose(); } /// @@ -269,6 +301,13 @@ public class ReliableFileStorage : IServiceType, IDisposable return path; } + + private void SetupDb(string path) + { + this.db = new SQLiteConnection(path, + SQLiteOpenFlags.ReadWrite | SQLiteOpenFlags.Create | SQLiteOpenFlags.FullMutex); + this.db.CreateTable(); + } private class DbFile { From 33abb5ec421c2d0bbe87896ae14af527cf5ba5fb Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 29 Sep 2023 18:39:36 +0200 Subject: [PATCH 194/585] chore: write manifests using new safer method --- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index f91d4cd56..49608ac9b 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -836,7 +836,7 @@ internal partial class PluginManager : IDisposable, IServiceType var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); // We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use. - File.WriteAllText(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented)); + Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented)); // Reload as a local manifest, add some attributes, and save again. var manifest = LocalPluginManifest.Load(manifestFile); From e9e234b340353ea6a636a61344938f4202bf6c48 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 29 Sep 2023 18:53:56 +0200 Subject: [PATCH 195/585] fix: actually use the correct path when setting up vfs, fix warnings use paramref instead of see fix warnings --- Dalamud/Interface/Utility/ImGuiClip.cs | 2 +- Dalamud/Logging/ScopedPluginLogService.cs | 4 +++- Dalamud/Storage/ReliableFileStorage.cs | 12 ++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Utility/ImGuiClip.cs b/Dalamud/Interface/Utility/ImGuiClip.cs index fafd026f0..c9321fe4c 100644 --- a/Dalamud/Interface/Utility/ImGuiClip.cs +++ b/Dalamud/Interface/Utility/ImGuiClip.cs @@ -60,7 +60,7 @@ public static class ImGuiClip } /// - /// Draws the enumerable data with number of items per line. + /// Draws the enumerable data with number of items per line. /// /// Enumerable containing data to draw. /// The function to draw a single item. diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs index d6bb1f82d..ca96fa64a 100644 --- a/Dalamud/Logging/ScopedPluginLogService.cs +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -48,7 +48,9 @@ public class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable set => this.levelSwitch.MinimumLevel = value; } - /// + /// + /// Gets a logger that may be exposed to plugins some day. + /// public ILogger Logger { get; } /// diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index fec461cc3..7fdd04880 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -41,7 +41,7 @@ public class ReliableFileStorage : IServiceType, IDisposable try { - this.SetupDb(vfsDbPath); + this.SetupDb(databasePath); } catch (Exception ex) { @@ -49,15 +49,15 @@ public class ReliableFileStorage : IServiceType, IDisposable try { - if (File.Exists(vfsDbPath)) - File.Delete(vfsDbPath); + if (File.Exists(databasePath)) + File.Delete(databasePath); + + this.SetupDb(databasePath); } catch (Exception) { - // ignored + // ignored, we can run without one } - - this.SetupDb(vfsDbPath); } } From 4b9de312403990b5cdcfc4a9b5f32d4f3d355647 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 29 Sep 2023 20:47:54 +0200 Subject: [PATCH 196/585] fix: scoped services must register their dependencies with PluginManager to ensure the backing services are kept alive long enough --- Dalamud/IoC/Internal/ServiceContainer.cs | 3 +- Dalamud/ServiceManager.cs | 35 ++++--------- Dalamud/Service{T}.cs | 67 +++++++++++++++++++++--- 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index a82440029..3dd76473f 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -53,7 +53,6 @@ internal class ServiceContainer : IServiceProvider, IServiceType } this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T)); - this.RegisterInterfaces(typeof(T)); } /// @@ -69,7 +68,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType foreach (var resolvableType in resolveViaTypes) { Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", type.FullName ?? "???"); - + Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed"); Debug.Assert(type.IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type"); diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index f2ff864c3..57e4ace10 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -145,12 +145,12 @@ internal static class ServiceManager if (serviceKind is ServiceKind.None) continue; - // Scoped service do not go through Service, so we must let ServiceContainer know what their interfaces map to - if (serviceKind is ServiceKind.ScopedService) - { - serviceContainer.RegisterInterfaces(serviceType); + // Let IoC know about the interfaces this service implements + serviceContainer.RegisterInterfaces(serviceType); + + // Scoped service do not go through Service and are never early loaded + if (serviceKind.HasFlag(ServiceKind.ScopedService)) continue; - } Debug.Assert( !serviceKind.HasFlag(ServiceKind.ManualService) && !serviceKind.HasFlag(ServiceKind.ScopedService), @@ -176,15 +176,10 @@ internal static class ServiceManager earlyLoadingServices.Add(serviceType); } - dependencyServicesMap[serviceType] = - (List)typeof(Service<>) - .MakeGenericType(serviceType) - .InvokeMember( - "GetDependencyServices", - BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, - null, - null, - null); + var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); + dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT) + .Select(x => typeof(Service<>).MakeGenericType(x)) + .ToList(); } _ = Task.Run(async () => @@ -327,16 +322,8 @@ internal static class ServiceManager Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!); - dependencyServicesMap[serviceType] = - ((List)typeof(Service<>) - .MakeGenericType(serviceType) - .InvokeMember( - "GetDependencyServices", - BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, - null, - null, - null))! - .Select(x => x.GetGenericArguments()[0]).ToList(); + var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); + dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT); allToUnload.Add(serviceType); } diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index aa10ead6e..539941c27 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -133,8 +134,8 @@ internal static class Service where T : IServiceType res.AddRange(typeof(T) .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Select(x => x.FieldType) - .Where(x => x.GetCustomAttribute(true) != null)); + .Where(x => x.GetCustomAttribute(true) != null) + .Select(x => x.FieldType)); res.AddRange(typeof(T) .GetCustomAttributes() @@ -148,13 +149,32 @@ internal static class Service where T : IServiceType { if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; - - var attr = serviceType.GetCustomAttribute(true); - if (attr == null) + + if (serviceType == typeof(PluginManager)) continue; - + // Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away. + // Nonetheless, their direct dependencies must be considered. if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService) + { + var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); + var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT); + ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count); + + foreach (var scopedDep in dependencies) + { + if (scopedDep == typeof(PluginManager)) + throw new Exception("Scoped plugin services cannot depend on PluginManager."); + + ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!); + res.Add(scopedDep); + } + + continue; + } + + var pluginInterfaceAttribute = serviceType.GetCustomAttribute(true); + if (pluginInterfaceAttribute == null) continue; ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!); @@ -164,7 +184,6 @@ internal static class Service where T : IServiceType return res .Distinct() - .Select(x => typeof(Service<>).MakeGenericType(x)) .ToList(); } @@ -295,3 +314,37 @@ internal static class Service where T : IServiceType } } } + +/// +/// Helper functions for services. +/// +internal static class ServiceHelpers +{ + /// + /// Get a list of dependencies for a service. Only accepts Service<T> types. + /// These are returned as Service<T> types. + /// + /// The dependencies for this service. + /// A list of dependencies. + public static List GetDependencies(Type serviceType) + { + return (List)serviceType.InvokeMember( + "GetDependencyServices", + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, + null, + null) ?? new List(); + } + + /// + /// Get the Service<T> type for a given service type. + /// This will throw if the service type is not a valid service. + /// + /// The type to obtain a Service<T> for. + /// The Service<T>. + public static Type GetAsService(Type type) + { + return typeof(Service<>) + .MakeGenericType(type); + } +} From 4a1b220d4a031a13304fbc798a19f342251f4af6 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 30 Sep 2023 01:09:25 +0200 Subject: [PATCH 197/585] fix: register interfaces for provided services --- Dalamud.CorePlugin/PluginImpl.cs | 5 ++- Dalamud.Injector/Hacks.cs | 20 +++++++++++ .../Internal/DalamudConfiguration.cs | 1 + Dalamud/Dalamud.cs | 1 + Dalamud/DalamudStartInfo.cs | 1 + Dalamud/Game/TargetSigScanner.cs | 1 + Dalamud/IoC/Internal/ServiceContainer.cs | 1 + Dalamud/ServiceManager.cs | 35 +++++++++---------- Dalamud/Storage/ReliableFileStorage.cs | 1 + 9 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 Dalamud.Injector/Hacks.cs diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 5ed672f2d..03f11e989 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -2,6 +2,7 @@ using System; using System.IO; using Dalamud.Configuration.Internal; +using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Plugin; @@ -55,7 +56,7 @@ namespace Dalamud.CorePlugin /// /// Dalamud plugin interface. /// Logging service. - public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log) + public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log, ISigScanner scanner) { try { @@ -65,6 +66,8 @@ namespace Dalamud.CorePlugin this.windowSystem.AddWindow(new PluginWindow()); + this.pluginLog.Information(scanner.ToString()); + this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; diff --git a/Dalamud.Injector/Hacks.cs b/Dalamud.Injector/Hacks.cs new file mode 100644 index 000000000..7bc4468af --- /dev/null +++ b/Dalamud.Injector/Hacks.cs @@ -0,0 +1,20 @@ +using System; + +// ReSharper disable once CheckNamespace +namespace Dalamud; + +// TODO: Get rid of this! Move StartInfo to another assembly, make this good + +/// +/// Class to initialize Service<T>s. +/// +internal static class ServiceManager +{ + /// + /// Indicates that the class is a service. + /// + [AttributeUsage(AttributeTargets.Class)] + public class Service : Attribute + { + } +} diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 63494931c..1e9bc1523 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -19,6 +19,7 @@ namespace Dalamud.Configuration.Internal; /// Class containing Dalamud settings. /// [Serializable] +[ServiceManager.Service] internal sealed class DalamudConfiguration : IServiceType, IDisposable { private static readonly JsonSerializerSettings SerializerSettings = new() diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 2187f0da2..a9a3a511a 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -29,6 +29,7 @@ namespace Dalamud; /// /// The main Dalamud class containing all subsystems. /// +[ServiceManager.Service] internal sealed class Dalamud : IServiceType { #region Internals diff --git a/Dalamud/DalamudStartInfo.cs b/Dalamud/DalamudStartInfo.cs index 63a61c97e..f22388e9b 100644 --- a/Dalamud/DalamudStartInfo.cs +++ b/Dalamud/DalamudStartInfo.cs @@ -10,6 +10,7 @@ namespace Dalamud; /// Struct containing information needed to initialize Dalamud. /// [Serializable] +[ServiceManager.Service] public record DalamudStartInfo : IServiceType { /// diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index 0360f95cc..9242c5e83 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -11,6 +11,7 @@ namespace Dalamud.Game; /// [PluginInterface] [InterfaceVersion("1.0")] +[ServiceManager.Service] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index 3dd76473f..ce7ce25a1 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -16,6 +16,7 @@ namespace Dalamud.IoC.Internal; /// This is only used to resolve dependencies for plugins. /// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// +[ServiceManager.Service] internal class ServiceContainer : IServiceProvider, IServiceType { private static readonly ModuleLog Log = new("SERVICECONTAINER"); diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 57e4ace10..bb680127c 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -92,28 +92,24 @@ internal static class ServiceManager var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs")); if (!cacheDir.Exists) cacheDir.Create(); - + lock (LoadedServices) { - Service.Provide(dalamud); - LoadedServices.Add(typeof(Dalamud)); - - Service.Provide(startInfo); - LoadedServices.Add(typeof(DalamudStartInfo)); + void ProvideService(T service) where T : IServiceType + { + Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ManualService), "Provided service must have Service attribute"); + Service.Provide(service); + LoadedServices.Add(typeof(T)); + } - Service.Provide(fs); - LoadedServices.Add(typeof(ReliableFileStorage)); - - Service.Provide(configuration); - LoadedServices.Add(typeof(DalamudConfiguration)); - - Service.Provide(new ServiceContainer()); - LoadedServices.Add(typeof(ServiceContainer)); - - Service.Provide( + ProvideService(dalamud); + ProvideService(startInfo); + ProvideService(fs); + ProvideService(configuration); + ProvideService(new ServiceContainer()); + ProvideService( new TargetSigScanner( - true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json")))); - LoadedServices.Add(typeof(TargetSigScanner)); + true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json")))); } using (Timings.Start("CS Resolver Init")) @@ -149,7 +145,8 @@ internal static class ServiceManager serviceContainer.RegisterInterfaces(serviceType); // Scoped service do not go through Service and are never early loaded - if (serviceKind.HasFlag(ServiceKind.ScopedService)) + // Manual services are provided + if (serviceKind.HasFlag(ServiceKind.ScopedService) || serviceKind.HasFlag(ServiceKind.ManualService)) continue; Debug.Assert( diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 7fdd04880..6bc6cabcc 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -23,6 +23,7 @@ namespace Dalamud.Storage; /// /// This is not an early-loaded service, as it is needed before they are initialized. /// +[ServiceManager.Service] public class ReliableFileStorage : IServiceType, IDisposable { private static readonly ModuleLog Log = new("VFS"); From 6f99cfe48c288002cbdaebab8f29e585230faa0a Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 30 Sep 2023 01:17:27 +0200 Subject: [PATCH 198/585] fix: ScopedPluginLogService should be internal --- Dalamud/Logging/ScopedPluginLogService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs index ca96fa64a..924b4885d 100644 --- a/Dalamud/Logging/ScopedPluginLogService.cs +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -17,7 +17,7 @@ namespace Dalamud.Logging; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable +internal class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable { private readonly LocalPlugin localPlugin; From f44c6794e77c701b46e1f47ec16586653856b521 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 30 Sep 2023 16:11:52 +0200 Subject: [PATCH 199/585] chore: tidy-up, move files shared between dalamud and injector into separate assembly --- Dalamud.Common/ClientLanguage.cs | 27 +++++++++++++ Dalamud.Common/Dalamud.Common.csproj | 13 +++++++ .../DalamudStartInfo.cs | 12 ++---- .../Game/GameVersion.cs | 18 ++++----- .../Game/GameVersionConverter.cs | 4 +- Dalamud.Injector/Dalamud.Injector.csproj | 8 +--- Dalamud.Injector/EntryPoint.cs | 3 +- Dalamud.Injector/Hacks.cs | 20 ---------- Dalamud.Test/Game/GameVersionTests.cs | 2 +- Dalamud.sln | 14 +++++++ Dalamud/ClientLanguage.cs | 2 + Dalamud/Dalamud.cs | 39 +++++++++++++++++-- Dalamud/Dalamud.csproj | 1 + Dalamud/Data/DataManager.cs | 28 +++++++------ Dalamud/EntryPoint.cs | 5 ++- Dalamud/Game/ChatHandlers.cs | 9 ++--- Dalamud/Game/ClientState/ClientState.cs | 4 +- Dalamud/Game/Command/CommandManager.cs | 6 +-- .../Interface/Internal/DalamudInterface.cs | 12 +++--- .../Interface/Internal/InterfaceManager.cs | 4 +- Dalamud/Interface/Internal/TextureManager.cs | 12 +++--- .../Internal/Windows/BranchSwitcherWindow.cs | 2 +- .../Windows/Data/Widgets/StartInfoWidget.cs | 2 +- .../PluginInstaller/PluginInstallerWindow.cs | 7 ++-- Dalamud/Plugin/Internal/PluginManager.cs | 12 +++--- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 8 ++-- .../Plugin/Internal/Types/PluginManifest.cs | 3 +- Dalamud/ServiceManager.cs | 22 ++--------- Dalamud/Support/Troubleshooting.cs | 4 +- 29 files changed, 174 insertions(+), 129 deletions(-) create mode 100644 Dalamud.Common/ClientLanguage.cs create mode 100644 Dalamud.Common/Dalamud.Common.csproj rename {Dalamud => Dalamud.Common}/DalamudStartInfo.cs (96%) rename {Dalamud => Dalamud.Common}/Game/GameVersion.cs (97%) rename {Dalamud => Dalamud.Common}/Game/GameVersionConverter.cs (98%) delete mode 100644 Dalamud.Injector/Hacks.cs diff --git a/Dalamud.Common/ClientLanguage.cs b/Dalamud.Common/ClientLanguage.cs new file mode 100644 index 000000000..1fd58dce1 --- /dev/null +++ b/Dalamud.Common/ClientLanguage.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Common; + +/// +/// Enum describing the language the game loads in. +/// +public enum ClientLanguage +{ + /// + /// Indicating a Japanese game client. + /// + Japanese, + + /// + /// Indicating an English game client. + /// + English, + + /// + /// Indicating a German game client. + /// + German, + + /// + /// Indicating a French game client. + /// + French, +} diff --git a/Dalamud.Common/Dalamud.Common.csproj b/Dalamud.Common/Dalamud.Common.csproj new file mode 100644 index 000000000..ac5d3fdba --- /dev/null +++ b/Dalamud.Common/Dalamud.Common.csproj @@ -0,0 +1,13 @@ + + + + net7.0 + enable + enable + + + + + + + diff --git a/Dalamud/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs similarity index 96% rename from Dalamud/DalamudStartInfo.cs rename to Dalamud.Common/DalamudStartInfo.cs index f22388e9b..069a0ef9f 100644 --- a/Dalamud/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -1,17 +1,13 @@ -using System; -using System.Collections.Generic; - -using Dalamud.Game; +using Dalamud.Common.Game; using Newtonsoft.Json; -namespace Dalamud; +namespace Dalamud.Common; /// /// Struct containing information needed to initialize Dalamud. /// [Serializable] -[ServiceManager.Service] -public record DalamudStartInfo : IServiceType +public record DalamudStartInfo { /// /// Initializes a new instance of the class. @@ -97,7 +93,7 @@ public record DalamudStartInfo : IServiceType /// /// Gets or sets troubleshooting information to attach when generating a tspack file. /// - public string TroubleshootingPackData { get; set; } + public string? TroubleshootingPackData { get; set; } /// /// Gets or sets a value that specifies how much to wait before a new Dalamud session. diff --git a/Dalamud/Game/GameVersion.cs b/Dalamud.Common/Game/GameVersion.cs similarity index 97% rename from Dalamud/Game/GameVersion.cs rename to Dalamud.Common/Game/GameVersion.cs index 2b2021e60..26ff0e48f 100644 --- a/Dalamud/Game/GameVersion.cs +++ b/Dalamud.Common/Game/GameVersion.cs @@ -1,11 +1,9 @@ -using System; using System.Globalization; -using System.Linq; using System.Text; using Newtonsoft.Json; -namespace Dalamud.Game; +namespace Dalamud.Common.Game; /// /// A GameVersion object contains give hierarchical numeric components: year, month, @@ -168,14 +166,14 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable new GameVersion(this.Year, this.Month, this.Day, this.Major, this.Minor); /// - public int CompareTo(object obj) + public int CompareTo(object? obj) { if (obj == null) return 1; @@ -315,7 +313,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable - public int CompareTo(GameVersion value) + public int CompareTo(GameVersion? value) { if (value == null) return 1; @@ -348,7 +346,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable - public override bool Equals(object obj) + public override bool Equals(object? obj) { if (obj is not GameVersion value) return false; @@ -357,7 +355,7 @@ public sealed class GameVersion : ICloneable, IComparable, IComparable - public bool Equals(GameVersion value) + public bool Equals(GameVersion? value) { if (value == null) { diff --git a/Dalamud/Game/GameVersionConverter.cs b/Dalamud.Common/Game/GameVersionConverter.cs similarity index 98% rename from Dalamud/Game/GameVersionConverter.cs rename to Dalamud.Common/Game/GameVersionConverter.cs index f307b6fb9..a1876869a 100644 --- a/Dalamud/Game/GameVersionConverter.cs +++ b/Dalamud.Common/Game/GameVersionConverter.cs @@ -1,8 +1,6 @@ -using System; - using Newtonsoft.Json; -namespace Dalamud.Game; +namespace Dalamud.Common.Game; /// /// Converts a to and from a string (e.g. "2010.01.01.1234.5678"). diff --git a/Dalamud.Injector/Dalamud.Injector.csproj b/Dalamud.Injector/Dalamud.Injector.csproj index ea9e4f0a3..d8a74e58d 100644 --- a/Dalamud.Injector/Dalamud.Injector.csproj +++ b/Dalamud.Injector/Dalamud.Injector.csproj @@ -81,12 +81,6 @@ - - - - - - - + diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index a35248062..bd9fa87f8 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -9,7 +9,8 @@ using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; -using Dalamud.Game; +using Dalamud.Common; +using Dalamud.Common.Game; using Newtonsoft.Json; using Reloaded.Memory.Buffers; using Serilog; diff --git a/Dalamud.Injector/Hacks.cs b/Dalamud.Injector/Hacks.cs deleted file mode 100644 index 7bc4468af..000000000 --- a/Dalamud.Injector/Hacks.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; - -// ReSharper disable once CheckNamespace -namespace Dalamud; - -// TODO: Get rid of this! Move StartInfo to another assembly, make this good - -/// -/// Class to initialize Service<T>s. -/// -internal static class ServiceManager -{ - /// - /// Indicates that the class is a service. - /// - [AttributeUsage(AttributeTargets.Class)] - public class Service : Attribute - { - } -} diff --git a/Dalamud.Test/Game/GameVersionTests.cs b/Dalamud.Test/Game/GameVersionTests.cs index 44a5813c8..dcace4279 100644 --- a/Dalamud.Test/Game/GameVersionTests.cs +++ b/Dalamud.Test/Game/GameVersionTests.cs @@ -1,4 +1,4 @@ -using Dalamud.Game; +using Dalamud.Common.Game; using Xunit; namespace Dalamud.Test.Game diff --git a/Dalamud.sln b/Dalamud.sln index 443f38496..200238a83 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -38,6 +38,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.InteropS EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "DalamudCrashHandler\DalamudCrashHandler.vcxproj", "{317A264C-920B-44A1-8A34-F3A6827B0705}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -202,6 +204,18 @@ Global {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.Build.0 = Release|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.ActiveCfg = Release|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.Build.0 = Release|x64 + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x64.ActiveCfg = Debug|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x64.Build.0 = Debug|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x86.ActiveCfg = Debug|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x86.Build.0 = Debug|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x64.ActiveCfg = Release|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x64.Build.0 = Release|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x86.ActiveCfg = Release|Any CPU + {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Dalamud/ClientLanguage.cs b/Dalamud/ClientLanguage.cs index 4e04d4a54..8f2c52456 100644 --- a/Dalamud/ClientLanguage.cs +++ b/Dalamud/ClientLanguage.cs @@ -1,5 +1,7 @@ namespace Dalamud; +// TODO(v10): Delete this, and use Dalamud.Common.ClientLanguage instead for everything. + /// /// Enum describing the language the game loads in. /// diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index a9a3a511a..8a75049ad 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics; using System.IO; using System.Linq; @@ -7,6 +6,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.Gui.Internal; @@ -14,6 +14,7 @@ using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Storage; using Dalamud.Utility; +using Dalamud.Utility.Timing; using PInvoke; using Serilog; @@ -47,10 +48,28 @@ internal sealed class Dalamud : IServiceType /// Event used to signal the main thread to continue. public Dalamud(DalamudStartInfo info, ReliableFileStorage fs, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent) { + this.StartInfo = info; + this.unloadSignal = new ManualResetEvent(false); this.unloadSignal.Reset(); + + // Directory resolved signatures(CS, our own) will be cached in + var cacheDir = new DirectoryInfo(Path.Combine(this.StartInfo.WorkingDirectory!, "cachedSigs")); + if (!cacheDir.Exists) + cacheDir.Create(); + + // Set up the SigScanner for our target module + TargetSigScanner scanner; + using (Timings.Start("SigScanner Init")) + { + scanner = new TargetSigScanner( + true, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}.json"))); + } - ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, fs, configuration); + ServiceManager.InitializeProvidedServices(this, fs, configuration, scanner); + + // Set up FFXIVClientStructs + this.SetupClientStructsResolver(cacheDir); if (!configuration.IsResumeGameAfterPluginLoad) { @@ -97,11 +116,16 @@ internal sealed class Dalamud : IServiceType }); } } + + /// + /// Gets the start information for this Dalamud instance. + /// + internal DalamudStartInfo StartInfo { get; private set; } /// /// Gets location of stored assets. /// - internal DirectoryInfo AssetDirectory => new(Service.Get().AssetDirectory!); + internal DirectoryInfo AssetDirectory => new(this.StartInfo.AssetDirectory!); /// /// Signal to the crash handler process that we should restart the game. @@ -176,4 +200,13 @@ internal sealed class Dalamud : IServiceType var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter); Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter); } + + private void SetupClientStructsResolver(DirectoryInfo cacheDir) + { + using (Timings.Start("CS Resolver Init")) + { + FFXIVClientStructs.Interop.Resolver.GetInstance.SetupSearchSpace(Service.Get().SearchBase, new FileInfo(Path.Combine(cacheDir.FullName, $"{this.StartInfo.GameVersion}_cs.json"))); + FFXIVClientStructs.Interop.Resolver.GetInstance.Resolve(); + } + } } diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 7ae97e1a6..69d08f517 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -89,6 +89,7 @@ + diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index f1f98229a..6195532ab 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -7,6 +7,7 @@ using System.Threading; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; +using Dalamud.Utility; using Dalamud.Utility.Timing; using JetBrains.Annotations; using Lumina; @@ -32,9 +33,9 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager private readonly CancellationTokenSource luminaCancellationTokenSource; [ServiceManager.ServiceConstructor] - private DataManager(DalamudStartInfo dalamudStartInfo, Dalamud dalamud) + private DataManager(Dalamud dalamud) { - this.Language = dalamudStartInfo.Language; + this.Language = (ClientLanguage)dalamud.StartInfo.Language; // Set up default values so plugins do not null-reference when data is being loaded. this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary()); @@ -82,17 +83,20 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager Log.Information("Lumina is ready: {0}", this.GameData.DataPath); - try + if (!dalamud.StartInfo.TroubleshootingPackData.IsNullOrEmpty()) { - var tsInfo = - JsonConvert.DeserializeObject( - dalamudStartInfo.TroubleshootingPackData); - this.HasModifiedGameDataFiles = - tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception; - } - catch - { - // ignored + try + { + var tsInfo = + JsonConvert.DeserializeObject( + dalamud.StartInfo.TroubleshootingPackData); + this.HasModifiedGameDataFiles = + tsInfo?.IndexIntegrity is LauncherTroubleshootingInfo.IndexIntegrityResult.Failed or LauncherTroubleshootingInfo.IndexIntegrityResult.Exception; + } + catch + { + // ignored + } } } diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index 6b53ee3a6..c9537eda6 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -1,4 +1,3 @@ -using System; using System.Diagnostics; using System.IO; using System.Net; @@ -6,6 +5,7 @@ using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; +using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Logging.Internal; using Dalamud.Logging.Retention; @@ -163,6 +163,9 @@ public sealed class EntryPoint Log.Information(new string('-', 80)); Log.Information("Initializing a session.."); + if (string.IsNullOrEmpty(info.WorkingDirectory)) + throw new Exception("Working directory was invalid"); + Reloaded.Hooks.Tools.Utilities.FasmBasePath = new DirectoryInfo(info.WorkingDirectory); // This is due to GitHub not supporting TLS 1.0, so we enable all TLS versions globally diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 896d296fc..d2f4b30c7 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -14,8 +13,6 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; -using Dalamud.IoC; -using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; using Serilog; @@ -104,6 +101,9 @@ internal class ChatHandlers : IServiceType private readonly DalamudLinkPayload openInstallerWindowLink; + [ServiceManager.ServiceDependency] + private readonly Dalamud dalamud = Service.Get(); + [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); @@ -160,7 +160,6 @@ internal class ChatHandlers : IServiceType private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) { - var startInfo = Service.Get(); var clientState = Service.GetNullable(); if (clientState == null) return; @@ -182,7 +181,7 @@ internal class ChatHandlers : IServiceType if (type == XivChatType.RetainerSale) { - foreach (var regex in this.retainerSaleRegexes[startInfo.Language]) + foreach (var regex in this.retainerSaleRegexes[(ClientLanguage)this.dalamud.StartInfo.Language]) { var matchInfo = regex.Match(message.TextValue); diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index ccb87ff0e..3b3f65128 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -41,7 +41,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState private bool lastFramePvP; [ServiceManager.ServiceConstructor] - private ClientState(TargetSigScanner sigScanner, DalamudStartInfo startInfo, GameLifecycle lifecycle) + private ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle) { this.lifecycle = lifecycle; this.address = new ClientStateAddressResolver(); @@ -49,7 +49,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState Log.Verbose("===== C L I E N T S T A T E ====="); - this.ClientLanguage = startInfo.Language; + this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language; Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}"); diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 218b89676..6b67f1892 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -35,15 +35,15 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage private readonly ChatGui chatGui = Service.Get(); [ServiceManager.ServiceConstructor] - private CommandManager(DalamudStartInfo startInfo) + private CommandManager(Dalamud dalamud) { - this.currentLangCommandRegex = startInfo.Language switch + this.currentLangCommandRegex = (ClientLanguage)dalamud.StartInfo.Language switch { ClientLanguage.Japanese => this.commandRegexJp, ClientLanguage.English => this.commandRegexEn, ClientLanguage.German => this.commandRegexDe, ClientLanguage.French => this.commandRegexFr, - _ => this.currentLangCommandRegex, + _ => this.commandRegexEn, }; this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled; diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index cfaae485a..9e4c215b3 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -635,9 +635,7 @@ internal class DalamudInterface : IDisposable, IServiceType ImGui.EndMenu(); } - - var startInfo = Service.Get(); - + var logSynchronously = configuration.LogSynchronously; if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously)) { @@ -645,10 +643,10 @@ internal class DalamudInterface : IDisposable, IServiceType configuration.QueueSave(); EntryPoint.InitLogging( - startInfo.LogPath!, - startInfo.BootShowConsole, + dalamud.StartInfo.LogPath!, + dalamud.StartInfo.BootShowConsole, configuration.LogSynchronously, - startInfo.LogName); + dalamud.StartInfo.LogName); } var antiDebug = Service.Get(); @@ -767,7 +765,7 @@ internal class DalamudInterface : IDisposable, IServiceType } ImGui.MenuItem(Util.AssemblyVersion, false); - ImGui.MenuItem(startInfo.GameVersion?.ToString() ?? "Unknown version", false); + ImGui.MenuItem(dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); ImGui.MenuItem($"D: {Util.GetGitHash()}[{Util.GetGitCommitCount()}] CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.Interop.Resolver.Version}]", false); ImGui.MenuItem($"CLR: {Environment.Version}", false); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d00f33180..dd85f1db4 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -563,10 +563,10 @@ internal class InterfaceManager : IDisposable, IServiceType return; } - var startInfo = Service.Get(); + var startInfo = Service.Get().StartInfo; var configuration = Service.Get(); - var iniFileInfo = new FileInfo(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath), "dalamudUI.ini")); + var iniFileInfo = new FileInfo(Path.Combine(Path.GetDirectoryName(startInfo.ConfigurationPath)!, "dalamudUI.ini")); try { diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 7c773bd36..9fecf74f4 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -39,7 +39,8 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe private readonly Framework framework; private readonly DataManager dataManager; private readonly InterfaceManager im; - private readonly DalamudStartInfo startInfo; + + private readonly ClientLanguage language; private readonly Dictionary activeTextures = new(); @@ -48,17 +49,18 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe /// /// Initializes a new instance of the class. /// + /// Dalamud instance. /// Framework instance. /// DataManager instance. /// InterfaceManager instance. - /// DalamudStartInfo instance. [ServiceManager.ServiceConstructor] - public TextureManager(Framework framework, DataManager dataManager, InterfaceManager im, DalamudStartInfo startInfo) + public TextureManager(Dalamud dalamud, Framework framework, DataManager dataManager, InterfaceManager im) { this.framework = framework; this.dataManager = dataManager; this.im = im; - this.startInfo = startInfo; + + this.language = (ClientLanguage)dalamud.StartInfo.Language; this.framework.Update += this.FrameworkOnUpdate; @@ -115,7 +117,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe if (this.dataManager.FileExists(path)) return path; - language ??= this.startInfo.Language; + language ??= this.language; var languageFolder = language switch { ClientLanguage.Japanese => "ja/", diff --git a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs index 05d8d04e8..dcde7d008 100644 --- a/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs +++ b/Dalamud/Interface/Internal/Windows/BranchSwitcherWindow.cs @@ -66,7 +66,7 @@ public class BranchSwitcherWindow : Window return; } - var si = Service.Get(); + var si = Service.Get().StartInfo; var itemsArray = this.branches.Select(x => x.Key).ToArray(); ImGui.ListBox("Branch", ref this.selectedBranchIndex, itemsArray, itemsArray.Length); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs index 65ed65e03..4dee316c5 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/StartInfoWidget.cs @@ -26,7 +26,7 @@ internal class StartInfoWidget : IDataWindowWidget /// public void Draw() { - var startInfo = Service.Get(); + var startInfo = Service.Get().StartInfo; ImGui.Text(JsonConvert.SerializeObject(startInfo, Formatting.Indented)); } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 6e2ad862c..7ff2f61e0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1994,7 +1994,6 @@ internal class PluginInstallerWindow : Window, IDisposable { var configuration = Service.Get(); var pluginManager = Service.Get(); - var startInfo = Service.Get(); if (ImGui.BeginPopupContextItem("ItemContextMenu")) { @@ -2022,10 +2021,10 @@ internal class PluginInstallerWindow : Window, IDisposable Task.Run(() => { pluginManager.PluginConfigs.Delete(manifest.InternalName); + var dir = pluginManager.PluginConfigs.GetDirectory(manifest.InternalName); - var path = Path.Combine(startInfo.PluginDirectory, manifest.InternalName); - if (Directory.Exists(path)) - Directory.Delete(path, true); + if (Directory.Exists(dir)) + Directory.Delete(dir, true); }) .ContinueWith(task => { diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 49608ac9b..dc658792c 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -74,7 +74,7 @@ internal partial class PluginManager : IDisposable, IServiceType private readonly DalamudConfiguration configuration = Service.Get(); [ServiceManager.ServiceDependency] - private readonly DalamudStartInfo startInfo = Service.Get(); + private readonly Dalamud dalamud = Service.Get(); [ServiceManager.ServiceDependency] private readonly ProfileManager profileManager = Service.Get(); @@ -90,12 +90,12 @@ internal partial class PluginManager : IDisposable, IServiceType [ServiceManager.ServiceConstructor] private PluginManager() { - this.pluginDirectory = new DirectoryInfo(this.startInfo.PluginDirectory!); + this.pluginDirectory = new DirectoryInfo(this.dalamud.StartInfo.PluginDirectory!); if (!this.pluginDirectory.Exists) this.pluginDirectory.Create(); - this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || this.configuration.PluginSafeMode || this.startInfo.NoLoadPlugins; + this.SafeMode = EnvironmentConfiguration.DalamudNoPlugins || this.configuration.PluginSafeMode || this.dalamud.StartInfo.NoLoadPlugins; try { @@ -119,9 +119,9 @@ internal partial class PluginManager : IDisposable, IServiceType this.configuration.QueueSave(); } - this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(this.startInfo.ConfigurationPath) ?? string.Empty, "pluginConfigs")); + this.PluginConfigs = new PluginConfigurations(Path.Combine(Path.GetDirectoryName(this.dalamud.StartInfo.ConfigurationPath) ?? string.Empty, "pluginConfigs")); - var bannedPluginsJson = File.ReadAllText(Path.Combine(this.startInfo.AssetDirectory!, "UIRes", "bannedplugin.json")); + var bannedPluginsJson = File.ReadAllText(Path.Combine(this.dalamud.StartInfo.AssetDirectory!, "UIRes", "bannedplugin.json")); this.bannedPlugins = JsonConvert.DeserializeObject(bannedPluginsJson); if (this.bannedPlugins == null) { @@ -1168,7 +1168,7 @@ internal partial class PluginManager : IDisposable, IServiceType } // Applicable version - if (manifest.ApplicableVersion < this.startInfo.GameVersion) + if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion) { Log.Verbose($"Game version: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}"); return false; diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 80d6edfd3..57bea0f57 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -1,10 +1,10 @@ -using System; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Dalamud.Common.Game; using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.Gui.Dtr; @@ -336,7 +336,7 @@ internal class LocalPlugin : IDisposable var framework = await Service.GetAsync(); var ioc = await Service.GetAsync(); var pluginManager = await Service.GetAsync(); - var startInfo = await Service.GetAsync(); + var dalamud = await Service.GetAsync(); // UiBuilder constructor requires the following two. await Service.GetAsync(); @@ -392,7 +392,7 @@ internal class LocalPlugin : IDisposable if (pluginManager.IsManifestBanned(this.manifest) && !this.IsDev) throw new BannedPluginException($"Unable to load {this.Name}, banned"); - if (this.manifest.ApplicableVersion < startInfo.GameVersion) + if (this.manifest.ApplicableVersion < dalamud.StartInfo.GameVersion) throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version"); if (this.manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels) @@ -624,7 +624,7 @@ internal class LocalPlugin : IDisposable /// Whether or not this plugin shouldn't load. public bool CheckPolicy() { - var startInfo = Service.Get(); + var startInfo = Service.Get().StartInfo; var manager = Service.Get(); if (startInfo.NoLoadPlugins) diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index 0b5ec26fc..34fa04f6e 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -1,7 +1,6 @@ -using System; using System.Collections.Generic; -using Dalamud.Game; +using Dalamud.Common.Game; using Dalamud.Plugin.Internal.Types.Manifest; using Newtonsoft.Json; diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index bb680127c..e8512e854 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -1,7 +1,5 @@ -using System; using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Reflection; using System.Threading; @@ -83,16 +81,11 @@ internal static class ServiceManager /// Initializes Provided Services and FFXIVClientStructs. /// /// Instance of . - /// Instance of . /// Instance of . /// Instance of . - public static void InitializeProvidedServicesAndClientStructs(Dalamud dalamud, DalamudStartInfo startInfo, ReliableFileStorage fs, DalamudConfiguration configuration) + /// Instance of . + public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner) { - // Initialize the process information. - var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs")); - if (!cacheDir.Exists) - cacheDir.Create(); - lock (LoadedServices) { void ProvideService(T service) where T : IServiceType @@ -103,19 +96,10 @@ internal static class ServiceManager } ProvideService(dalamud); - ProvideService(startInfo); ProvideService(fs); ProvideService(configuration); ProvideService(new ServiceContainer()); - ProvideService( - new TargetSigScanner( - true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json")))); - } - - using (Timings.Start("CS Resolver Init")) - { - FFXIVClientStructs.Interop.Resolver.GetInstance.SetupSearchSpace(Service.Get().SearchBase, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json"))); - FFXIVClientStructs.Interop.Resolver.GetInstance.Resolve(); + ProvideService(scanner); } } diff --git a/Dalamud/Support/Troubleshooting.cs b/Dalamud/Support/Troubleshooting.cs index 9893451f4..59ebcaa16 100644 --- a/Dalamud/Support/Troubleshooting.cs +++ b/Dalamud/Support/Troubleshooting.cs @@ -58,7 +58,7 @@ public static class Troubleshooting /// internal static void LogTroubleshooting() { - var startInfo = Service.Get(); + var startInfo = Service.Get().StartInfo; var configuration = Service.Get(); var interfaceManager = Service.GetNullable(); var pluginManager = Service.GetNullable(); @@ -72,7 +72,7 @@ public static class Troubleshooting EverStartedLoadingPlugins = pluginManager?.InstalledPlugins.Where(x => x.HasEverStartedLoad).Select(x => x.InternalName).ToList(), DalamudVersion = Util.AssemblyVersion, DalamudGitHash = Util.GetGitHash(), - GameVersion = startInfo.GameVersion.ToString(), + GameVersion = startInfo.GameVersion?.ToString() ?? "Unknown", Language = startInfo.Language.ToString(), BetaKey = configuration.DalamudBetaKey, DoPluginTest = configuration.DoPluginTest, From 55ab59a76be12ceb364e8ad27d5a908e5a0c595d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 30 Sep 2023 16:34:02 +0200 Subject: [PATCH 200/585] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index a1ddff097..bfcc02f0b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit a1ddff0974729a2e984d8cc1dc007eff19bd74ab +Subproject commit bfcc02f0ba0fa0ee14603ee6b6f385eaeebbe43c From 8fde0af0f976a230004e1dde37173aefe3d045b8 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 30 Sep 2023 18:25:17 -0700 Subject: [PATCH 201/585] Add IconBrowser Widget --- .../Internal/Windows/Data/DataWindow.cs | 2 +- .../Windows/Data/Widgets/IconBrowserWidget.cs | 216 ++++++++++++++++++ 2 files changed, 217 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 1363c6abe..ba47d2c8e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -1,4 +1,3 @@ -using System; using System.Linq; using System.Numerics; @@ -50,6 +49,7 @@ internal class DataWindow : Window new UIColorWidget(), new DataShareWidget(), new NetworkMonitorWidget(), + new IconBrowserWidget(), }; private readonly IOrderedEnumerable orderedModules; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs new file mode 100644 index 000000000..b37878045 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -0,0 +1,216 @@ +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Numerics; + +using Dalamud.Data; +using Dalamud.Interface.Utility; +using Dalamud.Utility; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Data widget for browsing in-game icons. +/// +public class IconBrowserWidget : IDataWindowWidget +{ + // Remove range 170,000 -> 180,000 by default, this specific range causes exceptions. + private readonly HashSet nullValues = Enumerable.Range(170000, 10000).ToHashSet(); + + private Vector2 iconSize = new(64.0f, 64.0f); + private Vector2 editIconSize = new(64.0f, 64.0f); + + private List valueRange = Enumerable.Range(0, 200000).ToList(); + + private int lastNullValueCount; + private int startRange; + private int stopRange = 200000; + private bool showTooltipImage; + + private Vector2 mouseDragStart; + private bool dragStarted; + private Vector2 lastWindowSize = Vector2.Zero; + + /// + public string[]? CommandShortcuts { get; init; } = { "icon", "icons" }; + + /// + public string DisplayName { get; init; } = "Icon Browser"; + + /// + public bool Ready { get; set; } = true; + + /// + public void Load() + { + } + + /// + public void Draw() + { + this.DrawOptions(); + + if (ImGui.BeginChild("ScrollableSection", ImGui.GetContentRegionAvail(), false, ImGuiWindowFlags.NoMove)) + { + var itemsPerRow = (int)MathF.Floor(ImGui.GetContentRegionMax().X / (this.iconSize.X + ImGui.GetStyle().ItemSpacing.X)); + var itemHeight = this.iconSize.Y + ImGui.GetStyle().ItemSpacing.Y; + + ImGuiClip.ClippedDraw(this.valueRange, this.DrawIcon, itemsPerRow, itemHeight); + } + + ImGui.EndChild(); + + this.ProcessMouseDragging(); + + if (this.lastNullValueCount != this.nullValues.Count) + { + this.RecalculateIndexRange(); + this.lastNullValueCount = this.nullValues.Count; + } + } + + // Limit the popup image to half our screen size. + private static float GetImageScaleFactor(IDalamudTextureWrap texture) + { + var workArea = ImGui.GetMainViewport().Size / 2.0f; + var scale = 1.0f; + + if (texture.Width > workArea.X || texture.Height > workArea.Y) + { + var widthRatio = workArea.X / texture.Width; + var heightRatio = workArea.Y / texture.Height; + + scale = MathF.Min(widthRatio, heightRatio); + } + + return scale; + } + + private void DrawOptions() + { + ImGui.Columns(2); + + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputInt("##StartRange", ref this.startRange, 0, 0)) this.RecalculateIndexRange(); + + ImGui.NextColumn(); + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputInt("##StopRange", ref this.stopRange, 0, 0)) this.RecalculateIndexRange(); + + ImGui.NextColumn(); + ImGui.Checkbox("Show Image in Tooltip", ref this.showTooltipImage); + + ImGui.NextColumn(); + ImGui.InputFloat2("Icon Size", ref this.editIconSize); + if (ImGui.IsItemDeactivatedAfterEdit()) + { + this.iconSize = this.editIconSize; + } + + ImGui.Columns(1); + } + + private void DrawIcon(int iconId) + { + try + { + var cursor = ImGui.GetCursorScreenPos(); + + if (!this.IsIconValid(iconId)) + { + this.nullValues.Add(iconId); + return; + } + + if (Service.Get().GetIcon((uint)iconId) is { } texture) + { + ImGui.Image(texture.ImGuiHandle, this.iconSize); + + // If we have the option to show a tooltip image, draw the image, but make sure it's not too big. + if (ImGui.IsItemHovered() && this.showTooltipImage) + { + ImGui.BeginTooltip(); + + var scale = GetImageScaleFactor(texture); + + var textSize = ImGui.CalcTextSize(iconId.ToString()); + ImGui.SetCursorPosX(texture.Size.X * scale / 2.0f - textSize.X / 2.0f + ImGui.GetStyle().FramePadding.X * 2.0f); + ImGui.Text(iconId.ToString()); + + ImGui.Image(texture.ImGuiHandle, texture.Size * scale); + ImGui.EndTooltip(); + } + + // else, just draw the iconId. + else if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(iconId.ToString()); + } + } + else + { + // This texture was null, draw nothing, and prevent from trying to show it again. + this.nullValues.Add(iconId); + } + + ImGui.GetWindowDrawList().AddRect(cursor, cursor + this.iconSize, ImGui.GetColorU32(KnownColor.White.Vector())); + } + catch (Exception) + { + // If something went wrong, prevent from trying to show this icon again. + this.nullValues.Add(iconId); + } + } + + private void ProcessMouseDragging() + { + if (ImGui.IsItemHovered() || this.dragStarted) + { + if (ImGui.GetWindowSize() == this.lastWindowSize) + { + if (ImGui.IsItemClicked(ImGuiMouseButton.Left) && !this.dragStarted) + { + this.mouseDragStart = ImGui.GetMousePos(); + this.dragStarted = true; + } + } + else + { + this.lastWindowSize = ImGui.GetWindowSize(); + this.dragStarted = false; + } + } + + if (ImGui.IsMouseDragging(ImGuiMouseButton.Left) && this.dragStarted) + { + var delta = this.mouseDragStart - ImGui.GetMousePos(); + ImGui.GetIO().AddMouseWheelEvent(0.0f, -delta.Y / 85.0f); + this.mouseDragStart = ImGui.GetMousePos(); + } + else if (ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + this.dragStarted = false; + } + } + + // Check if the icon has a valid filepath, and exists in the game data. + private bool IsIconValid(int iconId) + { + var filePath = Service.Get().GetIconPath((uint)iconId); + return !filePath.IsNullOrEmpty() && Service.Get().FileExists(filePath); + } + + private void RecalculateIndexRange() + { + if (this.stopRange <= this.startRange || this.stopRange <= 0 || this.startRange < 0) + { + this.valueRange = new List(); + } + else + { + this.valueRange = Enumerable.Range(this.startRange, this.stopRange - this.startRange).ToList(); + this.valueRange.RemoveAll(value => this.nullValues.Contains(value)); + } + } +} From 6e8a97336c9ba870c779b52ec8c36010a006f963 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 1 Oct 2023 12:58:31 +0200 Subject: [PATCH 202/585] fix: remove invalid assert in TextureManager --- Dalamud/Interface/Internal/TextureManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 9fecf74f4..40aa72913 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -297,10 +297,9 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe TextureInfo? info; lock (this.activeTextures) { + // This either is a new texture, or it had been evicted and now wants to be drawn again. if (!this.activeTextures.TryGetValue(path, out info)) { - Debug.Assert(rethrow, "This should never run when getting outside of creator"); - info = new TextureInfo(); this.activeTextures.Add(path, info); } From 0690f5dd2ab23f8c356b76ca19d6a1179a7c2eff Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 1 Oct 2023 16:40:48 +0200 Subject: [PATCH 203/585] fix: always use frame height to draw icon buttons --- .../Components/ImGuiComponents.IconButton.cs | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs index 1c484d423..116b04bd2 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs @@ -110,11 +110,32 @@ public static partial class ImGuiComponents numColors++; } + var icon = iconText; + if (icon.Contains("#")) + icon = icon[..icon.IndexOf("#", StringComparison.Ordinal)]; + + ImGui.PushID(iconText); + ImGui.PushFont(UiBuilder.IconFont); - - var button = ImGui.Button(iconText); - + var iconSize = ImGui.CalcTextSize(icon); ImGui.PopFont(); + + var dl = ImGui.GetWindowDrawList(); + var cursor = ImGui.GetCursorScreenPos(); + + // Draw an ImGui button with the icon and text + var buttonWidth = iconSize.X + (ImGui.GetStyle().FramePadding.X * 2); + var buttonHeight = ImGui.GetFrameHeight(); + var button = ImGui.Button(string.Empty, new Vector2(buttonWidth, buttonHeight)); + + // Draw the icon on the window drawlist + var iconPos = new Vector2(cursor.X + ImGui.GetStyle().FramePadding.X, cursor.Y + ImGui.GetStyle().FramePadding.Y); + + ImGui.PushFont(UiBuilder.IconFont); + dl.AddText(iconPos, ImGui.GetColorU32(ImGuiCol.Text), icon); + ImGui.PopFont(); + + ImGui.PopID(); if (numColors > 0) ImGui.PopStyleColor(numColors); @@ -167,7 +188,7 @@ public static partial class ImGuiComponents // Draw an ImGui button with the icon and text var buttonWidth = iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding; - var buttonHeight = Math.Max(iconSize.Y, textSize.Y) + (ImGui.GetStyle().FramePadding.Y * 2); + var buttonHeight = ImGui.GetFrameHeight(); var button = ImGui.Button(string.Empty, new Vector2(buttonWidth, buttonHeight)); // Draw the icon on the window drawlist From a1f1c169dce5ebe244826bb5df6e35f4fc5cdf17 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 1 Oct 2023 12:01:58 -0700 Subject: [PATCH 204/585] Allow icon 180000 --- .../Internal/Windows/Data/Widgets/IconBrowserWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index b37878045..dcae6e689 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -16,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; public class IconBrowserWidget : IDataWindowWidget { // Remove range 170,000 -> 180,000 by default, this specific range causes exceptions. - private readonly HashSet nullValues = Enumerable.Range(170000, 10000).ToHashSet(); + private readonly HashSet nullValues = Enumerable.Range(170000, 9999).ToHashSet(); private Vector2 iconSize = new(64.0f, 64.0f); private Vector2 editIconSize = new(64.0f, 64.0f); From 2bdb83757700d6ad138ed0509ad125e0a8a9b7a2 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 1 Oct 2023 21:12:27 +0200 Subject: [PATCH 205/585] feat: enable early loaded services to wait for provided services, some rewrites to make service kind declaration more explicit --- Dalamud/Dalamud.cs | 19 +++++----- .../Interface/Internal/InterfaceManager.cs | 1 + Dalamud/ServiceManager.cs | 36 +++++++++---------- Dalamud/Service{T}.cs | 7 ++++ 4 files changed, 37 insertions(+), 26 deletions(-) diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 8a75049ad..f50a39aa3 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -74,14 +74,17 @@ internal sealed class Dalamud : IServiceType if (!configuration.IsResumeGameAfterPluginLoad) { NativeFunctions.SetEvent(mainThreadContinueEvent); - try - { - _ = ServiceManager.InitializeEarlyLoadableServices(); - } - catch (Exception e) - { - Log.Error(e, "Service initialization failure"); - } + ServiceManager.InitializeEarlyLoadableServices() + .ContinueWith(t => + { + if (t.IsCompletedSuccessfully) + return; + + Log.Error(t.Exception!, "Service initialization failure"); + Util.Fatal( + "Dalamud failed to load all necessary services.\n\nThe game will continue, but you may not be able to use plugins.", + "Dalamud", false); + }); } else { diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index dd85f1db4..93d9bb1dd 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1276,6 +1276,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// Represents an instance of InstanceManager with scene ready for use. /// + [ServiceManager.Service] public class InterfaceManagerWithScene : IServiceType { /// diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index e8512e854..553e0a4af 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -47,9 +47,9 @@ internal static class ServiceManager None = 0, /// - /// Regular service. + /// Service that is loaded manually. /// - ManualService = 1 << 0, + ProvidedService = 1 << 0, /// /// Service that is loaded asynchronously while the game starts. @@ -90,7 +90,7 @@ internal static class ServiceManager { void ProvideService(T service) where T : IServiceType { - Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ManualService), "Provided service must have Service attribute"); + Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute"); Service.Provide(service); LoadedServices.Add(typeof(T)); } @@ -119,23 +119,18 @@ internal static class ServiceManager var serviceContainer = Service.Get(); - foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) + foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract)) { var serviceKind = serviceType.GetServiceKind(); if (serviceKind is ServiceKind.None) - continue; + throw new Exception($"Service<{serviceType.FullName}> did not specify a kind"); // Let IoC know about the interfaces this service implements serviceContainer.RegisterInterfaces(serviceType); // Scoped service do not go through Service and are never early loaded - // Manual services are provided - if (serviceKind.HasFlag(ServiceKind.ScopedService) || serviceKind.HasFlag(ServiceKind.ManualService)) + if (serviceKind.HasFlag(ServiceKind.ScopedService)) continue; - - Debug.Assert( - !serviceKind.HasFlag(ServiceKind.ManualService) && !serviceKind.HasFlag(ServiceKind.ScopedService), - "Regular and scoped services should never be loaded early"); var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType); @@ -147,9 +142,14 @@ internal static class ServiceManager null, null); + getAsyncTaskMap[serviceType] = getTask; + + // We don't actually need to load provided services, something else does + if (serviceKind.HasFlag(ServiceKind.ProvidedService)) + continue; + if (serviceKind.HasFlag(ServiceKind.BlockingEarlyLoadedService)) { - getAsyncTaskMap[serviceType] = getTask; blockingEarlyLoadingServices.Add(serviceType); } else @@ -191,13 +191,13 @@ internal static class ServiceManager var hasDeps = true; foreach (var dependency in dependencyServicesMap[serviceType]) { - var depServiceKind = dependency.GetServiceKind(); - var depResolveTask = getAsyncTaskMap.GetValueOrDefault(dependency); + var depUnderlyingServiceType = dependency.GetGenericArguments().First(); + var depResolveTask = getAsyncTaskMap.GetValueOrDefault(depUnderlyingServiceType); - if (depResolveTask == null && (depServiceKind.HasFlag(ServiceKind.EarlyLoadedService) || depServiceKind.HasFlag(ServiceKind.BlockingEarlyLoadedService))) + if (depResolveTask == null) { - Log.Error("{Type}: {Dependency} has no resolver task, is it early loaded or blocking early loaded?", serviceType.FullName!, dependency.FullName!); - Debug.Assert(false, $"No resolver for dependent service {dependency.FullName}"); + Log.Error("{Type}: {Dependency} has no resolver task", serviceType.FullName!, dependency.FullName!); + Debug.Assert(false, $"No resolver for dependent service {depUnderlyingServiceType.FullName}"); } else if (depResolveTask is { IsCompleted: false }) { @@ -386,7 +386,7 @@ internal static class ServiceManager if (attr.IsAssignableTo(typeof(ScopedService))) return ServiceKind.ScopedService; - return ServiceKind.ManualService; + return ServiceKind.ProvidedService; } /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 539941c27..b609c9082 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -123,6 +123,8 @@ internal static class Service where T : IServiceType public static List GetDependencyServices() { var res = new List(); + + ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name); var ctor = GetServiceConstructor(); if (ctor != null) @@ -181,6 +183,11 @@ internal static class Service where T : IServiceType res.Add(serviceType); } } + + foreach (var type in res) + { + ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name); + } return res .Distinct() From 263771c0824c78ab1f1959fee3a402c3770842d4 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 1 Oct 2023 21:35:21 +0200 Subject: [PATCH 206/585] chore: move all dalamud logo textures into special service --- Dalamud/Interface/Internal/Branding.cs | 58 +++++++++++++++++++ .../Interface/Internal/DalamudInterface.cs | 28 ++------- .../Internal/Windows/ChangelogWindow.cs | 6 +- .../Internal/Windows/PluginImageCache.cs | 5 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 5 +- Dalamud/ServiceManager.cs | 10 +++- 6 files changed, 77 insertions(+), 35 deletions(-) create mode 100644 Dalamud/Interface/Internal/Branding.cs diff --git a/Dalamud/Interface/Internal/Branding.cs b/Dalamud/Interface/Internal/Branding.cs new file mode 100644 index 000000000..4162cabeb --- /dev/null +++ b/Dalamud/Interface/Internal/Branding.cs @@ -0,0 +1,58 @@ +using System.IO; + +using Dalamud.IoC.Internal; + +namespace Dalamud.Interface.Internal; + +/// +/// Class containing various textures used by Dalamud windows for branding purposes. +/// +[ServiceManager.EarlyLoadedService] +#pragma warning disable SA1015 +[InherentDependency] // Can't load textures before this +#pragma warning restore SA1015 +internal class Branding : IServiceType, IDisposable +{ + private readonly Dalamud dalamud; + private readonly TextureManager tm; + + /// + /// Initializes a new instance of the class. + /// + /// Dalamud instance. + /// TextureManager instance. + [ServiceManager.ServiceConstructor] + public Branding(Dalamud dalamud, TextureManager tm) + { + this.dalamud = dalamud; + this.tm = tm; + + this.LoadTextures(); + } + + /// + /// Gets a full-size Dalamud logo texture. + /// + public IDalamudTextureWrap Logo { get; private set; } = null!; + + /// + /// Gets a small Dalamud logo texture. + /// + public IDalamudTextureWrap LogoSmall { get; private set; } = null!; + + /// + public void Dispose() + { + this.Logo.Dispose(); + this.LogoSmall.Dispose(); + } + + private void LoadTextures() + { + this.Logo = this.tm.GetTextureFromFile(new FileInfo(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png"))) + ?? throw new Exception("Could not load logo."); + + this.LogoSmall = this.tm.GetTextureFromFile(new FileInfo(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png"))) + ?? throw new Exception("Could not load TSM logo."); + } +} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 9e4c215b3..a7b1e80b5 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -66,9 +66,6 @@ internal class DalamudInterface : IDisposable, IServiceType private readonly BranchSwitcherWindow branchSwitcherWindow; private readonly HitchSettingsWindow hitchSettingsWindow; - private readonly IDalamudTextureWrap logoTexture; - private readonly IDalamudTextureWrap tsmLogoTexture; - private bool isCreditsDarkening = false; private OutCubic creditsDarkeningAnimation = new(TimeSpan.FromSeconds(10)); @@ -92,7 +89,8 @@ internal class DalamudInterface : IDisposable, IServiceType Dalamud dalamud, DalamudConfiguration configuration, InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, - PluginImageCache pluginImageCache) + PluginImageCache pluginImageCache, + Branding branding) { var interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -136,26 +134,13 @@ internal class DalamudInterface : IDisposable, IServiceType interfaceManager.Draw += this.OnDraw; - var logoTex = - interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "logo.png")); - var tsmLogoTex = - interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png")); - - if (logoTex == null || tsmLogoTex == null) - { - throw new Exception("Failed to load logo textures"); - } - - this.logoTexture = logoTex; - this.tsmLogoTexture = tsmLogoTex; - var tsm = Service.Get(); - tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), this.tsmLogoTexture, this.OpenPluginInstaller); - tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), this.tsmLogoTexture, this.OpenSettings); + tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, this.OpenPluginInstaller); + tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), branding.LogoSmall, this.OpenSettings); if (!configuration.DalamudBetaKind.IsNullOrEmpty()) { - tsm.AddEntryCore(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), this.tsmLogoTexture, () => this.isImGuiDrawDevMenu = true); + tsm.AddEntryCore(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), branding.LogoSmall, () => this.isImGuiDrawDevMenu = true); } this.creditsDarkeningAnimation.Point1 = Vector2.Zero; @@ -192,9 +177,6 @@ internal class DalamudInterface : IDisposable, IServiceType this.consoleWindow.Dispose(); this.pluginWindow.Dispose(); this.titleScreenMenuWindow.Dispose(); - - this.logoTexture.Dispose(); - this.tsmLogoTexture.Dispose(); } #region Open diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 61010ce0c..cd4618f24 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -49,11 +49,7 @@ Thanks and have fun!"; this.Size = new Vector2(885, 463); this.SizeCondition = ImGuiCond.Appearing; - var interfaceManager = Service.Get(); - var dalamud = Service.Get(); - - this.logoTexture = - interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "logo.png"))!; + this.logoTexture = Service.Get().Logo; } /// diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index c334cd4bd..b721b08c3 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -50,6 +50,9 @@ internal class PluginImageCache : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly InterfaceManager.InterfaceManagerWithScene imWithScene = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly Branding branding = Service.Get(); + [ServiceManager.ServiceDependency] private readonly HappyHttpClient happyHttpClient = Service.Get(); @@ -88,7 +91,7 @@ internal class PluginImageCache : IDisposable, IServiceType this.installedIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "installedIcon.png"))) ?? this.emptyTextureTask).Unwrap(); this.thirdIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdIcon.png"))) ?? this.emptyTextureTask).Unwrap(); this.thirdInstalledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdInstalledIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.corePluginIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png"))) ?? this.emptyTextureTask).Unwrap(); + this.corePluginIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(this.branding.LogoSmall)).Unwrap(); this.downloadTask = Task.Factory.StartNew( () => this.DownloadTask(8), TaskCreationOptions.LongRunning); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index ec9833b78..9b6a32617 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -181,10 +181,9 @@ Contribute at: https://github.com/goatcorp/Dalamud public SettingsTabAbout() { - var dalamud = Service.Get(); - var interfaceManager = Service.Get(); + var branding = Service.Get(); - this.logoTexture = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "logo.png"))!; + this.logoTexture = branding.Logo; this.creditsThrottler = new(); } diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 553e0a4af..453fa3530 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -16,7 +16,7 @@ using JetBrains.Annotations; namespace Dalamud; // TODO: -// - Unify dependency walking code(load/unload +// - Unify dependency walking code(load/unload) // - Visualize/output .dot or imgui thing /// @@ -122,8 +122,7 @@ internal static class ServiceManager foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract)) { var serviceKind = serviceType.GetServiceKind(); - if (serviceKind is ServiceKind.None) - throw new Exception($"Service<{serviceType.FullName}> did not specify a kind"); + Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind"); // Let IoC know about the interfaces this service implements serviceContainer.RegisterInterfaces(serviceType); @@ -148,6 +147,11 @@ internal static class ServiceManager if (serviceKind.HasFlag(ServiceKind.ProvidedService)) continue; + Debug.Assert( + serviceKind.HasFlag(ServiceKind.EarlyLoadedService) || + serviceKind.HasFlag(ServiceKind.BlockingEarlyLoadedService), + "At this point, service must be either early loaded or blocking early loaded"); + if (serviceKind.HasFlag(ServiceKind.BlockingEarlyLoadedService)) { blockingEarlyLoadingServices.Add(serviceType); From bef5e7c3f520e6c484babf8e06716de9934cf63d Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 1 Oct 2023 22:13:29 +0200 Subject: [PATCH 207/585] fix: we need ReliableFileStorage still when disposing config --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 1e9bc1523..65f10c4ba 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -6,6 +5,7 @@ using System.Linq; using Dalamud.Game.Text; using Dalamud.Interface.Style; +using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Storage; using Dalamud.Utility; @@ -20,6 +20,9 @@ namespace Dalamud.Configuration.Internal; /// [Serializable] [ServiceManager.Service] +#pragma warning disable SA1015 +[InherentDependency] // We must still have this when unloading +#pragma warning restore SA1015 internal sealed class DalamudConfiguration : IServiceType, IDisposable { private static readonly JsonSerializerSettings SerializerSettings = new() From a382f226894cd785d5feb0d268c22d02910787e8 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Mon, 2 Oct 2023 23:47:28 +0200 Subject: [PATCH 208/585] 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 25b558711..be7d6b6d3 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 e3aa0b21412cedecc7b3d64aef1adeacda28feb6 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Mon, 2 Oct 2023 23:50:03 -0700 Subject: [PATCH 209/585] Emergent 6.5 Hotfixes - Round 1 (#1444) * fix: New sig for ToggleUiHide * fix: New sig for AddonOnRequestedUpdate * chore: Bump CS --- Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +- Dalamud/Game/Gui/GameGuiAddressResolver.cs | 2 +- Dalamud/Interface/Internal/UiDebug.cs | 2 +- lib/FFXIVClientStructs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 7690db50d..7b276c903 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -45,7 +45,7 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); - this.AddonOnRequestedUpdate = sig.ScanText("FF 90 90 01 00 00 48 8B 5C 24 30 48 83 C4 20"); + this.AddonOnRequestedUpdate = sig.ScanText("FF 90 98 01 00 00 48 8B 5C 24 30 48 83 C4 20"); this.AddonOnRefresh = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 41 8B F8 48 8B DA"); } } diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs index e45b07487..acb5539f6 100644 --- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs @@ -67,7 +67,7 @@ internal sealed class GameGuiAddressResolver : BaseAddressResolver this.HandleActionOut = sig.ScanText("48 89 5C 24 ?? 57 48 83 EC 20 48 8B DA 48 8B F9 4D 85 C0 74 1F"); this.HandleImm = sig.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 48 83 FF 09"); this.GetMatrixSingleton = sig.ScanText("E8 ?? ?? ?? ?? 48 8D 4C 24 ?? 48 89 4c 24 ?? 4C 8D 4D ?? 4C 8D 44 24 ??"); - this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ?? B8 ?? ?? ?? ??"); + this.ToggleUiHide = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 0F B6 B9 ?? ?? ?? ??"); this.Utf8StringFromSequence = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8D 41 22 66 C7 41 ?? ?? ?? 48 89 01 49 8B D8"); } } diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 524759f4a..14f062e01 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -215,7 +215,7 @@ internal unsafe class UiDebug while (b > byte.MaxValue) b -= byte.MaxValue; while (b < byte.MinValue) b += byte.MaxValue; textNode->AlignmentFontType = (byte)b; - textNode->AtkResNode.Flags_2 |= 0x1; + textNode->AtkResNode.DrawFlags |= 0x1; } ImGui.Text($"Color: #{textNode->TextColor.R:X2}{textNode->TextColor.G:X2}{textNode->TextColor.B:X2}{textNode->TextColor.A:X2}"); diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index bfcc02f0b..b39667d61 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit bfcc02f0ba0fa0ee14603ee6b6f385eaeebbe43c +Subproject commit b39667d619d2446b2a03129a387e79faa7026c0f From b2b366032b03adb826863ba1b7a9735f87242242 Mon Sep 17 00:00:00 2001 From: Cara Date: Wed, 4 Oct 2023 03:18:37 +1030 Subject: [PATCH 210/585] Fix dtr (#1446) * Fix DtrBar * Zero some things * Update FFXIVClientStructs --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 12 +++++++++++- lib/FFXIVClientStructs | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 06d37e7ec..66cf8c7cc 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -417,7 +417,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private AtkTextNode* MakeNode(uint nodeId) { - var newTextNode = IMemorySpace.GetUISpace()->Create(); + var newTextNode = AtkUldManager.CreateAtkTextNode(); if (newTextNode == null) { Log.Debug("Failed to allocate memory for AtkTextNode"); @@ -443,6 +443,16 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar newTextNode->TextColor = new ByteColor { R = 255, G = 255, B = 255, A = 255 }; newTextNode->EdgeColor = new ByteColor { R = 142, G = 106, B = 12, A = 255 }; + // Memory is filled with random data after being created, zero out some things to avoid issues. + newTextNode->UnkPtr_1 = null; + newTextNode->SelectStart = 0; + newTextNode->SelectEnd = 0; + newTextNode->FontCacheHandle = 0; + newTextNode->CharSpacing = 0; + newTextNode->BackgroundColor = new ByteColor { R = 0, G = 0, B = 0, A = 0 }; + newTextNode->TextId = 0; + newTextNode->SheetType = 0; + return newTextNode; } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index b39667d61..a58001056 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b39667d619d2446b2a03129a387e79faa7026c0f +Subproject commit a580010564e460b979bb3bc55756827229a0ebc2 From 5cdb707ef3944deeb3d7c4a482158154f1e35343 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 3 Oct 2023 19:03:50 +0200 Subject: [PATCH 211/585] deps: upgrade Lumina.Excel to 6.5.0 --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 2 +- Dalamud/Dalamud.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 3938d0c80..2c2a86b5f 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -28,7 +28,7 @@ - + all diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 69d08f517..7e11b0975 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -69,7 +69,7 @@ - + From 482a607335b8b5b5db77029e92ea9537012b3530 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 3 Oct 2023 19:04:04 +0200 Subject: [PATCH 212/585] fix warnings --- Dalamud.CorePlugin/PluginImpl.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 03f11e989..ef99f6def 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -56,7 +56,7 @@ namespace Dalamud.CorePlugin /// /// Dalamud plugin interface. /// Logging service. - public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log, ISigScanner scanner) + public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log) { try { @@ -66,8 +66,6 @@ namespace Dalamud.CorePlugin this.windowSystem.AddWindow(new PluginWindow()); - this.pluginLog.Information(scanner.ToString()); - this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; From d7a30796ec8af4005441b2fc35bdc67258356432 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 3 Oct 2023 19:25:37 +0200 Subject: [PATCH 213/585] Update ClientStructs (#1443) 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 a58001056..01bea1fd1 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit a580010564e460b979bb3bc55756827229a0ebc2 +Subproject commit 01bea1fd1827f64c62166aae8233ab5b93f0a0e0 From ee57af709aa9327e989520bb0e417590c0e0a4ee Mon Sep 17 00:00:00 2001 From: awgil Date: Tue, 3 Oct 2023 20:41:24 +0300 Subject: [PATCH 214/585] Object/status table update. (#1447) --- Dalamud/Game/ClientState/Objects/ObjectTable.cs | 2 +- Dalamud/Game/ClientState/Statuses/StatusList.cs | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index c6320ccbb..278c0772f 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game.ClientState.Objects; #pragma warning restore SA1015 internal sealed partial class ObjectTable : IServiceType, IObjectTable { - private const int ObjectTableLength = 596; + private const int ObjectTableLength = 599; private readonly ClientStateAddressResolver address; diff --git a/Dalamud/Game/ClientState/Statuses/StatusList.cs b/Dalamud/Game/ClientState/Statuses/StatusList.cs index bcff50360..fce59e29b 100644 --- a/Dalamud/Game/ClientState/Statuses/StatusList.cs +++ b/Dalamud/Game/ClientState/Statuses/StatusList.cs @@ -10,8 +10,6 @@ namespace Dalamud.Game.ClientState.Statuses; /// public sealed unsafe partial class StatusList { - private const int StatusListLength = 30; - /// /// Initializes a new instance of the class. /// @@ -38,7 +36,7 @@ public sealed unsafe partial class StatusList /// /// Gets the amount of status effect slots the actor has. /// - public int Length => StatusListLength; + public int Length => Struct->NumValidStatuses; private static int StatusSize { get; } = Marshal.SizeOf(); @@ -53,7 +51,7 @@ public sealed unsafe partial class StatusList { get { - if (index < 0 || index > StatusListLength) + if (index < 0 || index > this.Length) return null; var addr = this.GetStatusAddress(index); @@ -107,7 +105,7 @@ public sealed unsafe partial class StatusList /// The memory address of the party member. public IntPtr GetStatusAddress(int index) { - if (index < 0 || index >= StatusListLength) + if (index < 0 || index >= this.Length) return IntPtr.Zero; return (IntPtr)(this.Struct->Status + (index * StatusSize)); @@ -134,7 +132,7 @@ public sealed partial class StatusList : IReadOnlyCollection, ICollectio /// public IEnumerator GetEnumerator() { - for (var i = 0; i < StatusListLength; i++) + for (var i = 0; i < this.Length; i++) { var status = this[i]; From 441e8976808e506e1d72c01457e0da0e21fb3d2f Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 3 Oct 2023 20:02:41 +0200 Subject: [PATCH 215/585] Update ClientStructs (#1448) 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 01bea1fd1..23de58456 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 01bea1fd1827f64c62166aae8233ab5b93f0a0e0 +Subproject commit 23de584564b12732be69e508988ca950d77715f3 From b3f10b822f3ce372744d46a48eb5eba4805d63f3 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 3 Oct 2023 21:20:39 +0200 Subject: [PATCH 216/585] Update ClientStructs (#1450) 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 23de58456..2b51a79c8 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 23de584564b12732be69e508988ca950d77715f3 +Subproject commit 2b51a79c83bb9edb9a5f04e9633f0d121babcc7d From feb1dc0f039ce208a8989571d2912ec0b6882123 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 3 Oct 2023 22:22:32 +0200 Subject: [PATCH 217/585] Update ClientStructs (#1451) 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 2b51a79c8..5c9fc4200 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 2b51a79c83bb9edb9a5f04e9633f0d121babcc7d +Subproject commit 5c9fc4200387edd32b9190d1b9f61e5d8a17114d From fcf29acc0277dad443ddeb11c2ad6d351fd716b2 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 3 Oct 2023 22:25:35 +0200 Subject: [PATCH 218/585] feat: updated changelog --- Dalamud/Game/ChatHandlers.cs | 3 +- Dalamud/Interface/Animation/Easing.cs | 8 + Dalamud/Interface/ColorHelpers.cs | 9 + .../Components/ImGuiComponents.IconButton.cs | 21 + .../Interface/Internal/DalamudInterface.cs | 4 +- .../Internal/Windows/ChangelogWindow.cs | 450 +++++++++++++----- .../Internal/Windows/TitleScreenMenuWindow.cs | 10 +- Dalamud/Interface/Windowing/Window.cs | 14 +- 8 files changed, 393 insertions(+), 126 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index d2f4b30c7..890cd0439 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -251,10 +251,9 @@ internal class ChatHandlers : IServiceType Type = XivChatType.Notice, }); - if (string.IsNullOrEmpty(this.configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(this.configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor))) + if (ChangelogWindow.WarrantsChangelog()) { dalamudInterface.OpenChangelogWindow(); - this.configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor; } this.configuration.LastVersion = assemblyVersion; diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index 2ac040143..54c41c16d 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -109,6 +109,14 @@ public abstract class Easing this.animationTimer.Restart(); } + /// + /// Resets the animation. + /// + public void Reset() + { + this.animationTimer.Reset(); + } + /// /// Updates the animation. /// diff --git a/Dalamud/Interface/ColorHelpers.cs b/Dalamud/Interface/ColorHelpers.cs index b2b489004..74561f9ef 100644 --- a/Dalamud/Interface/ColorHelpers.cs +++ b/Dalamud/Interface/ColorHelpers.cs @@ -259,6 +259,15 @@ public static class ColorHelpers hsv.A -= amount; return HsvToRgb(hsv); } + + /// + /// Set alpha of a color. + /// + /// The color. + /// The alpha value to set. + /// The color with the set alpha value. + public static Vector4 WithAlpha(this Vector4 color, float alpha) + => color with { W = alpha }; /// /// Fade a color. diff --git a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs index 116b04bd2..719f470b8 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.IconButton.cs @@ -209,4 +209,25 @@ public static partial class ImGuiComponents return button; } + + /// + /// Get width of IconButtonWithText component. + /// + /// Icon to use. + /// Text to use. + /// Width. + internal static float GetIconButtonWithTextWidth(FontAwesomeIcon icon, string text) + { + ImGui.PushFont(UiBuilder.IconFont); + var iconSize = ImGui.CalcTextSize(icon.ToIconString()); + ImGui.PopFont(); + + var textSize = ImGui.CalcTextSize(text); + var dl = ImGui.GetWindowDrawList(); + var cursor = ImGui.GetCursorScreenPos(); + + var iconPadding = 3 * ImGuiHelpers.GlobalScale; + + return iconSize.X + textSize.X + (ImGui.GetStyle().FramePadding.X * 2) + iconPadding; + } } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index a7b1e80b5..fc11f3f4b 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -94,8 +94,7 @@ internal class DalamudInterface : IDisposable, IServiceType { var interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); - - this.changelogWindow = new ChangelogWindow() { IsOpen = false }; + this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; this.dataWindow = new DataWindow() { IsOpen = false }; @@ -108,6 +107,7 @@ internal class DalamudInterface : IDisposable, IServiceType this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; this.titleScreenMenuWindow = new TitleScreenMenuWindow() { IsOpen = false }; + this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false }; diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index cd4618f24..1c0d3fa02 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,13 +1,18 @@ -using System; using System.IO; +using System.Linq; using System.Numerics; +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Internal; using Dalamud.Utility; using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows; @@ -16,141 +21,349 @@ namespace Dalamud.Interface.Internal.Windows; /// internal sealed class ChangelogWindow : Window, IDisposable { - /// - /// Whether the latest update warrants a changelog window. - /// - public const string WarrantsChangelogForMajorMinor = "7.4."; - + private const string WarrantsChangelogForMajorMinor = "9.0."; + private const string ChangeLog = - @"• Updated Dalamud for compatibility with Patch 6.3 -• Made things more speedy by updating to .NET 7 - -If you note any issues or need help, please check the FAQ, and reach out on our Discord if you need help. -Thanks and have fun!"; - - private const string UpdatePluginsInfo = - @"• All of your plugins were disabled automatically, due to this update. This is normal. -• Open the plugin installer, then click 'update plugins'. Updated plugins should update and then re-enable themselves. - => Please keep in mind that not all of your plugins may already be updated for the new version. - => If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."; - - private readonly string assemblyVersion = Util.AssemblyVersion; - + @"• Updated Dalamud for compatibility with Patch 6.5 +• A lot of behind-the-scenes changes to make Dalamud and plugins more stable and reliable +• Added plugin collections, allowing you to create lists of plugins that can be enabled or disabled together +• Plugins can now add tooltips and interaction to the server info bar +• The Dalamud/plugin installer UI has been refreshed +"; + + private readonly TitleScreenMenuWindow tsmWindow; private readonly IDalamudTextureWrap logoTexture; + + private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) + { + Point1 = Vector2.Zero, + Point2 = new Vector2(2f), + }; + + private readonly InOutCubic bodyFade = new(TimeSpan.FromSeconds(1f)) + { + Point1 = Vector2.Zero, + Point2 = Vector2.One, + }; + + private IDalamudTextureWrap? apiBumpExplainerTexture; + private GameFontHandle? bannerFont; + + private State state = State.WindowFadeIn; + private bool needFadeRestart = false; + /// /// Initializes a new instance of the class. /// - public ChangelogWindow() - : base("What's new in Dalamud?", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoResize) + /// TSM window. + public ChangelogWindow(TitleScreenMenuWindow tsmWindow) + : base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true) { + this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; - this.Size = new Vector2(885, 463); - this.SizeCondition = ImGuiCond.Appearing; - this.logoTexture = Service.Get().Logo; + + // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch + if (WarrantsChangelog()) + this.MakeFont(); + } + + private enum State + { + WindowFadeIn, + ExplainerIntro, + ExplainerApiBump, + Links, + } + + /// + /// Check if a changelog should be shown. + /// + /// True if a changelog should be shown. + public static bool WarrantsChangelog() + { + var configuration = Service.Get(); + var pm = Service.GetNullable(); + var pmWantsChangelog = pm?.InstalledPlugins.Any() ?? true; + return (string.IsNullOrEmpty(configuration.LastChangelogMajorMinor) || + (!WarrantsChangelogForMajorMinor.StartsWith(configuration.LastChangelogMajorMinor) && + Util.AssemblyVersion.StartsWith(WarrantsChangelogForMajorMinor))) && pmWantsChangelog; + } + + /// + public override void OnOpen() + { + Service.Get().SetCreditsDarkeningAnimation(true); + this.tsmWindow.AllowDrawing = false; + + this.MakeFont(); + + this.state = State.WindowFadeIn; + this.windowFade.Reset(); + this.bodyFade.Reset(); + this.needFadeRestart = true; + + if (this.apiBumpExplainerTexture == null) + { + var dalamud = Service.Get(); + var tm = Service.Get(); + this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png"))) + ?? throw new Exception("Could not load api bump explainer."); + } + + base.OnOpen(); + } + + /// + public override void OnClose() + { + base.OnClose(); + + this.tsmWindow.AllowDrawing = true; + Service.Get().SetCreditsDarkeningAnimation(false); + } + + /// + public override void PreDraw() + { + ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 10f); + + base.PreDraw(); + + if (this.needFadeRestart) + { + this.windowFade.Restart(); + this.needFadeRestart = false; + } + + this.windowFade.Update(); + ImGui.SetNextWindowBgAlpha(Math.Clamp(this.windowFade.EasedPoint.X, 0, 0.9f)); + + this.Size = new Vector2(900, 400); + this.SizeCondition = ImGuiCond.Always; + + // Center the window on the main viewport + var viewportSize = ImGuiHelpers.MainViewport.Size; + var windowSize = this.Size!.Value * ImGuiHelpers.GlobalScale; + ImGui.SetNextWindowPos(new Vector2(viewportSize.X / 2 - windowSize.X / 2, viewportSize.Y / 2 - windowSize.Y / 2)); + } + + /// + public override void PostDraw() + { + ImGui.PopStyleVar(3); + base.PostDraw(); } /// public override void Draw() { - ImGui.Text($"Dalamud has been updated to version D{this.assemblyVersion}."); - - ImGuiHelpers.ScaledDummy(10); - - ImGui.Text("The following changes were introduced:"); - + void Dismiss() + { + var configuration = Service.Get(); + configuration.LastChangelogMajorMinor = WarrantsChangelogForMajorMinor; + configuration.QueueSave(); + } + + var windowSize = ImGui.GetWindowSize(); + + var dummySize = 10 * ImGuiHelpers.GlobalScale; + ImGui.Dummy(new Vector2(dummySize)); ImGui.SameLine(); - ImGuiHelpers.ScaledDummy(0); - var imgCursor = ImGui.GetCursorPos(); - - ImGui.TextWrapped(ChangeLog); - - ImGuiHelpers.ScaledDummy(5); - - ImGui.TextColored(ImGuiColors.DalamudRed, " !!! ATTENTION !!!"); - - ImGui.TextWrapped(UpdatePluginsInfo); - - ImGuiHelpers.ScaledDummy(10); - - // ImGui.Text("Thank you for using our tools!"); - - // ImGuiHelpers.ScaledDummy(10); - - ImGui.PushFont(UiBuilder.IconFont); - - if (ImGui.Button(FontAwesomeIcon.Download.ToIconString())) + + var logoContainerSize = new Vector2(this.Size!.Value.X * 0.2f - dummySize, this.Size!.Value.Y); + using (var child = ImRaii.Child("###logoContainer", logoContainerSize, false)) { - Service.Get().OpenPluginInstaller(); - } + if (!child) + return; - if (ImGui.IsItemHovered()) - { - ImGui.PopFont(); - ImGui.SetTooltip("Open Plugin Installer"); - ImGui.PushFont(UiBuilder.IconFont); + var logoSize = new Vector2(logoContainerSize.X); + + // Center the logo in the container + ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2)); + + using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) + { + ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); + } } - + ImGui.SameLine(); - - if (ImGui.Button(FontAwesomeIcon.LaughBeam.ToIconString())) - { - Util.OpenLink("https://discord.gg/3NMcUV5"); - } - - if (ImGui.IsItemHovered()) - { - ImGui.PopFont(); - ImGui.SetTooltip("Join our Discord server"); - ImGui.PushFont(UiBuilder.IconFont); - } - + ImGui.Dummy(new Vector2(dummySize)); ImGui.SameLine(); - - if (ImGui.Button(FontAwesomeIcon.Globe.ToIconString())) + + using (var child = ImRaii.Child("###textContainer", new Vector2((this.Size!.Value.X * 0.8f) - dummySize * 4, this.Size!.Value.Y), false)) { - Util.OpenLink("https://goatcorp.github.io/faq/"); + if (!child) + return; + + ImGuiHelpers.ScaledDummy(20); + + using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) + { + using var font = ImRaii.PushFont(this.bannerFont!.ImFont); + + switch (this.state) + { + case State.WindowFadeIn: + case State.ExplainerIntro: + ImGuiHelpers.CenteredText("New And Improved"); + break; + + case State.ExplainerApiBump: + ImGuiHelpers.CenteredText("Plugin Updates"); + break; + + case State.Links: + ImGuiHelpers.CenteredText("Enjoy!"); + break; + } + } + + ImGuiHelpers.ScaledDummy(8); + + if (this.state == State.WindowFadeIn && this.windowFade.EasedPoint.X > 1.5f) + { + this.state = State.ExplainerIntro; + this.bodyFade.Restart(); + } + + this.bodyFade.Update(); + using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.bodyFade.EasedPoint.X, 0, 1f))) + { + void DrawNextButton(State nextState) + { + // Draw big, centered next button at the bottom of the window + var buttonHeight = 30 * ImGuiHelpers.GlobalScale; + var buttonText = "Next"; + var buttonWidth = ImGui.CalcTextSize(buttonText).X + 40 * ImGuiHelpers.GlobalScale; + ImGui.SetCursorPosY(windowSize.Y - buttonHeight - (20 * ImGuiHelpers.GlobalScale)); + ImGuiHelpers.CenterCursorFor((int)buttonWidth); + + if (ImGui.Button(buttonText, new Vector2(buttonWidth, buttonHeight))) + { + this.state = nextState; + this.bodyFade.Restart(); + } + } + + switch (this.state) + { + case State.WindowFadeIn: + case State.ExplainerIntro: + ImGui.TextWrapped($"Welcome to Dalamud v{Util.AssemblyVersion}!"); + ImGuiHelpers.ScaledDummy(5); + ImGui.TextWrapped(ChangeLog); + + DrawNextButton(State.ExplainerApiBump); + break; + + case State.ExplainerApiBump: + ImGui.TextWrapped("Take care! Due to changes in this patch, all of your plugins need to be updated and were disabled automatically."); + ImGui.TextWrapped("This is normal and required for major game updates."); + ImGuiHelpers.ScaledDummy(5); + ImGui.TextWrapped("To update your plugins, open the plugin installer and click 'update plugins'. Updated plugins should update and then re-enable themselves."); + ImGuiHelpers.ScaledDummy(5); + ImGui.TextWrapped("Please keep in mind that not all of your plugins may already be updated for the new version."); + ImGui.TextWrapped("If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."); + + ImGuiHelpers.ScaledDummy(15); + + ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width); + ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size); + + DrawNextButton(State.Links); + break; + + case State.Links: + ImGui.TextWrapped("If you note any issues or need help, please check the FAQ, and reach out on our Discord if you need help."); + ImGui.TextWrapped("Enjoy your time with the game and Dalamud!"); + + ImGuiHelpers.ScaledDummy(45); + + bool CenteredIconButton(FontAwesomeIcon icon, string text) + { + var buttonWidth = ImGuiComponents.GetIconButtonWithTextWidth(icon, text); + ImGuiHelpers.CenterCursorFor((int)buttonWidth); + return ImGuiComponents.IconButtonWithText(icon, text); + } + + if (CenteredIconButton(FontAwesomeIcon.Download, "Open Plugin Installer")) + { + Service.Get().OpenPluginInstaller(); + this.IsOpen = false; + Dismiss(); + } + + ImGuiHelpers.ScaledDummy(5); + + ImGuiHelpers.CenterCursorFor( + (int)(ImGuiComponents.GetIconButtonWithTextWidth(FontAwesomeIcon.Globe, "See the FAQ") + + ImGuiComponents.GetIconButtonWithTextWidth(FontAwesomeIcon.LaughBeam, "Join our Discord server") + + (5 * ImGuiHelpers.GlobalScale) + + (ImGui.GetStyle().ItemSpacing.X * 4))); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Globe, "See the FAQ")) + { + Util.OpenLink("https://goatcorp.github.io/faq/"); + } + + ImGui.SameLine(); + ImGuiHelpers.ScaledDummy(5); + ImGui.SameLine(); + + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.LaughBeam, "Join our Discord server")) + { + Util.OpenLink("https://discord.gg/3NMcUV5"); + } + + ImGuiHelpers.ScaledDummy(5); + + if (CenteredIconButton(FontAwesomeIcon.Heart, "Support what we care about")) + { + Util.OpenLink("https://goatcorp.github.io/faq/support"); + } + + var buttonHeight = 30 * ImGuiHelpers.GlobalScale; + var buttonText = "Close"; + var buttonWidth = ImGui.CalcTextSize(buttonText).X + 40 * ImGuiHelpers.GlobalScale; + ImGui.SetCursorPosY(windowSize.Y - buttonHeight - (20 * ImGuiHelpers.GlobalScale)); + ImGuiHelpers.CenterCursorFor((int)buttonWidth); + + if (ImGui.Button(buttonText, new Vector2(buttonWidth, buttonHeight))) + { + this.IsOpen = false; + Dismiss(); + } + + break; + } + } + + // Draw close button in the top right corner + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 100f); + var btnAlpha = Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f); + ImGui.PushStyleColor(ImGuiCol.Button, ImGuiColors.DalamudRed.WithAlpha(btnAlpha).Desaturate(0.3f)); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudWhite.WithAlpha(btnAlpha)); + + var childSize = ImGui.GetWindowSize(); + var closeButtonSize = 15 * ImGuiHelpers.GlobalScale; + ImGui.SetCursorPos(new Vector2(childSize.X - closeButtonSize - (5 * ImGuiHelpers.GlobalScale), 10 * ImGuiHelpers.GlobalScale)); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) + { + Dismiss(); + this.IsOpen = false; + } + + ImGui.PopStyleColor(2); + ImGui.PopStyleVar(); + + if (ImGui.IsItemHovered()) + ImGui.SetTooltip("I don't care about this"); } - - if (ImGui.IsItemHovered()) - { - ImGui.PopFont(); - ImGui.SetTooltip("See the FAQ"); - ImGui.PushFont(UiBuilder.IconFont); - } - - ImGui.SameLine(); - - if (ImGui.Button(FontAwesomeIcon.Heart.ToIconString())) - { - Util.OpenLink("https://goatcorp.github.io/faq/support"); - } - - if (ImGui.IsItemHovered()) - { - ImGui.PopFont(); - ImGui.SetTooltip("Support what we care about"); - ImGui.PushFont(UiBuilder.IconFont); - } - - ImGui.PopFont(); - - ImGui.SameLine(); - ImGuiHelpers.ScaledDummy(20, 0); - ImGui.SameLine(); - - if (ImGui.Button("Close")) - { - this.IsOpen = false; - } - - imgCursor.X += 750; - imgCursor.Y -= 30; - ImGui.SetCursorPos(imgCursor); - - ImGui.Image(this.logoTexture.ImGuiHandle, new Vector2(100)); } /// @@ -160,4 +373,13 @@ Thanks and have fun!"; { this.logoTexture.Dispose(); } + + private void MakeFont() + { + if (this.bannerFont == null) + { + var gfm = Service.Get(); + this.bannerFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); + } + } } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index e77a3db4e..a4ad62f4f 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -63,13 +63,18 @@ internal class TitleScreenMenuWindow : Window, IDisposable var framework = Service.Get(); framework.Update += this.FrameworkOnUpdate; } - + private enum State { Hide, Show, FadeOut, } + + /// + /// Gets or sets a value indicating whether drawing is allowed. + /// + public bool AllowDrawing { get; set; } = true; /// public override void PreDraw() @@ -97,6 +102,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public override void Draw() { + if (!this.AllowDrawing) + return; + var scale = ImGui.GetIO().FontGlobalScale; var entries = Service.Get().Entries .OrderByDescending(x => x.IsInternal) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index ad424f59a..f0914bb21 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -257,13 +257,7 @@ public abstract class Window if (hasNamespace) ImGui.PushID(this.Namespace); - - this.PreDraw(); - this.ApplyConditionals(); - - if (this.ForceMainWindow) - ImGuiHelpers.ForceNextWindowMainViewport(); - + if (this.internalLastIsOpen != this.internalIsOpen && this.internalIsOpen) { this.internalLastIsOpen = this.internalIsOpen; @@ -272,6 +266,12 @@ public abstract class Window if (doSoundEffects && !this.DisableWindowSounds) UIModule.PlaySound(this.OnOpenSfxId, 0, 0, 0); } + this.PreDraw(); + this.ApplyConditionals(); + + if (this.ForceMainWindow) + ImGuiHelpers.ForceNextWindowMainViewport(); + var wasFocused = this.IsFocused; if (wasFocused) { From 2cfd96a1fe17f11689a99d15127311c443ec03fe Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:29:41 -0700 Subject: [PATCH 219/585] Add Dalamud Verified (#1441) --- .../PluginInstaller/PluginInstallerWindow.cs | 109 +++++++++++++++++- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 7ff2f61e0..b86a2e523 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1,8 +1,8 @@ -using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Drawing; using System.IO; using System.Linq; using System.Numerics; @@ -28,7 +28,6 @@ using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Support; using Dalamud.Utility; using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -53,6 +52,8 @@ internal class PluginInstallerWindow : Window, IDisposable private readonly ProfileManagerWidget profileManagerWidget; + private readonly Stopwatch tooltipFadeInStopwatch = new(); + private DalamudChangelogManager? dalamudChangelogManager; private Task? dalamudChangelogRefreshTask; private CancellationTokenSource? dalamudChangelogRefreshTaskCts; @@ -114,6 +115,8 @@ internal class PluginInstallerWindow : Window, IDisposable private LoadingIndicatorKind loadingIndicatorKind = LoadingIndicatorKind.Unknown; + private string verifiedCheckmarkHoveredPlugin = string.Empty; + /// /// Initializes a new instance of the class. /// @@ -1639,7 +1642,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushStyleColor(ImGuiCol.ButtonActive, new Vector4(0.5f, 0.5f, 0.5f, 0.35f)); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0); - if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetWindowWidth() - (ImGuiHelpers.GlobalScale * 35), sectionSize))) + if (ImGui.Button($"###plugin{index}CollapsibleBtn", new Vector2(ImGui.GetContentRegionAvail().X, sectionSize))) { if (isOpen) { @@ -1718,6 +1721,29 @@ internal class PluginInstallerWindow : Window, IDisposable // Name ImGui.TextUnformatted(label); + + // Verified Checkmark, don't show for dev plugins + if (plugin is { IsDev: false }) + { + ImGui.SameLine(); + ImGui.Text(" "); + ImGui.SameLine(); + + var verifiedOutlineColor = KnownColor.White.Vector() with { W = 0.75f }; + var verifiedIconColor = KnownColor.RoyalBlue.Vector() with { W = 0.75f }; + var unverifiedIconColor = KnownColor.Orange.Vector() with { W = 0.75f }; + + if (!isThirdParty) + { + this.DrawFontawesomeIconOutlined(FontAwesomeIcon.CheckCircle, verifiedOutlineColor, verifiedIconColor); + this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_VerifiedTooltip); + } + else + { + this.DrawFontawesomeIconOutlined(FontAwesomeIcon.Circle, verifiedOutlineColor, unverifiedIconColor); + this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_UnverifiedTooltip); + } + } // Download count var downloadCountText = manifest.DownloadCount > 0 @@ -3128,6 +3154,70 @@ internal class PluginInstallerWindow : Window, IDisposable this.UpdateCategoriesOnSearchChange(); } + private void DrawFontawesomeIconOutlined(FontAwesomeIcon icon, Vector4 outline, Vector4 iconColor) + { + var positionOffset = ImGuiHelpers.ScaledVector2(0.0f, 1.0f); + var cursorStart = ImGui.GetCursorPos() + positionOffset; + ImGui.PushFont(UiBuilder.IconFont); + + ImGui.PushStyleColor(ImGuiCol.Text, outline); + foreach (var x in Enumerable.Range(-1, 3)) + { + foreach (var y in Enumerable.Range(-1, 3)) + { + if (x is 0 && y is 0) continue; + + ImGui.SetCursorPos(cursorStart + new Vector2(x, y)); + ImGui.Text(icon.ToIconString()); + } + } + + ImGui.PopStyleColor(); + + ImGui.PushStyleColor(ImGuiCol.Text, iconColor); + ImGui.SetCursorPos(cursorStart); + ImGui.Text(icon.ToIconString()); + ImGui.PopStyleColor(); + + ImGui.PopFont(); + + ImGui.SetCursorPos(ImGui.GetCursorPos() - positionOffset); + } + + // Animates a tooltip when hovering over the ImGui Item before this call. + private void VerifiedCheckmarkFadeTooltip(string source, string tooltip) + { + const float fadeInStartDelay = 500.0f; + const float fadeInTime = 250.0f; + + var isHoveringSameItem = this.verifiedCheckmarkHoveredPlugin == source; + + // If we just started a hover, start the timer + if (ImGui.IsItemHovered() && !this.tooltipFadeInStopwatch.IsRunning) + { + this.verifiedCheckmarkHoveredPlugin = source; + this.tooltipFadeInStopwatch.Restart(); + } + + // If we were last hovering this plugins item and are no longer hovered over that item, reset the timer + if (!ImGui.IsItemHovered() && isHoveringSameItem) + { + this.verifiedCheckmarkHoveredPlugin = string.Empty; + this.tooltipFadeInStopwatch.Stop(); + } + + // If we have been hovering this item for > fadeInStartDelay milliseconds, fade in tooltip over fadeInTime milliseconds + if (ImGui.IsItemHovered() && isHoveringSameItem && this.tooltipFadeInStopwatch.ElapsedMilliseconds >= fadeInStartDelay) + { + var fadePercent = Math.Clamp((this.tooltipFadeInStopwatch.ElapsedMilliseconds - fadeInStartDelay) / fadeInTime, 0.0f, 1.0f); + + ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.Text] with { W = fadePercent }); + ImGui.PushStyleColor(ImGuiCol.FrameBg, ImGui.GetStyle().Colors[(int)ImGuiCol.FrameBg] with { W = fadePercent }); + ImGui.SetTooltip(tooltip); + ImGui.PopStyleColor(2); + } + } + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Disregard here")] [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Locs")] internal static class Locs @@ -3513,5 +3603,18 @@ internal class PluginInstallerWindow : Window, IDisposable Loc.Localize("InstallerProfilesRemoveFromAll", "Remove from all collections"); #endregion + + #region VerifiedCheckmark + + public static string VerifiedCheckmark_VerifiedTooltip => + Loc.Localize("VerifiedCheckmarkVerifiedTooltip", "This plugin has been reviewed by the Dalamud team.\n" + + "It follows our technical and safety criteria, and adheres to our guidelines."); + + public static string VerifiedCheckmark_UnverifiedTooltip => + Loc.Localize("VerifiedCheckmarkUnverifiedTooltip", "This plugin has not been reviewed by the Dalamud team.\n" + + "We cannot take any responsibility for custom plugins and repositories.\n" + + "Please make absolutely sure that you only install plugins from developers you trust."); + + #endregion } } From eb835548d3352a4e53783825a84f25331078cd69 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 3 Oct 2023 13:35:35 -0700 Subject: [PATCH 220/585] Restore Create (#1449) --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 66cf8c7cc..764eec988 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -417,7 +417,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private AtkTextNode* MakeNode(uint nodeId) { - var newTextNode = AtkUldManager.CreateAtkTextNode(); + var newTextNode = IMemorySpace.GetUISpace()->Create(); // AtkUldManager.CreateAtkTextNode(); if (newTextNode == null) { Log.Debug("Failed to allocate memory for AtkTextNode"); @@ -443,15 +443,16 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar newTextNode->TextColor = new ByteColor { R = 255, G = 255, B = 255, A = 255 }; newTextNode->EdgeColor = new ByteColor { R = 142, G = 106, B = 12, A = 255 }; - // Memory is filled with random data after being created, zero out some things to avoid issues. - newTextNode->UnkPtr_1 = null; - newTextNode->SelectStart = 0; - newTextNode->SelectEnd = 0; - newTextNode->FontCacheHandle = 0; - newTextNode->CharSpacing = 0; - newTextNode->BackgroundColor = new ByteColor { R = 0, G = 0, B = 0, A = 0 }; - newTextNode->TextId = 0; - newTextNode->SheetType = 0; + // ICreatable was restored, this may be necessary if AtkUldManager.CreateAtkTextNode(); is used instead of Create + // // Memory is filled with random data after being created, zero out some things to avoid issues. + // newTextNode->UnkPtr_1 = null; + // newTextNode->SelectStart = 0; + // newTextNode->SelectEnd = 0; + // newTextNode->FontCacheHandle = 0; + // newTextNode->CharSpacing = 0; + // newTextNode->BackgroundColor = new ByteColor { R = 0, G = 0, B = 0, A = 0 }; + // newTextNode->TextId = 0; + // newTextNode->SheetType = 0; return newTextNode; } From e6608cb4a7e941be6fb0d7a766c313d65355e59d Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 3 Oct 2023 22:51:05 +0200 Subject: [PATCH 221/585] chore: tweak new checkmarks a little --- .../PluginInstaller/PluginInstallerWindow.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index b86a2e523..a364008cb 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Game.Command; +using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Notifications; @@ -53,6 +54,11 @@ internal class PluginInstallerWindow : Window, IDisposable private readonly ProfileManagerWidget profileManagerWidget; private readonly Stopwatch tooltipFadeInStopwatch = new(); + private readonly InOutCubic tooltipFadeEasing = new(TimeSpan.FromSeconds(0.2f)) + { + Point1 = Vector2.Zero, + Point2 = Vector2.One, + }; private DalamudChangelogManager? dalamudChangelogManager; private Task? dalamudChangelogRefreshTask; @@ -1704,10 +1710,12 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.Image(this.imageCache.OutdatedInstallableIcon.ImGuiHandle, iconSize); else if (pluginDisabled) ImGui.Image(this.imageCache.DisabledIcon.ImGuiHandle, iconSize); + /* NOTE: Replaced by the checkmarks for now, let's see if that is fine else if (isLoaded && isThirdParty) ImGui.Image(this.imageCache.ThirdInstalledIcon.ImGuiHandle, iconSize); else if (isThirdParty) ImGui.Image(this.imageCache.ThirdIcon.ImGuiHandle, iconSize); + */ else if (isLoaded) ImGui.Image(this.imageCache.InstalledIcon.ImGuiHandle, iconSize); else @@ -1723,7 +1731,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.TextUnformatted(label); // Verified Checkmark, don't show for dev plugins - if (plugin is { IsDev: false }) + if (plugin is null or { IsDev: false }) { ImGui.SameLine(); ImGui.Text(" "); @@ -1740,7 +1748,7 @@ internal class PluginInstallerWindow : Window, IDisposable } else { - this.DrawFontawesomeIconOutlined(FontAwesomeIcon.Circle, verifiedOutlineColor, unverifiedIconColor); + this.DrawFontawesomeIconOutlined(FontAwesomeIcon.ExclamationCircle, verifiedOutlineColor, unverifiedIconColor); this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_UnverifiedTooltip); } } @@ -3187,8 +3195,7 @@ internal class PluginInstallerWindow : Window, IDisposable // Animates a tooltip when hovering over the ImGui Item before this call. private void VerifiedCheckmarkFadeTooltip(string source, string tooltip) { - const float fadeInStartDelay = 500.0f; - const float fadeInTime = 250.0f; + const float fadeInStartDelay = 250.0f; var isHoveringSameItem = this.verifiedCheckmarkHoveredPlugin == source; @@ -3204,13 +3211,17 @@ internal class PluginInstallerWindow : Window, IDisposable { this.verifiedCheckmarkHoveredPlugin = string.Empty; this.tooltipFadeInStopwatch.Stop(); + this.tooltipFadeEasing.Reset(); } // If we have been hovering this item for > fadeInStartDelay milliseconds, fade in tooltip over fadeInTime milliseconds if (ImGui.IsItemHovered() && isHoveringSameItem && this.tooltipFadeInStopwatch.ElapsedMilliseconds >= fadeInStartDelay) { - var fadePercent = Math.Clamp((this.tooltipFadeInStopwatch.ElapsedMilliseconds - fadeInStartDelay) / fadeInTime, 0.0f, 1.0f); + if (!this.tooltipFadeEasing.IsRunning) + this.tooltipFadeEasing.Start(); + this.tooltipFadeEasing.Update(); + var fadePercent = this.tooltipFadeEasing.EasedPoint.X; ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int)ImGuiCol.Text] with { W = fadePercent }); ImGui.PushStyleColor(ImGuiCol.FrameBg, ImGui.GetStyle().Colors[(int)ImGuiCol.FrameBg] with { W = fadePercent }); ImGui.SetTooltip(tooltip); From 0cc9de9fab6340b342e43c4ab60df3d7067371ba Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 3 Oct 2023 23:16:17 +0200 Subject: [PATCH 222/585] fix: make BaseAddressResolver public again --- Dalamud/Game/BaseAddressResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index cd1ef8fd2..814e5ed09 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game; /// /// Base memory address resolver. /// -internal abstract class BaseAddressResolver +public abstract class BaseAddressResolver { /// /// Gets a list of memory addresses that were found, to list in /xldata. From daeec9a13fa14006964c3f20e8b0fa85f1ddb8fa Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 3 Oct 2023 23:19:44 +0200 Subject: [PATCH 223/585] fix: only create load context for plugins that are actually supposed to load --- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 104 ++++++++++--------- 1 file changed, 55 insertions(+), 49 deletions(-) diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 57bea0f57..d36cb585d 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -64,54 +64,6 @@ internal class LocalPlugin : IDisposable this.DllFile = dllFile; this.State = PluginState.Unloaded; - try - { - this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig); - } - catch (InvalidOperationException ex) - { - Log.Error(ex, "Loader.CreateFromAssemblyFile() failed"); - this.State = PluginState.DependencyResolutionFailed; - throw; - } - - try - { - this.pluginAssembly = this.loader.LoadDefaultAssembly(); - } - catch (Exception ex) - { - this.pluginAssembly = null; - this.pluginType = null; - this.loader.Dispose(); - - Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}"); - throw new InvalidPluginException(this.DllFile); - } - - try - { - this.pluginType = this.pluginAssembly.GetTypes().FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin))); - } - catch (ReflectionTypeLoadException ex) - { - Log.Error(ex, $"Could not load one or more types when searching for IDalamudPlugin: {this.DllFile.FullName}"); - // Something blew up when parsing types, but we still want to look for IDalamudPlugin. Let Load() handle the error. - this.pluginType = ex.Types.FirstOrDefault(type => type != null && type.IsAssignableTo(typeof(IDalamudPlugin))); - } - - if (this.pluginType == default) - { - this.pluginAssembly = null; - this.pluginType = null; - this.loader.Dispose(); - - Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}"); - throw new InvalidPluginException(this.DllFile); - } - - var assemblyVersion = this.pluginAssembly.GetName().Version; - // Although it is conditionally used here, we need to set the initial value regardless. this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile); @@ -123,7 +75,7 @@ internal class LocalPlugin : IDisposable Author = "developer", Name = Path.GetFileNameWithoutExtension(this.DllFile.Name), InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name), - AssemblyVersion = assemblyVersion ?? new Version("1.0.0.0"), + AssemblyVersion = new Version("1.0.0.0"), Description = string.Empty, ApplicableVersion = GameVersion.Any, DalamudApiLevel = PluginManager.DalamudApiLevel, @@ -410,6 +362,8 @@ internal class LocalPlugin : IDisposable this.State = PluginState.Loading; Log.Information($"Loading {this.DllFile.Name}"); + + this.EnsureLoader(); if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll"))) @@ -700,4 +654,56 @@ internal class LocalPlugin : IDisposable config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName()); config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); } + + private void EnsureLoader() + { + if (this.loader != null) + return; + + try + { + this.loader = PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig); + } + catch (InvalidOperationException ex) + { + Log.Error(ex, "Loader.CreateFromAssemblyFile() failed"); + this.State = PluginState.DependencyResolutionFailed; + throw; + } + + try + { + this.pluginAssembly = this.loader.LoadDefaultAssembly(); + } + catch (Exception ex) + { + this.pluginAssembly = null; + this.pluginType = null; + this.loader.Dispose(); + + Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}"); + throw new InvalidPluginException(this.DllFile); + } + + try + { + this.pluginType = this.pluginAssembly.GetTypes().FirstOrDefault(type => type.IsAssignableTo(typeof(IDalamudPlugin))); + } + catch (ReflectionTypeLoadException ex) + { + Log.Error(ex, $"Could not load one or more types when searching for IDalamudPlugin: {this.DllFile.FullName}"); + // Something blew up when parsing types, but we still want to look for IDalamudPlugin. Let Load() handle the error. + this.pluginType = ex.Types.FirstOrDefault(type => type != null && type.IsAssignableTo(typeof(IDalamudPlugin))); + } + + if (this.pluginType == default) + { + this.pluginAssembly = null; + this.pluginType = null; + this.loader.Dispose(); + + Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}"); + throw new InvalidPluginException(this.DllFile); + } + } } From 4487ef85f420c31990ff047f7580e2cabe8aad81 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 3 Oct 2023 23:37:02 +0200 Subject: [PATCH 224/585] fix: actually don't attempt to load plugins without manifests any longer --- Dalamud/Plugin/Internal/PluginManager.cs | 39 +++++++++++++++++--- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 27 +------------- Dalamud/Plugin/Internal/Types/PluginDef.cs | 4 +- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index dc658792c..ac808df89 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -444,6 +444,11 @@ internal partial class PluginManager : IDisposable, IServiceType continue; var manifest = LocalPluginManifest.Load(manifestFile); + if (manifest == null) + { + Log.Error("Manifest for plugin at {Path} was null", dllFile.FullName); + continue; + } if (manifest.IsTestingExclusive && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != manifest.InternalName)) this.configuration.PluginTestingOptIns.Add(new PluginTestingOptIn(manifest.InternalName)); @@ -490,9 +495,20 @@ internal partial class PluginManager : IDisposable, IServiceType { try { - // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. + // Manifests are now required for devPlugins var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); - var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null; + if (!manifestFile.Exists) + { + Log.Information("DLL at {DllPath} has no manifest, this is no longer valid", dllFile.FullName); + continue; + } + + var manifest = LocalPluginManifest.Load(manifestFile); + if (manifest == null) + { + Log.Information("Could not deserialize manifest for DLL at {DllPath}", dllFile.FullName); + continue; + } if (manifest != null && manifest.InternalName.IsNullOrEmpty()) { @@ -721,9 +737,20 @@ internal partial class PluginManager : IDisposable, IServiceType continue; } - // Manifests are not required for devPlugins. the Plugin type will handle any null manifests. + // Manifests are now required for devPlugins var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); - var manifest = manifestFile.Exists ? LocalPluginManifest.Load(manifestFile) : null; + if (!manifestFile.Exists) + { + Log.Information("DLL at {DllPath} has no manifest, this is no longer valid", dllFile.FullName); + continue; + } + + var manifest = LocalPluginManifest.Load(manifestFile); + if (manifest == null) + { + Log.Information("Could not deserialize manifest for DLL at {DllPath}", dllFile.FullName); + continue; + } try { @@ -738,7 +765,7 @@ internal partial class PluginManager : IDisposable, IServiceType } catch (Exception ex) { - Log.Error(ex, $"During devPlugin scan, an unexpected error occurred"); + Log.Error(ex, "During devPlugin scan, an unexpected error occurred"); } } @@ -1274,7 +1301,7 @@ internal partial class PluginManager : IDisposable, IServiceType /// If this plugin is being loaded at boot. /// Don't load the plugin, just don't do it. /// The loaded plugin. - private async Task LoadPluginAsync(FileInfo dllFile, LocalPluginManifest? manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false) + private async Task LoadPluginAsync(FileInfo dllFile, LocalPluginManifest manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false) { var name = manifest?.Name ?? dllFile.Name; var loadPlugin = !doNotLoad; diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index d36cb585d..5d132fd9c 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -51,7 +51,7 @@ internal class LocalPlugin : IDisposable /// /// Path to the DLL file. /// The plugin manifest. - public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest) + public LocalPlugin(FileInfo dllFile, LocalPluginManifest manifest) { if (dllFile.Name == "FFXIVClientStructs.Generators.dll") { @@ -66,30 +66,7 @@ internal class LocalPlugin : IDisposable // Although it is conditionally used here, we need to set the initial value regardless. this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile); - - // If the parameter manifest was null - if (manifest == null) - { - this.manifest = new LocalPluginManifest() - { - Author = "developer", - Name = Path.GetFileNameWithoutExtension(this.DllFile.Name), - InternalName = Path.GetFileNameWithoutExtension(this.DllFile.Name), - AssemblyVersion = new Version("1.0.0.0"), - Description = string.Empty, - ApplicableVersion = GameVersion.Any, - DalamudApiLevel = PluginManager.DalamudApiLevel, - IsHide = false, - }; - - // Save the manifest to disk so there won't be any problems later. - // We'll update the name property after it can be retrieved from the instance. - this.manifest.Save(this.manifestFile, "manifest was null"); - } - else - { - this.manifest = manifest; - } + this.manifest = manifest; var needsSaveDueToLegacyFiles = false; diff --git a/Dalamud/Plugin/Internal/Types/PluginDef.cs b/Dalamud/Plugin/Internal/Types/PluginDef.cs index 049e58d7d..25cd82423 100644 --- a/Dalamud/Plugin/Internal/Types/PluginDef.cs +++ b/Dalamud/Plugin/Internal/Types/PluginDef.cs @@ -15,7 +15,7 @@ internal struct PluginDef /// plugin dll file. /// plugin manifest. /// plugin dev indicator. - public PluginDef(FileInfo dllFile, LocalPluginManifest? manifest, bool isDev) + public PluginDef(FileInfo dllFile, LocalPluginManifest manifest, bool isDev) { this.DllFile = dllFile; this.Manifest = manifest; @@ -30,7 +30,7 @@ internal struct PluginDef /// /// Gets plugin manifest. /// - public LocalPluginManifest? Manifest { get; init; } + public LocalPluginManifest Manifest { get; init; } /// /// Gets a value indicating whether plugin is a dev plugin. From 38e4fd26733af56e4a77c989af7c93ef948d6cf4 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 4 Oct 2023 00:10:04 +0200 Subject: [PATCH 225/585] fix: synchronize all writes to ReliableFileStorage --- Dalamud/Storage/ReliableFileStorage.cs | 54 +++++++++++++------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 6bc6cabcc..9feb17c0d 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -1,10 +1,8 @@ using System.IO; -using System.Runtime.InteropServices; using System.Text; using Dalamud.Logging.Internal; using Dalamud.Utility; -using PInvoke; using SQLite; namespace Dalamud.Storage; @@ -28,6 +26,7 @@ public class ReliableFileStorage : IServiceType, IDisposable { private static readonly ModuleLog Log = new("VFS"); + private readonly object syncRoot = new(); private SQLiteConnection? db; /// @@ -118,34 +117,37 @@ public class ReliableFileStorage : IServiceType, IDisposable { ArgumentException.ThrowIfNullOrEmpty(path); - if (this.db == null) + lock (this.syncRoot) { - Util.WriteAllBytesSafe(path, bytes); - return; - } - - this.db.RunInTransaction(() => - { - var normalizedPath = NormalizePath(path); - var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); - if (file == null) + if (this.db == null) { - file = new DbFile + Util.WriteAllBytesSafe(path, bytes); + return; + } + + this.db.RunInTransaction(() => + { + var normalizedPath = NormalizePath(path); + var file = this.db.Table().FirstOrDefault(f => f.Path == normalizedPath && f.ContainerId == containerId); + if (file == null) { - ContainerId = containerId, - Path = normalizedPath, - Data = bytes, - }; - this.db.Insert(file); - } - else - { - file.Data = bytes; - this.db.Update(file); - } + file = new DbFile + { + ContainerId = containerId, + Path = normalizedPath, + Data = bytes, + }; + this.db.Insert(file); + } + else + { + file.Data = bytes; + this.db.Update(file); + } - Util.WriteAllBytesSafe(path, bytes); - }); + Util.WriteAllBytesSafe(path, bytes); + }); + } } /// From c1e8fd3519ebde198f74f75b63d492b4bb127954 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 4 Oct 2023 01:05:21 +0200 Subject: [PATCH 226/585] Update ClientStructs (#1453) 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 5c9fc4200..934483e70 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 5c9fc4200387edd32b9190d1b9f61e5d8a17114d +Subproject commit 934483e70691b0c69ce3725f119b9eb95ec92621 From 7902e85b30f5cb038cae63edb1fd67d7f04a4cd8 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 3 Oct 2023 18:45:46 -0700 Subject: [PATCH 227/585] Verified Checkmark - Better Colors (#1455) --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index a364008cb..036959233 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1738,8 +1738,9 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SameLine(); var verifiedOutlineColor = KnownColor.White.Vector() with { W = 0.75f }; + var unverifiedOutlineColor = KnownColor.Black.Vector(); var verifiedIconColor = KnownColor.RoyalBlue.Vector() with { W = 0.75f }; - var unverifiedIconColor = KnownColor.Orange.Vector() with { W = 0.75f }; + var unverifiedIconColor = KnownColor.Orange.Vector(); if (!isThirdParty) { @@ -1748,7 +1749,7 @@ internal class PluginInstallerWindow : Window, IDisposable } else { - this.DrawFontawesomeIconOutlined(FontAwesomeIcon.ExclamationCircle, verifiedOutlineColor, unverifiedIconColor); + this.DrawFontawesomeIconOutlined(FontAwesomeIcon.ExclamationCircle, unverifiedOutlineColor, unverifiedIconColor); this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_UnverifiedTooltip); } } From e18fdd48f889be56f2f8c16513e8b1cbd0d9114c Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 4 Oct 2023 04:29:10 +0200 Subject: [PATCH 228/585] Update ClientStructs (#1454) 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 934483e70..cb934ebc1 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 934483e70691b0c69ce3725f119b9eb95ec92621 +Subproject commit cb934ebc138dc631c45d2d6fe7330daa7671b97b From 2d8b7ab2c57abcc7d4af6168100eeb1569fa88ff Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 4 Oct 2023 04:43:39 +0200 Subject: [PATCH 229/585] Bump DalamudPackager to 2.1.12 in .targets (#1456) --- targets/Dalamud.Plugin.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targets/Dalamud.Plugin.targets b/targets/Dalamud.Plugin.targets index 2f8e029eb..37c0940d7 100644 --- a/targets/Dalamud.Plugin.targets +++ b/targets/Dalamud.Plugin.targets @@ -14,7 +14,7 @@ - + From 90605be61133b8ac984e74facd2cfc2a625e5249 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 4 Oct 2023 18:54:19 +0200 Subject: [PATCH 230/585] Update ClientStructs (#1457) 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 cb934ebc1..6e160ec14 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit cb934ebc138dc631c45d2d6fe7330daa7671b97b +Subproject commit 6e160ec14385f4505e68eae063f978c01f8726c6 From a3eb2705069a2102f9dc5ce70ced98ccdc3017e9 Mon Sep 17 00:00:00 2001 From: Ava Chaney Date: Wed, 4 Oct 2023 11:00:58 -0700 Subject: [PATCH 231/585] fix: hidpi fixes for changelog and console windows (#1459) --- Dalamud/Interface/Internal/Windows/ChangelogWindow.cs | 10 ++++++---- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 8 +++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 1c0d3fa02..45ad215d4 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -175,7 +175,7 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.Dummy(new Vector2(dummySize)); ImGui.SameLine(); - var logoContainerSize = new Vector2(this.Size!.Value.X * 0.2f - dummySize, this.Size!.Value.Y); + var logoContainerSize = new Vector2(windowSize.X * 0.2f - dummySize, windowSize.Y); using (var child = ImRaii.Child("###logoContainer", logoContainerSize, false)) { if (!child) @@ -196,7 +196,7 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.Dummy(new Vector2(dummySize)); ImGui.SameLine(); - using (var child = ImRaii.Child("###textContainer", new Vector2((this.Size!.Value.X * 0.8f) - dummySize * 4, this.Size!.Value.Y), false)) + using (var child = ImRaii.Child("###textContainer", new Vector2((windowSize.X * 0.8f) - dummySize * 4, windowSize.Y), false)) { if (!child) return; @@ -351,7 +351,7 @@ internal sealed class ChangelogWindow : Window, IDisposable var childSize = ImGui.GetWindowSize(); var closeButtonSize = 15 * ImGuiHelpers.GlobalScale; - ImGui.SetCursorPos(new Vector2(childSize.X - closeButtonSize - (5 * ImGuiHelpers.GlobalScale), 10 * ImGuiHelpers.GlobalScale)); + ImGui.SetCursorPos(new Vector2(childSize.X - closeButtonSize - 5, 10 * ImGuiHelpers.GlobalScale)); if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { Dismiss(); @@ -360,9 +360,11 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.PopStyleColor(2); ImGui.PopStyleVar(); - + if (ImGui.IsItemHovered()) + { ImGui.SetTooltip("I don't care about this"); + } } } diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index fe3c25784..6bb8ea25e 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -145,7 +145,7 @@ internal class ConsoleWindow : Window, IDisposable this.DrawFilterToolbar(); - ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); + ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55 * ImGuiHelpers.GlobalScale), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); if (this.clearLog) this.Clear(); @@ -268,11 +268,9 @@ internal class ConsoleWindow : Window, IDisposable } ImGui.SetItemDefaultFocus(); - if (getFocus) - ImGui.SetKeyboardFocusHere(-1); // Auto focus previous widget + if (getFocus) ImGui.SetKeyboardFocusHere(-1); // Auto focus previous widget - if (hadColor) - ImGui.PopStyleColor(); + if (hadColor) ImGui.PopStyleColor(); if (ImGui.Button("Send", ImGuiHelpers.ScaledVector2(80.0f, 23.0f))) { From 102318d74588125a15df01ebebe09bc1b0cafd9f Mon Sep 17 00:00:00 2001 From: Cara Date: Thu, 5 Oct 2023 06:18:18 +1030 Subject: [PATCH 232/585] Add new config options (#1458) --- Dalamud/Game/Config/UiConfigOption.cs | 63 +++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/Dalamud/Game/Config/UiConfigOption.cs b/Dalamud/Game/Config/UiConfigOption.cs index 82f823ffe..aaa86230a 100644 --- a/Dalamud/Game/Config/UiConfigOption.cs +++ b/Dalamud/Game/Config/UiConfigOption.cs @@ -3473,4 +3473,67 @@ public enum UiConfigOption /// [GameConfigOption("ItemInventryStoreEnd", ConfigType.UInt)] ItemInventryStoreEnd, + + /// + /// System option with the internal name HotbarXHBEditEnable. + /// This option is a UInt. + /// + [GameConfigOption("HotbarXHBEditEnable", ConfigType.UInt)] + HotbarXHBEditEnable, + + /// + /// System option with the internal name NamePlateDispJobIconInPublicParty. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateDispJobIconInPublicParty", ConfigType.UInt)] + NamePlateDispJobIconInPublicParty, + + /// + /// System option with the internal name NamePlateDispJobIconInPublicOther. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateDispJobIconInPublicOther", ConfigType.UInt)] + NamePlateDispJobIconInPublicOther, + + /// + /// System option with the internal name NamePlateDispJobIconInInstanceParty. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateDispJobIconInInstanceParty", ConfigType.UInt)] + NamePlateDispJobIconInInstanceParty, + + /// + /// System option with the internal name NamePlateDispJobIconInInstanceOther. + /// This option is a UInt. + /// + [GameConfigOption("NamePlateDispJobIconInInstanceOther", ConfigType.UInt)] + NamePlateDispJobIconInInstanceOther, + + /// + /// System option with the internal name CCProgressAllyFixLeftSide. + /// This option is a UInt. + /// + [GameConfigOption("CCProgressAllyFixLeftSide", ConfigType.UInt)] + CCProgressAllyFixLeftSide, + + /// + /// System option with the internal name CCMapAllyFixLeftSide. + /// This option is a UInt. + /// + [GameConfigOption("CCMapAllyFixLeftSide", ConfigType.UInt)] + CCMapAllyFixLeftSide, + + /// + /// System option with the internal name DispCCCountDown. + /// This option is a UInt. + /// + [GameConfigOption("DispCCCountDown", ConfigType.UInt)] + DispCCCountDown, + + /// + /// System option with the internal name TelepoCategoryType. + /// This option is a UInt. + /// + [GameConfigOption("TelepoCategoryType", ConfigType.UInt)] + TelepoCategoryType, } From 12354cd57c0a1e9901cfc30ba8473fb85dd28be6 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 4 Oct 2023 12:49:18 -0700 Subject: [PATCH 233/585] Add Additional AddonSetup (#1460) * Add Additional AddonSetup * Update Documentation --- Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs | 4 ++++ .../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index d4e45688d..b188095d0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -27,6 +27,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly AddonLifecycleAddressResolver address; private readonly CallHook onAddonSetupHook; + private readonly CallHook onAddonSetup2Hook; private readonly Hook onAddonFinalizeHook; private readonly CallHook onAddonDrawHook; private readonly CallHook onAddonUpdateHook; @@ -46,6 +47,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.framework.Update += this.OnFrameworkUpdate; this.onAddonSetupHook = new CallHook(this.address.AddonSetup, this.OnAddonSetup); + this.onAddonSetup2Hook = new CallHook(this.address.AddonSetup2, 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); @@ -71,6 +73,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.framework.Update -= this.OnFrameworkUpdate; this.onAddonSetupHook.Dispose(); + this.onAddonSetup2Hook.Dispose(); this.onAddonFinalizeHook.Dispose(); this.onAddonDrawHook.Dispose(); this.onAddonUpdateHook.Dispose(); @@ -120,6 +123,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void ContinueConstruction() { this.onAddonSetupHook.Enable(); + this.onAddonSetup2Hook.Enable(); this.onAddonFinalizeHook.Enable(); this.onAddonDrawHook.Enable(); this.onAddonUpdateHook.Enable(); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index 7b276c903..ff694c84d 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -7,9 +7,18 @@ 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. /// @@ -42,6 +51,7 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver protected override void Setup64Bit(SigScanner sig) { this.AddonSetup = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 93 ?? ?? ?? ?? 80 8B"); + this.AddonSetup2 = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 03 48 8B CB 80 8B"); this.AddonFinalize = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 41 8B C6"); this.AddonDraw = sig.ScanText("FF 90 ?? ?? ?? ?? 83 EB 01 79 C1"); this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); From 1d015c2ee074dc2d16ac2acb1ca8b48a93bc30a8 Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Wed, 4 Oct 2023 22:44:44 +0200 Subject: [PATCH 234/585] fix: use effective version in welcome message (#1462) --- Dalamud/Game/ChatHandlers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 890cd0439..a41a66fa1 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -239,7 +239,7 @@ internal class ChatHandlers : IServiceType { foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded)) { - chatGui.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.Manifest.AssemblyVersion)); + chatGui.Print(string.Format(Loc.Localize("DalamudPluginLoaded", " 》 {0} v{1} loaded."), plugin.Name, plugin.EffectiveVersion)); } } From 707bcd4d821c7b3993c93961e0b91e9f6e1fcac1 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 4 Oct 2023 23:45:11 +0200 Subject: [PATCH 235/585] chore: use CsWin32 for WriteAllBytesSafe() We should start converting more to it, as time goes on --- Dalamud/Dalamud.csproj | 3 +++ Dalamud/NativeMethods.txt | 5 +++++ Dalamud/Utility/Util.cs | 31 +++++++++++++++++-------------- 3 files changed, 25 insertions(+), 14 deletions(-) create mode 100644 Dalamud/NativeMethods.txt diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 7e11b0975..da1ef3f6a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -70,6 +70,9 @@ + + all + diff --git a/Dalamud/NativeMethods.txt b/Dalamud/NativeMethods.txt new file mode 100644 index 000000000..18143e1af --- /dev/null +++ b/Dalamud/NativeMethods.txt @@ -0,0 +1,5 @@ +CreateFile +FILE_ACCESS_RIGHTS +MoveFileEx +FlushFileBuffers +WriteFile diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 36918abd2..d27b79ea3 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; using System.IO.Compression; @@ -17,11 +18,10 @@ using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; -using Dalamud.Memory; using ImGuiNET; using Lumina.Excel.GeneratedSheets; -using PInvoke; using Serilog; +using Windows.Win32.Storage.FileSystem; namespace Dalamud.Utility; @@ -640,36 +640,39 @@ public static class Util /// /// The path of the file to write to. /// The data to write. - public static void WriteAllBytesSafe(string path, byte[] bytes) + public static unsafe void WriteAllBytesSafe(string path, byte[] bytes) { ArgumentException.ThrowIfNullOrEmpty(path); // Open the temp file var tempPath = path + ".tmp"; - using var tempFile = Kernel32 - .CreateFile(tempPath.AsSpan(), - new Kernel32.ACCESS_MASK(Kernel32.FileAccess.FILE_GENERIC_READ | Kernel32.FileAccess.FILE_GENERIC_WRITE), - Kernel32.FileShare.None, - null, - Kernel32.CreationDisposition.CREATE_ALWAYS, - Kernel32.CreateFileFlags.FILE_ATTRIBUTE_NORMAL, - Kernel32.SafeObjectHandle.Null); + using 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); if (tempFile.IsInvalid) throw new Win32Exception(); // Write the data - var bytesWritten = Kernel32.WriteFile(tempFile, new ArraySegment(bytes)); + uint bytesWritten = 0; + if (!Windows.Win32.PInvoke.WriteFile(tempFile, new ReadOnlySpan(bytes), &bytesWritten, null)) + throw new Win32Exception(); + if (bytesWritten != bytes.Length) throw new Exception($"Could not write all bytes to temp file ({bytesWritten} of {bytes.Length})"); - if (!Kernel32.FlushFileBuffers(tempFile)) + if (!Windows.Win32.PInvoke.FlushFileBuffers(tempFile)) throw new Win32Exception(); tempFile.Close(); - if (!MoveFileEx(tempPath, path, MoveFileFlags.MovefileReplaceExisting | MoveFileFlags.MovefileWriteThrough)) + if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) throw new Win32Exception(); } From 196504d625355d5f714ef309d20c95079d354961 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 4 Oct 2023 23:47:08 +0200 Subject: [PATCH 236/585] chore: remove now useless MoveFile import --- Dalamud/Utility/Util.cs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index d27b79ea3..3916a5789 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -812,18 +812,4 @@ public static class Util } } } - - [Flags] -#pragma warning disable SA1201 - private enum MoveFileFlags -#pragma warning restore SA1201 - { - MovefileReplaceExisting = 0x00000001, - MovefileWriteThrough = 0x00000008, - } - - [return: MarshalAs(UnmanagedType.Bool)] - [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] - private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, - MoveFileFlags dwFlags); } From db924747f2899e4f7601e6db226f21effe5fc738 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 5 Oct 2023 02:09:27 +0200 Subject: [PATCH 237/585] [master] Update ClientStructs (#1461) 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 6e160ec14..438010c7b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 6e160ec14385f4505e68eae063f978c01f8726c6 +Subproject commit 438010c7bd3e2cd64eb47d366792c07242a93937 From 59606ff854cbee1188f9be2beee6189fd47dec5c Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 5 Oct 2023 18:53:58 +0200 Subject: [PATCH 238/585] [master] Update ClientStructs (#1463) 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 438010c7b..dc48a0768 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 438010c7bd3e2cd64eb47d366792c07242a93937 +Subproject commit dc48a0768e84f06d6016588a8d605680f950f6e2 From 2083ccda00bca5ad865bf1f428c4d6fe3e86a69e Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 5 Oct 2023 10:01:03 -0700 Subject: [PATCH 239/585] Remove internal dependencies on opcodes (#1464) - Removes the opcode lists from internal API entirely - Move NetworkHandlers to use packet handler sigs - Remove opcode data from NetworkMonitorWidget --- Dalamud/Data/DataManager.cs | 29 +- .../Game/Network/Internal/NetworkHandlers.cs | 448 ++++++++++++------ .../NetworkHandlersAddressResolver.cs | 64 +++ .../Game/Network/Structures/MarketTaxRates.cs | 24 +- .../Data/Widgets/NetworkMonitorWidget.cs | 20 +- Dalamud/Plugin/Services/IDataManager.cs | 8 +- Dalamud/Utility/Util.cs | 45 +- 7 files changed, 417 insertions(+), 221 deletions(-) create mode 100644 Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 6195532ab..b08c6ffe7 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -37,26 +37,10 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager { this.Language = (ClientLanguage)dalamud.StartInfo.Language; - // Set up default values so plugins do not null-reference when data is being loaded. - this.ClientOpCodes = this.ServerOpCodes = new ReadOnlyDictionary(new Dictionary()); - - var baseDir = dalamud.AssetDirectory.FullName; try { Log.Verbose("Starting data load..."); - - var zoneOpCodeDict = JsonConvert.DeserializeObject>( - File.ReadAllText(Path.Combine(baseDir, "UIRes", "serveropcode.json")))!; - this.ServerOpCodes = new ReadOnlyDictionary(zoneOpCodeDict); - - Log.Verbose("Loaded {0} ServerOpCodes.", zoneOpCodeDict.Count); - - var clientOpCodeDict = JsonConvert.DeserializeObject>( - File.ReadAllText(Path.Combine(baseDir, "UIRes", "clientopcode.json")))!; - this.ClientOpCodes = new ReadOnlyDictionary(clientOpCodeDict); - - Log.Verbose("Loaded {0} ClientOpCodes.", clientOpCodeDict.Count); - + using (Timings.Start("Lumina Init")) { var luminaOptions = new LuminaOptions @@ -130,17 +114,6 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager /// public ClientLanguage Language { get; private set; } - /// - /// Gets a list of server opcodes. - /// - public ReadOnlyDictionary ServerOpCodes { get; private set; } - - /// - /// Gets a list of client opcodes. - /// - [UsedImplicitly] - public ReadOnlyDictionary ClientOpCodes { get; private set; } - /// public GameData GameData { get; private set; } diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 77bf99c1b..01e92a373 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reactive.Concurrency; using System.Reactive.Linq; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Dalamud.Configuration.Internal; @@ -14,7 +12,9 @@ using Dalamud.Game.Gui; using Dalamud.Game.Network.Internal.MarketBoardUploaders; using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis; using Dalamud.Game.Network.Structures; +using Dalamud.Hooking; using Dalamud.Utility; +using FFXIVClientStructs.FFXIV.Client.UI.Info; using Lumina.Excel.GeneratedSheets; using Serilog; @@ -24,16 +24,30 @@ namespace Dalamud.Game.Network.Internal; /// This class handles network notifications and uploading market board data. /// [ServiceManager.EarlyLoadedService] -internal class NetworkHandlers : IDisposable, IServiceType +internal unsafe class NetworkHandlers : IDisposable, IServiceType { private readonly IMarketBoardUploader uploader; - private readonly IObservable messages; + private readonly IObservable mbPurchaseObservable; + private readonly IObservable mbHistoryObservable; + private readonly IObservable mbTaxesObservable; + private readonly IObservable mbItemRequestObservable; + private readonly IObservable mbOfferingsObservable; + private readonly IObservable mbPurchaseSentObservable; private readonly IDisposable handleMarketBoardItemRequest; private readonly IDisposable handleMarketTaxRates; private readonly IDisposable handleMarketBoardPurchaseHandler; - private readonly IDisposable handleCfPop; + + private readonly NetworkHandlersAddressResolver addressResolver; + + private readonly Hook cfPopHook; + private readonly Hook mbPurchaseHook; + private readonly Hook mbHistoryHook; + private readonly Hook customTalkHook; // used for marketboard taxes + private readonly Hook mbItemRequestStartHook; + private readonly Hook mbOfferingsHook; + private readonly Hook mbSendPurchaseRequestHook; [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); @@ -41,42 +55,156 @@ internal class NetworkHandlers : IDisposable, IServiceType private bool disposing; [ServiceManager.ServiceConstructor] - private NetworkHandlers(GameNetwork gameNetwork) + private NetworkHandlers(GameNetwork gameNetwork, TargetSigScanner sigScanner) { this.uploader = new UniversalisMarketBoardUploader(); + + this.addressResolver = new NetworkHandlersAddressResolver(); + this.addressResolver.Setup(sigScanner); + this.CfPop = _ => { }; - this.messages = Observable.Create(observer => + this.mbPurchaseObservable = Observable.Create(observer => { - void Observe(IntPtr dataPtr, ushort opCode, uint sourceActorId, uint targetActorId, NetworkMessageDirection direction) - { - var dataManager = Service.GetNullable(); - observer.OnNext(new NetworkMessage - { - DataManager = dataManager, - Data = dataPtr, - Opcode = opCode, - SourceActorId = sourceActorId, - TargetActorId = targetActorId, - Direction = direction, - }); - } + this.MarketBoardPurchaseReceived += Observe; + return () => { this.MarketBoardPurchaseReceived -= Observe; }; - gameNetwork.NetworkMessage += Observe; - return () => { gameNetwork.NetworkMessage -= Observe; }; + void Observe(nint packetPtr) + { + observer.OnNext(MarketBoardPurchase.Read(packetPtr)); + } + }); + + this.mbHistoryObservable = Observable.Create(observer => + { + this.MarketBoardHistoryReceived += Observe; + return () => { this.MarketBoardHistoryReceived -= Observe; }; + + void Observe(nint packetPtr) + { + observer.OnNext(MarketBoardHistory.Read(packetPtr)); + } + }); + + this.mbTaxesObservable = Observable.Create(observer => + { + this.MarketBoardTaxesReceived += Observe; + return () => { this.MarketBoardTaxesReceived -= Observe; }; + + void Observe(nint dataPtr) + { + // n.b. we precleared the packet information so we're sure that this is *just* tax rate info. + observer.OnNext(MarketTaxRates.ReadFromCustomTalk(dataPtr)); + } + }); + + this.mbItemRequestObservable = Observable.Create(observer => + { + this.MarketBoardItemRequestStartReceived += Observe; + return () => this.MarketBoardItemRequestStartReceived -= Observe; + + void Observe(nint dataPtr) + { + observer.OnNext(MarketBoardItemRequest.Read(dataPtr)); + } + }); + + this.mbOfferingsObservable = Observable.Create(observer => + { + this.MarketBoardOfferingsReceived += Observe; + return () => { this.MarketBoardOfferingsReceived -= Observe; }; + + void Observe(nint packetPtr) + { + observer.OnNext(MarketBoardCurrentOfferings.Read(packetPtr)); + } + }); + + this.mbPurchaseSentObservable = Observable.Create(observer => + { + this.MarketBoardPurchaseRequestSent += Observe; + return () => { this.MarketBoardPurchaseRequestSent -= Observe; }; + + void Observe(nint dataPtr) + { + // fortunately, this dataptr has the same structure as the sent packet. + observer.OnNext(MarketBoardPurchaseHandler.Read(dataPtr)); + } }); this.handleMarketBoardItemRequest = this.HandleMarketBoardItemRequest(); this.handleMarketTaxRates = this.HandleMarketTaxRates(); this.handleMarketBoardPurchaseHandler = this.HandleMarketBoardPurchaseHandler(); - this.handleCfPop = this.HandleCfPop(); + + this.mbPurchaseHook = + Hook.FromAddress( + this.addressResolver.MarketBoardPurchasePacketHandler, + this.MarketPurchasePacketDetour); + this.mbPurchaseHook.Enable(); + + this.mbHistoryHook = + Hook.FromAddress( + this.addressResolver.MarketBoardHistoryPacketHandler, + this.MarketHistoryPacketDetour); + this.mbHistoryHook.Enable(); + + this.customTalkHook = + Hook.FromAddress( + this.addressResolver.CustomTalkEventResponsePacketHandler, + this.CustomTalkReceiveResponseDetour); + this.customTalkHook.Enable(); + + this.mbItemRequestStartHook = Hook.FromAddress( + this.addressResolver.MarketBoardItemRequestStartPacketHandler, + this.MarketItemRequestStartDetour); + this.mbItemRequestStartHook.Enable(); + + this.mbOfferingsHook = Hook.FromAddress( + this.addressResolver.InfoProxyItemSearchAddPage, + this.MarketBoardOfferingsDetour); + this.mbOfferingsHook.Enable(); + + this.mbSendPurchaseRequestHook = Hook.FromAddress( + this.addressResolver.BuildMarketBoardPurchaseHandlerPacket, + this.MarketBoardSendPurchaseRequestDetour); + this.mbSendPurchaseRequestHook.Enable(); + + this.cfPopHook = Hook.FromAddress(this.addressResolver.CfPopPacketHandler, this.CfPopDetour); + this.cfPopHook.Enable(); } + private delegate nint MarketBoardPurchasePacketHandler(nint a1, nint packetRef); + + private delegate nint MarketBoardHistoryPacketHandler(nint self, nint packetData, uint a3, char a4); + + private delegate void CustomTalkReceiveResponse( + nuint a1, ushort eventId, byte responseId, uint* args, byte argCount); + + private delegate nint MarketBoardItemRequestStartPacketHandler(nint a1, nint packetRef); + + private delegate byte InfoProxyItemSearchAddPage(nint self, nint packetRef); + + private delegate byte MarketBoardSendPurchaseRequestPacket(InfoProxyItemSearch* infoProxy); + + private delegate nint CfPopDelegate(nint packetData); + /// /// Event which gets fired when a duty is ready. /// public event Action CfPop; + private event Action? MarketBoardPurchaseReceived; + + private event Action? MarketBoardHistoryReceived; + + private event Action? MarketBoardTaxesReceived; + + private event Action? MarketBoardItemRequestStartReceived; + + private event Action? MarketBoardOfferingsReceived; + + private event Action? MarketBoardPurchaseRequestSent; + /// /// Disposes of managed and unmanaged resources. /// @@ -98,81 +226,75 @@ internal class NetworkHandlers : IDisposable, IServiceType this.handleMarketBoardItemRequest.Dispose(); this.handleMarketTaxRates.Dispose(); this.handleMarketBoardPurchaseHandler.Dispose(); - this.handleCfPop.Dispose(); + + this.mbPurchaseHook.Dispose(); + this.mbHistoryHook.Dispose(); + this.customTalkHook.Dispose(); + this.mbItemRequestStartHook.Dispose(); + this.mbOfferingsHook.Dispose(); + this.mbSendPurchaseRequestHook.Dispose(); + this.cfPopHook.Dispose(); } - private IObservable OnNetworkMessage() + private unsafe nint CfPopDetour(nint packetData) { - return this.messages.Where(message => message.DataManager?.IsDataReady == true); - } + var result = this.cfPopHook.OriginalDisposeSafe(packetData); - private IObservable OnMarketBoardItemRequestStart() - { - return this.OnNetworkMessage() - .Where(message => message.Direction == NetworkMessageDirection.ZoneDown) - .Where(message => message.Opcode == - message.DataManager?.ServerOpCodes["MarketBoardItemRequestStart"]) - .Select(message => MarketBoardItemRequest.Read(message.Data)); - } + try + { + using var stream = new UnmanagedMemoryStream((byte*)packetData, 64); + using var reader = new BinaryReader(stream); - private IObservable OnMarketBoardOfferings() - { - return this.OnNetworkMessage() - .Where(message => message.Direction == NetworkMessageDirection.ZoneDown) - .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["MarketBoardOfferings"]) - .Select(message => MarketBoardCurrentOfferings.Read(message.Data)); - } + var notifyType = reader.ReadByte(); + stream.Position += 0x1B; + var conditionId = reader.ReadUInt16(); - private IObservable OnMarketBoardHistory() - { - return this.OnNetworkMessage() - .Where(message => message.Direction == NetworkMessageDirection.ZoneDown) - .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["MarketBoardHistory"]) - .Select(message => MarketBoardHistory.Read(message.Data)); - } + if (notifyType != 3) + return result; - private IObservable OnMarketTaxRates() - { - return this.OnNetworkMessage() - .Where(message => message.Direction == NetworkMessageDirection.ZoneDown) - .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["MarketTaxRates"]) - .Where(message => - { - // Only some categories of the result dialog packet contain market tax rates - var category = (uint)Marshal.ReadInt32(message.Data); - return category == 720905; - }) - .Select(message => MarketTaxRates.Read(message.Data)) - .Where(taxes => taxes.Category == 0xb0009); - } + if (this.configuration.DutyFinderTaskbarFlash) + Util.FlashWindow(); - private IObservable OnMarketBoardPurchaseHandler() - { - return this.OnNetworkMessage() - .Where(message => message.Direction == NetworkMessageDirection.ZoneUp) - .Where(message => message.Opcode == message.DataManager?.ClientOpCodes["MarketBoardPurchaseHandler"]) - .Select(message => MarketBoardPurchaseHandler.Read(message.Data)); - } + var cfConditionSheet = Service.Get().GetExcelSheet()!; + var cfCondition = cfConditionSheet.GetRow(conditionId); - private IObservable OnMarketBoardPurchase() - { - return this.OnNetworkMessage() - .Where(message => message.Direction == NetworkMessageDirection.ZoneDown) - .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["MarketBoardPurchase"]) - .Select(message => MarketBoardPurchase.Read(message.Data)); - } + if (cfCondition == null) + { + Log.Error("CFC key {ConditionId} not in Lumina data", conditionId); + return result; + } - private IObservable OnCfNotifyPop() - { - return this.OnNetworkMessage() - .Where(message => message.Direction == NetworkMessageDirection.ZoneDown) - .Where(message => message.Opcode == message.DataManager?.ServerOpCodes["CfNotifyPop"]); + var cfcName = cfCondition.Name.ToString(); + if (cfcName.IsNullOrEmpty()) + { + cfcName = "Duty Roulette"; + cfCondition.Image = 112324; + } + + Task.Run(() => + { + if (this.configuration.DutyFinderChatMessage) + { + Service.GetNullable()?.Print($"Duty pop: {cfcName}"); + } + + this.CfPop.InvokeSafely(cfCondition); + }).ContinueWith( + task => Log.Error(task.Exception, "CfPop.Invoke failed"), + TaskContinuationOptions.OnlyOnFaulted); + } + catch (Exception ex) + { + Log.Error(ex, "CfPopDetour threw an exception"); + } + + return result; } private IObservable> OnMarketBoardListingsBatch( IObservable start) { - var offeringsObservable = this.OnMarketBoardOfferings().Publish().RefCount(); + var offeringsObservable = this.mbOfferingsObservable.Publish().RefCount(); void LogEndObserved(MarketBoardCurrentOfferings offerings) { @@ -222,7 +344,7 @@ internal class NetworkHandlers : IDisposable, IServiceType private IObservable> OnMarketBoardSalesBatch( IObservable start) { - var historyObservable = this.OnMarketBoardHistory().Publish().RefCount(); + var historyObservable = this.mbHistoryObservable.Publish().RefCount(); void LogHistoryObserved(MarketBoardHistory history) { @@ -265,7 +387,7 @@ internal class NetworkHandlers : IDisposable, IServiceType request.AmountToArrive); } - var startObservable = this.OnMarketBoardItemRequestStart() + var startObservable = this.mbItemRequestObservable .Where(request => request.Ok).Do(LogStartObserved) .Publish() .RefCount(); @@ -292,7 +414,9 @@ internal class NetworkHandlers : IDisposable, IServiceType { if (listings.Count != request.AmountToArrive) { - Log.Error("Wrong number of Market Board listings received for request: {ListingsCount} != {RequestAmountToArrive} item#{RequestCatalogId}", listings.Count, request.AmountToArrive, request.CatalogId); + Log.Error( + "Wrong number of Market Board listings received for request: {ListingsCount} != {RequestAmountToArrive} item#{RequestCatalogId}", + listings.Count, request.AmountToArrive, request.CatalogId); return; } @@ -319,7 +443,7 @@ internal class NetworkHandlers : IDisposable, IServiceType private IDisposable HandleMarketTaxRates() { - return this.OnMarketTaxRates() + return this.mbTaxesObservable .Where(this.ShouldUpload) .SubscribeOn(ThreadPoolScheduler.Instance) .Subscribe( @@ -345,8 +469,8 @@ internal class NetworkHandlers : IDisposable, IServiceType private IDisposable HandleMarketBoardPurchaseHandler() { - return this.OnMarketBoardPurchaseHandler() - .Zip(this.OnMarketBoardPurchase()) + return this.mbPurchaseSentObservable + .Zip(this.mbPurchaseObservable) .Where(this.ShouldUpload) .SubscribeOn(ThreadPoolScheduler.Instance) .Subscribe( @@ -376,85 +500,93 @@ internal class NetworkHandlers : IDisposable, IServiceType ex => Log.Error(ex, "Failed to handle Market Board purchase event")); } - private unsafe IDisposable HandleCfPop() - { - return this.OnCfNotifyPop() - .SubscribeOn(ThreadPoolScheduler.Instance) - .Subscribe( - message => - { - using var stream = new UnmanagedMemoryStream((byte*)message.Data.ToPointer(), 64); - using var reader = new BinaryReader(stream); - - var notifyType = reader.ReadByte(); - stream.Position += 0x1B; - var conditionId = reader.ReadUInt16(); - - if (notifyType != 3) - return; - - var cfConditionSheet = message.DataManager!.GetExcelSheet()!; - var cfCondition = cfConditionSheet.GetRow(conditionId); - - if (cfCondition == null) - { - Log.Error("CFC key {ConditionId} not in Lumina data", conditionId); - return; - } - - var cfcName = cfCondition.Name.ToString(); - if (cfcName.IsNullOrEmpty()) - { - cfcName = "Duty Roulette"; - cfCondition.Image = 112324; - } - - // Flash window - if (this.configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated()) - { - var flashInfo = new NativeFunctions.FlashWindowInfo - { - Size = (uint)Marshal.SizeOf(), - Count = uint.MaxValue, - Timeout = 0, - Flags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG, - Hwnd = Process.GetCurrentProcess().MainWindowHandle, - }; - NativeFunctions.FlashWindowEx(ref flashInfo); - } - - Task.Run(() => - { - if (this.configuration.DutyFinderChatMessage) - { - Service.GetNullable()?.Print($"Duty pop: {cfcName}"); - } - - this.CfPop.InvokeSafely(cfCondition); - }).ContinueWith( - task => Log.Error(task.Exception, "CfPop.Invoke failed"), - TaskContinuationOptions.OnlyOnFaulted); - }, - ex => Log.Error(ex, "Failed to handle Market Board purchase event")); - } - private bool ShouldUpload(T any) { return this.configuration.IsMbCollect; } - private class NetworkMessage + private nint MarketPurchasePacketDetour(nint a1, nint packetData) { - public DataManager? DataManager { get; init; } + try + { + this.MarketBoardPurchaseReceived?.InvokeSafely(packetData); + } + catch (Exception ex) + { + Log.Error(ex, "MarketPurchasePacketHandler threw an exception"); + } + + return this.mbPurchaseHook.OriginalDisposeSafe(a1, packetData); + } - public IntPtr Data { get; init; } + private nint MarketHistoryPacketDetour(nint a1, nint packetData, uint a3, char a4) + { + try + { + this.MarketBoardHistoryReceived?.InvokeSafely(packetData); + } + catch (Exception ex) + { + Log.Error(ex, "MarketHistoryPacketDetour threw an exception"); + } + + return this.mbHistoryHook.OriginalDisposeSafe(a1, packetData, a3, a4); + } - public ushort Opcode { get; init; } + private void CustomTalkReceiveResponseDetour(nuint a1, ushort eventId, byte responseId, uint* args, byte argCount) + { + try + { + if (eventId == 7 && responseId == 8) + this.MarketBoardTaxesReceived?.InvokeSafely((nint)args); + } + catch (Exception ex) + { + Log.Error(ex, "CustomTalkReceiveResponseDetour threw an exception"); + } - public uint SourceActorId { get; init; } + this.customTalkHook.OriginalDisposeSafe(a1, eventId, responseId, args, argCount); + } - public uint TargetActorId { get; init; } + private nint MarketItemRequestStartDetour(nint a1, nint packetRef) + { + try + { + this.MarketBoardItemRequestStartReceived?.InvokeSafely(packetRef); + } + catch (Exception ex) + { + Log.Error(ex, "MarketItemRequestStartDetour threw an exception"); + } + + return this.mbItemRequestStartHook.OriginalDisposeSafe(a1, packetRef); + } - public NetworkMessageDirection Direction { get; init; } + private byte MarketBoardOfferingsDetour(nint a1, nint packetRef) + { + try + { + this.MarketBoardOfferingsReceived?.InvokeSafely(packetRef); + } + catch (Exception ex) + { + Log.Error(ex, "MarketBoardOfferingsDetour threw an exception"); + } + + return this.mbOfferingsHook.OriginalDisposeSafe(a1, packetRef); + } + + private byte MarketBoardSendPurchaseRequestDetour(InfoProxyItemSearch* infoProxyItemSearch) + { + try + { + this.MarketBoardPurchaseRequestSent?.InvokeSafely((nint)infoProxyItemSearch + 0x5680); + } + catch (Exception ex) + { + Log.Error(ex, "MarketBoardSendPurchaseRequestDetour threw an exception"); + } + + return this.mbSendPurchaseRequestHook.OriginalDisposeSafe(infoProxyItemSearch); } } diff --git a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs new file mode 100644 index 000000000..8b4788c74 --- /dev/null +++ b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs @@ -0,0 +1,64 @@ +namespace Dalamud.Game.Network.Internal; + +/// +/// Internal address resolver for the network handlers. +/// +internal class NetworkHandlersAddressResolver : BaseAddressResolver +{ + /// + /// Gets or sets the pointer to the method responsible for handling CfPop packets. + /// + public nint CfPopPacketHandler { get; set; } + + /// + /// Gets or sets the pointer to the method responsible for handling market board history. In this case, we are + /// sigging the packet handler method directly. + /// + public nint MarketBoardHistoryPacketHandler { get; set; } + + /// + /// Gets or sets the pointer to the method responsible for processing the market board purchase packet. In this + /// case, we are sigging the packet handler method directly. + /// + public nint MarketBoardPurchasePacketHandler { get; set; } + + /// + /// Gets or sets the pointer to the method responsible for custom talk events. Necessary for marketboard tax data, + /// as this isn't really exposed anywhere else. + /// + public nint CustomTalkEventResponsePacketHandler { get; set; } + + /// + /// Gets or sets the pointer to the method responsible for the marketboard ItemRequestStart packet. + /// + public nint MarketBoardItemRequestStartPacketHandler { get; set; } + + /// + /// Gets or sets the pointer to the InfoProxyItemSearch.AddPage method, used to load market data. + /// + public nint InfoProxyItemSearchAddPage { get; set; } + + /// + /// Gets or sets the pointer to the method inside InfoProxyItemSearch that is responsible for building and sending + /// a purchase request packet. + /// + public nint BuildMarketBoardPurchaseHandlerPacket { get; set; } + + /// + protected override void Setup64Bit(SigScanner scanner) + { + this.CfPopPacketHandler = scanner.ScanText("40 53 57 48 83 EC 78 48 8B D9 48 8D 0D"); + this.MarketBoardHistoryPacketHandler = scanner.ScanText( + "40 53 48 83 EC 20 48 8B 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 85 C0 74 36 4C 8B 00 48 8B C8 41 FF 90 ?? ?? ?? ?? 48 8B C8 BA ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 74 17 48 8D 53 04"); + this.MarketBoardPurchasePacketHandler = + scanner.ScanText("40 55 53 57 48 8B EC 48 83 EC 70 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 45 F0 48 8B 0D"); + this.CustomTalkEventResponsePacketHandler = + scanner.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 49 8B D9 41 0F B6 F8 0F B7 F2 8B E9 E8 ?? ?? ?? ?? 48 8B C8 44 0F B6 CF 0F B6 44 24 ?? 44 0F B7 C6 88 44 24 ?? 8B D5 48 89 5C 24"); + this.MarketBoardItemRequestStartPacketHandler = + scanner.ScanText("48 89 5C 24 ?? 57 48 83 EC 40 48 8B 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B F8"); + this.InfoProxyItemSearchAddPage = + scanner.ScanText("48 89 5C 24 ?? 57 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 0F B6 82 ?? ?? ?? ?? 48 8B FA 48 8B D9 38 41 19 74 54"); + this.BuildMarketBoardPurchaseHandlerPacket = + scanner.ScanText("40 53 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B D9 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 4C 8B D0 48 85 C0 0F 84 ?? ?? ?? ?? 8B 8B"); + } +} diff --git a/Dalamud/Game/Network/Structures/MarketTaxRates.cs b/Dalamud/Game/Network/Structures/MarketTaxRates.cs index 53ce41d44..42e1d8cce 100644 --- a/Dalamud/Game/Network/Structures/MarketTaxRates.cs +++ b/Dalamud/Game/Network/Structures/MarketTaxRates.cs @@ -1,4 +1,3 @@ -using System; using System.IO; namespace Dalamud.Game.Network.Structures; @@ -77,4 +76,27 @@ public class MarketTaxRates return output; } + + /// + /// Generate a MarketTaxRates wrapper class from information located in a CustomTalk packet. + /// + /// The pointer to the relevant CustomTalk data. + /// Returns a wrapped and ready-to-go MarketTaxRates record. + public static unsafe MarketTaxRates ReadFromCustomTalk(IntPtr dataPtr) + { + using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 1544); + using var reader = new BinaryReader(stream); + + return new MarketTaxRates + { + Category = 0xb0009, // shim + LimsaLominsaTax = reader.ReadUInt32(), + GridaniaTax = reader.ReadUInt32(), + UldahTax = reader.ReadUInt32(), + IshgardTax = reader.ReadUInt32(), + KuganeTax = reader.ReadUInt32(), + CrystariumTax = reader.ReadUInt32(), + SharlayanTax = reader.ReadUInt32(), + }; + } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs index e7bce0b84..eab1ab781 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NetworkMonitorWidget.cs @@ -30,7 +30,6 @@ internal class NetworkMonitorWidget : IDataWindowWidget } private readonly ConcurrentQueue packets = new(); - private readonly Dictionary opCodeDict = new(); private bool trackNetwork; private int trackedPackets; @@ -71,9 +70,6 @@ internal class NetworkMonitorWidget : IDataWindowWidget this.filterString = string.Empty; this.packets.Clear(); this.Ready = true; - var dataManager = Service.Get(); - foreach (var (name, code) in dataManager.ClientOpCodes.Concat(dataManager.ServerOpCodes)) - this.opCodeDict.TryAdd(code, (name, this.GetSizeFromName(name))); } /// @@ -106,7 +102,7 @@ internal class NetworkMonitorWidget : IDataWindowWidget this.DrawFilterInput(); this.DrawNegativeFilterInput(); - ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "Known Name", "OpCode", "Hex", "Target", "Source", "Data"); + ImGuiTable.DrawTable(string.Empty, this.packets, this.DrawNetworkPacket, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg, "Direction", "OpCode", "Hex", "Target", "Source", "Data"); } private void DrawNetworkPacket(NetworkPacketData data) @@ -114,16 +110,6 @@ internal class NetworkMonitorWidget : IDataWindowWidget ImGui.TableNextColumn(); ImGui.TextUnformatted(data.Direction.ToString()); - ImGui.TableNextColumn(); - if (this.opCodeDict.TryGetValue(data.OpCode, out var pair)) - { - ImGui.TextUnformatted(pair.Name); - } - else - { - ImGui.Dummy(new Vector2(150 * ImGuiHelpers.GlobalScale, 0)); - } - ImGui.TableNextColumn(); ImGui.TextUnformatted(data.OpCode.ToString()); @@ -217,7 +203,7 @@ internal class NetworkMonitorWidget : IDataWindowWidget } private int GetSizeFromOpCode(ushort opCode) - => this.opCodeDict.TryGetValue(opCode, out var pair) ? pair.Size : 0; + => 0; /// Add known packet-name -> packet struct size associations here to copy the byte data for such packets. > private int GetSizeFromName(string name) @@ -228,5 +214,5 @@ internal class NetworkMonitorWidget : IDataWindowWidget /// The filter should find opCodes by number (decimal and hex) and name, if existing. private string OpCodeToString(ushort opCode) - => this.opCodeDict.TryGetValue(opCode, out var pair) ? $"{opCode}\0{opCode:X}\0{pair.Name}" : $"{opCode}\0{opCode:X}"; + => $"{opCode}\0{opCode:X}"; } diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index 4977b65b3..e4b249319 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -1,11 +1,5 @@ -using System; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; - -using ImGuiScene; using Lumina; using Lumina.Data; -using Lumina.Data.Files; using Lumina.Excel; namespace Dalamud.Plugin.Services; @@ -19,7 +13,7 @@ public interface IDataManager /// Gets the current game client language. /// public ClientLanguage Language { get; } - + /// /// Gets a object which gives access to any excel/game data. /// diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 3916a5789..782d2ab37 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -9,7 +9,6 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; - using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; @@ -40,7 +39,8 @@ public static class Util /// /// Gets the assembly version of Dalamud. /// - public static string AssemblyVersion { get; } = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); + public static string AssemblyVersion { get; } = + Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); /// /// Check two byte arrays for equality. @@ -276,14 +276,16 @@ public static class Util if (ImGui.TreeNode($"{obj}##print-obj-{addr:X}-{string.Join("-", path)}")) { ImGui.PopStyleColor(); - foreach (var f in obj.GetType().GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance)) + foreach (var f in obj.GetType() + .GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance)) { var fixedBuffer = (FixedBufferAttribute)f.GetCustomAttribute(typeof(FixedBufferAttribute)); if (fixedBuffer != null) { ImGui.Text($"fixed"); ImGui.SameLine(); - ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), $"{fixedBuffer.ElementType.Name}[0x{fixedBuffer.Length:X}]"); + ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), + $"{fixedBuffer.ElementType.Name}[0x{fixedBuffer.Length:X}]"); } else { @@ -294,7 +296,7 @@ public static class Util ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.4f, 1), $"{f.Name}: "); ImGui.SameLine(); - ShowValue(addr, new List(path) { f.Name }, f.FieldType, f.GetValue(obj)); + ShowValue(addr, new List(path) {f.Name}, f.FieldType, f.GetValue(obj)); } foreach (var p in obj.GetType().GetProperties().Where(p => p.GetGetMethod()?.GetParameters().Length == 0)) @@ -304,7 +306,7 @@ public static class Util ImGui.TextColored(new Vector4(0.2f, 0.6f, 0.4f, 1), $"{p.Name}: "); ImGui.SameLine(); - ShowValue(addr, new List(path) { p.Name }, p.PropertyType, p.GetValue(obj)); + ShowValue(addr, new List(path) {p.Name}, p.PropertyType, p.GetValue(obj)); } ImGui.TreePop(); @@ -399,7 +401,8 @@ public static class Util /// Specify whether to exit immediately. public static void Fatal(string message, string caption, bool exit = true) { - var flags = NativeFunctions.MessageBoxType.Ok | NativeFunctions.MessageBoxType.IconError | NativeFunctions.MessageBoxType.Topmost; + var flags = NativeFunctions.MessageBoxType.Ok | NativeFunctions.MessageBoxType.IconError | + NativeFunctions.MessageBoxType.Topmost; _ = NativeFunctions.MessageBoxW(Process.GetCurrentProcess().MainWindowHandle, message, caption, flags); if (exit) @@ -413,7 +416,7 @@ public static class Util /// Human readable version. public static string FormatBytes(long bytes) { - string[] suffix = { "B", "KB", "MB", "GB", "TB" }; + string[] suffix = {"B", "KB", "MB", "GB", "TB"}; int i; double dblSByte = bytes; for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) @@ -601,7 +604,7 @@ public static class Util } } } - } + } finally { foreach (var enumerator in enumerators) @@ -611,6 +614,27 @@ public static class Util } } + /// + /// Request that Windows flash the game window to grab the user's attention. + /// + /// Attempt to flash even if the game is currently focused. + public static void FlashWindow(bool flashIfOpen = false) + { + if (NativeFunctions.ApplicationIsActivated() && flashIfOpen) + return; + + var flashInfo = new NativeFunctions.FlashWindowInfo + { + Size = (uint)Marshal.SizeOf(), + Count = uint.MaxValue, + Timeout = 0, + Flags = NativeFunctions.FlashWindow.All | NativeFunctions.FlashWindow.TimerNoFG, + Hwnd = Process.GetCurrentProcess().MainWindowHandle, + }; + + NativeFunctions.FlashWindowEx(ref flashInfo); + } + /// /// Overwrite text in a file by first writing it to a temporary file, and then /// moving that file to the path specified. @@ -693,7 +717,8 @@ public static class Util /// Log message to print, if specified and an error occurs. /// Module logger, if any. /// The type of object to dispose. - internal static void ExplicitDisposeIgnoreExceptions(this T obj, string? logMessage = null, ModuleLog? moduleLog = null) where T : IDisposable + internal static void ExplicitDisposeIgnoreExceptions( + this T obj, string? logMessage = null, ModuleLog? moduleLog = null) where T : IDisposable { try { From 412d94e4373fb3e6ced430425ce9bb724d96d55d Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 5 Oct 2023 21:00:26 +0200 Subject: [PATCH 240/585] [master] Update ClientStructs (#1466) 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 dc48a0768..c9daf7d4b 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit dc48a0768e84f06d6016588a8d605680f950f6e2 +Subproject commit c9daf7d4b98c131e7272fc755309c8830c67815f From 3a922e4d58854a56aa2b6e5a7703f39a565b6546 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 5 Oct 2023 21:03:24 +0200 Subject: [PATCH 241/585] chore: make profiles category visible by default, ask before enabling --- .../Internal/PluginCategoryManager.cs | 7 +--- .../PluginInstaller/PluginInstallerWindow.cs | 4 --- .../PluginInstaller/ProfileManagerWidget.cs | 32 +++++++++++++++++++ .../Settings/Tabs/SettingsTabExperimental.cs | 2 ++ 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index 9515a55b5..28d0cddbd 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -28,7 +28,7 @@ internal class PluginCategoryManager new(11, "special.devIconTester", () => Locs.Category_IconTester), new(12, "special.dalamud", () => Locs.Category_Dalamud), new(13, "special.plugins", () => Locs.Category_Plugins), - new(14, "special.profiles", () => Locs.Category_PluginProfiles, CategoryInfo.AppearCondition.ProfilesEnabled), + new(14, "special.profiles", () => Locs.Category_PluginProfiles), new(FirstTagBasedCategoryId + 0, "other", () => Locs.Category_Other), new(FirstTagBasedCategoryId + 1, "jobs", () => Locs.Category_Jobs), new(FirstTagBasedCategoryId + 2, "ui", () => Locs.Category_UI), @@ -353,11 +353,6 @@ internal class PluginCategoryManager /// Check if plugin testing is enabled. /// DoPluginTest, - - /// - /// Check if plugin profiles are enabled. - /// - ProfilesEnabled, } /// diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 036959233..dafc4cc3b 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1218,10 +1218,6 @@ internal class PluginInstallerWindow : Window, IDisposable if (!Service.Get().DoPluginTest) continue; break; - case PluginCategoryManager.CategoryInfo.AppearCondition.ProfilesEnabled: - if (!Service.Get().ProfilesEnabled) - continue; - break; default: throw new ArgumentOutOfRangeException(); } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 039877158..3f8f25f3e 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -50,6 +50,12 @@ internal class ProfileManagerWidget /// public void Draw() { + if (!Service.Get().ProfilesEnabled) + { + this.DrawChoice(); + return; + } + var tutorialTitle = Locs.TutorialTitle + "###collectionsTutorWindow"; var tutorialId = ImGui.GetID(tutorialTitle); this.DrawTutorial(tutorialTitle); @@ -76,6 +82,23 @@ internal class ProfileManagerWidget this.pickerSearch = string.Empty; } + private void DrawChoice() + { + ImGuiHelpers.ScaledDummy(60); + ImGuiHelpers.CenteredText(Locs.Choice1); + ImGuiHelpers.CenteredText(Locs.Choice2); + ImGuiHelpers.ScaledDummy(20); + + var buttonWidth = ImGui.GetWindowWidth() / 3; + ImGuiHelpers.CenterCursorFor((int)buttonWidth); + if (ImGui.Button(Locs.ChoiceConfirmation, new Vector2(buttonWidth, 40 * ImGuiHelpers.GlobalScale))) + { + var config = Service.Get(); + config.ProfilesEnabled = true; + config.QueueSave(); + } + } + private void DrawTutorial(string modalTitle) { var open = true; @@ -606,6 +629,15 @@ internal class ProfileManagerWidget public static string TutorialCommandsEnd => Loc.Localize("ProfileManagerTutorialCommandsEnd", "If you run multiple of these commands, they will be executed in order."); + public static string Choice1 => + Loc.Localize("ProfileManagerChoice1", "Plugin collections are a new feature that allow you to group plugins into collections which can be toggled and shared."); + + public static string Choice2 => + Loc.Localize("ProfileManagerChoice2", "They are experimental and may still contain bugs. Do you want to enable them now?"); + + public static string ChoiceConfirmation => + Loc.Localize("ProfileManagerChoiceConfirmation", "Yes, enable Plugin Collections"); + public static string NotInstalled(string name) => Loc.Localize("ProfileManagerNotInstalled", "{0} (Not Installed)").Format(name); } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index de9d1bae4..bd90d8509 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -48,6 +48,7 @@ public class SettingsTabExperimental : SettingsTab new ThirdRepoSettingsEntry(), + /* Disabling profiles after they've been enabled doesn't make much sense, at least not if the user has already created profiles. new GapSettingsEntry(5, true), new SettingsEntry( @@ -55,6 +56,7 @@ public class SettingsTabExperimental : SettingsTab Loc.Localize("DalamudSettingsEnableProfilesHint", "Enables plugin collections, which lets you create toggleable lists of plugins."), c => c.ProfilesEnabled, (v, c) => c.ProfilesEnabled = v), + */ }; public override string Title => Loc.Localize("DalamudSettingsExperimental", "Experimental"); From 5033a4b77037a74e93fc8171163c13eae44034f5 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 5 Oct 2023 21:54:07 +0200 Subject: [PATCH 242/585] fix: force IsHide to false for main repo api9 plugins makes outdated plugins show again when searched for --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 1 - Dalamud/Plugin/Internal/Types/PluginManifest.cs | 4 ++-- Dalamud/Plugin/Internal/Types/PluginRepository.cs | 9 +++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index dafc4cc3b..0cebf8207 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1909,7 +1909,6 @@ internal class PluginInstallerWindow : Window, IDisposable private void DrawAvailablePlugin(RemotePluginManifest manifest, int index) { var configuration = Service.Get(); - var notifications = Service.Get(); var pluginManager = Service.Get(); var useTesting = pluginManager.UseTesting(manifest); diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index 34fa04f6e..baaf37558 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -42,11 +42,11 @@ internal record PluginManifest : IPluginManifest public List? CategoryTags { get; init; } /// - /// Gets a value indicating whether or not the plugin is hidden in the plugin installer. + /// Gets or sets a value indicating whether or not the plugin is hidden in the plugin installer. /// This value comes from the plugin master and is in addition to the list of hidden names kept by Dalamud. /// [JsonProperty] - public bool IsHide { get; init; } + public bool IsHide { get; set; } /// [JsonProperty] diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index a1097abce..aae603f42 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -148,6 +148,15 @@ internal class PluginRepository } this.PluginMaster = pluginMaster.Where(this.IsValidManifest).ToList().AsReadOnly(); + + // API9 HACK: Force IsHide to false, we should remove that + if (!this.IsThirdParty) + { + foreach (var manifest in this.PluginMaster) + { + manifest.IsHide = false; + } + } Log.Information($"Successfully fetched repo: {this.PluginMasterUrl}"); this.State = PluginRepositoryState.Success; From e4d175d5cb46dfe93f2182c577c03c77d6ec6492 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 5 Oct 2023 21:54:28 +0200 Subject: [PATCH 243/585] build: 9.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 da1ef3f6a..db7396fe0 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.0 + 9.0.0.1 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 2405dca9e10208fad1cde73cde08aceb8263f8b1 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 5 Oct 2023 14:10:24 -0700 Subject: [PATCH 244/585] fix: Fix a bug where flashIfOpen's behavior was inverted. (#1468) --- Dalamud/Utility/Util.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 782d2ab37..d5f59a06e 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -620,7 +620,7 @@ public static class Util /// Attempt to flash even if the game is currently focused. public static void FlashWindow(bool flashIfOpen = false) { - if (NativeFunctions.ApplicationIsActivated() && flashIfOpen) + if (NativeFunctions.ApplicationIsActivated() && !flashIfOpen) return; var flashInfo = new NativeFunctions.FlashWindowInfo From db5b9d1b8331182ab41254e32b76f0a9dec06dfb Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 5 Oct 2023 23:15:34 +0200 Subject: [PATCH 245/585] fix: BaseAddressResolver should take a ISigScanner instead --- .../Addon/Events/AddonEventManagerAddressResolver.cs | 2 +- .../Addon/Lifecycle/AddonLifecycleAddressResolver.cs | 2 +- Dalamud/Game/BaseAddressResolver.cs | 10 +++++----- Dalamud/Game/ClientState/ClientStateAddressResolver.cs | 2 +- Dalamud/Game/Config/GameConfigAddressResolver.cs | 2 +- Dalamud/Game/DutyState/DutyStateAddressResolver.cs | 2 +- Dalamud/Game/FrameworkAddressResolver.cs | 4 ++-- Dalamud/Game/Gui/ChatGuiAddressResolver.cs | 2 +- Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs | 2 +- Dalamud/Game/Gui/GameGuiAddressResolver.cs | 2 +- .../Game/Gui/PartyFinder/PartyFinderAddressResolver.cs | 2 +- Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs | 2 +- Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs | 2 +- Dalamud/Game/Libc/LibcFunctionAddressResolver.cs | 2 +- Dalamud/Game/Network/GameNetworkAddressResolver.cs | 2 +- .../Network/Internal/NetworkHandlersAddressResolver.cs | 2 +- 16 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs index 1405446fe..927ed87ab 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManagerAddressResolver.cs @@ -14,7 +14,7 @@ internal class AddonEventManagerAddressResolver : BaseAddressResolver /// Scan for and setup any configured address pointers. /// /// The signature scanner to facilitate setup. - protected override void Setup64Bit(SigScanner scanner) + protected override void Setup64Bit(ISigScanner scanner) { this.UpdateCursor = scanner.ScanText("48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 20 4C 8B F1 E8 ?? ?? ?? ?? 49 8B CE"); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index ff694c84d..c308d1676 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -48,7 +48,7 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver /// Scan for and setup any configured address pointers. /// /// The signature scanner to facilitate setup. - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { this.AddonSetup = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 93 ?? ?? ?? ?? 80 8B"); this.AddonSetup2 = sig.ScanText("FF 90 ?? ?? ?? ?? 48 8B 03 48 8B CB 80 8B"); diff --git a/Dalamud/Game/BaseAddressResolver.cs b/Dalamud/Game/BaseAddressResolver.cs index 814e5ed09..7a455aea0 100644 --- a/Dalamud/Game/BaseAddressResolver.cs +++ b/Dalamud/Game/BaseAddressResolver.cs @@ -18,7 +18,7 @@ public abstract class BaseAddressResolver public static Dictionary> DebugScannedValues { get; } = new(); /// - /// Gets or sets a value indicating whether the resolver has successfully run or . + /// Gets or sets a value indicating whether the resolver has successfully run or . /// protected bool IsResolved { get; set; } @@ -26,7 +26,7 @@ public abstract class BaseAddressResolver /// Setup the resolver, calling the appropriate method based on the process architecture. /// /// The SigScanner instance. - public void Setup(SigScanner scanner) + public void Setup(ISigScanner scanner) { // Because C# don't allow to call virtual function while in ctor // we have to do this shit :\ @@ -83,7 +83,7 @@ public abstract class BaseAddressResolver /// Setup the resolver by finding any necessary memory addresses. /// /// The SigScanner instance. - protected virtual void Setup32Bit(SigScanner scanner) + protected virtual void Setup32Bit(ISigScanner scanner) { throw new NotSupportedException("32 bit version is not supported."); } @@ -92,7 +92,7 @@ public abstract class BaseAddressResolver /// Setup the resolver by finding any necessary memory addresses. /// /// The SigScanner instance. - protected virtual void Setup64Bit(SigScanner scanner) + protected virtual void Setup64Bit(ISigScanner scanner) { throw new NotSupportedException("64 bit version is not supported."); } @@ -101,7 +101,7 @@ public abstract class BaseAddressResolver /// Setup the resolver by finding any necessary memory addresses. /// /// The SigScanner instance. - protected virtual void SetupInternal(SigScanner scanner) + protected virtual void SetupInternal(ISigScanner scanner) { // Do nothing } diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 305dda454..73ed24e95 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -79,7 +79,7 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver /// Scan for and setup any configured address pointers. /// /// The signature scanner to facilitate setup. - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { this.ObjectTable = sig.GetStaticAddressFromSig("48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 83 ?? ?? ?? ?? C6 83 ?? ?? ?? ?? ??"); diff --git a/Dalamud/Game/Config/GameConfigAddressResolver.cs b/Dalamud/Game/Config/GameConfigAddressResolver.cs index 674ee4764..c171932a9 100644 --- a/Dalamud/Game/Config/GameConfigAddressResolver.cs +++ b/Dalamud/Game/Config/GameConfigAddressResolver.cs @@ -11,7 +11,7 @@ internal sealed class GameConfigAddressResolver : BaseAddressResolver public nint ConfigChangeAddress { get; private set; } /// - protected override void Setup64Bit(SigScanner scanner) + protected override void Setup64Bit(ISigScanner scanner) { this.ConfigChangeAddress = scanner.ScanText("E8 ?? ?? ?? ?? 48 8B 3F 49 3B 3E"); } diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs index 772af79a8..c7160bddb 100644 --- a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs +++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs @@ -14,7 +14,7 @@ internal class DutyStateAddressResolver : BaseAddressResolver /// Scan for and setup any configured address pointers. /// /// The signature scanner to facilitate setup. - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { this.ContentDirectorNetworkMessage = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8B D9 49 8B F8 41 0F B7 08"); } diff --git a/Dalamud/Game/FrameworkAddressResolver.cs b/Dalamud/Game/FrameworkAddressResolver.cs index c47469a01..39ae15155 100644 --- a/Dalamud/Game/FrameworkAddressResolver.cs +++ b/Dalamud/Game/FrameworkAddressResolver.cs @@ -23,12 +23,12 @@ internal sealed class FrameworkAddressResolver : BaseAddressResolver public IntPtr TickAddress { get; private set; } /// - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { this.SetupFramework(sig); } - private void SetupFramework(SigScanner scanner) + private void SetupFramework(ISigScanner scanner) { this.DestroyAddress = scanner.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 20 48 8B 3D ?? ?? ?? ?? 48 8B D9 48 85 FF"); diff --git a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs index 494e0b3ed..d653ec146 100644 --- a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs @@ -83,7 +83,7 @@ internal sealed class ChatGuiAddressResolver : BaseAddressResolver */ /// - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1??? this.PrintMessage = sig.ScanText("40 55 53 56 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC 20 02 00 00 48 8B 05"); diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs b/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs index 677d92e57..c4bdc8dd5 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGuiAddressResolver.cs @@ -21,7 +21,7 @@ internal class FlyTextGuiAddressResolver : BaseAddressResolver public IntPtr CreateFlyText { get; private set; } /// - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { this.AddFlyText = sig.ScanText("E8 ?? ?? ?? ?? FF C7 41 D1 C7"); this.CreateFlyText = sig.ScanText("40 53 55 41 56 48 83 EC 40 48 63 EA"); diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs index acb5539f6..cbed42a65 100644 --- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs @@ -58,7 +58,7 @@ internal sealed class GameGuiAddressResolver : BaseAddressResolver public IntPtr Utf8StringFromSequence { get; private set; } /// - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { this.SetGlobalBgm = sig.ScanText("4C 8B 15 ?? ?? ?? ?? 4D 85 D2 74 58"); this.HandleItemHover = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 5C 24 ?? 48 89 AE ?? ?? ?? ?? 48 89 AE ?? ?? ?? ??"); diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs index c12721358..9cfbd8a12 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderAddressResolver.cs @@ -11,7 +11,7 @@ internal class PartyFinderAddressResolver : BaseAddressResolver public IntPtr ReceiveListing { get; private set; } /// - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { this.ReceiveListing = sig.ScanText("40 53 41 57 48 83 EC 28 48 8B D9"); } diff --git a/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs b/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs index ae5426023..0a8775540 100644 --- a/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/Toast/ToastGuiAddressResolver.cs @@ -21,7 +21,7 @@ internal class ToastGuiAddressResolver : BaseAddressResolver public IntPtr ShowErrorToast { get; private set; } /// - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { this.ShowNormalToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 83 3D ?? ?? ?? ?? ??"); this.ShowQuestToast = sig.ScanText("48 89 5C 24 ?? 48 89 6C 24 ?? 48 89 74 24 ?? 48 89 7C 24 ?? 41 56 48 83 EC 40 83 3D ?? ?? ?? ?? ??"); diff --git a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs index 50aae26ed..02ab2b59c 100644 --- a/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs +++ b/Dalamud/Game/Internal/DXGI/SwapChainVtableResolver.cs @@ -28,7 +28,7 @@ internal class SwapChainVtableResolver : BaseAddressResolver, ISwapChainAddressR public bool IsReshade { get; private set; } /// - protected override unsafe void Setup64Bit(SigScanner sig) + protected override unsafe void Setup64Bit(ISigScanner sig) { Device* kernelDev; SwapChain* swapChain; diff --git a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs index 4c3b7cdf8..3b8742678 100644 --- a/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs +++ b/Dalamud/Game/Libc/LibcFunctionAddressResolver.cs @@ -20,7 +20,7 @@ internal sealed class LibcFunctionAddressResolver : BaseAddressResolver public IntPtr StdStringDeallocate { get; private set; } /// - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { this.StdStringFromCstring = sig.ScanText("48 89 5C 24 08 48 89 74 24 10 57 48 83 EC 20 48 8D 41 22 66 C7 41 20 01 01 48 89 01 49 8B D8"); this.StdStringDeallocate = sig.ScanText("80 79 21 00 75 12 48 8B 51 08 41 B8 33 00 00 00 48 8B 09 E9 ?? ?? ?? 00 C3"); diff --git a/Dalamud/Game/Network/GameNetworkAddressResolver.cs b/Dalamud/Game/Network/GameNetworkAddressResolver.cs index fa6af8c93..f8a1b278d 100644 --- a/Dalamud/Game/Network/GameNetworkAddressResolver.cs +++ b/Dalamud/Game/Network/GameNetworkAddressResolver.cs @@ -16,7 +16,7 @@ internal sealed class GameNetworkAddressResolver : BaseAddressResolver public IntPtr ProcessZonePacketUp { get; private set; } /// - protected override void Setup64Bit(SigScanner sig) + protected override void Setup64Bit(ISigScanner sig) { // ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 7A FF 0F B7 57 02 8D 42 89 3D 5F 02 00 00 0F 87 60 01 00 00 4C 8D 05"); // ProcessZonePacket = sig.ScanText("48 89 74 24 18 57 48 83 EC 50 8B F2 49 8B F8 41 0F B7 50 02 8B CE E8 ?? ?? 73 FF 0F B7 57 02 8D 42 ?? 3D ?? ?? 00 00 0F 87 60 01 00 00 4C 8D 05"); diff --git a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs index 8b4788c74..cf47981c2 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlersAddressResolver.cs @@ -45,7 +45,7 @@ internal class NetworkHandlersAddressResolver : BaseAddressResolver public nint BuildMarketBoardPurchaseHandlerPacket { get; set; } /// - protected override void Setup64Bit(SigScanner scanner) + protected override void Setup64Bit(ISigScanner scanner) { this.CfPopPacketHandler = scanner.ScanText("40 53 57 48 83 EC 78 48 8B D9 48 8D 0D"); this.MarketBoardHistoryPacketHandler = scanner.ScanText( From 2b47ca42bc1024db645af4897971dbe607d6850c Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 5 Oct 2023 23:17:23 +0200 Subject: [PATCH 246/585] fix: add some more words on the changelog intro page --- Dalamud/Interface/Internal/Windows/ChangelogWindow.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 45ad215d4..4d1a8b5f0 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -258,6 +258,9 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.TextWrapped($"Welcome to Dalamud v{Util.AssemblyVersion}!"); ImGuiHelpers.ScaledDummy(5); ImGui.TextWrapped(ChangeLog); + ImGuiHelpers.ScaledDummy(5); + ImGui.TextWrapped("This changelog is a quick overview of the most important changes in this version."); + ImGui.TextWrapped("Please click next to see a quick guide to updating your plugins."); DrawNextButton(State.ExplainerApiBump); break; From a7f134b14eb0ff14098ad030e323e0e29b72af0c Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 5 Oct 2023 23:17:37 +0200 Subject: [PATCH 247/585] build: 9.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 db7396fe0..ec95a61a5 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.1 + 9.0.0.2 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 392d9ac884370238c89583f726561cd4d3817040 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 5 Oct 2023 23:58:04 -0700 Subject: [PATCH 248/585] fix: Don't generate dummy sale entries if no sales are present. (#1471) - Fixes a bug that's apparently been around for a while that submitted invalid data to Universalis. --- .../Game/Network/Structures/MarketBoardHistory.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Network/Structures/MarketBoardHistory.cs b/Dalamud/Game/Network/Structures/MarketBoardHistory.cs index 69532afd6..9a61b814e 100644 --- a/Dalamud/Game/Network/Structures/MarketBoardHistory.cs +++ b/Dalamud/Game/Network/Structures/MarketBoardHistory.cs @@ -42,10 +42,17 @@ public class MarketBoardHistory using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 1544); using var reader = new BinaryReader(stream); - var output = new MarketBoardHistory(); + var output = new MarketBoardHistory + { + CatalogId = reader.ReadUInt32(), + CatalogId2 = reader.ReadUInt32(), + }; - output.CatalogId = reader.ReadUInt32(); - output.CatalogId2 = reader.ReadUInt32(); + if (output.CatalogId2 == 0) + { + // No items found in the resulting packet - just return the empty history. + return output; + } for (var i = 0; i < 20; i++) { From 5cc7d4ed557cc756fd939d25fc8691a0f7a9d1cf Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 6 Oct 2023 22:31:58 +0200 Subject: [PATCH 249/585] Update ClientStructs (#1469) 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 c9daf7d4b..6c20599c7 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit c9daf7d4b98c131e7272fc755309c8830c67815f +Subproject commit 6c20599c7f34a154f93ce4142bb3f59f79a936d4 From 9fe5ca3214292ef63903b7d17e87a4266cdd1c90 Mon Sep 17 00:00:00 2001 From: foophoof Date: Sat, 7 Oct 2023 00:11:44 +0100 Subject: [PATCH 250/585] Show warning before deleting plugin data (#1474) --- .../PluginInstaller/PluginInstallerWindow.cs | 127 +++++++++++++++--- 1 file changed, 105 insertions(+), 22 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0cebf8207..36db58989 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -90,6 +90,11 @@ internal class PluginInstallerWindow : Window, IDisposable private bool testingWarningModalDrawing = true; private bool testingWarningModalOnNextFrame = false; + private bool deletePluginConfigWarningModalDrawing = true; + private bool deletePluginConfigWarningModalOnNextFrame = false; + private string deletePluginConfigWarningModalPluginName = string.Empty; + private TaskCompletionSource? deletePluginConfigWarningModalTaskCompletionSource; + private bool feedbackModalDrawing = true; private bool feedbackModalOnNextFrame = false; private bool feedbackModalOnNextFrameDontClear = false; @@ -259,6 +264,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.DrawErrorModal(); this.DrawUpdateModal(); this.DrawTestingWarningModal(); + this.DrawDeletePluginConfigWarningModal(); this.DrawFeedbackModal(); this.DrawProgressOverlay(); } @@ -825,6 +831,55 @@ internal class PluginInstallerWindow : Window, IDisposable } } + private Task ShowDeletePluginConfigWarningModal(string pluginName) + { + this.deletePluginConfigWarningModalOnNextFrame = true; + this.deletePluginConfigWarningModalPluginName = pluginName; + this.deletePluginConfigWarningModalTaskCompletionSource = new TaskCompletionSource(); + return this.deletePluginConfigWarningModalTaskCompletionSource.Task; + } + + private void DrawDeletePluginConfigWarningModal() + { + var modalTitle = Locs.DeletePluginConfigWarningModal_Title; + + if (ImGui.BeginPopupModal(modalTitle, ref this.deletePluginConfigWarningModalDrawing, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar)) + { + ImGui.Text(Locs.DeletePluginConfigWarningModal_Body(this.deletePluginConfigWarningModalPluginName)); + ImGui.Spacing(); + + var buttonWidth = 120f; + ImGui.SetCursorPosX((ImGui.GetWindowWidth() - ((buttonWidth * 2) - (ImGui.GetStyle().ItemSpacing.Y * 2))) / 2); + + if (ImGui.Button(Locs.DeletePluginConfirmWarningModal_Yes, new Vector2(buttonWidth, 40))) + { + ImGui.CloseCurrentPopup(); + this.deletePluginConfigWarningModalTaskCompletionSource?.SetResult(true); + } + + ImGui.SameLine(); + + if (ImGui.Button(Locs.DeletePluginConfirmWarningModal_No, new Vector2(buttonWidth, 40))) + { + ImGui.CloseCurrentPopup(); + this.deletePluginConfigWarningModalTaskCompletionSource?.SetResult(false); + } + + ImGui.EndPopup(); + } + + if (this.deletePluginConfigWarningModalOnNextFrame) + { + // NOTE(goat): ImGui cannot open a modal if no window is focused, at the moment. + // If people click out of the installer into the game while a plugin is installing, we won't be able to show a modal if we don't grab focus. + ImGui.SetWindowFocus(this.WindowName); + + ImGui.OpenPopup(modalTitle); + this.deletePluginConfigWarningModalOnNextFrame = false; + this.deletePluginConfigWarningModalDrawing = true; + } + } + private void DrawFeedbackModal() { var modalTitle = Locs.FeedbackModal_Title; @@ -2044,24 +2099,32 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.Selectable(Locs.PluginContext_DeletePluginConfig)) { - Log.Debug($"Deleting config for {manifest.InternalName}"); + this.ShowDeletePluginConfigWarningModal(manifest.Name).ContinueWith(t => + { + var shouldDelete = t.Result; - this.installStatus = OperationStatus.InProgress; - - Task.Run(() => + if (shouldDelete) { - pluginManager.PluginConfigs.Delete(manifest.InternalName); - var dir = pluginManager.PluginConfigs.GetDirectory(manifest.InternalName); + Log.Debug($"Deleting config for {manifest.InternalName}"); - if (Directory.Exists(dir)) - Directory.Delete(dir, true); - }) - .ContinueWith(task => - { - this.installStatus = OperationStatus.Idle; + this.installStatus = OperationStatus.InProgress; - this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(manifest.InternalName)); - }); + Task.Run(() => + { + pluginManager.PluginConfigs.Delete(manifest.InternalName); + var dir = pluginManager.PluginConfigs.GetDirectory(manifest.InternalName); + + if (Directory.Exists(dir)) + Directory.Delete(dir, true); + }) + .ContinueWith(task => + { + this.installStatus = OperationStatus.Idle; + + this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(manifest.InternalName)); + }); + } + }); } ImGui.EndPopup(); @@ -2384,17 +2447,25 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.MenuItem(Locs.PluginContext_DeletePluginConfigReload)) { - Log.Debug($"Deleting config for {plugin.Manifest.InternalName}"); + this.ShowDeletePluginConfigWarningModal(plugin.Name).ContinueWith(t => + { + var shouldDelete = t.Result; - this.installStatus = OperationStatus.InProgress; - - Task.Run(() => pluginManager.DeleteConfigurationAsync(plugin)) - .ContinueWith(task => + if (shouldDelete) { - this.installStatus = OperationStatus.Idle; + Log.Debug($"Deleting config for {plugin.Manifest.InternalName}"); - this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Name)); - }); + this.installStatus = OperationStatus.InProgress; + + Task.Run(() => pluginManager.DeleteConfigurationAsync(plugin)) + .ContinueWith(task => + { + this.installStatus = OperationStatus.Idle; + + this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Name)); + }); + } + }); } ImGui.EndPopup(); @@ -3578,6 +3649,18 @@ internal class PluginInstallerWindow : Window, IDisposable #endregion + #region Delete Plugin Config Warning Modal + + public static string DeletePluginConfigWarningModal_Title => Loc.Localize("InstallerDeletePluginConfigWarning", "Warning###InstallerDeletePluginConfigWarning"); + + public static string DeletePluginConfigWarningModal_Body(string pluginName) => Loc.Localize("InstallerDeletePluginConfigWarningBody", "Are you sure you want to delete all data and configuration for v{0}?").Format(pluginName); + + public static string DeletePluginConfirmWarningModal_Yes => Loc.Localize("InstallerDeletePluginConfigWarningYes", "Yes"); + + public static string DeletePluginConfirmWarningModal_No => Loc.Localize("InstallerDeletePluginConfigWarningNo", "No"); + + #endregion + #region Plugin Update chatbox public static string PluginUpdateHeader_Chatbox => Loc.Localize("DalamudPluginUpdates", "Updates:"); From c8906c674fd7291b76eb4de43c68d505187ac60a Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 7 Oct 2023 16:29:12 -0700 Subject: [PATCH 251/585] ConsoleWindow Copy Timestamp Fix (#1470) --- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 6bb8ea25e..595b09fef 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -322,7 +322,7 @@ internal class ConsoleWindow : Window, IDisposable { var allSelectedLines = this.FilteredLogEntries .Where(entry => entry.SelectedForCopy) - .Select(entry => $"{line.TimeStamp:HH:mm:ss.fff} {this.GetTextForLogEventLevel(entry.Level)} | {entry.Line}"); + .Select(entry => $"{entry.TimeStamp:HH:mm:ss.fff} {this.GetTextForLogEventLevel(entry.Level)} | {entry.Line}"); ImGui.SetClipboardText(string.Join("\n", allSelectedLines)); } From df2474bc87a8c3ce2ce198c77b501e90b74c0cb2 Mon Sep 17 00:00:00 2001 From: srkizer Date: Tue, 10 Oct 2023 00:37:16 +0900 Subject: [PATCH 252/585] Disable Utils.Show... from attempting to display Spans (#1467) --- Dalamud/Utility/Util.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index d5f59a06e..fb6c854a1 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -29,6 +29,7 @@ namespace Dalamud.Utility; /// public static class Util { + private static readonly Type GenericSpanType = typeof(Span<>); private static string? gitHashInternal; private static int? gitCommitCountInternal; private static string? gitHashClientStructsInternal; @@ -296,7 +297,10 @@ public static class Util ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.4f, 1), $"{f.Name}: "); ImGui.SameLine(); - ShowValue(addr, new List(path) {f.Name}, f.FieldType, f.GetValue(obj)); + if (f.FieldType.IsGenericType && f.FieldType.GetGenericTypeDefinition() == GenericSpanType) + ImGui.Text("Span preview is currently not supported."); + else + ShowValue(addr, new List(path) {f.Name}, f.FieldType, f.GetValue(obj)); } foreach (var p in obj.GetType().GetProperties().Where(p => p.GetGetMethod()?.GetParameters().Length == 0)) @@ -306,7 +310,10 @@ public static class Util ImGui.TextColored(new Vector4(0.2f, 0.6f, 0.4f, 1), $"{p.Name}: "); ImGui.SameLine(); - ShowValue(addr, new List(path) {p.Name}, p.PropertyType, p.GetValue(obj)); + if (p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == GenericSpanType) + ImGui.Text("Span preview is currently not supported."); + else + ShowValue(addr, new List(path) {p.Name}, p.PropertyType, p.GetValue(obj)); } ImGui.TreePop(); @@ -369,6 +376,13 @@ public static class Util foreach (var propertyInfo in type.GetProperties().Where(p => p.GetGetMethod()?.GetParameters().Length == 0)) { + if (propertyInfo.PropertyType.IsGenericType && + propertyInfo.PropertyType.GetGenericTypeDefinition() == GenericSpanType) + { + ImGui.TextColored(ImGuiColors.DalamudOrange, $" {propertyInfo.Name}: Span preview is currently not supported."); + continue; + } + var value = propertyInfo.GetValue(obj); var valueType = value?.GetType(); if (valueType == typeof(IntPtr)) From b6897f3a8b06ff6c5aa77bf215d33846a2105cf4 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:40:06 -0700 Subject: [PATCH 253/585] Fix crash on regex error (#1477) * Fix crash on regex error * Don't clear copylog flag until the end of the draw --- .../Internal/Windows/ConsoleWindow.cs | 208 ++++++++++-------- 1 file changed, 112 insertions(+), 96 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 595b09fef..63045ed36 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -1,6 +1,6 @@ -using System; using System.Collections.Generic; using System.Diagnostics; +using System.Drawing; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; @@ -12,6 +12,7 @@ using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; @@ -47,6 +48,7 @@ internal class ConsoleWindow : Window, IDisposable private bool killGameArmed; private bool autoScroll; private bool autoOpen; + private bool regexError; private int historyPos; private int copyStart = -1; @@ -111,7 +113,6 @@ internal class ConsoleWindow : Window, IDisposable public void CopyLog() { ImGui.LogToClipboard(); - this.copyLog = false; } /// @@ -145,6 +146,13 @@ internal class ConsoleWindow : Window, IDisposable this.DrawFilterToolbar(); + if (this.regexError) + { + const string regexErrorString = "Regex Filter Error"; + ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f); + ImGui.TextColored(KnownColor.OrangeRed.Vector(), regexErrorString); + } + ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55 * ImGuiHelpers.GlobalScale), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); if (this.clearLog) this.Clear(); @@ -276,6 +284,8 @@ internal class ConsoleWindow : Window, IDisposable { this.ProcessCommand(); } + + this.copyLog = false; } private void HandleCopyMode(int i, LogEntry line) @@ -440,44 +450,75 @@ internal class ConsoleWindow : Window, IDisposable if (!this.showFilterToolbar) return; PluginFilterEntry? removalEntry = null; - if (ImGui.BeginTable("plugin_filter_entries", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV)) + using var table = ImRaii.Table("plugin_filter_entries", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV); + if (!table) return; + + ImGui.TableSetupColumn("##remove_button", ImGuiTableColumnFlags.WidthFixed, 25.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##source_name", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##log_level", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##filter_text", ImGuiTableColumnFlags.WidthStretch); + + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton("add_entry", FontAwesomeIcon.Plus)) { - ImGui.TableSetupColumn("##remove_button", ImGuiTableColumnFlags.WidthFixed, 25.0f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##source_name", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##log_level", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("##filter_text", ImGuiTableColumnFlags.WidthStretch); - - ImGui.TableNextColumn(); - if (ImGuiComponents.IconButton("add_entry", FontAwesomeIcon.Plus)) + if (this.pluginFilters.All(entry => entry.Source != this.selectedSource)) { - if (this.pluginFilters.All(entry => entry.Source != this.selectedSource)) + this.pluginFilters.Add(new PluginFilterEntry { - this.pluginFilters.Add(new PluginFilterEntry - { - Source = this.selectedSource, - Filter = string.Empty, - Level = LogEventLevel.Debug, - }); - } + Source = this.selectedSource, + Filter = string.Empty, + Level = LogEventLevel.Debug, + }); + } - this.Refilter(); + this.Refilter(); + } + + ImGui.TableNextColumn(); + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.BeginCombo("##Sources", this.selectedSource)) + { + var sourceNames = Service.Get().InstalledPlugins + .Select(p => p.Manifest.InternalName) + .OrderBy(s => s) + .Prepend("DalamudInternal") + .ToList(); + + foreach (var selectable in sourceNames) + { + if (ImGui.Selectable(selectable, this.selectedSource == selectable)) + { + this.selectedSource = selectable; + } + } + + ImGui.EndCombo(); + } + + ImGui.TableNextColumn(); + ImGui.TableNextColumn(); + + foreach (var entry in this.pluginFilters) + { + ImGui.TableNextColumn(); + if (ImGuiComponents.IconButton($"remove{entry.Source}", FontAwesomeIcon.Trash)) + { + removalEntry = entry; } ImGui.TableNextColumn(); - ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.BeginCombo("##Sources", this.selectedSource)) + ImGui.Text(entry.Source); + + ImGui.TableNextColumn(); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.BeginCombo($"##levels{entry.Source}", $"{entry.Level}+")) { - var sourceNames = Service.Get().InstalledPlugins - .Select(p => p.Manifest.InternalName) - .OrderBy(s => s) - .Prepend("DalamudInternal") - .ToList(); - - foreach (var selectable in sourceNames) + foreach (var value in Enum.GetValues()) { - if (ImGui.Selectable(selectable, this.selectedSource == selectable)) + if (ImGui.Selectable(value.ToString(), value == entry.Level)) { - this.selectedSource = selectable; + entry.Level = value; + this.Refilter(); } } @@ -485,48 +526,15 @@ internal class ConsoleWindow : Window, IDisposable } ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - - foreach (var entry in this.pluginFilters) + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var entryFilter = entry.Filter; + if (ImGui.InputTextWithHint($"##filter{entry.Source}", $"{entry.Source} regex filter", ref entryFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)) { - ImGui.TableNextColumn(); - if (ImGuiComponents.IconButton($"remove{entry.Source}", FontAwesomeIcon.Trash)) - { - removalEntry = entry; - } - - ImGui.TableNextColumn(); - ImGui.Text(entry.Source); - - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.BeginCombo($"##levels{entry.Source}", $"{entry.Level}+")) - { - foreach (var value in Enum.GetValues()) - { - if (ImGui.Selectable(value.ToString(), value == entry.Level)) - { - entry.Level = value; - this.Refilter(); - } - } - - ImGui.EndCombo(); - } - - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - var entryFilter = entry.Filter; - if (ImGui.InputTextWithHint($"##filter{entry.Source}", $"{entry.Source} regex filter", ref entryFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)) - { - entry.Filter = entryFilter; - this.Refilter(); - } - - if (ImGui.IsItemDeactivatedAfterEdit()) this.Refilter(); + entry.Filter = entryFilter; + this.Refilter(); } - ImGui.EndTable(); + if (ImGui.IsItemDeactivatedAfterEdit()) this.Refilter(); } if (removalEntry is { } toRemove) @@ -540,7 +548,7 @@ internal class ConsoleWindow : Window, IDisposable { try { - if (this.commandText is['/', ..]) + if (this.commandText.StartsWith('/')) { this.commandText = this.commandText[1..]; } @@ -654,13 +662,11 @@ internal class ConsoleWindow : Window, IDisposable HasException = logEvent.Exception != null, }; - // TODO (v9): Remove SourceContext property check. if (logEvent.Properties.ContainsKey("Dalamud.ModuleName")) { entry.Source = "DalamudInternal"; } - else if ((logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) || - logEvent.Properties.TryGetValue("SourceContext", out sourceProp)) && + else if (logEvent.Properties.TryGetValue("Dalamud.PluginName", out var sourceProp) && sourceProp is ScalarValue { Value: string sourceValue }) { entry.Source = sourceValue; @@ -674,34 +680,44 @@ internal class ConsoleWindow : Window, IDisposable private bool IsFilterApplicable(LogEntry entry) { - // If this entry is below a newly set minimum level, fail it - if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level) - return false; + try + { + // If this entry is below a newly set minimum level, fail it + if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level) + return false; - // Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught) - // After log levels because uncaught exceptions should *never* fall below Error. - if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null) - return true; + // Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught) + // After log levels because uncaught exceptions should *never* fall below Error. + if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null) + return true; - // If we have a global filter, check that first - if (!this.textFilter.IsNullOrEmpty()) + // If we have a global filter, check that first + if (!this.textFilter.IsNullOrEmpty()) + { + // Someone will definitely try to just text filter a source without using the actual filters, should allow that. + var matchesSource = entry.Source is not null && Regex.IsMatch(entry.Source, this.textFilter, RegexOptions.IgnoreCase); + var matchesContent = Regex.IsMatch(entry.Line, this.textFilter, RegexOptions.IgnoreCase); + + return matchesSource || matchesContent; + } + + // If this entry has a filter, check the filter + if (this.pluginFilters.FirstOrDefault(filter => string.Equals(filter.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase)) is { } filterEntry) + { + var allowedLevel = filterEntry.Level <= entry.Level; + var matchesContent = filterEntry.Filter.IsNullOrEmpty() || Regex.IsMatch(entry.Line, filterEntry.Filter, RegexOptions.IgnoreCase); + + return allowedLevel && matchesContent; + } + } + catch (Exception) { - // Someone will definitely try to just text filter a source without using the actual filters, should allow that. - var matchesSource = entry.Source is not null && Regex.IsMatch(entry.Source, this.textFilter, RegexOptions.IgnoreCase); - var matchesContent = Regex.IsMatch(entry.Line, this.textFilter, RegexOptions.IgnoreCase); - - return matchesSource || matchesContent; - } - - // If this entry has a filter, check the filter - if (this.pluginFilters.FirstOrDefault(filter => string.Equals(filter.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase)) is { } filterEntry) - { - var allowedLevel = filterEntry.Level <= entry.Level; - var matchesContent = filterEntry.Filter.IsNullOrEmpty() || Regex.IsMatch(entry.Line, filterEntry.Filter, RegexOptions.IgnoreCase); - - return allowedLevel && matchesContent; + this.regexError = true; + return false; } + this.regexError = false; + // else we couldn't find a filter for this entry, if we have any filters, we need to block this entry. return !this.pluginFilters.Any(); } From 445c84b55647b530cafba07a44d0f5b08df9f11b Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:41:51 -0700 Subject: [PATCH 254/585] Add support disclaimer (#1478) --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 36db58989..13db3509d 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3703,7 +3703,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string VerifiedCheckmark_UnverifiedTooltip => Loc.Localize("VerifiedCheckmarkUnverifiedTooltip", "This plugin has not been reviewed by the Dalamud team.\n" + "We cannot take any responsibility for custom plugins and repositories.\n" + - "Please make absolutely sure that you only install plugins from developers you trust."); + "Please make absolutely sure that you only install plugins from developers you trust.\n\n" + + "You will not receive support for plugins installed from custom repositories on the XIVLauncher & Dalamud server."); #endregion } From 21d99be7d4d526a193fb2a4e9115752eb2c58f98 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:47:50 +0200 Subject: [PATCH 255/585] Update ClientStructs (#1475) 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 6c20599c7..69bbc4c83 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 6c20599c7f34a154f93ce4142bb3f59f79a936d4 +Subproject commit 69bbc4c83436dba2fa7fa432eda960c6e1ab3294 From 8be1e4b8efd531fae2ac41bfdae7aec25f2e5e0e Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 9 Oct 2023 22:06:35 +0200 Subject: [PATCH 256/585] fix: ignore the default profile when installing a plugin --- Dalamud/Plugin/Internal/PluginManager.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index ac808df89..51e994c69 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -784,6 +784,19 @@ internal partial class PluginManager : IDisposable, IServiceType public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason, Guid? inheritedWorkingPluginId = null) { Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})"); + + // If this plugin is in the default profile for whatever reason, delete the state + // If it was in multiple profiles and is still, the user uninstalled it and chose to keep it in there, + // or the user removed the plugin manually in which case we don't care + var defaultProfile = this.profileManager.DefaultProfile; + using (defaultProfile.GetSyncScope()) + { + if (defaultProfile.WantsPlugin(repoManifest.InternalName).HasValue) + { + // We don't need to apply, it doesn't matter + await defaultProfile.RemoveAsync(repoManifest.InternalName, false); + } + } // Ensure that we have a testing opt-in for this plugin if we are installing a testing version if (useTesting && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != repoManifest.InternalName)) From d15e35f3f68382b4a44967e7b2af7243c61d2e79 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 9 Oct 2023 13:10:49 -0700 Subject: [PATCH 257/585] AddonLifecycle Add AddonReceiveEvent (#1473) --- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 2 +- .../AddonArgTypes/AddonFinalizeArgs.cs | 2 +- .../AddonArgTypes/AddonReceiveEventArgs.cs | 30 ++++++ .../AddonArgTypes/AddonRefreshArgs.cs | 2 +- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 2 +- .../AddonArgTypes/AddonUpdateArgs.cs | 2 +- Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs | 5 + Dalamud/Game/Addon/Lifecycle/AddonEvent.cs | 10 ++ .../Game/Addon/Lifecycle/AddonLifecycle.cs | 96 ++++++++++++++++++- 9 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 6bb72f567..10d46a573 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -1,7 +1,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// -/// Addon argument data for Finalize events. +/// Addon argument data for Draw events. /// public class AddonDrawArgs : AddonArgs { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs index 782943955..caf422927 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -1,7 +1,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// -/// Addon argument data for Finalize events. +/// Addon argument data for ReceiveEvent events. /// public class AddonFinalizeArgs : AddonArgs { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs new file mode 100644 index 000000000..df75307f1 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -0,0 +1,30 @@ +namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; + +/// +/// Addon argument data for ReceiveEvent events. +/// +public class AddonReceiveEventArgs : AddonArgs +{ + /// + public override AddonArgsType Type => AddonArgsType.ReceiveEvent; + + /// + /// Gets the AtkEventType for this event message. + /// + public byte AtkEventType { get; init; } + + /// + /// Gets the event id for this event message. + /// + public int EventParam { get; init; } + + /// + /// Gets the pointer to an AtkEvent for this event message. + /// + public nint AtkEvent { get; init; } + + /// + /// Gets the pointer to a block of data for this event message. + /// + public nint Data { get; init; } +} diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index a50dc68f6..b6ac6d8b6 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -3,7 +3,7 @@ using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// -/// Addon argument data for Finalize events. +/// Addon argument data for Refresh events. /// public class AddonRefreshArgs : AddonArgs { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index e73d11e23..1b743b31a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -1,7 +1,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// -/// Addon argument data for Finalize events. +/// Addon argument data for OnRequestedUpdate events. /// public class AddonRequestedUpdateArgs : AddonArgs { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index 6870746db..651fbcafb 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -1,7 +1,7 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// -/// Addon argument data for Finalize events. +/// Addon argument data for Update events. /// public class AddonUpdateArgs : AddonArgs { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs index 11f73a4de..b58b5f4c7 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgsType.cs @@ -34,4 +34,9 @@ public enum AddonArgsType /// Contains argument data for Refresh. /// Refresh, + + /// + /// Contains argument data for ReceiveEvent. + /// + ReceiveEvent, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs index 75a77482d..7cbc93eb2 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonEvent.cs @@ -59,4 +59,14 @@ public enum AddonEvent /// Event that is fired after an addon has finished a refresh. /// PostRefresh, + + /// + /// Event that is fired before an addon begins processing an event. + /// + PreReceiveEvent, + + /// + /// Event that is fired after an addon has processed an event. + /// + PostReceiveEvent, } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index b188095d0..a12290c10 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -8,6 +8,7 @@ using Dalamud.Hooking.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -38,6 +39,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly ConcurrentBag removeEventListeners = new(); private readonly List eventListeners = new(); + private readonly Dictionary> receiveEventHooks = new(); + [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) { @@ -67,6 +70,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate byte AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); + private delegate void AddonReceiveEventDelegate(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint a5); + /// public void Dispose() { @@ -79,6 +84,11 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonUpdateHook.Dispose(); this.onAddonRefreshHook.Dispose(); this.onAddonRequestedUpdateHook.Dispose(); + + foreach (var (_, hook) in this.receiveEventHooks) + { + hook.Dispose(); + } } /// @@ -104,7 +114,21 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { if (this.newEventListeners.Any()) { - this.eventListeners.AddRange(this.newEventListeners); + foreach (var toAddListener in this.newEventListeners) + { + this.eventListeners.Add(toAddListener); + + // 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 (toAddListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) + { + if (this.receiveEventHooks.TryGetValue(toAddListener.AddonName, out var hook)) + { + hook.Enable(); + } + } + } + this.newEventListeners.Clear(); } @@ -142,6 +166,23 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonSetup(AtkUnitBase* addon, uint valueCount, AtkValue* values) { + try + { + // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. + var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name); + var receiveEventHook = Hook.FromAddress((nint)addon->VTable->ReceiveEvent, this.OnReceiveEvent); + this.receiveEventHooks.TryAdd(addonName, receiveEventHook); + + if (this.eventListeners.Any(listener => listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent)) + { + receiveEventHook.Enable(); + } + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); + } + try { this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs @@ -175,6 +216,21 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) { + try + { + // Remove this addons ReceiveEvent Registration + var addonName = MemoryHelper.ReadStringNullTerminated((nint)atkUnitBase[0]->Name); + if (this.receiveEventHooks.TryGetValue(addonName, out var hook)) + { + hook.Dispose(); + this.receiveEventHooks.Remove(addonName); + } + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); + } + try { this.InvokeListeners(AddonEvent.PreFinalize, new AddonFinalizeArgs { Addon = (nint)atkUnitBase[0] }); @@ -300,6 +356,44 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke."); } } + + private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint data) + { + try + { + this.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs + { + Addon = (nint)addon, + AtkEventType = (byte)eventType, + EventParam = eventParam, + AtkEvent = (nint)atkEvent, + Data = data, + }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke."); + } + + var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name); + this.receiveEventHooks[addonName].Original(addon, eventType, eventParam, atkEvent, data); + + try + { + this.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs + { + Addon = (nint)addon, + AtkEventType = (byte)eventType, + EventParam = eventParam, + AtkEvent = (nint)atkEvent, + Data = data, + }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke."); + } + } } /// From d7106e63ac852d9c7d777f1d290c08359d824c4c Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 9 Oct 2023 22:11:17 +0200 Subject: [PATCH 258/585] build: 9.0.0.3 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index ec95a61a5..10b620983 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.2 + 9.0.0.3 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 41667572c84d0d8b948926233596acaa2d1fbb6b Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Mon, 9 Oct 2023 18:21:39 -0700 Subject: [PATCH 259/585] Revert "fix: ignore the default profile when installing a plugin" (#1481) --- Dalamud/Plugin/Internal/PluginManager.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 51e994c69..ac808df89 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -784,19 +784,6 @@ internal partial class PluginManager : IDisposable, IServiceType public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason, Guid? inheritedWorkingPluginId = null) { Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})"); - - // If this plugin is in the default profile for whatever reason, delete the state - // If it was in multiple profiles and is still, the user uninstalled it and chose to keep it in there, - // or the user removed the plugin manually in which case we don't care - var defaultProfile = this.profileManager.DefaultProfile; - using (defaultProfile.GetSyncScope()) - { - if (defaultProfile.WantsPlugin(repoManifest.InternalName).HasValue) - { - // We don't need to apply, it doesn't matter - await defaultProfile.RemoveAsync(repoManifest.InternalName, false); - } - } // Ensure that we have a testing opt-in for this plugin if we are installing a testing version if (useTesting && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != repoManifest.InternalName)) From 2ecf016c80e07c641829651f29594ebde0c2a5ae Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 9 Oct 2023 18:23:52 -0700 Subject: [PATCH 260/585] Fix activating hooks for **all** addons (#1482) --- 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 a12290c10..07f12fe35 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -173,7 +173,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType var receiveEventHook = Hook.FromAddress((nint)addon->VTable->ReceiveEvent, this.OnReceiveEvent); this.receiveEventHooks.TryAdd(addonName, receiveEventHook); - if (this.eventListeners.Any(listener => listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent)) + if (this.eventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName)) { receiveEventHook.Enable(); } From a096bd547fb04c6fb0ed8c1acc1e95a8607ad101 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 10 Oct 2023 18:57:35 +0200 Subject: [PATCH 261/585] fix: ignore the default profile when installing a plugin(try 2) Ensures that we don't use old state for a plugin that we are installing fresh --- Dalamud/Plugin/Internal/PluginManager.cs | 14 ++++++++++++++ Dalamud/Plugin/Internal/Profiles/Profile.cs | 13 +++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index ac808df89..3ead8d5ea 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -785,6 +785,20 @@ internal partial class PluginManager : IDisposable, IServiceType { Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})"); + // If this plugin is in the default profile for whatever reason, delete the state + // If it was in multiple profiles and is still, the user uninstalled it and chose to keep it in there, + // or the user removed the plugin manually in which case we don't care + if (reason == PluginLoadReason.Installer) + { + // We don't need to apply, it doesn't matter + await this.profileManager.DefaultProfile.RemoveAsync(repoManifest.InternalName, false, false); + } + else + { + // If we are doing anything other than a fresh install, not having a workingPluginId is an error that must be fixed + Debug.Assert(inheritedWorkingPluginId != null, "inheritedWorkingPluginId != null"); + } + // Ensure that we have a testing opt-in for this plugin if we are installing a testing version if (useTesting && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != repoManifest.InternalName)) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index ac46d9153..0b2d62ccf 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -200,17 +200,22 @@ internal class Profile /// /// The internal name of the plugin. /// Whether or not the current state should immediately be applied. + /// + /// Throw if certain operations are invalid. + /// * Throw if the plugin is not in this profile. + /// * If this is the default profile, check if we are in any other, then throw if we aren't. + /// /// A representing the asynchronous operation. - public async Task RemoveAsync(string internalName, bool apply = true) + public async Task RemoveAsync(string internalName, bool apply = true, bool guardrails = true) { ProfileModelV1.ProfileModelV1Plugin entry; lock (this) { entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); - if (entry == null) + if (entry == null && guardrails) throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\""); - if (!this.modelV1.Plugins.Remove(entry)) + if (!this.modelV1.Plugins.Remove(entry) && guardrails) throw new Exception("Couldn't remove plugin from model collection"); } @@ -221,7 +226,7 @@ internal class Profile { await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, this.IsEnabled && entry.IsEnabled, false); } - else + else if (guardrails) { throw new Exception("Removed plugin from default profile, but wasn't in any other profile"); } From 6a01c8cc5b165ed8f53c4b9bc9fbae3281eb9152 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 10 Oct 2023 19:16:42 +0200 Subject: [PATCH 262/585] chore: handle profile errors properly and stop being lazy --- Dalamud/Plugin/Internal/PluginManager.cs | 11 +++- Dalamud/Plugin/Internal/Profiles/Profile.cs | 62 +++++++++++++++++---- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 3ead8d5ea..664406157 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -790,8 +790,15 @@ internal partial class PluginManager : IDisposable, IServiceType // or the user removed the plugin manually in which case we don't care if (reason == PluginLoadReason.Installer) { - // We don't need to apply, it doesn't matter - await this.profileManager.DefaultProfile.RemoveAsync(repoManifest.InternalName, false, false); + try + { + // We don't need to apply, it doesn't matter + await this.profileManager.DefaultProfile.RemoveAsync(repoManifest.InternalName, false); + } + catch (ProfileOperationException) + { + // ignored + } } else { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 0b2d62ccf..592720c14 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -200,22 +200,17 @@ internal class Profile /// /// The internal name of the plugin. /// Whether or not the current state should immediately be applied. - /// - /// Throw if certain operations are invalid. - /// * Throw if the plugin is not in this profile. - /// * If this is the default profile, check if we are in any other, then throw if we aren't. - /// /// A representing the asynchronous operation. - public async Task RemoveAsync(string internalName, bool apply = true, bool guardrails = true) + public async Task RemoveAsync(string internalName, bool apply = true) { ProfileModelV1.ProfileModelV1Plugin entry; lock (this) { entry = this.modelV1.Plugins.FirstOrDefault(x => x.InternalName == internalName); - if (entry == null && guardrails) - throw new ArgumentException($"No plugin \"{internalName}\" in profile \"{this.Guid}\""); + if (entry == null) + throw new PluginNotFoundException(internalName); - if (!this.modelV1.Plugins.Remove(entry) && guardrails) + if (!this.modelV1.Plugins.Remove(entry)) throw new Exception("Couldn't remove plugin from model collection"); } @@ -226,9 +221,9 @@ internal class Profile { await this.manager.DefaultProfile.AddOrUpdateAsync(internalName, this.IsEnabled && entry.IsEnabled, false); } - else if (guardrails) + else { - throw new Exception("Removed plugin from default profile, but wasn't in any other profile"); + throw new PluginNotInDefaultProfileException(internalName); } } @@ -241,3 +236,48 @@ internal class Profile /// public override string ToString() => $"{this.Guid} ({this.Name})"; } + +/// +/// Exception indicating an issue during a profile operation. +/// +internal abstract class ProfileOperationException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// Message to pass on. + protected ProfileOperationException(string message) + : base(message) + { + } +} + +/// +/// Exception indicating that a plugin was not found in the default profile. +/// +internal sealed class PluginNotInDefaultProfileException : ProfileOperationException +{ + /// + /// Initializes a new instance of the class. + /// + /// The internal name of the plugin causing the error. + public PluginNotInDefaultProfileException(string internalName) + : base($"The plugin '{internalName}' is not in the default profile, and cannot be removed") + { + } +} + +/// +/// Exception indicating that the plugin was not found. +/// +internal sealed class PluginNotFoundException : ProfileOperationException +{ + /// + /// Initializes a new instance of the class. + /// + /// The internal name of the plugin causing the error. + public PluginNotFoundException(string internalName) + : base($"The plugin '{internalName}' was not found in the profile") + { + } +} From 954ab4d8d6360fde7fa05d62a9bb870589800d75 Mon Sep 17 00:00:00 2001 From: Anna Date: Wed, 11 Oct 2023 22:41:13 -0400 Subject: [PATCH 263/585] Add appropriate HTTP headers to repository requests (#1486) --- Dalamud/Plugin/Internal/Types/PluginRepository.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index aae603f42..3bf67ecd7 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -37,10 +37,18 @@ internal class PluginRepository Timeout = TimeSpan.FromSeconds(20), DefaultRequestHeaders = { + Accept = + { + new MediaTypeWithQualityHeaderValue("application/json"), + }, CacheControl = new CacheControlHeaderValue { NoCache = true, }, + UserAgent = + { + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + }, }, }; From 0c554fe47dab3c3214f4aef9ee250d4f2420a878 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 14 Oct 2023 12:48:21 -0700 Subject: [PATCH 264/585] AddonLifecycle Extra Safety (#1483) * Disallow attempting to hook base AtkEventListener.ReceiveEvent * Fix incorrect address resolution * Add exception guards * Remove debug logging --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 87 ++++++++++++++++--- .../AddonLifecycleAddressResolver.cs | 7 ++ 2 files changed, 81 insertions(+), 13 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 07f12fe35..c42481d63 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -26,6 +26,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + private readonly nint disallowedReceiveEventAddress; + private readonly AddonLifecycleAddressResolver address; private readonly CallHook onAddonSetupHook; private readonly CallHook onAddonSetup2Hook; @@ -47,6 +49,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.address = new AddonLifecycleAddressResolver(); this.address.Setup(sigScanner); + // We want value of the function pointer at vFunc[2] + this.disallowedReceiveEventAddress = ((nint*)this.address.AtkEventListener)![2]; + this.framework.Update += this.OnFrameworkUpdate; this.onAddonSetupHook = new CallHook(this.address.AddonSetup, this.OnAddonSetup); @@ -169,13 +174,18 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType try { // 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 = MemoryHelper.ReadStringNullTerminated((nint)addon->Name); - var receiveEventHook = Hook.FromAddress((nint)addon->VTable->ReceiveEvent, this.OnReceiveEvent); - this.receiveEventHooks.TryAdd(addonName, receiveEventHook); - - if (this.eventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName)) + var receiveEventAddress = (nint)addon->VTable->ReceiveEvent; + if (receiveEventAddress != this.disallowedReceiveEventAddress) { - receiveEventHook.Enable(); + var receiveEventHook = Hook.FromAddress(receiveEventAddress, this.OnReceiveEvent); + this.receiveEventHooks.TryAdd(addonName, receiveEventHook); + + if (this.eventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName)) + { + receiveEventHook.Enable(); + } } } catch (Exception e) @@ -197,7 +207,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonSetup pre-setup invoke."); } - addon->OnSetup(valueCount, values); + 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."); + } try { @@ -240,7 +257,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke."); } - this.onAddonFinalizeHook.Original(unitManager, atkUnitBase); + 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) @@ -254,7 +278,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); } - addon->Draw(); + 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."); + } try { @@ -277,7 +308,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonUpdate pre-update invoke."); } - addon->Update(delta); + 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."); + } try { @@ -291,6 +329,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) { + byte result = 0; + try { this.InvokeListeners(AddonEvent.PreRefresh, new AddonRefreshArgs @@ -305,7 +345,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke."); } - var result = this.onAddonRefreshHook.Original(atkUnitManager, addon, valueCount, values); + try + { + result = this.onAddonRefreshHook.Original(atkUnitManager, 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."); + } try { @@ -340,7 +387,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke."); } - addon->OnUpdate(numberArrayData, stringArrayData); + try + { + addon->OnUpdate(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."); + } try { @@ -375,8 +429,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke."); } - var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name); - this.receiveEventHooks[addonName].Original(addon, eventType, eventParam, atkEvent, data); + try + { + var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name); + this.receiveEventHooks[addonName].Original(addon, eventType, eventParam, atkEvent, data); + } + 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."); + } try { diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs index c308d1676..df25d0a46 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleAddressResolver.cs @@ -43,6 +43,12 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver /// Gets the address of AtkUnitManager_vf10 which triggers addon onRefresh. /// public nint AddonOnRefresh { get; private set; } + + /// + /// Gets the address of AtkEventListener base vTable. + /// This is used to ensure that we do not hook ReceiveEvents that resolve back to the internal handler. + /// + public nint AtkEventListener { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -57,5 +63,6 @@ internal class AddonLifecycleAddressResolver : BaseAddressResolver this.AddonUpdate = sig.ScanText("FF 90 ?? ?? ?? ?? 40 88 AF"); this.AddonOnRequestedUpdate = sig.ScanText("FF 90 98 01 00 00 48 8B 5C 24 30 48 83 C4 20"); this.AddonOnRefresh = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 41 8B F8 48 8B DA"); + this.AtkEventListener = sig.GetStaticAddressFromSig("4C 8D 3D ?? ?? ?? ?? 49 8D 8E"); } } From 82c0109caabeaf24081f632a9353edafc11a4f32 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 14 Oct 2023 21:55:04 +0200 Subject: [PATCH 265/585] Update ClientStructs (#1480) 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 69bbc4c83..f8e6f0e01 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 69bbc4c83436dba2fa7fa432eda960c6e1ab3294 +Subproject commit f8e6f0e019af4492ae1ec328643373b6d61dfb2a From 377e265e3bae72142690cbac62103bb76d503058 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:17:23 +0200 Subject: [PATCH 266/585] [master] Update ClientStructs (#1488) 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 f8e6f0e01..69e3f849c 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit f8e6f0e019af4492ae1ec328643373b6d61dfb2a +Subproject commit 69e3f849cf772c2823f2bed88ab0103721336d0d From 822e26ef93f09226602225fdf80c67b31c55cf97 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 15 Oct 2023 19:18:33 +0900 Subject: [PATCH 267/585] Support Span preview through ShowStruct (#1485) --- Dalamud/Utility/Util.cs | 213 ++++++++++++++++++++++++++++++++++------ 1 file changed, 182 insertions(+), 31 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index fb6c854a1..49bbbd0b6 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -6,9 +6,11 @@ using System.IO.Compression; using System.Linq; using System.Numerics; using System.Reflection; +using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; + using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; @@ -242,10 +244,12 @@ public static class Util /// The address to the structure. /// Whether or not this structure should start out expanded. /// The already followed path. - public static void ShowStruct(object obj, ulong addr, bool autoExpand = false, IEnumerable? path = null) + /// Do not print addresses. Use when displaying a copied value. + public static void ShowStruct(object obj, ulong addr, bool autoExpand = false, IEnumerable? path = null, bool hideAddress = false) { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(3, 2)); path ??= new List(); + var pathList = path is List ? (List)path : path.ToList(); if (moduleEndAddr == 0 && moduleStartAddr == 0) { @@ -274,7 +278,7 @@ public static class Util ImGui.SetNextItemOpen(true, ImGuiCond.Appearing); } - if (ImGui.TreeNode($"{obj}##print-obj-{addr:X}-{string.Join("-", path)}")) + if (ImGui.TreeNode($"{obj}##print-obj-{addr:X}-{string.Join("-", pathList)}")) { ImGui.PopStyleColor(); foreach (var f in obj.GetType() @@ -297,10 +301,24 @@ public static class Util ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.4f, 1), $"{f.Name}: "); ImGui.SameLine(); - if (f.FieldType.IsGenericType && f.FieldType.GetGenericTypeDefinition() == GenericSpanType) - ImGui.Text("Span preview is currently not supported."); - else - ShowValue(addr, new List(path) {f.Name}, f.FieldType, f.GetValue(obj)); + pathList.Add(f.Name); + try + { + if (f.FieldType.IsGenericType && (f.FieldType.IsByRef || f.FieldType.IsByRefLike)) + ImGui.Text("Cannot preview ref typed fields."); // object never contains ref struct + else + ShowValue(addr, pathList, f.FieldType, f.GetValue(obj), hideAddress); + } + catch (Exception ex) + { + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.4f, 0.4f, 1f)); + ImGui.TextUnformatted($"Error: {ex.GetType().Name}: {ex.Message}"); + ImGui.PopStyleColor(); + } + finally + { + pathList.RemoveAt(pathList.Count - 1); + } } foreach (var p in obj.GetType().GetProperties().Where(p => p.GetGetMethod()?.GetParameters().Length == 0)) @@ -310,10 +328,26 @@ public static class Util ImGui.TextColored(new Vector4(0.2f, 0.6f, 0.4f, 1), $"{p.Name}: "); ImGui.SameLine(); - if (p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == GenericSpanType) - ImGui.Text("Span preview is currently not supported."); - else - ShowValue(addr, new List(path) {p.Name}, p.PropertyType, p.GetValue(obj)); + pathList.Add(p.Name); + try + { + if (p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == GenericSpanType) + ShowSpanProperty(addr, pathList, p, obj); + else if (p.PropertyType.IsGenericType && (p.PropertyType.IsByRef || p.PropertyType.IsByRefLike)) + ImGui.Text("Cannot preview ref typed properties."); + else + ShowValue(addr, pathList, p.PropertyType, p.GetValue(obj), hideAddress); + } + catch (Exception ex) + { + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.4f, 0.4f, 1f)); + ImGui.TextUnformatted($"Error: {ex.GetType().Name}: {ex.Message}"); + ImGui.PopStyleColor(); + } + finally + { + pathList.RemoveAt(pathList.Count - 1); + } } ImGui.TreePop(); @@ -374,21 +408,21 @@ public static class Util ImGui.Indent(); - foreach (var propertyInfo in type.GetProperties().Where(p => p.GetGetMethod()?.GetParameters().Length == 0)) + foreach (var p in type.GetProperties().Where(p => p.GetGetMethod()?.GetParameters().Length == 0)) { - if (propertyInfo.PropertyType.IsGenericType && - propertyInfo.PropertyType.GetGenericTypeDefinition() == GenericSpanType) + if (p.PropertyType.IsGenericType && (p.PropertyType.IsByRef || p.PropertyType.IsByRefLike)) { - ImGui.TextColored(ImGuiColors.DalamudOrange, $" {propertyInfo.Name}: Span preview is currently not supported."); - continue; + ImGui.TextColored(ImGuiColors.DalamudOrange, $" {p.Name}: (ref typed property)"); } - - var value = propertyInfo.GetValue(obj); - var valueType = value?.GetType(); - if (valueType == typeof(IntPtr)) - ImGui.TextColored(ImGuiColors.DalamudOrange, $" {propertyInfo.Name}: 0x{value:X}"); else - ImGui.TextColored(ImGuiColors.DalamudOrange, $" {propertyInfo.Name}: {value}"); + { + var value = p.GetValue(obj); + var valueType = value?.GetType(); + if (valueType == typeof(IntPtr)) + ImGui.TextColored(ImGuiColors.DalamudOrange, $" {p.Name}: 0x{value:X}"); + else + ImGui.TextColored(ImGuiColors.DalamudOrange, $" {p.Name}: {value}"); + } } ImGui.Unindent(); @@ -797,7 +831,120 @@ public static class Util } } - private static unsafe void ShowValue(ulong addr, IEnumerable path, Type type, object value) + private static void ShowSpanProperty(ulong addr, IList path, PropertyInfo p, object obj) + { + var objType = obj.GetType(); + var propType = p.PropertyType; + if (p.GetGetMethod() is not { } getMethod) + { + ImGui.Text("(No getter available)"); + return; + } + + var dm = new DynamicMethod( + "-", + MethodAttributes.Public | MethodAttributes.Static, + CallingConventions.Standard, + null, + new[] { typeof(object), typeof(IList), typeof(ulong) }, + obj.GetType(), + true); + + var ilg = dm.GetILGenerator(); + var objLocalIndex = unchecked((byte)ilg.DeclareLocal(objType, true).LocalIndex); + var propLocalIndex = unchecked((byte)ilg.DeclareLocal(propType, true).LocalIndex); + ilg.Emit(OpCodes.Ldarg_0); + if (objType.IsValueType) + { + ilg.Emit(OpCodes.Unbox_Any, objType); + ilg.Emit(OpCodes.Stloc_S, objLocalIndex); + ilg.Emit(OpCodes.Ldloca_S, objLocalIndex); + } + + ilg.Emit(OpCodes.Call, getMethod); + var mm = typeof(Util).GetMethod(nameof(ShowSpanPrivate), BindingFlags.Static | BindingFlags.NonPublic)! + .MakeGenericMethod(p.PropertyType.GetGenericArguments()); + ilg.Emit(OpCodes.Stloc_S, propLocalIndex); + ilg.Emit(OpCodes.Ldarg_2); // addr = arg2 + ilg.Emit(OpCodes.Ldarg_1); // path = arg1 + ilg.Emit(OpCodes.Ldc_I4_0); // offset = 0 + ilg.Emit(OpCodes.Ldc_I4_1); // isTop = true + ilg.Emit(OpCodes.Ldloca_S, propLocalIndex); // spanobj + ilg.Emit(OpCodes.Call, mm); + ilg.Emit(OpCodes.Ret); + + dm.Invoke(null, new[] { obj, path, addr }); + } + + private static unsafe void ShowSpanPrivate(ulong addr, IList path, int offset, bool isTop, in Span spanobj) + { +#pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + if (isTop) + { + fixed (void* p = spanobj) + { + if (!ImGui.TreeNode( + $"Span<{typeof(T).Name}> of length {spanobj.Length:n0} (0x{spanobj.Length:X})" + + $"##print-obj-{addr:X}-{string.Join("-", path)}-head")) + { + return; + } + } + } + + try + { + const int batchSize = 20; + if (spanobj.Length > batchSize) + { + var skip = batchSize; + while ((spanobj.Length + skip - 1) / skip > batchSize) + skip *= batchSize; + for (var i = 0; i < spanobj.Length; i += skip) + { + var next = Math.Min(i + skip, spanobj.Length); + path.Add($"{offset + i:X}_{skip}"); + if (ImGui.TreeNode( + $"{offset + i:n0} ~ {offset + next - 1:n0} (0x{offset + i:X} ~ 0x{offset + next - 1:X})" + + $"##print-obj-{addr:X}-{string.Join("-", path)}")) + { + try + { + ShowSpanPrivate(addr, path, offset + i, false, spanobj[i..next]); + } + finally + { + ImGui.TreePop(); + } + } + + path.RemoveAt(path.Count - 1); + } + } + else + { + fixed (T* p = spanobj) + { + var pointerType = typeof(T*); + for (var i = 0; i < spanobj.Length; i++) + { + ImGui.TextUnformatted($"[{offset + i:n0} (0x{offset + i:X})] "); + ImGui.SameLine(); + path.Add($"{offset + i}"); + ShowValue(addr, path, pointerType, Pointer.Box(p + i, pointerType), true); + } + } + } + } + finally + { + if (isTop) + ImGui.TreePop(); + } +#pragma warning restore CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + } + + private static unsafe void ShowValue(ulong addr, IList path, Type type, object value, bool hideAddress) { if (type.IsPointer) { @@ -805,28 +952,32 @@ public static class Util var unboxed = Pointer.Unbox(val); if (unboxed != null) { - var unboxedAddr = (ulong)unboxed; - ImGuiHelpers.ClickToCopyText($"{(ulong)unboxed:X}"); - if (moduleStartAddr > 0 && unboxedAddr >= moduleStartAddr && unboxedAddr <= moduleEndAddr) + if (!hideAddress) { + var unboxedAddr = (ulong)unboxed; + ImGuiHelpers.ClickToCopyText($"{(ulong)unboxed:X}"); + if (moduleStartAddr > 0 && unboxedAddr >= moduleStartAddr && unboxedAddr <= moduleEndAddr) + { + ImGui.SameLine(); + ImGui.PushStyleColor(ImGuiCol.Text, 0xffcbc0ff); + ImGuiHelpers.ClickToCopyText($"ffxiv_dx11.exe+{unboxedAddr - moduleStartAddr:X}"); + ImGui.PopStyleColor(); + } + ImGui.SameLine(); - ImGui.PushStyleColor(ImGuiCol.Text, 0xffcbc0ff); - ImGuiHelpers.ClickToCopyText($"ffxiv_dx11.exe+{unboxedAddr - moduleStartAddr:X}"); - ImGui.PopStyleColor(); } try { var eType = type.GetElementType(); var ptrObj = SafeMemory.PtrToStructure(new IntPtr(unboxed), eType); - ImGui.SameLine(); if (ptrObj == null) { ImGui.Text("null or invalid"); } else { - ShowStruct(ptrObj, (ulong)unboxed, path: new List(path)); + ShowStruct(ptrObj, addr, path: path, hideAddress: hideAddress); } } catch @@ -843,7 +994,7 @@ public static class Util { if (!type.IsPrimitive) { - ShowStruct(value, addr, path: new List(path)); + ShowStruct(value, addr, path: path, hideAddress: hideAddress); } else { From 0f349bb3dd8b7425b1bc1cc2c15a4d042284c4bc Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 15 Oct 2023 03:19:14 -0700 Subject: [PATCH 268/585] IChatGui Add Readonly RegisteredLinkHandlers (#1487) --- Dalamud/Game/Gui/ChatGui.cs | 6 ++++++ Dalamud/Plugin/Services/IChatGui.cs | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 5bf6232fa..50c5b2908 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -79,6 +79,9 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui /// public byte LastLinkedItemFlags { get; private set; } + /// + public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers => this.dalamudLinkHandlers; + /// /// Dispose of managed and unmanaged resources. /// @@ -453,6 +456,9 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui /// public byte LastLinkedItemFlags => this.chatGuiService.LastLinkedItemFlags; + /// + public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers => this.chatGuiService.RegisteredLinkHandlers; + /// public void Dispose() { diff --git a/Dalamud/Plugin/Services/IChatGui.cs b/Dalamud/Plugin/Services/IChatGui.cs index bafdabbb5..24fd4e830 100644 --- a/Dalamud/Plugin/Services/IChatGui.cs +++ b/Dalamud/Plugin/Services/IChatGui.cs @@ -1,4 +1,6 @@ -using Dalamud.Game.Gui; +using System.Collections.Generic; + +using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; @@ -76,6 +78,11 @@ public interface IChatGui /// Gets the flags of the last linked item. /// public byte LastLinkedItemFlags { get; } + + /// + /// Gets the dictionary of Dalamud Link Handlers. + /// + public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers { get; } /// /// Queue a chat message. Dalamud will send queued messages on the next framework event. From a3a3c8f797fc169a70807cb369f2dfef8e32e8ec Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 15 Oct 2023 12:28:02 +0200 Subject: [PATCH 269/585] build: 9.0.0.4 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 10b620983..9e0b58fcc 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.3 + 9.0.0.4 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 90dfa990fc541776c49657d10d604b04c797c337 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 16 Oct 2023 01:38:30 +0200 Subject: [PATCH 270/585] fix: undo breaking change in Util.ShowStruct moves implementation with new arg to an internal func --- Dalamud/Utility/Util.cs | 246 +++++++++++++++++++++------------------- 1 file changed, 128 insertions(+), 118 deletions(-) diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 49bbbd0b6..d53c2fe19 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -244,121 +244,8 @@ public static class Util /// The address to the structure. /// Whether or not this structure should start out expanded. /// The already followed path. - /// Do not print addresses. Use when displaying a copied value. - public static void ShowStruct(object obj, ulong addr, bool autoExpand = false, IEnumerable? path = null, bool hideAddress = false) - { - ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(3, 2)); - path ??= new List(); - var pathList = path is List ? (List)path : path.ToList(); - - if (moduleEndAddr == 0 && moduleStartAddr == 0) - { - try - { - var processModule = Process.GetCurrentProcess().MainModule; - if (processModule != null) - { - moduleStartAddr = (ulong)processModule.BaseAddress.ToInt64(); - moduleEndAddr = moduleStartAddr + (ulong)processModule.ModuleMemorySize; - } - else - { - moduleEndAddr = 1; - } - } - catch - { - moduleEndAddr = 1; - } - } - - ImGui.PushStyleColor(ImGuiCol.Text, 0xFF00FFFF); - if (autoExpand) - { - ImGui.SetNextItemOpen(true, ImGuiCond.Appearing); - } - - if (ImGui.TreeNode($"{obj}##print-obj-{addr:X}-{string.Join("-", pathList)}")) - { - ImGui.PopStyleColor(); - foreach (var f in obj.GetType() - .GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance)) - { - var fixedBuffer = (FixedBufferAttribute)f.GetCustomAttribute(typeof(FixedBufferAttribute)); - if (fixedBuffer != null) - { - ImGui.Text($"fixed"); - ImGui.SameLine(); - ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), - $"{fixedBuffer.ElementType.Name}[0x{fixedBuffer.Length:X}]"); - } - else - { - ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), $"{f.FieldType.Name}"); - } - - ImGui.SameLine(); - ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.4f, 1), $"{f.Name}: "); - ImGui.SameLine(); - - pathList.Add(f.Name); - try - { - if (f.FieldType.IsGenericType && (f.FieldType.IsByRef || f.FieldType.IsByRefLike)) - ImGui.Text("Cannot preview ref typed fields."); // object never contains ref struct - else - ShowValue(addr, pathList, f.FieldType, f.GetValue(obj), hideAddress); - } - catch (Exception ex) - { - ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.4f, 0.4f, 1f)); - ImGui.TextUnformatted($"Error: {ex.GetType().Name}: {ex.Message}"); - ImGui.PopStyleColor(); - } - finally - { - pathList.RemoveAt(pathList.Count - 1); - } - } - - foreach (var p in obj.GetType().GetProperties().Where(p => p.GetGetMethod()?.GetParameters().Length == 0)) - { - ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), $"{p.PropertyType.Name}"); - ImGui.SameLine(); - ImGui.TextColored(new Vector4(0.2f, 0.6f, 0.4f, 1), $"{p.Name}: "); - ImGui.SameLine(); - - pathList.Add(p.Name); - try - { - if (p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == GenericSpanType) - ShowSpanProperty(addr, pathList, p, obj); - else if (p.PropertyType.IsGenericType && (p.PropertyType.IsByRef || p.PropertyType.IsByRefLike)) - ImGui.Text("Cannot preview ref typed properties."); - else - ShowValue(addr, pathList, p.PropertyType, p.GetValue(obj), hideAddress); - } - catch (Exception ex) - { - ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.4f, 0.4f, 1f)); - ImGui.TextUnformatted($"Error: {ex.GetType().Name}: {ex.Message}"); - ImGui.PopStyleColor(); - } - finally - { - pathList.RemoveAt(pathList.Count - 1); - } - } - - ImGui.TreePop(); - } - else - { - ImGui.PopStyleColor(); - } - - ImGui.PopStyleVar(); - } + public static void ShowStruct(object obj, ulong addr, bool autoExpand = false, IEnumerable? path = null) + => ShowStructInternal(obj, addr, autoExpand, path); /// /// Show a structure in an ImGui context. @@ -464,7 +351,7 @@ public static class Util /// Human readable version. public static string FormatBytes(long bytes) { - string[] suffix = {"B", "KB", "MB", "GB", "TB"}; + string[] suffix = { "B", "KB", "MB", "GB", "TB" }; int i; double dblSByte = bytes; for (i = 0; i < suffix.Length && bytes >= 1024; i++, bytes /= 1024) @@ -977,7 +864,7 @@ public static class Util } else { - ShowStruct(ptrObj, addr, path: path, hideAddress: hideAddress); + ShowStructInternal(ptrObj, addr, path: path, hideAddress: hideAddress); } } catch @@ -994,7 +881,7 @@ public static class Util { if (!type.IsPrimitive) { - ShowStruct(value, addr, path: path, hideAddress: hideAddress); + ShowStructInternal(value, addr, path: path, hideAddress: hideAddress); } else { @@ -1002,4 +889,127 @@ public static class Util } } } + + /// + /// Show a structure in an ImGui context. + /// + /// The structure to show. + /// The address to the structure. + /// Whether or not this structure should start out expanded. + /// The already followed path. + /// Do not print addresses. Use when displaying a copied value. + private static void ShowStructInternal(object obj, ulong addr, bool autoExpand = false, IEnumerable? path = null, bool hideAddress = false) + { + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(3, 2)); + path ??= new List(); + var pathList = path is List ? (List)path : path.ToList(); + + if (moduleEndAddr == 0 && moduleStartAddr == 0) + { + try + { + var processModule = Process.GetCurrentProcess().MainModule; + if (processModule != null) + { + moduleStartAddr = (ulong)processModule.BaseAddress.ToInt64(); + moduleEndAddr = moduleStartAddr + (ulong)processModule.ModuleMemorySize; + } + else + { + moduleEndAddr = 1; + } + } + catch + { + moduleEndAddr = 1; + } + } + + ImGui.PushStyleColor(ImGuiCol.Text, 0xFF00FFFF); + if (autoExpand) + { + ImGui.SetNextItemOpen(true, ImGuiCond.Appearing); + } + + if (ImGui.TreeNode($"{obj}##print-obj-{addr:X}-{string.Join("-", pathList)}")) + { + ImGui.PopStyleColor(); + foreach (var f in obj.GetType() + .GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.Instance)) + { + var fixedBuffer = (FixedBufferAttribute)f.GetCustomAttribute(typeof(FixedBufferAttribute)); + if (fixedBuffer != null) + { + ImGui.Text($"fixed"); + ImGui.SameLine(); + ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), + $"{fixedBuffer.ElementType.Name}[0x{fixedBuffer.Length:X}]"); + } + else + { + ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), $"{f.FieldType.Name}"); + } + + ImGui.SameLine(); + ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.4f, 1), $"{f.Name}: "); + ImGui.SameLine(); + + pathList.Add(f.Name); + try + { + if (f.FieldType.IsGenericType && (f.FieldType.IsByRef || f.FieldType.IsByRefLike)) + ImGui.Text("Cannot preview ref typed fields."); // object never contains ref struct + else + ShowValue(addr, pathList, f.FieldType, f.GetValue(obj), hideAddress); + } + catch (Exception ex) + { + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.4f, 0.4f, 1f)); + ImGui.TextUnformatted($"Error: {ex.GetType().Name}: {ex.Message}"); + ImGui.PopStyleColor(); + } + finally + { + pathList.RemoveAt(pathList.Count - 1); + } + } + + foreach (var p in obj.GetType().GetProperties().Where(p => p.GetGetMethod()?.GetParameters().Length == 0)) + { + ImGui.TextColored(new Vector4(0.2f, 0.9f, 0.9f, 1), $"{p.PropertyType.Name}"); + ImGui.SameLine(); + ImGui.TextColored(new Vector4(0.2f, 0.6f, 0.4f, 1), $"{p.Name}: "); + ImGui.SameLine(); + + pathList.Add(p.Name); + try + { + if (p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == GenericSpanType) + ShowSpanProperty(addr, pathList, p, obj); + else if (p.PropertyType.IsGenericType && (p.PropertyType.IsByRef || p.PropertyType.IsByRefLike)) + ImGui.Text("Cannot preview ref typed properties."); + else + ShowValue(addr, pathList, p.PropertyType, p.GetValue(obj), hideAddress); + } + catch (Exception ex) + { + ImGui.PushStyleColor(ImGuiCol.Text, new Vector4(1f, 0.4f, 0.4f, 1f)); + ImGui.TextUnformatted($"Error: {ex.GetType().Name}: {ex.Message}"); + ImGui.PopStyleColor(); + } + finally + { + pathList.RemoveAt(pathList.Count - 1); + } + } + + ImGui.TreePop(); + } + else + { + ImGui.PopStyleColor(); + } + + ImGui.PopStyleVar(); + } } From 52a5f914a6a663d21cc64cedfa532312bafb8f01 Mon Sep 17 00:00:00 2001 From: goat Date: Mon, 16 Oct 2023 01:39:05 +0200 Subject: [PATCH 271/585] build: 9.0.0.5 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 9e0b58fcc..16c548eaf 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.4 + 9.0.0.5 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From deef16cdd742ca9faa403e388602795e9d3b54e9 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Mon, 16 Oct 2023 04:25:04 +0200 Subject: [PATCH 272/585] Update ClientStructs (#1489) 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 69e3f849c..2dafda5fb 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 69e3f849cf772c2823f2bed88ab0103721336d0d +Subproject commit 2dafda5fb2da77a27f154671312040b9ed1f156d From 3fb5bcc348b7567a8ecf10331643ba768ba73514 Mon Sep 17 00:00:00 2001 From: liam <6005409+lmcintyre@users.noreply.github.com> Date: Fri, 20 Oct 2023 14:11:13 -0400 Subject: [PATCH 273/585] Bump Luminas (#1495) * Bump Luminas * Bump CorePlugin too --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 4 ++-- Dalamud/Dalamud.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 2c2a86b5f..5815077c9 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,8 +27,8 @@ - - + + all diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 16c548eaf..d1fac251c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -68,8 +68,8 @@ - - + + all From 6ba31d27527880811fb0ba5bb7d5a01efcbfe92b Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Sat, 21 Oct 2023 03:19:37 +0900 Subject: [PATCH 274/585] Resize collision node instead of root node in DtrBar (#1494) --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 126 +++++++++++++++------------------ 1 file changed, 56 insertions(+), 70 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 764eec988..993bb951f 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Configuration.Internal; -using Dalamud.Game.Addon; using Dalamud.Game.Addon.Events; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -46,22 +45,26 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar private readonly AddonLifecycleEventListener dtrPostDrawListener; private readonly AddonLifecycleEventListener dtrPostRequestedUpdateListener; - + private readonly AddonLifecycleEventListener dtrPreFinalizeListener; + private readonly ConcurrentBag newEntries = new(); private readonly List entries = new(); private readonly Dictionary> eventHandles = new(); private uint runningNodeIds = BaseNodeId; + private float entryStartPos = float.NaN; [ServiceManager.ServiceConstructor] private DtrBar() { - this.dtrPostDrawListener = new AddonLifecycleEventListener(AddonEvent.PostDraw, "_DTR", this.OnDtrPostDraw); - this.dtrPostRequestedUpdateListener = new AddonLifecycleEventListener(AddonEvent.PostRequestedUpdate, "_DTR", this.OnAddonRequestedUpdateDetour); + this.dtrPostDrawListener = new AddonLifecycleEventListener(AddonEvent.PostDraw, "_DTR", this.FixCollision); + this.dtrPostRequestedUpdateListener = new AddonLifecycleEventListener(AddonEvent.PostRequestedUpdate, "_DTR", this.FixCollision); + this.dtrPreFinalizeListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, "_DTR", this.PreFinalize); this.addonLifecycle.RegisterListener(this.dtrPostDrawListener); this.addonLifecycle.RegisterListener(this.dtrPostRequestedUpdateListener); + this.addonLifecycle.RegisterListener(this.dtrPreFinalizeListener); this.framework.Update += this.Update; @@ -102,6 +105,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { this.addonLifecycle.UnregisterListener(this.dtrPostDrawListener); this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener); + this.addonLifecycle.UnregisterListener(this.dtrPreFinalizeListener); foreach (var entry in this.entries) this.RemoveNode(entry.TextNode); @@ -185,11 +189,10 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar var collisionNode = dtr->GetNodeById(17); if (collisionNode == null) return; - // If we are drawing backwards, we should start from the right side of the collision node. That is, - // collisionNode->X + collisionNode->Width. - var runningXPos = this.configuration.DtrSwapDirection - ? collisionNode->X + collisionNode->Width - : collisionNode->X; + // We haven't calculated the native size yet, so we don't know where to start positioning. + if (float.IsNaN(this.entryStartPos)) return; + + var runningXPos = this.entryStartPos; foreach (var data in this.entries) { @@ -255,56 +258,61 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } - private void OnDtrPostDraw(AddonEvent eventType, AddonArgs addonInfo) + private void FixCollision(AddonEvent eventType, AddonArgs addonInfo) { var addon = (AtkUnitBase*)addonInfo.Addon; + if (addon->RootNode is null || addon->UldManager.NodeList is null) return; - this.UpdateNodePositions(addon); - - if (!this.configuration.DtrSwapDirection) + float minX = addon->RootNode->Width; + var additionalWidth = 0; + AtkResNode* collisionNode = null; + + foreach (var index in Enumerable.Range(0, addon->UldManager.NodeListCount)) { - var targetSize = (ushort)this.CalculateTotalSize(); - var sizeDelta = MathF.Round((targetSize - addon->RootNode->Width) * addon->RootNode->ScaleX); - - if (addon->RootNode->Width != targetSize) + var node = addon->UldManager.NodeList[index]; + if (node->IsVisible) { - addon->RootNode->SetWidth(targetSize); - addon->SetX((short)(addon->GetX() - sizeDelta)); - - // force a RequestedUpdate immediately to force the game to right-justify it immediately. - addon->OnUpdate(AtkStage.GetSingleton()->GetNumberArrayData(), AtkStage.GetSingleton()->GetStringArrayData()); - } - } - } - - private void UpdateNodePositions(AtkUnitBase* addon) - { - // If we grow to the right, we need to left-justify the original elements. - // else if we grow to the left, the game right-justifies it for us. - if (this.configuration.DtrSwapDirection) - { - var targetSize = (ushort)this.CalculateTotalSize(); - addon->RootNode->SetWidth(targetSize); - var sizeOffset = addon->GetNodeById(17)->GetX(); - - var node = addon->RootNode->ChildNode; - while (node is not null) - { - if (node->NodeID < 1000 && node->IsVisible) + var nodeId = node->NodeID; + var nodeType = node->Type; + + if (nodeType == NodeType.Collision) { - node->SetX(node->GetX() - sizeOffset); + collisionNode = node; + } + else if (nodeId >= BaseNodeId) + { + // Dalamud-created node + additionalWidth += node->Width + this.configuration.DtrSpacing; + } + else if ((nodeType == NodeType.Res || (ushort)nodeType >= 1000) && + (node->ChildNode == null || node->ChildNode->IsVisible)) + { + // Native top-level node. These are are either res nodes or button components. + // Both the node and its child (if it has one) must be visible for the node to be counted. + minX = MathF.Min(minX, node->X); } - - node = node->PrevSiblingNode; } } + + if (collisionNode == null) return; + + var nativeWidth = addon->RootNode->Width - (int)minX; + var targetX = minX - (this.configuration.DtrSwapDirection ? 0 : additionalWidth); + var targetWidth = (ushort)(nativeWidth + additionalWidth); + + if (collisionNode->Width != targetWidth || collisionNode->X != targetX) + { + collisionNode->SetWidth(targetWidth); + collisionNode->SetX(targetX); + } + + // If we are drawing backwards, we should start from the right side of the native nodes. + this.entryStartPos = this.configuration.DtrSwapDirection ? minX + nativeWidth : minX; } - private void OnAddonRequestedUpdateDetour(AddonEvent eventType, AddonArgs addonInfo) + private void PreFinalize(AddonEvent type, AddonArgs args) { - var addon = (AtkUnitBase*)addonInfo.Addon; - - this.UpdateNodePositions(addon); + this.entryStartPos = float.NaN; } /// @@ -332,7 +340,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar { this.eventHandles.Clear(); } - + foreach (var entry in this.entries) { entry.TextNode = this.MakeNode(++this.runningNodeIds); @@ -340,28 +348,6 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } } - // Calculates the total width the dtr bar should be - private float CalculateTotalSize() - { - var addon = this.GetDtr(); - if (addon is null || addon->RootNode is null || addon->UldManager.NodeList is null) return 0; - - var totalSize = 0.0f; - - foreach (var index in Enumerable.Range(0, addon->UldManager.NodeListCount)) - { - var node = addon->UldManager.NodeList[index]; - - // Node 17 is the default CollisionNode that fits over the existing elements - if (node->NodeID is 17) totalSize += node->Width; - - // Node > 1000, are our custom nodes - if (node->NodeID is > 1000 && node->IsVisible) totalSize += node->Width + this.configuration.DtrSpacing; - } - - return totalSize; - } - private bool AddNode(AtkTextNode* node) { var dtr = this.GetDtr(); From 972abe78ceaa9740066ffe905fbb8c98e0796819 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 21 Oct 2023 00:38:11 +0200 Subject: [PATCH 275/585] Revert "Bump Luminas (#1495)" (#1497) This reverts commit 3fb5bcc348b7567a8ecf10331643ba768ba73514. --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 4 ++-- Dalamud/Dalamud.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 5815077c9..2c2a86b5f 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,8 +27,8 @@ - - + + all diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index d1fac251c..16c548eaf 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -68,8 +68,8 @@ - - + + all From dbe0f903c029bb125dad93c829b0b823194b94c6 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 22 Oct 2023 02:40:53 +0200 Subject: [PATCH 276/585] Update ClientStructs (#1491) 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 2dafda5fb..6ab86ffe3 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 2dafda5fb2da77a27f154671312040b9ed1f156d +Subproject commit 6ab86ffe30f0d9939e7f0e0fcaa86b2e72f6cd9d From e12ec0ceff8d37dbba5cca06f0455eb5babc8fb2 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 21 Oct 2023 18:06:02 -0700 Subject: [PATCH 277/585] Add FrameworkPluginScoped (#1442) Co-authored-by: KazWolfe --- Dalamud/Game/Framework.cs | 102 ++++++++++++++++++++++---- Dalamud/Plugin/Services/IFramework.cs | 5 +- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 22343fd8e..a13f0e209 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -12,23 +11,21 @@ using Dalamud.Game.Gui.Toast; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; -using Serilog; namespace Dalamud.Game; /// /// This class represents the Framework of the native game client and grants access to various subsystems. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 internal sealed class Framework : IDisposable, IServiceType, IFramework { + private static readonly ModuleLog Log = new("Framework"); + private static readonly Stopwatch StatsStopwatch = new(); private readonly GameLifecycle lifecycle; @@ -70,19 +67,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework /// A value indicating if the call was successful. public delegate bool OnRealDestroyDelegate(IntPtr framework); - /// - /// A delegate type used during the native Framework::free. - /// - /// The native Framework address. - public delegate IntPtr OnDestroyDelegate(); - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate bool OnUpdateDetour(IntPtr framework); - private delegate IntPtr OnDestroyDetour(); // OnDestroyDelegate - /// - public event IFramework.OnUpdateDelegate Update; + public event IFramework.OnUpdateDelegate? Update; /// /// Gets or sets a value indicating whether the collection of stats is enabled. @@ -523,3 +512,86 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework } } } + +/// +/// Plugin-scoped version of a Framework service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework +{ + [ServiceManager.ServiceDependency] + private readonly Framework frameworkService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal FrameworkPluginScoped() + { + this.frameworkService.Update += this.OnUpdateForward; + } + + /// + public event IFramework.OnUpdateDelegate? Update; + + /// + public DateTime LastUpdate => this.frameworkService.LastUpdate; + + /// + public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC; + + /// + public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta; + + /// + public bool IsInFrameworkUpdateThread => this.frameworkService.IsInFrameworkUpdateThread; + + /// + public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading; + + /// + public void Dispose() + { + this.frameworkService.Update -= this.OnUpdateForward; + + this.Update = null; + } + + /// + public Task RunOnFrameworkThread(Func func) + => this.frameworkService.RunOnFrameworkThread(func); + + /// + public Task RunOnFrameworkThread(Action action) + => this.frameworkService.RunOnFrameworkThread(action); + + /// + public Task RunOnFrameworkThread(Func> func) + => this.frameworkService.RunOnFrameworkThread(func); + + /// + public Task RunOnFrameworkThread(Func func) + => this.frameworkService.RunOnFrameworkThread(func); + + /// + public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken); + + /// + public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + => this.frameworkService.RunOnTick(action, delay, delayTicks, cancellationToken); + + /// + public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken); + + /// + public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken); + + private void OnUpdateForward(IFramework framework) => this.Update?.Invoke(framework); +} diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs index 334577b92..ca33c5867 100644 --- a/Dalamud/Plugin/Services/IFramework.cs +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -1,9 +1,6 @@ -using System; -using System.Threading; +using System.Threading; using System.Threading.Tasks; -using Dalamud.Game; - namespace Dalamud.Plugin.Services; /// From fdbdf26da615af3a5f479dd5201f4b04c1d9a49f Mon Sep 17 00:00:00 2001 From: ditzy Date: Mon, 23 Oct 2023 21:57:16 -0700 Subject: [PATCH 278/585] add support for area instance to SeString.CreateMapLink() (#1490) * add support for area instance to SeString.CreateMapLink() --------- Co-authored-by: ditzy --- .../Game/Text/SeStringHandling/SeString.cs | 62 +++++++++++++++++-- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 6132d0910..8cce5c286 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -254,10 +254,22 @@ public class SeString /// The raw x-coordinate for this link. /// The raw y-coordinate for this link.. /// An SeString containing all of the payloads necessary to display a map link in the chat log. - public static SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY) + public static SeString CreateMapLink(uint territoryId, uint mapId, int rawX, int rawY) => + CreateMapLinkWithInstance(territoryId, mapId, null, rawX, rawY); + + /// + /// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log. + /// + /// The id of the TerritoryType for this map link. + /// The id of the Map for this map link. + /// An optional area instance number to be included in this link. + /// The raw x-coordinate for this link. + /// The raw y-coordinate for this link.. + /// An SeString containing all of the payloads necessary to display a map link in the chat log. + public static SeString CreateMapLinkWithInstance(uint territoryId, uint mapId, int? instance, int rawX, int rawY) { var mapPayload = new MapLinkPayload(territoryId, mapId, rawX, rawY); - var nameString = $"{mapPayload.PlaceName} {mapPayload.CoordinateString}"; + var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString); var payloads = new List(new Payload[] { @@ -280,10 +292,24 @@ public class SeString /// The human-readable y-coordinate for this link. /// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases. /// An SeString containing all of the payloads necessary to display a map link in the chat log. - public static SeString CreateMapLink(uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) + public static SeString CreateMapLink( + uint territoryId, uint mapId, float xCoord, float yCoord, float fudgeFactor = 0.05f) => + CreateMapLinkWithInstance(territoryId, mapId, null, xCoord, yCoord, fudgeFactor); + + /// + /// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log. + /// + /// The id of the TerritoryType for this map link. + /// The id of the Map for this map link. + /// An optional area instance number to be included in this link. + /// The human-readable x-coordinate for this link. + /// The human-readable y-coordinate for this link. + /// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases. + /// An SeString containing all of the payloads necessary to display a map link in the chat log. + public static SeString CreateMapLinkWithInstance(uint territoryId, uint mapId, int? instance, float xCoord, float yCoord, float fudgeFactor = 0.05f) { var mapPayload = new MapLinkPayload(territoryId, mapId, xCoord, yCoord, fudgeFactor); - var nameString = $"{mapPayload.PlaceName} {mapPayload.CoordinateString}"; + var nameString = GetMapLinkNameString(mapPayload.PlaceName, instance, mapPayload.CoordinateString); var payloads = new List(new Payload[] { @@ -306,7 +332,20 @@ public class SeString /// The human-readable y-coordinate for this link. /// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases. /// An SeString containing all of the payloads necessary to display a map link in the chat log. - public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) + public static SeString? CreateMapLink(string placeName, float xCoord, float yCoord, float fudgeFactor = 0.05f) => + CreateMapLinkWithInstance(placeName, null, xCoord, yCoord, fudgeFactor); + + /// + /// Creates an SeString representing an entire Payload chain that can be used to link a map position in the chat log, matching a specified zone name. + /// Returns null if no corresponding PlaceName was found. + /// + /// The name of the location for this link. This should be exactly the name as seen in a displayed map link in-game for the same zone. + /// An optional area instance number to be included in this link. + /// The human-readable x-coordinate for this link. + /// The human-readable y-coordinate for this link. + /// An optional offset to account for rounding and truncation errors; it is best to leave this untouched in most cases. + /// An SeString containing all of the payloads necessary to display a map link in the chat log. + public static SeString? CreateMapLinkWithInstance(string placeName, int? instance, float xCoord, float yCoord, float fudgeFactor = 0.05f) { var data = Service.Get(); @@ -321,7 +360,7 @@ public class SeString var map = mapSheet.FirstOrDefault(row => row.PlaceName.Row == place.RowId); if (map != null && map.TerritoryType.Row != 0) { - return CreateMapLink(map.TerritoryType.Row, map.RowId, xCoord, yCoord, fudgeFactor); + return CreateMapLinkWithInstance(map.TerritoryType.Row, map.RowId, instance, xCoord, yCoord, fudgeFactor); } } @@ -329,6 +368,17 @@ public class SeString return null; } + private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString) + { + var instanceString = string.Empty; + if (instance is > 0 and < 10) + { + instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString(); + } + + return $"{placeName}{instanceString} {coordinateString}"; + } + /// /// Creates an SeString representing an entire payload chain that can be used to link party finder listings in the chat log. /// From 041937b2d87e6d85464c55de3c2bb5bcb419e7fb Mon Sep 17 00:00:00 2001 From: Sayu <104025906+SayuShira@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:49:33 +0200 Subject: [PATCH 279/585] fix: changelog crashes on improper LastUpdate timestamp (#1493) --- .../PluginInstaller/PluginChangelogEntry.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs index b4048536e..879034fd4 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginChangelogEntry.cs @@ -1,7 +1,6 @@ -using System; - -using CheapLoc; +using CheapLoc; using Dalamud.Plugin.Internal.Types; +using Serilog; namespace Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -36,7 +35,18 @@ internal class PluginChangelogEntry : IChangelogEntry this.Version = plugin.EffectiveVersion.ToString(); this.Text = plugin.Manifest.Changelog ?? Loc.Localize("ChangelogNoText", "No changelog for this version."); this.Author = plugin.Manifest.Author; - this.Date = DateTimeOffset.FromUnixTimeSeconds(this.Plugin.Manifest.LastUpdate).DateTime; + + try + { + this.Date = DateTimeOffset.FromUnixTimeSeconds(this.Plugin.Manifest.LastUpdate).DateTime; + } + catch (ArgumentOutOfRangeException ex) + { + Log.Warning(ex, "Manifest included improper timestamp, e.g. wrong unit: {PluginName}", + plugin.Manifest.Name); + // Create a Date from 0 as with a manifest that does not include a LastUpdate field + this.Date = DateTimeOffset.FromUnixTimeSeconds(0).DateTime; + } } /// From 9850ac3f1545622e2cedf8efa775bbf9dadbb689 Mon Sep 17 00:00:00 2001 From: liam <6005409+lmcintyre@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:03:55 -0400 Subject: [PATCH 280/585] Bump Luminas (#1499) * Bump Luminas * Bump Excel --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 4 ++-- Dalamud/Dalamud.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 2c2a86b5f..67ca26dee 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,8 +27,8 @@ - - + + all diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 16c548eaf..300fa402e 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -68,8 +68,8 @@ - - + + all From 9875a7ea313d96e7b343e46145f127b87ca8eef1 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 25 Oct 2023 17:32:42 +0200 Subject: [PATCH 281/585] feat: allow configuring the default page the installer opens to --- .../Internal/DalamudConfiguration.cs | 6 ++ Dalamud/Game/ChatHandlers.cs | 3 +- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 2 +- Dalamud/Interface/Internal/DalamudCommands.cs | 3 +- .../Interface/Internal/DalamudInterface.cs | 16 +--- .../Interface/Internal/InterfaceManager.cs | 3 +- .../Internal/PluginCategoryManager.cs | 2 +- .../PluginInstaller/PluginInstallerWindow.cs | 94 ++++++++++++++----- .../Windows/Settings/Tabs/SettingsTabLook.cs | 7 ++ Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- Dalamud/Plugin/Internal/PluginManager.cs | 3 +- 11 files changed, 98 insertions(+), 43 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 65f10c4ba..dbe95ea23 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -4,6 +4,7 @@ using System.IO; using System.Linq; using Dalamud.Game.Text; +using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Style; using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Profiles; @@ -424,6 +425,11 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// public double UiBuilderHitch { get; set; } = 100; + /// + /// Gets or sets the page of the plugin installer that is shown by default when opened. + /// + public PluginInstallerWindow.PluginInstallerOpenKind PluginInstallerOpen { get; set; } = PluginInstallerWindow.PluginInstallerOpenKind.AllPlugins; + /// /// Load a configuration from the provided path. /// diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index a41a66fa1..90a399d4c 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -13,6 +13,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; +using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Plugin.Internal; using Dalamud.Utility; using Serilog; @@ -118,7 +119,7 @@ internal class ChatHandlers : IServiceType this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) => { - Service.GetNullable()?.OpenPluginInstaller(); + Service.GetNullable()?.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins); }); } diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index b45b35c4d..9dc27e545 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -222,7 +222,7 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType switch (commandId) { case 69420: - dalamudInterface?.TogglePluginInstallerWindow(); + dalamudInterface?.TogglePluginInstallerWindowTo(this.configuration.PluginInstallerOpen); break; case 69421: dalamudInterface?.ToggleSettingsWindow(); diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index 307f79436..4654a019d 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -351,7 +351,8 @@ internal class DalamudCommands : IServiceType private void OnOpenInstallerCommand(string command, string arguments) { - Service.Get().TogglePluginInstallerWindow(); + var configuration = Service.Get(); + Service.Get().TogglePluginInstallerWindowTo(configuration.PluginInstallerOpen); } private void OnSetLanguageCommand(string command, string arguments) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index fc11f3f4b..189baab4d 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -254,18 +254,10 @@ internal class DalamudInterface : IDisposable, IServiceType /// /// Opens the on the plugin installed. /// - public void OpenPluginInstallerPluginInstalled() + /// The page of the installer to open. + public void OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind kind) { - this.pluginWindow.OpenInstalledPlugins(); - this.pluginWindow.BringToFront(); - } - - /// - /// Opens the on the plugin changelogs. - /// - public void OpenPluginInstallerPluginChangelogs() - { - this.pluginWindow.OpenPluginChangelogs(); + this.pluginWindow.OpenTo(kind); this.pluginWindow.BringToFront(); } @@ -397,7 +389,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// /// Toggles the . /// - public void TogglePluginInstallerWindow() => this.pluginWindow.Toggle(); + public void TogglePluginInstallerWindowTo(PluginInstallerWindow.PluginInstallerOpenKind kind) => this.pluginWindow.ToggleTo(kind); /// /// Toggles the . diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 93d9bb1dd..72b3bd6c8 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1242,7 +1242,8 @@ internal class InterfaceManager : IDisposable, IServiceType if (gamepadState.Pressed(GamepadButtons.R3) > 0) { - dalamudInterface.TogglePluginInstallerWindow(); + var configuration = Service.Get(); + dalamudInterface.TogglePluginInstallerWindowTo(configuration.PluginInstallerOpen); } } } diff --git a/Dalamud/Interface/Internal/PluginCategoryManager.cs b/Dalamud/Interface/Internal/PluginCategoryManager.cs index 28d0cddbd..ddfcff6bc 100644 --- a/Dalamud/Interface/Internal/PluginCategoryManager.cs +++ b/Dalamud/Interface/Internal/PluginCategoryManager.cs @@ -129,7 +129,7 @@ internal class PluginCategoryManager /// /// Gets a value indicating whether current group + category selection changed recently. - /// Changes in Available group should be followed with , everythine else can use . + /// Changes in Available group should be followed with , everything else can use . /// public bool IsContentDirty => this.isContentDirty; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 13db3509d..9ecc0c056 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -173,6 +173,27 @@ internal class PluginInstallerWindow : Window, IDisposable this.profileManagerWidget = new(this); } + /// + /// Enum describing pages the plugin installer can be opened to. + /// + public enum PluginInstallerOpenKind + { + /// + /// Open to the "All Plugins" page. + /// + AllPlugins, + + /// + /// Open to the "Installed Plugins" page. + /// + InstalledPlugins, + + /// + /// Open to the "Changelogs" page. + /// + Changelogs, + } + private enum OperationStatus { Idle, @@ -220,6 +241,28 @@ internal class PluginInstallerWindow : Window, IDisposable } } + /// + /// Open to the installer to the page specified by . + /// + /// The page of the installer to open. + public void OpenTo(PluginInstallerOpenKind kind) + { + this.IsOpen = true; + this.SetOpenPage(kind); + } + + /// + /// Toggle to the installer to the page specified by . + /// + /// The page of the installer to open. + public void ToggleTo(PluginInstallerOpenKind kind) + { + this.Toggle(); + + if (this.IsOpen) + this.SetOpenPage(kind); + } + /// public override void OnOpen() { @@ -278,30 +321,6 @@ internal class PluginInstallerWindow : Window, IDisposable this.imageCache.ClearIconCache(); } - /// - /// Open the window on the plugin changelogs. - /// - public void OpenInstalledPlugins() - { - // Installed group - this.categoryManager.CurrentGroupIdx = 1; - // All category - this.categoryManager.CurrentCategoryIdx = 0; - this.IsOpen = true; - } - - /// - /// Open the window on the plugin changelogs. - /// - public void OpenPluginChangelogs() - { - // Changelog group - this.categoryManager.CurrentGroupIdx = 3; - // Plugins category - this.categoryManager.CurrentCategoryIdx = 2; - this.IsOpen = true; - } - /// /// Sets the current search text and marks it as prefilled. /// @@ -386,6 +405,33 @@ internal class PluginInstallerWindow : Window, IDisposable return true; } + private void SetOpenPage(PluginInstallerOpenKind kind) + { + switch (kind) + { + case PluginInstallerOpenKind.AllPlugins: + // Plugins group + this.categoryManager.CurrentGroupIdx = 0; + // All category + this.categoryManager.CurrentCategoryIdx = 0; + break; + case PluginInstallerOpenKind.InstalledPlugins: + // Installed group + this.categoryManager.CurrentGroupIdx = 2; + // All category + this.categoryManager.CurrentCategoryIdx = 0; + break; + case PluginInstallerOpenKind.Changelogs: + // Changelog group + this.categoryManager.CurrentGroupIdx = 3; + // Plugins category + this.categoryManager.CurrentCategoryIdx = 2; + break; + default: + throw new ArgumentOutOfRangeException(nameof(kind), kind, null); + } + } + private void DrawProgressOverlay() { var pluginManager = Service.Get(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index b34a13cc5..7a6f894c1 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; using Dalamud.Interface.Utility; using Dalamud.Utility; @@ -120,6 +121,12 @@ public class SettingsTabLook : SettingsTab Loc.Localize("DalamudSettingToggleTsmHint", "This will allow you to access certain Dalamud and Plugin functionality from the title screen."), c => c.ShowTsm, (v, c) => c.ShowTsm = v), + + new SettingsEntry( + Loc.Localize("DalamudSettingInstallerOpenDefault", "Open the Plugin Installer to the \"Installed Plugins\" tab by default"), + Loc.Localize("DalamudSettingInstallerOpenDefaultHint", "This will allow you to open the Plugin Installer to the \"Installed Plugins\" tab by default, instead of the \"Available Plugins\" tab."), + c => c.PluginInstallerOpen == PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins, + (v, c) => c.PluginInstallerOpen = v ? PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins : PluginInstallerWindow.PluginInstallerOpenKind.AllPlugins), }; public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel"); diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 004b7196c..82f19aa49 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -223,7 +223,7 @@ public sealed class DalamudPluginInterface : IDisposable return false; } - dalamudInterface.OpenPluginInstallerPluginInstalled(); + dalamudInterface.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.InstalledPlugins); dalamudInterface.SetPluginInstallerSearchText(this.plugin.InternalName); return true; diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 664406157..04698947e 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -19,6 +19,7 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Networking.Http; @@ -130,7 +131,7 @@ internal partial class PluginManager : IDisposable, IServiceType this.openInstallerWindowPluginChangelogsLink = Service.Get().AddChatLinkHandler("Dalamud", 1003, (_, _) => { - Service.GetNullable()?.OpenPluginInstallerPluginChangelogs(); + Service.GetNullable()?.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.Changelogs); }); this.configuration.PluginTestingOptIns ??= new List(); From 62a61735c6ef415a66652b3573f8ad8cb0744298 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 25 Oct 2023 19:56:29 +0200 Subject: [PATCH 282/585] [master] Update ClientStructs (#1500) 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 6ab86ffe3..01feb9571 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 6ab86ffe30f0d9939e7f0e0fcaa86b2e72f6cd9d +Subproject commit 01feb9571e05b96146e2e0815e6f0fca92905e80 From b9be67ce98bb508087a2e2e3bf999a07ffbe10be Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 25 Oct 2023 20:17:27 +0200 Subject: [PATCH 283/585] [master] Update ClientStructs (#1501) 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 01feb9571..172633727 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 01feb9571e05b96146e2e0815e6f0fca92905e80 +Subproject commit 172633727aeae54e7f14b0ef55aa4b602dc1ed26 From b7eb0f122c38b885a456c4d6fdd77aaabf1720b2 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 25 Oct 2023 20:39:15 +0200 Subject: [PATCH 284/585] build: 9.0.0.6 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 300fa402e..82c4323e8 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.5 + 9.0.0.6 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 9187a100da8030f9768f11e2810b725df9ba49df Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 27 Oct 2023 12:21:10 +0200 Subject: [PATCH 285/585] fix: use correct installer group IDs --- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 4 ++-- Dalamud/Interface/Internal/DalamudInterface.cs | 15 ++++++++++----- .../PluginInstaller/PluginInstallerWindow.cs | 4 ++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 9dc27e545..0013dca4d 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -222,10 +222,10 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType switch (commandId) { case 69420: - dalamudInterface?.TogglePluginInstallerWindowTo(this.configuration.PluginInstallerOpen); + dalamudInterface?.OpenPluginInstaller(); break; case 69421: - dalamudInterface?.ToggleSettingsWindow(); + dalamudInterface?.OpenSettings(); break; default: this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId); diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 189baab4d..a6c4e243c 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -48,7 +48,9 @@ internal class DalamudInterface : IDisposable, IServiceType private const float CreditsDarkeningMaxAlpha = 0.8f; private static readonly ModuleLog Log = new("DUI"); - + + private readonly DalamudConfiguration configuration; + private readonly ChangelogWindow changelogWindow; private readonly ColorDemoWindow colorDemoWindow; private readonly ComponentDemoWindow componentDemoWindow; @@ -92,6 +94,8 @@ internal class DalamudInterface : IDisposable, IServiceType PluginImageCache pluginImageCache, Branding branding) { + this.configuration = configuration; + var interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -135,7 +139,7 @@ internal class DalamudInterface : IDisposable, IServiceType interfaceManager.Draw += this.OnDraw; var tsm = Service.Get(); - tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, this.OpenPluginInstaller); + tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, () => this.OpenPluginInstaller()); tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), branding.LogoSmall, this.OpenSettings); if (!configuration.DalamudBetaKind.IsNullOrEmpty()) @@ -241,13 +245,14 @@ internal class DalamudInterface : IDisposable, IServiceType this.pluginStatWindow.IsOpen = true; this.pluginStatWindow.BringToFront(); } - + /// - /// Opens the . + /// Opens the on the plugin installed. /// + /// The page of the installer to open. public void OpenPluginInstaller() { - this.pluginWindow.IsOpen = true; + this.pluginWindow.OpenTo(this.configuration.PluginInstallerOpen); this.pluginWindow.BringToFront(); } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 9ecc0c056..a66d132c7 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -411,13 +411,13 @@ internal class PluginInstallerWindow : Window, IDisposable { case PluginInstallerOpenKind.AllPlugins: // Plugins group - this.categoryManager.CurrentGroupIdx = 0; + this.categoryManager.CurrentGroupIdx = 2; // All category this.categoryManager.CurrentCategoryIdx = 0; break; case PluginInstallerOpenKind.InstalledPlugins: // Installed group - this.categoryManager.CurrentGroupIdx = 2; + this.categoryManager.CurrentGroupIdx = 1; // All category this.categoryManager.CurrentCategoryIdx = 0; break; From e5e962297407ca7896b3cd8519793c2e0b9b16d0 Mon Sep 17 00:00:00 2001 From: goat Date: Fri, 27 Oct 2023 12:21:49 +0200 Subject: [PATCH 286/585] build: 9.0.0.7 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 82c4323e8..fba6fd477 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.6 + 9.0.0.7 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From f60e7b7a865277ef858b424d638cbef54ba114dc Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 29 Oct 2023 19:38:31 +0900 Subject: [PATCH 287/585] Fulfill BuildLookupTable preconditions (#1507) --- .../Interface/GameFonts/GameFontManager.cs | 2 +- .../Interface/Internal/InterfaceManager.cs | 2 +- Dalamud/Interface/Utility/ImGuiHelpers.cs | 20 ++++++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index 48a1f7271..a7cd27b83 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -169,7 +169,7 @@ internal class GameFontManager : IServiceType } if (rebuildLookupTable && fontPtr.Glyphs.Size > 0) - fontPtr.BuildLookupTable(); + fontPtr.BuildLookupTableNonstandard(); } /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 72b3bd6c8..d5394fe8d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1028,7 +1028,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (font.FindGlyphNoFallback(Fallback1Codepoint).NativePtr != null) font.FallbackChar = Fallback1Codepoint; - font.BuildLookupTable(); + font.BuildLookupTableNonstandard(); } Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index dbb873edf..010178b26 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -240,7 +240,25 @@ public static class ImGuiHelpers } if (rebuildLookupTable && target.Value!.Glyphs.Size > 0) - target.Value!.BuildLookupTable(); + target.Value!.BuildLookupTableNonstandard(); + } + + /// + /// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions. + /// + /// The font. + public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font) + { + // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. + // FallbackGlyph is resolved after resolving ' '. + // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, + // making FindGlyph return nullptr. + // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, + // making ImGui attempt to treat whatever was there as a ' '. + // This may cause random glyphs to be sized randomly, if not an access violation exception. + font.NativePtr->FallbackGlyph = null; + + font.BuildLookupTable(); } /// From 28804b905e2040d88115260ea159c2641713e4fb Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 29 Oct 2023 06:01:48 -0700 Subject: [PATCH 288/585] Fix Framework Stat Tracking (#1506) --- Dalamud/Game/Framework.cs | 110 ++++++++++++++++++++++++-------------- 1 file changed, 71 insertions(+), 39 deletions(-) diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index a13f0e209..6db9f7312 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -98,6 +98,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework /// public bool IsFrameworkUnloading { get; internal set; } + /// + /// Gets the list of update sub-delegates that didn't get updated this frame. + /// + internal List NonUpdatedSubDelegates { get; private set; } = new(); + /// /// Gets or sets a value indicating whether to dispatch update events. /// @@ -272,6 +277,58 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework this.updateStopwatch.Reset(); StatsStopwatch.Reset(); } + + /// + /// Adds a update time to the stats history. + /// + /// Delegate Name. + /// Runtime. + internal static void AddToStats(string key, double ms) + { + if (!StatsHistory.ContainsKey(key)) + StatsHistory.Add(key, new List()); + + StatsHistory[key].Add(ms); + + if (StatsHistory[key].Count > 1000) + { + StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000); + } + } + + /// + /// Profiles each sub-delegate in the eventDelegate and logs to StatsHistory. + /// + /// The Delegate to Profile. + /// The Framework Instance to pass to delegate. + internal void ProfileAndInvoke(IFramework.OnUpdateDelegate? eventDelegate, IFramework frameworkInstance) + { + if (eventDelegate is null) return; + + var invokeList = eventDelegate.GetInvocationList(); + + // Individually invoke OnUpdate handlers and time them. + foreach (var d in invokeList) + { + var stopwatch = Stopwatch.StartNew(); + try + { + d.Method.Invoke(d.Target, new object[] { frameworkInstance }); + } + catch (Exception ex) + { + Log.Error(ex, "Exception while dispatching Framework::Update event."); + } + + stopwatch.Stop(); + + var key = $"{d.Target}::{d.Method.Name}"; + if (this.NonUpdatedSubDelegates.Contains(key)) + this.NonUpdatedSubDelegates.Remove(key); + + AddToStats(key, stopwatch.Elapsed.TotalMilliseconds); + } + } [ServiceManager.CallWhenServicesReady] private void ContinueConstruction() @@ -329,19 +386,6 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework this.LastUpdate = DateTime.Now; this.LastUpdateUTC = DateTime.UtcNow; - void AddToStats(string key, double ms) - { - if (!StatsHistory.ContainsKey(key)) - StatsHistory.Add(key, new List()); - - StatsHistory[key].Add(ms); - - if (StatsHistory[key].Count > 1000) - { - StatsHistory[key].RemoveRange(0, StatsHistory[key].Count - 1000); - } - } - if (StatsEnabled) { StatsStopwatch.Restart(); @@ -358,33 +402,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework if (StatsEnabled && this.Update != null) { // Stat Tracking for Framework Updates - var invokeList = this.Update.GetInvocationList(); - var notUpdated = StatsHistory.Keys.ToList(); - - // Individually invoke OnUpdate handlers and time them. - foreach (var d in invokeList) - { - StatsStopwatch.Restart(); - try - { - d.Method.Invoke(d.Target, new object[] { this }); - } - catch (Exception ex) - { - Log.Error(ex, "Exception while dispatching Framework::Update event."); - } - - StatsStopwatch.Stop(); - - var key = $"{d.Target}::{d.Method.Name}"; - if (notUpdated.Contains(key)) - notUpdated.Remove(key); - - AddToStats(key, StatsStopwatch.Elapsed.TotalMilliseconds); - } + this.NonUpdatedSubDelegates = StatsHistory.Keys.ToList(); + this.ProfileAndInvoke(this.Update, this); // Cleanup handlers that are no longer being called - foreach (var key in notUpdated) + foreach (var key in this.NonUpdatedSubDelegates) { if (key == nameof(this.RunPendingTickTasks)) continue; @@ -593,5 +615,15 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) => this.frameworkService.RunOnTick(func, delay, delayTicks, cancellationToken); - private void OnUpdateForward(IFramework framework) => this.Update?.Invoke(framework); + private void OnUpdateForward(IFramework framework) + { + if (Framework.StatsEnabled && this.Update != null) + { + this.frameworkService.ProfileAndInvoke(this.Update, framework); + } + else + { + this.Update?.Invoke(framework); + } + } } From af55d6fcfdf8ac042551506900f70ef8cc4ee259 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 29 Oct 2023 14:16:25 +0100 Subject: [PATCH 289/585] [master] Update ClientStructs (#1502) 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 172633727..b550805d9 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 172633727aeae54e7f14b0ef55aa4b602dc1ed26 +Subproject commit b550805d95e7b7be2ab5c47659b82761ba9f62bf From e30c904ad62bdcb527c72eaf6721418a23ef5078 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 29 Oct 2023 14:17:53 +0100 Subject: [PATCH 290/585] build: 9.0.0.8 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index fba6fd477..d0ce1f669 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.7 + 9.0.0.8 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 36364576a82f6522703f0a178f1413484dda4834 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Mon, 30 Oct 2023 21:40:46 +0100 Subject: [PATCH 291/585] feat: MVP pinning, clickthrough, alpha menu for Window System windows Needs saving, an actual button in the title bar --- Dalamud/Interface/Windowing/Window.cs | 130 +++++++++++++++++++++++++- 1 file changed, 128 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index f0914bb21..600e423e1 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -1,7 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; - +using System.Runtime.InteropServices; +using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using FFXIVClientStructs.FFXIV.Client.UI; @@ -20,6 +23,11 @@ public abstract class Window private bool internalLastIsOpen = false; private bool internalIsOpen = false; + private bool internalIsPinned = false; + private bool internalIsClickthrough = false; + private DateTimeOffset internalLastDisableClick = DateTimeOffset.MinValue; + private bool didPushInternalAlpha = false; + private float? internalAlpha = null; private bool nextFrameBringToFront = false; /// @@ -130,6 +138,16 @@ public abstract class Window /// public bool ShowCloseButton { get; set; } = true; + /// + /// Gets or sets a value indicating whether or not this window should offer to be pinned via the window's titlebar context menu. + /// + public bool AllowPinning { get; set; } = true; + + /// + /// Gets or sets a value indicating whether or not this window should offer to be made click-through via the window's titlebar context menu. + /// + public bool AllowClickthrough { get; set; } = true; + /// /// Gets or sets a value indicating whether or not this window will stay open. /// @@ -185,6 +203,11 @@ public abstract class Window /// public virtual void PreDraw() { + if (this.internalAlpha.HasValue) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, this.internalAlpha.Value); + this.didPushInternalAlpha = true; + } } /// @@ -192,6 +215,11 @@ public abstract class Window /// public virtual void PostDraw() { + if (this.didPushInternalAlpha) + { + ImGui.PopStyleVar(); + this.didPushInternalAlpha = false; + } } /// @@ -286,7 +314,15 @@ public abstract class Window this.nextFrameBringToFront = false; } - if (this.ShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, this.Flags) : ImGui.Begin(this.WindowName, this.Flags)) + var flags = this.Flags; + + if (this.internalIsPinned) + flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; + + if (this.internalIsClickthrough) + flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs; + + if (this.ShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags)) { // Draw the actual window contents try @@ -297,6 +333,80 @@ public abstract class Window { Log.Error(ex, $"Error during Draw(): {this.WindowName}"); } + + if (this.AllowPinning || this.AllowClickthrough) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); + + var popupName = "WindowSystemContextActions"; + if (ImGui.BeginPopup(popupName)) + { + if (this.internalIsClickthrough) + ImGui.BeginDisabled(); + + if (this.AllowPinning) + ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref this.internalIsPinned); + + if (this.internalIsClickthrough) + ImGui.EndDisabled(); + + if (this.AllowClickthrough) + { + if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"), + ref this.internalIsClickthrough)) + { + if (this.internalIsClickthrough) + this.internalIsPinned = true; + } + } + + var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f; + if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f, + 100f)) + { + this.internalAlpha = alpha / 100f; + } + + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset"))) + { + this.internalAlpha = null; + } + + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("WindowSystemContextActionDoubleClick", "Double click the title bar to disable clickthrough.")); + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("WindowSystemContextActionDisclaimer", "These options may not work for all plugins at the moment.")); + + ImGui.EndPopup(); + } + + ImGui.PopStyleVar(); + + var titleBarRect = Vector4.Zero; + unsafe + { + var window = ImGuiNativeAdditions.igGetCurrentWindow(); + ImGuiNativeAdditions.ImGuiWindow_TitleBarRect(&titleBarRect, window); + } + + if (ImGui.IsMouseHoveringRect(new Vector2(titleBarRect.X, titleBarRect.Y), new Vector2(titleBarRect.Z, titleBarRect.W), false)) + { + if (ImGui.IsWindowHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Right)) + { + ImGui.OpenPopup(popupName); + } + + if (ImGui.IsMouseDown(ImGuiMouseButton.Left)) + { + if (DateTime.Now - this.internalLastDisableClick < TimeSpan.FromMilliseconds(100)) + { + this.internalIsPinned = false; + this.internalIsClickthrough = false; + } + + this.internalLastDisableClick = DateTime.Now; + } + } + } } if (wasFocused) @@ -375,6 +485,12 @@ public abstract class Window { ImGui.SetNextWindowBgAlpha(this.BgAlpha.Value); } + + // Manually set alpha takes precedence, if devs don't want that, they should turn it off + if (this.internalAlpha.HasValue) + { + ImGui.SetNextWindowBgAlpha(this.internalAlpha.Value); + } } /// @@ -392,4 +508,14 @@ public abstract class Window /// public Vector2 MaximumSize { get; set; } } + + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "imports")] + private static unsafe class ImGuiNativeAdditions + { + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void* igGetCurrentWindow(); + + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + public static extern unsafe void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window); + } } From d8c3c4c789b10a65ec37a066b7ac491c3ed16778 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 31 Oct 2023 03:36:58 +0100 Subject: [PATCH 292/585] Update ClientStructs (#1509) 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 b550805d9..e2355970e 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b550805d95e7b7be2ab5c47659b82761ba9f62bf +Subproject commit e2355970e236ffe09dfc7af674d1e29fb6b93786 From 67ae069a2316a23bd005d7fc6faff12c25c65f06 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 30 Oct 2023 19:39:43 -0700 Subject: [PATCH 293/585] AddonLifecycle ReceiveEvent improvements (#1511) * Prototype * Add hook null safety Add a check to make sure addons that invoke virtual functions for other addons don't trigger lifecycle messages multiple times. * Expose event listeners for AddonLifecycleWidget.cs Disable hook when all listeners for an addon unregister * Add AddonLifecycleWidget.cs * Remove excess logging --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 171 ++++++++++-------- .../AddonLifecycleReceiveEventListener.cs | 119 ++++++++++++ .../Internal/Windows/Data/DataWindow.cs | 1 + .../Data/Widgets/AddonLifecycleWidget.cs | 135 ++++++++++++++ 4 files changed, 346 insertions(+), 80 deletions(-) create mode 100644 Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index c42481d63..fcfe7766f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -21,6 +21,16 @@ namespace Dalamud.Game.Addon.Lifecycle; [ServiceManager.EarlyLoadedService] internal unsafe class AddonLifecycle : IDisposable, IServiceType { + /// + /// List of all AddonLifecycle ReceiveEvent Listener Hooks. + /// + internal readonly List ReceiveEventListeners = new(); + + /// + /// List of all AddonLifecycle Event Listeners. + /// + internal readonly List EventListeners = new(); + private static readonly ModuleLog Log = new("AddonLifecycle"); [ServiceManager.ServiceDependency] @@ -39,9 +49,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); - private readonly List eventListeners = new(); - - private readonly Dictionary> receiveEventHooks = new(); [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) @@ -75,8 +82,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate byte AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); - private delegate void AddonReceiveEventDelegate(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint a5); - /// public void Dispose() { @@ -90,9 +95,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRefreshHook.Dispose(); this.onAddonRequestedUpdateHook.Dispose(); - foreach (var (_, hook) in this.receiveEventHooks) + foreach (var receiveEventListener in this.ReceiveEventListeners) { - hook.Dispose(); + receiveEventListener.Dispose(); } } @@ -114,6 +119,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.removeEventListeners.Add(listener); } + /// + /// Invoke listeners for the specified event type. + /// + /// Event Type. + /// AddonArgs. + internal void InvokeListeners(AddonEvent eventType, AddonArgs args) + { + // Match on string.empty for listeners that want events for all addons. + foreach (var listener in this.EventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) + { + listener.FunctionDelegate.Invoke(eventType, args); + } + } + // Used to prevent concurrency issues if plugins try to register during iteration of listeners. private void OnFrameworkUpdate(IFramework unused) { @@ -121,15 +140,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { foreach (var toAddListener in this.newEventListeners) { - this.eventListeners.Add(toAddListener); + this.EventListeners.Add(toAddListener); // 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 (toAddListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) { - if (this.receiveEventHooks.TryGetValue(toAddListener.AddonName, out var hook)) + if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toAddListener.AddonName)) is { } receiveEventListener) { - hook.Enable(); + receiveEventListener.Hook?.Enable(); } } } @@ -141,7 +160,21 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { foreach (var toRemoveListener in this.removeEventListeners) { - this.eventListeners.Remove(toRemoveListener); + this.EventListeners.Remove(toRemoveListener); + + // If we are disabling an ReceiveEvent listener, check if we should disable the hook. + if (toRemoveListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) + { + // Get the ReceiveEvent Listener for this addon + if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toRemoveListener.AddonName)) is { } receiveEventListener) + { + // If there are no other listeners listening for this event, disable the hook. + if (!this.EventListeners.Any(listener => listener.AddonName.Contains(toRemoveListener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent)) + { + receiveEventListener.Hook?.Disable(); + } + } + } } this.removeEventListeners.Clear(); @@ -160,12 +193,53 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonRequestedUpdateHook.Enable(); } - private void InvokeListeners(AddonEvent eventType, AddonArgs args) + private void RegisterReceiveEventHook(AtkUnitBase* addon) { - // Match on string.empty for listeners that want events for all addons. - foreach (var listener in this.eventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) + // 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 = MemoryHelper.ReadStringNullTerminated((nint)addon->Name); + var receiveEventAddress = (nint)addon->VTable->ReceiveEvent; + if (receiveEventAddress != this.disallowedReceiveEventAddress) { - listener.FunctionDelegate.Invoke(eventType, args); + // 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.HookAddress == receiveEventAddress) is { } existingListener) + { + if (!existingListener.AddonNames.Contains(addonName)) + { + existingListener.AddonNames.Add(addonName); + } + } + + // 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.Hook?.Enable(); + } + } + } + } + + private void UnregisterReceiveEventHook(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(); + } } } @@ -173,20 +247,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - // 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 = MemoryHelper.ReadStringNullTerminated((nint)addon->Name); - var receiveEventAddress = (nint)addon->VTable->ReceiveEvent; - if (receiveEventAddress != this.disallowedReceiveEventAddress) - { - var receiveEventHook = Hook.FromAddress(receiveEventAddress, this.OnReceiveEvent); - this.receiveEventHooks.TryAdd(addonName, receiveEventHook); - - if (this.eventListeners.Any(listener => (listener.EventType is AddonEvent.PostReceiveEvent or AddonEvent.PreReceiveEvent) && listener.AddonName == addonName)) - { - receiveEventHook.Enable(); - } - } + this.RegisterReceiveEventHook(addon); } catch (Exception e) { @@ -235,13 +296,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { try { - // Remove this addons ReceiveEvent Registration var addonName = MemoryHelper.ReadStringNullTerminated((nint)atkUnitBase[0]->Name); - if (this.receiveEventHooks.TryGetValue(addonName, out var hook)) - { - hook.Dispose(); - this.receiveEventHooks.Remove(addonName); - } + this.UnregisterReceiveEventHook(addonName); } catch (Exception e) { @@ -410,51 +466,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke."); } } - - private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint data) - { - try - { - this.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs - { - Addon = (nint)addon, - AtkEventType = (byte)eventType, - EventParam = eventParam, - AtkEvent = (nint)atkEvent, - Data = data, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke."); - } - - try - { - var addonName = MemoryHelper.ReadStringNullTerminated((nint)addon->Name); - this.receiveEventHooks[addonName].Original(addon, eventType, eventParam, atkEvent, data); - } - 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."); - } - - try - { - this.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs - { - Addon = (nint)addon, - AtkEventType = (byte)eventType, - EventParam = eventParam, - AtkEvent = (nint)atkEvent, - Data = data, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke."); - } - } } /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs new file mode 100644 index 000000000..10171eb16 --- /dev/null +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; + +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Hooking; +using Dalamud.Logging.Internal; +using Dalamud.Memory; +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"); + + /// + /// 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 = new List { addonName }; + this.Hook = Hook.FromAddress(receiveEventAddress, this.OnReceiveEvent); + } + + /// + /// Addon Receive Event Function delegate. + /// + /// Addon Pointer. + /// Event Type. + /// Unique Event ID. + /// Event Data. + /// Unknown. + public delegate void AddonReceiveEventDelegate(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint a5); + + /// + /// Gets the list of addons that use this receive event hook. + /// + public List AddonNames { get; init; } + + /// + /// Gets the address of the registered hook. + /// + public nint HookAddress => this.Hook?.Address ?? nint.Zero; + + /// + /// Gets the contained hook for these addons. + /// + public Hook? Hook { get; init; } + + /// + /// Gets or sets the Reference to AddonLifecycle service instance. + /// + private AddonLifecycle AddonLifecycle { get; set; } + + /// + public void Dispose() + { + this.Hook?.Dispose(); + } + + private void OnReceiveEvent(AtkUnitBase* addon, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, nint data) + { + // Check that we didn't get here through a call to another addons handler. + var addonName = MemoryHelper.ReadString((nint)addon->Name, 0x20); + if (!this.AddonNames.Contains(addonName)) + { + this.Hook!.Original(addon, eventType, eventParam, atkEvent, data); + return; + } + + try + { + this.AddonLifecycle.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs + { + Addon = (nint)addon, + AtkEventType = (byte)eventType, + EventParam = eventParam, + AtkEvent = (nint)atkEvent, + Data = data, + }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke."); + } + + try + { + this.Hook!.Original(addon, eventType, eventParam, atkEvent, data); + } + 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."); + } + + try + { + this.AddonLifecycle.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs + { + Addon = (nint)addon, + AtkEventType = (byte)eventType, + EventParam = eventParam, + AtkEvent = (nint)atkEvent, + Data = data, + }); + } + catch (Exception e) + { + Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke."); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index ba47d2c8e..d59b50e58 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -50,6 +50,7 @@ internal class DataWindow : Window new DataShareWidget(), new NetworkMonitorWidget(), new IconBrowserWidget(), + new AddonLifecycleWidget(), }; private readonly IOrderedEnumerable orderedModules; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs new file mode 100644 index 000000000..a0cba737c --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -0,0 +1,135 @@ +using System.Diagnostics.CodeAnalysis; +using System.Drawing; +using System.Linq; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Interface.Utility; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Debug widget for displaying AddonLifecycle data. +/// +public class AddonLifecycleWidget : IDataWindowWidget +{ + /// + public string[]? CommandShortcuts { get; init; } = { "AddonLifecycle" }; + + /// + public string DisplayName { get; init; } = "Addon Lifecycle"; + + /// + [MemberNotNullWhen(true, "AddonLifecycle")] + public bool Ready { get; set; } + + private AddonLifecycle? AddonLifecycle { get; set; } + + /// + public void Load() + { + this.AddonLifecycle = Service.GetNullable(); + if (this.AddonLifecycle is not null) this.Ready = true; + } + + /// + public void Draw() + { + if (!this.Ready) + { + ImGui.Text("AddonLifecycle Reference is null, reload module."); + return; + } + + if (ImGui.CollapsingHeader("Listeners")) + { + ImGui.Indent(); + this.DrawEventListeners(); + ImGui.Unindent(); + } + + if (ImGui.CollapsingHeader("ReceiveEvent Hooks")) + { + ImGui.Indent(); + this.DrawReceiveEventHooks(); + ImGui.Unindent(); + } + } + + private void DrawEventListeners() + { + if (!this.Ready) return; + + foreach (var eventType in Enum.GetValues()) + { + if (ImGui.CollapsingHeader(eventType.ToString())) + { + ImGui.Indent(); + var listeners = this.AddonLifecycle.EventListeners.Where(listener => listener.EventType == eventType).ToList(); + + if (!listeners.Any()) + { + ImGui.Text("No Listeners Registered for Event"); + } + + if (ImGui.BeginTable("AddonLifecycleListenersTable", 2)) + { + ImGui.TableSetupColumn("##AddonName", ImGuiTableColumnFlags.WidthFixed, 100.0f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("##MethodInvoke", ImGuiTableColumnFlags.WidthStretch); + + foreach (var listener in listeners) + { + ImGui.TableNextColumn(); + ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); + + ImGui.TableNextColumn(); + ImGui.Text($"{listener.FunctionDelegate.Target}::{listener.FunctionDelegate.Method.Name}"); + } + + ImGui.EndTable(); + } + + ImGui.Unindent(); + } + } + } + + private void DrawReceiveEventHooks() + { + if (!this.Ready) return; + + var listeners = this.AddonLifecycle.ReceiveEventListeners; + + if (!listeners.Any()) + { + ImGui.Text("No ReceiveEvent Hooks are Registered"); + } + + foreach (var receiveEventListener in this.AddonLifecycle.ReceiveEventListeners) + { + if (ImGui.CollapsingHeader(string.Join(", ", receiveEventListener.AddonNames))) + { + ImGui.Columns(2); + + ImGui.Text("Hook Address"); + ImGui.NextColumn(); + ImGui.Text(receiveEventListener.HookAddress.ToString("X")); + + ImGui.NextColumn(); + ImGui.Text("Hook Status"); + ImGui.NextColumn(); + if (receiveEventListener.Hook is null) + { + ImGui.Text("Hook is null"); + } + else + { + var color = receiveEventListener.Hook.IsEnabled ? KnownColor.Green.Vector() : KnownColor.OrangeRed.Vector(); + var text = receiveEventListener.Hook.IsEnabled ? "Enabled" : "Disabled"; + ImGui.TextColored(color, text); + } + + ImGui.Columns(1); + } + } + } +} From 9ccc389b0b1b0446f480f61903ca84b7a1f9a2d0 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:07:34 +0100 Subject: [PATCH 294/585] Update ClientStructs (#1513) 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 e2355970e..de55cd12a 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e2355970e236ffe09dfc7af674d1e29fb6b93786 +Subproject commit de55cd12a61f07bfa3f2d1e54782c0dd57b52426 From e538763f203ae252e1eb8c2bd1d8435d55a02c45 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 31 Oct 2023 13:21:55 -0700 Subject: [PATCH 295/585] chore: Fix most 6.51 warnings (#1514) --- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 20 ++++++++--------- Dalamud/Game/Gui/Dtr/DtrBar.cs | 2 +- .../Game/Text/SeStringHandling/SeString.cs | 22 +++++++++---------- .../Interface/Internal/DalamudInterface.cs | 2 +- Dalamud/Interface/Internal/UiDebug.cs | 2 +- .../Data/Widgets/AddonLifecycleWidget.cs | 1 + Dalamud/Interface/Windowing/Window.cs | 1 + 7 files changed, 26 insertions(+), 24 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index fcfe7766f..c7184ca11 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -21,16 +21,6 @@ namespace Dalamud.Game.Addon.Lifecycle; [ServiceManager.EarlyLoadedService] internal unsafe class AddonLifecycle : IDisposable, IServiceType { - /// - /// List of all AddonLifecycle ReceiveEvent Listener Hooks. - /// - internal readonly List ReceiveEventListeners = new(); - - /// - /// List of all AddonLifecycle Event Listeners. - /// - internal readonly List EventListeners = new(); - private static readonly ModuleLog Log = new("AddonLifecycle"); [ServiceManager.ServiceDependency] @@ -81,6 +71,16 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private delegate void AddonOnRequestedUpdateDelegate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData); private delegate byte AddonOnRefreshDelegate(AtkUnitManager* unitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values); + + /// + /// 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(); /// public void Dispose() diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 993bb951f..2e5ecb9c5 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -413,7 +413,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar newTextNode->AtkResNode.NodeID = nodeId; newTextNode->AtkResNode.Type = NodeType.Text; newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents; - newTextNode->AtkResNode.DrawFlags = 12; + newTextNode->AtkResNode.ViewFlags = (NodeViewFlags)12; newTextNode->AtkResNode.SetWidth(22); newTextNode->AtkResNode.SetHeight(22); newTextNode->AtkResNode.SetPositionFloat(-200, 2); diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 8cce5c286..47c38b227 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -368,17 +368,6 @@ public class SeString return null; } - private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString) - { - var instanceString = string.Empty; - if (instance is > 0 and < 10) - { - instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString(); - } - - return $"{placeName}{instanceString} {coordinateString}"; - } - /// /// Creates an SeString representing an entire payload chain that can be used to link party finder listings in the chat log. /// @@ -512,4 +501,15 @@ public class SeString { return this.TextValue; } + + private static string GetMapLinkNameString(string placeName, int? instance, string coordinateString) + { + var instanceString = string.Empty; + if (instance is > 0 and < 10) + { + instanceString = (SeIconChar.Instance1 + instance.Value - 1).ToIconString(); + } + + return $"{placeName}{instanceString} {coordinateString}"; + } } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index a6c4e243c..816352d80 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -249,7 +249,6 @@ internal class DalamudInterface : IDisposable, IServiceType /// /// Opens the on the plugin installed. /// - /// The page of the installer to open. public void OpenPluginInstaller() { this.pluginWindow.OpenTo(this.configuration.PluginInstallerOpen); @@ -394,6 +393,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// /// Toggles the . /// + /// The page of the installer to open. public void TogglePluginInstallerWindowTo(PluginInstallerWindow.PluginInstallerOpenKind kind) => this.pluginWindow.ToggleTo(kind); /// diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 14f062e01..99af6102c 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -215,7 +215,7 @@ internal unsafe class UiDebug while (b > byte.MaxValue) b -= byte.MaxValue; while (b < byte.MinValue) b += byte.MaxValue; textNode->AlignmentFontType = (byte)b; - textNode->AtkResNode.DrawFlags |= 0x1; + textNode->AtkResNode.ViewFlags |= NodeViewFlags.IsDirty; } ImGui.Text($"Color: #{textNode->TextColor.R:X2}{textNode->TextColor.G:X2}{textNode->TextColor.B:X2}{textNode->TextColor.A:X2}"); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index a0cba737c..7dc8e2f3c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Linq; + using Dalamud.Game.Addon.Lifecycle; using Dalamud.Interface.Utility; using ImGuiNET; diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 600e423e1..49f083728 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.InteropServices; + using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; From 20d5e264030f7b663e45fd4c5e3f35748a88d0ea Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 31 Oct 2023 21:22:27 +0100 Subject: [PATCH 296/585] build: 9.0.0.9 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index d0ce1f669..a8a202ef0 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.8 + 9.0.0.9 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 403e94f9d0ba07399e23ae4bd40cc93f5b0e499f Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 31 Oct 2023 20:06:57 -0700 Subject: [PATCH 297/585] chore: Bump CS to 9f22a2a2 (#1517) - Revert of AtkResNode DrawFlags --- Dalamud/Game/Gui/Dtr/DtrBar.cs | 2 +- Dalamud/Interface/Internal/UiDebug.cs | 2 +- lib/FFXIVClientStructs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 2e5ecb9c5..993bb951f 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -413,7 +413,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar newTextNode->AtkResNode.NodeID = nodeId; newTextNode->AtkResNode.Type = NodeType.Text; newTextNode->AtkResNode.NodeFlags = NodeFlags.AnchorLeft | NodeFlags.AnchorTop | NodeFlags.Enabled | NodeFlags.RespondToMouse | NodeFlags.HasCollision | NodeFlags.EmitsEvents; - newTextNode->AtkResNode.ViewFlags = (NodeViewFlags)12; + newTextNode->AtkResNode.DrawFlags = 12; newTextNode->AtkResNode.SetWidth(22); newTextNode->AtkResNode.SetHeight(22); newTextNode->AtkResNode.SetPositionFloat(-200, 2); diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 99af6102c..14f062e01 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -215,7 +215,7 @@ internal unsafe class UiDebug while (b > byte.MaxValue) b -= byte.MaxValue; while (b < byte.MinValue) b += byte.MaxValue; textNode->AlignmentFontType = (byte)b; - textNode->AtkResNode.ViewFlags |= NodeViewFlags.IsDirty; + textNode->AtkResNode.DrawFlags |= 0x1; } ImGui.Text($"Color: #{textNode->TextColor.R:X2}{textNode->TextColor.G:X2}{textNode->TextColor.B:X2}{textNode->TextColor.A:X2}"); diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index de55cd12a..9f22a2a2c 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit de55cd12a61f07bfa3f2d1e54782c0dd57b52426 +Subproject commit 9f22a2a2cddca870aecab27df41f636cba14af8b From 7f87d2a9d25fed0310c12dcaac93096a32bb7fcd Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 2 Nov 2023 19:42:32 +0100 Subject: [PATCH 298/585] fix: don't unload plugin until update is downloaded, show proper errors --- .../PluginInstaller/PluginInstallerWindow.cs | 10 +- Dalamud/Plugin/Internal/PluginManager.cs | 330 ++++++++++-------- .../Internal/Types/PluginUpdateStatus.cs | 51 ++- 3 files changed, 235 insertions(+), 156 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index a66d132c7..0e6918123 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -728,10 +728,10 @@ internal class PluginInstallerWindow : Window, IDisposable } else { - this.updatedPlugins = task.Result.Where(res => res.WasUpdated).ToList(); + this.updatedPlugins = task.Result.Where(res => res.Status == PluginUpdateStatus.StatusKind.Success).ToList(); this.updatePluginCount = this.updatedPlugins.Count; - var errorPlugins = task.Result.Where(res => !res.WasUpdated).ToList(); + var errorPlugins = task.Result.Where(res => res.Status != PluginUpdateStatus.StatusKind.Success).ToList(); var errorPluginCount = errorPlugins.Count; if (errorPluginCount > 0) @@ -739,9 +739,9 @@ internal class PluginInstallerWindow : Window, IDisposable var errorMessage = this.updatePluginCount > 0 ? Locs.ErrorModal_UpdaterFailPartial(this.updatePluginCount, errorPluginCount) : Locs.ErrorModal_UpdaterFail(errorPluginCount); - + var hintInsert = errorPlugins - .Aggregate(string.Empty, (current, pluginUpdateStatus) => $"{current}* {pluginUpdateStatus.InternalName}\n") + .Aggregate(string.Empty, (current, pluginUpdateStatus) => $"{current}* {pluginUpdateStatus.InternalName} ({PluginUpdateStatus.LocalizeUpdateStatusKind(pluginUpdateStatus.Status)})\n") .TrimEnd(); errorMessage += Locs.ErrorModal_HintBlame(hintInsert); @@ -2250,7 +2250,7 @@ internal class PluginInstallerWindow : Window, IDisposable var update = this.updatedPlugins.FirstOrDefault(update => update.InternalName == plugin.Manifest.InternalName); if (update != default) { - if (update.WasUpdated) + if (update.Status == PluginUpdateStatus.StatusKind.Success) { thisWasUpdated = true; label += Locs.PluginTitleMod_Updated; diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 04698947e..c5fda414a 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -303,7 +303,7 @@ internal partial class PluginManager : IDisposable, IServiceType foreach (var metadata in updateMetadata) { - if (metadata.WasUpdated) + if (metadata.Status == PluginUpdateStatus.StatusKind.Success) { chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); } @@ -311,7 +311,7 @@ internal partial class PluginManager : IDisposable, IServiceType { chatGui.Print(new XivChatEntry { - Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version), + Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version, PluginUpdateStatus.LocalizeUpdateStatusKind(metadata.Status)), Type = XivChatType.Urgent, }); } @@ -782,147 +782,14 @@ internal partial class PluginManager : IDisposable, IServiceType /// The reason this plugin was loaded. /// WorkingPluginId this plugin should inherit. /// A representing the asynchronous operation. - public async Task InstallPluginAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason, Guid? inheritedWorkingPluginId = null) + public async Task InstallPluginAsync( + RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason, + Guid? inheritedWorkingPluginId = null) { - Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting})"); - - // If this plugin is in the default profile for whatever reason, delete the state - // If it was in multiple profiles and is still, the user uninstalled it and chose to keep it in there, - // or the user removed the plugin manually in which case we don't care - if (reason == PluginLoadReason.Installer) - { - try - { - // We don't need to apply, it doesn't matter - await this.profileManager.DefaultProfile.RemoveAsync(repoManifest.InternalName, false); - } - catch (ProfileOperationException) - { - // ignored - } - } - else - { - // If we are doing anything other than a fresh install, not having a workingPluginId is an error that must be fixed - Debug.Assert(inheritedWorkingPluginId != null, "inheritedWorkingPluginId != null"); - } - - // Ensure that we have a testing opt-in for this plugin if we are installing a testing version - if (useTesting && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != repoManifest.InternalName)) - { - // TODO: this isn't safe - this.configuration.PluginTestingOptIns.Add(new PluginTestingOptIn(repoManifest.InternalName)); - this.configuration.QueueSave(); - } - - var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; - var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion; - - var response = await this.happyHttpClient.SharedHttpClient.GetAsync(downloadUrl); - response.EnsureSuccessStatusCode(); - - var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty)); - - try - { - if (outputDir.Exists) - outputDir.Delete(true); - - outputDir.Create(); - } - catch - { - // ignored, since the plugin may be loaded already - } - - Log.Debug($"Extracting to {outputDir}"); - // This throws an error, even with overwrite=false - // ZipFile.ExtractToDirectory(tempZip.FullName, outputDir.FullName, false); - using (var archive = new ZipArchive(await response.Content.ReadAsStreamAsync())) - { - foreach (var zipFile in archive.Entries) - { - var outputFile = new FileInfo(Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName))); - - if (!outputFile.FullName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase)) - { - throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability"); - } - - if (outputFile.Directory == null) - { - throw new IOException("Output directory invalid."); - } - - if (zipFile.Name.IsNullOrEmpty()) - { - // Assuming Empty for Directory - Log.Verbose($"ZipFile name is null or empty, treating as a directory: {outputFile.Directory.FullName}"); - Directory.CreateDirectory(outputFile.Directory.FullName); - continue; - } - - // Ensure directory is created - Directory.CreateDirectory(outputFile.Directory.FullName); - - try - { - zipFile.ExtractToFile(outputFile.FullName, true); - } - catch (Exception ex) - { - if (outputFile.Extension.EndsWith("dll")) - { - throw new IOException($"Could not overwrite {zipFile.Name}: {ex.Message}"); - } - - Log.Error($"Could not overwrite {zipFile.Name}: {ex.Message}"); - } - } - } - - var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest); - var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); - - // We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use. - Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented)); - - // Reload as a local manifest, add some attributes, and save again. - var manifest = LocalPluginManifest.Load(manifestFile); - - if (manifest == null) - throw new Exception("Plugin had no valid manifest"); - - if (manifest.InternalName != repoManifest.InternalName) - { - Directory.Delete(outputDir.FullName, true); - throw new Exception( - $"Distributed internal name does not match repo internal name: {manifest.InternalName} - {repoManifest.InternalName}"); - } - - if (manifest.WorkingPluginId != Guid.Empty) - throw new Exception("Plugin shall not specify a WorkingPluginId"); - - manifest.WorkingPluginId = inheritedWorkingPluginId ?? Guid.NewGuid(); - - if (useTesting) - { - manifest.Testing = true; - } - - // Document the url the plugin was installed from - manifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty ? repoManifest.SourceRepo.PluginMasterUrl : SpecialPluginSource.MainRepo; - - manifest.Save(manifestFile, "installation"); - - Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})"); - - var plugin = await this.LoadPluginAsync(dllFile, manifest, reason); - - this.NotifyinstalledPluginsListChanged(); - return plugin; + var stream = await this.DownloadPluginAsync(repoManifest, useTesting); + return await this.InstallPluginInternalAsync(repoManifest, useTesting, reason, stream, inheritedWorkingPluginId); } - + /// /// Remove a plugin. /// @@ -1098,12 +965,25 @@ internal partial class PluginManager : IDisposable, IServiceType Version = (metadata.UseTesting ? metadata.UpdateManifest.TestingAssemblyVersion : metadata.UpdateManifest.AssemblyVersion)!, - WasUpdated = true, + Status = PluginUpdateStatus.StatusKind.Success, HasChangelog = !metadata.UpdateManifest.Changelog.IsNullOrWhitespace(), }; if (!dryRun) { + // Download the update before unloading + Stream updateStream; + try + { + updateStream = await this.DownloadPluginAsync(metadata.UpdateManifest, metadata.UseTesting); + } + catch (Exception ex) + { + Log.Error(ex, "Error during download (update)"); + updateStatus.Status = PluginUpdateStatus.StatusKind.FailedDownload; + return updateStatus; + } + // Unload if loaded if (plugin.State is PluginState.Loaded or PluginState.LoadError or PluginState.DependencyResolutionFailed) { @@ -1114,7 +994,7 @@ internal partial class PluginManager : IDisposable, IServiceType catch (Exception ex) { Log.Error(ex, "Error during unload (update)"); - updateStatus.WasUpdated = false; + updateStatus.Status = PluginUpdateStatus.StatusKind.FailedUnload; return updateStatus; } } @@ -1139,8 +1019,8 @@ internal partial class PluginManager : IDisposable, IServiceType } catch (Exception ex) { - Log.Error(ex, "Error during disable (update)"); - updateStatus.WasUpdated = false; + Log.Error(ex, "Error during remove from plugin list (update)"); + updateStatus.Status = PluginUpdateStatus.StatusKind.FailedUnload; return updateStatus; } @@ -1150,17 +1030,17 @@ internal partial class PluginManager : IDisposable, IServiceType try { - await this.InstallPluginAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update, workingPluginId); + await this.InstallPluginInternalAsync(metadata.UpdateManifest, metadata.UseTesting, PluginLoadReason.Update, updateStream, workingPluginId); } catch (Exception ex) { Log.Error(ex, "Error during install (update)"); - updateStatus.WasUpdated = false; + updateStatus.Status = PluginUpdateStatus.StatusKind.FailedLoad; return updateStatus; } } - if (notify && updateStatus.WasUpdated) + if (notify && updateStatus.Status == PluginUpdateStatus.StatusKind.Success) this.NotifyinstalledPluginsListChanged(); return updateStatus; @@ -1313,6 +1193,158 @@ internal partial class PluginManager : IDisposable, IServiceType /// The calling plugin, or null. public LocalPlugin? FindCallingPlugin() => this.FindCallingPlugin(new StackTrace()); + private async Task DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting) + { + var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; + var response = await this.happyHttpClient.SharedHttpClient.GetAsync(downloadUrl); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStreamAsync(); + } + + /// + /// Install a plugin from a repository and load it. + /// + /// The plugin definition. + /// If the testing version should be used. + /// The reason this plugin was loaded. + /// WorkingPluginId this plugin should inherit. + /// A representing the asynchronous operation. + private async Task InstallPluginInternalAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason, Stream zipStream, Guid? inheritedWorkingPluginId = null) + { + var version = useTesting ? repoManifest.TestingAssemblyVersion : repoManifest.AssemblyVersion; + Log.Debug($"Installing plugin {repoManifest.Name} (testing={useTesting}, version={version}, reason={reason})"); + + // If this plugin is in the default profile for whatever reason, delete the state + // If it was in multiple profiles and is still, the user uninstalled it and chose to keep it in there, + // or the user removed the plugin manually in which case we don't care + if (reason == PluginLoadReason.Installer) + { + try + { + // We don't need to apply, it doesn't matter + await this.profileManager.DefaultProfile.RemoveAsync(repoManifest.InternalName, false); + } + catch (ProfileOperationException) + { + // ignored + } + } + else + { + // If we are doing anything other than a fresh install, not having a workingPluginId is an error that must be fixed + Debug.Assert(inheritedWorkingPluginId != null, "inheritedWorkingPluginId != null"); + } + + // Ensure that we have a testing opt-in for this plugin if we are installing a testing version + if (useTesting && this.configuration.PluginTestingOptIns!.All(x => x.InternalName != repoManifest.InternalName)) + { + // TODO: this isn't safe + this.configuration.PluginTestingOptIns.Add(new PluginTestingOptIn(repoManifest.InternalName)); + this.configuration.QueueSave(); + } + + var outputDir = new DirectoryInfo(Path.Combine(this.pluginDirectory.FullName, repoManifest.InternalName, version?.ToString() ?? string.Empty)); + + try + { + if (outputDir.Exists) + outputDir.Delete(true); + + outputDir.Create(); + } + catch + { + // ignored, since the plugin may be loaded already + } + + Log.Debug($"Extracting to {outputDir}"); + + using (var archive = new ZipArchive(zipStream)) + { + foreach (var zipFile in archive.Entries) + { + var outputFile = new FileInfo(Path.GetFullPath(Path.Combine(outputDir.FullName, zipFile.FullName))); + + if (!outputFile.FullName.StartsWith(outputDir.FullName, StringComparison.OrdinalIgnoreCase)) + { + throw new IOException("Trying to extract file outside of destination directory. See this link for more info: https://snyk.io/research/zip-slip-vulnerability"); + } + + if (outputFile.Directory == null) + { + throw new IOException("Output directory invalid."); + } + + if (zipFile.Name.IsNullOrEmpty()) + { + // Assuming Empty for Directory + Log.Verbose($"ZipFile name is null or empty, treating as a directory: {outputFile.Directory.FullName}"); + Directory.CreateDirectory(outputFile.Directory.FullName); + continue; + } + + // Ensure directory is created + Directory.CreateDirectory(outputFile.Directory.FullName); + + try + { + zipFile.ExtractToFile(outputFile.FullName, true); + } + catch (Exception ex) + { + if (outputFile.Extension.EndsWith("dll")) + { + throw new IOException($"Could not overwrite {zipFile.Name}: {ex.Message}"); + } + + Log.Error($"Could not overwrite {zipFile.Name}: {ex.Message}"); + } + } + } + + var dllFile = LocalPluginManifest.GetPluginFile(outputDir, repoManifest); + var manifestFile = LocalPluginManifest.GetManifestFile(dllFile); + + // We need to save the repoManifest due to how the repo fills in some fields that authors are not expected to use. + Util.WriteAllTextSafe(manifestFile.FullName, JsonConvert.SerializeObject(repoManifest, Formatting.Indented)); + + // Reload as a local manifest, add some attributes, and save again. + var manifest = LocalPluginManifest.Load(manifestFile); + + if (manifest == null) + throw new Exception("Plugin had no valid manifest"); + + if (manifest.InternalName != repoManifest.InternalName) + { + Directory.Delete(outputDir.FullName, true); + throw new Exception( + $"Distributed internal name does not match repo internal name: {manifest.InternalName} - {repoManifest.InternalName}"); + } + + if (manifest.WorkingPluginId != Guid.Empty) + throw new Exception("Plugin shall not specify a WorkingPluginId"); + + manifest.WorkingPluginId = inheritedWorkingPluginId ?? Guid.NewGuid(); + + if (useTesting) + { + manifest.Testing = true; + } + + // Document the url the plugin was installed from + manifest.InstalledFromUrl = repoManifest.SourceRepo.IsThirdParty ? repoManifest.SourceRepo.PluginMasterUrl : SpecialPluginSource.MainRepo; + + manifest.Save(manifestFile, "installation"); + + Log.Information($"Installed plugin {manifest.Name} (testing={useTesting})"); + + var plugin = await this.LoadPluginAsync(dllFile, manifest, reason); + + this.NotifyinstalledPluginsListChanged(); + return plugin; + } + /// /// Load a plugin. /// @@ -1543,7 +1575,7 @@ internal partial class PluginManager : IDisposable, IServiceType { public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version); - public static string DalamudPluginUpdateFailed(string name, Version version) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed.").Format(name, version); + public static string DalamudPluginUpdateFailed(string name, Version version, string why) => Loc.Localize("DalamudPluginUpdateFailed", " 》 {0} update to v{1} failed ({2}).").Format(name, version, why); } } diff --git a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs index 24ca5fe0f..391107691 100644 --- a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs +++ b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs @@ -1,4 +1,5 @@ using System; +using CheapLoc; namespace Dalamud.Plugin.Internal.Types; @@ -7,6 +8,37 @@ namespace Dalamud.Plugin.Internal.Types; /// internal class PluginUpdateStatus { + /// + /// Enum containing possible statuses of a plugin update. + /// + public enum StatusKind + { + /// + /// The update is pending. + /// + Pending, + + /// + /// The update failed to download. + /// + FailedDownload, + + /// + /// The outdated plugin did not unload correctly. + /// + FailedUnload, + + /// + /// The updated plugin did not load correctly. + /// + FailedLoad, + + /// + /// The update succeeded. + /// + Success, + } + /// /// Gets the plugin internal name. /// @@ -23,12 +55,27 @@ internal class PluginUpdateStatus public Version Version { get; init; } = null!; /// - /// Gets or sets a value indicating whether the plugin was updated. + /// Gets or sets a value indicating the status of the update. /// - public bool WasUpdated { get; set; } + public StatusKind Status { get; set; } = StatusKind.Pending; /// /// Gets a value indicating whether the plugin has a changelog if it was updated. /// public bool HasChangelog { get; init; } + + /// + /// Get a localized version of the update status. + /// + /// Status to localize. + /// Localized text. + public static string LocalizeUpdateStatusKind(StatusKind status) => status switch + { + StatusKind.Pending => Loc.Localize("InstallerUpdateStatusPending", "Pending"), + StatusKind.FailedDownload => Loc.Localize("InstallerUpdateStatusFailedDownload", "Download failed"), + StatusKind.FailedUnload => Loc.Localize("InstallerUpdateStatusFailedUnload", "Unload failed"), + StatusKind.FailedLoad => Loc.Localize("InstallerUpdateStatusFailedLoad", "Load failed"), + StatusKind.Success => Loc.Localize("InstallerUpdateStatusSuccess", "Success"), + _ => "???", + }; } From b73ac2f3f757181ebeea29c3e73a3c1f0f8fbd63 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 2 Nov 2023 19:52:23 +0100 Subject: [PATCH 299/585] feat: show why update failed when updating a single plugin --- .../PluginInstaller/PluginInstallerWindow.cs | 13 ++++++++++--- Dalamud/Plugin/Internal/PluginManager.cs | 3 ++- Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs | 1 - 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0e6918123..687526c9a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2779,8 +2779,15 @@ internal class PluginInstallerWindow : Window, IDisposable // There is no need to set as Complete for an individual plugin installation this.installStatus = OperationStatus.Idle; - var errorMessage = Locs.ErrorModal_SingleUpdateFail(update.UpdateManifest.Name); - return this.DisplayErrorContinuation(task, errorMessage); + if (task.IsCompletedSuccessfully && + task.Result.Status != PluginUpdateStatus.StatusKind.Success) + { + this.ShowErrorModal( + Locs.ErrorModal_SingleUpdateFail(update.UpdateManifest.Name, PluginUpdateStatus.LocalizeUpdateStatusKind(task.Result.Status))); + return false; + } + + return this.DisplayErrorContinuation(task, Locs.ErrorModal_SingleUpdateFail(update.UpdateManifest.Name, "Exception")); }); } @@ -3623,7 +3630,7 @@ internal class PluginInstallerWindow : Window, IDisposable public static string ErrorModal_InstallFail(string name) => Loc.Localize("InstallerInstallFail", "Failed to install plugin {0}.\n{1}").Format(name, ErrorModal_InstallContactAuthor); - public static string ErrorModal_SingleUpdateFail(string name) => Loc.Localize("InstallerSingleUpdateFail", "Failed to update plugin {0}.\n{1}").Format(name, ErrorModal_InstallContactAuthor); + public static string ErrorModal_SingleUpdateFail(string name, string why) => Loc.Localize("InstallerSingleUpdateFail", "Failed to update plugin {0} ({1}).\n{2}").Format(name, why, ErrorModal_InstallContactAuthor); public static string ErrorModal_DeleteConfigFail(string name) => Loc.Localize("InstallerDeleteConfigFail", "Failed to reset the plugin {0}.\n\nThe plugin may not support this action. You can try deleting the configuration manually while the game is shut down - please see the FAQ.").Format(name); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index c5fda414a..ff6b045be 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1201,13 +1201,14 @@ internal partial class PluginManager : IDisposable, IServiceType return await response.Content.ReadAsStreamAsync(); } - + /// /// Install a plugin from a repository and load it. /// /// The plugin definition. /// If the testing version should be used. /// The reason this plugin was loaded. + /// Stream of the ZIP archive containing the plugin that is about to be installed. /// WorkingPluginId this plugin should inherit. /// A representing the asynchronous operation. private async Task InstallPluginInternalAsync(RemotePluginManifest repoManifest, bool useTesting, PluginLoadReason reason, Stream zipStream, Guid? inheritedWorkingPluginId = null) diff --git a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs index 391107691..1f20ad960 100644 --- a/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs +++ b/Dalamud/Plugin/Internal/Types/PluginUpdateStatus.cs @@ -1,4 +1,3 @@ -using System; using CheapLoc; namespace Dalamud.Plugin.Internal.Types; From f10a5975663c2cb9f372a3d80da5000800e9ff2a Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Mon, 6 Nov 2023 23:10:55 +0100 Subject: [PATCH 300/585] feat: add title bar buttons API to Window, make clickthrough/pinning window a title bar button --- .../Internal/DalamudConfiguration.cs | 6 + .../Windows/Settings/Tabs/SettingsTabLook.cs | 6 + Dalamud/Interface/Windowing/Window.cs | 324 +++++++++++++----- 3 files changed, 252 insertions(+), 84 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index dbe95ea23..125267aaa 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -231,6 +231,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// public bool EnablePluginUISoundEffects { get; set; } + /// + /// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown + /// on plugin title bars when using the Window System. + /// + public bool EnablePluginUiAdditionalOptions { get; set; } = true; + /// /// Gets or sets a value indicating whether viewports should always be disabled. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 7a6f894c1..bb4acd6a5 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -109,6 +109,12 @@ public class SettingsTabLook : SettingsTab Loc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."), c => c.EnablePluginUISoundEffects, (v, c) => c.EnablePluginUISoundEffects = v), + + new SettingsEntry( + Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptions", "Add a button to the title bar of plugin windows to open additional options"), + Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptionsHint", "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."), + c => c.EnablePluginUiAdditionalOptions, + (v, c) => c.EnablePluginUiAdditionalOptions = v), new SettingsEntry( Loc.Localize("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"), diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 49f083728..9e06a1d75 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Numerics; using System.Runtime.InteropServices; @@ -6,10 +8,12 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; +using PInvoke; namespace Dalamud.Interface.Windowing; @@ -21,12 +25,11 @@ public abstract class Window private static readonly ModuleLog Log = new("WindowSystem"); private static bool wasEscPressedLastFrame = false; - + private bool internalLastIsOpen = false; private bool internalIsOpen = false; private bool internalIsPinned = false; private bool internalIsClickthrough = false; - private DateTimeOffset internalLastDisableClick = DateTimeOffset.MinValue; private bool didPushInternalAlpha = false; private float? internalAlpha = null; private bool nextFrameBringToFront = false; @@ -149,6 +152,15 @@ public abstract class Window /// public bool AllowClickthrough { get; set; } = true; + /// + /// Gets or sets a list of available title bar buttons. + /// + /// If or are set to true, and this features is not + /// disabled globally by the user, an internal title bar button to manage these is added when drawing, but it will + /// not appear in this collection. If you wish to remove this button, set both of these values to false. + /// + public List TitleBarButtons { get; set; } = new(); + /// /// Gets or sets a value indicating whether or not this window will stay open. /// @@ -157,6 +169,8 @@ public abstract class Window get => this.internalIsOpen; set => this.internalIsOpen = value; } + + private bool CanShowCloseButton => this.ShowCloseButton && !this.internalIsClickthrough; /// /// Toggle window is open state. @@ -252,7 +266,7 @@ public abstract class Window public virtual void Update() { } - + /// /// Draw the window via ImGui. /// @@ -317,13 +331,13 @@ public abstract class Window var flags = this.Flags; - if (this.internalIsPinned) + if (this.internalIsPinned || this.internalIsClickthrough) flags |= ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize; if (this.internalIsClickthrough) flags |= ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs; - if (this.ShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags)) + if (this.CanShowCloseButton ? ImGui.Begin(this.WindowName, ref this.internalIsOpen, flags) : ImGui.Begin(this.WindowName, flags)) { // Draw the actual window contents try @@ -334,79 +348,87 @@ public abstract class Window { Log.Error(ex, $"Error during Draw(): {this.WindowName}"); } + } - if (this.AllowPinning || this.AllowClickthrough) + var additionsPopupName = "WindowSystemContextActions"; + var flagsApplicableForTitleBarIcons = !flags.HasFlag(ImGuiWindowFlags.NoDecoration) && + !flags.HasFlag(ImGuiWindowFlags.NoTitleBar); + var showAdditions = (this.AllowPinning || this.AllowClickthrough) && + (configuration?.EnablePluginUiAdditionalOptions ?? true) && + flagsApplicableForTitleBarIcons; + if (showAdditions) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); + + if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove)) { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); + if (this.internalIsClickthrough) + ImGui.BeginDisabled(); - var popupName = "WindowSystemContextActions"; - if (ImGui.BeginPopup(popupName)) + if (this.AllowPinning) { - if (this.internalIsClickthrough) - ImGui.BeginDisabled(); - - if (this.AllowPinning) - ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref this.internalIsPinned); - - if (this.internalIsClickthrough) - ImGui.EndDisabled(); - - if (this.AllowClickthrough) - { - if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"), - ref this.internalIsClickthrough)) - { - if (this.internalIsClickthrough) - this.internalIsPinned = true; - } - } - - var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f; - if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f, - 100f)) - { - this.internalAlpha = alpha / 100f; - } - - ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset"))) - { - this.internalAlpha = null; - } - - ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("WindowSystemContextActionDoubleClick", "Double click the title bar to disable clickthrough.")); - ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("WindowSystemContextActionDisclaimer", "These options may not work for all plugins at the moment.")); - - ImGui.EndPopup(); + var showAsPinned = this.internalIsPinned || this.internalIsClickthrough; + if (ImGui.Checkbox(Loc.Localize("WindowSystemContextActionPin", "Pin Window"), ref showAsPinned)) + this.internalIsPinned = showAsPinned; } - ImGui.PopStyleVar(); - - var titleBarRect = Vector4.Zero; - unsafe + if (this.internalIsClickthrough) + ImGui.EndDisabled(); + + if (this.AllowClickthrough) + ImGui.Checkbox(Loc.Localize("WindowSystemContextActionClickthrough", "Make clickthrough"), ref this.internalIsClickthrough); + + var alpha = (this.internalAlpha ?? ImGui.GetStyle().Alpha) * 100f; + if (ImGui.SliderFloat(Loc.Localize("WindowSystemContextActionAlpha", "Opacity"), ref alpha, 20f, + 100f)) { - var window = ImGuiNativeAdditions.igGetCurrentWindow(); - ImGuiNativeAdditions.ImGuiWindow_TitleBarRect(&titleBarRect, window); + this.internalAlpha = alpha / 100f; } - if (ImGui.IsMouseHoveringRect(new Vector2(titleBarRect.X, titleBarRect.Y), new Vector2(titleBarRect.Z, titleBarRect.W), false)) + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("WindowSystemContextActionReset", "Reset"))) { - if (ImGui.IsWindowHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Right)) - { - ImGui.OpenPopup(popupName); - } - - if (ImGui.IsMouseDown(ImGuiMouseButton.Left)) - { - if (DateTime.Now - this.internalLastDisableClick < TimeSpan.FromMilliseconds(100)) - { - this.internalIsPinned = false; - this.internalIsClickthrough = false; - } - - this.internalLastDisableClick = DateTime.Now; - } + this.internalAlpha = null; } + + ImGui.TextColored(ImGuiColors.DalamudGrey, + Loc.Localize("WindowSystemContextActionClickthroughDisclaimer", + "Open this menu again to disable clickthrough.")); + ImGui.TextColored(ImGuiColors.DalamudGrey, + Loc.Localize("WindowSystemContextActionDisclaimer", + "These options may not work for all plugins at the moment.")); + + ImGui.EndPopup(); + } + + ImGui.PopStyleVar(); + } + + var titleBarRect = Vector4.Zero; + unsafe + { + var window = ImGuiNativeAdditions.igGetCurrentWindow(); + ImGuiNativeAdditions.ImGuiWindow_TitleBarRect(&titleBarRect, window); + + var additionsButton = new TitleBarButton + { + Icon = FontAwesomeIcon.Bars, + IconOffset = new Vector2(2.5f, 1), + Click = _ => + { + this.internalIsClickthrough = false; + ImGui.OpenPopup(additionsPopupName); + }, + Priority = int.MinValue, + AvailableClickthrough = true, + }; + + if (flagsApplicableForTitleBarIcons) + { + this.DrawTitleBarButtons(window, flags, titleBarRect, + showAdditions + ? this.TitleBarButtons.Append(additionsButton) + : this.TitleBarButtons); } } @@ -440,21 +462,6 @@ public abstract class Window ImGui.PopID(); } - // private void CheckState() - // { - // if (this.internalLastIsOpen != this.internalIsOpen) - // { - // if (this.internalIsOpen) - // { - // this.OnOpen(); - // } - // else - // { - // this.OnClose(); - // } - // } - // } - private void ApplyConditionals() { if (this.Position.HasValue) @@ -494,6 +501,106 @@ public abstract class Window } } + private unsafe void DrawTitleBarButtons(void* window, ImGuiWindowFlags flags, Vector4 titleBarRect, IEnumerable buttons) + { + ImGui.PushClipRect(ImGui.GetWindowPos(), ImGui.GetWindowPos() + ImGui.GetWindowSize(), false); + + var style = ImGui.GetStyle(); + var fontSize = ImGui.GetFontSize(); + var drawList = ImGui.GetWindowDrawList(); + + var padR = 0f; + var buttonSize = ImGui.GetFontSize(); + + var numNativeButtons = 0; + if (this.CanShowCloseButton) + numNativeButtons++; + + if (!flags.HasFlag(ImGuiWindowFlags.NoCollapse) && style.WindowMenuButtonPosition == ImGuiDir.Right) + numNativeButtons++; + + // If there are no native buttons, pad from the right to make some space + if (numNativeButtons == 0) + padR += style.FramePadding.X; + + // Pad to the left, to get out of the way of the native buttons + padR += numNativeButtons * (buttonSize + style.ItemInnerSpacing.X); + + Vector2 GetCenter(Vector4 rect) => new((rect.X + rect.Z) * 0.5f, (rect.Y + rect.W) * 0.5f); + + var numButtons = 0; + bool DrawButton(TitleBarButton button, Vector2 pos) + { + var id = ImGui.GetID($"###CustomTbButton{numButtons}"); + numButtons++; + + var min = pos; + var max = pos + new Vector2(fontSize, fontSize); + Vector4 bb = new(min.X, min.Y, max.X, max.Y); + var isClipped = !ImGuiNativeAdditions.igItemAdd(bb, id, null, 0); + bool hovered, held; + var pressed = false; + + if (this.internalIsClickthrough) + { + hovered = false; + held = false; + + // ButtonBehavior does not function if the window is clickthrough, so we have to do it ourselves + if (ImGui.IsMouseHoveringRect(min, max)) + { + hovered = true; + + // We can't use ImGui native functions here, because they don't work with clickthrough + if ((User32.GetKeyState((int)VirtualKey.LBUTTON) & 0x8000) != 0) + { + held = true; + pressed = true; + } + } + } + else + { + pressed = ImGuiNativeAdditions.igButtonBehavior(bb, id, &hovered, &held, ImGuiButtonFlags.None); + } + + if (isClipped) + return pressed; + + // Render + var bgCol = ImGui.GetColorU32((held && hovered) ? ImGuiCol.ButtonActive : hovered ? ImGuiCol.ButtonHovered : ImGuiCol.Button); + var textCol = ImGui.GetColorU32(ImGuiCol.Text); + if (hovered || held) + drawList.AddCircleFilled(GetCenter(bb) + new Vector2(0.0f, -0.5f), (fontSize * 0.5f) + 1.0f, bgCol); + + var offset = button.IconOffset * ImGuiHelpers.GlobalScale; + drawList.AddText(InterfaceManager.IconFont, (float)(fontSize * 0.8), new Vector2(bb.X + offset.X, bb.Y + offset.Y), textCol, button.Icon.ToIconString()); + + if (hovered) + button.ShowTooltip?.Invoke(); + + // Switch to moving the window after mouse is moved beyond the initial drag threshold + if (ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left) && !this.internalIsClickthrough) + ImGuiNativeAdditions.igStartMouseMovingWindow(window); + + return pressed; + } + + foreach (var button in buttons.OrderBy(x => x.Priority)) + { + if (this.internalIsClickthrough && !button.AvailableClickthrough) + return; + + Vector2 position = new(titleBarRect.Z - padR - buttonSize, titleBarRect.Y + style.FramePadding.Y); + padR += buttonSize + style.ItemInnerSpacing.X; + + if (DrawButton(button, position)) + button.Click?.Invoke(ImGuiMouseButton.Left); + } + + ImGui.PopClipRect(); + } + /// /// Structure detailing the size constraints of a window. /// @@ -509,14 +616,63 @@ public abstract class Window /// public Vector2 MaximumSize { get; set; } } + + /// + /// Structure describing a title bar button. + /// + public struct TitleBarButton + { + /// + /// Gets or sets the icon of the button. + /// + public FontAwesomeIcon Icon { get; set; } + + /// + /// Gets or sets a vector by which the position of the icon within the button shall be offset. + /// Automatically scaled by the global font scale for you. + /// + public Vector2 IconOffset { get; set; } + + /// + /// Gets or sets an action that is called when a tooltip shall be drawn. + /// May be null if no tooltip shall be drawn. + /// + public Action? ShowTooltip { get; set; } + + /// + /// Gets or sets an action that is called when the button is clicked. + /// + public Action Click { get; set; } + + /// + /// Gets or sets the priority the button shall be shown in. + /// Lower = closer to ImGui default buttons. + /// + public int Priority { get; set; } + + /// + /// Gets or sets a value indicating whether or not the button shall be clickable + /// when the respective window is set to clickthrough. + /// + public bool AvailableClickthrough { get; set; } + } [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "imports")] private static unsafe class ImGuiNativeAdditions { [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] - public static extern unsafe void* igGetCurrentWindow(); + public static extern bool igItemAdd(Vector4 bb, uint id, Vector4* navBb, uint flags); + + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + public static extern bool igButtonBehavior(Vector4 bb, uint id, bool* outHovered, bool* outHeld, ImGuiButtonFlags flags); + + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + public static extern void* igGetCurrentWindow(); + + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] + public static extern void igStartMouseMovingWindow(void* window); [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] - public static extern unsafe void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window); + public static extern void ImGuiWindow_TitleBarRect(Vector4* pOut, void* window); } } From c03d6ff0483e08446455f9b2d226157dc19e3323 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 7 Nov 2023 19:27:45 +0100 Subject: [PATCH 301/585] fix: only allow pinning/clickthrough if the window is within the main viewport --- Dalamud/Interface/Utility/ImGuiHelpers.cs | 7 ++++++ Dalamud/Interface/Windowing/Window.cs | 29 ++++++++++++++++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 010178b26..d11e1ce1c 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -25,6 +25,13 @@ public static class ImGuiHelpers /// public static float GlobalScale { get; private set; } + /// + /// Check if the current ImGui window is on the main viewport. + /// Only valid within a window. + /// + /// Whether the window is on the main viewport. + public static bool CheckIsWindowOnMainViewport() => MainViewport.ID == ImGui.GetWindowViewport().ID; + /// /// Gets a that is pre-scaled with the multiplier. /// diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 9e06a1d75..144eb35fc 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -362,6 +362,11 @@ public abstract class Window if (ImGui.BeginPopup(additionsPopupName, ImGuiWindowFlags.NoMove)) { + var isAvailable = ImGuiHelpers.CheckIsWindowOnMainViewport(); + + if (!isAvailable) + ImGui.BeginDisabled(); + if (this.internalIsClickthrough) ImGui.BeginDisabled(); @@ -391,13 +396,25 @@ public abstract class Window this.internalAlpha = null; } - ImGui.TextColored(ImGuiColors.DalamudGrey, - Loc.Localize("WindowSystemContextActionClickthroughDisclaimer", - "Open this menu again to disable clickthrough.")); - ImGui.TextColored(ImGuiColors.DalamudGrey, - Loc.Localize("WindowSystemContextActionDisclaimer", - "These options may not work for all plugins at the moment.")); + if (isAvailable) + { + ImGui.TextColored(ImGuiColors.DalamudGrey, + Loc.Localize("WindowSystemContextActionClickthroughDisclaimer", + "Open this menu again to disable clickthrough.")); + ImGui.TextColored(ImGuiColors.DalamudGrey, + Loc.Localize("WindowSystemContextActionDisclaimer", + "These options may not work for all plugins at the moment.")); + } + else + { + ImGui.TextColored(ImGuiColors.DalamudGrey, + Loc.Localize("WindowSystemContextActionViewportDisclaimer", + "These features are only available if this window is inside the game window.")); + } + if (!isAvailable) + ImGui.EndDisabled(); + ImGui.EndPopup(); } From 4e1e9a0fcec1e02f1a323f29d7a0e57acb0724ca Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Fri, 10 Nov 2023 21:03:28 +0100 Subject: [PATCH 302/585] fix: fly text atk arrays + offsets (#1523) --- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 3da8dc2a9..a7839afa8 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -86,9 +86,9 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui public unsafe void AddFlyText(FlyTextKind kind, uint actorIndex, uint val1, uint val2, SeString text1, SeString text2, uint color, uint icon, uint damageTypeIcon) { // Known valid flytext region within the atk arrays - var numIndex = 28; - var strIndex = 25; - var numOffset = 147u; + var numIndex = 30; + var strIndex = 27; + var numOffset = 161u; var strOffset = 28u; // Get the UI module and flytext addon pointers @@ -132,7 +132,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui 1, (IntPtr)numArray, numOffset, - 9, + 10, (IntPtr)strArray, strOffset, 2, From 9a8f370975cb533a9d4e70fac03c8ab718150910 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 10 Nov 2023 12:03:54 -0800 Subject: [PATCH 303/585] Update AddonEventType (#1516) --- Dalamud/Game/Addon/Events/AddonEventType.cs | 30 +++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventType.cs b/Dalamud/Game/Addon/Events/AddonEventType.cs index 2c6c96334..100168e22 100644 --- a/Dalamud/Game/Addon/Events/AddonEventType.cs +++ b/Dalamud/Game/Addon/Events/AddonEventType.cs @@ -80,6 +80,18 @@ public enum AddonEventType : byte /// ListItemToggle = 35, + /// + /// Drag Drop Begin. + /// Sent on MouseDown over a draggable icon (will NOT send for a locked icon). + /// + DragDropBegin = 47, + + /// + /// Drag Drop Insert. + /// Sent when dropping an icon into a hotbar/inventory slot or similar. + /// + DragDropInsert = 50, + /// /// Drag Drop Roll Over. /// @@ -91,13 +103,27 @@ public enum AddonEventType : byte DragDropRollOut = 53, /// - /// Drag Drop Unknown. + /// Drag Drop Discard. + /// Sent when dropping an icon into empty screenspace, eg to remove an action from a hotBar. /// - DragDropUnk54 = 54, + DragDropDiscard = 54, /// /// Drag Drop Unknown. /// + [Obsolete("Use DragDropDiscard")] + DragDropUnk54 = 54, + + /// + /// Drag Drop Cancel. + /// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon. + /// + DragDropCancel = 55, + + /// + /// Drag Drop Unknown. + /// + [Obsolete("Use DragDropCancel")] DragDropUnk55 = 55, /// From 744e26b500eb1873f01c03f4620dfb3db5056aeb Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 10 Nov 2023 21:07:19 +0100 Subject: [PATCH 304/585] Update ClientStructs (#1518) 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 9f22a2a2c..090e0c244 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9f22a2a2cddca870aecab27df41f636cba14af8b +Subproject commit 090e0c244df668454616026188c1363e5d25a1bc From 13bdbcb23659f9878ff75d69d72e0b17d1f06f2a Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 11 Nov 2023 13:04:14 +0100 Subject: [PATCH 305/585] build: 9.0.0.10 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index a8a202ef0..7fa94e540 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.9 + 9.0.0.10 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 3939731cb0f1a7fac74ebd4fb9c6717f6ff11665 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 11 Nov 2023 14:19:24 +0100 Subject: [PATCH 306/585] chore: make window additional options opt-in for now --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 125267aaa..894ed6e9c 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -235,7 +235,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown /// on plugin title bars when using the Window System. /// - public bool EnablePluginUiAdditionalOptions { get; set; } = true; + public bool EnablePluginUiAdditionalOptions { get; set; } = false; /// /// Gets or sets a value indicating whether viewports should always be disabled. From 245ecd4e5de295bd2f709a7a71edd009f554d372 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 11 Nov 2023 14:19:51 +0100 Subject: [PATCH 307/585] build: 9.0.0.11 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 7fa94e540..33067eb97 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.10 + 9.0.0.11 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From cb968d24c1c44ab835e9262e248f5014f2834136 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 14 Nov 2023 18:46:40 +0100 Subject: [PATCH 308/585] fix: TitleBarButton => class --- 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 144eb35fc..59cb4d570 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -637,7 +637,7 @@ public abstract class Window /// /// Structure describing a title bar button. /// - public struct TitleBarButton + public class TitleBarButton { /// /// Gets or sets the icon of the button. From 5daef60422178daf96742b8c164dfaa6435fca0e Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 14 Nov 2023 18:48:10 +0100 Subject: [PATCH 309/585] chore: rename config key for title bar options, move to experimental tab --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 1 + .../Windows/Settings/Tabs/SettingsTabExperimental.cs | 8 ++++++++ .../Internal/Windows/Settings/Tabs/SettingsTabLook.cs | 6 ------ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 894ed6e9c..1274744c6 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -235,6 +235,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown /// on plugin title bars when using the Window System. /// + [JsonProperty("EnablePluginUiAdditionalOptionsExperimental")] public bool EnablePluginUiAdditionalOptions { get; set; } = false; /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index bd90d8509..c706a42c1 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -28,6 +28,14 @@ public class SettingsTabExperimental : SettingsTab new HintSettingsEntry( Loc.Localize("DalamudSettingsPluginTestWarning", "Testing plugins may contain bugs or crash your game. Please only enable this if you are aware of the risks."), ImGuiColors.DalamudRed), + + new GapSettingsEntry(5), + + new SettingsEntry( + Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptions", "Add a button to the title bar of plugin windows to open additional options"), + Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptionsHint", "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."), + c => c.EnablePluginUiAdditionalOptions, + (v, c) => c.EnablePluginUiAdditionalOptions = v), new GapSettingsEntry(5), diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index bb4acd6a5..7a6f894c1 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -109,12 +109,6 @@ public class SettingsTabLook : SettingsTab Loc.Localize("DalamudSettingEnablePluginUISoundEffectsHint", "This will allow you to enable or disable sound effects generated by plugin user interfaces.\nThis is affected by your in-game `System Sounds` volume settings."), c => c.EnablePluginUISoundEffects, (v, c) => c.EnablePluginUISoundEffects = v), - - new SettingsEntry( - Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptions", "Add a button to the title bar of plugin windows to open additional options"), - Loc.Localize("DalamudSettingEnablePluginUIAdditionalOptionsHint", "This will allow you to pin certain plugin windows, make them clickthrough or adjust their opacity.\nThis may not be supported by all of your plugins. Contact the plugin author if you want them to support this feature."), - c => c.EnablePluginUiAdditionalOptions, - (v, c) => c.EnablePluginUiAdditionalOptions = v), new SettingsEntry( Loc.Localize("DalamudSettingToggleGamepadNavigation", "Control plugins via gamepad"), From 140d0c8a2fe1b669c3e1ab7908f87e25b7e3d3c8 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 14 Nov 2023 18:48:41 +0100 Subject: [PATCH 310/585] build: 9.0.0.12 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 33067eb97..5c971489a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.11 + 9.0.0.12 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 8b0c20057f4a96feb23e7bcbf718faf10ebd6594 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 14 Nov 2023 19:45:14 +0100 Subject: [PATCH 311/585] ci: re-enable auto integration --- .github/workflows/rollup.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml index be7d6b6d3..9d77fe244 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: @@ -11,7 +11,8 @@ jobs: strategy: matrix: branches: - - v9 + - net8 + - new_im_hooks defaults: run: From 48e8462550141db9b1a153cab9548e60238500c7 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 14 Nov 2023 19:46:42 +0100 Subject: [PATCH 312/585] ci: disable integration for unmergeable branch --- .github/workflows/rollup.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rollup.yml b/.github/workflows/rollup.yml index 9d77fe244..44116e7b2 100644 --- a/.github/workflows/rollup.yml +++ b/.github/workflows/rollup.yml @@ -12,7 +12,7 @@ jobs: matrix: branches: - net8 - - new_im_hooks + #- new_im_hooks # Unmergeable defaults: run: From 93f08a4cb56f0709c392940b740b7dcd4eb8e375 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 18 Nov 2023 12:17:18 -0800 Subject: [PATCH 313/585] fix: Use SetValue for FlyText string arrays (#1532) - Might fix a bug causing crashes with certain FlyText use cases, maybe. - Also allows building Dalamud under .NET 8 local envs. --- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 42 +++++++++++--------------- global.json | 4 +-- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index a7839afa8..36056883e 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -1,4 +1,3 @@ -using System; using System.Runtime.InteropServices; using System.Threading.Tasks; @@ -113,32 +112,26 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui numArray->IntArray[numOffset + 2] = unchecked((int)val1); numArray->IntArray[numOffset + 3] = unchecked((int)val2); numArray->IntArray[numOffset + 4] = unchecked((int)damageTypeIcon); // Icons for damage type - numArray->IntArray[numOffset + 5] = 5; // Unknown + numArray->IntArray[numOffset + 5] = 5; // Unknown numArray->IntArray[numOffset + 6] = unchecked((int)color); numArray->IntArray[numOffset + 7] = unchecked((int)icon); numArray->IntArray[numOffset + 8] = 0; // Unknown numArray->IntArray[numOffset + 9] = 0; // Unknown, has something to do with yOffset - fixed (byte* pText1 = text1.Encode()) - { - fixed (byte* pText2 = text2.Encode()) - { - strArray->StringArray[strOffset + 0] = pText1; - strArray->StringArray[strOffset + 1] = pText2; + strArray->SetValue((int)strOffset + 0, text1.Encode(), false, true, false); + strArray->SetValue((int)strOffset + 1, text2.Encode(), false, true, false); - this.addFlyTextNative( - flytext, - actorIndex, - 1, - (IntPtr)numArray, - numOffset, - 10, - (IntPtr)strArray, - strOffset, - 2, - 0); - } - } + this.addFlyTextNative( + flytext, + actorIndex, + 1, + (IntPtr)numArray, + numOffset, + 10, + (IntPtr)strArray, + strOffset, + 2, + 0); } private static byte[] Terminate(byte[] source) @@ -230,7 +223,8 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui if (!dirty) { Log.Verbose("[FlyText] Calling flytext with original args."); - return this.createFlyTextHook.Original(addonFlyText, kind, val1, val2, text2, color, icon, damageTypeIcon, text1, yOffset); + return this.createFlyTextHook.Original(addonFlyText, kind, val1, val2, text2, color, icon, + damageTypeIcon, text1, yOffset); } var terminated1 = Terminate(tmpText1.Encode()); @@ -299,10 +293,10 @@ internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui { this.flyTextGuiService.FlyTextCreated += this.FlyTextCreatedForward; } - + /// public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated; - + /// public void Dispose() { diff --git a/global.json b/global.json index 3d9090158..133f31ec2 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "sdk": { "version": "7.0.0", - "rollForward": "latestMinor", + "rollForward": "latestMajor", "allowPrerelease": true } -} \ No newline at end of file +} From c7fc943692c27750df0d720fe2bddefbb75ebfed Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 19 Nov 2023 13:32:05 +0100 Subject: [PATCH 314/585] build: 9.0.0.13 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 5c971489a..3a6a0257d 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.12 + 9.0.0.13 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 40e90a39c8250528eeb94191a026a821c8a1493c Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 22 Nov 2023 15:30:38 +0900 Subject: [PATCH 315/585] Add Dalamud.CorePlugin.json (#1533) --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 6 ++++++ Dalamud.CorePlugin/Dalamud.CorePlugin.json | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 Dalamud.CorePlugin/Dalamud.CorePlugin.json diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 67ca26dee..d7eb8499c 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -50,4 +50,10 @@ false + + + + Always + + diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.json b/Dalamud.CorePlugin/Dalamud.CorePlugin.json new file mode 100644 index 000000000..7db669a73 --- /dev/null +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.json @@ -0,0 +1,9 @@ +{ + "Author": "Dalamud Maintainers", + "Name": "CorePlugin", + "Punchline": "Testbed for developing Dalamud features.", + "Description": "Develop and debug internal Dalamud features using CorePlugin. You have full access to all types in Dalamud assembly.", + "InternalName": "CorePlugin", + "ApplicableVersion": "any", + "Tags": [] +} From a80ab30b4cb83e48350f38d91850529a49fa1905 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Sat, 25 Nov 2023 21:24:08 +0100 Subject: [PATCH 316/585] Add a log message for each leaked hook with its address. (#1543) --- Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index 59f2d2684..9958385b9 100644 --- a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -91,6 +91,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT foreach (var hook in notDisposed) { + Log.Warning("\t\t\tLeaked hook at +0x{Address:X}", hook.Address.ToInt64() - this.scanner.Module.BaseAddress.ToInt64()); hook.Dispose(); } From a6ea4aa56a9a4ae528f2ff8f4e6dc38cc1e5c955 Mon Sep 17 00:00:00 2001 From: srkizer Date: Mon, 27 Nov 2023 06:55:43 +0900 Subject: [PATCH 317/585] Update .editorconfig for Resharper/Rider to conform with StyleCop rules (#1534) --- .editorconfig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 0ae30cf95..66e123f53 100644 --- a/.editorconfig +++ b/.editorconfig @@ -57,6 +57,7 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly +dotnet_separate_import_directive_groups = true dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion @@ -97,6 +98,7 @@ resharper_apply_on_completion = true resharper_auto_property_can_be_made_get_only_global_highlighting = none resharper_auto_property_can_be_made_get_only_local_highlighting = none resharper_autodetect_indent_settings = true +resharper_blank_lines_around_single_line_auto_property = 1 resharper_braces_for_ifelse = required_for_multiline resharper_can_use_global_alias = false resharper_csharp_align_multiline_parameter = true @@ -105,14 +107,22 @@ resharper_csharp_empty_block_style = multiline resharper_csharp_int_align_comments = true resharper_csharp_new_line_before_while = true resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_after_invocation_lpar = true +resharper_csharp_wrap_arguments_style = chop_if_long resharper_enforce_line_ending_style = true +resharper_instance_members_qualify_declared_in = this_class, base_class resharper_member_can_be_private_global_highlighting = none resharper_member_can_be_private_local_highlighting = none -resharper_new_line_before_finally = false +resharper_new_line_before_finally = true +resharper_parentheses_non_obvious_operations = none, multiplicative, additive, arithmetic, shift, bitwise_and, bitwise_exclusive_or, bitwise_inclusive_or, bitwise +resharper_parentheses_redundancy_style = remove_if_not_clarifies_precedence resharper_place_accessorholder_attribute_on_same_line = false resharper_place_field_attribute_on_same_line = false +resharper_place_simple_initializer_on_single_line = true resharper_show_autodetect_configure_formatting_tip = false +resharper_space_within_single_line_array_initializer_braces = true resharper_use_indent_from_vs = false +resharper_wrap_array_initializer_style = chop_if_long # ReSharper inspection severities resharper_arrange_missing_parentheses_highlighting = hint From 7a0de45f872a1347ab64c3d64cafdadc423d6271 Mon Sep 17 00:00:00 2001 From: srkizer Date: Mon, 27 Nov 2023 06:58:26 +0900 Subject: [PATCH 318/585] Miscellaneous improvements (#1537) --- .../Internal/DalamudConfiguration.cs | 18 +- Dalamud/Interface/GameFonts/FdtReader.cs | 44 +- .../Interface/GameFonts/GameFontManager.cs | 4 +- Dalamud/Interface/GameFonts/GameFontStyle.cs | 152 ++-- .../Interface/Internal/InterfaceManager.cs | 12 +- .../Internal/Windows/Data/DataWindow.cs | 54 +- .../Windows/Data/IDataWindowWidget.cs | 5 +- .../Data/Widgets/AddonLifecycleWidget.cs | 10 +- .../Data/Widgets/FontAwesomeTestWidget.cs | 24 +- .../Internal/Windows/ProfilerWindow.cs | 23 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 77 +- Dalamud/Interface/Utility/ImGuiHelpers.cs | 288 ++++++-- Dalamud/Interface/Utility/ImVectorWrapper.cs | 687 ++++++++++++++++++ Dalamud/Logging/Internal/ModuleLog.cs | 16 +- Dalamud/NativeFunctions.cs | 1 + 15 files changed, 1136 insertions(+), 279 deletions(-) create mode 100644 Dalamud/Interface/Utility/ImVectorWrapper.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 1274744c6..35d5261da 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -34,7 +35,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable }; [JsonIgnore] - private string configPath; + private string? configPath; [JsonIgnore] private bool isSaveQueued; @@ -48,12 +49,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Event that occurs when dalamud configuration is saved. /// - public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved; + public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved; /// /// Gets or sets a list of muted works. /// - public List BadWords { get; set; } + public List? BadWords { get; set; } /// /// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found. @@ -68,12 +69,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets the language code to load Dalamud localization with. /// - public string LanguageOverride { get; set; } = null; + public string? LanguageOverride { get; set; } = null; /// /// Gets or sets the last loaded Dalamud version. /// - public string LastVersion { get; set; } = null; + public string? LastVersion { get; set; } = null; /// /// Gets or sets a value indicating the last seen FTUE version. @@ -84,7 +85,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets the last loaded Dalamud version. /// - public string LastChangelogMajorMinor { get; set; } = null; + public string? LastChangelogMajorMinor { get; set; } = null; /// /// Gets or sets the chat type used by default for plugin messages. @@ -229,6 +230,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. /// This setting is effected by the in-game "System Sounds" option and volume. /// + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")] public bool EnablePluginUISoundEffects { get; set; } /// @@ -266,7 +268,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets the kind of beta to download when matches the server value. /// - public string DalamudBetaKind { get; set; } + public string? DalamudBetaKind { get; set; } /// /// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started. @@ -514,6 +516,8 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable private void Save() { ThreadSafety.AssertMainThread(); + if (this.configPath is null) + throw new InvalidOperationException("configPath is not set."); Service.Get().WriteAllText( this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); diff --git a/Dalamud/Interface/GameFonts/FdtReader.cs b/Dalamud/Interface/GameFonts/FdtReader.cs index a68caba94..0e8f3fb59 100644 --- a/Dalamud/Interface/GameFonts/FdtReader.cs +++ b/Dalamud/Interface/GameFonts/FdtReader.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Runtime.InteropServices; @@ -22,7 +21,7 @@ public class FdtReader for (var i = 0; i < this.FontHeader.FontTableEntryCount; i++) this.Glyphs.Add(StructureFromByteArray(data, this.FileHeader.FontTableHeaderOffset + Marshal.SizeOf() + (Marshal.SizeOf() * i))); - for (int i = 0, i_ = Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); i < i_; i++) + for (int i = 0, to = Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); i < to; i++) this.Distances.Add(StructureFromByteArray(data, this.FileHeader.KerningTableHeaderOffset + Marshal.SizeOf() + (Marshal.SizeOf() * i))); } @@ -51,6 +50,14 @@ public class FdtReader /// public List Distances { get; init; } = new(); + /// + /// Finds the glyph index for the corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding index, or a negative number according to . + public int FindGlyphIndex(int codepoint) => + this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8Int32(codepoint) }); + /// /// Finds glyph definition for corresponding codepoint. /// @@ -58,7 +65,7 @@ public class FdtReader /// Corresponding FontTableEntry, or null if not found. public FontTableEntry? FindGlyph(int codepoint) { - var i = this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8Int32(codepoint) }); + var i = this.FindGlyphIndex(codepoint); if (i < 0 || i == this.Glyphs.Count) return null; return this.Glyphs[i]; @@ -91,17 +98,12 @@ public class FdtReader return this.Distances[i].RightOffset; } - private static unsafe T StructureFromByteArray(byte[] data, int offset) - { - var len = Marshal.SizeOf(); - if (offset + len > data.Length) - throw new Exception("Data too short"); - - fixed (byte* ptr = data) - return Marshal.PtrToStructure(new(ptr + offset)); - } - - private static int CodePointToUtf8Int32(int codepoint) + /// + /// Translates a UTF-32 codepoint to a containing a UTF-8 character. + /// + /// The codepoint. + /// The uint. + internal static int CodePointToUtf8Int32(int codepoint) { if (codepoint <= 0x7F) { @@ -131,6 +133,16 @@ public class FdtReader } } + private static unsafe T StructureFromByteArray(byte[] data, int offset) + { + var len = Marshal.SizeOf(); + if (offset + len > data.Length) + throw new Exception("Data too short"); + + fixed (byte* ptr = data) + return Marshal.PtrToStructure(new(ptr + offset)); + } + private static int Utf8Uint32ToCodePoint(int n) { if ((n & 0xFFFFFF80) == 0) @@ -252,7 +264,7 @@ public class FdtReader /// Glyph table entry. /// [StructLayout(LayoutKind.Sequential)] - public unsafe struct FontTableEntry : IComparable + public struct FontTableEntry : IComparable { /// /// Mapping of texture channel index to byte index. @@ -367,7 +379,7 @@ public class FdtReader /// Kerning table entry. /// [StructLayout(LayoutKind.Sequential)] - public unsafe struct KerningTableEntry : IComparable + public struct KerningTableEntry : IComparable { /// /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the left character. diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index a7cd27b83..71661682d 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -257,7 +257,7 @@ internal class GameFontManager : IServiceType /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - ImGuiHelpers.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable); + ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -269,7 +269,7 @@ internal class GameFontManager : IServiceType /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable); + ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); } /// diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index 40b810161..946473df4 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -1,5 +1,3 @@ -using System; - namespace Dalamud.Interface.GameFonts; /// @@ -153,7 +151,7 @@ public struct GameFontStyle GameFontFamilyAndSize.TrumpGothic184 => 18.4f, GameFontFamilyAndSize.TrumpGothic23 => 23, GameFontFamilyAndSize.TrumpGothic34 => 34, - GameFontFamilyAndSize.TrumpGothic68 => 8, + GameFontFamilyAndSize.TrumpGothic68 => 68, _ => throw new InvalidOperationException(), }; @@ -186,77 +184,77 @@ public struct GameFontStyle /// Font family. /// Font size in points. /// Recommended GameFontFamilyAndSize. - public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size) - { - if (size <= 0) - return GameFontFamilyAndSize.Undefined; - - switch (family) + public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size) => + family switch { - case GameFontFamily.Undefined: - return GameFontFamilyAndSize.Undefined; + _ when size <= 0 => GameFontFamilyAndSize.Undefined, + GameFontFamily.Undefined => GameFontFamilyAndSize.Undefined, + GameFontFamily.Axis => size switch + { + <= ((int)((9.6f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis96, + <= ((int)((12f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis12, + <= ((int)((14f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis14, + <= ((int)((18f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis18, + _ => GameFontFamilyAndSize.Axis36, + }, + GameFontFamily.Jupiter => size switch + { + <= ((int)((16f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter16, + <= ((int)((20f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter20, + <= ((int)((23f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter23, + _ => GameFontFamilyAndSize.Jupiter46, + }, + GameFontFamily.JupiterNumeric => size switch + { + <= ((int)((45f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter45, + _ => GameFontFamilyAndSize.Jupiter90, + }, + GameFontFamily.Meidinger => size switch + { + <= ((int)((16f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Meidinger16, + <= ((int)((20f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Meidinger20, + _ => GameFontFamilyAndSize.Meidinger40, + }, + GameFontFamily.MiedingerMid => size switch + { + <= ((int)((10f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid10, + <= ((int)((12f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid12, + <= ((int)((14f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid14, + <= ((int)((18f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid18, + _ => GameFontFamilyAndSize.MiedingerMid36, + }, + GameFontFamily.TrumpGothic => size switch + { + <= ((int)((18.4f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.TrumpGothic184, + <= ((int)((23f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.TrumpGothic23, + <= ((int)((34f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.TrumpGothic34, + _ => GameFontFamilyAndSize.TrumpGothic68, + }, + _ => GameFontFamilyAndSize.Undefined, + }; - case GameFontFamily.Axis: - if (size <= 9.601) - return GameFontFamilyAndSize.Axis96; - else if (size <= 12.001) - return GameFontFamilyAndSize.Axis12; - else if (size <= 14.001) - return GameFontFamilyAndSize.Axis14; - else if (size <= 18.001) - return GameFontFamilyAndSize.Axis18; - else - return GameFontFamilyAndSize.Axis36; - - case GameFontFamily.Jupiter: - if (size <= 16.001) - return GameFontFamilyAndSize.Jupiter16; - else if (size <= 20.001) - return GameFontFamilyAndSize.Jupiter20; - else if (size <= 23.001) - return GameFontFamilyAndSize.Jupiter23; - else - return GameFontFamilyAndSize.Jupiter46; - - case GameFontFamily.JupiterNumeric: - if (size <= 45.001) - return GameFontFamilyAndSize.Jupiter45; - else - return GameFontFamilyAndSize.Jupiter90; - - case GameFontFamily.Meidinger: - if (size <= 16.001) - return GameFontFamilyAndSize.Meidinger16; - else if (size <= 20.001) - return GameFontFamilyAndSize.Meidinger20; - else - return GameFontFamilyAndSize.Meidinger40; - - case GameFontFamily.MiedingerMid: - if (size <= 10.001) - return GameFontFamilyAndSize.MiedingerMid10; - else if (size <= 12.001) - return GameFontFamilyAndSize.MiedingerMid12; - else if (size <= 14.001) - return GameFontFamilyAndSize.MiedingerMid14; - else if (size <= 18.001) - return GameFontFamilyAndSize.MiedingerMid18; - else - return GameFontFamilyAndSize.MiedingerMid36; - - case GameFontFamily.TrumpGothic: - if (size <= 18.401) - return GameFontFamilyAndSize.TrumpGothic184; - else if (size <= 23.001) - return GameFontFamilyAndSize.TrumpGothic23; - else if (size <= 34.001) - return GameFontFamilyAndSize.TrumpGothic34; - else - return GameFontFamilyAndSize.TrumpGothic68; - - default: - return GameFontFamilyAndSize.Undefined; + /// + /// Calculates the adjustment to width resulting fron Weight and SkewStrength. + /// + /// Font header. + /// Glyph. + /// Width adjustment in pixel unit. + public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + { + var widthDelta = this.Weight; + switch (this.BaseSkewStrength) + { + case > 0: + widthDelta += (1f * this.BaseSkewStrength * (header.LineHeight - glyph.CurrentOffsetY)) + / header.LineHeight; + break; + case < 0: + widthDelta -= (1f * this.BaseSkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight)) + / header.LineHeight; + break; } + + return (int)MathF.Ceiling(widthDelta); } /// @@ -265,16 +263,8 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) - { - var widthDelta = this.Weight; - if (this.BaseSkewStrength > 0) - widthDelta += 1f * this.BaseSkewStrength * (reader.FontHeader.LineHeight - glyph.CurrentOffsetY) / reader.FontHeader.LineHeight; - else if (this.BaseSkewStrength < 0) - widthDelta -= 1f * this.BaseSkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight) / reader.FontHeader.LineHeight; - - return (int)Math.Ceiling(widthDelta); - } + public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// public override string ToString() diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d5394fe8d..9de87c6e3 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -52,8 +52,16 @@ namespace Dalamud.Interface.Internal; [ServiceManager.BlockingEarlyLoadedService] internal class InterfaceManager : IDisposable, IServiceType { - private const float DefaultFontSizePt = 12.0f; - private const float DefaultFontSizePx = DefaultFontSizePt * 4.0f / 3.0f; + /// + /// The default font size, in points. + /// + public const float DefaultFontSizePt = 12.0f; + + /// + /// The default font size, in pixels. + /// + public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; + private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index d59b50e58..e9d4152a5 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -18,39 +18,39 @@ internal class DataWindow : Window { private readonly IDataWindowWidget[] modules = { - new ServicesWidget(), - new AddressesWidget(), - new ObjectTableWidget(), - new FateTableWidget(), - new SeFontTestWidget(), - new FontAwesomeTestWidget(), - new PartyListWidget(), - new BuddyListWidget(), - new PluginIpcWidget(), - new ConditionWidget(), - new GaugeWidget(), - new CommandWidget(), - new AddonWidget(), new AddonInspectorWidget(), + new AddonLifecycleWidget(), + new AddonWidget(), + new AddressesWidget(), + new AetherytesWidget(), new AtkArrayDataBrowserWidget(), + new BuddyListWidget(), + new CommandWidget(), + new ConditionWidget(), + new ConfigurationWidget(), + new DataShareWidget(), + new DtrBarWidget(), + new FateTableWidget(), + new FlyTextWidget(), + new FontAwesomeTestWidget(), + new GamepadWidget(), + new GaugeWidget(), + new HookWidget(), + new IconBrowserWidget(), + new ImGuiWidget(), + new KeyStateWidget(), + new NetworkMonitorWidget(), + new ObjectTableWidget(), + new PartyListWidget(), + new PluginIpcWidget(), + new SeFontTestWidget(), + new ServicesWidget(), new StartInfoWidget(), new TargetWidget(), - new ToastWidget(), - new FlyTextWidget(), - new ImGuiWidget(), - new TexWidget(), - new KeyStateWidget(), - new GamepadWidget(), - new ConfigurationWidget(), new TaskSchedulerWidget(), - new HookWidget(), - new AetherytesWidget(), - new DtrBarWidget(), + new TexWidget(), + new ToastWidget(), new UIColorWidget(), - new DataShareWidget(), - new NetworkMonitorWidget(), - new IconBrowserWidget(), - new AddonLifecycleWidget(), }; private readonly IOrderedEnumerable orderedModules; diff --git a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs index 0e12e4c51..78df015ed 100644 --- a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs @@ -1,7 +1,6 @@ -using System; -using System.Linq; +using System.Linq; -namespace Dalamud.Interface.Internal.Windows; +namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class representing a date window entry. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 7dc8e2f3c..0e654d316 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -28,8 +28,14 @@ public class AddonLifecycleWidget : IDataWindowWidget /// public void Load() { - this.AddonLifecycle = Service.GetNullable(); - if (this.AddonLifecycle is not null) this.Ready = true; + Service + .GetAsync() + .ContinueWith( + r => + { + this.AddonLifecycle = r.Result; + this.Ready = true; + }); } /// diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 26bd2e623..22f615e8a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -38,12 +38,30 @@ internal class FontAwesomeTestWidget : IDataWindowWidget public void Draw() { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - - this.iconCategories ??= FontAwesomeHelpers.GetCategories(); + + this.iconCategories ??= new[] { "(Show All)", "(Undefined)" } + .Concat(FontAwesomeHelpers.GetCategories().Skip(1)) + .ToArray(); if (this.iconSearchChanged) { - this.icons = FontAwesomeHelpers.SearchIcons(this.iconSearchInput, this.iconCategories[this.selectedIconCategory]); + if (this.iconSearchInput == string.Empty && this.selectedIconCategory <= 1) + { + var en = InterfaceManager.IconFont.GlyphsWrapped() + .Select(x => (FontAwesomeIcon)x.Codepoint) + .Where(x => (ushort)x is >= 0xE000 and < 0xF000); + en = this.selectedIconCategory == 0 + ? en.Concat(FontAwesomeHelpers.SearchIcons(string.Empty, string.Empty)) + : en.Except(FontAwesomeHelpers.SearchIcons(string.Empty, string.Empty)); + this.icons = en.Distinct().Order().ToList(); + } + else + { + this.icons = FontAwesomeHelpers.SearchIcons( + this.iconSearchInput, + this.selectedIconCategory <= 1 ? string.Empty : this.iconCategories[this.selectedIconCategory]); + } + this.iconNames = this.icons.Select(icon => Enum.GetName(icon)!).ToList(); this.iconSearchChanged = false; } diff --git a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs index 16f253da9..cd653143b 100644 --- a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -46,7 +45,7 @@ public class ProfilerWindow : Window ImGui.Text("Timings"); - var childHeight = Math.Max(300, 20 * (2 + this.occupied.Count)); + var childHeight = Math.Max(300, 20 * (2.5f + this.occupied.Count)); if (ImGui.BeginChild("Timings", new Vector2(0, childHeight), true)) { @@ -115,7 +114,7 @@ public class ProfilerWindow : Window parentDepthDict[timingHandle.Id] = depth; startX = Math.Max(startX, 0); - endX = Math.Max(endX, 0); + endX = Math.Max(endX, startX + (ImGuiHelpers.GlobalScale * 16)); Vector4 rectColor; if (this.occupied[depth].Count % 2 == 0) @@ -129,11 +128,6 @@ public class ProfilerWindow : Window if (maxRectDept < depth) maxRectDept = (uint)depth; - if (startX == endX) - { - continue; - } - var minPos = pos + new Vector2((uint)startX, 20 * depth); var maxPos = pos + new Vector2((uint)endX, 20 * (depth + 1)); @@ -231,22 +225,22 @@ public class ProfilerWindow : Window ImGui.EndChild(); var sliderMin = (float)this.min / 1000f; - if (ImGui.SliderFloat("Start", ref sliderMin, (float)actualMin / 1000f, (float)this.max / 1000f, "%.1fs")) + if (ImGui.SliderFloat("Start", ref sliderMin, (float)actualMin / 1000f, (float)this.max / 1000f, "%.2fs")) { this.min = sliderMin * 1000f; } var sliderMax = (float)this.max / 1000f; - if (ImGui.SliderFloat("End", ref sliderMax, (float)this.min / 1000f, (float)actualMax / 1000f, "%.1fs")) + if (ImGui.SliderFloat("End", ref sliderMax, (float)this.min / 1000f, (float)actualMax / 1000f, "%.2fs")) { this.max = sliderMax * 1000f; } - var sizeShown = (float)(this.max - this.min); - var sizeActual = (float)(actualMax - actualMin); - if (ImGui.SliderFloat("Size", ref sizeShown, sizeActual / 10f, sizeActual, "%.1fs")) + var sizeShown = (float)(this.max - this.min) / 1000f; + var sizeActual = (float)(actualMax - actualMin) / 1000f; + if (ImGui.SliderFloat("Size", ref sizeShown, sizeActual / 10f, sizeActual, "%.2fs")) { - this.max = this.min + sizeShown; + this.max = this.min + (sizeShown * 1000f); } ImGui.Text("Min: " + actualMin.ToString("0.000")); @@ -257,6 +251,7 @@ public class ProfilerWindow : Window [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internals")] private class RectInfo { + // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized <- well you're wrong internal TimingHandle Timing; internal Vector2 MinPos; internal Vector2 MaxPos; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 7a6f894c1..02e8ce789 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,5 +1,6 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; using CheapLoc; using Dalamud.Configuration.Internal; @@ -16,6 +17,16 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] public class SettingsTabLook : SettingsTab { + private static readonly (string, float)[] GlobalUiScalePresets = + { + ("9.6pt##DalamudSettingsGlobalUiScaleReset96", 9.6f / InterfaceManager.DefaultFontSizePt), + ("12pt##DalamudSettingsGlobalUiScaleReset12", 12f / InterfaceManager.DefaultFontSizePt), + ("14pt##DalamudSettingsGlobalUiScaleReset14", 14f / InterfaceManager.DefaultFontSizePt), + ("18pt##DalamudSettingsGlobalUiScaleReset18", 18f / InterfaceManager.DefaultFontSizePt), + ("24pt##DalamudSettingsGlobalUiScaleReset24", 24f / InterfaceManager.DefaultFontSizePt), + ("36pt##DalamudSettingsGlobalUiScaleReset36", 36f / InterfaceManager.DefaultFontSizePt), + }; + private float globalUiScale; private float fontGamma; @@ -135,55 +146,22 @@ public class SettingsTabLook : SettingsTab { var interfaceManager = Service.Get(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); + ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); - ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3); - if (ImGui.Button("9.6pt##DalamudSettingsGlobalUiScaleReset96")) - { - this.globalUiScale = 9.6f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - ImGui.SameLine(); - if (ImGui.Button("12pt##DalamudSettingsGlobalUiScaleReset12")) + var buttonSize = + GlobalUiScalePresets + .Select(x => ImGui.CalcTextSize(x.Item1, 0, x.Item1.IndexOf('#'))) + .Aggregate(Vector2.Zero, Vector2.Max) + + (ImGui.GetStyle().FramePadding * 2); + foreach (var (buttonLabel, scale) in GlobalUiScalePresets) { - this.globalUiScale = 1.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("14pt##DalamudSettingsGlobalUiScaleReset14")) - { - this.globalUiScale = 14.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("18pt##DalamudSettingsGlobalUiScaleReset18")) - { - this.globalUiScale = 18.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("24pt##DalamudSettingsGlobalUiScaleReset24")) - { - this.globalUiScale = 24.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("36pt##DalamudSettingsGlobalUiScaleReset36")) - { - this.globalUiScale = 36.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); + ImGui.SameLine(); + if (ImGui.Button(buttonLabel, buttonSize) && Math.Abs(this.globalUiScale - scale) > float.Epsilon) + { + ImGui.GetIO().FontGlobalScale = this.globalUiScale = scale; + interfaceManager.RebuildFonts(); + } } var globalUiScaleInPt = 12f * this.globalUiScale; @@ -198,10 +176,9 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.ScaledDummy(5); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); + ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3); if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) { this.fontGamma = 1.4f; diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index d11e1ce1c..579d93f86 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Utility.Raii; using ImGuiNET; @@ -25,6 +26,20 @@ public static class ImGuiHelpers /// public static float GlobalScale { get; private set; } + /// + /// Gets a value indicating whether ImGui is initialized and ready for use.
+ /// This does not necessarily mean you can call drawing functions. + ///
+ public static unsafe bool IsImGuiInitialized => + ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null; + + /// + /// Gets the global Dalamud scale; even available before drawing is ready.
+ /// If you are sure that drawing is ready, at the point of using this, use instead. + ///
+ public static float GlobalScaleSafe => + IsImGuiInitialized ? ImGui.GetIO().FontGlobalScale : Service.Get().GlobalUiScale; + /// /// Check if the current ImGui window is on the main viewport. /// Only valid within a window. @@ -174,6 +189,47 @@ public static class ImGuiHelpers } } + /// + /// Unscales fonts after they have been rendered onto atlas. + /// + /// Font to scale. + /// Scale. + /// If a positive number is given, numbers will be rounded to this. + public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) + { + Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; + + var font = fontPtr.NativePtr; + font->FontSize = rounder(font->FontSize * scale); + font->Ascent = rounder(font->Ascent * scale); + font->Descent = font->FontSize - font->Ascent; + if (font->ConfigData != null) + font->ConfigData->SizePixels = rounder(font->ConfigData->SizePixels * scale); + + foreach (ref var glyphHotDataReal in new Span( + (void*)font->IndexedHotData.Data, + font->IndexedHotData.Size)) + { + glyphHotDataReal.AdvanceX = rounder(glyphHotDataReal.AdvanceX * scale); + glyphHotDataReal.OccupiedWidth = rounder(glyphHotDataReal.OccupiedWidth * scale); + } + + foreach (ref var glyphReal in new Span((void*)font->Glyphs.Data, font->Glyphs.Size)) + { + glyphReal.X0 *= scale; + glyphReal.X1 *= scale; + glyphReal.Y0 *= scale; + glyphReal.Y1 *= scale; + glyphReal.AdvanceX = rounder(glyphReal.AdvanceX * scale); + } + + foreach (ref var kp in new Span((void*)font->KerningPairs.Data, font->KerningPairs.Size)) + kp.AdvanceXAdjustment = rounder(kp.AdvanceXAdjustment * scale); + + foreach (ref var fkp in new Span((void*)font->FrequentKerningPairs.Data, font->FrequentKerningPairs.Size)) + fkp = rounder(fkp * scale); + } + /// /// Fills missing glyphs in target font from source font, if both are not null. /// @@ -183,71 +239,110 @@ public static class ImGuiHelpers /// Whether to call target.BuildLookupTable(). /// Low codepoint range to copy. /// High codepoing range to copy. - public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE) + [Obsolete("Use the non-nullable variant.", true)] + public static void CopyGlyphsAcrossFonts( + ImFontPtr? source, + ImFontPtr? target, + bool missingOnly, + bool rebuildLookupTable = true, + int rangeLow = 32, + int rangeHigh = 0xFFFE) => + CopyGlyphsAcrossFonts( + source ?? default, + target ?? default, + missingOnly, + rebuildLookupTable, + rangeLow, + rangeHigh); + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + public static unsafe void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + int rangeLow = 32, + int rangeHigh = 0xFFFE) { - if (!source.HasValue || !target.HasValue) + if (!source.IsNotNullAndLoaded() || !target.IsNotNullAndLoaded()) return; - var scale = target.Value!.FontSize / source.Value!.FontSize; + var changed = false; + var scale = target.FontSize / source.FontSize; var addedCodepoints = new HashSet(); - unsafe + + if (source.Glyphs.Size == 0) + return; + + var glyphs = (ImFontGlyphReal*)source.Glyphs.Data; + if (glyphs is null) + throw new InvalidOperationException("Glyphs data is empty but size is >0?"); + + for (int j = 0, k = source.Glyphs.Size; j < k; j++) { - var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data; - for (int j = 0, k = source.Value!.Glyphs.Size; j < k; j++) + var glyph = &glyphs![j]; + if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) + continue; + + var prevGlyphPtr = (ImFontGlyphReal*)target.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; + if ((IntPtr)prevGlyphPtr == IntPtr.Zero) { - Debug.Assert(glyphs != null, nameof(glyphs) + " != null"); - - var glyph = &glyphs[j]; - if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) - continue; - - var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; - if ((IntPtr)prevGlyphPtr == IntPtr.Zero) - { - addedCodepoints.Add(glyph->Codepoint); - target.Value!.AddGlyph( - target.Value!.ConfigData, - (ushort)glyph->Codepoint, - glyph->TextureIndex, - glyph->X0 * scale, - ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->X1 * scale, - ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->U0, - glyph->V0, - glyph->U1, - glyph->V1, - glyph->AdvanceX * scale); - } - else if (!missingOnly) - { - addedCodepoints.Add(glyph->Codepoint); - prevGlyphPtr->TextureIndex = glyph->TextureIndex; - prevGlyphPtr->X0 = glyph->X0 * scale; - prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->X1 = glyph->X1 * scale; - prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->U0 = glyph->U0; - prevGlyphPtr->V0 = glyph->V0; - prevGlyphPtr->U1 = glyph->U1; - prevGlyphPtr->V1 = glyph->V1; - prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; - } + addedCodepoints.Add(glyph->Codepoint); + target.AddGlyph( + target.ConfigData, + (ushort)glyph->Codepoint, + glyph->TextureIndex, + glyph->X0 * scale, + ((glyph->Y0 - source.Ascent) * scale) + target.Ascent, + glyph->X1 * scale, + ((glyph->Y1 - source.Ascent) * scale) + target.Ascent, + glyph->U0, + glyph->V0, + glyph->U1, + glyph->V1, + glyph->AdvanceX * scale); + changed = true; } - - var kernPairs = source.Value!.KerningPairs; - for (int j = 0, k = kernPairs.Size; j < k; j++) + else if (!missingOnly) { - if (!addedCodepoints.Contains(kernPairs[j].Left)) - continue; - if (!addedCodepoints.Contains(kernPairs[j].Right)) - continue; - target.Value.AddKerningPair(kernPairs[j].Left, kernPairs[j].Right, kernPairs[j].AdvanceXAdjustment); + addedCodepoints.Add(glyph->Codepoint); + prevGlyphPtr->TextureIndex = glyph->TextureIndex; + prevGlyphPtr->X0 = glyph->X0 * scale; + prevGlyphPtr->Y0 = ((glyph->Y0 - source.Ascent) * scale) + target.Ascent; + prevGlyphPtr->X1 = glyph->X1 * scale; + prevGlyphPtr->Y1 = ((glyph->Y1 - source.Ascent) * scale) + target.Ascent; + prevGlyphPtr->U0 = glyph->U0; + prevGlyphPtr->V0 = glyph->V0; + prevGlyphPtr->U1 = glyph->U1; + prevGlyphPtr->V1 = glyph->V1; + prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; } } - if (rebuildLookupTable && target.Value!.Glyphs.Size > 0) - target.Value!.BuildLookupTableNonstandard(); + if (target.Glyphs.Size == 0) + return; + + var kernPairs = source.KerningPairs; + for (int j = 0, k = kernPairs.Size; j < k; j++) + { + if (!addedCodepoints.Contains(kernPairs[j].Left)) + continue; + if (!addedCodepoints.Contains(kernPairs[j].Right)) + continue; + target.AddKerningPair(kernPairs[j].Left, kernPairs[j].Right, kernPairs[j].AdvanceXAdjustment); + changed = true; + } + + if (changed && rebuildLookupTable) + target.BuildLookupTableNonstandard(); } /// @@ -302,21 +397,35 @@ public static class ImGuiHelpers /// Center the ImGui cursor for a certain text. /// /// The text to center for. - public static void CenterCursorForText(string text) - { - var textWidth = ImGui.CalcTextSize(text).X; - CenterCursorFor((int)textWidth); - } + public static void CenterCursorForText(string text) => CenterCursorFor(ImGui.CalcTextSize(text).X); /// /// Center the ImGui cursor for an item with a certain width. /// /// The width to center for. - public static void CenterCursorFor(int itemWidth) - { - var window = (int)ImGui.GetWindowWidth(); - ImGui.SetCursorPosX((window / 2) - (itemWidth / 2)); - } + public static void CenterCursorFor(float itemWidth) => + ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + + /// + /// Determines whether is empty. + /// + /// The pointer. + /// Whether it is empty. + public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; + + /// + /// Determines whether is not null and loaded. + /// + /// The pointer. + /// Whether it is empty. + public static unsafe bool IsNotNullAndLoaded(this ImFontPtr ptr) => ptr.NativePtr != null && ptr.IsLoaded(); + + /// + /// Determines whether is empty. + /// + /// The pointer. + /// Whether it is empty. + public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; /// /// Get data needed for each new frame. @@ -330,19 +439,57 @@ public static class ImGuiHelpers /// ImFontGlyph the correct version. /// [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")] + [StructLayout(LayoutKind.Explicit, Size = 40)] public struct ImFontGlyphReal { + [FieldOffset(0)] public uint ColoredVisibleTextureIndexCodepoint; + + [FieldOffset(4)] public float AdvanceX; + + [FieldOffset(8)] public float X0; + + [FieldOffset(12)] public float Y0; + + [FieldOffset(16)] public float X1; + + [FieldOffset(20)] public float Y1; + + [FieldOffset(24)] public float U0; + + [FieldOffset(28)] public float V0; + + [FieldOffset(32)] public float U1; + + [FieldOffset(36)] public float V1; + [FieldOffset(8)] + public Vector2 XY0; + + [FieldOffset(16)] + public Vector2 XY1; + + [FieldOffset(24)] + public Vector2 UV0; + + [FieldOffset(32)] + public Vector2 UV1; + + [FieldOffset(8)] + public Vector4 XY; + + [FieldOffset(24)] + public Vector4 UV; + private const uint ColoredMask /*****/ = 0b_00000000_00000000_00000000_00000001u; private const uint VisibleMask /*****/ = 0b_00000000_00000000_00000000_00000010u; private const uint TextureMask /*****/ = 0b_00000000_00000000_00000111_11111100u; @@ -390,7 +537,7 @@ public static class ImGuiHelpers private const uint UseBisectMask /***/ = 0b_00000000_00000000_00000000_00000001u; private const uint OffsetMask /******/ = 0b_00000000_00001111_11111111_11111110u; - private const uint CountMask /*******/ = 0b_11111111_11110000_00000111_11111100u; + private const uint CountMask /*******/ = 0b_11111111_11110000_00000000_00000000u; private const int UseBisectShift = 0; private const int OffsetShift = 1; @@ -419,6 +566,7 @@ public static class ImGuiHelpers /// ImFontAtlasCustomRect the correct version. /// [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")] + [StructLayout(LayoutKind.Sequential)] public unsafe struct ImFontAtlasCustomRectReal { public ushort Width; @@ -431,10 +579,10 @@ public static class ImGuiHelpers public ImFont* Font; private const uint TextureIndexMask /***/ = 0b_00000000_00000000_00000111_11111100u; - private const uint GlyphIDMask /********/ = 0b_11111111_11111111_11111000_00000000u; + private const uint GlyphIdMask /********/ = 0b_11111111_11111111_11111000_00000000u; private const int TextureIndexShift = 2; - private const int GlyphIDShift = 11; + private const int GlyphIdShift = 11; public int TextureIndex { @@ -444,8 +592,8 @@ public static class ImGuiHelpers public int GlyphId { - get => (int)(this.TextureIndexAndGlyphId & GlyphIDMask) >> GlyphIDShift; - set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~GlyphIDMask) | ((uint)value << GlyphIDShift); + get => (int)(this.TextureIndexAndGlyphId & GlyphIdMask) >> GlyphIdShift; + set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~GlyphIdMask) | ((uint)value << GlyphIdShift); } } } diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs new file mode 100644 index 000000000..67b002179 --- /dev/null +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -0,0 +1,687 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; + +using ImGuiNET; + +using JetBrains.Annotations; + +namespace Dalamud.Interface.Utility; + +/// +/// Utility methods for . +/// +public static class ImVectorWrapper +{ + /// + /// Creates a new instance of the struct, initialized with + /// .
+ /// You must call after use. + ///
+ /// The item type. + /// The initial data. + /// The destroyer function to call on item removal. + /// The minimum capacity of the new vector. + /// The new wrapped vector, that has to be disposed after use. + public static ImVectorWrapper CreateFromEnumerable( + IEnumerable sourceEnumerable, + ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, + int minCapacity = 0) + where T : unmanaged + { + var res = new ImVectorWrapper(0, destroyer); + try + { + switch (sourceEnumerable) + { + case T[] c: + res.SetCapacity(Math.Max(minCapacity, c.Length + 1)); + res.LengthUnsafe = c.Length; + c.AsSpan().CopyTo(res.DataSpan); + break; + case ICollection c: + res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); + res.AddRange(sourceEnumerable); + break; + case ICollection c: + res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); + res.AddRange(sourceEnumerable); + break; + default: + res.SetCapacity(minCapacity); + res.AddRange(sourceEnumerable); + res.EnsureCapacity(res.LengthUnsafe + 1); + break; + } + + // Null termination + Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); + res.StorageSpan[res.LengthUnsafe] = default; + + return res; + } + catch + { + res.Dispose(); + throw; + } + } + + /// + /// Creates a new instance of the struct, initialized with + /// .
+ /// You must call after use. + ///
+ /// The item type. + /// The initial data. + /// The destroyer function to call on item removal. + /// The minimum capacity of the new vector. + /// The new wrapped vector, that has to be disposed after use. + public static ImVectorWrapper CreateFromSpan( + ReadOnlySpan sourceSpan, + ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, + int minCapacity = 0) + where T : unmanaged + { + var res = new ImVectorWrapper(Math.Max(minCapacity, sourceSpan.Length + 1), destroyer); + try + { + res.LengthUnsafe = sourceSpan.Length; + sourceSpan.CopyTo(res.DataSpan); + + // Null termination + Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); + res.StorageSpan[res.LengthUnsafe] = default; + return res; + } + catch + { + res.Dispose(); + throw; + } + } + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper ConfigDataWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->ConfigData, ImGuiNative.ImFontConfig_destroy); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper FontsWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Fonts, x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper TexturesWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Textures); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper GlyphsWrapped(this ImFontPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Glyphs); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper IndexedHotDataWrapped(this ImFontPtr obj) + => obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->IndexedHotData); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper IndexLookupWrapped(this ImFontPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->IndexLookup); +} + +/// +/// Wrapper for ImVector. +/// +/// Contained type. +public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDisposable + where T : unmanaged +{ + private ImVector* vector; + private ImGuiNativeDestroyDelegate? destroyer; + + /// + /// Initializes a new instance of the struct.
+ /// If is set to true, you must call after use, + /// and the underlying memory for must have been allocated using + /// . Otherwise, it will crash. + ///
+ /// The underlying vector. + /// The destroyer function to call on item removal. + /// Whether this wrapper owns the vector. + public ImVectorWrapper( + [NotNull] ImVector* vector, + ImGuiNativeDestroyDelegate? destroyer = null, + bool ownership = false) + { + if (vector is null) + throw new ArgumentException($"{nameof(vector)} cannot be null.", nameof(this.vector)); + + this.vector = vector; + this.destroyer = destroyer; + this.HasOwnership = ownership; + } + + /// + /// Initializes a new instance of the struct.
+ /// You must call after use. + ///
+ /// The initial capacity. + /// The destroyer function to call on item removal. + public ImVectorWrapper(int initialCapacity = 0, ImGuiNativeDestroyDelegate? destroyer = null) + { + if (initialCapacity < 0) + { + throw new ArgumentOutOfRangeException( + nameof(initialCapacity), + initialCapacity, + $"{nameof(initialCapacity)} cannot be a negative number."); + } + + this.vector = (ImVector*)ImGuiNative.igMemAlloc((uint)sizeof(ImVector)); + if (this.vector is null) + throw new OutOfMemoryException(); + *this.vector = default; + this.HasOwnership = true; + this.destroyer = destroyer; + + try + { + this.EnsureCapacity(initialCapacity); + } + catch + { + ImGuiNative.igMemFree(this.vector); + this.vector = null; + this.HasOwnership = false; + this.destroyer = null; + throw; + } + } + + /// + /// Destroy callback for items. + /// + /// Pointer to self. + public delegate void ImGuiNativeDestroyDelegate(T* self); + + /// + /// Gets the raw vector. + /// + public ImVector* RawVector => this.vector; + + /// + /// Gets a view of the underlying ImVector{T}, for the range of . + /// + public Span DataSpan => new(this.DataUnsafe, this.LengthUnsafe); + + /// + /// Gets a view of the underlying ImVector{T}, for the range of . + /// + public Span StorageSpan => new(this.DataUnsafe, this.CapacityUnsafe); + + /// + /// Gets a value indicating whether this is disposed. + /// + public bool IsDisposed => this.vector is null; + + /// + /// Gets a value indicating whether this has the ownership of the underlying + /// . + /// + public bool HasOwnership { get; private set; } + + /// + /// Gets the underlying . + /// + public ImVector* Vector => + this.vector is null ? throw new ObjectDisposedException(nameof(ImVectorWrapper)) : this.vector; + + /// + /// Gets the number of items contained inside the underlying ImVector{T}. + /// + public int Length => this.LengthUnsafe; + + /// + /// Gets the number of items that can be contained inside the underlying ImVector{T}. + /// + public int Capacity => this.CapacityUnsafe; + + /// + /// Gets the pointer to the first item in the data inside underlying ImVector{T}. + /// + public T* Data => this.DataUnsafe; + + /// + /// Gets the reference to the number of items contained inside the underlying ImVector{T}. + /// + public ref int LengthUnsafe => ref *&this.Vector->Size; + + /// + /// Gets the reference to the number of items that can be contained inside the underlying ImVector{T}. + /// + public ref int CapacityUnsafe => ref *&this.Vector->Capacity; + + /// + /// Gets the reference to the pointer to the first item in the data inside underlying ImVector{T}. + /// + /// This may be null, if is zero. + public ref T* DataUnsafe => ref *(T**)&this.Vector->Data; + + /// + public bool IsReadOnly => false; + + /// + int ICollection.Count => this.LengthUnsafe; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot { get; } = new(); + + /// + int ICollection.Count => this.LengthUnsafe; + + /// + int IReadOnlyCollection.Count => this.LengthUnsafe; + + /// + bool IList.IsFixedSize => false; + + /// + /// Gets the element at the specified index as a reference. + /// + /// Index of the item. + /// If is out of range. + public ref T this[int index] => ref this.DataUnsafe[this.EnsureIndex(index)]; + + /// + T IReadOnlyList.this[int index] => this[index]; + + /// + object? IList.this[int index] + { + get => this[index]; + set => this[index] = value is null ? default : (T)value; + } + + /// + T IList.this[int index] + { + get => this[index]; + set => this[index] = value; + } + + /// + public void Dispose() + { + if (this.HasOwnership) + { + this.Clear(); + this.SetCapacity(0); + Debug.Assert(this.vector->Data == 0, "SetCapacity(0) did not free the data"); + ImGuiNative.igMemFree(this.vector); + } + + this.vector = null; + this.HasOwnership = false; + this.destroyer = null; + } + + /// + public IEnumerator GetEnumerator() + { + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + yield return this[i]; + } + + /// + public void Add(in T item) + { + this.EnsureCapacityExponential(this.LengthUnsafe + 1); + this.DataUnsafe[this.LengthUnsafe++] = item; + } + + /// + public void AddRange(IEnumerable items) + { + if (items is ICollection { Count: var count }) + this.EnsureCapacityExponential(this.LengthUnsafe + count); + + foreach (var item in items) + this.Add(item); + } + + /// + public void AddRange(Span items) + { + this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); + foreach (var item in items) + this.Add(item); + } + + /// + public void Clear() => this.Clear(false); + + /// + /// Clears this vector, optionally skipping destroyer invocation. + /// + /// Whether to skip destroyer invocation. + public void Clear(bool skipDestroyer) + { + if (this.destroyer != null && !skipDestroyer) + { + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + this.destroyer(&this.DataUnsafe[i]); + } + + this.LengthUnsafe = 0; + } + + /// + public bool Contains(in T item) => this.IndexOf(in item) != -1; + + /// + /// Size down the underlying ImVector{T}. + /// + /// Capacity to reserve. + /// Whether the capacity has been changed. + public bool Compact(int reservation) => this.SetCapacity(Math.Max(reservation, this.LengthUnsafe)); + + /// + public void CopyTo(T[] array, int arrayIndex) + { + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException( + nameof(arrayIndex), + arrayIndex, + $"{nameof(arrayIndex)} is less than 0."); + } + + if (array.Length - arrayIndex < this.LengthUnsafe) + { + throw new ArgumentException( + "The number of elements in the source ImVectorWrapper is greater than the available space from arrayIndex to the end of the destination array.", + nameof(array)); + } + + fixed (void* p = array) + Buffer.MemoryCopy(this.DataUnsafe, p, this.LengthUnsafe * sizeof(T), this.LengthUnsafe * sizeof(T)); + } + + /// + /// Ensures that the capacity of this list is at least the specified .
+ /// On growth, the new capacity exactly matches . + ///
+ /// The minimum capacity to ensure. + /// Whether the capacity has been changed. + public bool EnsureCapacity(int capacity) => this.CapacityUnsafe < capacity && this.SetCapacity(capacity); + + /// + /// Ensures that the capacity of this list is at least the specified .
+ /// On growth, the new capacity may exceed . + ///
+ /// The minimum capacity to ensure. + /// Whether the capacity has been changed. + public bool EnsureCapacityExponential(int capacity) + => this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)this.LengthUnsafe))); + + /// + /// Resizes the underlying array and fills with zeroes if grown. + /// + /// New size. + /// New default value. + /// Whether to skip calling destroyer function. + public void Resize(int size, in T defaultValue = default, bool skipDestroyer = false) + { + this.EnsureCapacity(size); + var old = this.LengthUnsafe; + if (old > size && !skipDestroyer && this.destroyer is not null) + { + foreach (var v in this.DataSpan[size..]) + this.destroyer(&v); + } + + this.LengthUnsafe = size; + if (old < size) + this.DataSpan[old..].Fill(defaultValue); + } + + /// + public bool Remove(in T item) + { + var index = this.IndexOf(item); + if (index == -1) + return false; + + this.RemoveAt(index); + return true; + } + + /// + public int IndexOf(in T item) + { + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + { + if (Equals(item, this.DataUnsafe[i])) + return i; + } + + return -1; + } + + /// + public void Insert(int index, in T item) + { + // Note: index == this.LengthUnsafe is okay; we're just adding to the end then + if (index < 0 || index > this.LengthUnsafe) + throw new IndexOutOfRangeException(); + + this.EnsureCapacityExponential(this.CapacityUnsafe + 1); + var num = this.LengthUnsafe - index; + Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + 1, num * sizeof(T), num * sizeof(T)); + this.DataUnsafe[index] = item; + } + + /// + public void InsertRange(int index, IEnumerable items) + { + if (items is ICollection { Count: var count }) + { + this.EnsureCapacityExponential(this.LengthUnsafe + count); + var num = this.LengthUnsafe - index; + Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + count, num * sizeof(T), num * sizeof(T)); + foreach (var item in items) + this.DataUnsafe[index++] = item; + } + else + { + foreach (var item in items) + this.Insert(index++, item); + } + } + + /// + public void InsertRange(int index, Span items) + { + this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); + var num = this.LengthUnsafe - index; + Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + items.Length, num * sizeof(T), num * sizeof(T)); + foreach (var item in items) + this.DataUnsafe[index++] = item; + } + + /// + /// Removes the element at the given index. + /// + /// The index. + /// Whether to skip calling the destroyer function. + public void RemoveAt(int index, bool skipDestroyer = false) + { + this.EnsureIndex(index); + var num = this.LengthUnsafe - index - 1; + if (!skipDestroyer) + this.destroyer?.Invoke(&this.DataUnsafe[index]); + + Buffer.MemoryCopy(this.DataUnsafe + index + 1, this.DataUnsafe + index, num * sizeof(T), num * sizeof(T)); + } + + /// + void IList.RemoveAt(int index) => this.RemoveAt(index); + + /// + void IList.RemoveAt(int index) => this.RemoveAt(index); + + /// + /// Sets the capacity exactly as requested. + /// + /// New capacity. + /// Whether the capacity has been changed. + /// If is less than . + /// If memory for the requested capacity cannot be allocated. + public bool SetCapacity(int capacity) + { + if (capacity < this.LengthUnsafe) + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, null); + + if (capacity == this.LengthUnsafe) + { + if (capacity == 0 && this.DataUnsafe is not null) + { + ImGuiNative.igMemFree(this.DataUnsafe); + this.DataUnsafe = null; + } + + return false; + } + + var oldAlloc = this.DataUnsafe; + var oldSpan = new Span(oldAlloc, this.CapacityUnsafe); + + var newAlloc = (T*)(capacity == 0 + ? null + : ImGuiNative.igMemAlloc(checked((uint)(capacity * sizeof(T))))); + + if (newAlloc is null && capacity > 0) + throw new OutOfMemoryException(); + + var newSpan = new Span(newAlloc, capacity); + + if (!oldSpan.IsEmpty && !newSpan.IsEmpty) + oldSpan[..this.LengthUnsafe].CopyTo(newSpan); +// #if DEBUG +// new Span(newAlloc + this.LengthUnsafe, sizeof(T) * (capacity - this.LengthUnsafe)).Fill(0xCC); +// #endif + + if (oldAlloc != null) + ImGuiNative.igMemFree(oldAlloc); + + this.DataUnsafe = newAlloc; + this.CapacityUnsafe = capacity; + + return true; + } + + /// + void ICollection.Add(T item) => this.Add(in item); + + /// + bool ICollection.Contains(T item) => this.Contains(in item); + + /// + void ICollection.CopyTo(Array array, int index) + { + if (index < 0) + { + throw new ArgumentOutOfRangeException( + nameof(index), + index, + $"{nameof(index)} is less than 0."); + } + + if (array.Length - index < this.LengthUnsafe) + { + throw new ArgumentException( + "The number of elements in the source ImVectorWrapper is greater than the available space from arrayIndex to the end of the destination array.", + nameof(array)); + } + + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + array.SetValue(this.DataUnsafe[i], index); + } + + /// + bool ICollection.Remove(T item) => this.Remove(in item); + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + int IList.Add(object? value) + { + this.Add(value is null ? default : (T)value); + return this.LengthUnsafe - 1; + } + + /// + bool IList.Contains(object? value) => this.Contains(value is null ? default : (T)value); + + /// + int IList.IndexOf(object? value) => this.IndexOf(value is null ? default : (T)value); + + /// + void IList.Insert(int index, object? value) => this.Insert(index, value is null ? default : (T)value); + + /// + void IList.Remove(object? value) => this.Remove(value is null ? default : (T)value); + + /// + int IList.IndexOf(T item) => this.IndexOf(in item); + + /// + void IList.Insert(int index, T item) => this.Insert(index, in item); + + private int EnsureIndex(int i) => i >= 0 && i < this.LengthUnsafe ? i : throw new IndexOutOfRangeException(); +} diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index 2fb735640..5712f419b 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -1,6 +1,5 @@ -using System; - using Serilog; +using Serilog.Core; using Serilog.Events; namespace Dalamud.Logging.Internal; @@ -33,6 +32,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Verbose(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, null, values); @@ -42,6 +42,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Verbose(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values); @@ -50,6 +51,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Debug(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, null, values); @@ -59,6 +61,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Debug(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values); @@ -67,6 +70,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Information(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, null, values); @@ -76,6 +80,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Information(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values); @@ -84,6 +89,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Warning(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, null, values); @@ -93,6 +99,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Warning(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values); @@ -101,6 +108,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Error(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, null, values); @@ -110,6 +118,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Error(Exception? exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values); @@ -118,6 +127,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Fatal(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, null, values); @@ -127,9 +137,11 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Fatal(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); + [MessageTemplateFormatMethod("messageTemplate")] private void WriteLog( LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) { diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs index b77f71d08..92dfe5dd7 100644 --- a/Dalamud/NativeFunctions.cs +++ b/Dalamud/NativeFunctions.cs @@ -137,6 +137,7 @@ internal static partial class NativeFunctions /// /// MB_* from winuser. /// + [Flags] public enum MessageBoxType : uint { /// From 473e24301d281027e7e8385ee5516d850444d3a5 Mon Sep 17 00:00:00 2001 From: Sirius902 <10891979+Sirius902@users.noreply.github.com> Date: Sun, 26 Nov 2023 14:04:38 -0800 Subject: [PATCH 319/585] Fix incorrect ImGui code (#1546) * Add missing ImGui.EndTabBar * Add more ImGui fixes --- .../Internal/Windows/PluginStatWindow.cs | 3 +- .../Windows/Settings/SettingsWindow.cs | 2 + .../Windows/StyleEditor/StyleEditorWindow.cs | 189 +++++++++--------- 3 files changed, 99 insertions(+), 95 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index 44e43fbd3..a1d93bb8c 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -44,7 +44,8 @@ internal class PluginStatWindow : Window { var pluginManager = Service.Get(); - ImGui.BeginTabBar("Stat Tabs"); + if (!ImGui.BeginTabBar("Stat Tabs")) + return; if (ImGui.BeginTabItem("Draw times")) { diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 4f77c0502..7d4489f8d 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -155,6 +155,8 @@ internal class SettingsWindow : Window ImGui.EndTabItem(); } } + + ImGui.EndTabBar(); } ImGui.SetCursorPos(windowSize - ImGuiHelpers.ScaledVector2(70)); diff --git a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs index 3a3e871b0..c202a36ce 100644 --- a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs +++ b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs @@ -211,121 +211,122 @@ public class StyleEditorWindow : Window if (ImGui.BeginTabItem(Loc.Localize("StyleEditorVariables", "Variables"))) { - ImGui.BeginChild($"ScrollingVars", ImGuiHelpers.ScaledVector2(0, -32), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground); + if (ImGui.BeginChild($"ScrollingVars", ImGuiHelpers.ScaledVector2(0, -32), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground)) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); + ImGui.SliderFloat2("WindowPadding", ref style.WindowPadding, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("FramePadding", ref style.FramePadding, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("CellPadding", ref style.CellPadding, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("ItemSpacing", ref style.ItemSpacing, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("ItemInnerSpacing", ref style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("TouchExtraPadding", ref style.TouchExtraPadding, 0.0f, 10.0f, "%.0f"); + ImGui.SliderFloat("IndentSpacing", ref style.IndentSpacing, 0.0f, 30.0f, "%.0f"); + ImGui.SliderFloat("ScrollbarSize", ref style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); + ImGui.SliderFloat("GrabMinSize", ref style.GrabMinSize, 1.0f, 20.0f, "%.0f"); + ImGui.Text("Borders"); + ImGui.SliderFloat("WindowBorderSize", ref style.WindowBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.SliderFloat("ChildBorderSize", ref style.ChildBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.SliderFloat("PopupBorderSize", ref style.PopupBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.SliderFloat("FrameBorderSize", ref style.FrameBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.SliderFloat("TabBorderSize", ref style.TabBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.Text("Rounding"); + ImGui.SliderFloat("WindowRounding", ref style.WindowRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("ChildRounding", ref style.ChildRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("FrameRounding", ref style.FrameRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("PopupRounding", ref style.PopupRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("ScrollbarRounding", ref style.ScrollbarRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("GrabRounding", ref style.GrabRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("LogSliderDeadzone", ref style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("TabRounding", ref style.TabRounding, 0.0f, 12.0f, "%.0f"); + ImGui.Text("Alignment"); + ImGui.SliderFloat2("WindowTitleAlign", ref style.WindowTitleAlign, 0.0f, 1.0f, "%.2f"); + var windowMenuButtonPosition = (int)style.WindowMenuButtonPosition + 1; + if (ImGui.Combo("WindowMenuButtonPosition", ref windowMenuButtonPosition, "None\0Left\0Right\0")) + style.WindowMenuButtonPosition = (ImGuiDir)(windowMenuButtonPosition - 1); + ImGui.SliderFloat2("ButtonTextAlign", ref style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Alignment applies when a button is larger than its text content."); + ImGui.SliderFloat2("SelectableTextAlign", ref style.SelectableTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Alignment applies when a selectable is larger than its text content."); + ImGui.SliderFloat2("DisplaySafeAreaPadding", ref style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured)."); - ImGui.SliderFloat2("WindowPadding", ref style.WindowPadding, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("FramePadding", ref style.FramePadding, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("CellPadding", ref style.CellPadding, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("ItemSpacing", ref style.ItemSpacing, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("ItemInnerSpacing", ref style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("TouchExtraPadding", ref style.TouchExtraPadding, 0.0f, 10.0f, "%.0f"); - ImGui.SliderFloat("IndentSpacing", ref style.IndentSpacing, 0.0f, 30.0f, "%.0f"); - ImGui.SliderFloat("ScrollbarSize", ref style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); - ImGui.SliderFloat("GrabMinSize", ref style.GrabMinSize, 1.0f, 20.0f, "%.0f"); - ImGui.Text("Borders"); - ImGui.SliderFloat("WindowBorderSize", ref style.WindowBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.SliderFloat("ChildBorderSize", ref style.ChildBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.SliderFloat("PopupBorderSize", ref style.PopupBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.SliderFloat("FrameBorderSize", ref style.FrameBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.SliderFloat("TabBorderSize", ref style.TabBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.Text("Rounding"); - ImGui.SliderFloat("WindowRounding", ref style.WindowRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("ChildRounding", ref style.ChildRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("FrameRounding", ref style.FrameRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("PopupRounding", ref style.PopupRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("ScrollbarRounding", ref style.ScrollbarRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("GrabRounding", ref style.GrabRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("LogSliderDeadzone", ref style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("TabRounding", ref style.TabRounding, 0.0f, 12.0f, "%.0f"); - ImGui.Text("Alignment"); - ImGui.SliderFloat2("WindowTitleAlign", ref style.WindowTitleAlign, 0.0f, 1.0f, "%.2f"); - var windowMenuButtonPosition = (int)style.WindowMenuButtonPosition + 1; - if (ImGui.Combo("WindowMenuButtonPosition", ref windowMenuButtonPosition, "None\0Left\0Right\0")) - style.WindowMenuButtonPosition = (ImGuiDir)(windowMenuButtonPosition - 1); - ImGui.SliderFloat2("ButtonTextAlign", ref style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); - ImGui.SameLine(); - ImGuiComponents.HelpMarker("Alignment applies when a button is larger than its text content."); - ImGui.SliderFloat2("SelectableTextAlign", ref style.SelectableTextAlign, 0.0f, 1.0f, "%.2f"); - ImGui.SameLine(); - ImGuiComponents.HelpMarker("Alignment applies when a selectable is larger than its text content."); - ImGui.SliderFloat2("DisplaySafeAreaPadding", ref style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured)."); - ImGui.EndTabItem(); - - ImGui.EndChild(); + ImGui.EndChild(); + } ImGui.EndTabItem(); } if (ImGui.BeginTabItem(Loc.Localize("StyleEditorColors", "Colors"))) { - ImGui.BeginChild("ScrollingColors", ImGuiHelpers.ScaledVector2(0, -30), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground); - - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); - - if (ImGui.RadioButton("Opaque", this.alphaFlags == ImGuiColorEditFlags.None)) - this.alphaFlags = ImGuiColorEditFlags.None; - ImGui.SameLine(); - if (ImGui.RadioButton("Alpha", this.alphaFlags == ImGuiColorEditFlags.AlphaPreview)) - this.alphaFlags = ImGuiColorEditFlags.AlphaPreview; - ImGui.SameLine(); - if (ImGui.RadioButton("Both", this.alphaFlags == ImGuiColorEditFlags.AlphaPreviewHalf)) - this.alphaFlags = ImGuiColorEditFlags.AlphaPreviewHalf; - ImGui.SameLine(); - - ImGuiComponents.HelpMarker( - "In the color list:\n" + - "Left-click on color square to open color picker,\n" + - "Right-click to open edit options menu."); - - foreach (var imGuiCol in Enum.GetValues()) + if (ImGui.BeginChild("ScrollingColors", ImGuiHelpers.ScaledVector2(0, -30), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground)) { - if (imGuiCol == ImGuiCol.COUNT) - continue; + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); - ImGui.PushID(imGuiCol.ToString()); + if (ImGui.RadioButton("Opaque", this.alphaFlags == ImGuiColorEditFlags.None)) + this.alphaFlags = ImGuiColorEditFlags.None; + ImGui.SameLine(); + if (ImGui.RadioButton("Alpha", this.alphaFlags == ImGuiColorEditFlags.AlphaPreview)) + this.alphaFlags = ImGuiColorEditFlags.AlphaPreview; + ImGui.SameLine(); + if (ImGui.RadioButton("Both", this.alphaFlags == ImGuiColorEditFlags.AlphaPreviewHalf)) + this.alphaFlags = ImGuiColorEditFlags.AlphaPreviewHalf; + ImGui.SameLine(); - ImGui.ColorEdit4("##color", ref style.Colors[(int)imGuiCol], ImGuiColorEditFlags.AlphaBar | this.alphaFlags); + ImGuiComponents.HelpMarker( + "In the color list:\n" + + "Left-click on color square to open color picker,\n" + + "Right-click to open edit options menu."); - ImGui.SameLine(0.0f, style.ItemInnerSpacing.X); - ImGui.TextUnformatted(imGuiCol.ToString()); - - ImGui.PopID(); - } - - ImGui.Separator(); - - foreach (var property in typeof(DalamudColors).GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - ImGui.PushID(property.Name); - - var colorVal = property.GetValue(workStyle.BuiltInColors); - if (colorVal == null) + foreach (var imGuiCol in Enum.GetValues()) { - colorVal = property.GetValue(StyleModelV1.DalamudStandard.BuiltInColors); - property.SetValue(workStyle.BuiltInColors, colorVal); + if (imGuiCol == ImGuiCol.COUNT) + continue; + + ImGui.PushID(imGuiCol.ToString()); + + ImGui.ColorEdit4("##color", ref style.Colors[(int)imGuiCol], ImGuiColorEditFlags.AlphaBar | this.alphaFlags); + + ImGui.SameLine(0.0f, style.ItemInnerSpacing.X); + ImGui.TextUnformatted(imGuiCol.ToString()); + + ImGui.PopID(); } - var color = (Vector4)colorVal; + ImGui.Separator(); - if (ImGui.ColorEdit4("##color", ref color, ImGuiColorEditFlags.AlphaBar | this.alphaFlags)) + foreach (var property in typeof(DalamudColors).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { - property.SetValue(workStyle.BuiltInColors, color); - workStyle.BuiltInColors?.Apply(); + ImGui.PushID(property.Name); + + var colorVal = property.GetValue(workStyle.BuiltInColors); + if (colorVal == null) + { + colorVal = property.GetValue(StyleModelV1.DalamudStandard.BuiltInColors); + property.SetValue(workStyle.BuiltInColors, colorVal); + } + + var color = (Vector4)colorVal; + + if (ImGui.ColorEdit4("##color", ref color, ImGuiColorEditFlags.AlphaBar | this.alphaFlags)) + { + property.SetValue(workStyle.BuiltInColors, color); + workStyle.BuiltInColors?.Apply(); + } + + ImGui.SameLine(0.0f, style.ItemInnerSpacing.X); + ImGui.TextUnformatted(property.Name); + + ImGui.PopID(); } - ImGui.SameLine(0.0f, style.ItemInnerSpacing.X); - ImGui.TextUnformatted(property.Name); - - ImGui.PopID(); + ImGui.EndChild(); } - ImGui.EndChild(); - ImGui.EndTabItem(); } From fcebd150774b4d481c70f8d967e5da4e0f8e02f0 Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 28 Nov 2023 17:04:36 +0000 Subject: [PATCH 320/585] Send Dalamud user-agent when downloading plugins (#1550) Sets the user-agent on all HappyHttp requests to `Dalamud/`, and pass `Accept: application/zip` in plugin downloads. --- Dalamud/Networking/Http/HappyHttpClient.cs | 14 +++++++++++++- Dalamud/Plugin/Internal/PluginManager.cs | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index 8459f1453..4379a698f 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -1,6 +1,9 @@ using System; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; + +using Dalamud.Utility; namespace Dalamud.Networking.Http; @@ -25,7 +28,16 @@ internal class HappyHttpClient : IDisposable, IServiceType { AutomaticDecompression = DecompressionMethods.All, ConnectCallback = this.SharedHappyEyeballsCallback.ConnectCallback, - }); + }) + { + DefaultRequestHeaders = + { + UserAgent = + { + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + }, + }, + }; } /// diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index ff6b045be..9a651c64e 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -5,6 +5,8 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -1196,7 +1198,17 @@ internal partial class PluginManager : IDisposable, IServiceType private async Task DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting) { var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; - var response = await this.happyHttpClient.SharedHttpClient.GetAsync(downloadUrl); + var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl) + { + Headers = + { + Accept = + { + new MediaTypeWithQualityHeaderValue("application/zip"), + }, + }, + }; + var response = await this.happyHttpClient.SharedHttpClient.SendAsync(request); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStreamAsync(); From b66be84b939147505a4221a39da4c4b50ec18358 Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 29 Nov 2023 06:20:16 +0900 Subject: [PATCH 321/585] Better Service dependency handling (#1535) --- .../Internal/DalamudConfiguration.cs | 2 +- Dalamud/Dalamud.cs | 2 +- .../Game/Addon/Events/AddonEventManager.cs | 2 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/Config/GameConfig.cs | 2 +- Dalamud/Game/DutyState/DutyState.cs | 2 +- .../UniversalisMarketBoardUploader.cs | 9 +- .../Game/Network/Internal/NetworkHandlers.cs | 10 +- Dalamud/Game/TargetSigScanner.cs | 2 +- Dalamud/Interface/DragDrop/DragDropManager.cs | 2 +- .../Interface/GameFonts/GameFontManager.cs | 2 +- .../Interface/Internal/DalamudInterface.cs | 106 +++++++------ .../Interface/Internal/InterfaceManager.cs | 2 +- .../Internal/Windows/ChangelogWindow.cs | 14 +- .../Internal/Windows/ConsoleWindow.cs | 5 +- .../PluginInstaller/PluginInstallerWindow.cs | 7 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 56 ++++--- Dalamud/IoC/Internal/ServiceContainer.cs | 7 +- Dalamud/Plugin/Internal/PluginManager.cs | 28 ++-- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 4 - .../Plugin/Internal/Types/PluginRepository.cs | 65 ++++---- Dalamud/Plugin/Ipc/Internal/DataShare.cs | 2 +- Dalamud/ServiceManager.cs | 132 +++++++++++++--- Dalamud/Service{T}.cs | 145 ++++++++++++++---- Dalamud/Storage/ReliableFileStorage.cs | 2 +- 25 files changed, 415 insertions(+), 197 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 35d5261da..76c8f3603 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -21,7 +21,7 @@ namespace Dalamud.Configuration.Internal; /// Class containing Dalamud settings. /// [Serializable] -[ServiceManager.Service] +[ServiceManager.ProvidedService] #pragma warning disable SA1015 [InherentDependency] // We must still have this when unloading #pragma warning restore SA1015 diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index f50a39aa3..9896b87a6 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -30,7 +30,7 @@ namespace Dalamud; /// /// The main Dalamud class containing all subsystems. /// -[ServiceManager.Service] +[ServiceManager.ProvidedService] internal sealed class Dalamud : IServiceType { #region Internals diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index a91f5437c..d8f3427ef 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Events; /// Service provider for addon event management. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class AddonEventManager : IDisposable, IServiceType { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index c7184ca11..08a2d59ef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// This class provides events for in-game addon lifecycles. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class AddonLifecycle : IDisposable, IServiceType { private static readonly ModuleLog Log = new("AddonLifecycle"); diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index ae3205abc..b82d64f24 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -12,7 +12,7 @@ namespace Dalamud.Game.Config; /// This class represents the game's configuration. ///
[InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable { private readonly GameConfigAddressResolver address = new(); diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 6dda95a66..66356033b 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -12,7 +12,7 @@ namespace Dalamud.Game.DutyState; /// This class represents the state of the currently occupied duty. ///
[InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class DutyState : IDisposable, IServiceType, IDutyState { private readonly DutyStateAddressResolver address; diff --git a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs index b3175cad3..34a255e19 100644 --- a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs +++ b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Net.Http; using System.Text; @@ -22,14 +21,14 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader private const string ApiKey = "GGD6RdSfGyRiHM5WDnAo0Nj9Nv7aC5NDhMj3BebT"; - private readonly HttpClient httpClient = Service.Get().SharedHttpClient; + private readonly HttpClient httpClient; /// /// Initializes a new instance of the class. /// - public UniversalisMarketBoardUploader() - { - } + /// An instance of . + public UniversalisMarketBoardUploader(HappyHttpClient happyHttpClient) => + this.httpClient = happyHttpClient.SharedHttpClient; /// public async Task Upload(MarketBoardItemRequest request) diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 01e92a373..76d3b5659 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -13,6 +13,7 @@ using Dalamud.Game.Network.Internal.MarketBoardUploaders; using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis; using Dalamud.Game.Network.Structures; using Dalamud.Hooking; +using Dalamud.Networking.Http; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI.Info; using Lumina.Excel.GeneratedSheets; @@ -23,7 +24,7 @@ namespace Dalamud.Game.Network.Internal; /// /// This class handles network notifications and uploading market board data. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class NetworkHandlers : IDisposable, IServiceType { private readonly IMarketBoardUploader uploader; @@ -55,9 +56,12 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType private bool disposing; [ServiceManager.ServiceConstructor] - private NetworkHandlers(GameNetwork gameNetwork, TargetSigScanner sigScanner) + private NetworkHandlers( + GameNetwork gameNetwork, + TargetSigScanner sigScanner, + HappyHttpClient happyHttpClient) { - this.uploader = new UniversalisMarketBoardUploader(); + this.uploader = new UniversalisMarketBoardUploader(happyHttpClient); this.addressResolver = new NetworkHandlersAddressResolver(); this.addressResolver.Setup(sigScanner); diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index 9242c5e83..35c82562e 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -11,7 +11,7 @@ namespace Dalamud.Game; ///
[PluginInterface] [InterfaceVersion("1.0")] -[ServiceManager.Service] +[ServiceManager.ProvidedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index e8641035f..151ef28a0 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -15,7 +15,7 @@ namespace Dalamud.Interface.DragDrop; /// and can be used to create ImGui drag and drop sources and targets for those external events. ///
[PluginInterface] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index 71661682d..b3454e085 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -22,7 +22,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Loads game font for use in ImGui. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal class GameFontManager : IServiceType { private static readonly string?[] FontNames = diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 816352d80..18ab538c4 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1,7 +1,5 @@ -using System; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; using System.Numerics; using System.Reflection; @@ -9,6 +7,7 @@ using System.Runtime.InteropServices; using CheapLoc; using Dalamud.Configuration.Internal; +using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Game.Internal; @@ -25,14 +24,14 @@ using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; -using ImGuiScene; + using ImPlotNET; using PInvoke; using Serilog.Events; @@ -48,9 +47,11 @@ internal class DalamudInterface : IDisposable, IServiceType private const float CreditsDarkeningMaxAlpha = 0.8f; private static readonly ModuleLog Log = new("DUI"); - + + private readonly Dalamud dalamud; private readonly DalamudConfiguration configuration; - + private readonly InterfaceManager interfaceManager; + private readonly ChangelogWindow changelogWindow; private readonly ColorDemoWindow colorDemoWindow; private readonly ComponentDemoWindow componentDemoWindow; @@ -92,11 +93,16 @@ internal class DalamudInterface : IDisposable, IServiceType DalamudConfiguration configuration, InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, PluginImageCache pluginImageCache, - Branding branding) + Branding branding, + Game.Framework framework, + ClientState clientState, + TitleScreenMenu titleScreenMenu, + GameGui gameGui) { + this.dalamud = dalamud; this.configuration = configuration; + this.interfaceManager = interfaceManagerWithScene.Manager; - var interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; @@ -104,13 +110,20 @@ internal class DalamudInterface : IDisposable, IServiceType this.dataWindow = new DataWindow() { IsOpen = false }; this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow() { IsOpen = false }; this.imeWindow = new ImeWindow() { IsOpen = false }; - this.consoleWindow = new ConsoleWindow() { IsOpen = configuration.LogOpenAtStartup }; + this.consoleWindow = new ConsoleWindow(configuration) { IsOpen = configuration.LogOpenAtStartup }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; - this.pluginWindow = new PluginInstallerWindow(pluginImageCache) { IsOpen = false }; + this.pluginWindow = new PluginInstallerWindow(pluginImageCache, configuration) { IsOpen = false }; this.settingsWindow = new SettingsWindow() { IsOpen = false }; this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; - this.titleScreenMenuWindow = new TitleScreenMenuWindow() { IsOpen = false }; + this.titleScreenMenuWindow = new TitleScreenMenuWindow( + clientState, + dalamud, + configuration, + framework, + gameGui, + this.interfaceManager, + titleScreenMenu) { IsOpen = false }; this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; @@ -136,7 +149,7 @@ internal class DalamudInterface : IDisposable, IServiceType ImGuiManagedAsserts.AssertsEnabled = configuration.AssertsEnabledAtStartup; this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup; - interfaceManager.Draw += this.OnDraw; + this.interfaceManager.Draw += this.OnDraw; var tsm = Service.Get(); tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, () => this.OpenPluginInstaller()); @@ -173,7 +186,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// public void Dispose() { - Service.Get().Draw -= this.OnDraw; + this.interfaceManager.Draw -= this.OnDraw; this.WindowSystem.RemoveAllWindows(); @@ -356,7 +369,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// Toggles the . ///
/// The data kind to switch to after opening. - public void ToggleDataWindow(string dataKind = null) + public void ToggleDataWindow(string? dataKind = null) { this.dataWindow.Toggle(); if (dataKind != null && this.dataWindow.IsOpen) @@ -378,7 +391,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// /// Toggles the . /// - public void ToggleIMEWindow() => this.imeWindow.Toggle(); + public void ToggleImeWindow() => this.imeWindow.Toggle(); /// /// Toggles the . @@ -504,7 +517,8 @@ internal class DalamudInterface : IDisposable, IServiceType private void DrawCreditsDarkeningAnimation() { - using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding | ImGuiStyleVar.WindowBorderSize, 0f); + using var style1 = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f); + using var style2 = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 0f); using var color = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 0)); ImGui.SetNextWindowPos(new Vector2(0, 0)); @@ -579,18 +593,16 @@ internal class DalamudInterface : IDisposable, IServiceType { if (ImGui.BeginMainMenuBar()) { - var dalamud = Service.Get(); - var configuration = Service.Get(); var pluginManager = Service.Get(); if (ImGui.BeginMenu("Dalamud")) { ImGui.MenuItem("Draw dev menu", string.Empty, ref this.isImGuiDrawDevMenu); - var devBarAtStartup = configuration.DevBarOpenAtStartup; + var devBarAtStartup = this.configuration.DevBarOpenAtStartup; if (ImGui.MenuItem("Draw dev menu at startup", string.Empty, ref devBarAtStartup)) { - configuration.DevBarOpenAtStartup ^= true; - configuration.QueueSave(); + this.configuration.DevBarOpenAtStartup ^= true; + this.configuration.QueueSave(); } ImGui.Separator(); @@ -607,25 +619,25 @@ internal class DalamudInterface : IDisposable, IServiceType if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, EntryPoint.LogLevelSwitch.MinimumLevel == logLevel)) { EntryPoint.LogLevelSwitch.MinimumLevel = logLevel; - configuration.LogLevel = logLevel; - configuration.QueueSave(); + this.configuration.LogLevel = logLevel; + this.configuration.QueueSave(); } } ImGui.EndMenu(); } - var logSynchronously = configuration.LogSynchronously; + var logSynchronously = this.configuration.LogSynchronously; if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously)) { - configuration.LogSynchronously = logSynchronously; - configuration.QueueSave(); + this.configuration.LogSynchronously = logSynchronously; + this.configuration.QueueSave(); EntryPoint.InitLogging( - dalamud.StartInfo.LogPath!, - dalamud.StartInfo.BootShowConsole, - configuration.LogSynchronously, - dalamud.StartInfo.LogName); + this.dalamud.StartInfo.LogPath!, + this.dalamud.StartInfo.BootShowConsole, + this.configuration.LogSynchronously, + this.dalamud.StartInfo.LogName); } var antiDebug = Service.Get(); @@ -637,8 +649,8 @@ internal class DalamudInterface : IDisposable, IServiceType else antiDebug.Disable(); - configuration.IsAntiAntiDebugEnabled = newEnabled; - configuration.QueueSave(); + this.configuration.IsAntiAntiDebugEnabled = newEnabled; + this.configuration.QueueSave(); } ImGui.Separator(); @@ -730,10 +742,10 @@ internal class DalamudInterface : IDisposable, IServiceType } } - if (ImGui.MenuItem("Report crashes at shutdown", null, configuration.ReportShutdownCrashes)) + if (ImGui.MenuItem("Report crashes at shutdown", null, this.configuration.ReportShutdownCrashes)) { - configuration.ReportShutdownCrashes = !configuration.ReportShutdownCrashes; - configuration.QueueSave(); + this.configuration.ReportShutdownCrashes = !this.configuration.ReportShutdownCrashes; + this.configuration.QueueSave(); } ImGui.Separator(); @@ -744,7 +756,7 @@ internal class DalamudInterface : IDisposable, IServiceType } ImGui.MenuItem(Util.AssemblyVersion, false); - ImGui.MenuItem(dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); + ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); ImGui.MenuItem($"D: {Util.GetGitHash()}[{Util.GetGitCommitCount()}] CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.Interop.Resolver.Version}]", false); ImGui.MenuItem($"CLR: {Environment.Version}", false); @@ -766,10 +778,10 @@ internal class DalamudInterface : IDisposable, IServiceType ImGuiManagedAsserts.AssertsEnabled = val; } - if (ImGui.MenuItem("Enable asserts at startup", null, configuration.AssertsEnabledAtStartup)) + if (ImGui.MenuItem("Enable asserts at startup", null, this.configuration.AssertsEnabledAtStartup)) { - configuration.AssertsEnabledAtStartup = !configuration.AssertsEnabledAtStartup; - configuration.QueueSave(); + this.configuration.AssertsEnabledAtStartup = !this.configuration.AssertsEnabledAtStartup; + this.configuration.QueueSave(); } if (ImGui.MenuItem("Clear focus")) @@ -779,7 +791,7 @@ internal class DalamudInterface : IDisposable, IServiceType if (ImGui.MenuItem("Clear stacks")) { - Service.Get().ClearStacks(); + this.interfaceManager.ClearStacks(); } if (ImGui.MenuItem("Dump style")) @@ -792,7 +804,7 @@ internal class DalamudInterface : IDisposable, IServiceType { if (propertyInfo.PropertyType == typeof(Vector2)) { - var vec2 = (Vector2)propertyInfo.GetValue(style); + var vec2 = (Vector2)propertyInfo.GetValue(style)!; info += $"{propertyInfo.Name} = new Vector2({vec2.X.ToString(enCulture)}f, {vec2.Y.ToString(enCulture)}f),\n"; } else @@ -815,9 +827,9 @@ internal class DalamudInterface : IDisposable, IServiceType Log.Information(info); } - if (ImGui.MenuItem("Show dev bar info", null, configuration.ShowDevBarInfo)) + if (ImGui.MenuItem("Show dev bar info", null, this.configuration.ShowDevBarInfo)) { - configuration.ShowDevBarInfo = !configuration.ShowDevBarInfo; + this.configuration.ShowDevBarInfo = !this.configuration.ShowDevBarInfo; } ImGui.EndMenu(); @@ -827,7 +839,7 @@ internal class DalamudInterface : IDisposable, IServiceType { if (ImGui.MenuItem("Replace ExceptionHandler")) { - dalamud.ReplaceExceptionHandler(); + this.dalamud.ReplaceExceptionHandler(); } ImGui.EndMenu(); @@ -922,7 +934,7 @@ internal class DalamudInterface : IDisposable, IServiceType if (Service.Get().GameUiHidden) ImGui.BeginMenu("UI is hidden...", false); - if (configuration.ShowDevBarInfo) + if (this.configuration.ShowDevBarInfo) { ImGui.PushFont(InterfaceManager.MonoFont); @@ -931,9 +943,9 @@ internal class DalamudInterface : IDisposable, IServiceType ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false); - var videoMem = Service.Get().GetD3dMemoryInfo(); + var videoMem = this.interfaceManager.GetD3dMemoryInfo(); ImGui.BeginMenu( - !videoMem.HasValue ? $"V:???" : $"V:{Util.FormatBytes(videoMem.Value.Used)}", + !videoMem.HasValue ? "V:???" : $"V:{Util.FormatBytes(videoMem.Value.Used)}", false); ImGui.PopFont(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 9de87c6e3..c666a96a9 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1285,7 +1285,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// Represents an instance of InstanceManager with scene ready for use. /// - [ServiceManager.Service] + [ServiceManager.ProvidedService] public class InterfaceManagerWithScene : IServiceType { /// diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 4d1a8b5f0..e3f318223 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -67,7 +67,7 @@ internal sealed class ChangelogWindow : Window, IDisposable // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - this.MakeFont(); + Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); } private enum State @@ -98,7 +98,7 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - this.MakeFont(); + this.MakeFont(Service.Get()); this.state = State.WindowFadeIn; this.windowFade.Reset(); @@ -379,12 +379,6 @@ internal sealed class ChangelogWindow : Window, IDisposable this.logoTexture.Dispose(); } - private void MakeFont() - { - if (this.bannerFont == null) - { - var gfm = Service.Get(); - this.bannerFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); - } - } + private void MakeFont(GameFontManager gfm) => + this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 63045ed36..b285520d4 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -56,11 +56,10 @@ internal class ConsoleWindow : Window, IDisposable /// /// Initializes a new instance of the class. /// - public ConsoleWindow() + /// An instance of . + public ConsoleWindow(DalamudConfiguration configuration) : base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { - var configuration = Service.Get(); - this.autoScroll = configuration.LogAutoScroll; this.autoOpen = configuration.LogOpenAtStartup; SerilogEventSink.Instance.LogLine += this.OnLogLine; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 687526c9a..4233c169b 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -69,7 +69,7 @@ internal class PluginInstallerWindow : Window, IDisposable private string[] testerImagePaths = new string[5]; private string testerIconPath = string.Empty; - private IDalamudTextureWrap?[] testerImages; + private IDalamudTextureWrap?[]? testerImages; private IDalamudTextureWrap? testerIcon; private bool testerError = false; @@ -132,9 +132,10 @@ internal class PluginInstallerWindow : Window, IDisposable /// Initializes a new instance of the class. /// /// An instance of class. - public PluginInstallerWindow(PluginImageCache imageCache) + /// An instance of . + public PluginInstallerWindow(PluginImageCache imageCache, DalamudConfiguration configuration) : base( - Locs.WindowTitle + (Service.Get().DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", + Locs.WindowTitle + (configuration.DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar) { this.IsOpen = true; diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index a4ad62f4f..4034695e5 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -12,8 +12,8 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; + using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows; @@ -25,6 +25,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable private const float TargetFontSizePt = 18f; private const float TargetFontSizePx = TargetFontSizePt * 4 / 3; + private readonly ClientState clientState; + private readonly DalamudConfiguration configuration; + private readonly Framework framework; + private readonly GameGui gameGui; + private readonly TitleScreenMenu titleScreenMenu; + private readonly IDalamudTextureWrap shadeTexture; private readonly Dictionary shadeEasings = new(); @@ -39,12 +45,32 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// /// Initializes a new instance of the class. /// - public TitleScreenMenuWindow() + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + public TitleScreenMenuWindow( + ClientState clientState, + Dalamud dalamud, + DalamudConfiguration configuration, + Framework framework, + GameGui gameGui, + InterfaceManager interfaceManager, + TitleScreenMenu titleScreenMenu) : base( "TitleScreenMenuOverlay", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) { + this.clientState = clientState; + this.configuration = configuration; + this.framework = framework; + this.gameGui = gameGui; + this.titleScreenMenu = titleScreenMenu; + this.IsOpen = true; this.DisableWindowSounds = true; this.ForceMainWindow = true; @@ -53,17 +79,13 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; - var dalamud = Service.Get(); - var interfaceManager = Service.Get(); - var shadeTex = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmShade.png")); this.shadeTexture = shadeTex ?? throw new Exception("Could not load TSM background texture."); - var framework = Service.Get(); framework.Update += this.FrameworkOnUpdate; } - + private enum State { Hide, @@ -95,8 +117,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable public void Dispose() { this.shadeTexture.Dispose(); - var framework = Service.Get(); - framework.Update -= this.FrameworkOnUpdate; + this.framework.Update -= this.FrameworkOnUpdate; } /// @@ -106,9 +127,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable return; var scale = ImGui.GetIO().FontGlobalScale; - var entries = Service.Get().Entries - .OrderByDescending(x => x.IsInternal) - .ToList(); + var entries = this.titleScreenMenu.Entries.OrderByDescending(x => x.IsInternal).ToList(); switch (this.state) { @@ -369,17 +388,14 @@ internal class TitleScreenMenuWindow : Window, IDisposable private void FrameworkOnUpdate(IFramework framework) { - var clientState = Service.Get(); - this.IsOpen = !clientState.IsLoggedIn; + this.IsOpen = !this.clientState.IsLoggedIn; - var configuration = Service.Get(); - if (!configuration.ShowTsm) + if (!this.configuration.ShowTsm) this.IsOpen = false; - var gameGui = Service.Get(); - var charaSelect = gameGui.GetAddonByName("CharaSelect", 1); - var charaMake = gameGui.GetAddonByName("CharaMake", 1); - var titleDcWorldMap = gameGui.GetAddonByName("TitleDCWorldMap", 1); + var charaSelect = this.gameGui.GetAddonByName("CharaSelect", 1); + var charaMake = this.gameGui.GetAddonByName("CharaMake", 1); + var titleDcWorldMap = this.gameGui.GetAddonByName("TitleDCWorldMap", 1); if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index ce7ce25a1..5b141979e 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -16,7 +15,7 @@ namespace Dalamud.IoC.Internal; /// This is only used to resolve dependencies for plugins. /// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// -[ServiceManager.Service] +[ServiceManager.ProvidedService] internal class ServiceContainer : IServiceProvider, IServiceType { private static readonly ModuleLog Log = new("SERVICECONTAINER"); @@ -228,7 +227,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; - if (serviceType.GetCustomAttribute() != null) + if (serviceType.GetCustomAttribute() != null) { if (scope == null) { @@ -299,7 +298,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType var contains = types.Any(x => x.IsAssignableTo(type)); // Scoped services are created on-demand - return contains || type.GetCustomAttribute() != null; + return contains || type.GetCustomAttribute() != null; } var parameters = ctor.GetParameters(); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 9a651c64e..363d01f26 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -40,7 +39,7 @@ namespace Dalamud.Plugin.Internal; /// Class responsible for loading and unloading plugins. /// NOTE: ALL plugin exposed services are marked as dependencies for PluginManager in Service{T}. ///
-[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] #pragma warning disable SA1015 // DalamudTextureWrap registers textures to dispose with IM @@ -85,6 +84,9 @@ internal partial class PluginManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly HappyHttpClient happyHttpClient = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly ChatGui chatGui = Service.Get(); + static PluginManager() { DalamudApiLevel = typeof(PluginManager).Assembly.GetName().Version!.Major; @@ -131,12 +133,13 @@ internal partial class PluginManager : IDisposable, IServiceType throw new InvalidDataException("Couldn't deserialize banned plugins manifest."); } - this.openInstallerWindowPluginChangelogsLink = Service.Get().AddChatLinkHandler("Dalamud", 1003, (_, _) => + this.openInstallerWindowPluginChangelogsLink = this.chatGui.AddChatLinkHandler("Dalamud", 1003, (_, _) => { Service.GetNullable()?.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.Changelogs); }); - this.configuration.PluginTestingOptIns ??= new List(); + this.configuration.PluginTestingOptIns ??= new(); + this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient); this.ApplyPatches(); } @@ -199,6 +202,11 @@ internal partial class PluginManager : IDisposable, IServiceType } } + /// + /// Gets the main repository. + /// + public PluginRepository MainRepo { get; } + /// /// Gets a list of all plugin repositories. The main repo should always be first. /// @@ -284,11 +292,9 @@ internal partial class PluginManager : IDisposable, IServiceType /// The header text to send to chat prior to any update info. public void PrintUpdatedPlugins(List? updateMetadata, string header) { - var chatGui = Service.Get(); - if (updateMetadata is { Count: > 0 }) { - chatGui.Print(new XivChatEntry + this.chatGui.Print(new XivChatEntry { Message = new SeString(new List() { @@ -307,11 +313,11 @@ internal partial class PluginManager : IDisposable, IServiceType { if (metadata.Status == PluginUpdateStatus.StatusKind.Success) { - chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); + this.chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); } else { - chatGui.Print(new XivChatEntry + this.chatGui.Print(new XivChatEntry { Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version, PluginUpdateStatus.LocalizeUpdateStatusKind(metadata.Status)), Type = XivChatType.Urgent, @@ -407,10 +413,10 @@ internal partial class PluginManager : IDisposable, IServiceType /// A representing the asynchronous operation. public async Task SetPluginReposFromConfigAsync(bool notify) { - var repos = new List() { PluginRepository.MainRepo }; + var repos = new List { this.MainRepo }; repos.AddRange(this.configuration.ThirdRepoList .Where(repo => repo.IsEnabled) - .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled))); + .Select(repo => new PluginRepository(this.happyHttpClient, repo.Url, repo.IsEnabled))); this.Repos = repos; await this.ReloadPluginMastersAsync(notify); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 5d132fd9c..91f1625a7 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -267,10 +267,6 @@ internal class LocalPlugin : IDisposable var pluginManager = await Service.GetAsync(); var dalamud = await Service.GetAsync(); - // UiBuilder constructor requires the following two. - await Service.GetAsync(); - await Service.GetAsync(); - if (this.manifest.LoadRequiredState == 0) _ = await Service.GetAsync(); diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index 3bf67ecd7..18c528910 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -28,47 +28,44 @@ internal class PluginRepository private static readonly ModuleLog Log = new("PLUGINR"); - private static readonly HttpClient HttpClient = new(new SocketsHttpHandler - { - AutomaticDecompression = DecompressionMethods.All, - ConnectCallback = Service.Get().SharedHappyEyeballsCallback.ConnectCallback, - }) - { - Timeout = TimeSpan.FromSeconds(20), - DefaultRequestHeaders = - { - Accept = - { - new MediaTypeWithQualityHeaderValue("application/json"), - }, - CacheControl = new CacheControlHeaderValue - { - NoCache = true, - }, - UserAgent = - { - new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), - }, - }, - }; + private readonly HttpClient httpClient; /// /// Initializes a new instance of the class. /// + /// An instance of . /// The plugin master URL. /// Whether the plugin repo is enabled. - public PluginRepository(string pluginMasterUrl, bool isEnabled) + public PluginRepository(HappyHttpClient happyHttpClient, string pluginMasterUrl, bool isEnabled) { + this.httpClient = new(new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = happyHttpClient.SharedHappyEyeballsCallback.ConnectCallback, + }) + { + Timeout = TimeSpan.FromSeconds(20), + DefaultRequestHeaders = + { + Accept = + { + new MediaTypeWithQualityHeaderValue("application/json"), + }, + CacheControl = new CacheControlHeaderValue + { + NoCache = true, + }, + UserAgent = + { + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + }, + }, + }; this.PluginMasterUrl = pluginMasterUrl; this.IsThirdParty = pluginMasterUrl != MainRepoUrl; this.IsEnabled = isEnabled; } - /// - /// Gets a new instance of the class for the main repo. - /// - public static PluginRepository MainRepo => new(MainRepoUrl, true); - /// /// Gets the pluginmaster.json URL. /// @@ -94,6 +91,14 @@ internal class PluginRepository ///
public PluginRepositoryState State { get; private set; } + /// + /// Gets a new instance of the class for the main repo. + /// + /// An instance of . + /// The new instance of main repository. + public static PluginRepository CreateMainRepo(HappyHttpClient happyHttpClient) => + new(happyHttpClient, MainRepoUrl, true); + /// /// Reload the plugin master asynchronously in a task. /// @@ -107,7 +112,7 @@ internal class PluginRepository { Log.Information($"Fetching repo: {this.PluginMasterUrl}"); - using var response = await HttpClient.GetAsync(this.PluginMasterUrl); + using var response = await this.httpClient.GetAsync(this.PluginMasterUrl); response.EnsureSuccessStatusCode(); var data = await response.Content.ReadAsStringAsync(); diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index 5d0faabda..a3e314b80 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -13,7 +13,7 @@ namespace Dalamud.Plugin.Ipc.Internal; /// /// This class facilitates sharing data-references of standard types between plugins without using more expensive IPC. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal class DataShare : IServiceType { private readonly Dictionary caches = new(); diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 453fa3530..46a6ba509 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading; @@ -29,9 +30,17 @@ internal static class ServiceManager ///
public static readonly ModuleLog Log = new("SVC"); - private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); +#if DEBUG + /// + /// Marks which service constructor the current thread's in. For use from only. + /// + internal static readonly ThreadLocal CurrentConstructorServiceType = new(); + [SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Debugging purposes")] private static readonly List LoadedServices = new(); +#endif + + private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); private static ManualResetEvent unloadResetEvent = new(false); @@ -86,21 +95,34 @@ internal static class ServiceManager /// Instance of . public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner) { +#if DEBUG lock (LoadedServices) { - void ProvideService(T service) where T : IServiceType - { - Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute"); - Service.Provide(service); - LoadedServices.Add(typeof(T)); - } - ProvideService(dalamud); ProvideService(fs); ProvideService(configuration); ProvideService(new ServiceContainer()); ProvideService(scanner); } + + return; + + void ProvideService(T service) where T : IServiceType + { + Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute"); + Service.Provide(service); + LoadedServices.Add(typeof(T)); + } +#else + ProvideService(dalamud); + ProvideService(fs); + ProvideService(configuration); + ProvideService(new ServiceContainer()); + ProvideService(scanner); + return; + + void ProvideService(T service) where T : IServiceType => Service.Provide(service); +#endif } /// @@ -171,7 +193,22 @@ internal static class ServiceManager { try { - await Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); + var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); + while (await Task.WhenAny(whenBlockingComplete, Task.Delay(30000)) != whenBlockingComplete) + { + if (NativeFunctions.MessageBoxW( + IntPtr.Zero, + "Dalamud is taking a long time to load. Would you like to continue without Dalamud?\n" + + "This can be caused by a faulty plugin, or a bug in Dalamud.", + "Dalamud", + NativeFunctions.MessageBoxType.IconWarning | NativeFunctions.MessageBoxType.YesNo) == 6) + { + throw new TimeoutException( + "Failed to load services in the given time limit, " + + "and the user chose to continue without Dalamud."); + } + } + BlockingServicesLoadedTaskCompletionSource.SetResult(); Timings.Event("BlockingServices Initialized"); } @@ -215,13 +252,14 @@ internal static class ServiceManager tasks.Add((Task)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( - "StartLoader", + nameof(Service.StartLoader), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, null)); servicesToLoad.Remove(serviceType); +#if DEBUG tasks.Add(tasks.Last().ContinueWith(task => { if (task.IsFaulted) @@ -231,6 +269,7 @@ internal static class ServiceManager LoadedServices.Add(serviceType); } })); +#endif } if (!tasks.Any()) @@ -350,10 +389,12 @@ internal static class ServiceManager null); } +#if DEBUG lock (LoadedServices) { LoadedServices.Clear(); } +#endif unloadResetEvent.Set(); } @@ -373,7 +414,7 @@ internal static class ServiceManager /// The type of service this type is. public static ServiceKind GetServiceKind(this Type type) { - var attr = type.GetCustomAttribute(true)?.GetType(); + var attr = type.GetCustomAttribute(true)?.GetType(); if (attr == null) return ServiceKind.None; @@ -381,13 +422,13 @@ internal static class ServiceManager type.IsAssignableTo(typeof(IServiceType)), "Service did not inherit from IServiceType"); - if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService))) + if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedServiceAttribute))) return ServiceKind.BlockingEarlyLoadedService; - if (attr.IsAssignableTo(typeof(EarlyLoadedService))) + if (attr.IsAssignableTo(typeof(EarlyLoadedServiceAttribute))) return ServiceKind.EarlyLoadedService; - if (attr.IsAssignableTo(typeof(ScopedService))) + if (attr.IsAssignableTo(typeof(ScopedServiceAttribute))) return ServiceKind.ScopedService; return ServiceKind.ProvidedService; @@ -414,16 +455,57 @@ internal static class ServiceManager /// Indicates that the class is a service. /// [AttributeUsage(AttributeTargets.Class)] - public class Service : Attribute + public abstract class ServiceAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// The kind of the service. + protected ServiceAttribute(ServiceKind kind) => this.Kind = kind; + + /// + /// Gets the kind of the service. + /// + public ServiceKind Kind { get; } + } + + /// + /// Indicates that the class is a service, that is provided by some other source. + /// + [AttributeUsage(AttributeTargets.Class)] + public class ProvidedServiceAttribute : ServiceAttribute + { + /// + /// Initializes a new instance of the class. + /// + public ProvidedServiceAttribute() + : base(ServiceKind.ProvidedService) + { + } } /// /// Indicates that the class is a service, and will be instantiated automatically on startup. /// [AttributeUsage(AttributeTargets.Class)] - public class EarlyLoadedService : Service + public class EarlyLoadedServiceAttribute : ServiceAttribute { + /// + /// Initializes a new instance of the class. + /// + public EarlyLoadedServiceAttribute() + : this(ServiceKind.EarlyLoadedService) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The service kind. + protected EarlyLoadedServiceAttribute(ServiceKind kind) + : base(kind) + { + } } /// @@ -431,8 +513,15 @@ internal static class ServiceManager /// blocking game main thread until it completes. /// [AttributeUsage(AttributeTargets.Class)] - public class BlockingEarlyLoadedService : EarlyLoadedService + public class BlockingEarlyLoadedServiceAttribute : EarlyLoadedServiceAttribute { + /// + /// Initializes a new instance of the class. + /// + public BlockingEarlyLoadedServiceAttribute() + : base(ServiceKind.BlockingEarlyLoadedService) + { + } } /// @@ -440,8 +529,15 @@ internal static class ServiceManager /// service scope, and that it cannot be created outside of a scope. /// [AttributeUsage(AttributeTargets.Class)] - public class ScopedService : Service + public class ScopedServiceAttribute : ServiceAttribute { + /// + /// Initializes a new instance of the class. + /// + public ScopedServiceAttribute() + : base(ServiceKind.ScopedService) + { + } } /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index b609c9082..9c7f0411d 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -1,6 +1,5 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -20,17 +19,26 @@ namespace Dalamud; /// Only used internally within Dalamud, if plugins need access to things it should be _only_ via DI. /// /// The class you want to store in the service locator. +[SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")] internal static class Service where T : IServiceType { + private static readonly ServiceManager.ServiceAttribute ServiceAttribute; private static TaskCompletionSource instanceTcs = new(); + private static List? dependencyServices; static Service() { - var exposeToPlugins = typeof(T).GetCustomAttribute() != null; + var type = typeof(T); + ServiceAttribute = + type.GetCustomAttribute(true) + ?? throw new InvalidOperationException( + $"{nameof(T)} is missing {nameof(ServiceManager.ServiceAttribute)} annotations."); + + var exposeToPlugins = type.GetCustomAttribute() != null; if (exposeToPlugins) - ServiceManager.Log.Debug("Service<{0}>: Static ctor called; will be exposed to plugins", typeof(T).Name); + ServiceManager.Log.Debug("Service<{0}>: Static ctor called; will be exposed to plugins", type.Name); else - ServiceManager.Log.Debug("Service<{0}>: Static ctor called", typeof(T).Name); + ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name); if (exposeToPlugins) Service.Get().RegisterSingleton(instanceTcs.Task); @@ -63,8 +71,8 @@ internal static class Service where T : IServiceType /// Object to set. public static void Provide(T obj) { - instanceTcs.SetResult(obj); ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name); + instanceTcs.SetResult(obj); } /// @@ -83,6 +91,21 @@ internal static class Service where T : IServiceType /// The object. public static T Get() { +#if DEBUG + if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService + && ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType) + { + var deps = ServiceHelpers.GetDependencies(currentServiceType); + if (!deps.Contains(typeof(T))) + { + throw new InvalidOperationException( + $"Calling {nameof(Service)}<{typeof(T)}>.{nameof(Get)} which is not one of the" + + $" dependency services is forbidden from the service constructor of {currentServiceType}." + + $" This has a high chance of introducing hard-to-debug hangs."); + } + } +#endif + if (!instanceTcs.Task.IsCompleted) instanceTcs.Task.Wait(); return instanceTcs.Task.Result; @@ -116,12 +139,16 @@ internal static class Service where T : IServiceType } /// - /// Gets an enumerable containing Service<T>s that are required for this Service to initialize without blocking. + /// Gets an enumerable containing s that are required for this Service to initialize + /// without blocking. /// /// List of dependency services. [UsedImplicitly] public static List GetDependencyServices() { + if (dependencyServices is not null) + return dependencyServices; + var res = new List(); ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name); @@ -189,19 +216,42 @@ internal static class Service where T : IServiceType ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name); } - return res - .Distinct() - .ToList(); + var deps = res + .Distinct() + .ToList(); + if (typeof(T).GetCustomAttribute() is not null) + { + var offenders = deps.Where( + x => x.GetCustomAttribute(true)!.Kind + is not ServiceManager.ServiceKind.BlockingEarlyLoadedService + and not ServiceManager.ServiceKind.ProvidedService) + .ToArray(); + if (offenders.Any()) + { + ServiceManager.Log.Error( + "{me} is a {bels}; it can only depend on {bels} and {ps}.\nOffending dependencies:\n{offenders}", + typeof(T), + nameof(ServiceManager.BlockingEarlyLoadedServiceAttribute), + nameof(ServiceManager.BlockingEarlyLoadedServiceAttribute), + nameof(ServiceManager.ProvidedServiceAttribute), + string.Join("\n", offenders.Select(x => $"\t* {x.Name}"))); + } + } + + return dependencyServices = deps; } - [UsedImplicitly] - private static Task StartLoader() + /// + /// Starts the service loader. Only to be called from . + /// + /// The loader task. + internal static Task StartLoader() { if (instanceTcs.Task.IsCompleted) throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed."); - var attr = typeof(T).GetCustomAttribute(true)?.GetType(); - if (attr?.IsAssignableTo(typeof(ServiceManager.EarlyLoadedService)) != true) + var attr = ServiceAttribute.GetType(); + if (attr.IsAssignableTo(typeof(ServiceManager.EarlyLoadedServiceAttribute)) != true) throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService"); return Task.Run(Timings.AttachTimingHandle(async () => @@ -212,6 +262,7 @@ internal static class Service where T : IServiceType var instance = await ConstructObject(); instanceTcs.SetResult(instance); + List? tasks = null; foreach (var method in typeof(T).GetMethods( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { @@ -221,9 +272,24 @@ internal static class Service where T : IServiceType ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name); var args = await Task.WhenAll(method.GetParameters().Select( x => ResolveServiceFromTypeAsync(x.ParameterType))); - method.Invoke(instance, args); + try + { + if (method.Invoke(instance, args) is Task task) + { + tasks ??= new(); + tasks.Add(task); + } + } + catch (Exception e) + { + tasks ??= new(); + tasks.Add(Task.FromException(e)); + } } + if (tasks is not null) + await Task.WhenAll(tasks); + ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name); return instance; } @@ -303,7 +369,19 @@ internal static class Service where T : IServiceType ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType))); using (Timings.Start($"{typeof(T).Name} Construct")) { +#if DEBUG + ServiceManager.CurrentConstructorServiceType.Value = typeof(Service); + try + { + return (T)ctor.Invoke(args)!; + } + finally + { + ServiceManager.CurrentConstructorServiceType.Value = null; + } +#else return (T)ctor.Invoke(args)!; +#endif } } @@ -328,30 +406,43 @@ internal static class Service where T : IServiceType internal static class ServiceHelpers { /// - /// Get a list of dependencies for a service. Only accepts Service<T> types. - /// These are returned as Service<T> types. + /// Get a list of dependencies for a service. Only accepts types. + /// These are returned as types. /// /// The dependencies for this service. /// A list of dependencies. public static List GetDependencies(Type serviceType) { +#if DEBUG + if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>)) + { + throw new ArgumentException( + $"Expected an instance of {nameof(Service)}<>", + nameof(serviceType)); + } +#endif + return (List)serviceType.InvokeMember( - "GetDependencyServices", - BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, - null, - null, - null) ?? new List(); + nameof(Service.GetDependencyServices), + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, + null, + null) ?? new List(); } /// - /// Get the Service<T> type for a given service type. + /// Get the type for a given service type. /// This will throw if the service type is not a valid service. /// - /// The type to obtain a Service<T> for. - /// The Service<T>. + /// The type to obtain a for. + /// The . public static Type GetAsService(Type type) { - return typeof(Service<>) - .MakeGenericType(type); +#if DEBUG + if (!type.IsAssignableTo(typeof(IServiceType))) + throw new ArgumentException($"Expected an instance of {nameof(IServiceType)}", nameof(type)); +#endif + + return typeof(Service<>).MakeGenericType(type); } } diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 9feb17c0d..a013e95b5 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -21,7 +21,7 @@ namespace Dalamud.Storage; /// /// This is not an early-loaded service, as it is needed before they are initialized. /// -[ServiceManager.Service] +[ServiceManager.ProvidedService] public class ReliableFileStorage : IServiceType, IDisposable { private static readonly ModuleLog Log = new("VFS"); From 01153a24805fb31b9db59dd7ac85d4588c86f636 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 21 Nov 2023 13:49:00 +0900 Subject: [PATCH 322/585] Add DisposeSafety --- Dalamud/Utility/DisposeSafety.cs | 392 +++++++++++++++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 Dalamud/Utility/DisposeSafety.cs diff --git a/Dalamud/Utility/DisposeSafety.cs b/Dalamud/Utility/DisposeSafety.cs new file mode 100644 index 000000000..909c4e932 --- /dev/null +++ b/Dalamud/Utility/DisposeSafety.cs @@ -0,0 +1,392 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Disposables; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Dalamud.Utility; + +/// +/// Utilities for disposing stuff. +/// +public static class DisposeSafety +{ + /// + /// Interface that marks a disposable that it can call back on dispose. + /// + public interface IDisposeCallback : IDisposable + { + /// + /// Event to be fired before object dispose. First parameter is the object iself. + /// + event Action? BeforeDispose; + + /// + /// Event to be fired after object dispose. First parameter is the object iself. + /// + event Action? AfterDispose; + } + + /// + /// Returns a proxy that on dispose will dispose the result of the given + /// .
+ /// If any exception has occurred, it will be ignored. + ///
+ /// The task. + /// A disposable type. + /// The proxy . + public static IDisposable ToDisposableIgnoreExceptions(this Task task) + where T : IDisposable + { + return Disposable.Create(() => task.ContinueWith(r => + { + _ = r.Exception; + if (r.IsCompleted) + { + try + { + r.Dispose(); + } + catch + { + // ignore + } + } + })); + } + + /// + /// Transforms into a , disposing the content as necessary. + /// + /// The task. + /// Ignore all exceptions. + /// A disposable type. + /// A wrapper for the task. + public static Task ToContentDisposedTask(this Task task, bool ignoreAllExceptions = false) + where T : IDisposable => task.ContinueWith( + r => + { + if (!r.IsCompletedSuccessfully) + return ignoreAllExceptions ? Task.CompletedTask : r; + try + { + r.Result.Dispose(); + } + catch (Exception e) + { + if (!ignoreAllExceptions) + { + return Task.FromException( + new AggregateException( + new[] { e }.Concat( + (IEnumerable)r.Exception?.InnerExceptions + ?? new[] { new OperationCanceledException() }))); + } + } + + return Task.CompletedTask; + }).Unwrap(); + + /// + /// Returns a proxy that on dispose will dispose all the elements of the given + /// of s. + /// + /// The disposables. + /// The disposable types. + /// The proxy . + /// Error. + public static IDisposable AggregateToDisposable(this IEnumerable? disposables) + where T : IDisposable + { + if (disposables is not T[] array) + array = disposables?.ToArray() ?? Array.Empty(); + + return Disposable.Create(() => + { + List exceptions = null; + foreach (var d in array) + { + try + { + d?.Dispose(); + } + catch (Exception de) + { + exceptions ??= new(); + exceptions.Add(de); + } + } + + if (exceptions is not null) + throw new AggregateException(exceptions); + }); + } + + /// + /// Utility class for managing finalizing stuff. + /// + public class ScopedFinalizer : IDisposeCallback, IAsyncDisposable + { + private readonly List objects = new(); + + /// + public event Action? BeforeDispose; + + /// + public event Action? AfterDispose; + + /// + public void EnsureCapacity(int capacity) => this.objects.EnsureCapacity(capacity); + + /// + /// The parameter. + [return: NotNullIfNotNull(nameof(d))] + public T? Add(T? d) where T : IDisposable + { + if (d is not null) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + [return: NotNullIfNotNull(nameof(d))] + public Action? Add(Action? d) + { + if (d is not null) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + [return: NotNullIfNotNull(nameof(d))] + public Func? Add(Func? d) + { + if (d is not null) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + public GCHandle Add(GCHandle d) + { + if (d != default) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + /// Queue all the given to be disposed later. + /// + /// Disposables. + public void AddRange(IEnumerable ds) => + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + + /// + /// Queue all the given to be run later. + /// + /// Actions. + public void AddRange(IEnumerable ds) => + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + + /// + /// Queue all the given returning to be run later. + /// + /// Func{Task}s. + public void AddRange(IEnumerable?> ds) => + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + + /// + /// Queue all the given to be disposed later. + /// + /// GCHandles. + public void AddRange(IEnumerable ds) => + this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d))); + + /// + /// Cancel all pending disposals. + /// + /// Use this after successful initialization of multiple disposables. + public void Cancel() + { + foreach (var o in this.objects) + this.CheckRemove(o); + this.objects.Clear(); + } + + /// + /// This for method chaining. + public ScopedFinalizer WithEnsureCapacity(int capacity) + { + this.EnsureCapacity(capacity); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(IDisposable d) + { + this.Add(d); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(Action d) + { + this.Add(d); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(Func d) + { + this.Add(d); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(GCHandle d) + { + this.Add(d); + return this; + } + + /// + public void Dispose() + { + this.BeforeDispose?.InvokeSafely(this); + + List? exceptions = null; + while (this.objects.Any()) + { + var obj = this.objects[^1]; + this.objects.RemoveAt(this.objects.Count - 1); + + try + { + switch (obj) + { + case IDisposable x: + x.Dispose(); + break; + case Action a: + a.Invoke(); + break; + case Func a: + a.Invoke().Wait(); + break; + case GCHandle a: + a.Free(); + break; + } + } + catch (Exception ex) + { + exceptions ??= new(); + exceptions.Add(ex); + } + } + + this.objects.TrimExcess(); + + if (exceptions is not null) + { + var exs = exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); + try + { + this.AfterDispose?.Invoke(this, exs); + } + catch + { + // whatever + } + + throw exs; + } + } + + /// + public async ValueTask DisposeAsync() + { + this.BeforeDispose?.InvokeSafely(this); + + List? exceptions = null; + while (this.objects.Any()) + { + var obj = this.objects[^1]; + this.objects.RemoveAt(this.objects.Count - 1); + + try + { + switch (obj) + { + case IAsyncDisposable x: + await x.DisposeAsync(); + break; + case IDisposable x: + x.Dispose(); + break; + case Func a: + await a.Invoke(); + break; + case Action a: + a.Invoke(); + break; + case GCHandle a: + a.Free(); + break; + } + } + catch (Exception ex) + { + exceptions ??= new(); + exceptions.Add(ex); + } + } + + this.objects.TrimExcess(); + + if (exceptions is not null) + { + var exs = exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); + try + { + this.AfterDispose?.Invoke(this, exs); + } + catch + { + // whatever + } + + throw exs; + } + } + + private T CheckAdd(T item) + { + if (item is IDisposeCallback dc) + dc.BeforeDispose += this.OnItemDisposed; + + return item; + } + + private void CheckRemove(object item) + { + if (item is IDisposeCallback dc) + dc.BeforeDispose -= this.OnItemDisposed; + } + + private void OnItemDisposed(IDisposeCallback obj) + { + obj.BeforeDispose -= this.OnItemDisposed; + this.objects.Remove(obj); + } + } +} From a72f407357a748a1dc2ceedec02e276ae95efa6b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 21 Nov 2023 13:49:23 +0900 Subject: [PATCH 323/585] Add DalamudAssetManager --- Dalamud/DalamudAsset.cs | 146 +++++++ Dalamud/Interface/Internal/Branding.cs | 58 --- .../Interface/Internal/DalamudInterface.cs | 21 +- .../Interface/Internal/InterfaceManager.cs | 12 +- .../Internal/Windows/ChangelogWindow.cs | 7 +- .../Internal/Windows/PluginImageCache.cs | 106 ++--- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 7 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 15 +- .../Storage/Assets/DalamudAssetAttribute.cs | 36 ++ .../Storage/Assets/DalamudAssetExtensions.cs | 17 + Dalamud/Storage/Assets/DalamudAssetManager.cs | 365 ++++++++++++++++++ .../DalamudAssetOnlineSourceAttribute.cs | 48 +++ .../Assets/DalamudAssetPathAttribute.cs | 21 + Dalamud/Storage/Assets/DalamudAssetPurpose.cs | 27 ++ .../Assets/DalamudAssetRawTextureAttribute.cs | 45 +++ .../Storage/Assets/IDalamudAssetManager.cs | 79 ++++ Dalamud/Utility/EnumExtensions.cs | 36 +- 17 files changed, 867 insertions(+), 179 deletions(-) create mode 100644 Dalamud/DalamudAsset.cs delete mode 100644 Dalamud/Interface/Internal/Branding.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetAttribute.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetExtensions.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetManager.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetPurpose.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs create mode 100644 Dalamud/Storage/Assets/IDalamudAssetManager.cs diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs new file mode 100644 index 000000000..5b641c487 --- /dev/null +++ b/Dalamud/DalamudAsset.cs @@ -0,0 +1,146 @@ +using Dalamud.Storage.Assets; + +namespace Dalamud; + +/// +/// Specifies an asset that has been shipped as Dalamud Asset.
+/// Any asset can cease to exist at any point, even if the enum value exists.
+/// Either ship your own assets, or be prepared for errors. +///
+public enum DalamudAsset +{ + /// + /// Nothing. + /// + [DalamudAsset(DalamudAssetPurpose.Empty, data: new byte[0])] + Unspecified = 0, + + /// + /// : The fallback empty texture. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 })] + [DalamudAssetRawTexture(4, 8, 4, SharpDX.DXGI.Format.BC1_UNorm)] + Empty4X4 = 1000, + + /// + /// : The Dalamud logo. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "logo.png")] + Logo = 1001, + + /// + /// : The Dalamud logo, but smaller. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "tsmLogo.png")] + LogoSmall = 1002, + + /// + /// : The default plugin icon. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "defaultIcon.png")] + DefaultIcon = 1003, + + /// + /// : The disabled plugin icon. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "disabledIcon.png")] + DisabledIcon = 1004, + + /// + /// : The outdated installable plugin icon. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "outdatedInstallableIcon.png")] + OutdatedInstallableIcon = 1005, + + /// + /// : The plugin trouble icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "troubleIcon.png")] + TroubleIcon = 1006, + + /// + /// : The plugin update icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "updateIcon.png")] + UpdateIcon = 1007, + + /// + /// : The plugin installed icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "installedIcon.png")] + InstalledIcon = 1008, + + /// + /// : The third party plugin icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "thirdIcon.png")] + ThirdIcon = 1009, + + /// + /// : The installed third party plugin icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "thirdInstalledIcon.png")] + ThirdInstalledIcon = 1010, + + /// + /// : The API bump explainer icon. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "changelogApiBump.png")] + ChangelogApiBumpIcon = 1011, + + /// + /// : The background shade for + /// . + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "tsmShade.png")] + TitleScreenMenuShade = 1012, + + /// + /// : Noto Sans CJK JP Medium. + /// + [DalamudAsset(DalamudAssetPurpose.Font)] + [DalamudAssetPath("UIRes", "NotoSansCJKjp-Regular.otf")] + [DalamudAssetPath("UIRes", "NotoSansCJKjp-Medium.otf")] + NotoSansJpMedium = 2000, + + /// + /// : Noto Sans CJK KR Regular. + /// + [DalamudAsset(DalamudAssetPurpose.Font)] + [DalamudAssetPath("UIRes", "NotoSansCJKkr-Regular.otf")] + [DalamudAssetPath("UIRes", "NotoSansKR-Regular.otf")] + NotoSansKrRegular = 2001, + + /// + /// : Inconsolata Regular. + /// + [DalamudAsset(DalamudAssetPurpose.Font)] + [DalamudAssetPath("UIRes", "Inconsolata-Regular.ttf")] + InconsolataRegular = 2002, + + /// + /// : FontAwesome Free Solid. + /// + [DalamudAsset(DalamudAssetPurpose.Font)] + [DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")] + FontAwesomeFreeSolid = 2003, + + /// + /// : Game symbol fonts being used as webfonts at Lodestone. + /// + [DalamudAsset(DalamudAssetPurpose.Font, required: true)] + [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")] + LodestoneGameSymbol = 2004, +} diff --git a/Dalamud/Interface/Internal/Branding.cs b/Dalamud/Interface/Internal/Branding.cs deleted file mode 100644 index 4162cabeb..000000000 --- a/Dalamud/Interface/Internal/Branding.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.IO; - -using Dalamud.IoC.Internal; - -namespace Dalamud.Interface.Internal; - -/// -/// Class containing various textures used by Dalamud windows for branding purposes. -/// -[ServiceManager.EarlyLoadedService] -#pragma warning disable SA1015 -[InherentDependency] // Can't load textures before this -#pragma warning restore SA1015 -internal class Branding : IServiceType, IDisposable -{ - private readonly Dalamud dalamud; - private readonly TextureManager tm; - - /// - /// Initializes a new instance of the class. - /// - /// Dalamud instance. - /// TextureManager instance. - [ServiceManager.ServiceConstructor] - public Branding(Dalamud dalamud, TextureManager tm) - { - this.dalamud = dalamud; - this.tm = tm; - - this.LoadTextures(); - } - - /// - /// Gets a full-size Dalamud logo texture. - /// - public IDalamudTextureWrap Logo { get; private set; } = null!; - - /// - /// Gets a small Dalamud logo texture. - /// - public IDalamudTextureWrap LogoSmall { get; private set; } = null!; - - /// - public void Dispose() - { - this.Logo.Dispose(); - this.LogoSmall.Dispose(); - } - - private void LoadTextures() - { - this.Logo = this.tm.GetTextureFromFile(new FileInfo(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png"))) - ?? throw new Exception("Could not load logo."); - - this.LogoSmall = this.tm.GetTextureFromFile(new FileInfo(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png"))) - ?? throw new Exception("Could not load TSM logo."); - } -} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 18ab538c4..1a6e71194 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -26,6 +26,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; +using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.System.Framework; @@ -93,7 +94,7 @@ internal class DalamudInterface : IDisposable, IServiceType DalamudConfiguration configuration, InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, PluginImageCache pluginImageCache, - Branding branding, + DalamudAssetManager dalamudAssetManager, Game.Framework framework, ClientState clientState, TitleScreenMenu titleScreenMenu, @@ -118,11 +119,10 @@ internal class DalamudInterface : IDisposable, IServiceType this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; this.titleScreenMenuWindow = new TitleScreenMenuWindow( clientState, - dalamud, configuration, + dalamudAssetManager, framework, gameGui, - this.interfaceManager, titleScreenMenu) { IsOpen = false }; this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; @@ -152,12 +152,21 @@ internal class DalamudInterface : IDisposable, IServiceType this.interfaceManager.Draw += this.OnDraw; var tsm = Service.Get(); - tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, () => this.OpenPluginInstaller()); - tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), branding.LogoSmall, this.OpenSettings); + tsm.AddEntryCore( + Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + this.OpenPluginInstaller); + tsm.AddEntryCore( + Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + this.OpenSettings); if (!configuration.DalamudBetaKind.IsNullOrEmpty()) { - tsm.AddEntryCore(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), branding.LogoSmall, () => this.isImGuiDrawDevMenu = true); + tsm.AddEntryCore( + Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + () => this.isImGuiDrawDevMenu = true); } this.creditsDarkeningAnimation.Point1 = Vector2.Zero; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index c666a96a9..52e849c0e 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; +using Dalamud.Storage.Assets; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -1063,10 +1064,15 @@ internal class InterfaceManager : IDisposable, IServiceType } [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(TargetSigScanner sigScanner, Framework framework) + private void ContinueConstruction( + TargetSigScanner sigScanner, + DalamudAssetManager dalamudAssetManager, + DalamudConfiguration configuration) { + dalamudAssetManager.WaitForAllRequiredAssets().Wait(); + this.address.Setup(sigScanner); - framework.RunOnFrameworkThread(() => + this.framework.RunOnFrameworkThread(() => { while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) { @@ -1078,7 +1084,7 @@ internal class InterfaceManager : IDisposable, IServiceType try { - if (Service.Get().WindowIsImmersive) + if (configuration.WindowIsImmersive) this.SetImmersiveMode(true); } catch (Exception ex) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index e3f318223..b9e7ab686 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; +using Dalamud.Storage.Assets; using Dalamud.Utility; using ImGuiNET; @@ -32,7 +33,6 @@ internal sealed class ChangelogWindow : Window, IDisposable "; private readonly TitleScreenMenuWindow tsmWindow; - private readonly IDalamudTextureWrap logoTexture; private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) { @@ -47,6 +47,7 @@ internal sealed class ChangelogWindow : Window, IDisposable }; private IDalamudTextureWrap? apiBumpExplainerTexture; + private IDalamudTextureWrap? logoTexture; private GameFontHandle? bannerFont; private State state = State.WindowFadeIn; @@ -63,8 +64,6 @@ internal sealed class ChangelogWindow : Window, IDisposable this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; - this.logoTexture = Service.Get().Logo; - // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); @@ -188,6 +187,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) { + this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); } } @@ -376,7 +376,6 @@ internal sealed class ChangelogWindow : Window, IDisposable /// public void Dispose() { - this.logoTexture.Dispose(); } private void MakeFont(GameFontManager gfm) => diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index b721b08c3..528507229 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -12,8 +11,8 @@ using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; +using Dalamud.Storage.Assets; using Dalamud.Utility; -using ImGuiScene; using Serilog; namespace Dalamud.Interface.Internal.Windows; @@ -47,12 +46,6 @@ internal class PluginImageCache : IDisposable, IServiceType private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api6/{0}/{1}/images/{2}"; private const string MainRepoDip17ImageUrl = "https://raw.githubusercontent.com/goatcorp/PluginDistD17/main/{0}/{1}/images/{2}"; - [ServiceManager.ServiceDependency] - private readonly InterfaceManager.InterfaceManagerWithScene imWithScene = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly Branding branding = Service.Get(); - [ServiceManager.ServiceDependency] private readonly HappyHttpClient happyHttpClient = Service.Get(); @@ -64,35 +57,12 @@ internal class PluginImageCache : IDisposable, IServiceType private readonly ConcurrentDictionary pluginIconMap = new(); private readonly ConcurrentDictionary pluginImagesMap = new(); - - private readonly Task emptyTextureTask; - private readonly Task disabledIconTask; - private readonly Task outdatedInstallableIconTask; - private readonly Task defaultIconTask; - private readonly Task troubleIconTask; - private readonly Task updateIconTask; - private readonly Task installedIconTask; - private readonly Task thirdIconTask; - private readonly Task thirdInstalledIconTask; - private readonly Task corePluginIconTask; + private readonly DalamudAssetManager dalamudAssetManager; [ServiceManager.ServiceConstructor] - private PluginImageCache(Dalamud dalamud) + private PluginImageCache(Dalamud dalamud, DalamudAssetManager dalamudAssetManager) { - Task? TaskWrapIfNonNull(IDalamudTextureWrap? tw) => tw == null ? null : Task.FromResult(tw!); - var imwst = Task.Run(() => this.imWithScene); - - this.emptyTextureTask = imwst.ContinueWith(task => task.Result.Manager.LoadImageRaw(new byte[64], 8, 8, 4)!); - this.defaultIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.disabledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "disabledIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.outdatedInstallableIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "outdatedInstallableIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.troubleIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.updateIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.installedIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "installedIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.thirdIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.thirdInstalledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdInstalledIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.corePluginIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(this.branding.LogoSmall)).Unwrap(); - + this.dalamudAssetManager = dalamudAssetManager; this.downloadTask = Task.Factory.StartNew( () => this.DownloadTask(8), TaskCreationOptions.LongRunning); this.loadTask = Task.Factory.StartNew( @@ -102,72 +72,62 @@ internal class PluginImageCache : IDisposable, IServiceType /// /// Gets the fallback empty texture. /// - public IDalamudTextureWrap EmptyTexture => this.emptyTextureTask.IsCompleted - ? this.emptyTextureTask.Result - : this.emptyTextureTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap EmptyTexture => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.Empty4X4); /// /// Gets the disabled plugin icon. /// - public IDalamudTextureWrap DisabledIcon => this.disabledIconTask.IsCompleted - ? this.disabledIconTask.Result - : this.disabledIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap DisabledIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DisabledIcon, this.EmptyTexture); /// /// Gets the outdated installable plugin icon. /// - public IDalamudTextureWrap OutdatedInstallableIcon => this.outdatedInstallableIconTask.IsCompleted - ? this.outdatedInstallableIconTask.Result - : this.outdatedInstallableIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap OutdatedInstallableIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.OutdatedInstallableIcon, this.EmptyTexture); /// /// Gets the default plugin icon. /// - public IDalamudTextureWrap DefaultIcon => this.defaultIconTask.IsCompleted - ? this.defaultIconTask.Result - : this.defaultIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap DefaultIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DefaultIcon, this.EmptyTexture); /// /// Gets the plugin trouble icon overlay. /// - public IDalamudTextureWrap TroubleIcon => this.troubleIconTask.IsCompleted - ? this.troubleIconTask.Result - : this.troubleIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap TroubleIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TroubleIcon, this.EmptyTexture); /// /// Gets the plugin update icon overlay. /// - public IDalamudTextureWrap UpdateIcon => this.updateIconTask.IsCompleted - ? this.updateIconTask.Result - : this.updateIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap UpdateIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.UpdateIcon, this.EmptyTexture); /// /// Gets the plugin installed icon overlay. /// - public IDalamudTextureWrap InstalledIcon => this.installedIconTask.IsCompleted - ? this.installedIconTask.Result - : this.installedIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap InstalledIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.InstalledIcon, this.EmptyTexture); /// /// Gets the third party plugin icon overlay. /// - public IDalamudTextureWrap ThirdIcon => this.thirdIconTask.IsCompleted - ? this.thirdIconTask.Result - : this.thirdIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap ThirdIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.ThirdIcon, this.EmptyTexture); /// /// Gets the installed third party plugin icon overlay. /// - public IDalamudTextureWrap ThirdInstalledIcon => this.thirdInstalledIconTask.IsCompleted - ? this.thirdInstalledIconTask.Result - : this.thirdInstalledIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap ThirdInstalledIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon, this.EmptyTexture); /// /// Gets the core plugin icon. /// - public IDalamudTextureWrap CorePluginIcon => this.corePluginIconTask.IsCompleted - ? this.corePluginIconTask.Result - : this.corePluginIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap CorePluginIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall, this.EmptyTexture); /// public void Dispose() @@ -185,22 +145,6 @@ internal class PluginImageCache : IDisposable, IServiceType this.downloadQueue.Dispose(); this.loadQueue.Dispose(); - foreach (var task in new[] - { - this.defaultIconTask, - this.troubleIconTask, - this.updateIconTask, - this.installedIconTask, - this.thirdIconTask, - this.thirdInstalledIconTask, - this.corePluginIconTask, - }) - { - task.Wait(); - if (task.IsCompletedSuccessfully) - task.Result.Dispose(); - } - foreach (var icon in this.pluginIconMap.Values) { icon?.Dispose(); @@ -319,7 +263,7 @@ internal class PluginImageCache : IDisposable, IServiceType if (bytes == null) return null; - var interfaceManager = this.imWithScene.Manager; + var interfaceManager = (await Service.GetAsync()).Manager; var framework = await Service.GetAsync(); IDalamudTextureWrap? image; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 9b6a32617..5b6f6b02f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; +using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; @@ -171,19 +172,16 @@ Dalamud is licensed under AGPL v3 or later. Contribute at: https://github.com/goatcorp/Dalamud "; - private readonly IDalamudTextureWrap logoTexture; private readonly Stopwatch creditsThrottler; private string creditsText; private bool resetNow = false; + private IDalamudTextureWrap? logoTexture; private GameFontHandle? thankYouFont; public SettingsTabAbout() { - var branding = Service.Get(); - - this.logoTexture = branding.Logo; this.creditsThrottler = new(); } @@ -251,6 +249,7 @@ Contribute at: https://github.com/goatcorp/Dalamud const float imageSize = 190f; ImGui.SameLine((ImGui.GetWindowWidth() / 2) - (imageSize / 2)); + this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); ImGui.Image(this.logoTexture.ImGuiHandle, ImGuiHelpers.ScaledVector2(imageSize)); ImGuiHelpers.ScaledDummy(0, 20f); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 4034695e5..f60ebe4ef 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using System.Numerics; @@ -12,6 +11,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; +using Dalamud.Storage.Assets; using ImGuiNET; @@ -46,19 +46,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// Initializes a new instance of the class. /// /// An instance of . - /// An instance of . /// An instance of . + /// An instance of . /// An instance of . - /// An instance of . /// An instance of . /// An instance of . public TitleScreenMenuWindow( ClientState clientState, - Dalamud dalamud, DalamudConfiguration configuration, + DalamudAssetManager dalamudAssetManager, Framework framework, GameGui gameGui, - InterfaceManager interfaceManager, TitleScreenMenu titleScreenMenu) : base( "TitleScreenMenuOverlay", @@ -79,9 +77,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; - var shadeTex = - interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmShade.png")); - this.shadeTexture = shadeTex ?? throw new Exception("Could not load TSM background texture."); + this.shadeTexture = dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade); framework.Update += this.FrameworkOnUpdate; } @@ -116,7 +112,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public void Dispose() { - this.shadeTexture.Dispose(); this.framework.Update -= this.FrameworkOnUpdate; } @@ -386,7 +381,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable return isHover; } - private void FrameworkOnUpdate(IFramework framework) + private void FrameworkOnUpdate(IFramework unused) { this.IsOpen = !this.clientState.IsLoggedIn; diff --git a/Dalamud/Storage/Assets/DalamudAssetAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetAttribute.cs new file mode 100644 index 000000000..a3527cdbc --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetAttribute.cs @@ -0,0 +1,36 @@ +namespace Dalamud.Storage.Assets; + +/// +/// Stores the basic information of a Dalamud asset. +/// +[AttributeUsage(AttributeTargets.Field)] +internal class DalamudAssetAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The purpose. + /// The data. + /// Whether the asset is required. + public DalamudAssetAttribute(DalamudAssetPurpose purpose, byte[]? data = null, bool required = true) + { + this.Purpose = purpose; + this.Data = data; + this.Required = required; + } + + /// + /// Gets the purpose of the asset. + /// + public DalamudAssetPurpose Purpose { get; } + + /// + /// Gets the data, if available. + /// + public byte[]? Data { get; } + + /// + /// Gets a value indicating whether the asset is required. + /// + public bool Required { get; } +} diff --git a/Dalamud/Storage/Assets/DalamudAssetExtensions.cs b/Dalamud/Storage/Assets/DalamudAssetExtensions.cs new file mode 100644 index 000000000..9181f1a5d --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetExtensions.cs @@ -0,0 +1,17 @@ +using Dalamud.Utility; + +namespace Dalamud.Storage.Assets; + +/// +/// Extension methods for . +/// +public static class DalamudAssetExtensions +{ + /// + /// Gets the purpose. + /// + /// The asset. + /// The purpose. + public static DalamudAssetPurpose GetPurpose(this DalamudAsset asset) => + asset.GetAttribute()?.Purpose ?? DalamudAssetPurpose.Empty; +} diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs new file mode 100644 index 000000000..bbfd60636 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -0,0 +1,365 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Networking.Http; +using Dalamud.Utility; +using Dalamud.Utility.Timing; + +using JetBrains.Annotations; + +using Serilog; + +namespace Dalamud.Storage.Assets; + +/// +/// A concrete class for . +/// +[PluginInterface] +[ServiceManager.BlockingEarlyLoadedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudAssetManager +{ + private const int DownloadAttemptCount = 10; + private const int RenameAttemptCount = 10; + + private readonly object syncRoot = new(); + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly Dictionary?> fileStreams; + private readonly Dictionary?> textureWraps; + private readonly Dalamud dalamud; + private readonly HappyHttpClient httpClient; + private readonly string localSourceDirectory; + private readonly CancellationTokenSource cancellationTokenSource; + + private bool isDisposed; + + [ServiceManager.ServiceConstructor] + private DalamudAssetManager(Dalamud dalamud, HappyHttpClient httpClient) + { + this.dalamud = dalamud; + this.httpClient = httpClient; + this.localSourceDirectory = Path.Combine(this.dalamud.AssetDirectory.FullName, "..", "local"); + Directory.CreateDirectory(this.localSourceDirectory); + this.scopedFinalizer.Add(this.cancellationTokenSource = new()); + + this.fileStreams = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); + this.textureWraps = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); + + var loadTimings = Timings.Start("DAM LoadAll"); + this.WaitForAllRequiredAssets().ContinueWith(_ => loadTimings.Dispose()); + } + + /// + public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4); + + /// + public void Dispose() + { + lock (this.syncRoot) + { + if (this.isDisposed) + return; + + this.isDisposed = true; + } + + this.cancellationTokenSource.Cancel(); + Task.WaitAll( + Array.Empty() + .Concat(this.fileStreams.Values) + .Concat(this.textureWraps.Values) + .Where(x => x is not null) + .ToArray()); + this.scopedFinalizer.Dispose(); + } + + /// + /// Waits for all the required assets to be ready. Will result in a faulted task, if any of the required assets + /// has failed to load. + /// + /// The task. + [Pure] + public Task WaitForAllRequiredAssets() + { + lock (this.syncRoot) + { + return Task.WhenAll( + Enum.GetValues() + .Where(x => x is not DalamudAsset.Empty4X4) + .Select(this.CreateStreamAsync) + .Select(x => x.ToContentDisposedTask())); + } + } + + /// + [Pure] + public bool IsStreamImmediatelyAvailable(DalamudAsset asset) => + asset.GetAttribute()?.Data is not null + || this.fileStreams[asset]?.IsCompletedSuccessfully is true; + + /// + [Pure] + public Stream CreateStream(DalamudAsset asset) + { + var s = this.CreateStreamAsync(asset); + s.Wait(); + if (s.IsCompletedSuccessfully) + return s.Result; + if (s.Exception is not null) + throw new AggregateException(s.Exception.InnerExceptions); + throw new OperationCanceledException(); + } + + /// + [Pure] + public Task CreateStreamAsync(DalamudAsset asset) + { + if (asset.GetAttribute() is { Data: { } rawData }) + return Task.FromResult(new MemoryStream(rawData, false)); + + Task task; + lock (this.syncRoot) + { + if (this.isDisposed) + throw new ObjectDisposedException(nameof(DalamudAssetManager)); + + task = this.fileStreams[asset] ??= CreateInnerAsync(); + } + + return this.TransformImmediate( + task, + x => (Stream)new FileStream( + x.Name, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan)); + + async Task CreateInnerAsync() + { + string path; + List exceptions = null; + foreach (var name in asset.GetAttributes().Select(x => x.FileName)) + { + if (!File.Exists(path = Path.Combine(this.dalamud.AssetDirectory.FullName, name))) + continue; + + try + { + return File.OpenRead(path); + } + catch (Exception e) when (e is not OperationCanceledException) + { + exceptions ??= new(); + exceptions.Add(e); + } + } + + if (File.Exists(path = Path.Combine(this.localSourceDirectory, asset.ToString()))) + { + try + { + return File.OpenRead(path); + } + catch (Exception e) when (e is not OperationCanceledException) + { + exceptions ??= new(); + exceptions.Add(e); + } + } + + var tempPath = $"{path}.{Environment.ProcessId:x}.{Environment.CurrentManagedThreadId:x}"; + try + { + for (var i = 0; i < DownloadAttemptCount; i++) + { + var attemptedAny = false; + foreach (var url in asset.GetAttributes()) + { + Log.Information("[{who}] {asset}: Trying {url}", nameof(DalamudAssetManager), asset, url); + attemptedAny = true; + + try + { + await using var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write); + await url.DownloadAsync( + this.httpClient.SharedHttpClient, + tempPathStream, + this.cancellationTokenSource.Token); + tempPathStream.Dispose(); + for (var j = RenameAttemptCount; ; j--) + { + try + { + File.Move(tempPath, path); + } + catch (IOException ioe) + { + if (j == 0) + throw; + Log.Warning( + ioe, + "[{who}] {asset}: Renaming failed; trying again {n} more times", + nameof(DalamudAssetManager), + asset, + j); + await Task.Delay(1000, this.cancellationTokenSource.Token); + continue; + } + + return File.OpenRead(path); + } + } + catch (Exception e) when (e is not OperationCanceledException) + { + Log.Error(e, "[{who}] {asset}: Failed {url}", nameof(DalamudAssetManager), asset, url); + } + } + + if (!attemptedAny) + throw new FileNotFoundException($"Failed to find the asset {asset}.", asset.ToString()); + + // Wait up to 5 minutes + var delay = Math.Min(300, (1 << i) * 1000); + Log.Error( + "[{who}] {asset}: Failed to download. Trying again in {sec} seconds...", + nameof(DalamudAssetManager), + asset, + delay); + await Task.Delay(delay * 1000, this.cancellationTokenSource.Token); + } + + throw new FileNotFoundException($"Failed to load the asset {asset}.", asset.ToString()); + } + catch (Exception e) when (e is not OperationCanceledException) + { + exceptions ??= new(); + exceptions.Add(e); + try + { + File.Delete(tempPath); + } + catch + { + // don't care + } + } + + throw new AggregateException(exceptions); + } + } + + /// + [Pure] + public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) => + ExtractResult(this.GetDalamudTextureWrapAsync(asset)); + + /// + [Pure] + [return: NotNullIfNotNull(nameof(defaultWrap))] + public IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap) + { + var task = this.GetDalamudTextureWrapAsync(asset); + return task.IsCompletedSuccessfully ? task.Result : defaultWrap; + } + + /// + [Pure] + public Task GetDalamudTextureWrapAsync(DalamudAsset asset) + { + var purpose = asset.GetPurpose(); + if (purpose is not DalamudAssetPurpose.TextureFromPng and not DalamudAssetPurpose.TextureFromRaw) + throw new ArgumentOutOfRangeException(nameof(asset), asset, "The asset cannot be taken as a Texture2D."); + + Task task; + lock (this.syncRoot) + { + if (this.isDisposed) + throw new ObjectDisposedException(nameof(DalamudAssetManager)); + + task = this.textureWraps[asset] ??= CreateInnerAsync(); + } + + return task; + + async Task CreateInnerAsync() + { + var buf = Array.Empty(); + try + { + var im = (await Service.GetAsync()).Manager; + await using var stream = await this.CreateStreamAsync(asset); + var length = checked((int)stream.Length); + buf = ArrayPool.Shared.Rent(length); + stream.ReadExactly(buf, 0, length); + var image = purpose switch + { + DalamudAssetPurpose.TextureFromPng => im.LoadImage(buf), + DalamudAssetPurpose.TextureFromRaw => + asset.GetAttribute() is { } raw + ? im.LoadImageFromDxgiFormat(buf, raw.Pitch, raw.Width, raw.Height, raw.Format) + : throw new InvalidOperationException( + "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."), + _ => null, + }; + var disposeDeferred = + this.scopedFinalizer.Add(image) + ?? throw new InvalidOperationException("Something went wrong very badly"); + return new DisposeSuppressingDalamudTextureWrap(disposeDeferred); + } + catch (Exception e) + { + Log.Error(e, "[{name}] Failed to load {asset}.", nameof(DalamudAssetManager), asset); + throw; + } + finally + { + ArrayPool.Shared.Return(buf); + } + } + } + + private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); + + private Task TransformImmediate(Task task, Func transformer) + { + if (task.IsCompletedSuccessfully) + return Task.FromResult(transformer(task.Result)); + if (task.Exception is { } exc) + return Task.FromException(exc); + return task.ContinueWith(_ => this.TransformImmediate(task, transformer)).Unwrap(); + } + + private class DisposeSuppressingDalamudTextureWrap : IDalamudTextureWrap + { + private readonly IDalamudTextureWrap innerWrap; + + public DisposeSuppressingDalamudTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap; + + /// + public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle; + + /// + public int Width => this.innerWrap.Width; + + /// + public int Height => this.innerWrap.Height; + + /// + public void Dispose() + { + // suppressed + } + } +} diff --git a/Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs new file mode 100644 index 000000000..25ed995d7 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs @@ -0,0 +1,48 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Dalamud.Storage.Assets; + +/// +/// Marks that an asset can be download from online. +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] +internal class DalamudAssetOnlineSourceAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The URL. + public DalamudAssetOnlineSourceAttribute(string url) + { + this.Url = url; + } + + /// + /// Gets the source URL of the file. + /// + public string Url { get; } + + /// + /// Downloads to the given stream. + /// + /// The client. + /// The stream. + /// The cancellation token. + /// The task. + public async Task DownloadAsync(HttpClient client, Stream stream, CancellationToken cancellationToken) + { + using var resp = await client.GetAsync(this.Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + resp.EnsureSuccessStatusCode(); + if (resp.StatusCode != HttpStatusCode.OK) + throw new NotSupportedException($"Only 200 OK is supported; got {resp.StatusCode}"); + + await using var readStream = await resp.Content.ReadAsStreamAsync(cancellationToken); + await readStream.CopyToAsync(stream, cancellationToken); + if (resp.Content.Headers.ContentLength is { } length && stream.Length != length) + throw new IOException($"Expected {length} bytes; got {stream.Length} bytes."); + } +} diff --git a/Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs new file mode 100644 index 000000000..1df52aa39 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs @@ -0,0 +1,21 @@ +using System.IO; + +namespace Dalamud.Storage.Assets; + +/// +/// File names to look up in Dalamud assets. +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] +internal class DalamudAssetPathAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The path components. + public DalamudAssetPathAttribute(params string[] pathComponents) => this.FileName = Path.Join(pathComponents); + + /// + /// Gets the file name. + /// + public string FileName { get; } +} diff --git a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs new file mode 100644 index 000000000..b059cb3d6 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Storage.Assets; + +/// +/// Purposes of a Dalamud asset. +/// +public enum DalamudAssetPurpose +{ + /// + /// The asset has no purpose. + /// + Empty = 0, + + /// + /// 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 . + /// + TextureFromRaw = 1001, + + /// + /// The asset is a font file. + /// + Font = 2000, +} diff --git a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs new file mode 100644 index 000000000..b79abb7d7 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs @@ -0,0 +1,45 @@ +using SharpDX.DXGI; + +namespace Dalamud.Storage.Assets; + +/// +/// Provide raw texture data directly. +/// +[AttributeUsage(AttributeTargets.Field)] +internal class DalamudAssetRawTextureAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The width. + /// The pitch. + /// The height. + /// The format. + public DalamudAssetRawTextureAttribute(int width, int pitch, int height, Format format) + { + this.Width = width; + this.Pitch = pitch; + this.Height = height; + this.Format = format; + } + + /// + /// Gets the width. + /// + public int Width { get; } + + /// + /// Gets the pitch. + /// + public int Pitch { get; } + + /// + /// Gets the height. + /// + public int Height { get; } + + /// + /// Gets the format. + /// + public Format Format { get; } +} diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs new file mode 100644 index 000000000..4fb83df80 --- /dev/null +++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs @@ -0,0 +1,79 @@ +using System.Diagnostics.Contracts; +using System.IO; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; + +namespace Dalamud.Storage.Assets; + +/// +/// Holds Dalamud Assets' handles hostage, so that they do not get closed while Dalamud is running.
+/// Also, attempts to load optional assets.
+///
+/// Note on
+/// It will help you get notified if you discard the result of functions, mostly likely because of a mistake. +/// Think of C++ [[nodiscard]]. Also, like the intended meaning of the attribute, such methods will not have +/// externally visible state changes. +///
+internal interface IDalamudAssetManager +{ + /// + /// Gets the shared texture wrap for . + /// + IDalamudTextureWrap Empty4X4 { get; } + + /// + /// Gets whether the stream for the asset is instantly available. + /// + /// The asset. + /// Whether the stream of an asset is immediately available. + [Pure] + bool IsStreamImmediatelyAvailable(DalamudAsset asset); + + /// + /// Creates a stream backed by the specified asset, waiting as necessary.
+ /// Call after use. + ///
+ /// The asset. + /// The stream. + [Pure] + Stream CreateStream(DalamudAsset asset); + + /// + /// Creates a stream backed by the specified asset.
+ /// Call after use. + ///
+ /// The asset. + /// The stream, wrapped inside a . + [Pure] + Task CreateStreamAsync(DalamudAsset asset); + + /// + /// Gets a shared instance of , after waiting as necessary.
+ /// Calls to is unnecessary; they will be ignored. + ///
+ /// The texture asset. + /// The texture wrap. + [Pure] + IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset); + + /// + /// Gets a shared instance of if it is available instantly; + /// if it is not ready, returns .
+ /// Calls to is unnecessary; they will be ignored. + ///
+ /// The texture asset. + /// The default return value, if the asset is not ready for whatever reason. + /// The texture wrap. + [Pure] + IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap); + + /// + /// Gets a shared instance of in a .
+ /// Calls to is unnecessary; they will be ignored. + ///
+ /// The texture asset. + /// The new texture wrap, wrapped inside a . + [Pure] + Task GetDalamudTextureWrapAsync(DalamudAsset asset); +} diff --git a/Dalamud/Utility/EnumExtensions.cs b/Dalamud/Utility/EnumExtensions.cs index 0bb60962e..493e6be1f 100644 --- a/Dalamud/Utility/EnumExtensions.cs +++ b/Dalamud/Utility/EnumExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System.Collections.Generic; using System.Linq; namespace Dalamud.Utility; @@ -8,6 +8,26 @@ namespace Dalamud.Utility; /// public static class EnumExtensions { + /// + /// Gets attributes on an enum. + /// + /// The type of attribute to get. + /// The enum value that has an attached attribute. + /// The enumerable of the attached attributes. + public static IEnumerable GetAttributes(this Enum value) + where TAttribute : Attribute + { + var type = value.GetType(); + var name = Enum.GetName(type, value); + if (name.IsNullOrEmpty()) + return Array.Empty(); + + return type.GetField(name)? + .GetCustomAttributes(false) + .OfType() + ?? Array.Empty(); + } + /// /// Gets an attribute on an enum. /// @@ -15,18 +35,8 @@ public static class EnumExtensions /// The enum value that has an attached attribute. /// The attached attribute, if any. public static TAttribute? GetAttribute(this Enum value) - where TAttribute : Attribute - { - var type = value.GetType(); - var name = Enum.GetName(type, value); - if (name.IsNullOrEmpty()) - return null; - - return type.GetField(name)? - .GetCustomAttributes(false) - .OfType() - .SingleOrDefault(); - } + where TAttribute : Attribute => + value.GetAttributes().SingleOrDefault(); /// /// Gets an indicator if enum has been flagged as obsolete (deprecated). From d1fad70e8ff92ff11696ca10389a8015dd5c76c2 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 29 Nov 2023 05:14:58 +0900 Subject: [PATCH 324/585] Disable game symbol download for the time being --- Dalamud/DalamudAsset.cs | 4 ++-- Dalamud/Storage/Assets/DalamudAssetManager.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index 5b641c487..184193796 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -140,7 +140,7 @@ public enum DalamudAsset /// /// : Game symbol fonts being used as webfonts at Lodestone. /// - [DalamudAsset(DalamudAssetPurpose.Font, required: true)] - [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")] + [DalamudAsset(DalamudAssetPurpose.Font, required: false)] + // [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")] LodestoneGameSymbol = 2004, } diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index bbfd60636..30441f479 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -96,6 +96,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA return Task.WhenAll( Enum.GetValues() .Where(x => x is not DalamudAsset.Empty4X4) + .Where(x => x.GetAttribute()?.Required is true) .Select(this.CreateStreamAsync) .Select(x => x.ToContentDisposedTask())); } From a71cb813841909eba9a7e8f260e1bc57d91f312b Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 29 Nov 2023 06:43:41 +0900 Subject: [PATCH 325/585] Hold Shift to display Toggle Dev Menu TSM entry (#1538) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Interface/Internal/DalamudInterface.cs | 42 +++++++----- .../Internal/Windows/TitleScreenMenuWindow.cs | 29 +++++---- .../TitleScreenMenu/TitleScreenMenu.cs | 65 +++++++++++++++---- .../TitleScreenMenu/TitleScreenMenuEntry.cs | 28 +++++++- 4 files changed, 124 insertions(+), 40 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 1a6e71194..1dcc5c0c7 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -9,6 +9,7 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Gui; using Dalamud.Game.Internal; using Dalamud.Interface.Animation.EasingFunctions; @@ -151,23 +152,32 @@ internal class DalamudInterface : IDisposable, IServiceType this.interfaceManager.Draw += this.OnDraw; - var tsm = Service.Get(); - tsm.AddEntryCore( - Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), - dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), - this.OpenPluginInstaller); - tsm.AddEntryCore( - Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), - dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), - this.OpenSettings); + Service.GetAsync().ContinueWith( + _ => + { + titleScreenMenu.AddEntryCore( + Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + this.OpenPluginInstaller); + titleScreenMenu.AddEntryCore( + Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + this.OpenSettings); - if (!configuration.DalamudBetaKind.IsNullOrEmpty()) - { - tsm.AddEntryCore( - Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), - dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), - () => this.isImGuiDrawDevMenu = true); - } + titleScreenMenu.AddEntryCore( + "Toggle Dev Menu", + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + () => Service.GetNullable()?.ToggleDevMenu(), + VirtualKey.SHIFT); + + if (!configuration.DalamudBetaKind.IsNullOrEmpty()) + { + titleScreenMenu.AddEntryCore( + Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + () => this.isImGuiDrawDevMenu = true); + } + }); this.creditsDarkeningAnimation.Point1 = Vector2.Zero; this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index f60ebe4ef..42bca89ff 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -31,7 +31,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly GameGui gameGui; private readonly TitleScreenMenu titleScreenMenu; - private readonly IDalamudTextureWrap shadeTexture; + private readonly Lazy shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); @@ -77,7 +77,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; - this.shadeTexture = dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade); + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); framework.Update += this.FrameworkOnUpdate; } @@ -122,15 +122,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable return; var scale = ImGui.GetIO().FontGlobalScale; - var entries = this.titleScreenMenu.Entries.OrderByDescending(x => x.IsInternal).ToList(); + var entries = this.titleScreenMenu.Entries; switch (this.state) { case State.Show: { - for (var i = 0; i < entries.Count; i++) + var i = 0; + foreach (var entry in entries) { - var entry = entries[i]; + if (!entry.IsShowConditionSatisfied()) + continue; if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing)) { @@ -150,7 +152,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable moveEasing.Update(); - var finalPos = (i + 1) * this.shadeTexture.Height * scale; + var finalPos = (i + 1) * this.shadeTexture.Value.Height * scale; var pos = moveEasing.Value * finalPos; // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment. @@ -164,6 +166,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable var cursor = ImGui.GetCursorPos(); cursor.Y = (float)pos; ImGui.SetCursorPos(cursor); + i++; } if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | @@ -196,17 +199,20 @@ internal class TitleScreenMenuWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value)) { - for (var i = 0; i < entries.Count; i++) + var i = 0; + foreach (var entry in entries) { - var entry = entries[i]; + if (!entry.IsShowConditionSatisfied()) + continue; - var finalPos = (i + 1) * this.shadeTexture.Height * scale; + var finalPos = (i + 1) * this.shadeTexture.Value.Height * scale; this.DrawEntry(entry, i != 0, true, i == 0, false, false); var cursor = ImGui.GetCursorPos(); cursor.Y = finalPos; ImGui.SetCursorPos(cursor); + i++; } } @@ -280,7 +286,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)shadeEasing.Value)) { - ImGui.Image(this.shadeTexture.ImGuiHandle, new Vector2(this.shadeTexture.Width * scale, this.shadeTexture.Height * scale)); + var texture = this.shadeTexture.Value; + ImGui.Image(texture.ImGuiHandle, new Vector2(texture.Width, texture.Height) * scale); } var isHover = ImGui.IsItemHovered(); @@ -358,7 +365,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable // Drop shadow using (ImRaii.PushColor(ImGuiCol.Text, 0xFF000000)) { - for (int i = 0, i_ = (int)Math.Ceiling(1 * scale); i < i_; i++) + for (int i = 0, to = (int)Math.Ceiling(1 * scale); i < to; i++) { ImGui.SetCursorPos(new Vector2(cursor.X, cursor.Y + i)); ImGui.Text(entry.Name); diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs index 6665bbafb..1f9a5bc76 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs @@ -2,11 +2,12 @@ using System.Linq; using System.Reflection; +using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; -using ImGuiScene; +using Dalamud.Utility; namespace Dalamud.Interface; @@ -23,14 +24,32 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu internal const uint TextureSize = 64; private readonly List entries = new(); + private TitleScreenMenuEntry[]? entriesView; [ServiceManager.ServiceConstructor] private TitleScreenMenu() { } + /// + /// Event to be called when the entry list has been changed. + /// + internal event Action? EntryListChange; + /// - public IReadOnlyList Entries => this.entries; + public IReadOnlyList Entries + { + get + { + lock (this.entries) + { + if (!this.entries.Any()) + return Array.Empty(); + + return this.entriesView ??= this.entries.OrderByDescending(x => x.IsInternal).ToArray(); + } + } + } /// public TitleScreenMenuEntry AddEntry(string text, IDalamudTextureWrap texture, Action onTriggered) @@ -40,19 +59,23 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu throw new ArgumentException("Texture must be 64x64"); } + TitleScreenMenuEntry entry; lock (this.entries) { var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == Assembly.GetCallingAssembly()).ToList(); var priority = entriesOfAssembly.Any() ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) : 0; - var entry = new TitleScreenMenuEntry(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); + entry = new(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); var i = this.entries.BinarySearch(entry); if (i < 0) i = ~i; this.entries.Insert(i, entry); - return entry; + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); + return entry; } /// @@ -63,15 +86,19 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu throw new ArgumentException("Texture must be 64x64"); } + TitleScreenMenuEntry entry; lock (this.entries) { - var entry = new TitleScreenMenuEntry(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); + entry = new(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); var i = this.entries.BinarySearch(entry); if (i < 0) i = ~i; this.entries.Insert(i, entry); - return entry; + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); + return entry; } /// @@ -80,7 +107,10 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu lock (this.entries) { this.entries.Remove(entry); + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); } /// @@ -99,15 +129,19 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu throw new ArgumentException("Texture must be 64x64"); } + TitleScreenMenuEntry entry; lock (this.entries) { - var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered) + entry = new(null, priority, text, texture, onTriggered) { IsInternal = true, }; this.entries.Add(entry); - return entry; + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); + return entry; } /// @@ -116,28 +150,37 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu /// The text to show. /// The texture to show. /// The action to execute when the option is selected. + /// The keys that have to be held to display the menu. /// A object that can be used to manage the entry. /// Thrown when the texture provided does not match the required resolution(64x64). - internal TitleScreenMenuEntry AddEntryCore(string text, IDalamudTextureWrap texture, Action onTriggered) + internal TitleScreenMenuEntry AddEntryCore( + string text, + IDalamudTextureWrap texture, + Action onTriggered, + params VirtualKey[] showConditionKeys) { if (texture.Height != TextureSize || texture.Width != TextureSize) { throw new ArgumentException("Texture must be 64x64"); } + TitleScreenMenuEntry entry; lock (this.entries) { var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == null).ToList(); var priority = entriesOfAssembly.Any() ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) : 0; - var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered) + entry = new(null, priority, text, texture, onTriggered, showConditionKeys) { IsInternal = true, }; this.entries.Add(entry); - return entry; + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); + return entry; } } diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs index 76382ace2..8a400db7c 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs @@ -1,5 +1,9 @@ -using System.Reflection; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Internal; namespace Dalamud.Interface; @@ -19,13 +23,21 @@ public class TitleScreenMenuEntry : IComparable /// The text to show. /// The texture to show. /// The action to execute when the option is selected. - internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered) + /// The keys that have to be held to display the menu. + internal TitleScreenMenuEntry( + Assembly? callingAssembly, + ulong priority, + string text, + IDalamudTextureWrap texture, + Action onTriggered, + IEnumerable? showConditionKeys = null) { this.CallingAssembly = callingAssembly; this.Priority = priority; this.Name = text; this.Texture = texture; this.onTriggered = onTriggered; + this.ShowConditionKeys = (showConditionKeys ?? Array.Empty()).ToImmutableSortedSet(); } /// @@ -58,6 +70,11 @@ public class TitleScreenMenuEntry : IComparable /// internal Guid Id { get; init; } = Guid.NewGuid(); + /// + /// Gets the keys that have to be pressed to show the menu. + /// + internal IReadOnlySet ShowConditionKeys { get; init; } + /// public int CompareTo(TitleScreenMenuEntry? other) { @@ -84,6 +101,13 @@ public class TitleScreenMenuEntry : IComparable return 0; } + /// + /// Determines the displaying condition of this menu entry is met. + /// + /// True if met. + internal bool IsShowConditionSatisfied() => + this.ShowConditionKeys.All(x => Service.GetNullable()?[x] is true); + /// /// Trigger the action associated with this entry. /// From 13346b04db6d657216a6653b82d5f85cf2fc3295 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 28 Nov 2023 13:52:38 -0800 Subject: [PATCH 326/585] Remove second HttpClient from PluginRepository (#1551) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Plugin/Internal/Types/PluginRepository.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index 18c528910..8de25aa08 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -6,12 +6,14 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Networking.Http; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Utility; + using Newtonsoft.Json; namespace Dalamud.Plugin.Internal.Types; @@ -26,8 +28,9 @@ internal class PluginRepository /// public const string MainRepoUrl = "https://kamori.goats.dev/Plugin/PluginMaster"; - private static readonly ModuleLog Log = new("PLUGINR"); + private const int HttpRequestTimeoutSeconds = 20; + private static readonly ModuleLog Log = new("PLUGINR"); private readonly HttpClient httpClient; /// @@ -112,7 +115,8 @@ internal class PluginRepository { Log.Information($"Fetching repo: {this.PluginMasterUrl}"); - using var response = await this.httpClient.GetAsync(this.PluginMasterUrl); + using var response = await this.GetPluginMaster(this.PluginMasterUrl); + response.EnsureSuccessStatusCode(); var data = await response.Content.ReadAsStringAsync(); @@ -204,4 +208,17 @@ internal class PluginRepository return true; } + + private async Task GetPluginMaster(string url, int timeout = HttpRequestTimeoutSeconds) + { + var httpClient = Service.Get().SharedHttpClient; + + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; + + using var requestCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); + + return await httpClient.SendAsync(request, requestCts.Token); + } } From 334a3076ac79fb9869b838c1d223e44e56e125b0 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 28 Nov 2023 22:55:38 +0100 Subject: [PATCH 327/585] [master] Update ClientStructs (#1528) 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 090e0c244..cc6687524 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 090e0c244df668454616026188c1363e5d25a1bc +Subproject commit cc668752416a8459a3c23345c51277e359803de8 From d52118b3ad366a61216129c80c0fa250c885abac Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Wed, 29 Nov 2023 00:57:51 +0100 Subject: [PATCH 328/585] chore: bump up timeout to 120 seconds for now --- Dalamud/ServiceManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 46a6ba509..21c08ce72 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -194,7 +194,7 @@ internal static class ServiceManager try { var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); - while (await Task.WhenAny(whenBlockingComplete, Task.Delay(30000)) != whenBlockingComplete) + while (await Task.WhenAny(whenBlockingComplete, Task.Delay(120000)) != whenBlockingComplete) { if (NativeFunctions.MessageBoxW( IntPtr.Zero, From 92f4df625feda6b8049c0cdd6f4a32298550455b Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:15:34 -0800 Subject: [PATCH 329/585] [GameInventory] Service Prototype --- Dalamud/Game/Inventory/GameInventory.cs | 268 +++++++++++++ .../Game/Inventory/GameInventoryChangelog.cs | 28 ++ .../Inventory/GameInventoryChangelogState.cs | 17 + Dalamud/Game/Inventory/GameInventoryItem.cs | 98 +++++ Dalamud/Game/Inventory/GameInventoryType.cs | 351 ++++++++++++++++++ Dalamud/Plugin/Services/IGameInventory.cs | 69 ++++ 6 files changed, 831 insertions(+) create mode 100644 Dalamud/Game/Inventory/GameInventory.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryChangelog.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryChangelogState.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryItem.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryType.cs create mode 100644 Dalamud/Plugin/Services/IGameInventory.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs new file mode 100644 index 000000000..7cd2556e2 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -0,0 +1,268 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace Dalamud.Game.Inventory; + +/// +/// This class provides events for the players in-game inventory. +/// +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal class GameInventory : IDisposable, IServiceType, IGameInventory +{ + private static readonly ModuleLog Log = new("GameInventory"); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + private readonly Dictionary> inventoryCache; + + [ServiceManager.ServiceConstructor] + private GameInventory() + { + this.inventoryCache = new Dictionary>(); + + foreach (var inventoryType in Enum.GetValues()) + { + this.inventoryCache.Add(inventoryType, new Dictionary()); + } + + this.framework.Update += this.OnFrameworkUpdate; + } + + /// + public event IGameInventory.OnItemMovedDelegate? ItemMoved; + + /// + public event IGameInventory.OnItemRemovedDelegate? ItemRemoved; + + /// + public event IGameInventory.OnItemAddedDelegate? ItemAdded; + + /// + public event IGameInventory.OnItemChangedDelegate? ItemChanged; + + /// + public void Dispose() + { + this.framework.Update -= this.OnFrameworkUpdate; + } + + private void OnFrameworkUpdate(IFramework framework1) + { + // If no one is listening for event's then we don't need to track anything. + if (!this.AnyListeners()) return; + + var performanceMonitor = Stopwatch.StartNew(); + + var changelog = new List(); + + foreach (var (inventoryType, cachedInventoryItems) in this.inventoryCache) + { + foreach (var item in this.GetItemsForInventory(inventoryType)) + { + if (cachedInventoryItems.TryGetValue(item.Slot, out var inventoryItem)) + { + // Gained Item + // If the item we have cached has an item id of 0, then we expect it to be an empty slot. + // However, if the item we see in the game data has an item id that is not 0, then it now has an item. + if (inventoryItem.ItemID is 0 && item.ItemID is not 0) + { + var gameInventoryItem = new GameInventoryItem(item); + this.ItemAdded?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem); + changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Added, gameInventoryItem)); + + Log.Verbose($"New Item Added to {inventoryType}: {item.ItemID}"); + this.inventoryCache[inventoryType][item.Slot] = item; + } + + // Removed Item + // If the item we have cached has an item id of not 0, then we expect it to have an item. + // However, if the item we see in the game data has an item id that is 0, then it was removed from this inventory. + if (inventoryItem.ItemID is not 0 && item.ItemID is 0) + { + var gameInventoryItem = new GameInventoryItem(inventoryItem); + this.ItemRemoved?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem); + changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Removed, gameInventoryItem)); + + Log.Verbose($"Item Removed from {inventoryType}: {inventoryItem.ItemID}"); + this.inventoryCache[inventoryType][item.Slot] = item; + } + + // Changed Item + // If the item we have cached, does not match the item that we see in the game data + // AND if neither item is empty, then the item has been changed. + if (this.IsItemChanged(inventoryItem, item) && inventoryItem.ItemID is not 0 && item.ItemID is not 0) + { + var gameInventoryItem = new GameInventoryItem(inventoryItem); + this.ItemChanged?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem); + + Log.Verbose($"Item Changed {inventoryType}: {inventoryItem.ItemID}"); + this.inventoryCache[inventoryType][item.Slot] = item; + } + } + else + { + cachedInventoryItems.Add(item.Slot, item); + } + } + } + + // Resolve changelog for item moved + // Group all changelogs that have the same itemId, and check if there was an add and a remove event for that item. + foreach (var itemGroup in changelog.GroupBy(log => log.Item.ItemId)) + { + var hasAdd = false; + var hasRemove = false; + + foreach (var log in itemGroup) + { + switch (log.State) + { + case GameInventoryChangelogState.Added: + hasAdd = true; + break; + + case GameInventoryChangelogState.Removed: + hasRemove = true; + break; + + default: + throw new ArgumentOutOfRangeException(); + } + } + + var itemMoved = hasAdd && hasRemove; + if (itemMoved) + { + var added = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Added); + var removed = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Removed); + if (added is null || removed is null) continue; + + this.ItemMoved?.Invoke(removed.Item.ContainerType, removed.Item.InventorySlot, added.Item.ContainerType, added.Item.InventorySlot, added.Item); + + Log.Verbose($"Item Moved {removed.Item.ContainerType}:{removed.Item.InventorySlot} -> {added.Item.ContainerType}:{added.Item.InventorySlot}: {added.Item.ItemId}"); + } + } + + var elapsed = performanceMonitor.Elapsed; + + Log.Verbose($"Processing Time: {elapsed.Ticks}ticks :: {elapsed.TotalMilliseconds}ms"); + } + + private bool AnyListeners() + { + if (this.ItemMoved is not null) return true; + if (this.ItemRemoved is not null) return true; + if (this.ItemAdded is not null) return true; + if (this.ItemChanged is not null) return true; + + return false; + } + + private unsafe ReadOnlySpan GetItemsForInventory(GameInventoryType type) + { + var inventoryManager = InventoryManager.Instance(); + if (inventoryManager is null) return ReadOnlySpan.Empty; + + var inventory = inventoryManager->GetInventoryContainer((InventoryType)type); + if (inventory is null) return ReadOnlySpan.Empty; + + return new ReadOnlySpan(inventory->Items, (int)inventory->Size); + } + + private bool IsItemChanged(InventoryItem a, InventoryItem b) + { + if (a.Container != b.Container) return true; // Shouldn't be possible, but shouldn't hurt. + if (a.Slot != b.Slot) return true; // Shouldn't be possible, but shouldn't hurt. + if (a.ItemID != b.ItemID) return true; + if (a.Quantity != b.Quantity) return true; + if (a.Spiritbond != b.Spiritbond) return true; + if (a.Condition != b.Condition) return true; + if (a.Flags != b.Flags) return true; + if (a.CrafterContentID != b.CrafterContentID) return true; + if (this.IsMateriaChanged(a, b)) return true; + if (this.IsMateriaGradeChanged(a, b)) return true; + if (a.Stain != b.Stain) return true; + if (a.GlamourID != b.GlamourID) return true; + + return false; + } + + private unsafe bool IsMateriaChanged(InventoryItem a, InventoryItem b) + => new ReadOnlySpan(a.Materia, 5) == new ReadOnlySpan(b.Materia, 5); + + private unsafe bool IsMateriaGradeChanged(InventoryItem a, InventoryItem b) + => new ReadOnlySpan(a.MateriaGrade, 5) == new ReadOnlySpan(b.MateriaGrade, 5); +} + +/// +/// Plugin-scoped version of a GameInventory service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory +{ + [ServiceManager.ServiceDependency] + private readonly GameInventory gameInventoryService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + public GameInventoryPluginScoped() + { + this.gameInventoryService.ItemMoved += this.OnItemMovedForward; + this.gameInventoryService.ItemRemoved += this.OnItemRemovedForward; + this.gameInventoryService.ItemAdded += this.OnItemAddedForward; + this.gameInventoryService.ItemChanged += this.OnItemChangedForward; + } + + /// + public event IGameInventory.OnItemMovedDelegate? ItemMoved; + + /// + public event IGameInventory.OnItemRemovedDelegate? ItemRemoved; + + /// + public event IGameInventory.OnItemAddedDelegate? ItemAdded; + + /// + public event IGameInventory.OnItemChangedDelegate? ItemChanged; + + /// + public void Dispose() + { + this.gameInventoryService.ItemMoved -= this.OnItemMovedForward; + this.gameInventoryService.ItemRemoved -= this.OnItemRemovedForward; + this.gameInventoryService.ItemAdded -= this.OnItemAddedForward; + this.gameInventoryService.ItemChanged -= this.OnItemChangedForward; + + this.ItemMoved = null; + this.ItemRemoved = null; + this.ItemAdded = null; + this.ItemChanged = null; + } + + private void OnItemMovedForward(GameInventoryType source, uint sourceSlot, GameInventoryType destination, uint destinationSlot, GameInventoryItem item) + => this.ItemMoved?.Invoke(source, sourceSlot, destination, destinationSlot, item); + + private void OnItemRemovedForward(GameInventoryType source, uint sourceSlot, GameInventoryItem item) + => this.ItemRemoved?.Invoke(source, sourceSlot, item); + + private void OnItemAddedForward(GameInventoryType destination, uint destinationSlot, GameInventoryItem item) + => this.ItemAdded?.Invoke(destination, destinationSlot, item); + + private void OnItemChangedForward(GameInventoryType inventory, uint slot, GameInventoryItem item) + => this.ItemChanged?.Invoke(inventory, slot, item); +} diff --git a/Dalamud/Game/Inventory/GameInventoryChangelog.cs b/Dalamud/Game/Inventory/GameInventoryChangelog.cs new file mode 100644 index 000000000..52ada81e0 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryChangelog.cs @@ -0,0 +1,28 @@ +namespace Dalamud.Game.Inventory; + +/// +/// Class representing an inventory item change event. +/// +internal class GameInventoryItemChangelog +{ + /// + /// Initializes a new instance of the class. + /// + /// Item state. + /// Item. + internal GameInventoryItemChangelog(GameInventoryChangelogState state, GameInventoryItem item) + { + this.State = state; + this.Item = item; + } + + /// + /// Gets the state of this changelog event. + /// + internal GameInventoryChangelogState State { get; } + + /// + /// Gets the item for this changelog event. + /// + internal GameInventoryItem Item { get; } +} diff --git a/Dalamud/Game/Inventory/GameInventoryChangelogState.cs b/Dalamud/Game/Inventory/GameInventoryChangelogState.cs new file mode 100644 index 000000000..23e972419 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryChangelogState.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Game.Inventory; + +/// +/// Class representing a item's changelog state. +/// +internal enum GameInventoryChangelogState +{ + /// + /// Item was added to an inventory. + /// + Added, + + /// + /// Item was removed from an inventory. + /// + Removed, +} diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs new file mode 100644 index 000000000..286104c43 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -0,0 +1,98 @@ +using System.Runtime.CompilerServices; + +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace Dalamud.Game.Inventory; + +/// +/// Dalamud wrapper around a ClientStructs InventoryItem. +/// +public unsafe class GameInventoryItem +{ + private InventoryItem internalItem; + + /// + /// Initializes a new instance of the class. + /// + /// Inventory item to wrap. + internal GameInventoryItem(InventoryItem item) + { + this.internalItem = item; + } + + /// + /// Gets the container inventory type. + /// + public GameInventoryType ContainerType => (GameInventoryType)this.internalItem.Container; + + /// + /// Gets the inventory slot index this item is in. + /// + public uint InventorySlot => (uint)this.internalItem.Slot; + + /// + /// Gets the item id. + /// + public uint ItemId => this.internalItem.ItemID; + + /// + /// Gets the quantity of items in this item stack. + /// + public uint Quantity => this.internalItem.Quantity; + + /// + /// Gets the spiritbond of this item. + /// + public uint Spiritbond => this.internalItem.Spiritbond; + + /// + /// Gets the repair condition of this item. + /// + public uint Condition => this.internalItem.Condition; + + /// + /// Gets a value indicating whether the item is High Quality. + /// + public bool IsHq => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.HQ); + + /// + /// Gets a value indicating whether the item has a company crest applied. + /// + public bool IsCompanyCrestApplied => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.CompanyCrestApplied); + + /// + /// Gets a value indicating whether the item is a relic. + /// + public bool IsRelic => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.Relic); + + /// + /// Gets a value indicating whether the is a collectable. + /// + public bool IsCollectable => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.Collectable); + + /// + /// Gets the array of materia types. + /// + public ReadOnlySpan Materia => new(Unsafe.AsPointer(ref this.internalItem.Materia[0]), 5); + + /// + /// Gets the array of materia grades. + /// + public ReadOnlySpan MateriaGrade => new(Unsafe.AsPointer(ref this.internalItem.MateriaGrade[0]), 5); + + /// + /// Gets the color used for this item. + /// + public byte Stain => this.internalItem.Stain; + + /// + /// Gets the glamour id for this item. + /// + public uint GlmaourId => this.internalItem.GlamourID; + + /// + /// Gets the items crafter's content id. + /// NOTE: I'm not sure if this is a good idea to include or not in the dalamud api. Marked internal for now. + /// + internal ulong CrafterContentId => this.internalItem.CrafterContentID; +} diff --git a/Dalamud/Game/Inventory/GameInventoryType.cs b/Dalamud/Game/Inventory/GameInventoryType.cs new file mode 100644 index 000000000..733af32d3 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -0,0 +1,351 @@ +namespace Dalamud.Game.Inventory; + +/// +/// Enum representing various player inventories. +/// +public enum GameInventoryType : uint +{ + /// + /// First panel of main player inventory. + /// + Inventory1 = 0, + + /// + /// Second panel of main player inventory. + /// + Inventory2 = 1, + + /// + /// Third panel of main player inventory. + /// + Inventory3 = 2, + + /// + /// Fourth panel of main player inventory. + /// + Inventory4 = 3, + + /// + /// Items that are currently equipped by the player. + /// + EquippedItems = 1000, + + /// + /// Player currency container. + /// ie, gil, serpent seals, sacks of nuts. + /// + Currency = 2000, + + /// + /// Crystal container. + /// + Crystals = 2001, + + /// + /// Mail container. + /// + Mail = 2003, + + /// + /// Key item container. + /// + KeyItems = 2004, + + /// + /// Quest item hand-in inventory. + /// + HandIn = 2005, + + /// + /// DamagedGear container. + /// + DamagedGear = 2007, + + /// + /// Examine window container. + /// + Examine = 2009, + + /// + /// Doman Enclave Reconstruction Reclamation Box. + /// + ReconstructionBuyback = 2013, + + /// + /// Armory off-hand weapon container. + /// + ArmoryOffHand = 3200, + + /// + /// Armory head container. + /// + ArmoryHead = 3201, + + /// + /// Armory body container. + /// + ArmoryBody = 3202, + + /// + /// Armory hand/gloves container. + /// + ArmoryHands = 3203, + + /// + /// Armory waist container. + /// + /// This container should be unused as belt items were removed from the game in Shadowbringers. + /// + /// + ArmoryWaist = 3204, + + /// + /// Armory legs/pants/skirt container. + /// + ArmoryLegs = 3205, + + /// + /// Armory feet/boots/shoes container. + /// + ArmoryFeets = 3206, + + /// + /// Armory earring container. + /// + ArmoryEar = 3207, + + /// + /// Armory necklace container. + /// + ArmoryNeck = 3208, + + /// + /// Armory bracelet container. + /// + ArmoryWrist = 3209, + + /// + /// Armory ring container. + /// + ArmoryRings = 3300, + + /// + /// Armory soul crystal container. + /// + ArmorySoulCrystal = 3400, + + /// + /// Armory main-hand weapon container. + /// + ArmoryMainHand = 3500, + + /// + /// First panel of saddelbag inventory. + /// + SaddleBag1 = 4000, + + /// + /// Second panel of Saddlebag inventory. + /// + SaddleBag2 = 4001, + + /// + /// First panel of premium saddlebag inventory. + /// + PremiumSaddleBag1 = 4100, + + /// + /// Second panel of premium saddlebag inventory. + /// + PremiumSaddleBag2 = 4101, + + /// + /// First panel of retainer inventory. + /// + RetainerPage1 = 10000, + + /// + /// Second panel of retainer inventory. + /// + RetainerPage2 = 10001, + + /// + /// Third panel of retainer inventory. + /// + RetainerPage3 = 10002, + + /// + /// Fourth panel of retainer inventory. + /// + RetainerPage4 = 10003, + + /// + /// Fifth panel of retainer inventory. + /// + RetainerPage5 = 10004, + + /// + /// Sixth panel of retainer inventory. + /// + RetainerPage6 = 10005, + + /// + /// Seventh panel of retainer inventory. + /// + RetainerPage7 = 10006, + + /// + /// Retainer equipment container. + /// + RetainerEquippedItems = 11000, + + /// + /// Retainer currency container. + /// + RetainerGil = 12000, + + /// + /// Retainer crystal container. + /// + RetainerCrystals = 12001, + + /// + /// Retainer market item container. + /// + RetainerMarket = 12002, + + /// + /// First panel of Free Company inventory. + /// + FreeCompanyPage1 = 20000, + + /// + /// Second panel of Free Company inventory. + /// + FreeCompanyPage2 = 20001, + + /// + /// Third panel of Free Company inventory. + /// + FreeCompanyPage3 = 20002, + + /// + /// Fourth panel of Free Company inventory. + /// + FreeCompanyPage4 = 20003, + + /// + /// Fifth panel of Free Company inventory. + /// + FreeCompanyPage5 = 20004, + + /// + /// Free Company currency container. + /// + FreeCompanyGil = 22000, + + /// + /// Free Company crystal container. + /// + FreeCompanyCrystals = 22001, + + /// + /// Housing exterior appearance container. + /// + HousingExteriorAppearance = 25000, + + /// + /// Housing exterior placed items container. + /// + HousingExteriorPlacedItems = 25001, + + /// + /// Housing interior appearance container. + /// + HousingInteriorAppearance = 25002, + + /// + /// First panel of housing interior inventory. + /// + HousingInteriorPlacedItems1 = 25003, + + /// + /// Second panel of housing interior inventory. + /// + HousingInteriorPlacedItems2 = 25004, + + /// + /// Third panel of housing interior inventory. + /// + HousingInteriorPlacedItems3 = 25005, + + /// + /// Fourth panel of housing interior inventory. + /// + HousingInteriorPlacedItems4 = 25006, + + /// + /// Fifth panel of housing interior inventory. + /// + HousingInteriorPlacedItems5 = 25007, + + /// + /// Sixth panel of housing interior inventory. + /// + HousingInteriorPlacedItems6 = 25008, + + /// + /// Seventh panel of housing interior inventory. + /// + HousingInteriorPlacedItems7 = 25009, + + /// + /// Eighth panel of housing interior inventory. + /// + HousingInteriorPlacedItems8 = 25010, + + /// + /// Housing exterior storeroom inventory. + /// + HousingExteriorStoreroom = 27000, + + /// + /// First panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom1 = 27001, + + /// + /// Second panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom2 = 27002, + + /// + /// Third panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom3 = 27003, + + /// + /// Fourth panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom4 = 27004, + + /// + /// Fifth panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom5 = 27005, + + /// + /// Sixth panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom6 = 27006, + + /// + /// Seventh panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom7 = 27007, + + /// + /// Eighth panel of housing interior storeroom inventory. + /// + HousingInteriorStoreroom8 = 27008, +} diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs new file mode 100644 index 000000000..0e796e8d8 --- /dev/null +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -0,0 +1,69 @@ +using Dalamud.Game.Inventory; + +namespace Dalamud.Plugin.Services; + +/// +/// This class provides events for the in-game inventory. +/// +public interface IGameInventory +{ + /// + /// Delegate function for when an item is moved from one inventory to the next. + /// + /// Which inventory the item was moved from. + /// The slot this item was moved from. + /// Which inventory the item was moved to. + /// The slot this item was moved to. + /// The item moved. + public delegate void OnItemMovedDelegate(GameInventoryType source, uint sourceSlot, GameInventoryType destination, uint destinationSlot, GameInventoryItem item); + + /// + /// Delegate function for when an item is removed from an inventory. + /// + /// Which inventory the item was removed from. + /// The slot this item was removed from. + /// The item removed. + public delegate void OnItemRemovedDelegate(GameInventoryType source, uint sourceSlot, GameInventoryItem item); + + /// + /// Delegate function for when an item is added to an inventory. + /// + /// Which inventory the item was added to. + /// The slot this item was added to. + /// The item added. + public delegate void OnItemAddedDelegate(GameInventoryType destination, uint destinationSlot, GameInventoryItem item); + + /// + /// Delegate function for when an items properties are changed. + /// + /// Which inventory the item that was changed is in. + /// The slot the item that was changed is in. + /// The item changed. + public delegate void OnItemChangedDelegate(GameInventoryType inventory, uint slot, GameInventoryItem item); + + /// + /// Event that is fired when an item is moved from one inventory to another. + /// + public event OnItemMovedDelegate ItemMoved; + + /// + /// Event that is fired when an item is removed from one inventory. + /// + /// + /// This event will also be fired when an item is moved from one inventory to another. + /// + public event OnItemRemovedDelegate ItemRemoved; + + /// + /// Event that is fired when an item is added to one inventory. + /// + /// + /// This event will also be fired when an item is moved from one inventory to another. + /// + public event OnItemAddedDelegate ItemAdded; + + /// + /// Event that is fired when an items properties are changed. + /// + public event OnItemChangedDelegate ItemChanged; +} From 805615d9f4dcd6655efcc7bbbd848c0545b7f23e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:40:36 -0800 Subject: [PATCH 330/585] Fix incorrect equality operator --- Dalamud/Game/Inventory/GameInventory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 7cd2556e2..c9285b246 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -197,10 +197,10 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } private unsafe bool IsMateriaChanged(InventoryItem a, InventoryItem b) - => new ReadOnlySpan(a.Materia, 5) == new ReadOnlySpan(b.Materia, 5); + => new ReadOnlySpan(a.Materia, 5) != new ReadOnlySpan(b.Materia, 5); private unsafe bool IsMateriaGradeChanged(InventoryItem a, InventoryItem b) - => new ReadOnlySpan(a.MateriaGrade, 5) == new ReadOnlySpan(b.MateriaGrade, 5); + => new ReadOnlySpan(a.MateriaGrade, 5) != new ReadOnlySpan(b.MateriaGrade, 5); } /// From 5204bb723d824072bf415759b9e4f23c84d8c9d0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 16:47:54 +0900 Subject: [PATCH 331/585] Optimizations --- Dalamud/Game/Inventory/GameInventory.cs | 396 ++++++++++-------- .../Game/Inventory/GameInventoryChangelog.cs | 28 -- .../Inventory/GameInventoryChangelogState.cs | 17 - Dalamud/Game/Inventory/GameInventoryEvent.cs | 34 ++ Dalamud/Game/Inventory/GameInventoryItem.cs | 118 ++++-- Dalamud/Game/Inventory/GameInventoryType.cs | 7 +- Dalamud/Plugin/Services/IGameInventory.cs | 123 +++--- lib/FFXIVClientStructs | 2 +- 8 files changed, 424 insertions(+), 301 deletions(-) delete mode 100644 Dalamud/Game/Inventory/GameInventoryChangelog.cs delete mode 100644 Dalamud/Game/Inventory/GameInventoryChangelogState.cs create mode 100644 Dalamud/Game/Inventory/GameInventoryEvent.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index c9285b246..cfb22ca0d 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -1,11 +1,12 @@ using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; + using FFXIVClientStructs.FFXIV.Client.Game; namespace Dalamud.Game.Inventory; @@ -14,193 +15,258 @@ namespace Dalamud.Game.Inventory; /// This class provides events for the players in-game inventory. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal class GameInventory : IDisposable, IServiceType, IGameInventory { private static readonly ModuleLog Log = new("GameInventory"); + + private readonly List changelog = new(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - private readonly Dictionary> inventoryCache; + private readonly GameInventoryType[] inventoryTypes; + private readonly GameInventoryItem[][] inventoryItems; + private readonly unsafe GameInventoryItem*[] inventoryItemsPointers; [ServiceManager.ServiceConstructor] - private GameInventory() + private unsafe GameInventory() { - this.inventoryCache = new Dictionary>(); + this.inventoryTypes = Enum.GetValues(); - foreach (var inventoryType in Enum.GetValues()) + // Using GC.AllocateArray(pinned: true), so that Unsafe.AsPointer(ref array[0]) does not fail. + this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; + this.inventoryItemsPointers = new GameInventoryItem*[this.inventoryTypes.Length]; + for (var i = 0; i < this.inventoryItems.Length; i++) { - this.inventoryCache.Add(inventoryType, new Dictionary()); + this.inventoryItems[i] = GC.AllocateArray(1, true); + this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref this.inventoryItems[i][0]); } - + this.framework.Update += this.OnFrameworkUpdate; } /// - public event IGameInventory.OnItemMovedDelegate? ItemMoved; - - /// - public event IGameInventory.OnItemRemovedDelegate? ItemRemoved; - - /// - public event IGameInventory.OnItemAddedDelegate? ItemAdded; - - /// - public event IGameInventory.OnItemChangedDelegate? ItemChanged; + public event IGameInventory.InventoryChangeDelegate? InventoryChanged; /// public void Dispose() { this.framework.Update -= this.OnFrameworkUpdate; } - - private void OnFrameworkUpdate(IFramework framework1) - { - // If no one is listening for event's then we don't need to track anything. - if (!this.AnyListeners()) return; - var performanceMonitor = Stopwatch.StartNew(); - - var changelog = new List(); - - foreach (var (inventoryType, cachedInventoryItems) in this.inventoryCache) + /// + /// Gets a view of s, wrapped as . + /// + /// The inventory type. + /// The span. + private static unsafe Span GetItemsForInventory(GameInventoryType type) + { + var inventoryManager = InventoryManager.Instance(); + if (inventoryManager is null) return default; + + var inventory = inventoryManager->GetInventoryContainer((InventoryType)type); + if (inventory is null) return default; + + return new(inventory->Items, (int)inventory->Size); + } + + /// + /// Looks for the first index of , or the supposed position one should be if none could be found. + /// + /// The span to look in. + /// The type. + /// The index. + private static int FindTypeIndex(Span span, GameInventoryEvent type) + { + // Use linear lookup if span size is small enough + if (span.Length < 64) { - foreach (var item in this.GetItemsForInventory(inventoryType)) + var i = 0; + for (; i < span.Length; i++) { - if (cachedInventoryItems.TryGetValue(item.Slot, out var inventoryItem)) + if (type <= span[i].Type) + break; + } + + return i; + } + + var lo = 0; + var hi = span.Length - 1; + while (lo <= hi) + { + var i = lo + ((hi - lo) >> 1); + var type2 = span[i].Type; + if (type == type2) + return i; + if (type < type2) + lo = i + 1; + else + hi = i - 1; + } + + return lo; + } + + private unsafe void OnFrameworkUpdate(IFramework framework1) + { + // TODO: Uncomment this + // // If no one is listening for event's then we don't need to track anything. + // if (this.InventoryChanged is null) return; + + for (var i = 0; i < this.inventoryTypes.Length;) + { + var oldItemsArray = this.inventoryItems[i]; + var oldItemsLength = oldItemsArray.Length; + var oldItemsPointer = this.inventoryItemsPointers[i]; + + var resizeRequired = 0; + foreach (ref var newItem in GetItemsForInventory(this.inventoryTypes[i])) + { + var slot = newItem.InternalItem.Slot; + if (slot >= oldItemsLength) { - // Gained Item - // If the item we have cached has an item id of 0, then we expect it to be an empty slot. - // However, if the item we see in the game data has an item id that is not 0, then it now has an item. - if (inventoryItem.ItemID is 0 && item.ItemID is not 0) - { - var gameInventoryItem = new GameInventoryItem(item); - this.ItemAdded?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem); - changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Added, gameInventoryItem)); - - Log.Verbose($"New Item Added to {inventoryType}: {item.ItemID}"); - this.inventoryCache[inventoryType][item.Slot] = item; - } - - // Removed Item - // If the item we have cached has an item id of not 0, then we expect it to have an item. - // However, if the item we see in the game data has an item id that is 0, then it was removed from this inventory. - if (inventoryItem.ItemID is not 0 && item.ItemID is 0) - { - var gameInventoryItem = new GameInventoryItem(inventoryItem); - this.ItemRemoved?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem); - changelog.Add(new GameInventoryItemChangelog(GameInventoryChangelogState.Removed, gameInventoryItem)); - - Log.Verbose($"Item Removed from {inventoryType}: {inventoryItem.ItemID}"); - this.inventoryCache[inventoryType][item.Slot] = item; - } - - // Changed Item - // If the item we have cached, does not match the item that we see in the game data - // AND if neither item is empty, then the item has been changed. - if (this.IsItemChanged(inventoryItem, item) && inventoryItem.ItemID is not 0 && item.ItemID is not 0) - { - var gameInventoryItem = new GameInventoryItem(inventoryItem); - this.ItemChanged?.Invoke(inventoryType, (uint)item.Slot, gameInventoryItem); - - Log.Verbose($"Item Changed {inventoryType}: {inventoryItem.ItemID}"); - this.inventoryCache[inventoryType][item.Slot] = item; - } + resizeRequired = Math.Max(resizeRequired, slot + 1); + continue; + } + + // We already checked the range above. Go raw. + ref var oldItem = ref oldItemsPointer[slot]; + + if (oldItem.IsEmpty) + { + if (newItem.IsEmpty) + continue; + this.changelog.Add(new(GameInventoryEvent.Added, default, newItem)); } else { - cachedInventoryItems.Add(item.Slot, item); + if (newItem.IsEmpty) + this.changelog.Add(new(GameInventoryEvent.Removed, oldItem, default)); + else if (!oldItem.Equals(newItem)) + this.changelog.Add(new(GameInventoryEvent.Changed, oldItem, newItem)); + else + continue; } + + Log.Verbose($"[{this.changelog.Count - 1}] {this.changelog[^1]}"); + oldItem = newItem; + } + + // Did the max slot number get changed? + if (resizeRequired != 0) + { + // Resize our buffer, and then try again. + var oldItemsExpanded = GC.AllocateArray(resizeRequired, true); + oldItemsArray.CopyTo(oldItemsExpanded, 0); + this.inventoryItems[i] = oldItemsExpanded; + this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref oldItemsExpanded[0]); + } + else + { + // Proceed to the next inventory. + i++; } } - - // Resolve changelog for item moved - // Group all changelogs that have the same itemId, and check if there was an add and a remove event for that item. - foreach (var itemGroup in changelog.GroupBy(log => log.Item.ItemId)) + + // Was there any change? If not, stop further processing. + if (this.changelog.Count == 0) + return; + + try { - var hasAdd = false; - var hasRemove = false; - - foreach (var log in itemGroup) + // From this point, the size of changelog shall not change. + var span = CollectionsMarshal.AsSpan(this.changelog); + + span.Sort((a, b) => a.Type.CompareTo(b.Type)); + var addedFrom = FindTypeIndex(span, GameInventoryEvent.Added); + var removedFrom = FindTypeIndex(span, GameInventoryEvent.Removed); + var changedFrom = FindTypeIndex(span, GameInventoryEvent.Changed); + + // Resolve changelog for item moved, from 1 added + 1 removed + for (var iAdded = addedFrom; iAdded < removedFrom; iAdded++) { - switch (log.State) + ref var added = ref span[iAdded]; + for (var iRemoved = removedFrom; iRemoved < changedFrom; iRemoved++) { - case GameInventoryChangelogState.Added: - hasAdd = true; + ref var removed = ref span[iRemoved]; + if (added.Target.ItemId == removed.Source.ItemId) + { + span[iAdded] = new(GameInventoryEvent.Moved, span[iRemoved].Source, span[iAdded].Target); + span[iRemoved] = default; + Log.Verbose($"[{iAdded}] Interpreting instead as: {span[iAdded]}"); + Log.Verbose($"[{iRemoved}] Discarding"); break; - - case GameInventoryChangelogState.Removed: - hasRemove = true; - break; - - default: - throw new ArgumentOutOfRangeException(); + } } } - var itemMoved = hasAdd && hasRemove; - if (itemMoved) + // Resolve changelog for item moved, from 2 changeds + for (var i = changedFrom; i < this.changelog.Count; i++) { - var added = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Added); - var removed = itemGroup.FirstOrDefault(log => log.State == GameInventoryChangelogState.Removed); - if (added is null || removed is null) continue; - - this.ItemMoved?.Invoke(removed.Item.ContainerType, removed.Item.InventorySlot, added.Item.ContainerType, added.Item.InventorySlot, added.Item); - - Log.Verbose($"Item Moved {removed.Item.ContainerType}:{removed.Item.InventorySlot} -> {added.Item.ContainerType}:{added.Item.InventorySlot}: {added.Item.ItemId}"); + if (span[i].IsEmpty) + continue; + + ref var e1 = ref span[i]; + for (var j = i + 1; j < this.changelog.Count; j++) + { + ref var e2 = ref span[j]; + if (e1.Target.ItemId == e2.Source.ItemId && e1.Source.ItemId == e2.Target.ItemId) + { + if (e1.Target.IsEmpty) + { + // e1 got moved to e2 + e1 = new(GameInventoryEvent.Moved, e1.Source, e2.Target); + e2 = default; + Log.Verbose($"[{i}] Interpreting instead as: {e1}"); + Log.Verbose($"[{j}] Discarding"); + } + else if (e2.Target.IsEmpty) + { + // e2 got moved to e1 + e1 = new(GameInventoryEvent.Moved, e2.Source, e1.Target); + e2 = default; + Log.Verbose($"[{i}] Interpreting instead as: {e1}"); + Log.Verbose($"[{j}] Discarding"); + } + else + { + // e1 and e2 got swapped + (e1, e2) = (new(GameInventoryEvent.Moved, e1.Target, e2.Target), + new(GameInventoryEvent.Moved, e2.Target, e1.Target)); + + Log.Verbose($"[{i}] Interpreting instead as: {e1}"); + Log.Verbose($"[{j}] Interpreting instead as: {e2}"); + } + } + } } + + // Filter out the emptied out entries. + // We do not care about the order of items in the changelog anymore. + for (var i = 0; i < span.Length;) + { + if (span[i].IsEmpty) + { + span[i] = span[^1]; + span = span[..^1]; + } + else + { + i++; + } + } + + // Actually broadcast the changes to subscribers. + if (!span.IsEmpty) + this.InventoryChanged?.Invoke(span); + } + finally + { + this.changelog.Clear(); } - - var elapsed = performanceMonitor.Elapsed; - - Log.Verbose($"Processing Time: {elapsed.Ticks}ticks :: {elapsed.TotalMilliseconds}ms"); } - - private bool AnyListeners() - { - if (this.ItemMoved is not null) return true; - if (this.ItemRemoved is not null) return true; - if (this.ItemAdded is not null) return true; - if (this.ItemChanged is not null) return true; - - return false; - } - - private unsafe ReadOnlySpan GetItemsForInventory(GameInventoryType type) - { - var inventoryManager = InventoryManager.Instance(); - if (inventoryManager is null) return ReadOnlySpan.Empty; - - var inventory = inventoryManager->GetInventoryContainer((InventoryType)type); - if (inventory is null) return ReadOnlySpan.Empty; - - return new ReadOnlySpan(inventory->Items, (int)inventory->Size); - } - - private bool IsItemChanged(InventoryItem a, InventoryItem b) - { - if (a.Container != b.Container) return true; // Shouldn't be possible, but shouldn't hurt. - if (a.Slot != b.Slot) return true; // Shouldn't be possible, but shouldn't hurt. - if (a.ItemID != b.ItemID) return true; - if (a.Quantity != b.Quantity) return true; - if (a.Spiritbond != b.Spiritbond) return true; - if (a.Condition != b.Condition) return true; - if (a.Flags != b.Flags) return true; - if (a.CrafterContentID != b.CrafterContentID) return true; - if (this.IsMateriaChanged(a, b)) return true; - if (this.IsMateriaGradeChanged(a, b)) return true; - if (a.Stain != b.Stain) return true; - if (a.GlamourID != b.GlamourID) return true; - - return false; - } - - private unsafe bool IsMateriaChanged(InventoryItem a, InventoryItem b) - => new ReadOnlySpan(a.Materia, 5) != new ReadOnlySpan(b.Materia, 5); - - private unsafe bool IsMateriaGradeChanged(InventoryItem a, InventoryItem b) - => new ReadOnlySpan(a.MateriaGrade, 5) != new ReadOnlySpan(b.MateriaGrade, 5); } /// @@ -222,47 +288,19 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public GameInventoryPluginScoped() { - this.gameInventoryService.ItemMoved += this.OnItemMovedForward; - this.gameInventoryService.ItemRemoved += this.OnItemRemovedForward; - this.gameInventoryService.ItemAdded += this.OnItemAddedForward; - this.gameInventoryService.ItemChanged += this.OnItemChangedForward; + this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward; } /// - public event IGameInventory.OnItemMovedDelegate? ItemMoved; - - /// - public event IGameInventory.OnItemRemovedDelegate? ItemRemoved; - - /// - public event IGameInventory.OnItemAddedDelegate? ItemAdded; - - /// - public event IGameInventory.OnItemChangedDelegate? ItemChanged; + public event IGameInventory.InventoryChangeDelegate? InventoryChanged; /// public void Dispose() { - this.gameInventoryService.ItemMoved -= this.OnItemMovedForward; - this.gameInventoryService.ItemRemoved -= this.OnItemRemovedForward; - this.gameInventoryService.ItemAdded -= this.OnItemAddedForward; - this.gameInventoryService.ItemChanged -= this.OnItemChangedForward; - - this.ItemMoved = null; - this.ItemRemoved = null; - this.ItemAdded = null; - this.ItemChanged = null; + this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward; + this.InventoryChanged = null; } - private void OnItemMovedForward(GameInventoryType source, uint sourceSlot, GameInventoryType destination, uint destinationSlot, GameInventoryItem item) - => this.ItemMoved?.Invoke(source, sourceSlot, destination, destinationSlot, item); - - private void OnItemRemovedForward(GameInventoryType source, uint sourceSlot, GameInventoryItem item) - => this.ItemRemoved?.Invoke(source, sourceSlot, item); - - private void OnItemAddedForward(GameInventoryType destination, uint destinationSlot, GameInventoryItem item) - => this.ItemAdded?.Invoke(destination, destinationSlot, item); - - private void OnItemChangedForward(GameInventoryType inventory, uint slot, GameInventoryItem item) - => this.ItemChanged?.Invoke(inventory, slot, item); + private void OnInventoryChangedForward(ReadOnlySpan events) + => this.InventoryChanged?.Invoke(events); } diff --git a/Dalamud/Game/Inventory/GameInventoryChangelog.cs b/Dalamud/Game/Inventory/GameInventoryChangelog.cs deleted file mode 100644 index 52ada81e0..000000000 --- a/Dalamud/Game/Inventory/GameInventoryChangelog.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace Dalamud.Game.Inventory; - -/// -/// Class representing an inventory item change event. -/// -internal class GameInventoryItemChangelog -{ - /// - /// Initializes a new instance of the class. - /// - /// Item state. - /// Item. - internal GameInventoryItemChangelog(GameInventoryChangelogState state, GameInventoryItem item) - { - this.State = state; - this.Item = item; - } - - /// - /// Gets the state of this changelog event. - /// - internal GameInventoryChangelogState State { get; } - - /// - /// Gets the item for this changelog event. - /// - internal GameInventoryItem Item { get; } -} diff --git a/Dalamud/Game/Inventory/GameInventoryChangelogState.cs b/Dalamud/Game/Inventory/GameInventoryChangelogState.cs deleted file mode 100644 index 23e972419..000000000 --- a/Dalamud/Game/Inventory/GameInventoryChangelogState.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Dalamud.Game.Inventory; - -/// -/// Class representing a item's changelog state. -/// -internal enum GameInventoryChangelogState -{ - /// - /// Item was added to an inventory. - /// - Added, - - /// - /// Item was removed from an inventory. - /// - Removed, -} diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs new file mode 100644 index 000000000..c23d79f30 --- /dev/null +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -0,0 +1,34 @@ +namespace Dalamud.Game.Inventory; + +/// +/// Class representing a item's changelog state. +/// +[Flags] +public enum GameInventoryEvent +{ + /// + /// A value indicating that there was no event.
+ /// You should not see this value, unless you explicitly used it yourself, or APIs using this enum say otherwise. + ///
+ Empty = 0, + + /// + /// Item was added to an inventory. + /// + Added = 1 << 0, + + /// + /// Item was removed from an inventory. + /// + Removed = 1 << 1, + + /// + /// Properties are changed for an item in an inventory. + /// + Changed = 1 << 2, + + /// + /// Item has been moved, possibly across different inventories. + /// + Moved = 1 << 3, +} diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 286104c43..9073073cb 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -1,4 +1,6 @@ -using System.Runtime.CompilerServices; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Game; @@ -7,92 +9,160 @@ namespace Dalamud.Game.Inventory; /// /// Dalamud wrapper around a ClientStructs InventoryItem. /// -public unsafe class GameInventoryItem +[StructLayout(LayoutKind.Explicit, Size = StructSizeInBytes)] +public unsafe struct GameInventoryItem : IEquatable { - private InventoryItem internalItem; + /// + /// An empty instance of . + /// + internal static readonly GameInventoryItem Empty = default; /// - /// Initializes a new instance of the class. + /// The actual data. + /// + [FieldOffset(0)] + internal readonly InventoryItem InternalItem; + + private const int StructSizeInBytes = 0x38; + + /// + /// The view of the backing data, in . + /// + [FieldOffset(0)] + private fixed ulong dataUInt64[StructSizeInBytes / 0x8]; + + static GameInventoryItem() + { + Debug.Assert( + sizeof(InventoryItem) == StructSizeInBytes, + $"Definition of {nameof(InventoryItem)} has been changed. " + + $"Update {nameof(StructSizeInBytes)} to {sizeof(InventoryItem)} to accommodate for the size change."); + } + + /// + /// Initializes a new instance of the struct. /// /// Inventory item to wrap. - internal GameInventoryItem(InventoryItem item) - { - this.internalItem = item; - } + internal GameInventoryItem(InventoryItem item) => this.InternalItem = item; + + /// + /// Gets a value indicating whether the this is empty. + /// + public bool IsEmpty => this.InternalItem.ItemID == 0; /// /// Gets the container inventory type. /// - public GameInventoryType ContainerType => (GameInventoryType)this.internalItem.Container; + public GameInventoryType ContainerType => (GameInventoryType)this.InternalItem.Container; /// /// Gets the inventory slot index this item is in. /// - public uint InventorySlot => (uint)this.internalItem.Slot; + public uint InventorySlot => (uint)this.InternalItem.Slot; /// /// Gets the item id. /// - public uint ItemId => this.internalItem.ItemID; + public uint ItemId => this.InternalItem.ItemID; /// /// Gets the quantity of items in this item stack. /// - public uint Quantity => this.internalItem.Quantity; + public uint Quantity => this.InternalItem.Quantity; /// /// Gets the spiritbond of this item. /// - public uint Spiritbond => this.internalItem.Spiritbond; + public uint Spiritbond => this.InternalItem.Spiritbond; /// /// Gets the repair condition of this item. /// - public uint Condition => this.internalItem.Condition; + public uint Condition => this.InternalItem.Condition; /// /// Gets a value indicating whether the item is High Quality. /// - public bool IsHq => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.HQ); + public bool IsHq => (this.InternalItem.Flags & InventoryItem.ItemFlags.HQ) != 0; /// /// Gets a value indicating whether the item has a company crest applied. /// - public bool IsCompanyCrestApplied => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.CompanyCrestApplied); - + public bool IsCompanyCrestApplied => (this.InternalItem.Flags & InventoryItem.ItemFlags.CompanyCrestApplied) != 0; + /// /// Gets a value indicating whether the item is a relic. /// - public bool IsRelic => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.Relic); + public bool IsRelic => (this.InternalItem.Flags & InventoryItem.ItemFlags.Relic) != 0; /// /// Gets a value indicating whether the is a collectable. /// - public bool IsCollectable => this.internalItem.Flags.HasFlag(InventoryItem.ItemFlags.Collectable); + public bool IsCollectable => (this.InternalItem.Flags & InventoryItem.ItemFlags.Collectable) != 0; /// /// Gets the array of materia types. /// - public ReadOnlySpan Materia => new(Unsafe.AsPointer(ref this.internalItem.Materia[0]), 5); + public ReadOnlySpan Materia => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.Materia[0])), 5); /// /// Gets the array of materia grades. /// - public ReadOnlySpan MateriaGrade => new(Unsafe.AsPointer(ref this.internalItem.MateriaGrade[0]), 5); + public ReadOnlySpan MateriaGrade => + new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5); /// /// Gets the color used for this item. /// - public byte Stain => this.internalItem.Stain; + public byte Stain => this.InternalItem.Stain; /// /// Gets the glamour id for this item. /// - public uint GlmaourId => this.internalItem.GlamourID; + public uint GlmaourId => this.InternalItem.GlamourID; /// /// Gets the items crafter's content id. /// NOTE: I'm not sure if this is a good idea to include or not in the dalamud api. Marked internal for now. /// - internal ulong CrafterContentId => this.internalItem.CrafterContentID; + internal ulong CrafterContentId => this.InternalItem.CrafterContentID; + + public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r); + + public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r); + + /// + readonly bool IEquatable.Equals(GameInventoryItem other) => this.Equals(other); + + /// Indicates whether the current object is equal to another object of the same type. + /// An object to compare with this object. + /// true if the current object is equal to the parameter; otherwise, false. + public readonly bool Equals(in GameInventoryItem other) + { + for (var i = 0; i < StructSizeInBytes / 8; i++) + { + if (this.dataUInt64[i] != other.dataUInt64[i]) + return false; + } + + return true; + } + + /// + public override bool Equals(object obj) => obj is GameInventoryItem gii && this.Equals(gii); + + /// + public override int GetHashCode() + { + var k = 0x5a8447b91aff51b4UL; + for (var i = 0; i < StructSizeInBytes / 8; i++) + k ^= this.dataUInt64[i]; + return unchecked((int)(k ^ (k >> 32))); + } + + /// + public override string ToString() => + this.IsEmpty + ? "" + : $"Item #{this.ItemId} at slot {this.InventorySlot} in {this.ContainerType}"; } diff --git a/Dalamud/Game/Inventory/GameInventoryType.cs b/Dalamud/Game/Inventory/GameInventoryType.cs index 733af32d3..c982fa80f 100644 --- a/Dalamud/Game/Inventory/GameInventoryType.cs +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -3,7 +3,7 @@ /// /// Enum representing various player inventories. /// -public enum GameInventoryType : uint +public enum GameInventoryType : ushort { /// /// First panel of main player inventory. @@ -348,4 +348,9 @@ public enum GameInventoryType : uint /// Eighth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom8 = 27008, + + /// + /// An invalid value. + /// + Invalid = ushort.MaxValue, } diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 0e796e8d8..b2ffe64d0 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -8,62 +8,83 @@ namespace Dalamud.Plugin.Services; public interface IGameInventory { /// - /// Delegate function for when an item is moved from one inventory to the next. + /// Delegate function to be called when inventories have been changed. /// - /// Which inventory the item was moved from. - /// The slot this item was moved from. - /// Which inventory the item was moved to. - /// The slot this item was moved to. - /// The item moved. - public delegate void OnItemMovedDelegate(GameInventoryType source, uint sourceSlot, GameInventoryType destination, uint destinationSlot, GameInventoryItem item); - - /// - /// Delegate function for when an item is removed from an inventory. - /// - /// Which inventory the item was removed from. - /// The slot this item was removed from. - /// The item removed. - public delegate void OnItemRemovedDelegate(GameInventoryType source, uint sourceSlot, GameInventoryItem item); - - /// - /// Delegate function for when an item is added to an inventory. - /// - /// Which inventory the item was added to. - /// The slot this item was added to. - /// The item added. - public delegate void OnItemAddedDelegate(GameInventoryType destination, uint destinationSlot, GameInventoryItem item); - - /// - /// Delegate function for when an items properties are changed. - /// - /// Which inventory the item that was changed is in. - /// The slot the item that was changed is in. - /// The item changed. - public delegate void OnItemChangedDelegate(GameInventoryType inventory, uint slot, GameInventoryItem item); - - /// - /// Event that is fired when an item is moved from one inventory to another. - /// - public event OnItemMovedDelegate ItemMoved; + /// The events. + public delegate void InventoryChangeDelegate(ReadOnlySpan events); /// - /// Event that is fired when an item is removed from one inventory. + /// Event that is fired when the inventory has been changed. /// - /// - /// This event will also be fired when an item is moved from one inventory to another. - /// - public event OnItemRemovedDelegate ItemRemoved; + public event InventoryChangeDelegate InventoryChanged; /// - /// Event that is fired when an item is added to one inventory. + /// Argument for . /// - /// - /// This event will also be fired when an item is moved from one inventory to another. - /// - public event OnItemAddedDelegate ItemAdded; - - /// - /// Event that is fired when an items properties are changed. - /// - public event OnItemChangedDelegate ItemChanged; + public readonly struct GameInventoryEventArgs + { + /// + /// The type of the event. + /// + public readonly GameInventoryEvent Type; + + /// + /// The content of the item in the source inventory.
+ /// Relevant if is , , or . + ///
+ public readonly GameInventoryItem Source; + + /// + /// The content of the item in the target inventory
+ /// Relevant if is , , or . + ///
+ public readonly GameInventoryItem Target; + + /// + /// Initializes a new instance of the struct. + /// + /// The type of the event. + /// The source inventory item. + /// The target inventory item. + public GameInventoryEventArgs(GameInventoryEvent type, GameInventoryItem source, GameInventoryItem target) + { + this.Type = type; + this.Source = source; + this.Target = target; + } + + /// + /// Gets a value indicating whether this instance of contains no information. + /// + public bool IsEmpty => this.Type == GameInventoryEvent.Empty; + + // TODO: are the following two aliases useful? + + /// + /// Gets the type of the source inventory.
+ /// Relevant for and . + ///
+ public GameInventoryType SourceType => this.Source.ContainerType; + + /// + /// Gets the type of the target inventory.
+ /// Relevant for , , and + /// . + ///
+ public GameInventoryType TargetType => this.Target.ContainerType; + + /// + public override string ToString() => this.Type switch + { + GameInventoryEvent.Empty => + $"<{this.Type}>", + GameInventoryEvent.Added => + $"<{this.Type}> ({this.Target})", + GameInventoryEvent.Removed => + $"<{this.Type}> ({this.Source})", + GameInventoryEvent.Changed or GameInventoryEvent.Moved => + $"<{this.Type}> ({this.Source}) to ({this.Target})", + _ => $" {this.Source} => {this.Target}", + }; + } } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index cc6687524..090e0c244 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit cc668752416a8459a3c23345c51277e359803de8 +Subproject commit 090e0c244df668454616026188c1363e5d25a1bc From 000d16c553801e06c320e2099eb708f1e4625550 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 13:15:19 +0900 Subject: [PATCH 332/585] Assume the size of inventory does not change once it's set --- Dalamud/Game/Inventory/GameInventory.cs | 140 +++++++----------------- 1 file changed, 37 insertions(+), 103 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index cfb22ca0d..cac7d5266 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Dalamud.IoC; @@ -26,22 +25,13 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory private readonly Framework framework = Service.Get(); private readonly GameInventoryType[] inventoryTypes; - private readonly GameInventoryItem[][] inventoryItems; - private readonly unsafe GameInventoryItem*[] inventoryItemsPointers; + private readonly GameInventoryItem[]?[] inventoryItems; [ServiceManager.ServiceConstructor] - private unsafe GameInventory() + private GameInventory() { this.inventoryTypes = Enum.GetValues(); - - // Using GC.AllocateArray(pinned: true), so that Unsafe.AsPointer(ref array[0]) does not fail. this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; - this.inventoryItemsPointers = new GameInventoryItem*[this.inventoryTypes.Length]; - for (var i = 0; i < this.inventoryItems.Length; i++) - { - this.inventoryItems[i] = GC.AllocateArray(1, true); - this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref this.inventoryItems[i][0]); - } this.framework.Update += this.OnFrameworkUpdate; } @@ -70,69 +60,25 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory return new(inventory->Items, (int)inventory->Size); } - - /// - /// Looks for the first index of , or the supposed position one should be if none could be found. - /// - /// The span to look in. - /// The type. - /// The index. - private static int FindTypeIndex(Span span, GameInventoryEvent type) - { - // Use linear lookup if span size is small enough - if (span.Length < 64) - { - var i = 0; - for (; i < span.Length; i++) - { - if (type <= span[i].Type) - break; - } - - return i; - } - - var lo = 0; - var hi = span.Length - 1; - while (lo <= hi) - { - var i = lo + ((hi - lo) >> 1); - var type2 = span[i].Type; - if (type == type2) - return i; - if (type < type2) - lo = i + 1; - else - hi = i - 1; - } - - return lo; - } - private unsafe void OnFrameworkUpdate(IFramework framework1) + private void OnFrameworkUpdate(IFramework framework1) { // TODO: Uncomment this // // If no one is listening for event's then we don't need to track anything. // if (this.InventoryChanged is null) return; - for (var i = 0; i < this.inventoryTypes.Length;) + for (var i = 0; i < this.inventoryTypes.Length; i++) { - var oldItemsArray = this.inventoryItems[i]; - var oldItemsLength = oldItemsArray.Length; - var oldItemsPointer = this.inventoryItemsPointers[i]; + var newItems = GetItemsForInventory(this.inventoryTypes[i]); + if (newItems.IsEmpty) + continue; - var resizeRequired = 0; - foreach (ref var newItem in GetItemsForInventory(this.inventoryTypes[i])) + // Assumption: newItems is sorted by slots, and the last item has the highest slot number. + var oldItems = this.inventoryItems[i] ??= new GameInventoryItem[newItems[^1].InternalItem.Slot + 1]; + + foreach (ref var newItem in newItems) { - var slot = newItem.InternalItem.Slot; - if (slot >= oldItemsLength) - { - resizeRequired = Math.Max(resizeRequired, slot + 1); - continue; - } - - // We already checked the range above. Go raw. - ref var oldItem = ref oldItemsPointer[slot]; + ref var oldItem = ref oldItems[newItem.InternalItem.Slot]; if (oldItem.IsEmpty) { @@ -153,21 +99,6 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory Log.Verbose($"[{this.changelog.Count - 1}] {this.changelog[^1]}"); oldItem = newItem; } - - // Did the max slot number get changed? - if (resizeRequired != 0) - { - // Resize our buffer, and then try again. - var oldItemsExpanded = GC.AllocateArray(resizeRequired, true); - oldItemsArray.CopyTo(oldItemsExpanded, 0); - this.inventoryItems[i] = oldItemsExpanded; - this.inventoryItemsPointers[i] = (GameInventoryItem*)Unsafe.AsPointer(ref oldItemsExpanded[0]); - } - else - { - // Proceed to the next inventory. - i++; - } } // Was there any change? If not, stop further processing. @@ -179,65 +110,68 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // From this point, the size of changelog shall not change. var span = CollectionsMarshal.AsSpan(this.changelog); + // Ensure that changelog is in order of Added, Removed, and then Changed. span.Sort((a, b) => a.Type.CompareTo(b.Type)); - var addedFrom = FindTypeIndex(span, GameInventoryEvent.Added); - var removedFrom = FindTypeIndex(span, GameInventoryEvent.Removed); - var changedFrom = FindTypeIndex(span, GameInventoryEvent.Changed); + + var removedFrom = 0; + while (removedFrom < span.Length && span[removedFrom].Type != GameInventoryEvent.Removed) + removedFrom++; + + var changedFrom = removedFrom; + while (changedFrom < span.Length && span[changedFrom].Type != GameInventoryEvent.Changed) + changedFrom++; + + var addedSpan = span[..removedFrom]; + var removedSpan = span[removedFrom..changedFrom]; + var changedSpan = span[changedFrom..]; // Resolve changelog for item moved, from 1 added + 1 removed - for (var iAdded = addedFrom; iAdded < removedFrom; iAdded++) + foreach (ref var added in addedSpan) { - ref var added = ref span[iAdded]; - for (var iRemoved = removedFrom; iRemoved < changedFrom; iRemoved++) + foreach (ref var removed in removedSpan) { - ref var removed = ref span[iRemoved]; if (added.Target.ItemId == removed.Source.ItemId) { - span[iAdded] = new(GameInventoryEvent.Moved, span[iRemoved].Source, span[iAdded].Target); - span[iRemoved] = default; - Log.Verbose($"[{iAdded}] Interpreting instead as: {span[iAdded]}"); - Log.Verbose($"[{iRemoved}] Discarding"); + Log.Verbose($"Move: reinterpreting {removed} + {added}"); + added = new(GameInventoryEvent.Moved, removed.Source, added.Target); + removed = default; break; } } } // Resolve changelog for item moved, from 2 changeds - for (var i = changedFrom; i < this.changelog.Count; i++) + for (var i = 0; i < changedSpan.Length; i++) { if (span[i].IsEmpty) continue; - ref var e1 = ref span[i]; - for (var j = i + 1; j < this.changelog.Count; j++) + ref var e1 = ref changedSpan[i]; + for (var j = i + 1; j < changedSpan.Length; j++) { - ref var e2 = ref span[j]; + ref var e2 = ref changedSpan[j]; if (e1.Target.ItemId == e2.Source.ItemId && e1.Source.ItemId == e2.Target.ItemId) { if (e1.Target.IsEmpty) { // e1 got moved to e2 + Log.Verbose($"Move: reinterpreting {e1} + {e2}"); e1 = new(GameInventoryEvent.Moved, e1.Source, e2.Target); e2 = default; - Log.Verbose($"[{i}] Interpreting instead as: {e1}"); - Log.Verbose($"[{j}] Discarding"); } else if (e2.Target.IsEmpty) { // e2 got moved to e1 + Log.Verbose($"Move: reinterpreting {e2} + {e1}"); e1 = new(GameInventoryEvent.Moved, e2.Source, e1.Target); e2 = default; - Log.Verbose($"[{i}] Interpreting instead as: {e1}"); - Log.Verbose($"[{j}] Discarding"); } else { // e1 and e2 got swapped + Log.Verbose($"Move(Swap): reinterpreting {e1} + {e2}"); (e1, e2) = (new(GameInventoryEvent.Moved, e1.Target, e2.Target), new(GameInventoryEvent.Moved, e2.Target, e1.Target)); - - Log.Verbose($"[{i}] Interpreting instead as: {e1}"); - Log.Verbose($"[{j}] Interpreting instead as: {e2}"); } } } From 40575e1a8897a650f275280ce171053e81d00747 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 30 Nov 2023 21:28:37 -0800 Subject: [PATCH 333/585] Use ReadOnlySpan --- Dalamud/Game/Inventory/GameInventory.cs | 26 +++++++++++-------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index cac7d5266..d370574d7 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -50,7 +50,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory ///
/// The inventory type. /// The span. - private static unsafe Span GetItemsForInventory(GameInventoryType type) + private static unsafe ReadOnlySpan GetItemsForInventory(GameInventoryType type) { var inventoryManager = InventoryManager.Instance(); if (inventoryManager is null) return default; @@ -58,15 +58,11 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory var inventory = inventoryManager->GetInventoryContainer((InventoryType)type); if (inventory is null) return default; - return new(inventory->Items, (int)inventory->Size); + return new ReadOnlySpan(inventory->Items, (int)inventory->Size); } private void OnFrameworkUpdate(IFramework framework1) { - // TODO: Uncomment this - // // If no one is listening for event's then we don't need to track anything. - // if (this.InventoryChanged is null) return; - for (var i = 0; i < this.inventoryTypes.Length; i++) { var newItems = GetItemsForInventory(this.inventoryTypes[i]); @@ -76,7 +72,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // Assumption: newItems is sorted by slots, and the last item has the highest slot number. var oldItems = this.inventoryItems[i] ??= new GameInventoryItem[newItems[^1].InternalItem.Slot + 1]; - foreach (ref var newItem in newItems) + foreach (ref readonly var newItem in newItems) { ref var oldItem = ref oldItems[newItem.InternalItem.Slot]; @@ -84,14 +80,14 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { if (newItem.IsEmpty) continue; - this.changelog.Add(new(GameInventoryEvent.Added, default, newItem)); + this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Added, default, newItem)); } else { if (newItem.IsEmpty) - this.changelog.Add(new(GameInventoryEvent.Removed, oldItem, default)); + this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Removed, oldItem, default)); else if (!oldItem.Equals(newItem)) - this.changelog.Add(new(GameInventoryEvent.Changed, oldItem, newItem)); + this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Changed, oldItem, newItem)); else continue; } @@ -133,7 +129,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory if (added.Target.ItemId == removed.Source.ItemId) { Log.Verbose($"Move: reinterpreting {removed} + {added}"); - added = new(GameInventoryEvent.Moved, removed.Source, added.Target); + added = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, removed.Source, added.Target); removed = default; break; } @@ -156,22 +152,22 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { // e1 got moved to e2 Log.Verbose($"Move: reinterpreting {e1} + {e2}"); - e1 = new(GameInventoryEvent.Moved, e1.Source, e2.Target); + e1 = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e1.Source, e2.Target); e2 = default; } else if (e2.Target.IsEmpty) { // e2 got moved to e1 Log.Verbose($"Move: reinterpreting {e2} + {e1}"); - e1 = new(GameInventoryEvent.Moved, e2.Source, e1.Target); + e1 = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e2.Source, e1.Target); e2 = default; } else { // e1 and e2 got swapped Log.Verbose($"Move(Swap): reinterpreting {e1} + {e2}"); - (e1, e2) = (new(GameInventoryEvent.Moved, e1.Target, e2.Target), - new(GameInventoryEvent.Moved, e2.Target, e1.Target)); + (e1, e2) = (new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e1.Target, e2.Target), + new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e2.Target, e1.Target)); } } } From 7c6f98dc9fe6e7fbe5b97e82dbd6c46becff2630 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:18:33 -0800 Subject: [PATCH 334/585] Proposed API Surface --- Dalamud/Game/Inventory/GameInventory.cs | 178 +++++++++++++++--- Dalamud/Game/Inventory/GameInventoryEvent.cs | 2 +- Dalamud/Game/Inventory/GameInventoryItem.cs | 2 +- Dalamud/Game/Inventory/GameInventoryType.cs | 2 +- .../InventoryEventArgs.cs | 29 +++ .../InventoryItemAddedArgs.cs | 20 ++ .../InventoryItemChangedArgs.cs | 26 +++ .../InventoryItemMovedArgs.cs | 30 +++ .../InventoryItemRemovedArgs.cs | 20 ++ Dalamud/Plugin/Services/IGameInventory.cs | 95 +++------- 10 files changed, 311 insertions(+), 93 deletions(-) create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index d370574d7..c2603f1bf 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -8,7 +8,7 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; -namespace Dalamud.Game.Inventory; +namespace Dalamud.Game.GameInventory; /// /// This class provides events for the players in-game inventory. @@ -19,7 +19,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { private static readonly ModuleLog Log = new("GameInventory"); - private readonly List changelog = new(); + private readonly List changelog = new(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -37,7 +37,19 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } /// - public event IGameInventory.InventoryChangeDelegate? InventoryChanged; + public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemAdded; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemRemoved; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMoved; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemChanged; /// public void Dispose() @@ -80,16 +92,39 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { if (newItem.IsEmpty) continue; - this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Added, default, newItem)); + + this.changelog.Add(new InventoryItemAddedArgs + { + Item = newItem, + Inventory = newItem.ContainerType, + Slot = newItem.InventorySlot, + }); } else { if (newItem.IsEmpty) - this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Removed, oldItem, default)); + { + this.changelog.Add(new InventoryItemRemovedArgs + { + Item = oldItem, + Inventory = oldItem.ContainerType, + Slot = oldItem.InventorySlot, + }); + } else if (!oldItem.Equals(newItem)) - this.changelog.Add(new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Changed, oldItem, newItem)); + { + this.changelog.Add(new InventoryItemChangedArgs + { + OldItemState = oldItem, + Item = newItem, + Inventory = newItem.ContainerType, + Slot = newItem.InventorySlot, + }); + } else + { continue; + } } Log.Verbose($"[{this.changelog.Count - 1}] {this.changelog[^1]}"); @@ -126,48 +161,86 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { foreach (ref var removed in removedSpan) { - if (added.Target.ItemId == removed.Source.ItemId) + if (added.Item.ItemId == removed.Item.ItemId) { Log.Verbose($"Move: reinterpreting {removed} + {added}"); - added = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, removed.Source, added.Target); + added = new InventoryItemMovedArgs + { + Item = removed.Item, + SourceInventory = removed.Item.ContainerType, + SourceSlot = removed.Item.InventorySlot, + TargetInventory = added.Item.ContainerType, + TargetSlot = added.Item.InventorySlot, + }; removed = default; break; } } } - // Resolve changelog for item moved, from 2 changeds + // Resolve changelog for item moved, from 2 changes for (var i = 0; i < changedSpan.Length; i++) { - if (span[i].IsEmpty) + if (span[i].Type is GameInventoryEvent.Empty) continue; ref var e1 = ref changedSpan[i]; for (var j = i + 1; j < changedSpan.Length; j++) { ref var e2 = ref changedSpan[j]; - if (e1.Target.ItemId == e2.Source.ItemId && e1.Source.ItemId == e2.Target.ItemId) + if (e1.Item.ItemId == e2.Item.ItemId && e1.Item.ItemId == e2.Item.ItemId) { - if (e1.Target.IsEmpty) + if (e1.Item.IsEmpty) { // e1 got moved to e2 Log.Verbose($"Move: reinterpreting {e1} + {e2}"); - e1 = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e1.Source, e2.Target); + e1 = new InventoryItemMovedArgs + { + Item = e2.Item, + SourceInventory = e1.Item.ContainerType, + SourceSlot = e1.Item.InventorySlot, + TargetInventory = e2.Item.ContainerType, + TargetSlot = e2.Item.InventorySlot, + }; e2 = default; } - else if (e2.Target.IsEmpty) + else if (e2.Item.IsEmpty) { // e2 got moved to e1 Log.Verbose($"Move: reinterpreting {e2} + {e1}"); - e1 = new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e2.Source, e1.Target); + e1 = new InventoryItemMovedArgs + { + Item = e1.Item, + SourceInventory = e2.Item.ContainerType, + SourceSlot = e2.Item.InventorySlot, + TargetInventory = e1.Item.ContainerType, + TargetSlot = e1.Item.InventorySlot, + }; e2 = default; } else { // e1 and e2 got swapped Log.Verbose($"Move(Swap): reinterpreting {e1} + {e2}"); - (e1, e2) = (new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e1.Target, e2.Target), - new IGameInventory.GameInventoryEventArgs(GameInventoryEvent.Moved, e2.Target, e1.Target)); + var newEvent1 = new InventoryItemMovedArgs + { + Item = e2.Item, + SourceInventory = e1.Item.ContainerType, + SourceSlot = e1.Item.InventorySlot, + TargetInventory = e2.Item.ContainerType, + TargetSlot = e2.Item.InventorySlot, + }; + + var newEvent2 = new InventoryItemMovedArgs + { + Item = e1.Item, + SourceInventory = e2.Item.ContainerType, + SourceSlot = e2.Item.InventorySlot, + TargetInventory = e1.Item.ContainerType, + TargetSlot = e1.Item.InventorySlot, + }; + + (e1, e2) = (newEvent1, newEvent2); } } } @@ -177,7 +250,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // We do not care about the order of items in the changelog anymore. for (var i = 0; i < span.Length;) { - if (span[i].IsEmpty) + if (span[i] is null || span[i].Type is GameInventoryEvent.Empty) { span[i] = span[^1]; span = span[..^1]; @@ -190,7 +263,31 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // Actually broadcast the changes to subscribers. if (!span.IsEmpty) + { this.InventoryChanged?.Invoke(span); + + foreach (var change in span) + { + switch (change) + { + case InventoryItemAddedArgs: + this.ItemAdded?.Invoke(GameInventoryEvent.Added, change); + break; + + case InventoryItemRemovedArgs: + this.ItemRemoved?.Invoke(GameInventoryEvent.Removed, change); + break; + + case InventoryItemMovedArgs: + this.ItemMoved?.Invoke(GameInventoryEvent.Moved, change); + break; + + case InventoryItemChangedArgs: + this.ItemChanged?.Invoke(GameInventoryEvent.Changed, change); + break; + } + } + } } finally { @@ -219,18 +316,55 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public GameInventoryPluginScoped() { this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward; + this.gameInventoryService.ItemAdded += this.OnInventoryItemAddedForward; + this.gameInventoryService.ItemRemoved += this.OnInventoryItemRemovedForward; + this.gameInventoryService.ItemMoved += this.OnInventoryItemMovedForward; + this.gameInventoryService.ItemChanged += this.OnInventoryItemChangedForward; } - + /// - public event IGameInventory.InventoryChangeDelegate? InventoryChanged; - + public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemAdded; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemRemoved; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMoved; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemChanged; + /// public void Dispose() { this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward; + this.gameInventoryService.ItemAdded -= this.OnInventoryItemAddedForward; + this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; + this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; + this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; + this.InventoryChanged = null; + this.ItemAdded = null; + this.ItemRemoved = null; + this.ItemMoved = null; + this.ItemChanged = null; } - private void OnInventoryChangedForward(ReadOnlySpan events) + private void OnInventoryChangedForward(ReadOnlySpan events) => this.InventoryChanged?.Invoke(events); + + private void OnInventoryItemAddedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemAdded?.Invoke(type, data); + + private void OnInventoryItemRemovedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemRemoved?.Invoke(type, data); + + private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemMoved?.Invoke(type, data); + + private void OnInventoryItemChangedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemChanged?.Invoke(type, data); } diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs index c23d79f30..805306671 100644 --- a/Dalamud/Game/Inventory/GameInventoryEvent.cs +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory; +namespace Dalamud.Game.GameInventory; /// /// Class representing a item's changelog state. diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 9073073cb..794785e5c 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Game; -namespace Dalamud.Game.Inventory; +namespace Dalamud.Game.GameInventory; /// /// Dalamud wrapper around a ClientStructs InventoryItem. diff --git a/Dalamud/Game/Inventory/GameInventoryType.cs b/Dalamud/Game/Inventory/GameInventoryType.cs index c982fa80f..0eeeebe20 100644 --- a/Dalamud/Game/Inventory/GameInventoryType.cs +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory; +namespace Dalamud.Game.GameInventory; /// /// Enum representing various player inventories. diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs new file mode 100644 index 000000000..a427dc840 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs @@ -0,0 +1,29 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Abstract base class representing inventory changed events. +/// +public abstract class InventoryEventArgs +{ + /// + /// Gets the type of event for these args. + /// + public abstract GameInventoryEvent Type { get; } + + /// + /// Gets the item associated with this event. + /// This is a copy of the item data. + /// + required public GameInventoryItem Item { get; init; } + + /// + public override string ToString() => this.Type switch + { + GameInventoryEvent.Empty => $"<{this.Type}>", + GameInventoryEvent.Added => $"<{this.Type}> ({this.Item})", + GameInventoryEvent.Removed => $"<{this.Type}> ({this.Item})", + GameInventoryEvent.Changed => $"<{this.Type}> ({this.Item})", + GameInventoryEvent.Moved when this is InventoryItemMovedArgs args => $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {args.SourceSlot} in {args.SourceInventory}) to (slot {args.TargetSlot} in {args.TargetInventory})", + _ => $" {this.Item}", + }; +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs new file mode 100644 index 000000000..8d3e99823 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Represents the data associated with an item being added to an inventory. +/// +public class InventoryItemAddedArgs : InventoryEventArgs +{ + /// + public override GameInventoryEvent Type => GameInventoryEvent.Added; + + /// + /// Gets the inventory this item was added to. + /// + required public GameInventoryType Inventory { get; init; } + + /// + /// Gets the slot this item was added to. + /// + required public uint Slot { get; init; } +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs new file mode 100644 index 000000000..1e2632722 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs @@ -0,0 +1,26 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Represents the data associated with an items properties being changed. +/// This also includes an items stack count changing. +/// +public class InventoryItemChangedArgs : InventoryEventArgs +{ + /// + public override GameInventoryEvent Type => GameInventoryEvent.Changed; + + /// + /// Gets the inventory this item is in. + /// + required public GameInventoryType Inventory { get; init; } + + /// + /// Gets the inventory slot this item is in. + /// + required public uint Slot { get; init; } + + /// + /// Gets the state of the item from before it was changed. + /// + required public GameInventoryItem OldItemState { get; init; } +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs new file mode 100644 index 000000000..655f43445 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -0,0 +1,30 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Represents the data associated with an item being moved from one inventory and added to another. +/// +public class InventoryItemMovedArgs : InventoryEventArgs +{ + /// + public override GameInventoryEvent Type => GameInventoryEvent.Moved; + + /// + /// Gets the inventory this item was moved from. + /// + required public GameInventoryType SourceInventory { get; init; } + + /// + /// Gets the inventory this item was moved to. + /// + required public GameInventoryType TargetInventory { get; init; } + + /// + /// Gets the slot this item was moved from. + /// + required public uint SourceSlot { get; init; } + + /// + /// Gets the slot this item was moved to. + /// + required public uint TargetSlot { get; init; } +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs new file mode 100644 index 000000000..2d4db2384 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Game.GameInventory; + +/// +/// Represents the data associated with an item being removed from an inventory. +/// +public class InventoryItemRemovedArgs : InventoryEventArgs +{ + /// + public override GameInventoryEvent Type => GameInventoryEvent.Removed; + + /// + /// Gets the inventory this item was removed from. + /// + required public GameInventoryType Inventory { get; init; } + + /// + /// Gets the slot this item was removed from. + /// + required public uint Slot { get; init; } +} diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index b2ffe64d0..40b4bd84f 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -1,4 +1,4 @@ -using Dalamud.Game.Inventory; +using Dalamud.Game.GameInventory; namespace Dalamud.Plugin.Services; @@ -9,82 +9,41 @@ public interface IGameInventory { /// /// Delegate function to be called when inventories have been changed. + /// This delegate sends the entire set of changes recorded. /// /// The events. - public delegate void InventoryChangeDelegate(ReadOnlySpan events); + public delegate void InventoryChangelogDelegate(ReadOnlySpan events); + + /// + /// Delegate function to be called for each change to inventories. + /// This delegate sends individual events for changes. + /// + /// The event try that triggered this message. + /// Data for the triggered event. + public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data); /// /// Event that is fired when the inventory has been changed. /// - public event InventoryChangeDelegate InventoryChanged; - + public event InventoryChangelogDelegate InventoryChanged; + /// - /// Argument for . + /// Event that is fired when an item is added to an inventory. /// - public readonly struct GameInventoryEventArgs - { - /// - /// The type of the event. - /// - public readonly GameInventoryEvent Type; + public event InventoryChangedDelegate ItemAdded; - /// - /// The content of the item in the source inventory.
- /// Relevant if is , , or . - ///
- public readonly GameInventoryItem Source; - - /// - /// The content of the item in the target inventory
- /// Relevant if is , , or . - ///
- public readonly GameInventoryItem Target; + /// + /// Event that is fired when an item is removed from an inventory. + /// + public event InventoryChangedDelegate ItemRemoved; - /// - /// Initializes a new instance of the struct. - /// - /// The type of the event. - /// The source inventory item. - /// The target inventory item. - public GameInventoryEventArgs(GameInventoryEvent type, GameInventoryItem source, GameInventoryItem target) - { - this.Type = type; - this.Source = source; - this.Target = target; - } + /// + /// Event that is fired when an item is moved from one inventory into another. + /// + public event InventoryChangedDelegate ItemMoved; - /// - /// Gets a value indicating whether this instance of contains no information. - /// - public bool IsEmpty => this.Type == GameInventoryEvent.Empty; - - // TODO: are the following two aliases useful? - - /// - /// Gets the type of the source inventory.
- /// Relevant for and . - ///
- public GameInventoryType SourceType => this.Source.ContainerType; - - /// - /// Gets the type of the target inventory.
- /// Relevant for , , and - /// . - ///
- public GameInventoryType TargetType => this.Target.ContainerType; - - /// - public override string ToString() => this.Type switch - { - GameInventoryEvent.Empty => - $"<{this.Type}>", - GameInventoryEvent.Added => - $"<{this.Type}> ({this.Target})", - GameInventoryEvent.Removed => - $"<{this.Type}> ({this.Source})", - GameInventoryEvent.Changed or GameInventoryEvent.Moved => - $"<{this.Type}> ({this.Source}) to ({this.Target})", - _ => $" {this.Source} => {this.Target}", - }; - } + /// + /// Event that is fired when an items properties are changed. + /// + public event InventoryChangedDelegate ItemChanged; } From 34e3adb3f25028bac795c83e71d641d884dfd20d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 18:10:09 +0900 Subject: [PATCH 335/585] wip; needs testing and more thinking --- Dalamud/Game/Inventory/GameInventory.cs | 338 +++++++++--------- Dalamud/Game/Inventory/GameInventoryEvent.cs | 2 +- Dalamud/Game/Inventory/GameInventoryItem.cs | 2 +- Dalamud/Game/Inventory/GameInventoryType.cs | 2 +- .../InventoryEventArgs.cs | 30 +- .../InventoryItemAddedArgs.cs | 20 +- .../InventoryItemChangedArgs.cs | 26 +- .../InventoryItemMovedArgs.cs | 46 ++- .../InventoryItemRemovedArgs.cs | 18 +- Dalamud/Plugin/Services/IGameInventory.cs | 34 +- 10 files changed, 286 insertions(+), 232 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index c2603f1bf..4ee66ffaf 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; -using System.Runtime.InteropServices; +using Dalamud.Configuration.Internal; +using Dalamud.Game.Inventory.InventoryChangeArgsTypes; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; @@ -8,7 +9,9 @@ using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; -namespace Dalamud.Game.GameInventory; +using Serilog.Events; + +namespace Dalamud.Game.Inventory; /// /// This class provides events for the players in-game inventory. @@ -19,10 +22,17 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { private static readonly ModuleLog Log = new("GameInventory"); - private readonly List changelog = new(); + private readonly List allEvents = new(); + private readonly List addedEvents = new(); + private readonly List removedEvents = new(); + private readonly List changedEvents = new(); + private readonly List movedEvents = new(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; @@ -39,6 +49,9 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory /// public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; + /// + public event IGameInventory.InventoryChangelogDelegate? InventoryChangedRaw; + /// public event IGameInventory.InventoryChangedDelegate? ItemAdded; @@ -72,6 +85,32 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory return new ReadOnlySpan(inventory->Items, (int)inventory->Size); } + + private static void InvokeSafely( + IGameInventory.InventoryChangelogDelegate? cb, + IReadOnlyCollection data) + { + try + { + cb?.Invoke(data); + } + catch (Exception e) + { + Log.Error(e, "Exception during batch callback"); + } + } + + private static void InvokeSafely(IGameInventory.InventoryChangedDelegate? cb, InventoryEventArgs arg) + { + try + { + cb?.Invoke(arg.Type, arg); + } + catch (Exception e) + { + Log.Error(e, "Exception during {argType} callback", arg.Type); + } + } private void OnFrameworkUpdate(IFramework framework1) { @@ -90,208 +129,146 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory if (oldItem.IsEmpty) { - if (newItem.IsEmpty) - continue; - - this.changelog.Add(new InventoryItemAddedArgs + if (!newItem.IsEmpty) { - Item = newItem, - Inventory = newItem.ContainerType, - Slot = newItem.InventorySlot, - }); + this.addedEvents.Add(new(newItem)); + oldItem = newItem; + } } else { if (newItem.IsEmpty) { - this.changelog.Add(new InventoryItemRemovedArgs - { - Item = oldItem, - Inventory = oldItem.ContainerType, - Slot = oldItem.InventorySlot, - }); + this.removedEvents.Add(new(oldItem)); + oldItem = newItem; } else if (!oldItem.Equals(newItem)) { - this.changelog.Add(new InventoryItemChangedArgs - { - OldItemState = oldItem, - Item = newItem, - Inventory = newItem.ContainerType, - Slot = newItem.InventorySlot, - }); - } - else - { - continue; + this.changedEvents.Add(new(oldItem, newItem)); + oldItem = newItem; } } - - Log.Verbose($"[{this.changelog.Count - 1}] {this.changelog[^1]}"); - oldItem = newItem; } } // Was there any change? If not, stop further processing. - if (this.changelog.Count == 0) + // Note that... + // * this.movedEvents is not checked; it will be populated after this check. + // * this.allEvents is not checked; it is a temporary list to be used after this check. + if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0) return; try { - // From this point, the size of changelog shall not change. - var span = CollectionsMarshal.AsSpan(this.changelog); + // Broadcast InventoryChangedRaw, if necessary. + if (this.InventoryChangedRaw is not null) + { + this.allEvents.Clear(); + this.allEvents.EnsureCapacity( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count); + this.allEvents.AddRange(this.addedEvents); + this.allEvents.AddRange(this.removedEvents); + this.allEvents.AddRange(this.changedEvents); + InvokeSafely(this.InventoryChangedRaw, this.allEvents); + } - // Ensure that changelog is in order of Added, Removed, and then Changed. - span.Sort((a, b) => a.Type.CompareTo(b.Type)); + // Resolve changelog for item moved, from 1 added + 1 removed event. + for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) + { + var added = this.addedEvents[iAdded]; + for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved) + { + var removed = this.removedEvents[iRemoved]; + if (added.Item.ItemId != removed.Item.ItemId) + continue; + + this.movedEvents.Add(new(removed, added)); + + // Remove the reinterpreted entries. + this.addedEvents.RemoveAt(iAdded); + this.removedEvents.RemoveAt(iRemoved); + break; + } + } + + // Resolve changelog for item moved, from 2 changed events. + for (var i = this.changedEvents.Count - 1; i >= 0; --i) + { + var e1 = this.changedEvents[i]; + for (var j = i - 1; j >= 0; --j) + { + var e2 = this.changedEvents[j]; + if (e1.Item.ItemId != e2.Item.ItemId || e1.Item.ItemId != e2.Item.ItemId) + continue; + + // move happened, and e2 has an item + if (!e2.Item.IsEmpty) + this.movedEvents.Add(new(e1, e2)); + + // move happened, and e1 has an item + if (!e1.Item.IsEmpty) + this.movedEvents.Add(new(e2, e1)); + + // Remove the reinterpreted entries. Note that i > j. + this.changedEvents.RemoveAt(i); + this.changedEvents.RemoveAt(j); + break; + } + } + + // Log only if it matters. + if (this.dalamudConfiguration.LogLevel >= LogEventLevel.Verbose) + { + foreach (var x in this.addedEvents) + Log.Verbose($"{x}"); - var removedFrom = 0; - while (removedFrom < span.Length && span[removedFrom].Type != GameInventoryEvent.Removed) - removedFrom++; + foreach (var x in this.removedEvents) + Log.Verbose($"{x}"); - var changedFrom = removedFrom; - while (changedFrom < span.Length && span[changedFrom].Type != GameInventoryEvent.Changed) - changedFrom++; - - var addedSpan = span[..removedFrom]; - var removedSpan = span[removedFrom..changedFrom]; - var changedSpan = span[changedFrom..]; - - // Resolve changelog for item moved, from 1 added + 1 removed - foreach (ref var added in addedSpan) - { - foreach (ref var removed in removedSpan) - { - if (added.Item.ItemId == removed.Item.ItemId) - { - Log.Verbose($"Move: reinterpreting {removed} + {added}"); - added = new InventoryItemMovedArgs - { - Item = removed.Item, - SourceInventory = removed.Item.ContainerType, - SourceSlot = removed.Item.InventorySlot, - TargetInventory = added.Item.ContainerType, - TargetSlot = added.Item.InventorySlot, - }; - removed = default; - break; - } - } + foreach (var x in this.changedEvents) + Log.Verbose($"{x}"); + + foreach (var x in this.movedEvents) + Log.Verbose($"{x} (({x.SourceEvent}) + ({x.TargetEvent}))"); } - // Resolve changelog for item moved, from 2 changes - for (var i = 0; i < changedSpan.Length; i++) + // Broadcast InventoryChanged, if necessary. + if (this.InventoryChanged is not null) { - if (span[i].Type is GameInventoryEvent.Empty) - continue; - - ref var e1 = ref changedSpan[i]; - for (var j = i + 1; j < changedSpan.Length; j++) - { - ref var e2 = ref changedSpan[j]; - if (e1.Item.ItemId == e2.Item.ItemId && e1.Item.ItemId == e2.Item.ItemId) - { - if (e1.Item.IsEmpty) - { - // e1 got moved to e2 - Log.Verbose($"Move: reinterpreting {e1} + {e2}"); - e1 = new InventoryItemMovedArgs - { - Item = e2.Item, - SourceInventory = e1.Item.ContainerType, - SourceSlot = e1.Item.InventorySlot, - TargetInventory = e2.Item.ContainerType, - TargetSlot = e2.Item.InventorySlot, - }; - e2 = default; - } - else if (e2.Item.IsEmpty) - { - // e2 got moved to e1 - Log.Verbose($"Move: reinterpreting {e2} + {e1}"); - e1 = new InventoryItemMovedArgs - { - Item = e1.Item, - SourceInventory = e2.Item.ContainerType, - SourceSlot = e2.Item.InventorySlot, - TargetInventory = e1.Item.ContainerType, - TargetSlot = e1.Item.InventorySlot, - }; - e2 = default; - } - else - { - // e1 and e2 got swapped - Log.Verbose($"Move(Swap): reinterpreting {e1} + {e2}"); - var newEvent1 = new InventoryItemMovedArgs - { - Item = e2.Item, - SourceInventory = e1.Item.ContainerType, - SourceSlot = e1.Item.InventorySlot, - TargetInventory = e2.Item.ContainerType, - TargetSlot = e2.Item.InventorySlot, - }; - - var newEvent2 = new InventoryItemMovedArgs - { - Item = e1.Item, - SourceInventory = e2.Item.ContainerType, - SourceSlot = e2.Item.InventorySlot, - TargetInventory = e1.Item.ContainerType, - TargetSlot = e1.Item.InventorySlot, - }; - - (e1, e2) = (newEvent1, newEvent2); - } - } - } + this.allEvents.Clear(); + this.allEvents.EnsureCapacity( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count + + this.movedEvents.Count); + this.allEvents.AddRange(this.addedEvents); + this.allEvents.AddRange(this.removedEvents); + this.allEvents.AddRange(this.changedEvents); + this.allEvents.AddRange(this.movedEvents); + InvokeSafely(this.InventoryChanged, this.allEvents); } - // Filter out the emptied out entries. - // We do not care about the order of items in the changelog anymore. - for (var i = 0; i < span.Length;) - { - if (span[i] is null || span[i].Type is GameInventoryEvent.Empty) - { - span[i] = span[^1]; - span = span[..^1]; - } - else - { - i++; - } - } - - // Actually broadcast the changes to subscribers. - if (!span.IsEmpty) - { - this.InventoryChanged?.Invoke(span); - - foreach (var change in span) - { - switch (change) - { - case InventoryItemAddedArgs: - this.ItemAdded?.Invoke(GameInventoryEvent.Added, change); - break; - - case InventoryItemRemovedArgs: - this.ItemRemoved?.Invoke(GameInventoryEvent.Removed, change); - break; - - case InventoryItemMovedArgs: - this.ItemMoved?.Invoke(GameInventoryEvent.Moved, change); - break; - - case InventoryItemChangedArgs: - this.ItemChanged?.Invoke(GameInventoryEvent.Changed, change); - break; - } - } - } + // Broadcast the rest. + foreach (var x in this.addedEvents) + InvokeSafely(this.ItemAdded, x); + + foreach (var x in this.removedEvents) + InvokeSafely(this.ItemRemoved, x); + + foreach (var x in this.changedEvents) + InvokeSafely(this.ItemChanged, x); + + foreach (var x in this.movedEvents) + InvokeSafely(this.ItemMoved, x); } finally { - this.changelog.Clear(); + this.addedEvents.Clear(); + this.removedEvents.Clear(); + this.changedEvents.Clear(); + this.movedEvents.Clear(); } } } @@ -316,6 +293,7 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public GameInventoryPluginScoped() { this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward; + this.gameInventoryService.InventoryChangedRaw += this.OnInventoryChangedRawForward; this.gameInventoryService.ItemAdded += this.OnInventoryItemAddedForward; this.gameInventoryService.ItemRemoved += this.OnInventoryItemRemovedForward; this.gameInventoryService.ItemMoved += this.OnInventoryItemMovedForward; @@ -325,6 +303,9 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; + /// + public event IGameInventory.InventoryChangelogDelegate? InventoryChangedRaw; + /// public event IGameInventory.InventoryChangedDelegate? ItemAdded; @@ -341,20 +322,25 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public void Dispose() { this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward; + this.gameInventoryService.InventoryChangedRaw -= this.OnInventoryChangedRawForward; this.gameInventoryService.ItemAdded -= this.OnInventoryItemAddedForward; this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; this.InventoryChanged = null; + this.InventoryChangedRaw = null; this.ItemAdded = null; this.ItemRemoved = null; this.ItemMoved = null; this.ItemChanged = null; } - private void OnInventoryChangedForward(ReadOnlySpan events) + private void OnInventoryChangedForward(IReadOnlyCollection events) => this.InventoryChanged?.Invoke(events); + + private void OnInventoryChangedRawForward(IReadOnlyCollection events) + => this.InventoryChangedRaw?.Invoke(events); private void OnInventoryItemAddedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemAdded?.Invoke(type, data); diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs index 805306671..c23d79f30 100644 --- a/Dalamud/Game/Inventory/GameInventoryEvent.cs +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory; /// /// Class representing a item's changelog state. diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 794785e5c..9073073cb 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -4,7 +4,7 @@ using System.Runtime.InteropServices; using FFXIVClientStructs.FFXIV.Client.Game; -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory; /// /// Dalamud wrapper around a ClientStructs InventoryItem. diff --git a/Dalamud/Game/Inventory/GameInventoryType.cs b/Dalamud/Game/Inventory/GameInventoryType.cs index 0eeeebe20..c982fa80f 100644 --- a/Dalamud/Game/Inventory/GameInventoryType.cs +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory; /// /// Enum representing various player inventories. diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs index a427dc840..070d8a8db 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs @@ -1,29 +1,35 @@ -namespace Dalamud.Game.GameInventory; +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Abstract base class representing inventory changed events. /// +[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "It literally says , , and then . required is not an access modifier.")] public abstract class InventoryEventArgs { + /// + /// Initializes a new instance of the class. + /// + /// Type of the event. + /// Item about the event. + protected InventoryEventArgs(GameInventoryEvent type, in GameInventoryItem item) + { + this.Type = type; + this.Item = item; + } + /// /// Gets the type of event for these args. /// - public abstract GameInventoryEvent Type { get; } + public GameInventoryEvent Type { get; } /// /// Gets the item associated with this event. /// This is a copy of the item data. /// - required public GameInventoryItem Item { get; init; } + public GameInventoryItem Item { get; } /// - public override string ToString() => this.Type switch - { - GameInventoryEvent.Empty => $"<{this.Type}>", - GameInventoryEvent.Added => $"<{this.Type}> ({this.Item})", - GameInventoryEvent.Removed => $"<{this.Type}> ({this.Item})", - GameInventoryEvent.Changed => $"<{this.Type}> ({this.Item})", - GameInventoryEvent.Moved when this is InventoryItemMovedArgs args => $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {args.SourceSlot} in {args.SourceInventory}) to (slot {args.TargetSlot} in {args.TargetInventory})", - _ => $" {this.Item}", - }; + public override string ToString() => $"<{this.Type}> ({this.Item})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs index 8d3e99823..f68b23106 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs @@ -1,20 +1,26 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an item being added to an inventory. /// public class InventoryItemAddedArgs : InventoryEventArgs { - /// - public override GameInventoryEvent Type => GameInventoryEvent.Added; - + /// + /// Initializes a new instance of the class. + /// + /// The item. + internal InventoryItemAddedArgs(in GameInventoryItem item) + : base(GameInventoryEvent.Added, item) + { + } + /// /// Gets the inventory this item was added to. /// - required public GameInventoryType Inventory { get; init; } - + public GameInventoryType Inventory => this.Item.ContainerType; + /// /// Gets the slot this item was added to. /// - required public uint Slot { get; init; } + public uint Slot => this.Item.InventorySlot; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs index 1e2632722..1c47d3b83 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an items properties being changed. @@ -6,21 +6,29 @@ /// public class InventoryItemChangedArgs : InventoryEventArgs { - /// - public override GameInventoryEvent Type => GameInventoryEvent.Changed; - + /// + /// Initializes a new instance of the class. + /// + /// The item before change. + /// The item after change. + internal InventoryItemChangedArgs(in GameInventoryItem oldItem, in GameInventoryItem newItem) + : base(GameInventoryEvent.Changed, newItem) + { + this.OldItemState = oldItem; + } + /// /// Gets the inventory this item is in. /// - required public GameInventoryType Inventory { get; init; } - + public GameInventoryType Inventory => this.Item.ContainerType; + /// /// Gets the inventory slot this item is in. /// - required public uint Slot { get; init; } - + public uint Slot => this.Item.InventorySlot; + /// /// Gets the state of the item from before it was changed. /// - required public GameInventoryItem OldItemState { get; init; } + public GameInventoryItem OldItemState { get; init; } } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs index 655f43445..2f1113b02 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -1,30 +1,56 @@ -namespace Dalamud.Game.GameInventory; +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an item being moved from one inventory and added to another. /// +[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "It literally says , , and then . required is not an access modifier.")] public class InventoryItemMovedArgs : InventoryEventArgs { - /// - public override GameInventoryEvent Type => GameInventoryEvent.Moved; - + /// + /// Initializes a new instance of the class. + /// + /// The item at before slot. + /// The item at after slot. + internal InventoryItemMovedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) + : base(GameInventoryEvent.Moved, targetEvent.Item) + { + this.SourceEvent = sourceEvent; + this.TargetEvent = targetEvent; + } + /// /// Gets the inventory this item was moved from. /// - required public GameInventoryType SourceInventory { get; init; } - + public GameInventoryType SourceInventory => this.SourceEvent.Item.ContainerType; + /// /// Gets the inventory this item was moved to. /// - required public GameInventoryType TargetInventory { get; init; } + public GameInventoryType TargetInventory => this.Item.ContainerType; /// /// Gets the slot this item was moved from. /// - required public uint SourceSlot { get; init; } - + public uint SourceSlot => this.SourceEvent.Item.InventorySlot; + /// /// Gets the slot this item was moved to. /// - required public uint TargetSlot { get; init; } + public uint TargetSlot => this.Item.InventorySlot; + + /// + /// Gets the associated source event. + /// + internal InventoryEventArgs SourceEvent { get; } + + /// + /// Gets the associated target event. + /// + internal InventoryEventArgs TargetEvent { get; } + + /// + public override string ToString() => + $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {this.SourceSlot} in {this.SourceInventory}) to (slot {this.TargetSlot} in {this.TargetInventory})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs index 2d4db2384..bd982d702 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs @@ -1,20 +1,26 @@ -namespace Dalamud.Game.GameInventory; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an item being removed from an inventory. /// public class InventoryItemRemovedArgs : InventoryEventArgs { - /// - public override GameInventoryEvent Type => GameInventoryEvent.Removed; - + /// + /// Initializes a new instance of the class. + /// + /// The item. + internal InventoryItemRemovedArgs(in GameInventoryItem item) + : base(GameInventoryEvent.Removed, item) + { + } + /// /// Gets the inventory this item was removed from. /// - required public GameInventoryType Inventory { get; init; } + public GameInventoryType Inventory => this.Item.ContainerType; /// /// Gets the slot this item was removed from. /// - required public uint Slot { get; init; } + public uint Slot => this.Item.InventorySlot; } diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 40b4bd84f..979e2d6a6 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -1,4 +1,7 @@ -using Dalamud.Game.GameInventory; +using System.Collections.Generic; + +using Dalamud.Game.Inventory; +using Dalamud.Game.Inventory.InventoryChangeArgsTypes; namespace Dalamud.Plugin.Services; @@ -12,7 +15,7 @@ public interface IGameInventory /// This delegate sends the entire set of changes recorded. /// /// The events. - public delegate void InventoryChangelogDelegate(ReadOnlySpan events); + public delegate void InventoryChangelogDelegate(IReadOnlyCollection events); /// /// Delegate function to be called for each change to inventories. @@ -26,24 +29,37 @@ public interface IGameInventory /// Event that is fired when the inventory has been changed. /// public event InventoryChangelogDelegate InventoryChanged; + + /// + /// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes + /// as a move event as appropriate.
+ /// In other words, does not fire in this event. + ///
+ public event InventoryChangelogDelegate InventoryChangedRaw; /// - /// Event that is fired when an item is added to an inventory. + /// Event that is fired when an item is added to an inventory.
+ /// If an accompanying item remove event happens, then will be called instead.
+ /// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemAdded; /// - /// Event that is fired when an item is removed from an inventory. + /// Event that is fired when an item is removed from an inventory.
+ /// If an accompanying item add event happens, then will be called instead.
+ /// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemRemoved; + /// + /// Event that is fired when an items properties are changed.
+ /// If an accompanying item change event happens, then will be called instead.
+ /// Use if you do not want such reinterpretation. + ///
+ public event InventoryChangedDelegate ItemChanged; + /// /// Event that is fired when an item is moved from one inventory into another. /// public event InventoryChangedDelegate ItemMoved; - - /// - /// Event that is fired when an items properties are changed. - /// - public event InventoryChangedDelegate ItemChanged; } From 35f4ff5c94674b823f7629953bb0e6f3dd29eac7 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 20:55:46 +0900 Subject: [PATCH 336/585] wip --- Dalamud/Game/Inventory/GameInventory.cs | 250 +++++++++--------- Dalamud/Game/Inventory/GameInventoryEvent.cs | 17 +- Dalamud/Game/Inventory/GameInventoryItem.cs | 15 +- Dalamud/Game/Inventory/GameInventoryType.cs | 120 ++++----- .../InventoryEventArgs.cs | 16 +- .../InventoryItemChangedArgs.cs | 8 +- .../InventoryItemMovedArgs.cs | 9 +- .../InventoryItemRemovedArgs.cs | 4 +- Dalamud/Plugin/Services/IGameInventory.cs | 4 +- 9 files changed, 226 insertions(+), 217 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 4ee66ffaf..5842996b6 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; +using System.Linq; using Dalamud.Configuration.Internal; using Dalamud.Game.Inventory.InventoryChangeArgsTypes; @@ -22,21 +24,20 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { private static readonly ModuleLog Log = new("GameInventory"); - private readonly List allEvents = new(); private readonly List addedEvents = new(); private readonly List removedEvents = new(); private readonly List changedEvents = new(); private readonly List movedEvents = new(); - + [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - + [ServiceManager.ServiceDependency] private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; - + [ServiceManager.ServiceConstructor] private GameInventory() { @@ -59,10 +60,10 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory public event IGameInventory.InventoryChangedDelegate? ItemRemoved; /// - public event IGameInventory.InventoryChangedDelegate? ItemMoved; + public event IGameInventory.InventoryChangedDelegate? ItemChanged; /// - public event IGameInventory.InventoryChangedDelegate? ItemChanged; + public event IGameInventory.InventoryChangedDelegate? ItemMoved; /// public void Dispose() @@ -111,7 +112,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory Log.Error(e, "Exception during {argType} callback", arg.Type); } } - + private void OnFrameworkUpdate(IFramework framework1) { for (var i = 0; i < this.inventoryTypes.Length; i++) @@ -152,124 +153,135 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } // Was there any change? If not, stop further processing. - // Note that... - // * this.movedEvents is not checked; it will be populated after this check. - // * this.allEvents is not checked; it is a temporary list to be used after this check. + // Note that this.movedEvents is not checked; it will be populated after this check. if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0) return; - try + // Broadcast InventoryChangedRaw. + InvokeSafely( + this.InventoryChangedRaw, + new DeferredReadOnlyCollection( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count, + () => Array.Empty() + .Concat(this.addedEvents) + .Concat(this.removedEvents) + .Concat(this.changedEvents))); + + // Resolve changelog for item moved, from 1 added + 1 removed event. + for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) { - // Broadcast InventoryChangedRaw, if necessary. - if (this.InventoryChangedRaw is not null) + var added = this.addedEvents[iAdded]; + for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved) { - this.allEvents.Clear(); - this.allEvents.EnsureCapacity( - this.addedEvents.Count - + this.removedEvents.Count - + this.changedEvents.Count); - this.allEvents.AddRange(this.addedEvents); - this.allEvents.AddRange(this.removedEvents); - this.allEvents.AddRange(this.changedEvents); - InvokeSafely(this.InventoryChangedRaw, this.allEvents); - } + var removed = this.removedEvents[iRemoved]; + if (added.Item.ItemId != removed.Item.ItemId) + continue; - // Resolve changelog for item moved, from 1 added + 1 removed event. - for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) + this.movedEvents.Add(new(removed, added)); + + // Remove the reinterpreted entries. + this.addedEvents.RemoveAt(iAdded); + this.removedEvents.RemoveAt(iRemoved); + break; + } + } + + // Resolve changelog for item moved, from 2 changed events. + for (var i = this.changedEvents.Count - 1; i >= 0; --i) + { + var e1 = this.changedEvents[i]; + for (var j = i - 1; j >= 0; --j) { - var added = this.addedEvents[iAdded]; - for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved) - { - var removed = this.removedEvents[iRemoved]; - if (added.Item.ItemId != removed.Item.ItemId) - continue; + var e2 = this.changedEvents[j]; + if (e1.Item.ItemId != e2.Item.ItemId || e1.Item.ItemId != e2.Item.ItemId) + continue; - this.movedEvents.Add(new(removed, added)); - - // Remove the reinterpreted entries. - this.addedEvents.RemoveAt(iAdded); - this.removedEvents.RemoveAt(iRemoved); - break; - } + // move happened, and e2 has an item + if (!e2.Item.IsEmpty) + this.movedEvents.Add(new(e1, e2)); + + // move happened, and e1 has an item + if (!e1.Item.IsEmpty) + this.movedEvents.Add(new(e2, e1)); + + // Remove the reinterpreted entries. Note that i > j. + this.changedEvents.RemoveAt(i); + this.changedEvents.RemoveAt(j); + break; } + } - // Resolve changelog for item moved, from 2 changed events. - for (var i = this.changedEvents.Count - 1; i >= 0; --i) - { - var e1 = this.changedEvents[i]; - for (var j = i - 1; j >= 0; --j) - { - var e2 = this.changedEvents[j]; - if (e1.Item.ItemId != e2.Item.ItemId || e1.Item.ItemId != e2.Item.ItemId) - continue; - - // move happened, and e2 has an item - if (!e2.Item.IsEmpty) - this.movedEvents.Add(new(e1, e2)); - - // move happened, and e1 has an item - if (!e1.Item.IsEmpty) - this.movedEvents.Add(new(e2, e1)); - - // Remove the reinterpreted entries. Note that i > j. - this.changedEvents.RemoveAt(i); - this.changedEvents.RemoveAt(j); - break; - } - } - - // Log only if it matters. - if (this.dalamudConfiguration.LogLevel >= LogEventLevel.Verbose) - { - foreach (var x in this.addedEvents) - Log.Verbose($"{x}"); - - foreach (var x in this.removedEvents) - Log.Verbose($"{x}"); - - foreach (var x in this.changedEvents) - Log.Verbose($"{x}"); - - foreach (var x in this.movedEvents) - Log.Verbose($"{x} (({x.SourceEvent}) + ({x.TargetEvent}))"); - } - - // Broadcast InventoryChanged, if necessary. - if (this.InventoryChanged is not null) - { - this.allEvents.Clear(); - this.allEvents.EnsureCapacity( - this.addedEvents.Count - + this.removedEvents.Count - + this.changedEvents.Count - + this.movedEvents.Count); - this.allEvents.AddRange(this.addedEvents); - this.allEvents.AddRange(this.removedEvents); - this.allEvents.AddRange(this.changedEvents); - this.allEvents.AddRange(this.movedEvents); - InvokeSafely(this.InventoryChanged, this.allEvents); - } - - // Broadcast the rest. + // Log only if it matters. + if (this.dalamudConfiguration.LogLevel <= LogEventLevel.Verbose) + { foreach (var x in this.addedEvents) - InvokeSafely(this.ItemAdded, x); - + Log.Verbose($"{x}"); + foreach (var x in this.removedEvents) - InvokeSafely(this.ItemRemoved, x); - + Log.Verbose($"{x}"); + foreach (var x in this.changedEvents) - InvokeSafely(this.ItemChanged, x); - + Log.Verbose($"{x}"); + foreach (var x in this.movedEvents) - InvokeSafely(this.ItemMoved, x); + Log.Verbose($"{x} (({x.SourceEvent}) + ({x.TargetEvent}))"); } - finally + + // Broadcast the rest. + InvokeSafely( + this.InventoryChanged, + new DeferredReadOnlyCollection( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count + + this.movedEvents.Count, + () => Array.Empty() + .Concat(this.addedEvents) + .Concat(this.removedEvents) + .Concat(this.changedEvents) + .Concat(this.movedEvents))); + + foreach (var x in this.addedEvents) + InvokeSafely(this.ItemAdded, x); + + foreach (var x in this.removedEvents) + InvokeSafely(this.ItemRemoved, x); + + foreach (var x in this.changedEvents) + InvokeSafely(this.ItemChanged, x); + + foreach (var x in this.movedEvents) + InvokeSafely(this.ItemMoved, x); + + // We're done using the lists. Clean them up. + this.addedEvents.Clear(); + this.removedEvents.Clear(); + this.changedEvents.Clear(); + this.movedEvents.Clear(); + } + + /// + /// A view of , so that the number of items + /// contained within can be known in advance, and it can be enumerated multiple times. + /// + /// The type of elements being enumerated. + private class DeferredReadOnlyCollection : IReadOnlyCollection + { + private readonly Func> enumerableGenerator; + + public DeferredReadOnlyCollection(int count, Func> enumerableGenerator) { - this.addedEvents.Clear(); - this.removedEvents.Clear(); - this.changedEvents.Clear(); - this.movedEvents.Clear(); + this.enumerableGenerator = enumerableGenerator; + this.Count = count; } + + public int Count { get; } + + public IEnumerator GetEnumerator() => this.enumerableGenerator().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.enumerableGenerator().GetEnumerator(); } } @@ -313,10 +325,10 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public event IGameInventory.InventoryChangedDelegate? ItemRemoved; /// - public event IGameInventory.InventoryChangedDelegate? ItemMoved; + public event IGameInventory.InventoryChangedDelegate? ItemChanged; /// - public event IGameInventory.InventoryChangedDelegate? ItemChanged; + public event IGameInventory.InventoryChangedDelegate? ItemMoved; /// public void Dispose() @@ -325,15 +337,15 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.InventoryChangedRaw -= this.OnInventoryChangedRawForward; this.gameInventoryService.ItemAdded -= this.OnInventoryItemAddedForward; this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; - this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; - + this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; + this.InventoryChanged = null; this.InventoryChangedRaw = null; this.ItemAdded = null; this.ItemRemoved = null; - this.ItemMoved = null; this.ItemChanged = null; + this.ItemMoved = null; } private void OnInventoryChangedForward(IReadOnlyCollection events) @@ -341,16 +353,16 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven private void OnInventoryChangedRawForward(IReadOnlyCollection events) => this.InventoryChangedRaw?.Invoke(events); - + private void OnInventoryItemAddedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemAdded?.Invoke(type, data); - + private void OnInventoryItemRemovedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemRemoved?.Invoke(type, data); - private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemMoved?.Invoke(type, data); - private void OnInventoryItemChangedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemChanged?.Invoke(type, data); + + private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemMoved?.Invoke(type, data); } diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs index c23d79f30..6a4bb86e2 100644 --- a/Dalamud/Game/Inventory/GameInventoryEvent.cs +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -3,7 +3,6 @@ /// /// Class representing a item's changelog state. /// -[Flags] public enum GameInventoryEvent { /// @@ -11,24 +10,24 @@ public enum GameInventoryEvent /// You should not see this value, unless you explicitly used it yourself, or APIs using this enum say otherwise. /// Empty = 0, - + /// /// Item was added to an inventory. /// - Added = 1 << 0, - + Added = 1, + /// /// Item was removed from an inventory. /// - Removed = 1 << 1, - + Removed = 2, + /// /// Properties are changed for an item in an inventory. /// - Changed = 1 << 2, - + Changed = 3, + /// /// Item has been moved, possibly across different inventories. /// - Moved = 1 << 3, + Moved = 4, } diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 9073073cb..52a5c5e3c 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -12,11 +12,6 @@ namespace Dalamud.Game.Inventory; [StructLayout(LayoutKind.Explicit, Size = StructSizeInBytes)] public unsafe struct GameInventoryItem : IEquatable { - /// - /// An empty instance of . - /// - internal static readonly GameInventoryItem Empty = default; - /// /// The actual data. /// @@ -104,7 +99,7 @@ public unsafe struct GameInventoryItem : IEquatable /// Gets the array of materia types. ///
public ReadOnlySpan Materia => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.Materia[0])), 5); - + /// /// Gets the array of materia grades. /// @@ -119,8 +114,8 @@ public unsafe struct GameInventoryItem : IEquatable /// /// Gets the glamour id for this item. /// - public uint GlmaourId => this.InternalItem.GlamourID; - + public uint GlamourId => this.InternalItem.GlamourID; + /// /// Gets the items crafter's content id. /// NOTE: I'm not sure if this is a good idea to include or not in the dalamud api. Marked internal for now. @@ -163,6 +158,6 @@ public unsafe struct GameInventoryItem : IEquatable /// public override string ToString() => this.IsEmpty - ? "" - : $"Item #{this.ItemId} at slot {this.InventorySlot} in {this.ContainerType}"; + ? "no item" + : $"item #{this.ItemId} at slot {this.InventorySlot} in {this.ContainerType}"; } diff --git a/Dalamud/Game/Inventory/GameInventoryType.cs b/Dalamud/Game/Inventory/GameInventoryType.cs index c982fa80f..00c65046f 100644 --- a/Dalamud/Game/Inventory/GameInventoryType.cs +++ b/Dalamud/Game/Inventory/GameInventoryType.cs @@ -9,17 +9,17 @@ public enum GameInventoryType : ushort /// First panel of main player inventory. /// Inventory1 = 0, - + /// /// Second panel of main player inventory. /// Inventory2 = 1, - + /// /// Third panel of main player inventory. /// Inventory3 = 2, - + /// /// Fourth panel of main player inventory. /// @@ -40,32 +40,32 @@ public enum GameInventoryType : ushort /// Crystal container. ///
Crystals = 2001, - + /// /// Mail container. /// Mail = 2003, - + /// /// Key item container. /// KeyItems = 2004, - + /// /// Quest item hand-in inventory. /// HandIn = 2005, - + /// /// DamagedGear container. /// DamagedGear = 2007, - + /// /// Examine window container. /// Examine = 2009, - + /// /// Doman Enclave Reconstruction Reclamation Box. /// @@ -75,22 +75,22 @@ public enum GameInventoryType : ushort /// Armory off-hand weapon container. ///
ArmoryOffHand = 3200, - + /// /// Armory head container. /// ArmoryHead = 3201, - + /// /// Armory body container. /// ArmoryBody = 3202, - + /// /// Armory hand/gloves container. /// ArmoryHands = 3203, - + /// /// Armory waist container. /// @@ -98,42 +98,42 @@ public enum GameInventoryType : ushort /// /// ArmoryWaist = 3204, - + /// /// Armory legs/pants/skirt container. /// ArmoryLegs = 3205, - + /// /// Armory feet/boots/shoes container. /// ArmoryFeets = 3206, - + /// /// Armory earring container. /// ArmoryEar = 3207, - + /// /// Armory necklace container. /// ArmoryNeck = 3208, - + /// /// Armory bracelet container. /// ArmoryWrist = 3209, - + /// /// Armory ring container. /// ArmoryRings = 3300, - + /// /// Armory soul crystal container. /// ArmorySoulCrystal = 3400, - + /// /// Armory main-hand weapon container. /// @@ -143,17 +143,17 @@ public enum GameInventoryType : ushort /// First panel of saddelbag inventory. ///
SaddleBag1 = 4000, - + /// /// Second panel of Saddlebag inventory. /// SaddleBag2 = 4001, - + /// /// First panel of premium saddlebag inventory. /// PremiumSaddleBag1 = 4100, - + /// /// Second panel of premium saddlebag inventory. /// @@ -163,52 +163,52 @@ public enum GameInventoryType : ushort /// First panel of retainer inventory. ///
RetainerPage1 = 10000, - + /// /// Second panel of retainer inventory. /// RetainerPage2 = 10001, - + /// /// Third panel of retainer inventory. /// RetainerPage3 = 10002, - + /// /// Fourth panel of retainer inventory. /// RetainerPage4 = 10003, - + /// /// Fifth panel of retainer inventory. /// RetainerPage5 = 10004, - + /// /// Sixth panel of retainer inventory. /// RetainerPage6 = 10005, - + /// /// Seventh panel of retainer inventory. /// RetainerPage7 = 10006, - + /// /// Retainer equipment container. /// RetainerEquippedItems = 11000, - + /// /// Retainer currency container. /// RetainerGil = 12000, - + /// /// Retainer crystal container. /// RetainerCrystals = 12001, - + /// /// Retainer market item container. /// @@ -218,32 +218,32 @@ public enum GameInventoryType : ushort /// First panel of Free Company inventory. ///
FreeCompanyPage1 = 20000, - + /// /// Second panel of Free Company inventory. /// FreeCompanyPage2 = 20001, - + /// /// Third panel of Free Company inventory. /// FreeCompanyPage3 = 20002, - + /// /// Fourth panel of Free Company inventory. /// FreeCompanyPage4 = 20003, - + /// /// Fifth panel of Free Company inventory. /// FreeCompanyPage5 = 20004, - + /// /// Free Company currency container. /// FreeCompanyGil = 22000, - + /// /// Free Company crystal container. /// @@ -253,102 +253,102 @@ public enum GameInventoryType : ushort /// Housing exterior appearance container. ///
HousingExteriorAppearance = 25000, - + /// /// Housing exterior placed items container. /// HousingExteriorPlacedItems = 25001, - + /// /// Housing interior appearance container. /// HousingInteriorAppearance = 25002, - + /// /// First panel of housing interior inventory. /// HousingInteriorPlacedItems1 = 25003, - + /// /// Second panel of housing interior inventory. /// HousingInteriorPlacedItems2 = 25004, - + /// /// Third panel of housing interior inventory. /// HousingInteriorPlacedItems3 = 25005, - + /// /// Fourth panel of housing interior inventory. /// HousingInteriorPlacedItems4 = 25006, - + /// /// Fifth panel of housing interior inventory. /// HousingInteriorPlacedItems5 = 25007, - + /// /// Sixth panel of housing interior inventory. /// HousingInteriorPlacedItems6 = 25008, - + /// /// Seventh panel of housing interior inventory. /// HousingInteriorPlacedItems7 = 25009, - + /// /// Eighth panel of housing interior inventory. /// HousingInteriorPlacedItems8 = 25010, - + /// /// Housing exterior storeroom inventory. /// HousingExteriorStoreroom = 27000, - + /// /// First panel of housing interior storeroom inventory. /// HousingInteriorStoreroom1 = 27001, - + /// /// Second panel of housing interior storeroom inventory. /// HousingInteriorStoreroom2 = 27002, - + /// /// Third panel of housing interior storeroom inventory. /// HousingInteriorStoreroom3 = 27003, - + /// /// Fourth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom4 = 27004, - + /// /// Fifth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom5 = 27005, - + /// /// Sixth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom6 = 27006, - + /// /// Seventh panel of housing interior storeroom inventory. /// HousingInteriorStoreroom7 = 27007, - + /// /// Eighth panel of housing interior storeroom inventory. /// HousingInteriorStoreroom8 = 27008, - + /// /// An invalid value. /// diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs index 070d8a8db..301715bf2 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs @@ -1,13 +1,12 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Abstract base class representing inventory changed events. /// -[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "It literally says , , and then . required is not an access modifier.")] public abstract class InventoryEventArgs { + private readonly GameInventoryItem item; + /// /// Initializes a new instance of the class. /// @@ -16,7 +15,7 @@ public abstract class InventoryEventArgs protected InventoryEventArgs(GameInventoryEvent type, in GameInventoryItem item) { this.Type = type; - this.Item = item; + this.item = item; } /// @@ -28,8 +27,11 @@ public abstract class InventoryEventArgs /// Gets the item associated with this event. /// This is a copy of the item data. /// - public GameInventoryItem Item { get; } - + // impl note: we return a ref readonly view, to avoid making copies every time this property is accessed. + // see: https://devblogs.microsoft.com/premier-developer/avoiding-struct-and-readonly-reference-performance-pitfalls-with-errorprone-net/ + // "Consider using ref readonly locals and ref return for library code" + public ref readonly GameInventoryItem Item => ref this.item; + /// public override string ToString() => $"<{this.Type}> ({this.Item})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs index 1c47d3b83..1682ae32d 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs @@ -6,6 +6,8 @@ ///
public class InventoryItemChangedArgs : InventoryEventArgs { + private readonly GameInventoryItem oldItemState; + /// /// Initializes a new instance of the class. /// @@ -14,7 +16,7 @@ public class InventoryItemChangedArgs : InventoryEventArgs internal InventoryItemChangedArgs(in GameInventoryItem oldItem, in GameInventoryItem newItem) : base(GameInventoryEvent.Changed, newItem) { - this.OldItemState = oldItem; + this.oldItemState = oldItem; } /// @@ -29,6 +31,8 @@ public class InventoryItemChangedArgs : InventoryEventArgs /// /// Gets the state of the item from before it was changed. + /// This is a copy of the item data. /// - public GameInventoryItem OldItemState { get; init; } + // impl note: see InventoryEventArgs.Item. + public ref readonly GameInventoryItem OldItemState => ref this.oldItemState; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs index 2f1113b02..b6f490a2c 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -1,11 +1,8 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; /// /// Represents the data associated with an item being moved from one inventory and added to another. /// -[SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1206:Declaration keywords should follow order", Justification = "It literally says , , and then . required is not an access modifier.")] public class InventoryItemMovedArgs : InventoryEventArgs { /// @@ -29,7 +26,7 @@ public class InventoryItemMovedArgs : InventoryEventArgs /// Gets the inventory this item was moved to. /// public GameInventoryType TargetInventory => this.Item.ContainerType; - + /// /// Gets the slot this item was moved from. /// @@ -49,7 +46,7 @@ public class InventoryItemMovedArgs : InventoryEventArgs /// Gets the associated target event. /// internal InventoryEventArgs TargetEvent { get; } - + /// public override string ToString() => $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {this.SourceSlot} in {this.SourceInventory}) to (slot {this.TargetSlot} in {this.TargetInventory})"; diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs index bd982d702..41ca9d380 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs @@ -13,12 +13,12 @@ public class InventoryItemRemovedArgs : InventoryEventArgs : base(GameInventoryEvent.Removed, item) { } - + /// /// Gets the inventory this item was removed from. /// public GameInventoryType Inventory => this.Item.ContainerType; - + /// /// Gets the slot this item was removed from. /// diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 979e2d6a6..058bcbd27 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -24,12 +24,12 @@ public interface IGameInventory /// The event try that triggered this message. /// Data for the triggered event. public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data); - + /// /// Event that is fired when the inventory has been changed. /// public event InventoryChangelogDelegate InventoryChanged; - + /// /// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes /// as a move event as appropriate.
From f8dff15fe07847cc6a05a54215191443518dc11f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 22:02:08 +0900 Subject: [PATCH 337/585] fix bugs --- Dalamud/Game/Inventory/GameInventory.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 5842996b6..b5e3029b9 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -22,7 +22,7 @@ namespace Dalamud.Game.Inventory; [ServiceManager.BlockingEarlyLoadedService] internal class GameInventory : IDisposable, IServiceType, IGameInventory { - private static readonly ModuleLog Log = new("GameInventory"); + private static readonly ModuleLog Log = new(nameof(GameInventory)); private readonly List addedEvents = new(); private readonly List removedEvents = new(); @@ -195,20 +195,23 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory for (var j = i - 1; j >= 0; --j) { var e2 = this.changedEvents[j]; - if (e1.Item.ItemId != e2.Item.ItemId || e1.Item.ItemId != e2.Item.ItemId) + if (e1.Item.ItemId != e2.OldItemState.ItemId || e1.OldItemState.ItemId != e2.Item.ItemId) continue; - // move happened, and e2 has an item + // Move happened, and e2 has an item. if (!e2.Item.IsEmpty) this.movedEvents.Add(new(e1, e2)); - // move happened, and e1 has an item + // Move happened, and e1 has an item. if (!e1.Item.IsEmpty) this.movedEvents.Add(new(e2, e1)); // Remove the reinterpreted entries. Note that i > j. this.changedEvents.RemoveAt(i); this.changedEvents.RemoveAt(j); + + // We've removed two. Adjust the outer counter. + --i; break; } } From 6dd34ebda46d4984300ee00f0e21301fb3966201 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Dec 2023 22:35:44 +0900 Subject: [PATCH 338/585] support merge/split events --- Dalamud/Game/Inventory/GameInventory.cs | 108 +++++++++++++++--- Dalamud/Game/Inventory/GameInventoryEvent.cs | 10 ++ Dalamud/Game/Inventory/GameInventoryItem.cs | 4 +- .../InventoryComplexEventArgs.cs | 54 +++++++++ .../InventoryEventArgs.cs | 2 +- .../InventoryItemAddedArgs.cs | 2 +- .../InventoryItemChangedArgs.cs | 2 +- .../InventoryItemMergedArgs.cs | 26 +++++ .../InventoryItemMovedArgs.cs | 39 +------ .../InventoryItemRemovedArgs.cs | 2 +- .../InventoryItemSplitArgs.cs | 26 +++++ Dalamud/Plugin/Services/IGameInventory.cs | 19 ++- 12 files changed, 234 insertions(+), 60 deletions(-) create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs create mode 100644 Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index b5e3029b9..36e6756bf 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -28,6 +28,8 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory private readonly List removedEvents = new(); private readonly List changedEvents = new(); private readonly List movedEvents = new(); + private readonly List splitEvents = new(); + private readonly List mergedEvents = new(); [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -45,6 +47,21 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; this.framework.Update += this.OnFrameworkUpdate; + + // Separate log logic as an event handler. + this.InventoryChanged += events => + { + if (this.dalamudConfiguration.LogLevel > LogEventLevel.Verbose) + return; + + foreach (var e in events) + { + if (e is InventoryComplexEventArgs icea) + Log.Verbose($"{icea}\n\t├ {icea.SourceEvent}\n\t└ {icea.TargetEvent}"); + else + Log.Verbose($"{e}"); + } + }; } /// @@ -65,6 +82,12 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory /// public event IGameInventory.InventoryChangedDelegate? ItemMoved; + /// + public event IGameInventory.InventoryChangedDelegate? ItemSplit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMerged; + /// public void Dispose() { @@ -153,11 +176,12 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } // Was there any change? If not, stop further processing. - // Note that this.movedEvents is not checked; it will be populated after this check. + // Note that only these three are checked; the rest will be populated after this check. if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0) return; // Broadcast InventoryChangedRaw. + // Same reason with the above on why are there 3 lists of events involved. InvokeSafely( this.InventoryChangedRaw, new DeferredReadOnlyCollection( @@ -169,7 +193,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory .Concat(this.removedEvents) .Concat(this.changedEvents))); - // Resolve changelog for item moved, from 1 added + 1 removed event. + // Resolve moved items, from 1 added + 1 removed event. for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) { var added = this.addedEvents[iAdded]; @@ -188,7 +212,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } } - // Resolve changelog for item moved, from 2 changed events. + // Resolve moved items, from 2 changed events. for (var i = this.changedEvents.Count - 1; i >= 0; --i) { var e1 = this.changedEvents[i]; @@ -209,27 +233,49 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory // Remove the reinterpreted entries. Note that i > j. this.changedEvents.RemoveAt(i); this.changedEvents.RemoveAt(j); - + // We've removed two. Adjust the outer counter. --i; break; } } - // Log only if it matters. - if (this.dalamudConfiguration.LogLevel <= LogEventLevel.Verbose) + // Resolve split items, from 1 added + 1 changed event. + for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) { - foreach (var x in this.addedEvents) - Log.Verbose($"{x}"); + var added = this.addedEvents[iAdded]; + for (var iChanged = this.changedEvents.Count - 1; iChanged >= 0; --iChanged) + { + var changed = this.changedEvents[iChanged]; + if (added.Item.ItemId != changed.Item.ItemId || added.Item.ItemId != changed.OldItemState.ItemId) + continue; - foreach (var x in this.removedEvents) - Log.Verbose($"{x}"); + this.splitEvents.Add(new(changed, added)); - foreach (var x in this.changedEvents) - Log.Verbose($"{x}"); + // Remove the reinterpreted entries. + this.addedEvents.RemoveAt(iAdded); + this.changedEvents.RemoveAt(iChanged); + break; + } + } - foreach (var x in this.movedEvents) - Log.Verbose($"{x} (({x.SourceEvent}) + ({x.TargetEvent}))"); + // Resolve merged items, from 1 removed + 1 changed event. + for (var iRemoved = this.removedEvents.Count - 1; iRemoved >= 0; --iRemoved) + { + var removed = this.removedEvents[iRemoved]; + for (var iChanged = this.changedEvents.Count - 1; iChanged >= 0; --iChanged) + { + var changed = this.changedEvents[iChanged]; + if (removed.Item.ItemId != changed.Item.ItemId || removed.Item.ItemId != changed.OldItemState.ItemId) + continue; + + this.mergedEvents.Add(new(removed, changed)); + + // Remove the reinterpreted entries. + this.removedEvents.RemoveAt(iRemoved); + this.changedEvents.RemoveAt(iChanged); + break; + } } // Broadcast the rest. @@ -239,12 +285,16 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory this.addedEvents.Count + this.removedEvents.Count + this.changedEvents.Count + - this.movedEvents.Count, + this.movedEvents.Count + + this.splitEvents.Count + + this.mergedEvents.Count, () => Array.Empty() .Concat(this.addedEvents) .Concat(this.removedEvents) .Concat(this.changedEvents) - .Concat(this.movedEvents))); + .Concat(this.movedEvents) + .Concat(this.splitEvents) + .Concat(this.mergedEvents))); foreach (var x in this.addedEvents) InvokeSafely(this.ItemAdded, x); @@ -258,11 +308,19 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory foreach (var x in this.movedEvents) InvokeSafely(this.ItemMoved, x); + foreach (var x in this.splitEvents) + InvokeSafely(this.ItemSplit, x); + + foreach (var x in this.mergedEvents) + InvokeSafely(this.ItemMerged, x); + // We're done using the lists. Clean them up. this.addedEvents.Clear(); this.removedEvents.Clear(); this.changedEvents.Clear(); this.movedEvents.Clear(); + this.splitEvents.Clear(); + this.mergedEvents.Clear(); } /// @@ -313,6 +371,8 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.ItemRemoved += this.OnInventoryItemRemovedForward; this.gameInventoryService.ItemMoved += this.OnInventoryItemMovedForward; this.gameInventoryService.ItemChanged += this.OnInventoryItemChangedForward; + this.gameInventoryService.ItemSplit += this.OnInventoryItemSplitForward; + this.gameInventoryService.ItemMerged += this.OnInventoryItemMergedForward; } /// @@ -333,6 +393,12 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public event IGameInventory.InventoryChangedDelegate? ItemMoved; + /// + public event IGameInventory.InventoryChangedDelegate? ItemSplit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMerged; + /// public void Dispose() { @@ -342,6 +408,8 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; + this.gameInventoryService.ItemSplit -= this.OnInventoryItemSplitForward; + this.gameInventoryService.ItemMerged -= this.OnInventoryItemMergedForward; this.InventoryChanged = null; this.InventoryChangedRaw = null; @@ -349,6 +417,8 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.ItemRemoved = null; this.ItemChanged = null; this.ItemMoved = null; + this.ItemSplit = null; + this.ItemMerged = null; } private void OnInventoryChangedForward(IReadOnlyCollection events) @@ -368,4 +438,10 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemMoved?.Invoke(type, data); + + private void OnInventoryItemSplitForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemSplit?.Invoke(type, data); + + private void OnInventoryItemMergedForward(GameInventoryEvent type, InventoryEventArgs data) + => this.ItemMerged?.Invoke(type, data); } diff --git a/Dalamud/Game/Inventory/GameInventoryEvent.cs b/Dalamud/Game/Inventory/GameInventoryEvent.cs index 6a4bb86e2..16efab648 100644 --- a/Dalamud/Game/Inventory/GameInventoryEvent.cs +++ b/Dalamud/Game/Inventory/GameInventoryEvent.cs @@ -30,4 +30,14 @@ public enum GameInventoryEvent /// Item has been moved, possibly across different inventories. /// Moved = 4, + + /// + /// Item has been split into two stacks from one, possibly across different inventories. + /// + Split = 5, + + /// + /// Item has been merged into one stack from two, possibly across different inventories. + /// + Merged = 6, } diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 52a5c5e3c..4958574aa 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -158,6 +158,6 @@ public unsafe struct GameInventoryItem : IEquatable /// public override string ToString() => this.IsEmpty - ? "no item" - : $"item #{this.ItemId} at slot {this.InventorySlot} in {this.ContainerType}"; + ? "empty" + : $"item({this.ItemId}@{this.ContainerType}#{this.InventorySlot})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs new file mode 100644 index 000000000..c44bfb991 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs @@ -0,0 +1,54 @@ +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; + +/// +/// Represents the data associated with an item being affected across different slots, possibly in different containers. +/// +public abstract class InventoryComplexEventArgs : InventoryEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// Type of the event. + /// The item at before slot. + /// The item at after slot. + internal InventoryComplexEventArgs( + GameInventoryEvent type, InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) + : base(type, targetEvent.Item) + { + this.SourceEvent = sourceEvent; + this.TargetEvent = targetEvent; + } + + /// + /// Gets the inventory this item was at. + /// + public GameInventoryType SourceInventory => this.SourceEvent.Item.ContainerType; + + /// + /// Gets the inventory this item now is. + /// + public GameInventoryType TargetInventory => this.Item.ContainerType; + + /// + /// Gets the slot this item was at. + /// + public uint SourceSlot => this.SourceEvent.Item.InventorySlot; + + /// + /// Gets the slot this item now is. + /// + public uint TargetSlot => this.Item.InventorySlot; + + /// + /// Gets the associated source event. + /// + public InventoryEventArgs SourceEvent { get; } + + /// + /// Gets the associated target event. + /// + public InventoryEventArgs TargetEvent { get; } + + /// + public override string ToString() => $"{this.Type}({this.SourceEvent}, {this.TargetEvent})"; +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs index 301715bf2..8197e28f5 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs @@ -33,5 +33,5 @@ public abstract class InventoryEventArgs public ref readonly GameInventoryItem Item => ref this.item; /// - public override string ToString() => $"<{this.Type}> ({this.Item})"; + public override string ToString() => $"{this.Type}({this.Item})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs index f68b23106..45a35739a 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs @@ -3,7 +3,7 @@ /// /// Represents the data associated with an item being added to an inventory. /// -public class InventoryItemAddedArgs : InventoryEventArgs +public sealed class InventoryItemAddedArgs : InventoryEventArgs { /// /// Initializes a new instance of the class. diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs index 1682ae32d..191cfa1d8 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs @@ -4,7 +4,7 @@ /// Represents the data associated with an items properties being changed. /// This also includes an items stack count changing. /// -public class InventoryItemChangedArgs : InventoryEventArgs +public sealed class InventoryItemChangedArgs : InventoryEventArgs { private readonly GameInventoryItem oldItemState; diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs new file mode 100644 index 000000000..0f088f24b --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs @@ -0,0 +1,26 @@ +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; + +/// +/// Represents the data associated with an item being merged from two stacks into one. +/// +public sealed class InventoryItemMergedArgs : InventoryComplexEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The item at before slot. + /// The item at after slot. + internal InventoryItemMergedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) + : base(GameInventoryEvent.Merged, sourceEvent, targetEvent) + { + } + + /// + public override string ToString() => + this.TargetEvent is InventoryItemChangedArgs iica + ? $"{this.Type}(" + + $"item({this.Item.ItemId}), " + + $"{this.SourceInventory}#{this.SourceSlot}({this.SourceEvent.Item.Quantity} to 0), " + + $"{this.TargetInventory}#{this.TargetSlot}({iica.OldItemState.Quantity} to {iica.Item.Quantity}))" + : base.ToString(); +} diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs index b6f490a2c..ac33acd0d 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -3,7 +3,7 @@ /// /// Represents the data associated with an item being moved from one inventory and added to another. /// -public class InventoryItemMovedArgs : InventoryEventArgs +public sealed class InventoryItemMovedArgs : InventoryComplexEventArgs { /// /// Initializes a new instance of the class. @@ -11,43 +11,14 @@ public class InventoryItemMovedArgs : InventoryEventArgs /// The item at before slot. /// The item at after slot. internal InventoryItemMovedArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) - : base(GameInventoryEvent.Moved, targetEvent.Item) + : base(GameInventoryEvent.Moved, sourceEvent, targetEvent) { - this.SourceEvent = sourceEvent; - this.TargetEvent = targetEvent; } - /// - /// Gets the inventory this item was moved from. - /// - public GameInventoryType SourceInventory => this.SourceEvent.Item.ContainerType; - - /// - /// Gets the inventory this item was moved to. - /// - public GameInventoryType TargetInventory => this.Item.ContainerType; - - /// - /// Gets the slot this item was moved from. - /// - public uint SourceSlot => this.SourceEvent.Item.InventorySlot; - - /// - /// Gets the slot this item was moved to. - /// - public uint TargetSlot => this.Item.InventorySlot; - - /// - /// Gets the associated source event. - /// - internal InventoryEventArgs SourceEvent { get; } - - /// - /// Gets the associated target event. - /// - internal InventoryEventArgs TargetEvent { get; } + // /// + // public override string ToString() => $"{this.Type}({this.SourceEvent}, {this.TargetEvent})"; /// public override string ToString() => - $"<{this.Type}> (Item #{this.Item.ItemId}) from (slot {this.SourceSlot} in {this.SourceInventory}) to (slot {this.TargetSlot} in {this.TargetInventory})"; + $"{this.Type}(item({this.Item.ItemId}) from {this.SourceInventory}#{this.SourceSlot} to {this.TargetInventory}#{this.TargetSlot})"; } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs index 41ca9d380..fe40c870b 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs @@ -3,7 +3,7 @@ /// /// Represents the data associated with an item being removed from an inventory. /// -public class InventoryItemRemovedArgs : InventoryEventArgs +public sealed class InventoryItemRemovedArgs : InventoryEventArgs { /// /// Initializes a new instance of the class. diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs new file mode 100644 index 000000000..2a3d41c09 --- /dev/null +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs @@ -0,0 +1,26 @@ +namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; + +/// +/// Represents the data associated with an item being split from one stack into two. +/// +public sealed class InventoryItemSplitArgs : InventoryComplexEventArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// The item at before slot. + /// The item at after slot. + internal InventoryItemSplitArgs(InventoryEventArgs sourceEvent, InventoryEventArgs targetEvent) + : base(GameInventoryEvent.Split, sourceEvent, targetEvent) + { + } + + /// + public override string ToString() => + this.SourceEvent is InventoryItemChangedArgs iica + ? $"{this.Type}(" + + $"item({this.Item.ItemId}), " + + $"{this.SourceInventory}#{this.SourceSlot}({iica.OldItemState.Quantity} to {iica.Item.Quantity}), " + + $"{this.TargetInventory}#{this.TargetSlot}(0 to {this.Item.Quantity}))" + : base.ToString(); +} diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 058bcbd27..a6f4b4adf 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -33,27 +33,28 @@ public interface IGameInventory /// /// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes /// as a move event as appropriate.
- /// In other words, does not fire in this event. + /// In other words, , , and + /// do not fire in this event. ///
public event InventoryChangelogDelegate InventoryChangedRaw; /// /// Event that is fired when an item is added to an inventory.
- /// If an accompanying item remove event happens, then will be called instead.
+ /// If this event is a part of multi-step event, then this event will not be called.
/// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemAdded; /// /// Event that is fired when an item is removed from an inventory.
- /// If an accompanying item add event happens, then will be called instead.
+ /// If this event is a part of multi-step event, then this event will not be called.
/// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemRemoved; /// /// Event that is fired when an items properties are changed.
- /// If an accompanying item change event happens, then will be called instead.
+ /// If this event is a part of multi-step event, then this event will not be called.
/// Use if you do not want such reinterpretation. ///
public event InventoryChangedDelegate ItemChanged; @@ -62,4 +63,14 @@ public interface IGameInventory /// Event that is fired when an item is moved from one inventory into another. ///
public event InventoryChangedDelegate ItemMoved; + + /// + /// Event that is fired when an item is split from one stack into two. + /// + public event InventoryChangedDelegate ItemSplit; + + /// + /// Event that is fired when an item is merged from two stacks into one. + /// + public event InventoryChangedDelegate ItemMerged; } From 1039c1eb8a6bdbc4b7a8f8a53e6f22e8bcf38564 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:22:07 +0900 Subject: [PATCH 339/585] Cleanup --- Dalamud/Game/Inventory/GameInventory.cs | 20 +--------- Dalamud/Game/Inventory/GameInventoryItem.cs | 38 +++++++++++++++++++ .../InventoryItemMovedArgs.cs | 3 -- .../Internal/Windows/ConsoleWindow.cs | 3 ++ Dalamud/Plugin/Services/IGameInventory.cs | 8 +++- 5 files changed, 48 insertions(+), 24 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 36e6756bf..b8e81ced7 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -9,8 +9,6 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game; - using Serilog.Events; namespace Dalamud.Game.Inventory; @@ -94,22 +92,6 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory this.framework.Update -= this.OnFrameworkUpdate; } - /// - /// Gets a view of s, wrapped as . - /// - /// The inventory type. - /// The span. - private static unsafe ReadOnlySpan GetItemsForInventory(GameInventoryType type) - { - var inventoryManager = InventoryManager.Instance(); - if (inventoryManager is null) return default; - - var inventory = inventoryManager->GetInventoryContainer((InventoryType)type); - if (inventory is null) return default; - - return new ReadOnlySpan(inventory->Items, (int)inventory->Size); - } - private static void InvokeSafely( IGameInventory.InventoryChangelogDelegate? cb, IReadOnlyCollection data) @@ -140,7 +122,7 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory { for (var i = 0; i < this.inventoryTypes.Length; i++) { - var newItems = GetItemsForInventory(this.inventoryTypes[i]); + var newItems = GameInventoryItem.GetReadOnlySpanOfInventory(this.inventoryTypes[i]); if (newItems.IsEmpty) continue; diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 4958574aa..1e71f6914 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -106,6 +106,28 @@ public unsafe struct GameInventoryItem : IEquatable public ReadOnlySpan MateriaGrade => new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5); + /// + /// Gets the address of native inventory item in the game.
+ /// Can be 0 if this instance of does not point to a valid set of container type and slot. + ///
+ public nint NativeAddress + { + get + { + var s = GetReadOnlySpanOfInventory(this.ContainerType); + if (s.IsEmpty) + return 0; + + foreach (ref readonly var i in s) + { + if (i.InventorySlot == this.InventorySlot) + return (nint)Unsafe.AsPointer(ref Unsafe.AsRef(in i)); + } + + return 0; + } + } + /// /// Gets the color used for this item. /// @@ -160,4 +182,20 @@ public unsafe struct GameInventoryItem : IEquatable this.IsEmpty ? "empty" : $"item({this.ItemId}@{this.ContainerType}#{this.InventorySlot})"; + + /// + /// Gets a view of s, wrapped as . + /// + /// The inventory type. + /// The span. + internal static ReadOnlySpan GetReadOnlySpanOfInventory(GameInventoryType type) + { + var inventoryManager = InventoryManager.Instance(); + if (inventoryManager is null) return default; + + var inventory = inventoryManager->GetInventoryContainer((InventoryType)type); + if (inventory is null) return default; + + return new ReadOnlySpan(inventory->Items, (int)inventory->Size); + } } diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs index ac33acd0d..6a59d1304 100644 --- a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs @@ -15,9 +15,6 @@ public sealed class InventoryItemMovedArgs : InventoryComplexEventArgs { } - // /// - // public override string ToString() => $"{this.Type}({this.SourceEvent}, {this.TargetEvent})"; - /// public override string ToString() => $"{this.Type}(item({this.Item.ItemId}) from {this.SourceInventory}#{this.SourceSlot} to {this.TargetInventory}#{this.TargetSlot})"; diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index b285520d4..89dd153cc 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -679,6 +679,9 @@ internal class ConsoleWindow : Window, IDisposable private bool IsFilterApplicable(LogEntry entry) { + if (this.regexError) + return false; + try { // If this entry is below a newly set minimum level, fail it diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index a6f4b4adf..6e84e780a 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -26,7 +26,11 @@ public interface IGameInventory public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data); /// - /// Event that is fired when the inventory has been changed. + /// Event that is fired when the inventory has been changed.
+ /// Note that some events, such as , , and + /// currently is subject to reinterpretation as , , and + /// .
+ /// Use if you do not want such reinterpretation. ///
public event InventoryChangelogDelegate InventoryChanged; @@ -34,7 +38,7 @@ public interface IGameInventory /// Event that is fired when the inventory has been changed, without trying to interpret two inventory slot changes /// as a move event as appropriate.
/// In other words, , , and - /// do not fire in this event. + /// currently do not fire in this event. ///
public event InventoryChangelogDelegate InventoryChangedRaw; From e4370ed5d3e9d08c4f0d69ac723c96a5594a8f4d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:23:36 +0900 Subject: [PATCH 340/585] Extra note --- Dalamud/Game/Inventory/GameInventoryItem.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 1e71f6914..970f75081 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -108,7 +108,9 @@ public unsafe struct GameInventoryItem : IEquatable /// /// Gets the address of native inventory item in the game.
- /// Can be 0 if this instance of does not point to a valid set of container type and slot. + /// Can be 0 if this instance of does not point to a valid set of container type and slot.
+ /// Note that this instance of can be a snapshot; it may not necessarily match the + /// data you can query from the game using this address value. ///
public nint NativeAddress { From 05820ad9c714471cb9bd67d35366316a67c3fa9a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:25:10 +0900 Subject: [PATCH 341/585] Rename --- Dalamud/Game/Inventory/GameInventoryItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 970f75081..912b91f53 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -112,7 +112,7 @@ public unsafe struct GameInventoryItem : IEquatable /// Note that this instance of can be a snapshot; it may not necessarily match the /// data you can query from the game using this address value. ///
- public nint NativeAddress + public nint Address { get { From b2fc0c4ad249ae1d6121b5437ae32abc2f37d8f7 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:33:20 +0900 Subject: [PATCH 342/585] Adjust namespaces --- .../{InventoryChangeArgsTypes => }/InventoryComplexEventArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryEventArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemAddedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemChangedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemMergedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemMovedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemRemovedArgs.cs | 0 .../{InventoryChangeArgsTypes => }/InventoryItemSplitArgs.cs | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryComplexEventArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryEventArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemAddedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemChangedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemMergedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemMovedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemRemovedArgs.cs (100%) rename Dalamud/Game/Inventory/{InventoryChangeArgsTypes => }/InventoryItemSplitArgs.cs (100%) diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs b/Dalamud/Game/Inventory/InventoryComplexEventArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryComplexEventArgs.cs rename to Dalamud/Game/Inventory/InventoryComplexEventArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryEventArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryItemAddedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemAddedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemAddedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryItemChangedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemChangedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemChangedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs b/Dalamud/Game/Inventory/InventoryItemMergedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMergedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemMergedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryItemMovedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemMovedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemMovedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryItemRemovedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemRemovedArgs.cs rename to Dalamud/Game/Inventory/InventoryItemRemovedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs b/Dalamud/Game/Inventory/InventoryItemSplitArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryChangeArgsTypes/InventoryItemSplitArgs.cs rename to Dalamud/Game/Inventory/InventoryItemSplitArgs.cs From 35b0d53e801ff56a89d6b8396c62be448cedde69 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 10:59:13 +0900 Subject: [PATCH 343/585] Add typed event variants --- Dalamud/Game/Inventory/GameInventory.cs | 97 +++++++++++++++++++++++ Dalamud/Plugin/Services/IGameInventory.cs | 26 ++++++ 2 files changed, 123 insertions(+) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index b8e81ced7..fffe95d53 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -86,6 +86,24 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory /// public event IGameInventory.InventoryChangedDelegate? ItemMerged; + /// + public event IGameInventory.InventoryChangedDelegate? ItemAddedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemRemovedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemChangedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMovedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemSplitExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMergedExplicit; + /// public void Dispose() { @@ -118,6 +136,19 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } } + private static void InvokeSafely(IGameInventory.InventoryChangedDelegate? cb, T arg) + where T : InventoryEventArgs + { + try + { + cb?.Invoke(arg); + } + catch (Exception e) + { + Log.Error(e, "Exception during {argType} callback", arg.Type); + } + } + private void OnFrameworkUpdate(IFramework framework1) { for (var i = 0; i < this.inventoryTypes.Length; i++) @@ -279,22 +310,40 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory .Concat(this.mergedEvents))); foreach (var x in this.addedEvents) + { InvokeSafely(this.ItemAdded, x); + InvokeSafely(this.ItemAddedExplicit, x); + } foreach (var x in this.removedEvents) + { InvokeSafely(this.ItemRemoved, x); + InvokeSafely(this.ItemRemovedExplicit, x); + } foreach (var x in this.changedEvents) + { InvokeSafely(this.ItemChanged, x); + InvokeSafely(this.ItemChangedExplicit, x); + } foreach (var x in this.movedEvents) + { InvokeSafely(this.ItemMoved, x); + InvokeSafely(this.ItemMovedExplicit, x); + } foreach (var x in this.splitEvents) + { InvokeSafely(this.ItemSplit, x); + InvokeSafely(this.ItemSplitExplicit, x); + } foreach (var x in this.mergedEvents) + { InvokeSafely(this.ItemMerged, x); + InvokeSafely(this.ItemMergedExplicit, x); + } // We're done using the lists. Clean them up. this.addedEvents.Clear(); @@ -381,6 +430,24 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public event IGameInventory.InventoryChangedDelegate? ItemMerged; + /// + public event IGameInventory.InventoryChangedDelegate? ItemAddedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemRemovedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemChangedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMovedExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemSplitExplicit; + + /// + public event IGameInventory.InventoryChangedDelegate? ItemMergedExplicit; + /// public void Dispose() { @@ -392,6 +459,12 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; this.gameInventoryService.ItemSplit -= this.OnInventoryItemSplitForward; this.gameInventoryService.ItemMerged -= this.OnInventoryItemMergedForward; + this.gameInventoryService.ItemAddedExplicit -= this.OnInventoryItemAddedExplicitForward; + this.gameInventoryService.ItemRemovedExplicit -= this.OnInventoryItemRemovedExplicitForward; + this.gameInventoryService.ItemChangedExplicit -= this.OnInventoryItemChangedExplicitForward; + this.gameInventoryService.ItemMovedExplicit -= this.OnInventoryItemMovedExplicitForward; + this.gameInventoryService.ItemSplitExplicit -= this.OnInventoryItemSplitExplicitForward; + this.gameInventoryService.ItemMergedExplicit -= this.OnInventoryItemMergedExplicitForward; this.InventoryChanged = null; this.InventoryChangedRaw = null; @@ -401,6 +474,12 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.ItemMoved = null; this.ItemSplit = null; this.ItemMerged = null; + this.ItemAddedExplicit = null; + this.ItemRemovedExplicit = null; + this.ItemChangedExplicit = null; + this.ItemMovedExplicit = null; + this.ItemSplitExplicit = null; + this.ItemMergedExplicit = null; } private void OnInventoryChangedForward(IReadOnlyCollection events) @@ -426,4 +505,22 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven private void OnInventoryItemMergedForward(GameInventoryEvent type, InventoryEventArgs data) => this.ItemMerged?.Invoke(type, data); + + private void OnInventoryItemAddedExplicitForward(InventoryItemAddedArgs data) + => this.ItemAddedExplicit?.Invoke(data); + + private void OnInventoryItemRemovedExplicitForward(InventoryItemRemovedArgs data) + => this.ItemRemovedExplicit?.Invoke(data); + + private void OnInventoryItemChangedExplicitForward(InventoryItemChangedArgs data) + => this.ItemChangedExplicit?.Invoke(data); + + private void OnInventoryItemMovedExplicitForward(InventoryItemMovedArgs data) + => this.ItemMovedExplicit?.Invoke(data); + + private void OnInventoryItemSplitExplicitForward(InventoryItemSplitArgs data) + => this.ItemSplitExplicit?.Invoke(data); + + private void OnInventoryItemMergedExplicitForward(InventoryItemMergedArgs data) + => this.ItemMergedExplicit?.Invoke(data); } diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index 6e84e780a..cd289bc54 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -25,6 +25,14 @@ public interface IGameInventory /// Data for the triggered event. public delegate void InventoryChangedDelegate(GameInventoryEvent type, InventoryEventArgs data); + /// + /// Delegate function to be called for each change to inventories. + /// This delegate sends individual events for changes. + /// + /// The event arg type. + /// Data for the triggered event. + public delegate void InventoryChangedDelegate(T data) where T : InventoryEventArgs; + /// /// Event that is fired when the inventory has been changed.
/// Note that some events, such as , , and @@ -77,4 +85,22 @@ public interface IGameInventory /// Event that is fired when an item is merged from two stacks into one. ///
public event InventoryChangedDelegate ItemMerged; + + /// + public event InventoryChangedDelegate ItemAddedExplicit; + + /// + public event InventoryChangedDelegate ItemRemovedExplicit; + + /// + public event InventoryChangedDelegate ItemChangedExplicit; + + /// + public event InventoryChangedDelegate ItemMovedExplicit; + + /// + public event InventoryChangedDelegate ItemSplitExplicit; + + /// + public event InventoryChangedDelegate ItemMergedExplicit; } From e7afde82b23457cf695d686533f9fc42207370a1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 11:02:37 +0900 Subject: [PATCH 344/585] fix strange change --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 090e0c244..cc6687524 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 090e0c244df668454616026188c1363e5d25a1bc +Subproject commit cc668752416a8459a3c23345c51277e359803de8 From 6b4094d89a0aff26a2468d79fa3f9cafa45fc413 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 11:06:11 +0900 Subject: [PATCH 345/585] Fix missing event handler registration --- Dalamud/Game/Inventory/GameInventory.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index fffe95d53..fba950c09 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -404,6 +404,12 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.gameInventoryService.ItemChanged += this.OnInventoryItemChangedForward; this.gameInventoryService.ItemSplit += this.OnInventoryItemSplitForward; this.gameInventoryService.ItemMerged += this.OnInventoryItemMergedForward; + this.gameInventoryService.ItemAddedExplicit += this.OnInventoryItemAddedExplicitForward; + this.gameInventoryService.ItemRemovedExplicit += this.OnInventoryItemRemovedExplicitForward; + this.gameInventoryService.ItemChangedExplicit += this.OnInventoryItemChangedExplicitForward; + this.gameInventoryService.ItemMovedExplicit += this.OnInventoryItemMovedExplicitForward; + this.gameInventoryService.ItemSplitExplicit += this.OnInventoryItemSplitExplicitForward; + this.gameInventoryService.ItemMergedExplicit += this.OnInventoryItemMergedExplicitForward; } /// From 5f0b65a6c4cbe2b3ed272391a5bb6a35b2c8d45a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 11:08:12 +0900 Subject: [PATCH 346/585] last --- .../{ => InventoryEventArgTypes}/InventoryComplexEventArgs.cs | 0 .../Inventory/{ => InventoryEventArgTypes}/InventoryEventArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemAddedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemChangedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemMergedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemMovedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemRemovedArgs.cs | 0 .../{ => InventoryEventArgTypes}/InventoryItemSplitArgs.cs | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryComplexEventArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryEventArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemAddedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemChangedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemMergedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemMovedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemRemovedArgs.cs (100%) rename Dalamud/Game/Inventory/{ => InventoryEventArgTypes}/InventoryItemSplitArgs.cs (100%) diff --git a/Dalamud/Game/Inventory/InventoryComplexEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryComplexEventArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryEventArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemAddedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemChangedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemMergedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemMergedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemMovedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemRemovedArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs diff --git a/Dalamud/Game/Inventory/InventoryItemSplitArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs similarity index 100% rename from Dalamud/Game/Inventory/InventoryItemSplitArgs.cs rename to Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs From e594d59986123c438a364daaef908097e13a9409 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 12:58:55 +0900 Subject: [PATCH 347/585] Enable tracking only when there exists a subscriber --- Dalamud/Game/Inventory/GameInventory.cs | 433 +++++++++--------- .../InventoryComplexEventArgs.cs | 2 +- .../InventoryEventArgs.cs | 2 +- .../InventoryItemAddedArgs.cs | 2 +- .../InventoryItemChangedArgs.cs | 2 +- .../InventoryItemMergedArgs.cs | 2 +- .../InventoryItemMovedArgs.cs | 2 +- .../InventoryItemRemovedArgs.cs | 2 +- .../InventoryItemSplitArgs.cs | 2 +- .../Internal/Windows/Data/DataWindow.cs | 1 + .../Windows/Data/GameInventoryTestWidget.cs | 163 +++++++ Dalamud/Plugin/Services/IGameInventory.cs | 2 +- 12 files changed, 381 insertions(+), 234 deletions(-) create mode 100644 Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index fba950c09..9a0388113 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -2,15 +2,13 @@ using System.Collections.Generic; using System.Linq; -using Dalamud.Configuration.Internal; -using Dalamud.Game.Inventory.InventoryChangeArgsTypes; +using Dalamud.Game.Inventory.InventoryEventArgTypes; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; -using Serilog.Events; - namespace Dalamud.Game.Inventory; /// @@ -18,9 +16,10 @@ namespace Dalamud.Game.Inventory; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal class GameInventory : IDisposable, IServiceType, IGameInventory +internal class GameInventory : IDisposable, IServiceType { - private static readonly ModuleLog Log = new(nameof(GameInventory)); + private readonly List subscribersPendingChange = new(); + private readonly List subscribers = new(); private readonly List addedEvents = new(); private readonly List removedEvents = new(); @@ -32,120 +31,58 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly DalamudConfiguration dalamudConfiguration = Service.Get(); - private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; + private bool subscribersChanged; + [ServiceManager.ServiceConstructor] private GameInventory() { this.inventoryTypes = Enum.GetValues(); this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; - - this.framework.Update += this.OnFrameworkUpdate; - - // Separate log logic as an event handler. - this.InventoryChanged += events => - { - if (this.dalamudConfiguration.LogLevel > LogEventLevel.Verbose) - return; - - foreach (var e in events) - { - if (e is InventoryComplexEventArgs icea) - Log.Verbose($"{icea}\n\t├ {icea.SourceEvent}\n\t└ {icea.TargetEvent}"); - else - Log.Verbose($"{e}"); - } - }; } - /// - public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; - - /// - public event IGameInventory.InventoryChangelogDelegate? InventoryChangedRaw; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemAdded; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemRemoved; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemChanged; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemMoved; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemSplit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemMerged; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemAddedExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemRemovedExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemChangedExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemMovedExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemSplitExplicit; - - /// - public event IGameInventory.InventoryChangedDelegate? ItemMergedExplicit; - /// public void Dispose() { - this.framework.Update -= this.OnFrameworkUpdate; - } - - private static void InvokeSafely( - IGameInventory.InventoryChangelogDelegate? cb, - IReadOnlyCollection data) - { - try + lock (this.subscribersPendingChange) { - cb?.Invoke(data); - } - catch (Exception e) - { - Log.Error(e, "Exception during batch callback"); + this.subscribers.Clear(); + this.subscribersPendingChange.Clear(); + this.subscribersChanged = false; + this.framework.Update -= this.OnFrameworkUpdate; } } - private static void InvokeSafely(IGameInventory.InventoryChangedDelegate? cb, InventoryEventArgs arg) + /// + /// Subscribe to events. + /// + /// The event target. + public void Subscribe(GameInventoryPluginScoped s) { - try + lock (this.subscribersPendingChange) { - cb?.Invoke(arg.Type, arg); - } - catch (Exception e) - { - Log.Error(e, "Exception during {argType} callback", arg.Type); + this.subscribersPendingChange.Add(s); + this.subscribersChanged = true; + if (this.subscribersPendingChange.Count == 1) + this.framework.Update += this.OnFrameworkUpdate; } } - private static void InvokeSafely(IGameInventory.InventoryChangedDelegate? cb, T arg) - where T : InventoryEventArgs + /// + /// Unsubscribe from events. + /// + /// The event target. + public void Unsubscribe(GameInventoryPluginScoped s) { - try + lock (this.subscribersPendingChange) { - cb?.Invoke(arg); - } - catch (Exception e) - { - Log.Error(e, "Exception during {argType} callback", arg.Type); + if (!this.subscribersPendingChange.Remove(s)) + return; + this.subscribersChanged = true; + if (this.subscribersPendingChange.Count == 0) + this.framework.Update -= this.OnFrameworkUpdate; } } @@ -193,18 +130,40 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory if (this.addedEvents.Count == 0 && this.removedEvents.Count == 0 && this.changedEvents.Count == 0) return; + // Make a copy of subscribers, to accommodate self removal during the loop. + if (this.subscribersChanged) + { + bool isNew; + lock (this.subscribersPendingChange) + { + isNew = this.subscribersPendingChange.Any() && !this.subscribers.Any(); + this.subscribers.Clear(); + this.subscribers.AddRange(this.subscribersPendingChange); + this.subscribersChanged = false; + } + + // Is this the first time (resuming) scanning for changes? Then discard the "changes". + if (isNew) + { + this.addedEvents.Clear(); + this.removedEvents.Clear(); + this.changedEvents.Clear(); + return; + } + } + // Broadcast InventoryChangedRaw. // Same reason with the above on why are there 3 lists of events involved. - InvokeSafely( - this.InventoryChangedRaw, - new DeferredReadOnlyCollection( - this.addedEvents.Count + - this.removedEvents.Count + - this.changedEvents.Count, - () => Array.Empty() - .Concat(this.addedEvents) - .Concat(this.removedEvents) - .Concat(this.changedEvents))); + var allRawEventsCollection = new DeferredReadOnlyCollection( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count, + () => Array.Empty() + .Concat(this.addedEvents) + .Concat(this.removedEvents) + .Concat(this.changedEvents)); + foreach (var s in this.subscribers) + s.InvokeChangedRaw(allRawEventsCollection); // Resolve moved items, from 1 added + 1 removed event. for (var iAdded = this.addedEvents.Count - 1; iAdded >= 0; --iAdded) @@ -291,58 +250,32 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory } } + // Create a collection view of all events. + var allEventsCollection = new DeferredReadOnlyCollection( + this.addedEvents.Count + + this.removedEvents.Count + + this.changedEvents.Count + + this.movedEvents.Count + + this.splitEvents.Count + + this.mergedEvents.Count, + () => Array.Empty() + .Concat(this.addedEvents) + .Concat(this.removedEvents) + .Concat(this.changedEvents) + .Concat(this.movedEvents) + .Concat(this.splitEvents) + .Concat(this.mergedEvents)); + // Broadcast the rest. - InvokeSafely( - this.InventoryChanged, - new DeferredReadOnlyCollection( - this.addedEvents.Count + - this.removedEvents.Count + - this.changedEvents.Count + - this.movedEvents.Count + - this.splitEvents.Count + - this.mergedEvents.Count, - () => Array.Empty() - .Concat(this.addedEvents) - .Concat(this.removedEvents) - .Concat(this.changedEvents) - .Concat(this.movedEvents) - .Concat(this.splitEvents) - .Concat(this.mergedEvents))); - - foreach (var x in this.addedEvents) + foreach (var s in this.subscribers) { - InvokeSafely(this.ItemAdded, x); - InvokeSafely(this.ItemAddedExplicit, x); - } - - foreach (var x in this.removedEvents) - { - InvokeSafely(this.ItemRemoved, x); - InvokeSafely(this.ItemRemovedExplicit, x); - } - - foreach (var x in this.changedEvents) - { - InvokeSafely(this.ItemChanged, x); - InvokeSafely(this.ItemChangedExplicit, x); - } - - foreach (var x in this.movedEvents) - { - InvokeSafely(this.ItemMoved, x); - InvokeSafely(this.ItemMovedExplicit, x); - } - - foreach (var x in this.splitEvents) - { - InvokeSafely(this.ItemSplit, x); - InvokeSafely(this.ItemSplitExplicit, x); - } - - foreach (var x in this.mergedEvents) - { - InvokeSafely(this.ItemMerged, x); - InvokeSafely(this.ItemMergedExplicit, x); + s.InvokeChanged(allEventsCollection); + s.Invoke(this.addedEvents); + s.Invoke(this.removedEvents); + s.Invoke(this.changedEvents); + s.Invoke(this.movedEvents); + s.Invoke(this.splitEvents); + s.Invoke(this.mergedEvents); } // We're done using the lists. Clean them up. @@ -388,29 +321,15 @@ internal class GameInventory : IDisposable, IServiceType, IGameInventory #pragma warning restore SA1015 internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory { + private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped)); + [ServiceManager.ServiceDependency] private readonly GameInventory gameInventoryService = Service.Get(); /// /// Initializes a new instance of the class. /// - public GameInventoryPluginScoped() - { - this.gameInventoryService.InventoryChanged += this.OnInventoryChangedForward; - this.gameInventoryService.InventoryChangedRaw += this.OnInventoryChangedRawForward; - this.gameInventoryService.ItemAdded += this.OnInventoryItemAddedForward; - this.gameInventoryService.ItemRemoved += this.OnInventoryItemRemovedForward; - this.gameInventoryService.ItemMoved += this.OnInventoryItemMovedForward; - this.gameInventoryService.ItemChanged += this.OnInventoryItemChangedForward; - this.gameInventoryService.ItemSplit += this.OnInventoryItemSplitForward; - this.gameInventoryService.ItemMerged += this.OnInventoryItemMergedForward; - this.gameInventoryService.ItemAddedExplicit += this.OnInventoryItemAddedExplicitForward; - this.gameInventoryService.ItemRemovedExplicit += this.OnInventoryItemRemovedExplicitForward; - this.gameInventoryService.ItemChangedExplicit += this.OnInventoryItemChangedExplicitForward; - this.gameInventoryService.ItemMovedExplicit += this.OnInventoryItemMovedExplicitForward; - this.gameInventoryService.ItemSplitExplicit += this.OnInventoryItemSplitExplicitForward; - this.gameInventoryService.ItemMergedExplicit += this.OnInventoryItemMergedExplicitForward; - } + public GameInventoryPluginScoped() => this.gameInventoryService.Subscribe(this); /// public event IGameInventory.InventoryChangelogDelegate? InventoryChanged; @@ -457,20 +376,7 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven /// public void Dispose() { - this.gameInventoryService.InventoryChanged -= this.OnInventoryChangedForward; - this.gameInventoryService.InventoryChangedRaw -= this.OnInventoryChangedRawForward; - this.gameInventoryService.ItemAdded -= this.OnInventoryItemAddedForward; - this.gameInventoryService.ItemRemoved -= this.OnInventoryItemRemovedForward; - this.gameInventoryService.ItemChanged -= this.OnInventoryItemChangedForward; - this.gameInventoryService.ItemMoved -= this.OnInventoryItemMovedForward; - this.gameInventoryService.ItemSplit -= this.OnInventoryItemSplitForward; - this.gameInventoryService.ItemMerged -= this.OnInventoryItemMergedForward; - this.gameInventoryService.ItemAddedExplicit -= this.OnInventoryItemAddedExplicitForward; - this.gameInventoryService.ItemRemovedExplicit -= this.OnInventoryItemRemovedExplicitForward; - this.gameInventoryService.ItemChangedExplicit -= this.OnInventoryItemChangedExplicitForward; - this.gameInventoryService.ItemMovedExplicit -= this.OnInventoryItemMovedExplicitForward; - this.gameInventoryService.ItemSplitExplicit -= this.OnInventoryItemSplitExplicitForward; - this.gameInventoryService.ItemMergedExplicit -= this.OnInventoryItemMergedExplicitForward; + this.gameInventoryService.Unsubscribe(this); this.InventoryChanged = null; this.InventoryChangedRaw = null; @@ -488,45 +394,122 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven this.ItemMergedExplicit = null; } - private void OnInventoryChangedForward(IReadOnlyCollection events) - => this.InventoryChanged?.Invoke(events); + /// + /// Invoke . + /// + /// The data. + internal void InvokeChanged(IReadOnlyCollection data) + { + try + { + this.InventoryChanged?.Invoke(data); + } + catch (Exception e) + { + Log.Error( + e, + "[{plugin}] Exception during {argType} callback", + Service.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)", + nameof(this.InventoryChanged)); + } + } - private void OnInventoryChangedRawForward(IReadOnlyCollection events) - => this.InventoryChangedRaw?.Invoke(events); + /// + /// Invoke . + /// + /// The data. + internal void InvokeChangedRaw(IReadOnlyCollection data) + { + try + { + this.InventoryChangedRaw?.Invoke(data); + } + catch (Exception e) + { + Log.Error( + e, + "[{plugin}] Exception during {argType} callback", + Service.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)", + nameof(this.InventoryChangedRaw)); + } + } + + // Note below: using List instead of IEnumerable, since List has a specialized lightweight enumerator. - private void OnInventoryItemAddedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemAdded?.Invoke(type, data); + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemAdded, this.ItemAddedExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemRemoved, this.ItemRemovedExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemChanged, this.ItemChangedExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemMoved, this.ItemMovedExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemSplit, this.ItemSplitExplicit, events); + + /// + /// Invoke the appropriate event handler. + /// + /// The data. + internal void Invoke(List events) => + Invoke(this.ItemMerged, this.ItemMergedExplicit, events); + + private static void Invoke( + IGameInventory.InventoryChangedDelegate? cb, + IGameInventory.InventoryChangedDelegate? cbt, + List events) where T : InventoryEventArgs + { + foreach (var evt in events) + { + try + { + cb?.Invoke(evt.Type, evt); + } + catch (Exception e) + { + Log.Error( + e, + "[{plugin}] Exception during untyped callback for {evt}", + Service.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)", + evt); + } - private void OnInventoryItemRemovedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemRemoved?.Invoke(type, data); - - private void OnInventoryItemChangedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemChanged?.Invoke(type, data); - - private void OnInventoryItemMovedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemMoved?.Invoke(type, data); - - private void OnInventoryItemSplitForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemSplit?.Invoke(type, data); - - private void OnInventoryItemMergedForward(GameInventoryEvent type, InventoryEventArgs data) - => this.ItemMerged?.Invoke(type, data); - - private void OnInventoryItemAddedExplicitForward(InventoryItemAddedArgs data) - => this.ItemAddedExplicit?.Invoke(data); - - private void OnInventoryItemRemovedExplicitForward(InventoryItemRemovedArgs data) - => this.ItemRemovedExplicit?.Invoke(data); - - private void OnInventoryItemChangedExplicitForward(InventoryItemChangedArgs data) - => this.ItemChangedExplicit?.Invoke(data); - - private void OnInventoryItemMovedExplicitForward(InventoryItemMovedArgs data) - => this.ItemMovedExplicit?.Invoke(data); - - private void OnInventoryItemSplitExplicitForward(InventoryItemSplitArgs data) - => this.ItemSplitExplicit?.Invoke(data); - - private void OnInventoryItemMergedExplicitForward(InventoryItemMergedArgs data) - => this.ItemMergedExplicit?.Invoke(data); + try + { + cbt?.Invoke(evt); + } + catch (Exception e) + { + Log.Error( + e, + "[{plugin}] Exception during typed callback for {evt}", + Service.GetNullable()?.FindCallingPlugin(new(e))?.Name ?? "(unknown plugin)", + evt); + } + } + } } diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs index c44bfb991..95d7e8238 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryComplexEventArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being affected across different slots, possibly in different containers. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs index 8197e28f5..198e0395b 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryEventArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Abstract base class representing inventory changed events. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs index 45a35739a..ceb64c6f9 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemAddedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being added to an inventory. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs index 191cfa1d8..372418793 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemChangedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an items properties being changed. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs index 0f088f24b..d7056356e 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMergedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being merged from two stacks into one. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs index 6a59d1304..8d0bbca17 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemMovedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being moved from one inventory and added to another. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs index fe40c870b..5677e3cc4 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemRemovedArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being removed from an inventory. diff --git a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs index 2a3d41c09..5f717cf60 100644 --- a/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs +++ b/Dalamud/Game/Inventory/InventoryEventArgTypes/InventoryItemSplitArgs.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Inventory.InventoryChangeArgsTypes; +namespace Dalamud.Game.Inventory.InventoryEventArgTypes; /// /// Represents the data associated with an item being split from one stack into two. diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index e9d4152a5..20c3d6d01 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -33,6 +33,7 @@ internal class DataWindow : Window new FateTableWidget(), new FlyTextWidget(), new FontAwesomeTestWidget(), + new GameInventoryTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), diff --git a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs new file mode 100644 index 000000000..c19f56654 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs @@ -0,0 +1,163 @@ +using System.Collections.Generic; + +using Dalamud.Configuration.Internal; +using Dalamud.Game.Inventory; +using Dalamud.Game.Inventory.InventoryEventArgTypes; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +using Serilog.Events; + +namespace Dalamud.Interface.Internal.Windows.Data; + +/// +/// Tester for . +/// +internal class GameInventoryTestWidget : IDataWindowWidget +{ + private static readonly ModuleLog Log = new(nameof(GameInventoryTestWidget)); + + private GameInventoryPluginScoped? scoped; + private bool standardEnabled; + private bool rawEnabled; + + /// + public string[]? CommandShortcuts { get; init; } = { "gameinventorytest" }; + + /// + public string DisplayName { get; init; } = "GameInventory Test"; + + /// + public bool Ready { get; set; } + + /// + public void Load() => this.Ready = true; + + /// + public void Draw() + { + if (Service.Get().LogLevel > LogEventLevel.Information) + { + ImGuiHelpers.SafeTextColoredWrapped( + ImGuiColors.DalamudRed, + "Enable LogLevel=Information display to see the logs."); + } + + using var table = ImRaii.Table(this.DisplayName, 3, ImGuiTableFlags.SizingFixedFit); + if (!table.Success) + return; + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Standard Logging"); + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(this.standardEnabled)) + { + if (ImGui.Button("Enable##standard-enable") && !this.standardEnabled) + { + this.scoped ??= new(); + this.scoped.InventoryChanged += ScopedOnInventoryChanged; + this.standardEnabled = true; + } + } + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(!this.standardEnabled)) + { + if (ImGui.Button("Disable##standard-disable") && this.scoped is not null && this.standardEnabled) + { + this.scoped.InventoryChanged -= ScopedOnInventoryChanged; + this.standardEnabled = false; + if (!this.rawEnabled) + { + this.scoped.Dispose(); + this.scoped = null; + } + } + } + + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("Raw Logging"); + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(this.rawEnabled)) + { + if (ImGui.Button("Enable##raw-enable") && !this.rawEnabled) + { + this.scoped ??= new(); + this.scoped.InventoryChangedRaw += ScopedOnInventoryChangedRaw; + this.rawEnabled = true; + } + } + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(!this.rawEnabled)) + { + if (ImGui.Button("Disable##raw-disable") && this.scoped is not null && this.rawEnabled) + { + this.scoped.InventoryChangedRaw -= ScopedOnInventoryChangedRaw; + this.rawEnabled = false; + if (!this.standardEnabled) + { + this.scoped.Dispose(); + this.scoped = null; + } + } + } + + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted("All"); + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(this.standardEnabled && this.rawEnabled)) + { + if (ImGui.Button("Enable##all-enable")) + { + this.scoped ??= new(); + if (!this.standardEnabled) + this.scoped.InventoryChanged += ScopedOnInventoryChanged; + if (!this.rawEnabled) + this.scoped.InventoryChangedRaw += ScopedOnInventoryChangedRaw; + this.standardEnabled = this.rawEnabled = true; + } + } + + ImGui.TableNextColumn(); + using (ImRaii.Disabled(this.scoped is null)) + { + if (ImGui.Button("Disable##all-disable")) + { + this.scoped?.Dispose(); + this.scoped = null; + this.standardEnabled = this.rawEnabled = false; + } + } + } + + private static void ScopedOnInventoryChangedRaw(IReadOnlyCollection events) + { + var i = 0; + foreach (var e in events) + Log.Information($"[{++i}/{events.Count}] Raw: {e}"); + } + + private static void ScopedOnInventoryChanged(IReadOnlyCollection events) + { + var i = 0; + foreach (var e in events) + { + if (e is InventoryComplexEventArgs icea) + Log.Information($"[{++i}/{events.Count}] {icea}\n\t├ {icea.SourceEvent}\n\t└ {icea.TargetEvent}"); + else + Log.Information($"[{++i}/{events.Count}] {e}"); + } + } +} diff --git a/Dalamud/Plugin/Services/IGameInventory.cs b/Dalamud/Plugin/Services/IGameInventory.cs index cd289bc54..a1b1114d7 100644 --- a/Dalamud/Plugin/Services/IGameInventory.cs +++ b/Dalamud/Plugin/Services/IGameInventory.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Dalamud.Game.Inventory; -using Dalamud.Game.Inventory.InventoryChangeArgsTypes; +using Dalamud.Game.Inventory.InventoryEventArgTypes; namespace Dalamud.Plugin.Services; From 841c47e1866db19b87a4dad81cc527bea4d53313 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 13:44:28 +0900 Subject: [PATCH 348/585] Use RaptureAtkModule.Update as a cue for checking inventory changes --- Dalamud/Game/Inventory/GameInventory.cs | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 9a0388113..4dc7d7251 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -3,12 +3,15 @@ using System.Collections.Generic; using System.Linq; using Dalamud.Game.Inventory.InventoryEventArgTypes; +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.UI; + namespace Dalamud.Game.Inventory; /// @@ -31,18 +34,30 @@ internal class GameInventory : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + private readonly Hook raptureAtkModuleUpdateHook; + private readonly GameInventoryType[] inventoryTypes; private readonly GameInventoryItem[]?[] inventoryItems; private bool subscribersChanged; + private bool inventoriesMightBeChanged; [ServiceManager.ServiceConstructor] private GameInventory() { this.inventoryTypes = Enum.GetValues(); this.inventoryItems = new GameInventoryItem[this.inventoryTypes.Length][]; + + unsafe + { + this.raptureAtkModuleUpdateHook = Hook.FromFunctionPointerVariable( + new(&((RaptureAtkModule.RaptureAtkModuleVTable*)RaptureAtkModule.StaticAddressPointers.VTable)->Update), + this.RaptureAtkModuleUpdateDetour); + } } + private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1); + /// public void Dispose() { @@ -52,6 +67,7 @@ internal class GameInventory : IDisposable, IServiceType this.subscribersPendingChange.Clear(); this.subscribersChanged = false; this.framework.Update -= this.OnFrameworkUpdate; + this.raptureAtkModuleUpdateHook.Dispose(); } } @@ -66,7 +82,11 @@ internal class GameInventory : IDisposable, IServiceType this.subscribersPendingChange.Add(s); this.subscribersChanged = true; if (this.subscribersPendingChange.Count == 1) + { + this.inventoriesMightBeChanged = true; this.framework.Update += this.OnFrameworkUpdate; + this.raptureAtkModuleUpdateHook.Enable(); + } } } @@ -82,12 +102,20 @@ internal class GameInventory : IDisposable, IServiceType return; this.subscribersChanged = true; if (this.subscribersPendingChange.Count == 0) + { this.framework.Update -= this.OnFrameworkUpdate; + this.raptureAtkModuleUpdateHook.Disable(); + } } } private void OnFrameworkUpdate(IFramework framework1) { + if (!this.inventoriesMightBeChanged) + return; + + this.inventoriesMightBeChanged = false; + for (var i = 0; i < this.inventoryTypes.Length; i++) { var newItems = GameInventoryItem.GetReadOnlySpanOfInventory(this.inventoryTypes[i]); @@ -287,6 +315,12 @@ internal class GameInventory : IDisposable, IServiceType this.mergedEvents.Clear(); } + private unsafe void RaptureAtkModuleUpdateDetour(RaptureAtkModule* ram, float f1) + { + this.inventoriesMightBeChanged |= ram->AgentUpdateFlag != 0; + this.raptureAtkModuleUpdateHook.Original(ram, f1); + } + /// /// A view of , so that the number of items /// contained within can be known in advance, and it can be enumerated multiple times. From ba5e3407d62db4ef88e6640cb50f3948d944bce4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 2 Dec 2023 13:53:00 +0900 Subject: [PATCH 349/585] Permaenable raptureAtkModuleUpdateHook --- Dalamud/Game/Inventory/GameInventory.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 4dc7d7251..1c7f3e3bf 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -54,6 +54,8 @@ internal class GameInventory : IDisposable, IServiceType new(&((RaptureAtkModule.RaptureAtkModuleVTable*)RaptureAtkModule.StaticAddressPointers.VTable)->Update), this.RaptureAtkModuleUpdateDetour); } + + this.raptureAtkModuleUpdateHook.Enable(); } private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1); @@ -85,7 +87,6 @@ internal class GameInventory : IDisposable, IServiceType { this.inventoriesMightBeChanged = true; this.framework.Update += this.OnFrameworkUpdate; - this.raptureAtkModuleUpdateHook.Enable(); } } } @@ -102,10 +103,7 @@ internal class GameInventory : IDisposable, IServiceType return; this.subscribersChanged = true; if (this.subscribersPendingChange.Count == 0) - { this.framework.Update -= this.OnFrameworkUpdate; - this.raptureAtkModuleUpdateHook.Disable(); - } } } From 37bcff84b17714343ad52c9d3aa583874ecbc53c Mon Sep 17 00:00:00 2001 From: Sirius902 <10891979+Sirius902@users.noreply.github.com> Date: Sat, 2 Dec 2023 20:25:43 -0800 Subject: [PATCH 350/585] Fix Dalamud trying to unload IServiceType and crashing (#1557) --- Dalamud/ServiceManager.cs | 2 +- Dalamud/Service{T}.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 21c08ce72..00447da9e 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -336,7 +336,7 @@ internal static class ServiceManager foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { - if (!serviceType.IsAssignableTo(typeof(IServiceType))) + if (serviceType.IsAbstract || !serviceType.IsAssignableTo(typeof(IServiceType))) continue; // Scoped services shall never be unloaded here. diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 9c7f0411d..08c362433 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -176,7 +176,7 @@ internal static class Service where T : IServiceType { foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { - if (!serviceType.IsAssignableTo(typeof(IServiceType))) + if (serviceType.IsAbstract || !serviceType.IsAssignableTo(typeof(IServiceType))) continue; if (serviceType == typeof(PluginManager)) From 70249a4db00a462c62a88786eae6c72366b23fdf Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 3 Dec 2023 13:57:53 +0900 Subject: [PATCH 351/585] Revert "Fix Dalamud trying to unload IServiceType and crashing (#1557)" (#1559) This reverts commit 37bcff84b17714343ad52c9d3aa583874ecbc53c. --- Dalamud/ServiceManager.cs | 2 +- Dalamud/Service{T}.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 00447da9e..21c08ce72 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -336,7 +336,7 @@ internal static class ServiceManager foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { - if (serviceType.IsAbstract || !serviceType.IsAssignableTo(typeof(IServiceType))) + if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; // Scoped services shall never be unloaded here. diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 08c362433..9c7f0411d 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -176,7 +176,7 @@ internal static class Service where T : IServiceType { foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { - if (serviceType.IsAbstract || !serviceType.IsAssignableTo(typeof(IServiceType))) + if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; if (serviceType == typeof(PluginManager)) From 5777745ab3cc0a52ee1c7f799aae19c312513095 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 7 Dec 2023 14:06:39 +0900 Subject: [PATCH 352/585] Add injector option to not apply any exception handlers (#1541) * Add injector option to not apply any exception handlers * Log as warning if NoExceptionHandlers is set --- Dalamud.Boot/DalamudStartInfo.cpp | 1 + Dalamud.Boot/DalamudStartInfo.h | 1 + Dalamud.Boot/dllmain.cpp | 4 +++- Dalamud.Common/DalamudStartInfo.cs | 37 ++++-------------------------- Dalamud.Injector/EntryPoint.cs | 6 +++-- Dalamud/EntryPoint.cs | 6 +++-- 6 files changed, 18 insertions(+), 37 deletions(-) diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index 15faf82ad..e2fed1beb 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -103,6 +103,7 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) { } config.CrashHandlerShow = json.value("CrashHandlerShow", config.CrashHandlerShow); + config.NoExceptionHandlers = json.value("NoExceptionHandlers", config.NoExceptionHandlers); } void DalamudStartInfo::from_envvars() { diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 66109abf7..73a1a0d34 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -49,6 +49,7 @@ struct DalamudStartInfo { std::set BootUnhookDlls{}; bool CrashHandlerShow = false; + bool NoExceptionHandlers = false; friend void from_json(const nlohmann::json&, DalamudStartInfo&); void from_envvars(); diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 94f1c7d0f..8ffef40b0 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -133,7 +133,9 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { // ============================== VEH ======================================== // logging::I("Initializing VEH..."); - if (utils::is_running_on_wine()) { + if (g_startInfo.NoExceptionHandlers) { + logging::W("=> Exception handlers are disabled from DalamudStartInfo."); + } else if (utils::is_running_on_wine()) { logging::I("=> VEH was disabled, running on wine"); } else if (g_startInfo.BootVehEnabled) { if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory)) diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index 069a0ef9f..5126fe3a4 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -17,38 +17,6 @@ public record DalamudStartInfo // ignored } - /// - /// Initializes a new instance of the class. - /// - /// Object to copy values from. - public DalamudStartInfo(DalamudStartInfo other) - { - this.WorkingDirectory = other.WorkingDirectory; - this.ConfigurationPath = other.ConfigurationPath; - this.LogPath = other.LogPath; - this.LogName = other.LogName; - this.PluginDirectory = other.PluginDirectory; - this.AssetDirectory = other.AssetDirectory; - this.Language = other.Language; - this.GameVersion = other.GameVersion; - this.DelayInitializeMs = other.DelayInitializeMs; - this.TroubleshootingPackData = other.TroubleshootingPackData; - this.NoLoadPlugins = other.NoLoadPlugins; - this.NoLoadThirdPartyPlugins = other.NoLoadThirdPartyPlugins; - this.BootLogPath = other.BootLogPath; - this.BootShowConsole = other.BootShowConsole; - this.BootDisableFallbackConsole = other.BootDisableFallbackConsole; - this.BootWaitMessageBox = other.BootWaitMessageBox; - this.BootWaitDebugger = other.BootWaitDebugger; - this.BootVehEnabled = other.BootVehEnabled; - this.BootVehFull = other.BootVehFull; - this.BootEnableEtw = other.BootEnableEtw; - this.BootDotnetOpenProcessHookMode = other.BootDotnetOpenProcessHookMode; - this.BootEnabledGameFixes = other.BootEnabledGameFixes; - this.BootUnhookDlls = other.BootUnhookDlls; - this.CrashHandlerShow = other.CrashHandlerShow; - } - /// /// Gets or sets the working directory of the XIVLauncher installations. /// @@ -169,4 +137,9 @@ public record DalamudStartInfo /// Gets or sets a value indicating whether to show crash handler console window. /// public bool CrashHandlerShow { get; set; } + + /// + /// Gets or sets a value indicating whether to disable all kinds of global exception handlers. + /// + public bool NoExceptionHandlers { get; set; } } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index bd9fa87f8..3ffb7ba18 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -96,6 +96,7 @@ namespace Dalamud.Injector args.Remove("--no-plugin"); args.Remove("--no-3rd-plugin"); args.Remove("--crash-handler-console"); + args.Remove("--no-exception-handlers"); var mainCommand = args[1].ToLowerInvariant(); if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand) @@ -393,6 +394,7 @@ namespace Dalamud.Injector startInfo.NoLoadThirdPartyPlugins = args.Contains("--no-3rd-plugin"); // startInfo.BootUnhookDlls = new List() { "kernel32.dll", "ntdll.dll", "user32.dll" }; startInfo.CrashHandlerShow = args.Contains("--crash-handler-console"); + startInfo.NoExceptionHandlers = args.Contains("--no-exception-handlers"); return startInfo; } @@ -434,7 +436,7 @@ namespace Dalamud.Injector Console.WriteLine("Verbose logging:\t[-v]"); Console.WriteLine("Show Console:\t[--console] [--crash-handler-console]"); Console.WriteLine("Enable ETW:\t[--etw]"); - Console.WriteLine("Enable VEH:\t[--veh], [--veh-full]"); + Console.WriteLine("Enable VEH:\t[--veh], [--veh-full], [--no-exception-handlers]"); Console.WriteLine("Show messagebox:\t[--msgbox1], [--msgbox2], [--msgbox3]"); Console.WriteLine("No plugins:\t[--no-plugin] [--no-3rd-plugin]"); Console.WriteLine("Logging:\t[--logname=] [--logpath=]"); @@ -889,7 +891,7 @@ namespace Dalamud.Injector var gameVerStr = File.ReadAllText(Path.Combine(ffxivDir, "ffxivgame.ver")); var gameVer = GameVersion.Parse(gameVerStr); - return new DalamudStartInfo(startInfo) + return startInfo with { GameVersion = gameVer, }; diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index c9537eda6..d0f9e8845 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -147,7 +147,8 @@ public sealed class EntryPoint LogLevelSwitch.MinimumLevel = configuration.LogLevel; // Log any unhandled exception. - AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + if (!info.NoExceptionHandlers) + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; var unloadFailed = false; @@ -196,7 +197,8 @@ public sealed class EntryPoint finally { TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; - AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException; + if (!info.NoExceptionHandlers) + AppDomain.CurrentDomain.UnhandledException -= OnUnhandledException; Log.Information("Session has ended."); Log.CloseAndFlush(); From a0f4baf8fa81dae18c3d975b07ff4b60e4f4f8d5 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 7 Dec 2023 14:29:46 +0900 Subject: [PATCH 353/585] Less footguns in service dependency handling (#1560) --- .../Game/Addon/Events/AddonEventManager.cs | 8 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 20 +- Dalamud/Game/ClientState/ClientState.cs | 8 +- .../Game/ClientState/Conditions/Condition.cs | 23 +- .../Game/ClientState/GamePad/GamepadState.cs | 7 +- Dalamud/Game/DutyState/DutyState.cs | 8 +- Dalamud/Game/Framework.cs | 10 +- Dalamud/Game/Gui/ChatGui.cs | 12 +- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 8 +- Dalamud/Game/Gui/GameGui.cs | 22 +- Dalamud/Game/Gui/Internal/DalamudIME.cs | 2 +- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 7 +- Dalamud/Game/Gui/Toast/ToastGui.cs | 12 +- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 12 +- Dalamud/Game/Network/GameNetwork.cs | 10 +- .../Interface/Internal/InterfaceManager.cs | 10 +- .../Windows/Data/Widgets/ServicesWidget.cs | 306 +++++++++++++++++- Dalamud/Plugin/Internal/PluginManager.cs | 89 ++++- .../Plugin/Internal/StartupPluginLoader.cs | 50 --- Dalamud/ServiceManager.cs | 127 ++++++-- Dalamud/Service{T}.cs | 165 +++++----- Dalamud/Storage/Assets/DalamudAssetManager.cs | 35 +- Dalamud/Utility/ArrayExtensions.cs | 10 + 23 files changed, 659 insertions(+), 302 deletions(-) delete mode 100644 Dalamud/Plugin/Internal/StartupPluginLoader.cs diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index d8f3427ef..23f3b1a6d 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -57,6 +57,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.finalizeEventListener = new AddonLifecycleEventListener(AddonEvent.PreFinalize, string.Empty, this.OnAddonFinalize); this.addonLifecycle.RegisterListener(this.finalizeEventListener); + + this.onUpdateCursor.Enable(); } private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); @@ -149,12 +151,6 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.onUpdateCursor.Enable(); - } - /// /// When an addon finalizes, check it for any registered events, and unregister them. /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 08a2d59ef..3528de562 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -58,6 +58,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType this.onAddonUpdateHook = new CallHook(this.address.AddonUpdate, this.OnAddonUpdate); this.onAddonRefreshHook = Hook.FromAddress(this.address.AddonOnRefresh, this.OnAddonRefresh); this.onAddonRequestedUpdateHook = new CallHook(this.address.AddonOnRequestedUpdate, this.OnRequestedUpdate); + + this.onAddonSetupHook.Enable(); + this.onAddonSetup2Hook.Enable(); + this.onAddonFinalizeHook.Enable(); + this.onAddonDrawHook.Enable(); + this.onAddonUpdateHook.Enable(); + this.onAddonRefreshHook.Enable(); + this.onAddonRequestedUpdateHook.Enable(); } private delegate void AddonSetupDelegate(AtkUnitBase* addon, uint valueCount, AtkValue* values); @@ -181,18 +189,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.onAddonSetupHook.Enable(); - this.onAddonSetup2Hook.Enable(); - this.onAddonFinalizeHook.Enable(); - this.onAddonDrawHook.Enable(); - this.onAddonUpdateHook.Enable(); - this.onAddonRefreshHook.Enable(); - this.onAddonRequestedUpdateHook.Enable(); - } - private void RegisterReceiveEventHook(AtkUnitBase* addon) { // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 3b3f65128..d387c2e2d 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -58,6 +58,8 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState this.framework.Update += this.FrameworkOnOnUpdateEvent; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; + + this.setupTerritoryTypeHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -120,12 +122,6 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.setupTerritoryTypeHook.Enable(); - } - private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType) { this.TerritoryType = terriType; diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 2db47ea4d..a298b1502 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -16,6 +16,9 @@ internal sealed partial class Condition : IServiceType, 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; + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); private readonly bool[] cache = new bool[MaxConditionEntries]; @@ -24,6 +27,12 @@ internal sealed partial class Condition : IServiceType, ICondition { var resolver = clientState.AddressResolver; this.Address = resolver.ConditionFlags; + + // Initialization + for (var i = 0; i < MaxConditionEntries; i++) + this.cache[i] = this[i]; + + this.framework.Update += this.FrameworkUpdate; } /// @@ -80,17 +89,7 @@ internal sealed partial class Condition : IServiceType, ICondition return false; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(Framework framework) - { - // Initialization - for (var i = 0; i < MaxConditionEntries; i++) - this.cache[i] = this[i]; - - framework.Update += this.FrameworkUpdate; - } - - private void FrameworkUpdate(IFramework framework) + private void FrameworkUpdate(IFramework unused) { for (var i = 0; i < MaxConditionEntries; i++) { @@ -144,7 +143,7 @@ internal sealed partial class Condition : IDisposable if (disposing) { - Service.Get().Update -= this.FrameworkUpdate; + this.framework.Update -= this.FrameworkUpdate; } this.isDisposed = true; diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index b03db6df2..40e632113 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -38,6 +38,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState var resolver = clientState.AddressResolver; Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}"); this.gamepadPoll = Hook.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour); + this.gamepadPoll?.Enable(); } private delegate int ControllerPoll(IntPtr controllerInput); @@ -114,12 +115,6 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState GC.SuppressFinalize(this); } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.gamepadPoll?.Enable(); - } - private int GamepadPollDetour(IntPtr gamepadInput) { var original = this.gamepadPoll!.Original(gamepadInput); diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 66356033b..c4bda0d19 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -37,6 +37,8 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState this.framework.Update += this.FrameworkOnUpdateEvent; this.clientState.TerritoryChanged += this.TerritoryOnChangedEvent; + + this.contentDirectorNetworkMessageHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -67,12 +69,6 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.contentDirectorNetworkMessageHook.Enable(); - } - private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* a3) { var category = *a3; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 6db9f7312..ce34f2c06 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -58,6 +58,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework this.updateHook = Hook.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate); this.destroyHook = Hook.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy); + + this.updateHook.Enable(); + this.destroyHook.Enable(); } /// @@ -330,13 +333,6 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.updateHook.Enable(); - this.destroyHook.Enable(); - } - private void RunPendingTickTasks() { if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 50c5b2908..8f2a617cf 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -50,6 +50,10 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui this.printMessageHook = Hook.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour); this.populateItemLinkHook = Hook.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); this.interactableLinkClickedHook = Hook.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); + + this.printMessageHook.Enable(); + this.populateItemLinkHook.Enable(); + this.interactableLinkClickedHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -182,14 +186,6 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui this.dalamudLinkHandlers.Remove((pluginName, commandId)); } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.printMessageHook.Enable(); - this.populateItemLinkHook.Enable(); - this.interactableLinkClickedHook.Enable(); - } - private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color) { var builder = new SeStringBuilder(); diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 36056883e..2383b4e53 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -36,6 +36,8 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer(this.Address.AddFlyText); this.createFlyTextHook = Hook.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour); + + this.createFlyTextHook.Enable(); } /// @@ -143,12 +145,6 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui return terminated; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(GameGui gameGui) - { - this.createFlyTextHook.Enable(); - } - private IntPtr CreateFlyTextDetour( IntPtr addonFlyText, FlyTextKind kind, diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index a1a17436e..a97e19a0a 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -75,6 +75,15 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui this.toggleUiHideHook = Hook.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour); this.utf8StringFromSequenceHook = Hook.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour); + + this.setGlobalBgmHook.Enable(); + this.handleItemHoverHook.Enable(); + this.handleItemOutHook.Enable(); + this.handleImmHook.Enable(); + this.toggleUiHideHook.Enable(); + this.handleActionHoverHook.Enable(); + this.handleActionOutHook.Enable(); + this.utf8StringFromSequenceHook.Enable(); } // Marshaled delegates @@ -376,19 +385,6 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui this.GameUiHidden = false; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.setGlobalBgmHook.Enable(); - this.handleItemHoverHook.Enable(); - this.handleItemOutHook.Enable(); - this.handleImmHook.Enable(); - this.toggleUiHideHook.Enable(); - this.handleActionHoverHook.Enable(); - this.handleActionOutHook.Enable(); - this.utf8StringFromSequenceHook.Enable(); - } - private IntPtr HandleSetGlobalBgmDetour(ushort bgmKey, byte a2, uint a3, uint a4, uint a5, byte a6) { var retVal = this.setGlobalBgmHook.Original(bgmKey, a2, a3, a4, a5, a6); diff --git a/Dalamud/Game/Gui/Internal/DalamudIME.cs b/Dalamud/Game/Gui/Internal/DalamudIME.cs index 37c072806..a9f6991ae 100644 --- a/Dalamud/Game/Gui/Internal/DalamudIME.cs +++ b/Dalamud/Game/Gui/Internal/DalamudIME.cs @@ -253,7 +253,7 @@ internal unsafe class DalamudIME : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady] + [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) { try diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 61c0f62e4..4a8332d24 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -35,6 +35,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize); this.receiveListingHook = Hook.FromAddress(this.address.ReceiveListing, this.HandleReceiveListingDetour); + this.receiveListingHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -60,12 +61,6 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(GameGui gameGui) - { - this.receiveListingHook.Enable(); - } - private void HandleReceiveListingDetour(IntPtr managerPtr, IntPtr data) { try diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 362edb3be..7491b7f13 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -41,6 +41,10 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui this.showNormalToastHook = Hook.FromAddress(this.address.ShowNormalToast, this.HandleNormalToastDetour); this.showQuestToastHook = Hook.FromAddress(this.address.ShowQuestToast, this.HandleQuestToastDetour); this.showErrorToastHook = Hook.FromAddress(this.address.ShowErrorToast, this.HandleErrorToastDetour); + + this.showNormalToastHook.Enable(); + this.showQuestToastHook.Enable(); + this.showErrorToastHook.Enable(); } #region Marshal delegates @@ -109,14 +113,6 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui return terminated; } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(GameGui gameGui) - { - this.showNormalToastHook.Enable(); - this.showQuestToastHook.Enable(); - this.showErrorToastHook.Enable(); - } - private SeString ParseString(IntPtr text) { var bytes = new List(); diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 0013dca4d..4eb605a76 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -63,6 +63,10 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings"); // this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened; + + this.hookAgentHudOpenSystemMenu.Enable(); + this.hookUiModuleRequestMainCommand.Enable(); + this.hookAtkUnitBaseReceiveGlobalEvent.Enable(); } private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize); @@ -75,14 +79,6 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5); - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(DalamudInterface dalamudInterface) - { - this.hookAgentHudOpenSystemMenu.Enable(); - this.hookUiModuleRequestMainCommand.Enable(); - this.hookAtkUnitBaseReceiveGlobalEvent.Enable(); - } - /* private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args) { diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 9ea3e491e..4099f228e 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -44,6 +44,9 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork this.processZonePacketDownHook = Hook.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour); this.processZonePacketUpHook = Hook.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour); + + this.processZonePacketDownHook.Enable(); + this.processZonePacketUpHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -62,13 +65,6 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork this.processZonePacketUpHook.Dispose(); } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction() - { - this.processZonePacketDownHook.Enable(); - this.processZonePacketUpHook.Enable(); - } - private void ProcessZonePacketDownDetour(IntPtr a, uint targetId, IntPtr dataPtr) { this.baseAddress = a; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 52e849c0e..1b12fd853 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1063,14 +1063,10 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction( - TargetSigScanner sigScanner, - DalamudAssetManager dalamudAssetManager, - DalamudConfiguration configuration) + [ServiceManager.CallWhenServicesReady( + "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] + private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration) { - dalamudAssetManager.WaitForAllRequiredAssets().Wait(); - this.address.Setup(sigScanner); this.framework.RunOnFrameworkThread(() => { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs index 49f3c1b90..22b53cdaa 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; @@ -13,6 +15,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class ServicesWidget : IDataWindowWidget { + private readonly Dictionary nodeRects = new(); + private readonly HashSet selectedNodes = new(); + private readonly HashSet tempRelatedNodes = new(); + + private bool includeUnloadDependencies; + private List>? dependencyNodes; + /// public string[]? CommandShortcuts { get; init; } = { "services" }; @@ -33,27 +42,294 @@ internal class ServicesWidget : IDataWindowWidget { var container = Service.Get(); - foreach (var instance in container.Instances) + if (ImGui.CollapsingHeader("Dependencies")) { - var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key); - var isPublic = instance.Key.IsPublic; + if (ImGui.Button("Clear selection")) + this.selectedNodes.Clear(); - ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})"); - - using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) + ImGui.SameLine(); + switch (this.includeUnloadDependencies) { - ImGui.Text(hasInterface - ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" - : "\t => NO INTERFACE!!!"); + case true when ImGui.Button("Show load-time dependencies"): + this.includeUnloadDependencies = false; + this.dependencyNodes = null; + break; + case false when ImGui.Button("Show unload-time dependencies"): + this.includeUnloadDependencies = true; + this.dependencyNodes = null; + break; } - if (isPublic) + this.dependencyNodes ??= ServiceDependencyNode.CreateTreeByLevel(this.includeUnloadDependencies); + var cellPad = ImGui.CalcTextSize("WW"); + var margin = ImGui.CalcTextSize("W\nW\nW"); + var rowHeight = cellPad.Y * 3; + var width = ImGui.GetContentRegionAvail().X; + if (ImGui.BeginChild( + "dependency-graph", + new(width, (this.dependencyNodes.Count * (rowHeight + margin.Y)) + cellPad.Y), + false, + ImGuiWindowFlags.HorizontalScrollbar)) { - using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.Text("\t => PUBLIC!!!"); + const uint rectBaseBorderColor = 0xFFFFFFFF; + const uint rectHoverFillColor = 0xFF404040; + const uint rectHoverRelatedFillColor = 0xFF802020; + const uint rectSelectedFillColor = 0xFF20A020; + const uint rectSelectedRelatedFillColor = 0xFF204020; + const uint lineBaseColor = 0xFF808080; + const uint lineHoverColor = 0xFFFF8080; + const uint lineHoverNotColor = 0xFF404040; + const uint lineSelectedColor = 0xFF80FF00; + const uint lineInvalidColor = 0xFFFF0000; + + ServiceDependencyNode? hoveredNode = null; + + var pos = ImGui.GetCursorScreenPos(); + var dl = ImGui.GetWindowDrawList(); + var mouse = ImGui.GetMousePos(); + var maxRowWidth = 0f; + + // 1. Layout + for (var level = 0; level < this.dependencyNodes.Count; level++) + { + var levelNodes = this.dependencyNodes[level]; + + var rowWidth = 0f; + foreach (var node in levelNodes) + rowWidth += ImGui.CalcTextSize(node.TypeName).X + cellPad.X + margin.X; + + var off = cellPad / 2; + if (rowWidth < width) + off.X += ImGui.GetScrollX() + ((width - rowWidth) / 2); + else if (rowWidth - ImGui.GetScrollX() < width) + off.X += width - (rowWidth - ImGui.GetScrollX()); + off.Y = (rowHeight + margin.Y) * level; + + foreach (var node in levelNodes) + { + var textSize = ImGui.CalcTextSize(node.TypeName); + var cellSize = textSize + cellPad; + + var rc = new Vector4(pos + off, pos.X + off.X + cellSize.X, pos.Y + off.Y + cellSize.Y); + this.nodeRects[node] = rc; + if (rc.X <= mouse.X && mouse.X < rc.Z && rc.Y <= mouse.Y && mouse.Y < rc.W) + { + hoveredNode = node; + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + if (this.selectedNodes.Contains(node.Type)) + this.selectedNodes.Remove(node.Type); + else + this.selectedNodes.Add(node.Type); + } + } + + off.X += cellSize.X + margin.X; + } + + maxRowWidth = Math.Max(maxRowWidth, rowWidth); + } + + // 2. Draw non-hovered lines + foreach (var levelNodes in this.dependencyNodes) + { + foreach (var node in levelNodes) + { + var rect = this.nodeRects[node]; + var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y); + + foreach (var parent in node.InvalidParents) + { + rect = this.nodeRects[parent]; + var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W); + if (node == hoveredNode || parent == hoveredNode) + continue; + + dl.AddLine(point1, point2, lineInvalidColor, 2f * ImGuiHelpers.GlobalScale); + } + + foreach (var parent in node.Parents) + { + rect = this.nodeRects[parent]; + var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W); + if (node == hoveredNode || parent == hoveredNode) + continue; + + var isSelected = this.selectedNodes.Contains(node.Type) || + this.selectedNodes.Contains(parent.Type); + dl.AddLine( + point1, + point2, + isSelected + ? lineSelectedColor + : hoveredNode is not null + ? lineHoverNotColor + : lineBaseColor); + } + } + } + + // 3. Draw boxes + foreach (var levelNodes in this.dependencyNodes) + { + foreach (var node in levelNodes) + { + var textSize = ImGui.CalcTextSize(node.TypeName); + var cellSize = textSize + cellPad; + + var rc = this.nodeRects[node]; + if (hoveredNode == node) + dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectHoverFillColor); + else if (this.selectedNodes.Contains(node.Type)) + dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectSelectedFillColor); + else if (node.Relatives.Any(x => this.selectedNodes.Contains(x.Type))) + dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectSelectedRelatedFillColor); + else if (hoveredNode?.Relatives.Select(x => x.Type).Contains(node.Type) is true) + dl.AddRectFilled(new(rc.X, rc.Y), new(rc.Z, rc.W), rectHoverRelatedFillColor); + + dl.AddRect(new(rc.X, rc.Y), new(rc.Z, rc.W), rectBaseBorderColor); + ImGui.SetCursorPos((new Vector2(rc.X, rc.Y) - pos) + ((cellSize - textSize) / 2)); + ImGui.TextUnformatted(node.TypeName); + } + } + + // 4. Draw hovered lines + if (hoveredNode is not null) + { + foreach (var levelNodes in this.dependencyNodes) + { + foreach (var node in levelNodes) + { + var rect = this.nodeRects[node]; + var point1 = new Vector2((rect.X + rect.Z) / 2, rect.Y); + foreach (var parent in node.Parents) + { + if (node == hoveredNode || parent == hoveredNode) + { + rect = this.nodeRects[parent]; + var point2 = new Vector2((rect.X + rect.Z) / 2, rect.W); + dl.AddLine( + point1, + point2, + lineHoverColor, + 2 * ImGuiHelpers.GlobalScale); + } + } + } + } + } + + ImGui.SetCursorPos(default); + ImGui.Dummy(new(maxRowWidth, this.dependencyNodes.Count * rowHeight)); + ImGui.EndChild(); } - - ImGuiHelpers.ScaledDummy(2); + } + + if (ImGui.CollapsingHeader("Plugin-facing Services")) + { + foreach (var instance in container.Instances) + { + var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key); + var isPublic = instance.Key.IsPublic; + + ImGui.BulletText($"{instance.Key.FullName} ({instance.Key.GetServiceKind()})"); + + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) + { + ImGui.Text( + hasInterface + ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" + : "\t => NO INTERFACE!!!"); + } + + if (isPublic) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.Text("\t => PUBLIC!!!"); + } + + ImGuiHelpers.ScaledDummy(2); + } + } + } + + private class ServiceDependencyNode + { + private readonly List parents = new(); + private readonly List children = new(); + private readonly List invalidParents = new(); + + private ServiceDependencyNode(Type t) => this.Type = t; + + public Type Type { get; } + + public string TypeName => this.Type.Name; + + public IReadOnlyList Parents => this.parents; + + public IReadOnlyList Children => this.children; + + public IReadOnlyList InvalidParents => this.invalidParents; + + public IEnumerable Relatives => + this.parents.Concat(this.children).Concat(this.invalidParents); + + public int Level { get; private set; } + + public static List CreateTree(bool includeUnloadDependencies) + { + var nodes = new Dictionary(); + foreach (var t in ServiceManager.GetConcreteServiceTypes()) + nodes.Add(typeof(Service<>).MakeGenericType(t), new(t)); + foreach (var t in ServiceManager.GetConcreteServiceTypes()) + { + var st = typeof(Service<>).MakeGenericType(t); + var node = nodes[st]; + foreach (var depType in ServiceHelpers.GetDependencies(st, includeUnloadDependencies)) + { + var depServiceType = typeof(Service<>).MakeGenericType(depType); + var depNode = nodes[depServiceType]; + if (node.IsAncestorOf(depType)) + { + node.invalidParents.Add(depNode); + } + else + { + depNode.UpdateNodeLevel(1); + node.UpdateNodeLevel(depNode.Level + 1); + node.parents.Add(depNode); + depNode.children.Add(node); + } + } + } + + return nodes.Values.OrderBy(x => x.Level).ThenBy(x => x.Type.Name).ToList(); + } + + public static List> CreateTreeByLevel(bool includeUnloadDependencies) + { + var res = new List>(); + foreach (var n in CreateTree(includeUnloadDependencies)) + { + while (res.Count <= n.Level) + res.Add(new()); + res[n.Level].Add(n); + } + + return res; + } + + private bool IsAncestorOf(Type type) => + this.children.Any(x => x.Type == type) || this.children.Any(x => x.IsAncestorOf(type)); + + private void UpdateNodeLevel(int newLevel) + { + if (this.Level >= newLevel) + return; + + this.Level = newLevel; + foreach (var c in this.children) + c.UpdateNodeLevel(newLevel + 1); } } } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 363d01f26..0ef3d49f8 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -21,6 +21,7 @@ using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.PluginInstaller; +using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Networking.Http; @@ -29,6 +30,7 @@ using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Plugin.Ipc.Internal; +using Dalamud.Support; using Dalamud.Utility; using Dalamud.Utility.Timing; using Newtonsoft.Json; @@ -93,7 +95,9 @@ internal partial class PluginManager : IDisposable, IServiceType } [ServiceManager.ServiceConstructor] - private PluginManager() + private PluginManager( + ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker, + ServiceManager.RegisterUnloadAfterDelegate registerUnloadAfter) { this.pluginDirectory = new DirectoryInfo(this.dalamud.StartInfo.PluginDirectory!); @@ -142,6 +146,14 @@ internal partial class PluginManager : IDisposable, IServiceType this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient); this.ApplyPatches(); + + registerStartupBlocker( + Task.Run(this.LoadAndStartLoadSyncPlugins), + "Waiting for plugins that asked to be loaded before the game."); + + registerUnloadAfter( + ResolvePossiblePluginDependencyServices(), + "See the attached comment for the called function."); } /// @@ -1201,6 +1213,49 @@ internal partial class PluginManager : IDisposable, IServiceType /// The calling plugin, or null. public LocalPlugin? FindCallingPlugin() => this.FindCallingPlugin(new StackTrace()); + /// + /// Resolves the services that a plugin may have a dependency on.
+ /// This is required, as the lifetime of a plugin cannot be longer than PluginManager, + /// and we want to ensure that dependency services to be kept alive at least until all the plugins, and thus + /// PluginManager to be gone. + ///
+ /// The dependency services. + private static IEnumerable ResolvePossiblePluginDependencyServices() + { + foreach (var serviceType in ServiceManager.GetConcreteServiceTypes()) + { + if (serviceType == typeof(PluginManager)) + continue; + + // Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away. + // Nonetheless, their direct dependencies must be considered. + if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService) + { + var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); + var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT, false); + ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count); + + foreach (var scopedDep in dependencies) + { + if (scopedDep == typeof(PluginManager)) + throw new Exception("Scoped plugin services cannot depend on PluginManager."); + + ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!); + yield return scopedDep; + } + + continue; + } + + var pluginInterfaceAttribute = serviceType.GetCustomAttribute(true); + if (pluginInterfaceAttribute == null) + continue; + + ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!); + yield return serviceType; + } + } + private async Task DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting) { var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; @@ -1590,6 +1645,38 @@ internal partial class PluginManager : IDisposable, IServiceType } } + private void LoadAndStartLoadSyncPlugins() + { + try + { + using (Timings.Start("PM Load Plugin Repos")) + { + _ = this.SetPluginReposFromConfigAsync(false); + this.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting); + + Log.Information("[T3] PM repos OK!"); + } + + using (Timings.Start("PM Cleanup Plugins")) + { + this.CleanupPlugins(); + Log.Information("[T3] PMC OK!"); + } + + using (Timings.Start("PM Load Sync Plugins")) + { + this.LoadAllPlugins().Wait(); + Log.Information("[T3] PML OK!"); + } + + _ = Task.Run(Troubleshooting.LogTroubleshooting); + } + catch (Exception ex) + { + Log.Error(ex, "Plugin load failed"); + } + } + private static class Locs { public static string DalamudPluginUpdateSuccessful(string name, Version version) => Loc.Localize("DalamudPluginUpdateSuccessful", " 》 {0} updated to v{1}.").Format(name, version); diff --git a/Dalamud/Plugin/Internal/StartupPluginLoader.cs b/Dalamud/Plugin/Internal/StartupPluginLoader.cs deleted file mode 100644 index 4f68d39fc..000000000 --- a/Dalamud/Plugin/Internal/StartupPluginLoader.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading.Tasks; - -using Dalamud.Logging.Internal; -using Dalamud.Support; -using Dalamud.Utility.Timing; - -namespace Dalamud.Plugin.Internal; - -/// -/// Class responsible for loading plugins on startup. -/// -[ServiceManager.BlockingEarlyLoadedService] -public class StartupPluginLoader : IServiceType -{ - private static readonly ModuleLog Log = new("SPL"); - - [ServiceManager.ServiceConstructor] - private StartupPluginLoader(PluginManager pluginManager) - { - try - { - using (Timings.Start("PM Load Plugin Repos")) - { - _ = pluginManager.SetPluginReposFromConfigAsync(false); - pluginManager.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting); - - Log.Information("[T3] PM repos OK!"); - } - - using (Timings.Start("PM Cleanup Plugins")) - { - pluginManager.CleanupPlugins(); - Log.Information("[T3] PMC OK!"); - } - - using (Timings.Start("PM Load Sync Plugins")) - { - pluginManager.LoadAllPlugins().Wait(); - Log.Information("[T3] PML OK!"); - } - - Task.Run(Troubleshooting.LogTroubleshooting); - } - catch (Exception ex) - { - Log.Error(ex, "Plugin load failed"); - } - } -} diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 21c08ce72..3ff7cde76 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -11,6 +11,7 @@ using Dalamud.Game; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Storage; +using Dalamud.Utility; using Dalamud.Utility.Timing; using JetBrains.Annotations; @@ -21,7 +22,7 @@ namespace Dalamud; // - Visualize/output .dot or imgui thing /// -/// Class to initialize Service<T>s. +/// Class to initialize . /// internal static class ServiceManager { @@ -43,6 +44,26 @@ internal static class ServiceManager private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); private static ManualResetEvent unloadResetEvent = new(false); + + /// + /// Delegate for registering startup blocker task.
+ /// Do not use this delegate outside the constructor. + ///
+ /// The blocker task. + /// The justification for using this feature. + [InjectableType] + public delegate void RegisterStartupBlockerDelegate(Task t, string justification); + + /// + /// Delegate for registering services that should be unloaded before self.
+ /// Intended for use with . If you think you need to use this outside + /// of that, consider having a discussion first.
+ /// Do not use this delegate outside the constructor. + ///
+ /// Services that should be unloaded first. + /// The justification for using this feature. + [InjectableType] + public delegate void RegisterUnloadAfterDelegate(IEnumerable unloadAfter, string justification); /// /// Kinds of services. @@ -125,6 +146,15 @@ internal static class ServiceManager #endif } + /// + /// Gets the concrete types of services, i.e. the non-abstract non-interface types. + /// + /// The enumerable of service types, that may be enumerated only once per call. + public static IEnumerable GetConcreteServiceTypes() => + Assembly.GetExecutingAssembly() + .GetTypes() + .Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract); + /// /// Kicks off construction of services that can handle early loading. /// @@ -141,7 +171,7 @@ internal static class ServiceManager var serviceContainer = Service.Get(); - foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes().Where(x => x.IsAssignableTo(typeof(IServiceType)) && !x.IsInterface && !x.IsAbstract)) + foreach (var serviceType in GetConcreteServiceTypes()) { var serviceKind = serviceType.GetServiceKind(); Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind"); @@ -157,7 +187,7 @@ internal static class ServiceManager var getTask = (Task)genericWrappedServiceType .InvokeMember( - "GetAsync", + nameof(Service.GetAsync), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, @@ -184,17 +214,42 @@ internal static class ServiceManager } var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); - dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT) + dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, false) .Select(x => typeof(Service<>).MakeGenericType(x)) .ToList(); } + var blockerTasks = new List(); _ = Task.Run(async () => { try { - var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); - while (await Task.WhenAny(whenBlockingComplete, Task.Delay(120000)) != whenBlockingComplete) + // Wait for all blocking constructors to complete first. + await WaitWithTimeoutConsent(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); + + // All the BlockingEarlyLoadedService constructors have been run, + // and blockerTasks now will not change. Now wait for them. + // Note that ServiceManager.CallWhenServicesReady does not get to register a blocker. + await WaitWithTimeoutConsent(blockerTasks); + + BlockingServicesLoadedTaskCompletionSource.SetResult(); + Timings.Event("BlockingServices Initialized"); + } + catch (Exception e) + { + BlockingServicesLoadedTaskCompletionSource.SetException(e); + } + + return; + + async Task WaitWithTimeoutConsent(IEnumerable tasksEnumerable) + { + var tasks = tasksEnumerable.AsReadOnlyCollection(); + if (tasks.Count == 0) + return; + + var aggregatedTask = Task.WhenAll(tasks); + while (await Task.WhenAny(aggregatedTask, Task.Delay(120000)) != aggregatedTask) { if (NativeFunctions.MessageBoxW( IntPtr.Zero, @@ -208,13 +263,6 @@ internal static class ServiceManager "and the user chose to continue without Dalamud."); } } - - BlockingServicesLoadedTaskCompletionSource.SetResult(); - Timings.Event("BlockingServices Initialized"); - } - catch (Exception e) - { - BlockingServicesLoadedTaskCompletionSource.SetException(e); } }).ConfigureAwait(false); @@ -249,6 +297,25 @@ internal static class ServiceManager if (!hasDeps) continue; + // This object will be used in a task. Each task must receive a new object. + var startLoaderArgs = new List(); + if (serviceType.GetCustomAttribute() is not null) + { + startLoaderArgs.Add( + new RegisterStartupBlockerDelegate( + (task, justification) => + { +#if DEBUG + if (CurrentConstructorServiceType.Value != serviceType) + throw new InvalidOperationException("Forbidden."); +#endif + blockerTasks.Add(task); + + // No need to store the justification; the fact that the reason is specified is good enough. + _ = justification; + })); + } + tasks.Add((Task)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( @@ -256,7 +323,7 @@ internal static class ServiceManager BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, - null)); + new object[] { startLoaderArgs })); servicesToLoad.Remove(serviceType); #if DEBUG @@ -328,13 +395,13 @@ internal static class ServiceManager unloadResetEvent.Reset(); - var dependencyServicesMap = new Dictionary>(); + var dependencyServicesMap = new Dictionary>(); var allToUnload = new HashSet(); var unloadOrder = new List(); Log.Information("==== COLLECTING SERVICES TO UNLOAD ===="); - foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) + foreach (var serviceType in GetConcreteServiceTypes()) { if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; @@ -347,7 +414,7 @@ internal static class ServiceManager Log.Verbose("Calling GetDependencyServices for '{ServiceName}'", serviceType.FullName!); var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); - dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT); + dependencyServicesMap[serviceType] = ServiceHelpers.GetDependencies(typeAsServiceT, true); allToUnload.Add(serviceType); } @@ -541,11 +608,35 @@ internal static class ServiceManager } /// - /// Indicates that the method should be called when the services given in the constructor are ready. + /// Indicates that the method should be called when the services given in the marked method's parameters are ready. + /// This will be executed immediately after the constructor has run, if all services specified as its parameters + /// are already ready, or no parameter is given. /// [AttributeUsage(AttributeTargets.Method)] [MeansImplicitUse] public class CallWhenServicesReady : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// Specify the reason here. + public CallWhenServicesReady(string justification) + { + // No need to store the justification; the fact that the reason is specified is good enough. + _ = justification; + } + } + + /// + /// Indicates that something is a candidate for being considered as an injected parameter for constructors. + /// + [AttributeUsage( + AttributeTargets.Delegate + | AttributeTargets.Class + | AttributeTargets.Struct + | AttributeTargets.Enum + | AttributeTargets.Interface)] + public class InjectableTypeAttribute : Attribute { } } diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 9c7f0411d..08f592826 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Dalamud.IoC; using Dalamud.IoC.Internal; -using Dalamud.Plugin.Internal; using Dalamud.Utility.Timing; using JetBrains.Annotations; @@ -25,6 +24,7 @@ internal static class Service where T : IServiceType private static readonly ServiceManager.ServiceAttribute ServiceAttribute; private static TaskCompletionSource instanceTcs = new(); private static List? dependencyServices; + private static List? dependencyServicesForUnload; static Service() { @@ -95,7 +95,7 @@ internal static class Service where T : IServiceType if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService && ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType) { - var deps = ServiceHelpers.GetDependencies(currentServiceType); + var deps = ServiceHelpers.GetDependencies(typeof(Service<>).MakeGenericType(currentServiceType), false); if (!deps.Contains(typeof(T))) { throw new InvalidOperationException( @@ -115,7 +115,6 @@ internal static class Service where T : IServiceType /// Pull the instance out of the service locator, waiting if necessary. /// /// The object. - [UsedImplicitly] public static Task GetAsync() => instanceTcs.Task; /// @@ -141,11 +140,15 @@ internal static class Service where T : IServiceType /// /// Gets an enumerable containing s that are required for this Service to initialize /// without blocking. + /// These are NOT returned as types; raw types will be returned. /// + /// Whether to include the unload dependencies. /// List of dependency services. - [UsedImplicitly] - public static List GetDependencyServices() + public static IReadOnlyCollection GetDependencyServices(bool includeUnloadDependencies) { + if (includeUnloadDependencies && dependencyServicesForUnload is not null) + return dependencyServicesForUnload; + if (dependencyServices is not null) return dependencyServices; @@ -158,7 +161,8 @@ internal static class Service where T : IServiceType { res.AddRange(ctor .GetParameters() - .Select(x => x.ParameterType)); + .Select(x => x.ParameterType) + .Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None)); } res.AddRange(typeof(T) @@ -171,50 +175,8 @@ internal static class Service where T : IServiceType .OfType() .Select(x => x.GetType().GetGenericArguments().First())); - // HACK: PluginManager needs to depend on ALL plugin exposed services - if (typeof(T) == typeof(PluginManager)) - { - foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) - { - if (!serviceType.IsAssignableTo(typeof(IServiceType))) - continue; - - if (serviceType == typeof(PluginManager)) - continue; - - // Scoped plugin services lifetime is tied to their scopes. They go away when LocalPlugin goes away. - // Nonetheless, their direct dependencies must be considered. - if (serviceType.GetServiceKind() == ServiceManager.ServiceKind.ScopedService) - { - var typeAsServiceT = ServiceHelpers.GetAsService(serviceType); - var dependencies = ServiceHelpers.GetDependencies(typeAsServiceT); - ServiceManager.Log.Verbose("Found dependencies of scoped plugin service {Type} ({Cnt})", serviceType.FullName!, dependencies!.Count); - - foreach (var scopedDep in dependencies) - { - if (scopedDep == typeof(PluginManager)) - throw new Exception("Scoped plugin services cannot depend on PluginManager."); - - ServiceManager.Log.Verbose("PluginManager MUST depend on {Type} via {BaseType}", scopedDep.FullName!, serviceType.FullName!); - res.Add(scopedDep); - } - - continue; - } - - var pluginInterfaceAttribute = serviceType.GetCustomAttribute(true); - if (pluginInterfaceAttribute == null) - continue; - - ServiceManager.Log.Verbose("PluginManager MUST depend on {Type}", serviceType.FullName!); - res.Add(serviceType); - } - } - foreach (var type in res) - { ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name); - } var deps = res .Distinct() @@ -244,8 +206,9 @@ internal static class Service where T : IServiceType /// /// Starts the service loader. Only to be called from . /// + /// Additional objects available to constructors. /// The loader task. - internal static Task StartLoader() + internal static Task StartLoader(IReadOnlyCollection additionalProvidedTypedObjects) { if (instanceTcs.Task.IsCompleted) throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed."); @@ -256,10 +219,27 @@ internal static class Service where T : IServiceType return Task.Run(Timings.AttachTimingHandle(async () => { + var ctorArgs = new List(additionalProvidedTypedObjects.Count + 1); + ctorArgs.AddRange(additionalProvidedTypedObjects); + ctorArgs.Add( + new ServiceManager.RegisterUnloadAfterDelegate( + (additionalDependencies, justification) => + { +#if DEBUG + if (ServiceManager.CurrentConstructorServiceType.Value != typeof(T)) + throw new InvalidOperationException("Forbidden."); +#endif + dependencyServicesForUnload ??= new(GetDependencyServices(false)); + dependencyServicesForUnload.AddRange(additionalDependencies); + + // No need to store the justification; the fact that the reason is specified is good enough. + _ = justification; + })); + ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name); try { - var instance = await ConstructObject(); + var instance = await ConstructObject(ctorArgs).ConfigureAwait(false); instanceTcs.SetResult(instance); List? tasks = null; @@ -270,8 +250,17 @@ internal static class Service where T : IServiceType continue; ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name); - var args = await Task.WhenAll(method.GetParameters().Select( - x => ResolveServiceFromTypeAsync(x.ParameterType))); + var args = await ResolveInjectedParameters( + method.GetParameters(), + Array.Empty()).ConfigureAwait(false); + if (args.Length == 0) + { + ServiceManager.Log.Warning( + "Service<{0}>: Method {1} does not have any arguments. Consider merging it with the ctor.", + typeof(T).Name, + method.Name); + } + try { if (method.Invoke(instance, args) is Task task) @@ -331,24 +320,6 @@ internal static class Service where T : IServiceType instanceTcs.SetException(new UnloadedException()); } - private static async Task ResolveServiceFromTypeAsync(Type type) - { - var task = (Task)typeof(Service<>) - .MakeGenericType(type) - .InvokeMember( - "GetAsync", - BindingFlags.InvokeMethod | - BindingFlags.Static | - BindingFlags.Public, - null, - null, - null)!; - await task; - return typeof(Task<>).MakeGenericType(type) - .GetProperty("Result", BindingFlags.Instance | BindingFlags.Public)! - .GetValue(task); - } - private static ConstructorInfo? GetServiceConstructor() { const BindingFlags ctorBindingFlags = @@ -359,18 +330,18 @@ internal static class Service where T : IServiceType .SingleOrDefault(x => x.GetCustomAttributes(typeof(ServiceManager.ServiceConstructor), true).Any()); } - private static async Task ConstructObject() + private static async Task ConstructObject(IReadOnlyCollection additionalProvidedTypedObjects) { var ctor = GetServiceConstructor(); if (ctor == null) throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor"); - var args = await Task.WhenAll( - ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType))); + var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects) + .ConfigureAwait(false); using (Timings.Start($"{typeof(T).Name} Construct")) { #if DEBUG - ServiceManager.CurrentConstructorServiceType.Value = typeof(Service); + ServiceManager.CurrentConstructorServiceType.Value = typeof(T); try { return (T)ctor.Invoke(args)!; @@ -385,6 +356,43 @@ internal static class Service where T : IServiceType } } + private static Task ResolveInjectedParameters( + IReadOnlyList argDefs, + IReadOnlyCollection additionalProvidedTypedObjects) + { + var argTasks = new Task[argDefs.Count]; + for (var i = 0; i < argDefs.Count; i++) + { + var argType = argDefs[i].ParameterType; + ref var argTask = ref argTasks[i]; + + if (argType.GetCustomAttribute() is not null) + { + argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType)); + continue; + } + + argTask = (Task)typeof(Service<>) + .MakeGenericType(argType) + .InvokeMember( + nameof(GetAsyncAsObject), + BindingFlags.InvokeMethod | + BindingFlags.Static | + BindingFlags.NonPublic, + null, + null, + null)!; + } + + return Task.WhenAll(argTasks); + } + + /// + /// Pull the instance out of the service locator, waiting if necessary. + /// + /// The object. + private static Task GetAsyncAsObject() => instanceTcs.Task.ContinueWith(r => (object)r.Result); + /// /// Exception thrown when service is attempted to be retrieved when it's unloaded. /// @@ -407,11 +415,12 @@ internal static class ServiceHelpers { /// /// Get a list of dependencies for a service. Only accepts types. - /// These are returned as types. + /// These are NOT returned as types; raw types will be returned. /// /// The dependencies for this service. + /// Whether to include the unload dependencies. /// A list of dependencies. - public static List GetDependencies(Type serviceType) + public static IReadOnlyCollection GetDependencies(Type serviceType, bool includeUnloadDependencies) { #if DEBUG if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>)) @@ -422,12 +431,12 @@ internal static class ServiceHelpers } #endif - return (List)serviceType.InvokeMember( + return (IReadOnlyCollection)serviceType.InvokeMember( nameof(Service.GetDependencyServices), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, null, null, - null) ?? new List(); + new object?[] { includeUnloadDependencies }) ?? new List(); } /// diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 30441f479..70a91c4bf 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -44,7 +44,10 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA private bool isDisposed; [ServiceManager.ServiceConstructor] - private DalamudAssetManager(Dalamud dalamud, HappyHttpClient httpClient) + private DalamudAssetManager( + Dalamud dalamud, + HappyHttpClient httpClient, + ServiceManager.RegisterStartupBlockerDelegate registerStartupBlocker) { this.dalamud = dalamud; this.httpClient = httpClient; @@ -55,8 +58,17 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA this.fileStreams = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); this.textureWraps = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); + // Block until all the required assets to be ready. var loadTimings = Timings.Start("DAM LoadAll"); - this.WaitForAllRequiredAssets().ContinueWith(_ => loadTimings.Dispose()); + registerStartupBlocker( + Task.WhenAll( + Enum.GetValues() + .Where(x => x is not DalamudAsset.Empty4X4) + .Where(x => x.GetAttribute()?.Required is true) + .Select(this.CreateStreamAsync) + .Select(x => x.ToContentDisposedTask())) + .ContinueWith(_ => loadTimings.Dispose()), + "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); } /// @@ -83,25 +95,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA this.scopedFinalizer.Dispose(); } - /// - /// Waits for all the required assets to be ready. Will result in a faulted task, if any of the required assets - /// has failed to load. - /// - /// The task. - [Pure] - public Task WaitForAllRequiredAssets() - { - lock (this.syncRoot) - { - return Task.WhenAll( - Enum.GetValues() - .Where(x => x is not DalamudAsset.Empty4X4) - .Where(x => x.GetAttribute()?.Required is true) - .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask())); - } - } - /// [Pure] public bool IsStreamImmediatelyAvailable(DalamudAsset asset) => diff --git a/Dalamud/Utility/ArrayExtensions.cs b/Dalamud/Utility/ArrayExtensions.cs index afb1511e3..fa6e3dbe9 100644 --- a/Dalamud/Utility/ArrayExtensions.cs +++ b/Dalamud/Utility/ArrayExtensions.cs @@ -87,4 +87,14 @@ internal static class ArrayExtensions result = default; return false; } + + /// + /// Interprets the given array as an , so that you can enumerate it multiple + /// times, and know the number of elements within. + /// + /// The enumerable. + /// The element type. + /// casted as a if it is one; otherwise the result of . + public static IReadOnlyCollection AsReadOnlyCollection(this IEnumerable array) => + array as IReadOnlyCollection ?? array.ToArray(); } From 30c2872400ec21360ea21334ab5ff9a2521c7b11 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 8 Dec 2023 08:08:43 +0900 Subject: [PATCH 354/585] Fix ChatGui race condition (#1563) --- Dalamud/Game/Gui/ChatGui.cs | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 8f2a617cf..5e8b4f3a2 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Runtime.InteropServices; @@ -40,6 +40,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui private readonly LibcFunction libcFunction = Service.Get(); private IntPtr baseAddress = IntPtr.Zero; + private ImmutableDictionary<(string PluginName, uint CommandId), Action>? dalamudLinkHandlersCopy; [ServiceManager.ServiceConstructor] private ChatGui(TargetSigScanner sigScanner) @@ -84,7 +85,21 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui public byte LastLinkedItemFlags { get; private set; } /// - public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers => this.dalamudLinkHandlers; + public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers + { + get + { + var copy = this.dalamudLinkHandlersCopy; + if (copy is not null) + return copy; + + lock (this.dalamudLinkHandlers) + { + return this.dalamudLinkHandlersCopy ??= + this.dalamudLinkHandlers.ToImmutableDictionary(x => x.Key, x => x.Value); + } + } + } /// /// Dispose of managed and unmanaged resources. @@ -160,7 +175,12 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action commandAction) { var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId }; - this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction); + lock (this.dalamudLinkHandlers) + { + this.dalamudLinkHandlers.Add((pluginName, commandId), commandAction); + this.dalamudLinkHandlersCopy = null; + } + return payload; } @@ -170,9 +190,14 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui /// The name of the plugin handling the links. internal void RemoveChatLinkHandler(string pluginName) { - foreach (var handler in this.dalamudLinkHandlers.Keys.ToList().Where(k => k.PluginName == pluginName)) + lock (this.dalamudLinkHandlers) { - this.dalamudLinkHandlers.Remove(handler); + var changed = false; + + foreach (var handler in this.RegisteredLinkHandlers.Keys.Where(k => k.PluginName == pluginName)) + changed |= this.dalamudLinkHandlers.Remove(handler); + if (changed) + this.dalamudLinkHandlersCopy = null; } } @@ -183,7 +208,11 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui /// The ID of the command to be removed. internal void RemoveChatLinkHandler(string pluginName, uint commandId) { - this.dalamudLinkHandlers.Remove((pluginName, commandId)); + lock (this.dalamudLinkHandlers) + { + if (this.dalamudLinkHandlers.Remove((pluginName, commandId))) + this.dalamudLinkHandlersCopy = null; + } } private void PrintTagged(string message, XivChatType channel, string? tag, ushort? color) @@ -391,7 +420,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui var linkPayload = payloads[0]; if (linkPayload is DalamudLinkPayload link) { - if (this.dalamudLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value)) + if (this.RegisteredLinkHandlers.TryGetValue((link.Plugin, link.CommandId), out var value)) { Log.Verbose($"Sending DalamudLink to {link.Plugin}: {link.CommandId}"); value.Invoke(link.CommandId, new SeString(payloads)); From 6637bd82073fb3c9e84038026900db7fee3df6ce Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Fri, 8 Dec 2023 00:10:15 +0100 Subject: [PATCH 355/585] Use the substitution provider for textures in the ULD Wrapper. (#1553) --- Dalamud/Interface/UldWrapper.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index e78546ed9..127ea85ec 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using Dalamud.Data; using Dalamud.Interface.Internal; using Dalamud.Utility; -using ImGuiScene; using Lumina.Data.Files; using Lumina.Data.Parsing.Uld; @@ -155,20 +155,27 @@ public class UldWrapper : IDisposable // Try to load HD textures first. var hrPath = texturePath.Replace(".tex", "_hr1.tex"); + var substitution = Service.Get(); + hrPath = substitution.GetSubstitutedPath(hrPath); var hd = true; - var file = this.data.GetFile(hrPath); - if (file == null) + var tex = Path.IsPathRooted(hrPath) + ? this.data.GameData.GetFileFromDisk(hrPath) + : this.data.GetFile(hrPath); + if (tex == null) { hd = false; - file = this.data.GetFile(texturePath); + texturePath = substitution.GetSubstitutedPath(texturePath); + tex = Path.IsPathRooted(texturePath) + ? this.data.GameData.GetFileFromDisk(texturePath) + : this.data.GetFile(texturePath); // Neither texture could be loaded. - if (file == null) + if (tex == null) { return null; } } - return (id, file.Header.Width, file.Header.Height, hd, file.GetRgbaImageData()); + return (id, tex.Header.Width, tex.Header.Height, hd, tex.GetRgbaImageData()); } } From 9489c4ec20c98c01b76fd1c5bd359a881fd505ee Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 8 Dec 2023 00:22:09 +0100 Subject: [PATCH 356/585] Refactor ChatGui internals to use CS additions (#1520) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- Dalamud/Game/Gui/ChatGui.cs | 121 +++++++-------------- Dalamud/Game/Gui/ChatGuiAddressResolver.cs | 73 ------------- 2 files changed, 41 insertions(+), 153 deletions(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 5e8b4f3a2..1214850b0 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -4,26 +4,34 @@ using System.Linq; using System.Runtime.InteropServices; using Dalamud.Configuration.Internal; -using Dalamud.Game.Libc; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Memory; using Dalamud.Plugin.Services; using Dalamud.Utility; -using Serilog; +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; namespace Dalamud.Game.Gui; +// TODO(api10): Update IChatGui, ChatGui and XivChatEntry to use correct types and names: +// "uint SenderId" should be "int Timestamp". +// "IntPtr Parameters" should be something like "bool Silent". It suppresses new message sounds in certain channels. + /// /// This class handles interacting with the native chat UI. /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class ChatGui : IDisposable, IServiceType, IChatGui +internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui { + private static readonly ModuleLog Log = new("ChatGui"); + private readonly ChatGuiAddressResolver address; private readonly Queue chatQueue = new(); @@ -36,10 +44,6 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly LibcFunction libcFunction = Service.Get(); - - private IntPtr baseAddress = IntPtr.Zero; private ImmutableDictionary<(string PluginName, uint CommandId), Action>? dalamudLinkHandlersCopy; [ServiceManager.ServiceConstructor] @@ -48,7 +52,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui this.address = new ChatGuiAddressResolver(); this.address.Setup(sigScanner); - this.printMessageHook = Hook.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour); + this.printMessageHook = Hook.FromAddress((nint)RaptureLogModule.Addresses.PrintMessage.Value, this.HandlePrintMessageDetour); this.populateItemLinkHook = Hook.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); this.interactableLinkClickedHook = Hook.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); @@ -58,7 +62,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr PrintMessageDelegate(IntPtr manager, XivChatType chatType, IntPtr senderName, IntPtr message, uint senderId, IntPtr parameter); + private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, bool silent); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr); @@ -150,18 +154,13 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui { var chat = this.chatQueue.Dequeue(); - if (this.baseAddress == IntPtr.Zero) - { - continue; - } + var sender = Utf8String.FromSequence(chat.Name.Encode()); + var message = Utf8String.FromSequence(chat.Message.Encode()); - var senderRaw = (chat.Name ?? string.Empty).Encode(); - using var senderOwned = this.libcFunction.NewString(senderRaw); + this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, chat.Parameters != 0); - var messageRaw = (chat.Message ?? string.Empty).Encode(); - using var messageOwned = this.libcFunction.NewString(messageRaw); - - this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters); + sender->Dtor(true); + message->Dtor(true); } } @@ -279,29 +278,17 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui } } - private IntPtr HandlePrintMessageDetour(IntPtr manager, XivChatType chatType, IntPtr pSenderName, IntPtr pMessage, uint senderId, IntPtr parameter) + private uint HandlePrintMessageDetour(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, bool silent) { - var retVal = IntPtr.Zero; + var messageId = 0u; try { - var sender = StdString.ReadFromPointer(pSenderName); - var parsedSender = SeString.Parse(sender.RawData); - var originalSenderData = (byte[])sender.RawData.Clone(); - var oldEditedSender = parsedSender.Encode(); - var senderPtr = pSenderName; - OwnedStdString allocatedString = null; + var originalSenderData = sender->Span.ToArray(); + var originalMessageData = message->Span.ToArray(); - var message = StdString.ReadFromPointer(pMessage); - var parsedMessage = SeString.Parse(message.RawData); - var originalMessageData = (byte[])message.RawData.Clone(); - var oldEdited = parsedMessage.Encode(); - var messagePtr = pMessage; - OwnedStdString allocatedStringSender = null; - - // Log.Verbose("[CHATGUI][{0}][{1}]", parsedSender.TextValue, parsedMessage.TextValue); - - // Log.Debug($"HandlePrintMessageDetour {manager} - [{chattype}] [{BitConverter.ToString(message.RawData).Replace("-", " ")}] {message.Value} from {senderName.Value}"); + var parsedSender = SeString.Parse(originalSenderData); + var parsedMessage = SeString.Parse(originalMessageData); // Call events var isHandled = false; @@ -312,7 +299,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui try { var messageHandledDelegate = @delegate as IChatGui.OnCheckMessageHandledDelegate; - messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled); + messageHandledDelegate!.Invoke(chatType, (uint)timestamp, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) { @@ -328,7 +315,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui try { var messageHandledDelegate = @delegate as IChatGui.OnMessageDelegate; - messageHandledDelegate!.Invoke(chatType, senderId, ref parsedSender, ref parsedMessage, ref isHandled); + messageHandledDelegate!.Invoke(chatType, (uint)timestamp, ref parsedSender, ref parsedMessage, ref isHandled); } catch (Exception e) { @@ -337,61 +324,39 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui } } - var newEdited = parsedMessage.Encode(); - if (!Util.FastByteArrayCompare(oldEdited, newEdited)) + var possiblyModifiedSenderData = parsedSender.Encode(); + var possiblyModifiedMessageData = parsedMessage.Encode(); + + if (!Util.FastByteArrayCompare(originalSenderData, possiblyModifiedSenderData)) { - Log.Verbose("SeString was edited, taking precedence over StdString edit."); - message.RawData = newEdited; - // Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}"); + Log.Verbose($"HandlePrintMessageDetour Sender modified: {SeString.Parse(originalSenderData)} -> {parsedSender}"); + sender->SetString(possiblyModifiedSenderData); } - if (!Util.FastByteArrayCompare(originalMessageData, message.RawData)) + if (!Util.FastByteArrayCompare(originalMessageData, possiblyModifiedMessageData)) { - allocatedString = this.libcFunction.NewString(message.RawData); - Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})"); - messagePtr = allocatedString.Address; - } - - var newEditedSender = parsedSender.Encode(); - if (!Util.FastByteArrayCompare(oldEditedSender, newEditedSender)) - { - Log.Verbose("SeString was edited, taking precedence over StdString edit."); - sender.RawData = newEditedSender; - // Log.Debug($"\nOLD: {BitConverter.ToString(originalMessageData)}\nNEW: {BitConverter.ToString(newEdited)}"); - } - - if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData)) - { - allocatedStringSender = this.libcFunction.NewString(sender.RawData); - Log.Debug( - $"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})"); - senderPtr = allocatedStringSender.Address; + Log.Verbose($"HandlePrintMessageDetour Message modified: {SeString.Parse(originalMessageData)} -> {parsedMessage}"); + message->SetString(possiblyModifiedMessageData); } // Print the original chat if it's handled. if (isHandled) { - this.ChatMessageHandled?.Invoke(chatType, senderId, parsedSender, parsedMessage); + this.ChatMessageHandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage); } else { - retVal = this.printMessageHook.Original(manager, chatType, senderPtr, messagePtr, senderId, parameter); - this.ChatMessageUnhandled?.Invoke(chatType, senderId, parsedSender, parsedMessage); + messageId = this.printMessageHook.Original(manager, chatType, sender, message, timestamp, silent); + this.ChatMessageUnhandled?.Invoke(chatType, (uint)timestamp, parsedSender, parsedMessage); } - - if (this.baseAddress == IntPtr.Zero) - this.baseAddress = manager; - - allocatedString?.Dispose(); - allocatedStringSender?.Dispose(); } catch (Exception ex) { Log.Error(ex, "Exception on OnChatMessage hook."); - retVal = this.printMessageHook.Original(manager, chatType, pSenderName, pMessage, senderId, parameter); + messageId = this.printMessageHook.Original(manager, chatType, sender, message, timestamp, silent); } - return retVal; + return messageId; } private void InteractableLinkClickedDetour(IntPtr managerPtr, IntPtr messagePtr) @@ -409,11 +374,7 @@ internal sealed class ChatGui : IDisposable, IServiceType, IChatGui Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10); - var messageSize = 0; - while (Marshal.ReadByte(payloadPtr, messageSize) != 0) messageSize++; - var payloadBytes = new byte[messageSize]; - Marshal.Copy(payloadPtr, payloadBytes, 0, messageSize); - var seStr = SeString.Parse(payloadBytes); + var seStr = MemoryHelper.ReadSeStringNullTerminated(messagePtr); var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator); var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads; if (payloads.Count == 0) return; diff --git a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs index d653ec146..ae53f90e9 100644 --- a/Dalamud/Game/Gui/ChatGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/ChatGuiAddressResolver.cs @@ -5,11 +5,6 @@ namespace Dalamud.Game.Gui; /// internal sealed class ChatGuiAddressResolver : BaseAddressResolver { - /// - /// Gets the address of the native PrintMessage method. - /// - public IntPtr PrintMessage { get; private set; } - /// /// Gets the address of the native PopulateItemLinkObject method. /// @@ -20,77 +15,9 @@ internal sealed class ChatGuiAddressResolver : BaseAddressResolver /// public IntPtr InteractableLinkClicked { get; private set; } - /* - --- for reference: 4.57 --- - .text:00000001405CD210 ; __int64 __fastcall Xiv::Gui::ChatGui::PrintMessage(__int64 handler, unsigned __int16 chatType, __int64 senderName, __int64 message, int senderActorId, char isLocal) - .text:00000001405CD210 Xiv__Gui__ChatGui__PrintMessage proc near - .text:00000001405CD210 ; CODE XREF: sub_1401419F0+201↑p - .text:00000001405CD210 ; sub_140141D10+220↑p ... - .text:00000001405CD210 - .text:00000001405CD210 var_220 = qword ptr -220h - .text:00000001405CD210 var_218 = byte ptr -218h - .text:00000001405CD210 var_210 = word ptr -210h - .text:00000001405CD210 var_208 = byte ptr -208h - .text:00000001405CD210 var_200 = word ptr -200h - .text:00000001405CD210 var_1FC = dword ptr -1FCh - .text:00000001405CD210 var_1F8 = qword ptr -1F8h - .text:00000001405CD210 var_1F0 = qword ptr -1F0h - .text:00000001405CD210 var_1E8 = qword ptr -1E8h - .text:00000001405CD210 var_1E0 = dword ptr -1E0h - .text:00000001405CD210 var_1DC = word ptr -1DCh - .text:00000001405CD210 var_1DA = word ptr -1DAh - .text:00000001405CD210 var_1D8 = qword ptr -1D8h - .text:00000001405CD210 var_1D0 = byte ptr -1D0h - .text:00000001405CD210 var_1C8 = qword ptr -1C8h - .text:00000001405CD210 var_1B0 = dword ptr -1B0h - .text:00000001405CD210 var_1AC = dword ptr -1ACh - .text:00000001405CD210 var_1A8 = dword ptr -1A8h - .text:00000001405CD210 var_1A4 = dword ptr -1A4h - .text:00000001405CD210 var_1A0 = dword ptr -1A0h - .text:00000001405CD210 var_160 = dword ptr -160h - .text:00000001405CD210 var_15C = dword ptr -15Ch - .text:00000001405CD210 var_140 = dword ptr -140h - .text:00000001405CD210 var_138 = dword ptr -138h - .text:00000001405CD210 var_130 = byte ptr -130h - .text:00000001405CD210 var_C0 = byte ptr -0C0h - .text:00000001405CD210 var_50 = qword ptr -50h - .text:00000001405CD210 var_38 = qword ptr -38h - .text:00000001405CD210 var_30 = qword ptr -30h - .text:00000001405CD210 var_28 = qword ptr -28h - .text:00000001405CD210 var_20 = qword ptr -20h - .text:00000001405CD210 senderActorId = dword ptr 30h - .text:00000001405CD210 isLocal = byte ptr 38h - .text:00000001405CD210 - .text:00000001405CD210 ; __unwind { // __GSHandlerCheck - .text:00000001405CD210 push rbp - .text:00000001405CD212 push rdi - .text:00000001405CD213 push r14 - .text:00000001405CD215 push r15 - .text:00000001405CD217 lea rbp, [rsp-128h] - .text:00000001405CD21F sub rsp, 228h - .text:00000001405CD226 mov rax, cs:__security_cookie - .text:00000001405CD22D xor rax, rsp - .text:00000001405CD230 mov [rbp+140h+var_50], rax - .text:00000001405CD237 xor r10b, r10b - .text:00000001405CD23A mov [rsp+240h+var_1F8], rcx - .text:00000001405CD23F xor eax, eax - .text:00000001405CD241 mov r11, r9 - .text:00000001405CD244 mov r14, r8 - .text:00000001405CD247 mov r9d, eax - .text:00000001405CD24A movzx r15d, dx - .text:00000001405CD24E lea r8, [rcx+0C10h] - .text:00000001405CD255 mov rdi, rcx - */ - /// protected override void Setup64Bit(ISigScanner sig) { - // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24D8FEFFFF 4881EC28020000 488B05???????? 4833C4 488985F0000000 4532D2 48894C2448"); LAST PART FOR 5.1??? - this.PrintMessage = sig.ScanText("40 55 53 56 41 54 41 57 48 8D AC 24 ?? ?? ?? ?? 48 81 EC 20 02 00 00 48 8B 05"); - // PrintMessage = sig.ScanText("4055 57 41 ?? 41 ?? 488DAC24E8FEFFFF 4881EC18020000 488B05???????? 4833C4 488985E0000000 4532D2 48894C2438"); old - - // PrintMessage = sig.ScanText("40 55 57 41 56 41 57 48 8D AC 24 D8 FE FF FF 48 81 EC 28 02 00 00 48 8B 05 63 47 4A 01 48 33 C4 48 89 85 F0 00 00 00 45 32 D2 48 89 4C 24 48 33"); - // PopulateItemLinkObject = sig.ScanText("48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 FA F2 B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); // PopulateItemLinkObject = sig.ScanText( "48 89 5C 24 08 57 48 83 EC 20 80 7A 06 00 48 8B DA 48 8B F9 74 14 48 8B CA E8 32 03 00 00 48 8B C8 E8 ?? ?? B0 FF 8B C8 EB 1D 0F B6 42 14 8B 4A"); 5.0 From 496bed4c69118c945940d41b6fb19cf2ec14e637 Mon Sep 17 00:00:00 2001 From: grittyfrog <148605153+grittyfrog@users.noreply.github.com> Date: Fri, 8 Dec 2023 10:30:11 +1100 Subject: [PATCH 357/585] Fix multi-line copy/paste between ImGui and XIV (#1525) --- .../Internal/ImGuiClipboardConfig.cs | 80 +++++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 2 + 2 files changed, 82 insertions(+) create mode 100644 Dalamud/Interface/Internal/ImGuiClipboardConfig.cs diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs new file mode 100644 index 000000000..b3302add4 --- /dev/null +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -0,0 +1,80 @@ +using System.Runtime.InteropServices; +using ImGuiNET; + +namespace Dalamud.Interface.Internal; + +/// +/// Configures the ImGui clipboard behaviour to work nicely with XIV. +/// +/// +/// +/// XIV uses '\r' for line endings and will truncate all text after a '\n' character. +/// This means that copy/pasting multi-line text from ImGui to XIV will only copy the first line. +/// +/// +/// ImGui uses '\n' for line endings and will ignore '\r' entirely. +/// This means that copy/pasting multi-line text from XIV to ImGui will copy all the text +/// without line breaks. +/// +/// +/// To fix this we normalize all clipboard line endings entering/exiting ImGui to '\r\n' which +/// works for both ImGui and XIV. +/// +/// +internal static class ImGuiClipboardConfig +{ + private delegate void SetClipboardTextDelegate(IntPtr userData, string text); + private delegate string GetClipboardTextDelegate(); + + private static SetClipboardTextDelegate? _setTextOriginal = null; + private static GetClipboardTextDelegate? _getTextOriginal = null; + + // These must exist as variables to prevent them from being GC'd + private static SetClipboardTextDelegate? _setText = null; + private static GetClipboardTextDelegate? _getText = null; + + public static void Apply() + { + var io = ImGui.GetIO(); + if (_setTextOriginal == null) + { + _setTextOriginal = + Marshal.GetDelegateForFunctionPointer(io.SetClipboardTextFn); + } + + if (_getTextOriginal == null) + { + _getTextOriginal = + Marshal.GetDelegateForFunctionPointer(io.GetClipboardTextFn); + } + + _setText = new SetClipboardTextDelegate(SetClipboardText); + _getText = new GetClipboardTextDelegate(GetClipboardText); + + io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setText); + io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getText); + } + + public static void Unapply() + { + var io = ImGui.GetIO(); + if (_setTextOriginal != null) + { + io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setTextOriginal); + } + if (_getTextOriginal != null) + { + io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getTextOriginal); + } + } + + private static void SetClipboardText(IntPtr userData, string text) + { + _setTextOriginal!(userData, text.ReplaceLineEndings("\r\n")); + } + + private static string GetClipboardText() + { + return _getTextOriginal!().ReplaceLineEndings("\r\n"); + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 1b12fd853..7d164c01f 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -240,6 +240,7 @@ internal class InterfaceManager : IDisposable, IServiceType this.processMessageHook?.Dispose(); }).Wait(); + ImGuiClipboardConfig.Unapply(); this.scene?.Dispose(); } @@ -628,6 +629,7 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; this.SetupFonts(); + ImGuiClipboardConfig.Apply(); if (!configuration.IsDocking) { From e6f5801ab5793048db2edf9bacb8e144d8df0be4 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 8 Dec 2023 00:35:45 +0100 Subject: [PATCH 358/585] [master] Update ClientStructs (#1556) 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 cc6687524..dcc913975 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit cc668752416a8459a3c23345c51277e359803de8 +Subproject commit dcc9139758bf5e2ff5c0b53d73a3566eb0eec4f0 From 0bfcc557749df00f92b824c3d062ee10cfbf5a13 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 8 Dec 2023 08:49:09 +0900 Subject: [PATCH 359/585] Reduce heap allocation every frame in AddonLifecycle (#1555) --- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 35 +++- .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 8 +- .../AddonArgTypes/AddonFinalizeArgs.cs | 8 +- .../AddonArgTypes/AddonReceiveEventArgs.cs | 24 ++- .../AddonArgTypes/AddonRefreshArgs.cs | 16 +- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 16 +- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 16 +- .../AddonArgTypes/AddonUpdateArgs.cs | 10 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 188 ++++++------------ .../AddonLifecycleReceiveEventListener.cs | 47 ++--- Dalamud/Memory/MemoryHelper.cs | 32 +++ 11 files changed, 212 insertions(+), 188 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 334542c71..077ca7c93 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -14,22 +14,51 @@ public abstract unsafe class AddonArgs public const string InvalidAddon = "NullAddon"; private string? addonName; + private IntPtr addon; /// /// Gets the name of the addon this args referrers to. /// public string AddonName => this.GetAddonName(); - + /// /// Gets the pointer to the addons AtkUnitBase. /// - public nint Addon { get; init; } - + public nint Addon + { + get => this.addon; + internal set + { + if (this.addon == value) + return; + + this.addon = value; + this.addonName = null; + } + } + /// /// Gets the type of these args. /// 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(ReadOnlySpan name) + { + if (this.Addon == nint.Zero) return false; + if (name.Length is 0 or > 0x20) + return false; + + var addonPointer = (AtkUnitBase*)this.Addon; + if (addonPointer->Name is null) return false; + + return MemoryHelper.EqualsZeroTerminatedString(name, (nint)addonPointer->Name, null, 0x20); + } + /// /// Helper method for ensuring the name of the addon is valid. /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 10d46a573..1e1013dd5 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -3,8 +3,14 @@ /// /// Addon argument data for Draw events. /// -public class AddonDrawArgs : AddonArgs +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 caf422927..fc26a6c33 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -3,8 +3,14 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonFinalizeArgs : AddonArgs +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/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index df75307f1..8f9003b4c 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -3,28 +3,34 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for ReceiveEvent events. /// -public class AddonReceiveEventArgs : AddonArgs +public class AddonReceiveEventArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.ReceiveEvent; /// - /// Gets the AtkEventType for this event message. + /// Gets or sets the AtkEventType for this event message. /// - public byte AtkEventType { get; init; } + public byte AtkEventType { get; set; } /// - /// Gets the event id for this event message. + /// Gets or sets the event id for this event message. /// - public int EventParam { get; init; } + public int EventParam { get; set; } /// - /// Gets the pointer to an AtkEvent for this event message. + /// Gets or sets the pointer to an AtkEvent for this event message. /// - public nint AtkEvent { get; init; } + public nint AtkEvent { get; set; } /// - /// Gets the pointer to a block of data for this event message. + /// Gets or sets the pointer to a block of data for this event message. /// - public nint Data { get; init; } + public nint Data { get; set; } + + /// + public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index b6ac6d8b6..bfcf02544 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -5,23 +5,29 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Refresh events. /// -public class AddonRefreshArgs : AddonArgs +public class AddonRefreshArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Refresh; /// - /// Gets the number of AtkValues. + /// Gets or sets the number of AtkValues. /// - public uint AtkValueCount { get; init; } + public uint AtkValueCount { get; set; } /// - /// Gets the address of the AtkValue array. + /// Gets or sets the address of the AtkValue array. /// - public nint AtkValues { get; init; } + public nint AtkValues { get; set; } /// /// Gets the AtkValues in the form of a span. /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index 1b743b31a..219288ccf 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -3,18 +3,24 @@ /// /// Addon argument data for OnRequestedUpdate events. /// -public class AddonRequestedUpdateArgs : AddonArgs +public class AddonRequestedUpdateArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.RequestedUpdate; /// - /// Gets the NumberArrayData** for this event. + /// Gets or sets the NumberArrayData** for this event. /// - public nint NumberArrayData { get; init; } + public nint NumberArrayData { get; set; } /// - /// Gets the StringArrayData** for this event. + /// Gets or sets the StringArrayData** for this event. /// - public nint StringArrayData { get; init; } + public nint StringArrayData { get; set; } + + /// + public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index df2ec26be..bd60879b8 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -5,23 +5,29 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// /// Addon argument data for Setup events. /// -public class AddonSetupArgs : AddonArgs +public class AddonSetupArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Setup; /// - /// Gets the number of AtkValues. + /// Gets or sets the number of AtkValues. /// - public uint AtkValueCount { get; init; } + public uint AtkValueCount { get; set; } /// - /// Gets the address of the AtkValue array. + /// Gets or sets the address of the AtkValue array. /// - public nint AtkValues { get; init; } + public nint AtkValues { get; set; } /// /// Gets the AtkValues in the form of a span. /// public unsafe Span AtkValueSpan => new(this.AtkValues.ToPointer(), (int)this.AtkValueCount); + + /// + public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index 651fbcafb..b087ac15a 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 +public class AddonUpdateArgs : AddonArgs, ICloneable { /// public override AddonArgsType Type => AddonArgsType.Update; @@ -11,5 +11,11 @@ public class AddonUpdateArgs : AddonArgs /// /// Gets the time since the last update. /// - public float TimeDelta { get; init; } + public float TimeDelta { get; internal set; } + + /// + public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); + + /// + object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 3528de562..decb7a9f4 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Hooking; @@ -40,6 +41,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly ConcurrentBag newEventListeners = new(); private readonly ConcurrentBag removeEventListeners = new(); + // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet + // package, and these events are always called from the main thread, this is fine. + private readonly AddonSetupArgs recyclingSetupArgs = new(); + private readonly AddonFinalizeArgs recyclingFinalizeArgs = new(); + private readonly AddonDrawArgs recyclingDrawArgs = new(); + private readonly AddonUpdateArgs recyclingUpdateArgs = new(); + private readonly AddonRefreshArgs recyclingRefreshArgs = new(); + private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new(); + [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) { @@ -132,12 +142,27 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType /// /// Event Type. /// AddonArgs. - internal void InvokeListeners(AddonEvent eventType, AddonArgs args) + /// What to blame on errors. + internal void InvokeListenersSafely(AddonEvent eventType, AddonArgs args, [CallerMemberName] string blame = "") { - // Match on string.empty for listeners that want events for all addons. - foreach (var listener in this.EventListeners.Where(listener => listener.EventType == eventType && (listener.AddonName == args.AddonName || listener.AddonName == string.Empty))) + // Do not use linq; this is a high-traffic function, and more heap allocations avoided, the better. + foreach (var listener in this.EventListeners) { - listener.FunctionDelegate.Invoke(eventType, args); + if (listener.EventType != eventType) + continue; + + // Match on string.empty for listeners that want events for all addons. + if (!string.IsNullOrWhiteSpace(listener.AddonName) && !args.IsAddon(listener.AddonName)) + continue; + + try + { + listener.FunctionDelegate.Invoke(eventType, args); + } + catch (Exception e) + { + Log.Error(e, $"Exception in {blame} during {eventType} invoke."); + } } } @@ -249,20 +274,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); } - - try - { - this.InvokeListeners(AddonEvent.PreSetup, new AddonSetupArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup pre-setup invoke."); - } + + this.recyclingSetupArgs.Addon = (nint)addon; + this.recyclingSetupArgs.AtkValueCount = valueCount; + this.recyclingSetupArgs.AtkValues = (nint)values; + this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs); + valueCount = this.recyclingSetupArgs.AtkValueCount; + values = (AtkValue*)this.recyclingSetupArgs.AtkValues; try { @@ -273,19 +291,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonSetup. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostSetup, new AddonSetupArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonSetup post-setup invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostSetup, this.recyclingSetupArgs); } private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) @@ -299,15 +305,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); } - - try - { - this.InvokeListeners(AddonEvent.PreFinalize, new AddonFinalizeArgs { Addon = (nint)atkUnitBase[0] }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonFinalize pre-finalize invoke."); - } + + this.recyclingFinalizeArgs.Addon = (nint)atkUnitBase[0]; + this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs); try { @@ -321,14 +321,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonDraw(AtkUnitBase* addon) { - try - { - this.InvokeListeners(AddonEvent.PreDraw, new AddonDrawArgs { Addon = (nint)addon }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonDraw pre-draw invoke."); - } + this.recyclingDrawArgs.Addon = (nint)addon; + this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs); try { @@ -339,26 +333,14 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonDraw. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostDraw, new AddonDrawArgs { Addon = (nint)addon }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonDraw post-draw invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostDraw, this.recyclingDrawArgs); } private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - try - { - this.InvokeListeners(AddonEvent.PreUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonUpdate pre-update invoke."); - } + this.recyclingUpdateArgs.Addon = (nint)addon; + this.recyclingUpdateArgs.TimeDelta = delta; + this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs); try { @@ -369,33 +351,19 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonUpdate. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostUpdate, new AddonUpdateArgs { Addon = (nint)addon, TimeDelta = delta }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonUpdate post-update invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostUpdate, this.recyclingUpdateArgs); } private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) { byte result = 0; - - try - { - this.InvokeListeners(AddonEvent.PreRefresh, new AddonRefreshArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh pre-refresh invoke."); - } + + this.recyclingRefreshArgs.Addon = (nint)addon; + this.recyclingRefreshArgs.AtkValueCount = valueCount; + this.recyclingRefreshArgs.AtkValues = (nint)values; + this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs); + valueCount = this.recyclingRefreshArgs.AtkValueCount; + values = (AtkValue*)this.recyclingRefreshArgs.AtkValues; try { @@ -406,38 +374,18 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonRefresh. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostRefresh, new AddonRefreshArgs - { - Addon = (nint)addon, - AtkValueCount = valueCount, - AtkValues = (nint)values, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh post-refresh invoke."); - } - + this.InvokeListenersSafely(AddonEvent.PostRefresh, this.recyclingRefreshArgs); return result; } private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - try - { - this.InvokeListeners(AddonEvent.PreRequestedUpdate, new AddonRequestedUpdateArgs - { - Addon = (nint)addon, - NumberArrayData = (nint)numberArrayData, - StringArrayData = (nint)stringArrayData, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnRequestedUpdate pre-requestedUpdate invoke."); - } + this.recyclingRequestedUpdateArgs.Addon = (nint)addon; + this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData; + this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData; + this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs); + numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData; + stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.StringArrayData; try { @@ -448,19 +396,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Caught exception when calling original AddonRequestedUpdate. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.InvokeListeners(AddonEvent.PostRequestedUpdate, new AddonRequestedUpdateArgs - { - Addon = (nint)addon, - NumberArrayData = (nint)numberArrayData, - StringArrayData = (nint)stringArrayData, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnRequestedUpdate post-requestedUpdate invoke."); - } + this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, this.recyclingRequestedUpdateArgs); } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs index 10171eb16..1c138e447 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs @@ -16,6 +16,10 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable { private static readonly ModuleLog Log = new("AddonLifecycle"); + // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet + // package, and these events are always called from the main thread, this is fine. + private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new(); + /// /// Initializes a new instance of the class. /// @@ -74,22 +78,17 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable this.Hook!.Original(addon, eventType, eventParam, atkEvent, data); return; } - - try - { - this.AddonLifecycle.InvokeListeners(AddonEvent.PreReceiveEvent, new AddonReceiveEventArgs - { - Addon = (nint)addon, - AtkEventType = (byte)eventType, - EventParam = eventParam, - AtkEvent = (nint)atkEvent, - Data = data, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnReceiveEvent pre-receiveEvent invoke."); - } + + this.recyclingReceiveEventArgs.Addon = (nint)addon; + this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType; + this.recyclingReceiveEventArgs.EventParam = eventParam; + this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent; + this.recyclingReceiveEventArgs.Data = data; + this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs); + eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType; + eventParam = this.recyclingReceiveEventArgs.EventParam; + atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent; + data = this.recyclingReceiveEventArgs.Data; try { @@ -100,20 +99,6 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable Log.Error(e, "Caught exception when calling original AddonReceiveEvent. This may be a bug in the game or another plugin hooking this method."); } - try - { - this.AddonLifecycle.InvokeListeners(AddonEvent.PostReceiveEvent, new AddonReceiveEventArgs - { - Addon = (nint)addon, - AtkEventType = (byte)eventType, - EventParam = eventParam, - AtkEvent = (nint)atkEvent, - Data = data, - }); - } - catch (Exception e) - { - Log.Error(e, "Exception in OnAddonRefresh post-receiveEvent invoke."); - } + this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.recyclingReceiveEventArgs); } } diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs index 3ceecf6a6..552817646 100644 --- a/Dalamud/Memory/MemoryHelper.cs +++ b/Dalamud/Memory/MemoryHelper.cs @@ -163,6 +163,38 @@ public static unsafe class MemoryHelper #region ReadString + /// + /// Compares if the given char span equals to the null-terminated string at . + /// + /// The character span. + /// The address of null-terminated string. + /// The encoding of the null-terminated string. + /// The maximum length of the null-terminated string. + /// Whether they are equal. + public static bool EqualsZeroTerminatedString( + ReadOnlySpan charSpan, + nint memoryAddress, + Encoding? encoding = null, + int maxLength = int.MaxValue) + { + encoding ??= Encoding.UTF8; + maxLength = Math.Min(maxLength, charSpan.Length + 4); + + var pmem = ((byte*)memoryAddress)!; + var length = 0; + while (length < maxLength && pmem[length] != 0) + length++; + + var mem = new Span(pmem, length); + var memCharCount = encoding.GetCharCount(mem); + if (memCharCount != charSpan.Length) + return false; + + Span chars = stackalloc char[memCharCount]; + encoding.GetChars(mem, chars); + return charSpan.SequenceEqual(chars); + } + /// /// Read a UTF-8 encoded string from a specified memory address. /// From b89df8b130b03c37714922fddd81933b59ab9d12 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:20:05 +0900 Subject: [PATCH 360/585] Prevent end comment aligning (Resharper/SA conflict) --- .editorconfig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 66e123f53..141e8c9c9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -104,13 +104,14 @@ resharper_can_use_global_alias = false resharper_csharp_align_multiline_parameter = true resharper_csharp_align_multiple_declaration = true resharper_csharp_empty_block_style = multiline -resharper_csharp_int_align_comments = true +resharper_csharp_int_align_comments = false resharper_csharp_new_line_before_while = true resharper_csharp_wrap_after_declaration_lpar = true resharper_csharp_wrap_after_invocation_lpar = true resharper_csharp_wrap_arguments_style = chop_if_long resharper_enforce_line_ending_style = true resharper_instance_members_qualify_declared_in = this_class, base_class +resharper_int_align = false resharper_member_can_be_private_global_highlighting = none resharper_member_can_be_private_local_highlighting = none resharper_new_line_before_finally = true From 0c3ebd4b5b969e2645f23c22d8547276b5528cf4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:20:25 +0900 Subject: [PATCH 361/585] Fix missing length modification in ImVectorWrapper.Insert --- Dalamud/Interface/Utility/ImVectorWrapper.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs index 67b002179..d41ee0094 100644 --- a/Dalamud/Interface/Utility/ImVectorWrapper.cs +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -519,10 +519,11 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi if (index < 0 || index > this.LengthUnsafe) throw new IndexOutOfRangeException(); - this.EnsureCapacityExponential(this.CapacityUnsafe + 1); + this.EnsureCapacityExponential(this.LengthUnsafe + 1); var num = this.LengthUnsafe - index; Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + 1, num * sizeof(T), num * sizeof(T)); this.DataUnsafe[index] = item; + this.LengthUnsafe += 1; } /// @@ -535,6 +536,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + count, num * sizeof(T), num * sizeof(T)); foreach (var item in items) this.DataUnsafe[index++] = item; + this.LengthUnsafe += count; } else { @@ -551,6 +553,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + items.Length, num * sizeof(T), num * sizeof(T)); foreach (var item in items) this.DataUnsafe[index++] = item; + this.LengthUnsafe += items.Length; } /// @@ -566,6 +569,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi this.destroyer?.Invoke(&this.DataUnsafe[index]); Buffer.MemoryCopy(this.DataUnsafe + index + 1, this.DataUnsafe + index, num * sizeof(T), num * sizeof(T)); + this.LengthUnsafe -= 1; } /// From 8967174cd968d6993401fa706bc7682657127ed5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:28:12 +0900 Subject: [PATCH 362/585] Reimplement clipboard text normalizer to use the correct buffers --- .../Internal/ImGuiClipboardConfig.cs | 232 +++++++++++++++--- .../Interface/Internal/InterfaceManager.cs | 2 - 2 files changed, 193 insertions(+), 41 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs index b3302add4..5dc04d736 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -1,4 +1,9 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + using ImGuiNET; namespace Dalamud.Interface.Internal; @@ -21,60 +26,209 @@ namespace Dalamud.Interface.Internal; /// works for both ImGui and XIV. /// /// -internal static class ImGuiClipboardConfig +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable { - private delegate void SetClipboardTextDelegate(IntPtr userData, string text); - private delegate string GetClipboardTextDelegate(); + private readonly nint clipboardUserDataOriginal; + private readonly delegate* unmanaged setTextOriginal; + private readonly delegate* unmanaged getTextOriginal; - private static SetClipboardTextDelegate? _setTextOriginal = null; - private static GetClipboardTextDelegate? _getTextOriginal = null; + [ServiceManager.ServiceConstructor] + private ImGuiClipboardConfig(InterfaceManager.InterfaceManagerWithScene imws) + { + // Effectively waiting for ImGui to become available. + _ = imws; + Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); - // These must exist as variables to prevent them from being GC'd - private static SetClipboardTextDelegate? _setText = null; - private static GetClipboardTextDelegate? _getText = null; + var io = ImGui.GetIO(); + this.setTextOriginal = (delegate* unmanaged)io.SetClipboardTextFn; + this.getTextOriginal = (delegate* unmanaged)io.GetClipboardTextFn; + this.clipboardUserDataOriginal = io.ClipboardUserData; + io.SetClipboardTextFn = (nint)(delegate* unmanaged)(&StaticSetClipboardTextImpl); + io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; + io.ClipboardUserData = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + return; - public static void Apply() + [UnmanagedCallersOnly] + static void StaticSetClipboardTextImpl(nint userData, byte* text) => + ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text); + + [UnmanagedCallersOnly] + static byte* StaticGetClipboardTextImpl(nint userData) => + ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); + } + + /// + /// Finalizes an instance of the class. + /// + ~ImGuiClipboardConfig() => this.ReleaseUnmanagedResources(); + + [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")] + private static ImVectorWrapper ImGuiCurrentContextClipboardHandlerData => + new((ImVector*)(ImGui.GetCurrentContext() + 0x5520)); + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + private void ReleaseUnmanagedResources() { var io = ImGui.GetIO(); - if (_setTextOriginal == null) - { - _setTextOriginal = - Marshal.GetDelegateForFunctionPointer(io.SetClipboardTextFn); - } + if (io.ClipboardUserData == default) + return; - if (_getTextOriginal == null) - { - _getTextOriginal = - Marshal.GetDelegateForFunctionPointer(io.GetClipboardTextFn); - } - - _setText = new SetClipboardTextDelegate(SetClipboardText); - _getText = new GetClipboardTextDelegate(GetClipboardText); - - io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setText); - io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getText); + GCHandle.FromIntPtr(io.ClipboardUserData).Free(); + io.SetClipboardTextFn = (nint)this.setTextOriginal; + io.GetClipboardTextFn = (nint)this.getTextOriginal; + io.ClipboardUserData = this.clipboardUserDataOriginal; } - public static void Unapply() + private void SetClipboardTextImpl(byte* text) { - var io = ImGui.GetIO(); - if (_setTextOriginal != null) - { - io.SetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_setTextOriginal); - } - if (_getTextOriginal != null) - { - io.GetClipboardTextFn = Marshal.GetFunctionPointerForDelegate(_getTextOriginal); - } + var buffer = ImGuiCurrentContextClipboardHandlerData; + Utf8Utils.SetFromNullTerminatedBytes(ref buffer, text); + Utf8Utils.Normalize(ref buffer); + Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data); } - private static void SetClipboardText(IntPtr userData, string text) + private byte* GetClipboardTextImpl() { - _setTextOriginal!(userData, text.ReplaceLineEndings("\r\n")); + _ = this.getTextOriginal(this.clipboardUserDataOriginal); + + var buffer = ImGuiCurrentContextClipboardHandlerData; + Utf8Utils.TrimNullTerminator(ref buffer); + Utf8Utils.Normalize(ref buffer); + Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + return buffer.Data; } - private static string GetClipboardText() + private static class Utf8Utils { - return _getTextOriginal!().ReplaceLineEndings("\r\n"); + /// + /// Sets from , a null terminated UTF-8 string. + /// + /// The target buffer. It will not contain a null terminator. + /// The pointer to the null-terminated UTF-8 string. + public static void SetFromNullTerminatedBytes(ref ImVectorWrapper buf, byte* psz) + { + var len = 0; + while (psz[len] != 0) + len++; + + buf.Clear(); + buf.AddRange(new Span(psz, len)); + } + + /// + /// Removes the null terminator. + /// + /// The UTF-8 string buffer. + public static void TrimNullTerminator(ref ImVectorWrapper buf) + { + while (buf.Length > 0 && buf[^1] == 0) + buf.LengthUnsafe--; + } + + /// + /// Adds a null terminator to the buffer. + /// + /// The buffer. + public static void AddNullTerminatorIfMissing(ref ImVectorWrapper buf) + { + if (buf.Length > 0 && buf[^1] == 0) + return; + buf.Add(0); + } + + /// + /// Counts the number of bytes for the UTF-8 character. + /// + /// The bytes. + /// Available number of bytes. + /// Number of bytes taken, or -1 if the byte was invalid. + public static int CountBytes(byte* b, int avail) + { + if (avail <= 0) + return 0; + if ((b[0] & 0x80) == 0) + return 1; + if ((b[0] & 0xE0) == 0xC0 && avail >= 2) + return (b[1] & 0xC0) == 0x80 ? 2 : -1; + if ((b[0] & 0xF0) == 0xE0 && avail >= 3) + return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 ? 3 : -1; + if ((b[0] & 0xF8) == 0xF0 && avail >= 4) + return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80 ? 4 : -1; + return -1; + } + + /// + /// Gets the codepoint. + /// + /// The bytes. + /// The result from . + /// The codepoint, or \xFFFD replacement character if failed. + public static int GetCodepoint(byte* b, int cb) => cb switch + { + 1 => b[0], + 2 => ((b[0] & 0x8F) << 6) | (b[1] & 0x3F), + 3 => ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F), + 4 => ((b[0] & 0x0F) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F), + _ => 0xFFFD, + }; + + /// + /// Normalize the given text for our use case. + /// + /// The buffer. + public static void Normalize(ref ImVectorWrapper buf) + { + for (var i = 0; i < buf.Length;) + { + // Already correct? + if (buf[i] is 0x0D && buf[i + 1] is 0x0A) + { + i += 2; + continue; + } + + var cb = CountBytes(buf.Data + i, buf.Length - i); + var currInt = GetCodepoint(buf.Data + i, cb); + switch (currInt) + { + case 0xFFFF: // Simply invalid + case > char.MaxValue: // ImWchar is same size with char; does not support + case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support + // Replace with \uFFFD in UTF-8: EF BF BD + buf[i++] = 0xEF; + buf.Insert(i++, 0xBF); + buf.Insert(i++, 0xBD); + break; + + // See String.Manipulation.cs: IndexOfNewlineChar. + case '\r': // CR; Carriage Return + case '\n': // LF; Line Feed + case '\f': // FF; Form Feed + buf[i++] = 0x0D; + buf.Insert(i++, 0x0A); + break; + + case '\u0085': // NEL; Next Line + case '\u2028': // LS; Line Separator + case '\u2029': // PS; Paragraph Separator + buf[i++] = 0x0D; + buf[i++] = 0x0A; + break; + + default: + // Not a newline char. + i += cb; + break; + } + } + } } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 7d164c01f..1b12fd853 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -240,7 +240,6 @@ internal class InterfaceManager : IDisposable, IServiceType this.processMessageHook?.Dispose(); }).Wait(); - ImGuiClipboardConfig.Unapply(); this.scene?.Dispose(); } @@ -629,7 +628,6 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; this.SetupFonts(); - ImGuiClipboardConfig.Apply(); if (!configuration.IsDocking) { From 2521658a984f2b605c320952745ac9eae27a8b94 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:33:35 +0900 Subject: [PATCH 363/585] Better cleanup logic --- Dalamud/Interface/Internal/ImGuiClipboardConfig.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs index 5dc04d736..db47d9734 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -32,6 +32,7 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable private readonly nint clipboardUserDataOriginal; private readonly delegate* unmanaged setTextOriginal; private readonly delegate* unmanaged getTextOriginal; + private GCHandle clipboardUserData; [ServiceManager.ServiceConstructor] private ImGuiClipboardConfig(InterfaceManager.InterfaceManagerWithScene imws) @@ -46,7 +47,7 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable this.clipboardUserDataOriginal = io.ClipboardUserData; io.SetClipboardTextFn = (nint)(delegate* unmanaged)(&StaticSetClipboardTextImpl); io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; - io.ClipboardUserData = GCHandle.ToIntPtr(GCHandle.Alloc(this)); + io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this)); return; [UnmanagedCallersOnly] @@ -76,14 +77,15 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable private void ReleaseUnmanagedResources() { - var io = ImGui.GetIO(); - if (io.ClipboardUserData == default) + if (!this.clipboardUserData.IsAllocated) return; - GCHandle.FromIntPtr(io.ClipboardUserData).Free(); + var io = ImGui.GetIO(); io.SetClipboardTextFn = (nint)this.setTextOriginal; io.GetClipboardTextFn = (nint)this.getTextOriginal; io.ClipboardUserData = this.clipboardUserDataOriginal; + + this.clipboardUserData.Free(); } private void SetClipboardTextImpl(byte* text) From 683464ed2da8a50b2b1c296c7612efd8c5c640da Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:37:41 +0900 Subject: [PATCH 364/585] Fix normalization buffer offsetting --- .../Internal/ImGuiClipboardConfig.cs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs index db47d9734..286f58a81 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -206,23 +206,40 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support // Replace with \uFFFD in UTF-8: EF BF BD buf[i++] = 0xEF; - buf.Insert(i++, 0xBF); - buf.Insert(i++, 0xBD); + + if (cb >= 2) + buf[i++] = 0xBF; + else + buf.Insert(i++, 0xBF); + + if (cb >= 3) + buf[i++] = 0xBD; + else + buf.Insert(i++, 0xBD); + + if (cb >= 4) + buf.RemoveAt(i); break; // See String.Manipulation.cs: IndexOfNewlineChar. case '\r': // CR; Carriage Return case '\n': // LF; Line Feed case '\f': // FF; Form Feed - buf[i++] = 0x0D; - buf.Insert(i++, 0x0A); - break; - case '\u0085': // NEL; Next Line case '\u2028': // LS; Line Separator case '\u2029': // PS; Paragraph Separator buf[i++] = 0x0D; - buf[i++] = 0x0A; + + if (cb >= 2) + buf[i++] = 0x0A; + else + buf.Insert(i++, 0x0A); + + if (cb >= 3) + buf.RemoveAt(i); + + if (cb >= 4) + buf.RemoveAt(i); break; default: From a0b7f53b0103a393f79b12f9038f2356f34ef6ab Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 23:45:07 +0900 Subject: [PATCH 365/585] Remove finalizer; cannot be finalized due to the use of GCHandle --- Dalamud/Interface/Internal/ImGuiClipboardConfig.cs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs index 286f58a81..ab8730682 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs @@ -59,23 +59,12 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); } - /// - /// Finalizes an instance of the class. - /// - ~ImGuiClipboardConfig() => this.ReleaseUnmanagedResources(); - [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")] private static ImVectorWrapper ImGuiCurrentContextClipboardHandlerData => new((ImVector*)(ImGui.GetCurrentContext() + 0x5520)); /// public void Dispose() - { - this.ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - - private void ReleaseUnmanagedResources() { if (!this.clipboardUserData.IsAllocated) return; From ddee969d883f71f2fc5eec767a9ae1eaac6bff78 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 00:00:28 +0900 Subject: [PATCH 366/585] Rename service --- ...ipboardConfig.cs => ImGuiClipboardFunctionProvider.cs} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename Dalamud/Interface/Internal/{ImGuiClipboardConfig.cs => ImGuiClipboardFunctionProvider.cs} (95%) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs similarity index 95% rename from Dalamud/Interface/Internal/ImGuiClipboardConfig.cs rename to Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index ab8730682..a99064a9d 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardConfig.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -27,7 +27,7 @@ namespace Dalamud.Interface.Internal; /// /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable +internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable { private readonly nint clipboardUserDataOriginal; private readonly delegate* unmanaged setTextOriginal; @@ -35,7 +35,7 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable private GCHandle clipboardUserData; [ServiceManager.ServiceConstructor] - private ImGuiClipboardConfig(InterfaceManager.InterfaceManagerWithScene imws) + private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScene imws) { // Effectively waiting for ImGui to become available. _ = imws; @@ -52,11 +52,11 @@ internal sealed unsafe class ImGuiClipboardConfig : IServiceType, IDisposable [UnmanagedCallersOnly] static void StaticSetClipboardTextImpl(nint userData, byte* text) => - ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text); + ((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.SetClipboardTextImpl(text); [UnmanagedCallersOnly] static byte* StaticGetClipboardTextImpl(nint userData) => - ((ImGuiClipboardConfig)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); + ((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); } [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")] From 3d51f88a335eb933206f79149e2661344610c65c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 00:03:10 +0900 Subject: [PATCH 367/585] fix --- Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index a99064a9d..20e49823a 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -165,9 +165,9 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis public static int GetCodepoint(byte* b, int cb) => cb switch { 1 => b[0], - 2 => ((b[0] & 0x8F) << 6) | (b[1] & 0x3F), + 2 => ((b[0] & 0x1F) << 6) | (b[1] & 0x3F), 3 => ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F), - 4 => ((b[0] & 0x0F) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F), + 4 => ((b[0] & 0x07) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F), _ => 0xFFFD, }; From ca321e59e402a5fb83191e0508ae19cafa167959 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 00:14:10 +0900 Subject: [PATCH 368/585] Make logic clearer --- .../ImGuiClipboardFunctionProvider.cs | 75 ++++++++++--------- 1 file changed, 41 insertions(+), 34 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 20e49823a..f14210b97 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -171,43 +171,61 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis _ => 0xFFFD, }; + /// + /// Replaces a sequence with another. + /// + /// The buffer. + /// Offset of the sequence to be replaced. + /// Length of the sequence to be replaced. + /// The replacement sequence. + /// The length of . + public static int ReplaceSequence( + ref ImVectorWrapper buf, + int offset, + int length, + ReadOnlySpan replacement) + { + var i = 0; + for (; i < replacement.Length; i++) + { + if (length >= i + 1) + buf[offset++] = replacement[i]; + else + buf.Insert(offset++, replacement[i]); + } + + for (; i < length; i++) + buf.RemoveAt(offset); + + return replacement.Length; + } + /// /// Normalize the given text for our use case. /// /// The buffer. public static void Normalize(ref ImVectorWrapper buf) { + // Ensure an implicit null after the end of the string. + buf.EnsureCapacity(buf.Length + 1); + buf.StorageSpan[buf.Length] = 0; + for (var i = 0; i < buf.Length;) { - // Already correct? - if (buf[i] is 0x0D && buf[i + 1] is 0x0A) - { - i += 2; - continue; - } - var cb = CountBytes(buf.Data + i, buf.Length - i); var currInt = GetCodepoint(buf.Data + i, cb); switch (currInt) { - case 0xFFFF: // Simply invalid + // Note that buf.Data[i + 1] is always defined. See the beginning of the function. + case '\r' when buf.Data[i + 1] == '\n': // Already CR LF? + i += 2; + continue; + + case 0xFFFE or 0xFFFF: // Simply invalid case > char.MaxValue: // ImWchar is same size with char; does not support case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support // Replace with \uFFFD in UTF-8: EF BF BD - buf[i++] = 0xEF; - - if (cb >= 2) - buf[i++] = 0xBF; - else - buf.Insert(i++, 0xBF); - - if (cb >= 3) - buf[i++] = 0xBD; - else - buf.Insert(i++, 0xBD); - - if (cb >= 4) - buf.RemoveAt(i); + i += ReplaceSequence(ref buf, i, cb, "\uFFFD"u8); break; // See String.Manipulation.cs: IndexOfNewlineChar. @@ -217,18 +235,7 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis case '\u0085': // NEL; Next Line case '\u2028': // LS; Line Separator case '\u2029': // PS; Paragraph Separator - buf[i++] = 0x0D; - - if (cb >= 2) - buf[i++] = 0x0A; - else - buf.Insert(i++, 0x0A); - - if (cb >= 3) - buf.RemoveAt(i); - - if (cb >= 4) - buf.RemoveAt(i); + i += ReplaceSequence(ref buf, i, cb, "\r\n"u8); break; default: From 6b65ee9940fa309b0cab7953c44d71669e63470f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 01:40:54 +0900 Subject: [PATCH 369/585] Extract functions to ImVectorWrapper --- .../ImGuiClipboardFunctionProvider.cs | 162 +------------- .../ImVectorWrapper.ZeroTerminatedSequence.cs | 207 ++++++++++++++++++ Dalamud/Interface/Utility/ImVectorWrapper.cs | 91 ++++++-- 3 files changed, 286 insertions(+), 174 deletions(-) create mode 100644 Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index f14210b97..265bc5812 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -80,9 +80,9 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis private void SetClipboardTextImpl(byte* text) { var buffer = ImGuiCurrentContextClipboardHandlerData; - Utf8Utils.SetFromNullTerminatedBytes(ref buffer, text); - Utf8Utils.Normalize(ref buffer); - Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + buffer.SetFromZeroTerminatedSequence(text); + buffer.Utf8Normalize(); + buffer.AddZeroTerminatorIfMissing(); this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data); } @@ -91,159 +91,9 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis _ = this.getTextOriginal(this.clipboardUserDataOriginal); var buffer = ImGuiCurrentContextClipboardHandlerData; - Utf8Utils.TrimNullTerminator(ref buffer); - Utf8Utils.Normalize(ref buffer); - Utf8Utils.AddNullTerminatorIfMissing(ref buffer); + buffer.TrimZeroTerminator(); + buffer.Utf8Normalize(); + buffer.AddZeroTerminatorIfMissing(); return buffer.Data; } - - private static class Utf8Utils - { - /// - /// Sets from , a null terminated UTF-8 string. - /// - /// The target buffer. It will not contain a null terminator. - /// The pointer to the null-terminated UTF-8 string. - public static void SetFromNullTerminatedBytes(ref ImVectorWrapper buf, byte* psz) - { - var len = 0; - while (psz[len] != 0) - len++; - - buf.Clear(); - buf.AddRange(new Span(psz, len)); - } - - /// - /// Removes the null terminator. - /// - /// The UTF-8 string buffer. - public static void TrimNullTerminator(ref ImVectorWrapper buf) - { - while (buf.Length > 0 && buf[^1] == 0) - buf.LengthUnsafe--; - } - - /// - /// Adds a null terminator to the buffer. - /// - /// The buffer. - public static void AddNullTerminatorIfMissing(ref ImVectorWrapper buf) - { - if (buf.Length > 0 && buf[^1] == 0) - return; - buf.Add(0); - } - - /// - /// Counts the number of bytes for the UTF-8 character. - /// - /// The bytes. - /// Available number of bytes. - /// Number of bytes taken, or -1 if the byte was invalid. - public static int CountBytes(byte* b, int avail) - { - if (avail <= 0) - return 0; - if ((b[0] & 0x80) == 0) - return 1; - if ((b[0] & 0xE0) == 0xC0 && avail >= 2) - return (b[1] & 0xC0) == 0x80 ? 2 : -1; - if ((b[0] & 0xF0) == 0xE0 && avail >= 3) - return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 ? 3 : -1; - if ((b[0] & 0xF8) == 0xF0 && avail >= 4) - return (b[1] & 0xC0) == 0x80 && (b[2] & 0xC0) == 0x80 && (b[3] & 0xC0) == 0x80 ? 4 : -1; - return -1; - } - - /// - /// Gets the codepoint. - /// - /// The bytes. - /// The result from . - /// The codepoint, or \xFFFD replacement character if failed. - public static int GetCodepoint(byte* b, int cb) => cb switch - { - 1 => b[0], - 2 => ((b[0] & 0x1F) << 6) | (b[1] & 0x3F), - 3 => ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F), - 4 => ((b[0] & 0x07) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F), - _ => 0xFFFD, - }; - - /// - /// Replaces a sequence with another. - /// - /// The buffer. - /// Offset of the sequence to be replaced. - /// Length of the sequence to be replaced. - /// The replacement sequence. - /// The length of . - public static int ReplaceSequence( - ref ImVectorWrapper buf, - int offset, - int length, - ReadOnlySpan replacement) - { - var i = 0; - for (; i < replacement.Length; i++) - { - if (length >= i + 1) - buf[offset++] = replacement[i]; - else - buf.Insert(offset++, replacement[i]); - } - - for (; i < length; i++) - buf.RemoveAt(offset); - - return replacement.Length; - } - - /// - /// Normalize the given text for our use case. - /// - /// The buffer. - public static void Normalize(ref ImVectorWrapper buf) - { - // Ensure an implicit null after the end of the string. - buf.EnsureCapacity(buf.Length + 1); - buf.StorageSpan[buf.Length] = 0; - - for (var i = 0; i < buf.Length;) - { - var cb = CountBytes(buf.Data + i, buf.Length - i); - var currInt = GetCodepoint(buf.Data + i, cb); - switch (currInt) - { - // Note that buf.Data[i + 1] is always defined. See the beginning of the function. - case '\r' when buf.Data[i + 1] == '\n': // Already CR LF? - i += 2; - continue; - - case 0xFFFE or 0xFFFF: // Simply invalid - case > char.MaxValue: // ImWchar is same size with char; does not support - case >= 0xD800 and <= 0xDBFF: // UTF-16 surrogate; does not support - // Replace with \uFFFD in UTF-8: EF BF BD - i += ReplaceSequence(ref buf, i, cb, "\uFFFD"u8); - break; - - // See String.Manipulation.cs: IndexOfNewlineChar. - case '\r': // CR; Carriage Return - case '\n': // LF; Line Feed - case '\f': // FF; Form Feed - case '\u0085': // NEL; Next Line - case '\u2028': // LS; Line Separator - case '\u2029': // PS; Paragraph Separator - i += ReplaceSequence(ref buf, i, cb, "\r\n"u8); - break; - - default: - // Not a newline char. - i += cb; - break; - } - } - } - } } diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs b/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs new file mode 100644 index 000000000..507bdce20 --- /dev/null +++ b/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs @@ -0,0 +1,207 @@ +using System.Numerics; +using System.Text; + +namespace Dalamud.Interface.Utility; + +/// +/// Utility methods for . +/// +public static partial class ImVectorWrapper +{ + /// + /// Appends from , a zero terminated sequence. + /// + /// The element type. + /// The target buffer. + /// The pointer to the zero-terminated sequence. + public static unsafe void AppendZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) + where T : unmanaged, INumber + { + var len = 0; + while (psz[len] != default) + len++; + + buf.AddRange(new Span(psz, len)); + } + + /// + /// Sets from , a zero terminated sequence. + /// + /// The element type. + /// The target buffer. + /// The pointer to the zero-terminated sequence. + public static unsafe void SetFromZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) + where T : unmanaged, INumber + { + buf.Clear(); + buf.AppendZeroTerminatedSequence(psz); + } + + /// + /// Trims zero terminator(s). + /// + /// The element type. + /// The buffer. + public static void TrimZeroTerminator(this ref ImVectorWrapper buf) + where T : unmanaged, INumber + { + ref var len = ref buf.LengthUnsafe; + while (len > 0 && buf[len - 1] == default) + len--; + } + + /// + /// Adds a zero terminator to the buffer, if missing. + /// + /// The element type. + /// The buffer. + public static void AddZeroTerminatorIfMissing(this ref ImVectorWrapper buf) + where T : unmanaged, INumber + { + if (buf.Length > 0 && buf[^1] == default) + return; + buf.Add(default); + } + + /// + /// Gets the codepoint at the given offset. + /// + /// The buffer containing bytes in UTF-8. + /// The offset in bytes. + /// Number of bytes occupied by the character, invalid or not. + /// The fallback character, if no valid UTF-8 character could be found. + /// The parsed codepoint, or if it could not be parsed correctly. + public static unsafe int Utf8GetCodepoint( + this in ImVectorWrapper buf, + int offset, + out int numBytes, + int invalid = 0xFFFD) + { + var cb = buf.LengthUnsafe - offset; + if (cb <= 0) + { + numBytes = 0; + return invalid; + } + + numBytes = 1; + + var b = buf.DataUnsafe + offset; + if ((b[0] & 0x80) == 0) + return b[0]; + + if (cb < 2 || (b[1] & 0xC0) != 0x80) + return invalid; + if ((b[0] & 0xE0) == 0xC0) + { + numBytes = 2; + return ((b[0] & 0x1F) << 6) | (b[1] & 0x3F); + } + + if (cb < 3 || (b[2] & 0xC0) != 0x80) + return invalid; + if ((b[0] & 0xF0) == 0xE0) + { + numBytes = 3; + return ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F); + } + + if (cb < 4 || (b[3] & 0xC0) != 0x80) + return invalid; + if ((b[0] & 0xF8) == 0xF0) + { + numBytes = 4; + return ((b[0] & 0x07) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F); + } + + return invalid; + } + + /// + /// Normalizes the given UTF-8 string.
+ /// Using the default values will ensure the best interop between the game, ImGui, and Windows. + ///
+ /// The buffer containing bytes in UTF-8. + /// The replacement line ending. If empty, CR LF will be used. + /// The replacement invalid character. If empty, U+FFFD REPLACEMENT CHARACTER will be used. + /// Specify whether to normalize the line endings. + /// Specify whether to replace invalid characters. + /// Specify whether to replace characters that requires the use of surrogate, when encoded in UTF-16. + /// Specify whether to make sense out of WTF-8. + public static unsafe void Utf8Normalize( + this ref ImVectorWrapper buf, + ReadOnlySpan lineEnding = default, + ReadOnlySpan invalidChar = default, + bool normalizeLineEndings = true, + bool sanitizeInvalidCharacters = true, + bool sanitizeNonUcs2Characters = true, + bool sanitizeSurrogates = true) + { + if (lineEnding.IsEmpty) + lineEnding = "\r\n"u8; + if (invalidChar.IsEmpty) + invalidChar = "\uFFFD"u8; + + // Ensure an implicit null after the end of the string. + buf.EnsureCapacity(buf.Length + 1); + buf.StorageSpan[buf.Length] = 0; + + Span charsBuf = stackalloc char[2]; + Span bytesBuf = stackalloc byte[4]; + for (var i = 0; i < buf.Length;) + { + var c1 = buf.Utf8GetCodepoint(i, out var cb, -1); + switch (c1) + { + // Note that buf.Data[i + 1] is always defined. See the beginning of the function. + case '\r' when buf.Data[i + 1] == '\n': + // If it's already CR LF, it passes all filters. + i += 2; + break; + + case >= 0xD800 and <= 0xDFFF when sanitizeSurrogates: + { + var c2 = buf.Utf8GetCodepoint(i + cb, out var cb2); + if (c1 is < 0xD800 or >= 0xDC00) + goto case -2; + if (c2 is < 0xDC00 or >= 0xE000) + goto case -2; + charsBuf[0] = unchecked((char)c1); + charsBuf[1] = unchecked((char)c2); + var bytesLen = Encoding.UTF8.GetBytes(charsBuf, bytesBuf); + buf.ReplaceRange(i, cb + cb2, bytesBuf[..bytesLen]); + // Do not alter i; now that the WTF-8 has been dealt with, apply other filters. + break; + } + + case -2: + case -1 or 0xFFFE or 0xFFFF when sanitizeInvalidCharacters: + case >= 0xD800 and <= 0xDFFF when sanitizeInvalidCharacters: + case > char.MaxValue when sanitizeNonUcs2Characters: + { + buf.ReplaceRange(i, cb, invalidChar); + i += invalidChar.Length; + break; + } + + // See String.Manipulation.cs: IndexOfNewlineChar. + // CR; Carriage Return + // LF; Line Feed + // FF; Form Feed + // NEL; Next Line + // LS; Line Separator + // PS; Paragraph Separator + case '\r' or '\n' or '\f' or '\u0085' or '\u2028' or '\u2029' when normalizeLineEndings: + { + buf.ReplaceRange(i, cb, lineEnding); + i += lineEnding.Length; + break; + } + + default: + i += cb; + break; + } + } + } +} diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs index d41ee0094..51524efc4 100644 --- a/Dalamud/Interface/Utility/ImVectorWrapper.cs +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -13,7 +13,7 @@ namespace Dalamud.Interface.Utility; /// /// Utility methods for . /// -public static class ImVectorWrapper +public static partial class ImVectorWrapper { /// /// Creates a new instance of the struct, initialized with @@ -394,7 +394,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi } /// - public void AddRange(Span items) + public void AddRange(ReadOnlySpan items) { this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); foreach (var item in items) @@ -466,7 +466,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi /// The minimum capacity to ensure. /// Whether the capacity has been changed. public bool EnsureCapacityExponential(int capacity) - => this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)this.LengthUnsafe))); + => this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)capacity))); /// /// Resizes the underlying array and fills with zeroes if grown. @@ -545,8 +545,8 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi } } - /// - public void InsertRange(int index, Span items) + /// + public void InsertRange(int index, ReadOnlySpan items) { this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); var num = this.LengthUnsafe - index; @@ -561,16 +561,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi /// /// The index. /// Whether to skip calling the destroyer function. - public void RemoveAt(int index, bool skipDestroyer = false) - { - this.EnsureIndex(index); - var num = this.LengthUnsafe - index - 1; - if (!skipDestroyer) - this.destroyer?.Invoke(&this.DataUnsafe[index]); - - Buffer.MemoryCopy(this.DataUnsafe + index + 1, this.DataUnsafe + index, num * sizeof(T), num * sizeof(T)); - this.LengthUnsafe -= 1; - } + public void RemoveAt(int index, bool skipDestroyer = false) => this.RemoveRange(index, 1, skipDestroyer); /// void IList.RemoveAt(int index) => this.RemoveAt(index); @@ -578,6 +569,73 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi /// void IList.RemoveAt(int index) => this.RemoveAt(index); + /// + /// Removes elements at the given index. + /// + /// The index of the first item to remove. + /// Number of items to remove. + /// Whether to skip calling the destroyer function. + public void RemoveRange(int index, int count, bool skipDestroyer = false) + { + this.EnsureIndex(index); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), count, "Must be positive."); + if (count == 0) + return; + + if (!skipDestroyer && this.destroyer is { } d) + { + for (var i = 0; i < count; i++) + d(this.DataUnsafe + index + i); + } + + var numItemsToMove = this.LengthUnsafe - index - count; + var numBytesToMove = numItemsToMove * sizeof(T); + Buffer.MemoryCopy(this.DataUnsafe + index + count, this.DataUnsafe + index, numBytesToMove, numBytesToMove); + this.LengthUnsafe -= count; + } + + /// + /// Replaces a sequence at given offset of items with + /// . + /// + /// The index of the first item to be replaced. + /// The number of items to be replaced. + /// The replacement. + /// Whether to skip calling the destroyer function. + public void ReplaceRange(int index, int count, ReadOnlySpan replacement, bool skipDestroyer = false) + { + this.EnsureIndex(index); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), count, "Must be positive."); + if (count == 0) + return; + + // Ensure the capacity first, so that we can safely destroy the items first. + this.EnsureCapacityExponential((this.LengthUnsafe + replacement.Length) - count); + + if (!skipDestroyer && this.destroyer is { } d) + { + for (var i = 0; i < count; i++) + d(this.DataUnsafe + index + i); + } + + if (count == replacement.Length) + { + replacement.CopyTo(this.DataSpan[index..]); + } + else if (count > replacement.Length) + { + replacement.CopyTo(this.DataSpan[index..]); + this.RemoveRange(index + replacement.Length, count - replacement.Length); + } + else + { + replacement[..count].CopyTo(this.DataSpan[index..]); + this.InsertRange(index + count, replacement[count..]); + } + } + /// /// Sets the capacity exactly as requested. /// @@ -615,9 +673,6 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi if (!oldSpan.IsEmpty && !newSpan.IsEmpty) oldSpan[..this.LengthUnsafe].CopyTo(newSpan); -// #if DEBUG -// new Span(newAlloc + this.LengthUnsafe, sizeof(T) * (capacity - this.LengthUnsafe)).Fill(0xCC); -// #endif if (oldAlloc != null) ImGuiNative.igMemFree(oldAlloc); From f6d16d5624f7a922de7d497fa18fb4f0f8458b6a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 02:04:04 +0900 Subject: [PATCH 370/585] Do operations that may throw first --- .../Interface/Internal/ImGuiClipboardFunctionProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 265bc5812..76e8e73e6 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -42,12 +42,12 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); var io = ImGui.GetIO(); + this.clipboardUserDataOriginal = io.ClipboardUserData; this.setTextOriginal = (delegate* unmanaged)io.SetClipboardTextFn; this.getTextOriginal = (delegate* unmanaged)io.GetClipboardTextFn; - this.clipboardUserDataOriginal = io.ClipboardUserData; - io.SetClipboardTextFn = (nint)(delegate* unmanaged)(&StaticSetClipboardTextImpl); - io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this)); + io.SetClipboardTextFn = (nint)(delegate* unmanaged)&StaticSetClipboardTextImpl; + io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; return; [UnmanagedCallersOnly] From 8f243762cc29d6a24ac82331ca49769af0a77880 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 9 Dec 2023 01:23:04 +0100 Subject: [PATCH 371/585] ChatGui: fix for new message sounds and interactable links (#1568) * Change PrintMessage parameters type to byte * Use Utf8String.AsSpan * Fix InteractableLinkClickedDetour using the wrong variable --- Dalamud/Game/Gui/ChatGui.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 1214850b0..02b52ee56 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -22,6 +22,7 @@ namespace Dalamud.Game.Gui; // TODO(api10): Update IChatGui, ChatGui and XivChatEntry to use correct types and names: // "uint SenderId" should be "int Timestamp". // "IntPtr Parameters" should be something like "bool Silent". It suppresses new message sounds in certain channels. +// This has to be a 1 byte boolean, so only change it to bool if marshalling is disabled. /// /// This class handles interacting with the native chat UI. @@ -62,7 +63,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, bool silent); + private delegate uint PrintMessageDelegate(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent); [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate void PopulateItemLinkDelegate(IntPtr linkObjectPtr, IntPtr itemInfoPtr); @@ -157,7 +158,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui var sender = Utf8String.FromSequence(chat.Name.Encode()); var message = Utf8String.FromSequence(chat.Message.Encode()); - this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, chat.Parameters != 0); + this.HandlePrintMessageDetour(RaptureLogModule.Instance(), chat.Type, sender, message, (int)chat.SenderId, (byte)(chat.Parameters != 0 ? 1 : 0)); sender->Dtor(true); message->Dtor(true); @@ -278,14 +279,14 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui } } - private uint HandlePrintMessageDetour(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, bool silent) + private uint HandlePrintMessageDetour(RaptureLogModule* manager, XivChatType chatType, Utf8String* sender, Utf8String* message, int timestamp, byte silent) { var messageId = 0u; try { - var originalSenderData = sender->Span.ToArray(); - var originalMessageData = message->Span.ToArray(); + var originalSenderData = sender->AsSpan().ToArray(); + var originalMessageData = message->AsSpan().ToArray(); var parsedSender = SeString.Parse(originalSenderData); var parsedMessage = SeString.Parse(originalMessageData); @@ -374,7 +375,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui Log.Verbose($"InteractableLinkClicked: {Payload.EmbeddedInfoType.DalamudLink}"); var payloadPtr = Marshal.ReadIntPtr(messagePtr, 0x10); - var seStr = MemoryHelper.ReadSeStringNullTerminated(messagePtr); + var seStr = MemoryHelper.ReadSeStringNullTerminated(payloadPtr); var terminatorIndex = seStr.Payloads.IndexOf(RawPayload.LinkTerminator); var payloads = terminatorIndex >= 0 ? seStr.Payloads.Take(terminatorIndex + 1).ToList() : seStr.Payloads; if (payloads.Count == 0) return; From 06938509e79f90f15077f9180a74432e3e799687 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Dec 2023 15:25:50 +0900 Subject: [PATCH 372/585] Just use win32 APIs --- Dalamud/Dalamud.csproj | 1 + .../ImGuiClipboardFunctionProvider.cs | 144 ++++++++++-- .../ImVectorWrapper.ZeroTerminatedSequence.cs | 207 ------------------ Dalamud/Interface/Utility/ImVectorWrapper.cs | 4 +- 4 files changed, 125 insertions(+), 231 deletions(-) delete mode 100644 Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 3a6a0257d..d31f79e0c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -90,6 +90,7 @@ + diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 76e8e73e6..fd07d824f 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -1,11 +1,19 @@ using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; +using System.Text; +using CheapLoc; + +using Dalamud.Game.Gui.Toast; using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; using ImGuiNET; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + namespace Dalamud.Interface.Internal; /// @@ -29,9 +37,15 @@ namespace Dalamud.Interface.Internal; [ServiceManager.EarlyLoadedService] internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable { + private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider)); private readonly nint clipboardUserDataOriginal; - private readonly delegate* unmanaged setTextOriginal; - private readonly delegate* unmanaged getTextOriginal; + private readonly nint setTextOriginal; + private readonly nint getTextOriginal; + + [ServiceManager.ServiceDependency] + private readonly ToastGui toastGui = Service.Get(); + + private ImVectorWrapper clipboardData; private GCHandle clipboardUserData; [ServiceManager.ServiceConstructor] @@ -43,11 +57,13 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis var io = ImGui.GetIO(); this.clipboardUserDataOriginal = io.ClipboardUserData; - this.setTextOriginal = (delegate* unmanaged)io.SetClipboardTextFn; - this.getTextOriginal = (delegate* unmanaged)io.GetClipboardTextFn; + this.setTextOriginal = io.SetClipboardTextFn; + this.getTextOriginal = io.GetClipboardTextFn; io.ClipboardUserData = GCHandle.ToIntPtr(this.clipboardUserData = GCHandle.Alloc(this)); io.SetClipboardTextFn = (nint)(delegate* unmanaged)&StaticSetClipboardTextImpl; io.GetClipboardTextFn = (nint)(delegate* unmanaged)&StaticGetClipboardTextImpl; + + this.clipboardData = new(0); return; [UnmanagedCallersOnly] @@ -59,10 +75,6 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis ((ImGuiClipboardFunctionProvider)GCHandle.FromIntPtr(userData).Target)!.GetClipboardTextImpl(); } - [SuppressMessage("ReSharper", "AssignNullToNotNullAttribute", Justification = "If it's null, it's crashworthy")] - private static ImVectorWrapper ImGuiCurrentContextClipboardHandlerData => - new((ImVector*)(ImGui.GetCurrentContext() + 0x5520)); - /// public void Dispose() { @@ -70,30 +82,118 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis return; var io = ImGui.GetIO(); - io.SetClipboardTextFn = (nint)this.setTextOriginal; - io.GetClipboardTextFn = (nint)this.getTextOriginal; + io.SetClipboardTextFn = this.setTextOriginal; + io.GetClipboardTextFn = this.getTextOriginal; io.ClipboardUserData = this.clipboardUserDataOriginal; this.clipboardUserData.Free(); + this.clipboardData.Dispose(); + } + + private bool OpenClipboardOrShowError() + { + if (!OpenClipboard(default)) + { + this.toastGui.ShowError( + Loc.Localize( + "ImGuiClipboardFunctionProviderClipboardInUse", + "Some other application is using the clipboard. Try again later.")); + return false; + } + + return true; } private void SetClipboardTextImpl(byte* text) { - var buffer = ImGuiCurrentContextClipboardHandlerData; - buffer.SetFromZeroTerminatedSequence(text); - buffer.Utf8Normalize(); - buffer.AddZeroTerminatorIfMissing(); - this.setTextOriginal(this.clipboardUserDataOriginal, buffer.Data); + if (!this.OpenClipboardOrShowError()) + return; + + try + { + var len = 0; + while (text[len] != 0) + len++; + var str = Encoding.UTF8.GetString(text, len); + str = str.ReplaceLineEndings("\r\n"); + var hMem = GlobalAlloc(GMEM.GMEM_MOVEABLE, (nuint)((str.Length + 1) * 2)); + if (hMem == 0) + throw new OutOfMemoryException(); + + var ptr = (char*)GlobalLock(hMem); + if (ptr == null) + { + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) + ?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed."); + } + + str.AsSpan().CopyTo(new(ptr, str.Length)); + ptr[str.Length] = default; + GlobalUnlock(hMem); + + SetClipboardData(CF.CF_UNICODETEXT, hMem); + } + catch (Exception e) + { + Log.Error(e, $"Error in {nameof(this.SetClipboardTextImpl)}"); + this.toastGui.ShowError( + Loc.Localize( + "ImGuiClipboardFunctionProviderErrorCopy", + "Failed to copy. See logs for details.")); + } + finally + { + CloseClipboard(); + } } private byte* GetClipboardTextImpl() { - _ = this.getTextOriginal(this.clipboardUserDataOriginal); + this.clipboardData.Clear(); + + var formats = stackalloc uint[] { CF.CF_UNICODETEXT, CF.CF_TEXT }; + if (GetPriorityClipboardFormat(formats, 2) < 1 || !this.OpenClipboardOrShowError()) + { + this.clipboardData.Add(0); + return this.clipboardData.Data; + } - var buffer = ImGuiCurrentContextClipboardHandlerData; - buffer.TrimZeroTerminator(); - buffer.Utf8Normalize(); - buffer.AddZeroTerminatorIfMissing(); - return buffer.Data; + try + { + var hMem = (HGLOBAL)GetClipboardData(CF.CF_UNICODETEXT); + if (hMem != default) + { + var ptr = (char*)GlobalLock(hMem); + if (ptr == null) + { + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) + ?? throw new InvalidOperationException($"{nameof(GlobalLock)} failed."); + } + + var str = new string(ptr); + str = str.ReplaceLineEndings("\r\n"); + this.clipboardData.Resize(Encoding.UTF8.GetByteCount(str) + 1); + Encoding.UTF8.GetBytes(str, this.clipboardData.DataSpan); + this.clipboardData[^1] = 0; + } + else + { + this.clipboardData.Add(0); + } + } + catch (Exception e) + { + Log.Error(e, $"Error in {nameof(this.GetClipboardTextImpl)}"); + this.toastGui.ShowError( + Loc.Localize( + "ImGuiClipboardFunctionProviderErrorPaste", + "Failed to paste. See logs for details.")); + } + finally + { + CloseClipboard(); + } + + return this.clipboardData.Data; } } diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs b/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs deleted file mode 100644 index 507bdce20..000000000 --- a/Dalamud/Interface/Utility/ImVectorWrapper.ZeroTerminatedSequence.cs +++ /dev/null @@ -1,207 +0,0 @@ -using System.Numerics; -using System.Text; - -namespace Dalamud.Interface.Utility; - -/// -/// Utility methods for . -/// -public static partial class ImVectorWrapper -{ - /// - /// Appends from , a zero terminated sequence. - /// - /// The element type. - /// The target buffer. - /// The pointer to the zero-terminated sequence. - public static unsafe void AppendZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) - where T : unmanaged, INumber - { - var len = 0; - while (psz[len] != default) - len++; - - buf.AddRange(new Span(psz, len)); - } - - /// - /// Sets from , a zero terminated sequence. - /// - /// The element type. - /// The target buffer. - /// The pointer to the zero-terminated sequence. - public static unsafe void SetFromZeroTerminatedSequence(this ref ImVectorWrapper buf, T* psz) - where T : unmanaged, INumber - { - buf.Clear(); - buf.AppendZeroTerminatedSequence(psz); - } - - /// - /// Trims zero terminator(s). - /// - /// The element type. - /// The buffer. - public static void TrimZeroTerminator(this ref ImVectorWrapper buf) - where T : unmanaged, INumber - { - ref var len = ref buf.LengthUnsafe; - while (len > 0 && buf[len - 1] == default) - len--; - } - - /// - /// Adds a zero terminator to the buffer, if missing. - /// - /// The element type. - /// The buffer. - public static void AddZeroTerminatorIfMissing(this ref ImVectorWrapper buf) - where T : unmanaged, INumber - { - if (buf.Length > 0 && buf[^1] == default) - return; - buf.Add(default); - } - - /// - /// Gets the codepoint at the given offset. - /// - /// The buffer containing bytes in UTF-8. - /// The offset in bytes. - /// Number of bytes occupied by the character, invalid or not. - /// The fallback character, if no valid UTF-8 character could be found. - /// The parsed codepoint, or if it could not be parsed correctly. - public static unsafe int Utf8GetCodepoint( - this in ImVectorWrapper buf, - int offset, - out int numBytes, - int invalid = 0xFFFD) - { - var cb = buf.LengthUnsafe - offset; - if (cb <= 0) - { - numBytes = 0; - return invalid; - } - - numBytes = 1; - - var b = buf.DataUnsafe + offset; - if ((b[0] & 0x80) == 0) - return b[0]; - - if (cb < 2 || (b[1] & 0xC0) != 0x80) - return invalid; - if ((b[0] & 0xE0) == 0xC0) - { - numBytes = 2; - return ((b[0] & 0x1F) << 6) | (b[1] & 0x3F); - } - - if (cb < 3 || (b[2] & 0xC0) != 0x80) - return invalid; - if ((b[0] & 0xF0) == 0xE0) - { - numBytes = 3; - return ((b[0] & 0x0F) << 12) | ((b[1] & 0x3F) << 6) | (b[2] & 0x3F); - } - - if (cb < 4 || (b[3] & 0xC0) != 0x80) - return invalid; - if ((b[0] & 0xF8) == 0xF0) - { - numBytes = 4; - return ((b[0] & 0x07) << 18) | ((b[1] & 0x3F) << 12) | ((b[2] & 0x3F) << 6) | (b[3] & 0x3F); - } - - return invalid; - } - - /// - /// Normalizes the given UTF-8 string.
- /// Using the default values will ensure the best interop between the game, ImGui, and Windows. - ///
- /// The buffer containing bytes in UTF-8. - /// The replacement line ending. If empty, CR LF will be used. - /// The replacement invalid character. If empty, U+FFFD REPLACEMENT CHARACTER will be used. - /// Specify whether to normalize the line endings. - /// Specify whether to replace invalid characters. - /// Specify whether to replace characters that requires the use of surrogate, when encoded in UTF-16. - /// Specify whether to make sense out of WTF-8. - public static unsafe void Utf8Normalize( - this ref ImVectorWrapper buf, - ReadOnlySpan lineEnding = default, - ReadOnlySpan invalidChar = default, - bool normalizeLineEndings = true, - bool sanitizeInvalidCharacters = true, - bool sanitizeNonUcs2Characters = true, - bool sanitizeSurrogates = true) - { - if (lineEnding.IsEmpty) - lineEnding = "\r\n"u8; - if (invalidChar.IsEmpty) - invalidChar = "\uFFFD"u8; - - // Ensure an implicit null after the end of the string. - buf.EnsureCapacity(buf.Length + 1); - buf.StorageSpan[buf.Length] = 0; - - Span charsBuf = stackalloc char[2]; - Span bytesBuf = stackalloc byte[4]; - for (var i = 0; i < buf.Length;) - { - var c1 = buf.Utf8GetCodepoint(i, out var cb, -1); - switch (c1) - { - // Note that buf.Data[i + 1] is always defined. See the beginning of the function. - case '\r' when buf.Data[i + 1] == '\n': - // If it's already CR LF, it passes all filters. - i += 2; - break; - - case >= 0xD800 and <= 0xDFFF when sanitizeSurrogates: - { - var c2 = buf.Utf8GetCodepoint(i + cb, out var cb2); - if (c1 is < 0xD800 or >= 0xDC00) - goto case -2; - if (c2 is < 0xDC00 or >= 0xE000) - goto case -2; - charsBuf[0] = unchecked((char)c1); - charsBuf[1] = unchecked((char)c2); - var bytesLen = Encoding.UTF8.GetBytes(charsBuf, bytesBuf); - buf.ReplaceRange(i, cb + cb2, bytesBuf[..bytesLen]); - // Do not alter i; now that the WTF-8 has been dealt with, apply other filters. - break; - } - - case -2: - case -1 or 0xFFFE or 0xFFFF when sanitizeInvalidCharacters: - case >= 0xD800 and <= 0xDFFF when sanitizeInvalidCharacters: - case > char.MaxValue when sanitizeNonUcs2Characters: - { - buf.ReplaceRange(i, cb, invalidChar); - i += invalidChar.Length; - break; - } - - // See String.Manipulation.cs: IndexOfNewlineChar. - // CR; Carriage Return - // LF; Line Feed - // FF; Form Feed - // NEL; Next Line - // LS; Line Separator - // PS; Paragraph Separator - case '\r' or '\n' or '\f' or '\u0085' or '\u2028' or '\u2029' when normalizeLineEndings: - { - buf.ReplaceRange(i, cb, lineEnding); - i += lineEnding.Length; - break; - } - - default: - i += cb; - break; - } - } - } -} diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs index 51524efc4..5ba1aec2f 100644 --- a/Dalamud/Interface/Utility/ImVectorWrapper.cs +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -13,7 +13,7 @@ namespace Dalamud.Interface.Utility; /// /// Utility methods for . /// -public static partial class ImVectorWrapper +public static class ImVectorWrapper { /// /// Creates a new instance of the struct, initialized with @@ -208,7 +208,7 @@ public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDi /// /// The initial capacity. /// The destroyer function to call on item removal. - public ImVectorWrapper(int initialCapacity = 0, ImGuiNativeDestroyDelegate? destroyer = null) + public ImVectorWrapper(int initialCapacity, ImGuiNativeDestroyDelegate? destroyer = null) { if (initialCapacity < 0) { From 4d0cce134fa25ae03533b01a38d999ef42151f8b Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 10 Dec 2023 12:35:40 +0900 Subject: [PATCH 373/585] Fix AddonLifecycle ABI; deprecate arg class public ctors (#1570) --- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 28 +++++++++++++------ .../Lifecycle/AddonArgTypes/AddonDrawArgs.cs | 10 ++++++- .../AddonArgTypes/AddonFinalizeArgs.cs | 10 ++++++- .../AddonArgTypes/AddonReceiveEventArgs.cs | 18 ++++++++---- .../AddonArgTypes/AddonRefreshArgs.cs | 16 ++++++++--- .../AddonArgTypes/AddonRequestedUpdateArgs.cs | 14 ++++++++-- .../Lifecycle/AddonArgTypes/AddonSetupArgs.cs | 16 ++++++++--- .../AddonArgTypes/AddonUpdateArgs.cs | 23 +++++++++++++-- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 17 ++++++----- .../AddonLifecycleReceiveEventListener.cs | 5 +++- 10 files changed, 119 insertions(+), 38 deletions(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 077ca7c93..d82bf29a9 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -1,4 +1,5 @@ using Dalamud.Memory; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -12,7 +13,7 @@ public abstract unsafe class AddonArgs /// Constant string representing the name of an addon that is invalid. ///
public const string InvalidAddon = "NullAddon"; - + private string? addonName; private IntPtr addon; @@ -26,8 +27,22 @@ public abstract unsafe class AddonArgs ///
public nint Addon { - get => this.addon; - internal set + get => this.AddonInternal; + init => this.AddonInternal = value; + } + + /// + /// Gets the type of these args. + /// + public abstract AddonArgsType Type { get; } + + /// + /// Gets or sets the pointer to the addons AtkUnitBase. + /// + internal nint AddonInternal + { + get => this.Addon; + set { if (this.addon == value) return; @@ -37,11 +52,6 @@ public abstract unsafe class AddonArgs } } - /// - /// Gets the type of these args. - /// - public abstract AddonArgsType Type { get; } - /// /// Checks if addon name matches the given span of char. /// @@ -55,7 +65,7 @@ public abstract unsafe class AddonArgs var addonPointer = (AtkUnitBase*)this.Addon; if (addonPointer->Name is null) return false; - + return MemoryHelper.EqualsZeroTerminatedString(name, (nint)addonPointer->Name, null, 0x20); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs index 1e1013dd5..989e11912 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonDrawArgs.cs @@ -5,12 +5,20 @@ ///
public class AddonDrawArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonDrawArgs() + { + } + /// 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 fc26a6c33..d9401b414 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonFinalizeArgs.cs @@ -5,12 +5,20 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; ///
public class AddonFinalizeArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonFinalizeArgs() + { + } + /// 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/AddonReceiveEventArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs index 8f9003b4c..a557b0cb3 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonReceiveEventArgs.cs @@ -5,24 +5,32 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// public class AddonReceiveEventArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonReceiveEventArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.ReceiveEvent; - + /// /// Gets or sets the AtkEventType for this event message. /// public byte AtkEventType { get; set; } - + /// /// Gets or sets the event id for this event message. /// public int EventParam { get; set; } - + /// /// Gets or sets the pointer to an AtkEvent for this event message. /// public nint AtkEvent { get; set; } - + /// /// Gets or sets the pointer to a block of data for this event message. /// @@ -30,7 +38,7 @@ public class AddonReceiveEventArgs : AddonArgs, ICloneable /// public AddonReceiveEventArgs Clone() => (AddonReceiveEventArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs index bfcf02544..6e1b11ead 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRefreshArgs.cs @@ -7,19 +7,27 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// public class AddonRefreshArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonRefreshArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.Refresh; - + /// /// Gets or sets the number of AtkValues. /// public uint AtkValueCount { get; set; } - + /// /// Gets or sets the address of the AtkValue array. /// public nint AtkValues { get; set; } - + /// /// Gets the AtkValues in the form of a span. /// @@ -27,7 +35,7 @@ public class AddonRefreshArgs : AddonArgs, ICloneable /// public AddonRefreshArgs Clone() => (AddonRefreshArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs index 219288ccf..26357abb0 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonRequestedUpdateArgs.cs @@ -5,14 +5,22 @@ /// public class AddonRequestedUpdateArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonRequestedUpdateArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.RequestedUpdate; - + /// /// Gets or sets the NumberArrayData** for this event. /// public nint NumberArrayData { get; set; } - + /// /// Gets or sets the StringArrayData** for this event. /// @@ -20,7 +28,7 @@ public class AddonRequestedUpdateArgs : AddonArgs, ICloneable /// public AddonRequestedUpdateArgs Clone() => (AddonRequestedUpdateArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs index bd60879b8..19c93ce25 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonSetupArgs.cs @@ -7,19 +7,27 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// public class AddonSetupArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonSetupArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.Setup; - + /// /// Gets or sets the number of AtkValues. /// public uint AtkValueCount { get; set; } - + /// /// Gets or sets the address of the AtkValue array. /// public nint AtkValues { get; set; } - + /// /// Gets the AtkValues in the form of a span. /// @@ -27,7 +35,7 @@ public class AddonSetupArgs : AddonArgs, ICloneable /// public AddonSetupArgs Clone() => (AddonSetupArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs index b087ac15a..cc34a7531 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonUpdateArgs.cs @@ -5,17 +5,34 @@ namespace Dalamud.Game.Addon.Lifecycle.AddonArgTypes; /// public class AddonUpdateArgs : AddonArgs, ICloneable { + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Not intended for public construction.", false)] + public AddonUpdateArgs() + { + } + /// public override AddonArgsType Type => AddonArgsType.Update; - + /// /// Gets the time since the last update. /// - public float TimeDelta { get; internal set; } + public float TimeDelta + { + get => this.TimeDeltaInternal; + init => this.TimeDeltaInternal = value; + } + + /// + /// Gets or sets the time since the last update. + /// + internal float TimeDeltaInternal { get; set; } /// public AddonUpdateArgs Clone() => (AddonUpdateArgs)this.MemberwiseClone(); - + /// object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index decb7a9f4..6288cd2cd 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -43,12 +43,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet // package, and these events are always called from the main thread, this is fine. +#pragma warning disable CS0618 // Type or member is obsolete + // TODO: turn constructors of these internal private readonly AddonSetupArgs recyclingSetupArgs = new(); private readonly AddonFinalizeArgs recyclingFinalizeArgs = new(); private readonly AddonDrawArgs recyclingDrawArgs = new(); private readonly AddonUpdateArgs recyclingUpdateArgs = new(); private readonly AddonRefreshArgs recyclingRefreshArgs = new(); private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new(); +#pragma warning restore CS0618 // Type or member is obsolete [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) @@ -275,7 +278,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); } - this.recyclingSetupArgs.Addon = (nint)addon; + this.recyclingSetupArgs.AddonInternal = (nint)addon; this.recyclingSetupArgs.AtkValueCount = valueCount; this.recyclingSetupArgs.AtkValues = (nint)values; this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs); @@ -306,7 +309,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); } - this.recyclingFinalizeArgs.Addon = (nint)atkUnitBase[0]; + this.recyclingFinalizeArgs.AddonInternal = (nint)atkUnitBase[0]; this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs); try @@ -321,7 +324,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonDraw(AtkUnitBase* addon) { - this.recyclingDrawArgs.Addon = (nint)addon; + this.recyclingDrawArgs.AddonInternal = (nint)addon; this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs); try @@ -338,8 +341,8 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.recyclingUpdateArgs.Addon = (nint)addon; - this.recyclingUpdateArgs.TimeDelta = delta; + this.recyclingUpdateArgs.AddonInternal = (nint)addon; + this.recyclingUpdateArgs.TimeDeltaInternal = delta; this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs); try @@ -358,7 +361,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType { byte result = 0; - this.recyclingRefreshArgs.Addon = (nint)addon; + this.recyclingRefreshArgs.AddonInternal = (nint)addon; this.recyclingRefreshArgs.AtkValueCount = valueCount; this.recyclingRefreshArgs.AtkValues = (nint)values; this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs); @@ -380,7 +383,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.recyclingRequestedUpdateArgs.Addon = (nint)addon; + this.recyclingRequestedUpdateArgs.AddonInternal = (nint)addon; this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData; this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData; this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs); diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs index 1c138e447..43aa71661 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs @@ -18,7 +18,10 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet // package, and these events are always called from the main thread, this is fine. +#pragma warning disable CS0618 // Type or member is obsolete + // TODO: turn constructors of these internal private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new(); +#pragma warning restore CS0618 // Type or member is obsolete /// /// Initializes a new instance of the class. @@ -79,7 +82,7 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable return; } - this.recyclingReceiveEventArgs.Addon = (nint)addon; + this.recyclingReceiveEventArgs.AddonInternal = (nint)addon; this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType; this.recyclingReceiveEventArgs.EventParam = eventParam; this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent; From 5a5cc5701ab9dbf268b57e31b857bbe7a2513695 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 9 Dec 2023 20:30:58 -0800 Subject: [PATCH 374/585] Hotfix for AddonArgs infinite loop (#1571) --- Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index d82bf29a9..4ab3de5ca 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -41,7 +41,7 @@ public abstract unsafe class AddonArgs /// internal nint AddonInternal { - get => this.Addon; + get => this.addon; set { if (this.addon == value) From fb864dd56d9dbc868496f579138f50399b7acec8 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 15 Dec 2023 18:15:24 +0100 Subject: [PATCH 375/585] Update ClientStructs (#1565) 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 dcc913975..3364dfea7 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit dcc9139758bf5e2ff5c0b53d73a3566eb0eec4f0 +Subproject commit 3364dfea769b79e43aebaa955b6b98ec1d6eb458 From df1cdff1a53919d879f9bc8dbcf8425219cb0a16 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 16 Dec 2023 12:01:40 -0800 Subject: [PATCH 376/585] AddonEventManager fix thread safety (#1576) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Game/Addon/Events/AddonEventManager.cs | 33 +++++-- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 89 +++++++------------ 2 files changed, 56 insertions(+), 66 deletions(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 23f3b1a6d..af713a771 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -9,6 +9,8 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -31,6 +33,9 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycle = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private readonly AddonLifecycleEventListener finalizeEventListener; private readonly AddonEventManagerAddressResolver address; @@ -87,6 +92,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// IAddonEventHandle used to remove the event. internal IAddonEventHandle? AddEvent(string pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) { + if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported."); + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) { return eventController.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler); @@ -103,6 +110,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// The Unique Id for this event. internal void RemoveEvent(string pluginId, IAddonEventHandle eventHandle) { + if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported."); + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) { eventController.RemoveEvent(eventHandle); @@ -130,11 +139,14 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// Unique ID for this plugin. internal void AddPluginEventController(string pluginId) { - if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId)) + this.framework.RunOnFrameworkThread(() => { - Log.Verbose($"Creating new PluginEventController for: {pluginId}"); - this.pluginEventControllers.Add(new PluginEventController(pluginId)); - } + if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId)) + { + Log.Verbose($"Creating new PluginEventController for: {pluginId}"); + this.pluginEventControllers.Add(new PluginEventController(pluginId)); + } + }); } /// @@ -143,12 +155,15 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// Unique ID for this plugin. internal void RemovePluginEventController(string pluginId) { - if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller) + this.framework.RunOnFrameworkThread(() => { - Log.Verbose($"Removing PluginEventController for: {pluginId}"); - this.pluginEventControllers.Remove(controller); - controller.Dispose(); - } + if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller) + { + Log.Verbose($"Removing PluginEventController for: {pluginId}"); + this.pluginEventControllers.Remove(controller); + controller.Dispose(); + } + }); } /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 6288cd2cd..beaab7fcd 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -38,9 +38,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly Hook onAddonRefreshHook; private readonly CallHook onAddonRequestedUpdateHook; - private readonly ConcurrentBag newEventListeners = new(); - private readonly ConcurrentBag removeEventListeners = new(); - // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet // package, and these events are always called from the main thread, this is fine. #pragma warning disable CS0618 // Type or member is obsolete @@ -61,8 +58,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType // We want value of the function pointer at vFunc[2] this.disallowedReceiveEventAddress = ((nint*)this.address.AtkEventListener)![2]; - - this.framework.Update += this.OnFrameworkUpdate; this.onAddonSetupHook = new CallHook(this.address.AddonSetup, this.OnAddonSetup); this.onAddonSetup2Hook = new CallHook(this.address.AddonSetup2, this.OnAddonSetup); @@ -106,8 +101,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType /// public void Dispose() { - this.framework.Update -= this.OnFrameworkUpdate; - this.onAddonSetupHook.Dispose(); this.onAddonSetup2Hook.Dispose(); this.onAddonFinalizeHook.Dispose(); @@ -128,7 +121,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType /// The listener to register. internal void RegisterListener(AddonLifecycleEventListener listener) { - this.newEventListeners.Add(listener); + 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.Hook?.Enable(); + } + } + }); } /// @@ -137,7 +143,24 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType /// The listener to unregister. internal void UnregisterListener(AddonLifecycleEventListener listener) { - this.removeEventListeners.Add(listener); + 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.Hook?.Disable(); + } + } + } + }); } /// @@ -169,54 +192,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType } } - // Used to prevent concurrency issues if plugins try to register during iteration of listeners. - private void OnFrameworkUpdate(IFramework unused) - { - if (this.newEventListeners.Any()) - { - foreach (var toAddListener in this.newEventListeners) - { - this.EventListeners.Add(toAddListener); - - // 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 (toAddListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toAddListener.AddonName)) is { } receiveEventListener) - { - receiveEventListener.Hook?.Enable(); - } - } - } - - this.newEventListeners.Clear(); - } - - if (this.removeEventListeners.Any()) - { - foreach (var toRemoveListener in this.removeEventListeners) - { - this.EventListeners.Remove(toRemoveListener); - - // If we are disabling an ReceiveEvent listener, check if we should disable the hook. - if (toRemoveListener is { EventType: AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent }) - { - // Get the ReceiveEvent Listener for this addon - if (this.ReceiveEventListeners.FirstOrDefault(listener => listener.AddonNames.Contains(toRemoveListener.AddonName)) is { } receiveEventListener) - { - // If there are no other listeners listening for this event, disable the hook. - if (!this.EventListeners.Any(listener => listener.AddonName.Contains(toRemoveListener.AddonName) && listener.EventType is AddonEvent.PreReceiveEvent or AddonEvent.PostReceiveEvent)) - { - receiveEventListener.Hook?.Disable(); - } - } - } - } - - this.removeEventListeners.Clear(); - } - } - private void RegisterReceiveEventHook(AtkUnitBase* addon) { // Hook the addon's ReceiveEvent function here, but only enable the hook if we have an active listener. From 5998fc687f00d75379df77011266e76600324f40 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 17 Dec 2023 05:05:13 +0900 Subject: [PATCH 377/585] Fix DataShare race condition, and add debug features (#1573) --- .../Windows/Data/Widgets/DataShareWidget.cs | 311 +++++++++++++++++- Dalamud/Plugin/Ipc/Internal/CallGate.cs | 50 ++- .../Plugin/Ipc/Internal/CallGateChannel.cs | 71 +++- Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs | 20 +- .../Plugin/Ipc/Internal/CallGatePubSubBase.cs | 8 +- Dalamud/Plugin/Ipc/Internal/DataCache.cs | 84 ++++- Dalamud/Plugin/Ipc/Internal/DataShare.cs | 128 +++---- 7 files changed, 545 insertions(+), 127 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 570b63332..92f340a7b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -1,19 +1,44 @@ -using Dalamud.Interface.Utility; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; +using System.Reflection; +using System.Text; + +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Ipc.Internal; + using ImGuiNET; +using Newtonsoft.Json; + +using Formatting = Newtonsoft.Json.Formatting; + namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying plugin data share modules. /// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] internal class DataShareWidget : IDataWindowWidget { + private const ImGuiTabItemFlags NoCloseButton = (ImGuiTabItemFlags)(1 << 20); + + private readonly List<(string Name, byte[]? Data)> dataView = new(); + private int nextTab = -1; + private IReadOnlyDictionary? gates; + private List? gatesSorted; + /// public string[]? CommandShortcuts { get; init; } = { "datashare" }; - + /// - public string DisplayName { get; init; } = "Data Share"; + public string DisplayName { get; init; } = "Data Share & Call Gate"; /// public bool Ready { get; set; } @@ -25,28 +50,290 @@ internal class DataShareWidget : IDataWindowWidget } /// - public void Draw() + public unsafe void Draw() { - if (!ImGui.BeginTable("###DataShareTable", 4, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg)) + using var tabbar = ImRaii.TabBar("##tabbar"); + if (!tabbar.Success) + return; + + var d = true; + using (var tabitem = ImRaii.TabItem( + "Data Share##tabbar-datashare", + ref d, + NoCloseButton | (this.nextTab == 0 ? ImGuiTabItemFlags.SetSelected : 0))) + { + if (tabitem.Success) + this.DrawDataShare(); + } + + using (var tabitem = ImRaii.TabItem( + "Call Gate##tabbar-callgate", + ref d, + NoCloseButton | (this.nextTab == 1 ? ImGuiTabItemFlags.SetSelected : 0))) + { + if (tabitem.Success) + this.DrawCallGate(); + } + + for (var i = 0; i < this.dataView.Count; i++) + { + using var idpush = ImRaii.PushId($"##tabbar-data-{i}"); + var (name, data) = this.dataView[i]; + d = true; + using var tabitem = ImRaii.TabItem( + name, + ref d, + this.nextTab == 2 + i ? ImGuiTabItemFlags.SetSelected : 0); + if (!d) + this.dataView.RemoveAt(i--); + if (!tabitem.Success) + continue; + + if (ImGui.Button("Refresh")) + data = null; + + if (data is null) + { + try + { + var dataShare = Service.Get(); + var data2 = dataShare.GetData(name); + try + { + data = Encoding.UTF8.GetBytes( + JsonConvert.SerializeObject( + data2, + Formatting.Indented, + new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All })); + } + finally + { + dataShare.RelinquishData(name); + } + } + catch (Exception e) + { + data = Encoding.UTF8.GetBytes(e.ToString()); + } + + this.dataView[i] = (name, data); + } + + ImGui.SameLine(); + if (ImGui.Button("Copy")) + { + fixed (byte* pData = data) + ImGuiNative.igSetClipboardText(pData); + } + + fixed (byte* pLabel = "text"u8) + fixed (byte* pData = data) + { + ImGuiNative.igInputTextMultiline( + pLabel, + pData, + (uint)data.Length, + ImGui.GetContentRegionAvail(), + ImGuiInputTextFlags.ReadOnly, + null, + null); + } + } + + this.nextTab = -1; + } + + private static string ReprMethod(MethodInfo? mi, bool withParams) + { + if (mi is null) + return "-"; + + var sb = new StringBuilder(); + sb.Append(ReprType(mi.DeclaringType)) + .Append("::") + .Append(mi.Name); + if (!withParams) + return sb.ToString(); + sb.Append('('); + var parfirst = true; + foreach (var par in mi.GetParameters()) + { + if (!parfirst) + sb.Append(", "); + else + parfirst = false; + sb.AppendLine() + .Append('\t') + .Append(ReprType(par.ParameterType)) + .Append(' ') + .Append(par.Name); + } + + if (!parfirst) + sb.AppendLine(); + sb.Append(')'); + if (mi.ReturnType != typeof(void)) + sb.Append(" -> ").Append(ReprType(mi.ReturnType)); + return sb.ToString(); + + static string WithoutGeneric(string s) + { + var i = s.IndexOf('`'); + return i != -1 ? s[..i] : s; + } + + static string ReprType(Type? t) => + t switch + { + null => "null", + _ when t == typeof(string) => "string", + _ when t == typeof(object) => "object", + _ when t == typeof(void) => "void", + _ when t == typeof(decimal) => "decimal", + _ when t == typeof(bool) => "bool", + _ when t == typeof(double) => "double", + _ when t == typeof(float) => "float", + _ when t == typeof(char) => "char", + _ when t == typeof(ulong) => "ulong", + _ when t == typeof(long) => "long", + _ when t == typeof(uint) => "uint", + _ when t == typeof(int) => "int", + _ when t == typeof(ushort) => "ushort", + _ when t == typeof(short) => "short", + _ when t == typeof(byte) => "byte", + _ when t == typeof(sbyte) => "sbyte", + _ when t == typeof(nint) => "nint", + _ when t == typeof(nuint) => "nuint", + _ when t.IsArray && t.HasElementType => ReprType(t.GetElementType()) + "[]", + _ when t.IsPointer && t.HasElementType => ReprType(t.GetElementType()) + "*", + _ when t.IsGenericTypeDefinition => + t.Assembly == typeof(object).Assembly + ? t.Name + "<>" + : (t.FullName ?? t.Name) + "<>", + _ when t.IsGenericType && t.GetGenericTypeDefinition() == typeof(Nullable<>) => + ReprType(t.GetGenericArguments()[0]) + "?", + _ when t.IsGenericType => + WithoutGeneric(ReprType(t.GetGenericTypeDefinition())) + + "<" + string.Join(", ", t.GetGenericArguments().Select(ReprType)) + ">", + _ => t.Assembly == typeof(object).Assembly ? t.Name : t.FullName ?? t.Name, + }; + } + + private void DrawTextCell(string s, Func? tooltip = null, bool framepad = false) + { + ImGui.TableNextColumn(); + var offset = ImGui.GetCursorScreenPos() + new Vector2(0, framepad ? ImGui.GetStyle().FramePadding.Y : 0); + if (framepad) + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(s); + if (ImGui.IsItemHovered()) + { + ImGui.SetNextWindowPos(offset - ImGui.GetStyle().WindowPadding); + var vp = ImGui.GetWindowViewport(); + var wrx = (vp.WorkPos.X + vp.WorkSize.X) - offset.X; + ImGui.SetNextWindowSizeConstraints(Vector2.One, new(wrx, float.MaxValue)); + using (ImRaii.Tooltip()) + { + ImGui.PushTextWrapPos(wrx); + ImGui.TextWrapped((tooltip?.Invoke() ?? s).Replace("%", "%%")); + ImGui.PopTextWrapPos(); + } + } + + if (ImGui.IsItemClicked()) + { + ImGui.SetClipboardText(tooltip?.Invoke() ?? s); + Service.Get().AddNotification( + $"Copied {ImGui.TableGetColumnName()} to clipboard.", + this.DisplayName, + NotificationType.Success); + } + } + + private void DrawCallGate() + { + var callGate = Service.Get(); + if (ImGui.Button("Purge empty call gates")) + callGate.PurgeEmptyGates(); + + using var table = ImRaii.Table("##callgate-table", 5); + ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.DefaultSort); + ImGui.TableSetupColumn("Action"); + ImGui.TableSetupColumn("Func"); + ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Subscriber"); + ImGui.TableHeadersRow(); + + var gates2 = callGate.Gates; + if (!ReferenceEquals(gates2, this.gates) || this.gatesSorted is null) + { + this.gatesSorted = (this.gates = gates2).Values.ToList(); + this.gatesSorted.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + } + + foreach (var item in this.gatesSorted) + { + var subs = item.Subscriptions; + for (var i = 0; i < subs.Count || i == 0; i++) + { + ImGui.TableNextRow(); + this.DrawTextCell(item.Name); + this.DrawTextCell( + ReprMethod(item.Action?.Method, false), + () => ReprMethod(item.Action?.Method, true)); + this.DrawTextCell( + ReprMethod(item.Func?.Method, false), + () => ReprMethod(item.Func?.Method, true)); + if (subs.Count == 0) + { + this.DrawTextCell("0"); + continue; + } + + this.DrawTextCell($"{i + 1}/{subs.Count}"); + this.DrawTextCell($"{subs[i].Method.DeclaringType}::{subs[i].Method.Name}"); + } + } + } + + private void DrawDataShare() + { + if (!ImGui.BeginTable("###DataShareTable", 5, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg)) return; try { ImGui.TableSetupColumn("Shared Tag"); + ImGui.TableSetupColumn("Show"); ImGui.TableSetupColumn("Creator Assembly"); ImGui.TableSetupColumn("#", ImGuiTableColumnFlags.WidthFixed, 30 * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("Consumers"); ImGui.TableHeadersRow(); foreach (var share in Service.Get().GetAllShares()) { + ImGui.TableNextRow(); + this.DrawTextCell(share.Tag, null, true); + ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.Tag); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.CreatorAssembly); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(share.Users.Length.ToString()); - ImGui.TableNextColumn(); - ImGui.TextUnformatted(string.Join(", ", share.Users)); + if (ImGui.Button($"Show##datasharetable-show-{share.Tag}")) + { + var index = 0; + for (; index < this.dataView.Count; index++) + { + if (this.dataView[index].Name == share.Tag) + break; + } + + if (index == this.dataView.Count) + this.dataView.Add((share.Tag, null)); + else + this.dataView[index] = (share.Tag, null); + this.nextTab = 2 + index; + } + + this.DrawTextCell(share.CreatorAssembly, null, true); + this.DrawTextCell(share.Users.Length.ToString(), null, true); + this.DrawTextCell(string.Join(", ", share.Users), null, true); } } finally diff --git a/Dalamud/Plugin/Ipc/Internal/CallGate.cs b/Dalamud/Plugin/Ipc/Internal/CallGate.cs index 7d0f90cb6..fef4b97d0 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGate.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGate.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Collections.Immutable; namespace Dalamud.Plugin.Ipc.Internal; @@ -10,11 +11,28 @@ internal class CallGate : IServiceType { private readonly Dictionary gates = new(); + private ImmutableDictionary? gatesCopy; + [ServiceManager.ServiceConstructor] private CallGate() { } + /// + /// Gets the thread-safe view of the registered gates. + /// + public IReadOnlyDictionary Gates + { + get + { + var copy = this.gatesCopy; + if (copy is not null) + return copy; + lock (this.gates) + return this.gatesCopy ??= this.gates.ToImmutableDictionary(x => x.Key, x => x.Value); + } + } + /// /// Gets the provider associated with the specified name. /// @@ -22,8 +40,34 @@ internal class CallGate : IServiceType /// A CallGate registered under the given name. public CallGateChannel GetOrCreateChannel(string name) { - if (!this.gates.TryGetValue(name, out var gate)) - gate = this.gates[name] = new CallGateChannel(name); - return gate; + lock (this.gates) + { + if (!this.gates.TryGetValue(name, out var gate)) + { + gate = this.gates[name] = new(name); + this.gatesCopy = null; + } + + return gate; + } + } + + /// + /// Remove empty gates from . + /// + public void PurgeEmptyGates() + { + lock (this.gates) + { + var changed = false; + foreach (var (k, v) in this.Gates) + { + if (v.IsEmpty) + changed |= this.gates.Remove(k); + } + + if (changed) + this.gatesCopy = null; + } } } diff --git a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs index 2e2c7249e..54adf2163 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGateChannel.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Reflection; @@ -14,6 +14,17 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal class CallGateChannel { + /// + /// The actual storage. + /// + private readonly HashSet subscriptions = new(); + + /// + /// A copy of the actual storage, that will be cleared and populated depending on changes made to + /// . + /// + private ImmutableList? subscriptionsCopy; + /// /// Initializes a new instance of the class. /// @@ -31,17 +42,52 @@ internal class CallGateChannel /// /// Gets a list of delegate subscriptions for when SendMessage is called. /// - public List Subscriptions { get; } = new(); + public IReadOnlyList Subscriptions + { + get + { + var copy = this.subscriptionsCopy; + if (copy is not null) + return copy; + lock (this.subscriptions) + return this.subscriptionsCopy ??= this.subscriptions.ToImmutableList(); + } + } /// /// Gets or sets an action for when InvokeAction is called. /// - public Delegate Action { get; set; } + public Delegate? Action { get; set; } /// /// Gets or sets a func for when InvokeFunc is called. /// - public Delegate Func { get; set; } + public Delegate? Func { get; set; } + + /// + /// Gets a value indicating whether this is not being used. + /// + public bool IsEmpty => this.Action is null && this.Func is null && this.Subscriptions.Count == 0; + + /// + internal void Subscribe(Delegate action) + { + lock (this.subscriptions) + { + this.subscriptionsCopy = null; + this.subscriptions.Add(action); + } + } + + /// + internal void Unsubscribe(Delegate action) + { + lock (this.subscriptions) + { + this.subscriptionsCopy = null; + this.subscriptions.Remove(action); + } + } /// /// Invoke all actions that have subscribed to this IPC. @@ -49,9 +95,6 @@ internal class CallGateChannel /// Message arguments. internal void SendMessage(object?[]? args) { - if (this.Subscriptions.Count == 0) - return; - foreach (var subscription in this.Subscriptions) { var methodInfo = subscription.GetMethodInfo(); @@ -105,7 +148,14 @@ internal class CallGateChannel var paramTypes = methodInfo.GetParameters() .Select(pi => pi.ParameterType).ToArray(); - if (args?.Length != paramTypes.Length) + if (args is null) + { + if (paramTypes.Length == 0) + return; + throw new IpcLengthMismatchError(this.Name, 0, paramTypes.Length); + } + + if (args.Length != paramTypes.Length) throw new IpcLengthMismatchError(this.Name, args.Length, paramTypes.Length); for (var i = 0; i < args.Length; i++) @@ -137,7 +187,7 @@ internal class CallGateChannel } } - private IEnumerable GenerateTypes(Type type) + private IEnumerable GenerateTypes(Type? type) { while (type != null && type != typeof(object)) { @@ -148,6 +198,9 @@ internal class CallGateChannel private object? ConvertObject(object? obj, Type type) { + if (obj is null) + return null; + var json = JsonConvert.SerializeObject(obj); try diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs index 39d5b9f4d..cc54a563b 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSub.cs @@ -1,5 +1,3 @@ -using System; - #pragma warning disable SA1402 // File may only contain a single type namespace Dalamud.Plugin.Ipc.Internal; @@ -37,7 +35,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider base.InvokeAction(); - /// + /// public TRet InvokeFunc() => this.InvokeFunc(); } @@ -75,7 +73,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvider< public void InvokeAction(T1 arg1) => base.InvokeAction(arg1); - /// + /// public TRet InvokeFunc(T1 arg1) => this.InvokeFunc(arg1); } @@ -113,7 +111,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateProvi public void InvokeAction(T1 arg1, T2 arg2) => base.InvokeAction(arg1, arg2); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2) => this.InvokeFunc(arg1, arg2); } @@ -151,7 +149,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallGateP public void InvokeAction(T1 arg1, T2 arg2, T3 arg3) => base.InvokeAction(arg1, arg2, arg3); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3) => this.InvokeFunc(arg1, arg2, arg3); } @@ -189,7 +187,7 @@ internal class CallGatePubSub : CallGatePubSubBase, ICallG public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => base.InvokeAction(arg1, arg2, arg3, arg4); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4) => this.InvokeFunc(arg1, arg2, arg3, arg4); } @@ -227,7 +225,7 @@ internal class CallGatePubSub : CallGatePubSubBase, IC public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5); } @@ -265,7 +263,7 @@ internal class CallGatePubSub : CallGatePubSubBase public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5, arg6); } @@ -303,7 +301,7 @@ internal class CallGatePubSub : CallGatePubSub public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5, arg6, arg7); } @@ -341,7 +339,7 @@ internal class CallGatePubSub : CallGatePu public void InvokeAction(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => base.InvokeAction(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); - /// + /// public TRet InvokeFunc(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8) => this.InvokeFunc(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8); } diff --git a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs index 40c0c4a59..b6a4e8a61 100644 --- a/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs +++ b/Dalamud/Plugin/Ipc/Internal/CallGatePubSubBase.cs @@ -1,5 +1,3 @@ -using System; - using Dalamud.Plugin.Ipc.Exceptions; namespace Dalamud.Plugin.Ipc.Internal; @@ -13,7 +11,7 @@ internal abstract class CallGatePubSubBase /// Initializes a new instance of the class. /// /// The name of the IPC registration. - public CallGatePubSubBase(string name) + protected CallGatePubSubBase(string name) { this.Channel = Service.Get().GetOrCreateChannel(name); } @@ -54,14 +52,14 @@ internal abstract class CallGatePubSubBase /// /// Action to subscribe. private protected void Subscribe(Delegate action) - => this.Channel.Subscriptions.Add(action); + => this.Channel.Subscribe(action); /// /// Unsubscribe an expression from this registration. /// /// Action to unsubscribe. private protected void Unsubscribe(Delegate action) - => this.Channel.Subscriptions.Remove(action); + => this.Channel.Unsubscribe(action); /// /// Invoke an action registered for inter-plugin communication. diff --git a/Dalamud/Plugin/Ipc/Internal/DataCache.cs b/Dalamud/Plugin/Ipc/Internal/DataCache.cs index c357f77c2..38cea4866 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataCache.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataCache.cs @@ -1,5 +1,10 @@ -using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; + +using Dalamud.Plugin.Ipc.Exceptions; + +using Serilog; namespace Dalamud.Plugin.Ipc.Internal; @@ -8,10 +13,14 @@ namespace Dalamud.Plugin.Ipc.Internal; /// internal readonly struct DataCache { + /// Name of the data. + internal readonly string Tag; + /// The assembly name of the initial creator. internal readonly string CreatorAssemblyName; /// A not-necessarily distinct list of current users. + /// Also used as a reference count tracker. internal readonly List UserAssemblyNames; /// The type the data was registered as. @@ -23,14 +32,83 @@ internal readonly struct DataCache /// /// Initializes a new instance of the struct. /// + /// Name of the data. /// The assembly name of the initial creator. /// A reference to data. /// The type of the data. - public DataCache(string creatorAssemblyName, object? data, Type type) + public DataCache(string tag, string creatorAssemblyName, object? data, Type type) { + this.Tag = tag; this.CreatorAssemblyName = creatorAssemblyName; - this.UserAssemblyNames = new List { creatorAssemblyName }; + this.UserAssemblyNames = new(); this.Data = data; this.Type = type; } + + /// + /// Creates a new instance of the struct, using the given data generator function. + /// + /// The name for the data cache. + /// The assembly name of the initial creator. + /// The function that generates the data if it does not already exist. + /// The type of the stored data - needs to be a reference type that is shared through Dalamud itself, not loaded by the plugin. + /// The new instance of . + public static DataCache From(string tag, string creatorAssemblyName, Func dataGenerator) + where T : class + { + try + { + var result = new DataCache(tag, creatorAssemblyName, dataGenerator.Invoke(), typeof(T)); + Log.Verbose( + "[{who}] Created new data for [{Tag:l}] for creator {Creator:l}.", + nameof(DataShare), + tag, + creatorAssemblyName); + return result; + } + catch (Exception e) + { + throw ExceptionDispatchInfo.SetCurrentStackTrace( + new DataCacheCreationError(tag, creatorAssemblyName, typeof(T), e)); + } + } + + /// + /// Attempts to fetch the data. + /// + /// The name of the caller assembly. + /// The value, if succeeded. + /// The exception, if failed. + /// Desired type of the data. + /// true on success. + public bool TryGetData( + string callerName, + [NotNullWhen(true)] out T? value, + [NotNullWhen(false)] out Exception? ex) + where T : class + { + switch (this.Data) + { + case null: + value = null; + ex = ExceptionDispatchInfo.SetCurrentStackTrace(new DataCacheValueNullError(this.Tag, this.Type)); + return false; + + case T data: + value = data; + ex = null; + + // Register the access history + lock (this.UserAssemblyNames) + this.UserAssemblyNames.Add(callerName); + + return true; + + default: + value = null; + ex = ExceptionDispatchInfo.SetCurrentStackTrace( + new DataCacheTypeMismatchError(this.Tag, this.CreatorAssemblyName, typeof(T), this.Type)); + return false; + } + } } diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index a3e314b80..b122f481d 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -1,9 +1,7 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reflection; using Dalamud.Plugin.Ipc.Exceptions; using Serilog; @@ -16,7 +14,11 @@ namespace Dalamud.Plugin.Ipc.Internal; [ServiceManager.BlockingEarlyLoadedService] internal class DataShare : IServiceType { - private readonly Dictionary caches = new(); + /// + /// Dictionary of cached values. Note that is being used, as it does its own locking, + /// effectively preventing calling the data generator multiple times concurrently. + /// + private readonly Dictionary> caches = new(); [ServiceManager.ServiceConstructor] private DataShare() @@ -39,38 +41,15 @@ internal class DataShare : IServiceType where T : class { var callerName = GetCallerName(); + + Lazy cacheLazy; lock (this.caches) { - if (this.caches.TryGetValue(tag, out var cache)) - { - if (!cache.Type.IsAssignableTo(typeof(T))) - { - throw new DataCacheTypeMismatchError(tag, cache.CreatorAssemblyName, typeof(T), cache.Type); - } - - cache.UserAssemblyNames.Add(callerName); - return cache.Data as T ?? throw new DataCacheValueNullError(tag, cache.Type); - } - - try - { - var obj = dataGenerator.Invoke(); - if (obj == null) - { - throw new Exception("Returned data was null."); - } - - cache = new DataCache(callerName, obj, typeof(T)); - this.caches[tag] = cache; - - Log.Verbose("[DataShare] Created new data for [{Tag:l}] for creator {Creator:l}.", tag, callerName); - return obj; - } - catch (Exception e) - { - throw new DataCacheCreationError(tag, callerName, typeof(T), e); - } + if (!this.caches.TryGetValue(tag, out cacheLazy)) + this.caches[tag] = cacheLazy = new(() => DataCache.From(tag, callerName, dataGenerator)); } + + return cacheLazy.Value.TryGetData(callerName, out var value, out var ex) ? value : throw ex; } /// @@ -80,34 +59,36 @@ internal class DataShare : IServiceType /// The name for the data cache. public void RelinquishData(string tag) { + DataCache cache; lock (this.caches) { - if (!this.caches.TryGetValue(tag, out var cache)) - { + if (!this.caches.TryGetValue(tag, out var cacheLazy)) return; - } var callerName = GetCallerName(); - lock (this.caches) - { - if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) - { - return; - } - if (this.caches.Remove(tag)) - { - if (cache.Data is IDisposable disposable) - { - disposable.Dispose(); - Log.Verbose("[DataShare] Disposed [{Tag:l}] after it was removed from all shares.", tag); - } - else - { - Log.Verbose("[DataShare] Removed [{Tag:l}] from all shares.", tag); - } - } + cache = cacheLazy.Value; + if (!cache.UserAssemblyNames.Remove(callerName) || cache.UserAssemblyNames.Count > 0) + return; + if (!this.caches.Remove(tag)) + return; + } + + if (cache.Data is IDisposable disposable) + { + try + { + disposable.Dispose(); + Log.Verbose("[DataShare] Disposed [{Tag:l}] after it was removed from all shares.", tag); } + catch (Exception e) + { + Log.Error(e, "[DataShare] Failed to dispose [{Tag:l}] after it was removed from all shares.", tag); + } + } + else + { + Log.Verbose("[DataShare] Removed [{Tag:l}] from all shares.", tag); } } @@ -123,23 +104,14 @@ internal class DataShare : IServiceType where T : class { data = null; + Lazy cacheLazy; lock (this.caches) { - if (!this.caches.TryGetValue(tag, out var cache) || !cache.Type.IsAssignableTo(typeof(T))) - { + if (!this.caches.TryGetValue(tag, out cacheLazy)) return false; - } - - var callerName = GetCallerName(); - data = cache.Data as T; - if (data == null) - { - return false; - } - - cache.UserAssemblyNames.Add(callerName); - return true; } + + return cacheLazy.Value.TryGetData(GetCallerName(), out data, out _); } /// @@ -155,27 +127,14 @@ internal class DataShare : IServiceType public T GetData(string tag) where T : class { + Lazy cacheLazy; lock (this.caches) { - if (!this.caches.TryGetValue(tag, out var cache)) - { + if (!this.caches.TryGetValue(tag, out cacheLazy)) throw new KeyNotFoundException($"The data cache [{tag}] is not registered."); - } - - var callerName = Assembly.GetCallingAssembly().GetName().Name ?? string.Empty; - if (!cache.Type.IsAssignableTo(typeof(T))) - { - throw new DataCacheTypeMismatchError(tag, callerName, typeof(T), cache.Type); - } - - if (cache.Data is not T data) - { - throw new DataCacheValueNullError(tag, typeof(T)); - } - - cache.UserAssemblyNames.Add(callerName); - return data; } + + return cacheLazy.Value.TryGetData(GetCallerName(), out var value, out var ex) ? value : throw ex; } /// @@ -186,7 +145,8 @@ internal class DataShare : IServiceType { lock (this.caches) { - return this.caches.Select(kvp => (kvp.Key, kvp.Value.CreatorAssemblyName, kvp.Value.UserAssemblyNames.ToArray())); + return this.caches.Select( + kvp => (kvp.Key, kvp.Value.Value.CreatorAssemblyName, kvp.Value.Value.UserAssemblyNames.ToArray())); } } From 280a9d6b05102cc90c55a3cd6f65ba0d3d69ac41 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 16 Dec 2023 21:05:49 +0100 Subject: [PATCH 378/585] build: 9.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 d31f79e0c..a870bee17 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.13 + 9.0.0.14 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From b6d88f798a600c5033887f949b2a3d419e12afa9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 7 Dec 2023 22:41:10 +0900 Subject: [PATCH 379/585] Make CJK imes work better --- Dalamud/Dalamud.cs | 3 +- Dalamud/Game/Gui/Internal/DalamudIME.cs | 301 ---------- Dalamud/Interface/Internal/DalamudIme.cs | 521 ++++++++++++++++++ .../Interface/Internal/DalamudInterface.cs | 10 +- .../Interface/Internal/InterfaceManager.cs | 59 +- .../Internal/Windows/DalamudImeWindow.cs | 223 ++++++++ .../Interface/Internal/Windows/IMEWindow.cs | 120 ---- .../Interface/Internal/WndProcHookManager.cs | 273 +++++++++ Dalamud/Interface/Utility/ImGuiHelpers.cs | 20 + 9 files changed, 1064 insertions(+), 466 deletions(-) delete mode 100644 Dalamud/Game/Gui/Internal/DalamudIME.cs create mode 100644 Dalamud/Interface/Internal/DalamudIme.cs create mode 100644 Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs delete mode 100644 Dalamud/Interface/Internal/Windows/IMEWindow.cs create mode 100644 Dalamud/Interface/Internal/WndProcHookManager.cs diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 9896b87a6..4ab617d0a 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; -using Dalamud.Game.Gui.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Storage; @@ -178,7 +177,7 @@ internal sealed class Dalamud : IServiceType // this must be done before unloading interface manager, in order to do rebuild // the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game // will not receive any windows messages - Service.GetNullable()?.Dispose(); + Service.GetNullable()?.Dispose(); // this must be done before unloading plugins, or it can cause a race condition // due to rendering happening on another thread, where a plugin might receive diff --git a/Dalamud/Game/Gui/Internal/DalamudIME.cs b/Dalamud/Game/Gui/Internal/DalamudIME.cs deleted file mode 100644 index a9f6991ae..000000000 --- a/Dalamud/Game/Gui/Internal/DalamudIME.cs +++ /dev/null @@ -1,301 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; - -using Dalamud.Hooking; -using Dalamud.Interface.Internal; -using Dalamud.Logging.Internal; -using ImGuiNET; -using PInvoke; - -using static Dalamud.NativeFunctions; - -namespace Dalamud.Game.Gui.Internal; - -/// -/// This class handles IME for non-English users. -/// -[ServiceManager.EarlyLoadedService] -internal unsafe class DalamudIME : IDisposable, IServiceType -{ - private static readonly ModuleLog Log = new("IME"); - - private AsmHook imguiTextInputCursorHook; - private Vector2* cursorPos; - - [ServiceManager.ServiceConstructor] - private DalamudIME() - { - } - - /// - /// Gets a value indicating whether the module is enabled. - /// - internal bool IsEnabled { get; private set; } - - /// - /// Gets the index of the first imm candidate in relation to the full list. - /// - internal CandidateList ImmCandNative { get; private set; } = default; - - /// - /// Gets the imm candidates. - /// - internal List ImmCand { get; private set; } = new(); - - /// - /// Gets the selected imm component. - /// - internal string ImmComp { get; private set; } = string.Empty; - - /// - public void Dispose() - { - this.imguiTextInputCursorHook?.Dispose(); - Marshal.FreeHGlobal((IntPtr)this.cursorPos); - } - - /// - /// Processes window messages. - /// - /// Handle of the window. - /// Type of window message. - /// wParam or the pointer to it. - /// lParam or the pointer to it. - /// Return value, if not doing further processing. - public unsafe IntPtr? ProcessWndProcW(IntPtr hWnd, User32.WindowMessage msg, void* wParamPtr, void* lParamPtr) - { - try - { - if (ImGui.GetCurrentContext() != IntPtr.Zero && ImGui.GetIO().WantTextInput) - { - var io = ImGui.GetIO(); - var wmsg = (WindowsMessage)msg; - long wParam = (long)wParamPtr, lParam = (long)lParamPtr; - try - { - wParam = Marshal.ReadInt32((IntPtr)wParamPtr); - } - catch - { - // ignored - } - - try - { - lParam = Marshal.ReadInt32((IntPtr)lParamPtr); - } - catch - { - // ignored - } - - switch (wmsg) - { - case WindowsMessage.WM_IME_NOTIFY: - switch ((IMECommand)(IntPtr)wParam) - { - case IMECommand.ChangeCandidate: - this.ToggleWindow(true); - this.LoadCand(hWnd); - break; - case IMECommand.OpenCandidate: - this.ToggleWindow(true); - this.ImmCandNative = default; - // this.ImmCand.Clear(); - break; - - case IMECommand.CloseCandidate: - this.ToggleWindow(false); - this.ImmCandNative = default; - // this.ImmCand.Clear(); - break; - - default: - break; - } - - break; - case WindowsMessage.WM_IME_COMPOSITION: - if (((long)(IMEComposition.CompStr | IMEComposition.CompAttr | IMEComposition.CompClause | - IMEComposition.CompReadAttr | IMEComposition.CompReadClause | IMEComposition.CompReadStr) & (long)(IntPtr)lParam) > 0) - { - var hIMC = ImmGetContext(hWnd); - if (hIMC == IntPtr.Zero) - return IntPtr.Zero; - - var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, IntPtr.Zero, 0); - var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize); - ImmGetCompositionStringW(hIMC, IMEComposition.CompStr, unmanagedPointer, (uint)dwSize); - - var bytes = new byte[dwSize]; - Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize); - Marshal.FreeHGlobal(unmanagedPointer); - - var lpstr = Encoding.Unicode.GetString(bytes); - this.ImmComp = lpstr; - if (lpstr == string.Empty) - { - this.ToggleWindow(false); - } - else - { - this.LoadCand(hWnd); - } - } - - if (((long)(IntPtr)lParam & (long)IMEComposition.ResultStr) > 0) - { - var hIMC = ImmGetContext(hWnd); - if (hIMC == IntPtr.Zero) - return IntPtr.Zero; - - var dwSize = ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, IntPtr.Zero, 0); - var unmanagedPointer = Marshal.AllocHGlobal((int)dwSize); - ImmGetCompositionStringW(hIMC, IMEComposition.ResultStr, unmanagedPointer, (uint)dwSize); - - var bytes = new byte[dwSize]; - Marshal.Copy(unmanagedPointer, bytes, 0, (int)dwSize); - Marshal.FreeHGlobal(unmanagedPointer); - - var lpstr = Encoding.Unicode.GetString(bytes); - io.AddInputCharactersUTF8(lpstr); - - this.ImmComp = string.Empty; - this.ImmCandNative = default; - this.ImmCand.Clear(); - this.ToggleWindow(false); - } - - break; - - default: - break; - } - } - } - catch (Exception ex) - { - Log.Error(ex, "Prevented a crash in an IME hook"); - } - - return null; - } - - /// - /// Get the position of the cursor. - /// - /// The position of the cursor. - internal Vector2 GetCursorPos() - { - return new Vector2(this.cursorPos->X, this.cursorPos->Y); - } - - private unsafe void LoadCand(IntPtr hWnd) - { - if (hWnd == IntPtr.Zero) - return; - - var hImc = ImmGetContext(hWnd); - if (hImc == IntPtr.Zero) - return; - - var size = ImmGetCandidateListW(hImc, 0, IntPtr.Zero, 0); - if (size == 0) - return; - - var candlistPtr = Marshal.AllocHGlobal((int)size); - size = ImmGetCandidateListW(hImc, 0, candlistPtr, (uint)size); - - var candlist = this.ImmCandNative = Marshal.PtrToStructure(candlistPtr); - var pageSize = candlist.PageSize; - var candCount = candlist.Count; - - if (pageSize > 0 && candCount > 1) - { - var dwOffsets = new int[candCount]; - for (var i = 0; i < candCount; i++) - { - dwOffsets[i] = Marshal.ReadInt32(candlistPtr + ((i + 6) * sizeof(int))); - } - - var pageStart = candlist.PageStart; - - var cand = new string[pageSize]; - this.ImmCand.Clear(); - - for (var i = 0; i < pageSize; i++) - { - var offStart = dwOffsets[i + pageStart]; - var offEnd = i + pageStart + 1 < candCount ? dwOffsets[i + pageStart + 1] : size; - - var pStrStart = candlistPtr + (int)offStart; - var pStrEnd = candlistPtr + (int)offEnd; - - var len = (int)(pStrEnd.ToInt64() - pStrStart.ToInt64()); - if (len > 0) - { - var candBytes = new byte[len]; - Marshal.Copy(pStrStart, candBytes, 0, len); - - var candStr = Encoding.Unicode.GetString(candBytes); - cand[i] = candStr; - - this.ImmCand.Add(candStr); - } - } - - Marshal.FreeHGlobal(candlistPtr); - } - } - - [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] - private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) - { - try - { - var module = Process.GetCurrentProcess().Modules.Cast().First(m => m.ModuleName == "cimgui.dll"); - var scanner = new SigScanner(module); - var cursorDrawingPtr = scanner.ScanModule("F3 0F 11 75 ?? 0F 28 CF"); - Log.Debug($"Found cursorDrawingPtr at {cursorDrawingPtr:X}"); - - this.cursorPos = (Vector2*)Marshal.AllocHGlobal(sizeof(Vector2)); - this.cursorPos->X = 0f; - this.cursorPos->Y = 0f; - - var asm = new[] - { - "use64", - $"push rax", - $"mov rax, {(IntPtr)this.cursorPos + sizeof(float)}", - $"movss [rax],xmm7", - $"mov rax, {(IntPtr)this.cursorPos}", - $"movss [rax],xmm6", - $"pop rax", - }; - - Log.Debug($"Asm Code:\n{string.Join("\n", asm)}"); - this.imguiTextInputCursorHook = new AsmHook(cursorDrawingPtr, asm, "ImguiTextInputCursorHook"); - this.imguiTextInputCursorHook?.Enable(); - - this.IsEnabled = true; - Log.Information("Enabled!"); - } - catch (Exception ex) - { - Log.Information(ex, "Enable failed"); - } - } - - private void ToggleWindow(bool visible) - { - if (visible) - Service.GetNullable()?.OpenImeWindow(); - else - Service.GetNullable()?.CloseImeWindow(); - } -} diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs new file mode 100644 index 000000000..1fc70b0f6 --- /dev/null +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -0,0 +1,521 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +using Dalamud.Game.Text; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// +/// This class handles IME for non-English users. +/// +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class DalamudIme : IDisposable, IServiceType +{ + private static readonly ModuleLog Log = new("IME"); + + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; + + [ServiceManager.ServiceConstructor] + private DalamudIme() => this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData; + + /// + /// Finalizes an instance of the class. + /// + ~DalamudIme() => this.ReleaseUnmanagedResources(); + + private delegate void ImGuiSetPlatformImeDataDelegate(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data); + + /// + /// Gets a value indicating whether to display the cursor in input text. This also deals with blinking. + /// + internal static bool ShowCursorInInputText + { + get + { + if (!ImGui.GetIO().ConfigInputTextCursorBlink) + return true; + ref var textState = ref TextState; + if (textState.Id == 0 || (textState.Flags & ImGuiInputTextFlags.ReadOnly) != 0) + return true; + if (textState.CursorAnim <= 0) + return true; + return textState.CursorAnim % 1.2f <= 0.8f; + } + } + + /// + /// Gets the cursor position, in screen coordinates. + /// + internal Vector2 CursorPos { get; private set; } + + /// + /// Gets the associated viewport. + /// + internal ImGuiViewportPtr AssociatedViewport { get; private set; } + + /// + /// Gets the index of the first imm candidate in relation to the full list. + /// + internal CANDIDATELIST ImmCandNative { get; private set; } + + /// + /// Gets the imm candidates. + /// + internal List ImmCand { get; private set; } = new(); + + /// + /// Gets the selected imm component. + /// + internal string ImmComp { get; private set; } = string.Empty; + + /// + /// Gets the partial conversion from-range. + /// + internal int PartialConversionFrom { get; private set; } + + /// + /// Gets the partial conversion to-range. + /// + internal int PartialConversionTo { get; private set; } + + /// + /// Gets the cursor offset in the composition string. + /// + internal int CompositionCursorOffset { get; private set; } + + /// + /// Gets a value indicating whether to display partial conversion status. + /// + internal bool ShowPartialConversion => this.PartialConversionFrom != 0 || + this.PartialConversionTo != this.ImmComp.Length; + + /// + /// Gets the input mode icon from . + /// + internal string? InputModeIcon { get; private set; } + + private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588); + + /// + public void Dispose() + { + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + /// Processes window messages. + /// + /// The arguments. + public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs args) + { + if (!ImGuiHelpers.IsImGuiInitialized) + return; + + // Are we not the target of text input? + if (!ImGui.GetIO().WantTextInput) + return; + + var hImc = ImmGetContext(args.Hwnd); + if (hImc == nint.Zero) + return; + + try + { + var invalidTarget = TextState.Id == 0 || (TextState.Flags & ImGuiInputTextFlags.ReadOnly) != 0; + + switch (args.Message) + { + case WM.WM_IME_NOTIFY when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: + this.UpdateImeWindowStatus(hImc); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_STARTCOMPOSITION: + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_COMPOSITION: + if (invalidTarget) + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); + else + this.ReplaceCompositionString(hImc, (uint)args.LParam); + + // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_ENDCOMPOSITION: + // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_CONTROL: + // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_REQUEST: + // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressAndReturn(0); + break; + + case WM.WM_IME_SETCONTEXT: + // Hide candidate and composition windows. + args.LParam = (LPARAM)((nint)args.LParam & ~(ISC_SHOWUICOMPOSITIONWINDOW | 0xF)); + + // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + args.SuppressWithDefault(); + break; + + case WM.WM_IME_NOTIFY: + // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); + break; + } + + this.UpdateInputLanguage(hImc); + } + finally + { + ImmReleaseContext(args.Hwnd, hImc); + } + } + + private static string ImmGetCompositionString(HIMC hImc, uint comp) + { + var numBytes = ImmGetCompositionStringW(hImc, comp, null, 0); + if (numBytes == 0) + return string.Empty; + + var data = stackalloc char[numBytes / 2]; + _ = ImmGetCompositionStringW(hImc, comp, data, (uint)numBytes); + return new(data, 0, numBytes / 2); + } + + private void ReleaseUnmanagedResources() => ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + + private void UpdateInputLanguage(HIMC hImc) + { + uint conv, sent; + ImmGetConversionStatus(hImc, &conv, &sent); + var lang = GetKeyboardLayout(0); + var open = ImmGetOpenStatus(hImc) != false; + + // Log.Verbose($"{nameof(this.UpdateInputLanguage)}: conv={conv:X} sent={sent:X} open={open} lang={lang:X}"); + + var native = (conv & 1) != 0; + var katakana = (conv & 2) != 0; + var fullwidth = (conv & 8) != 0; + switch (lang & 0x3F) + { + case LANG.LANG_KOREAN: + if (native) + this.InputModeIcon = "\uE025"; + else if (fullwidth) + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + else + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + break; + + case LANG.LANG_JAPANESE: + // wtf + // see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0 + if (open && native && katakana && fullwidth) + this.InputModeIcon = $"{(char)SeIconChar.ImeKatakana}"; + else if (open && native && katakana) + this.InputModeIcon = $"{(char)SeIconChar.ImeKatakanaHalfWidth}"; + else if (open && native) + this.InputModeIcon = $"{(char)SeIconChar.ImeHiragana}"; + else if (open && fullwidth) + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + else + this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + break; + + case LANG.LANG_CHINESE: + // TODO: does Chinese IME also need "open" check? + if (native) + this.InputModeIcon = "\uE026"; + else + this.InputModeIcon = "\uE027"; + break; + + default: + this.InputModeIcon = null; + break; + } + + this.UpdateImeWindowStatus(hImc); + } + + private void ReplaceCompositionString(HIMC hImc, uint comp) + { + ref var textState = ref TextState; + var finalCommit = (comp & GCS.GCS_RESULTSTR) != 0; + + ref var s = ref textState.Stb.SelectStart; + ref var e = ref textState.Stb.SelectEnd; + ref var c = ref textState.Stb.Cursor; + s = Math.Clamp(s, 0, textState.CurLenW); + e = Math.Clamp(e, 0, textState.CurLenW); + c = Math.Clamp(c, 0, textState.CurLenW); + if (s == e) + s = e = c; + if (s > e) + (s, e) = (e, s); + + var newString = finalCommit + ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) + : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); + + if (s != e) + textState.DeleteChars(s, e - s); + textState.InsertChars(s, newString); + + if (finalCommit) + s = e = s + newString.Length; + else + e = s + newString.Length; + + this.ImmComp = finalCommit ? string.Empty : newString; + + this.CompositionCursorOffset = + finalCommit + ? 0 + : ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); + + if (finalCommit) + { + this.PartialConversionFrom = this.PartialConversionTo = 0; + } + else if ((comp & GCS.GCS_COMPATTR) != 0) + { + var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); + var attrPtr = stackalloc byte[attrLength]; + var attr = new Span(attrPtr, Math.Min(this.ImmComp.Length, attrLength)); + _ = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, attrPtr, (uint)attrLength); + var l = 0; + while (l < attr.Length && attr[l] is not ATTR_TARGET_CONVERTED and not ATTR_TARGET_NOTCONVERTED) + l++; + + var r = l; + while (r < attr.Length && attr[r] is ATTR_TARGET_CONVERTED or ATTR_TARGET_NOTCONVERTED) + r++; + + if (r == 0 || l == this.ImmComp.Length) + (l, r) = (0, this.ImmComp.Length); + + (this.PartialConversionFrom, this.PartialConversionTo) = (l, r); + } + else + { + this.PartialConversionFrom = 0; + this.PartialConversionTo = this.ImmComp.Length; + } + + // Put the cursor at the beginning, so that the candidate window appears aligned with the text. + c = s; + this.UpdateImeWindowStatus(hImc); + } + + private void ClearState() + { + this.ImmComp = string.Empty; + this.PartialConversionFrom = this.PartialConversionTo = 0; + this.UpdateImeWindowStatus(default); + + ref var textState = ref TextState; + textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; + } + + private void LoadCand(HIMC hImc) + { + this.ImmCand.Clear(); + this.ImmCandNative = default; + + if (hImc == default) + return; + + var size = (int)ImmGetCandidateListW(hImc, 0, null, 0); + if (size == 0) + return; + + var pStorage = stackalloc byte[size]; + if (size != ImmGetCandidateListW(hImc, 0, (CANDIDATELIST*)pStorage, (uint)size)) + return; + + ref var candlist = ref *(CANDIDATELIST*)pStorage; + this.ImmCandNative = candlist; + + if (candlist.dwPageSize == 0 || candlist.dwCount == 0) + return; + + foreach (var i in Enumerable.Range( + (int)candlist.dwPageStart, + (int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize))) + { + this.ImmCand.Add(new((char*)(pStorage + candlist.dwOffset[i]))); + } + } + + private void UpdateImeWindowStatus(HIMC hImc) + { + if (Service.GetNullable() is not { } di) + return; + + this.LoadCand(hImc); + if (this.ImmCand.Count != 0 || this.ShowPartialConversion || this.InputModeIcon != default) + di.OpenImeWindow(); + else + di.CloseImeWindow(); + } + + private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data) + { + this.CursorPos = data.InputPos; + if (data.WantVisible) + { + this.AssociatedViewport = viewport; + } + else + { + this.AssociatedViewport = default; + this.ClearState(); + } + } + + [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] + private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) => + ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + + /// + /// Ported from imstb_textedit.h. + /// + [StructLayout(LayoutKind.Sequential, Size = 0xE2C)] + private struct StbTextEditState + { + /// + /// Position of the text cursor within the string. + /// + public int Cursor; + + /// + /// Selection start point. + /// + public int SelectStart; + + /// + /// selection start and end point in characters; if equal, no selection. + /// + /// + /// Note that start may be less than or greater than end (e.g. when dragging the mouse, + /// start is where the initial click was, and you can drag in either direction.) + /// + public int SelectEnd; + + /// + /// Each text field keeps its own insert mode state. + /// To keep an app-wide insert mode, copy this value in/out of the app state. + /// + public byte InsertMode; + + /// + /// Page size in number of row. + /// This value MUST be set to >0 for pageup or pagedown in multilines documents. + /// + public int RowCountPerPage; + + // Remainder is stb-private data. + } + + [StructLayout(LayoutKind.Sequential)] + private struct ImGuiInputTextState + { + public uint Id; + public int CurLenW; + public int CurLenA; + public ImVector TextWRaw; + public ImVector TextARaw; + public ImVector InitialTextARaw; + public bool TextAIsValid; + public int BufCapacityA; + public float ScrollX; + public StbTextEditState Stb; + public float CursorAnim; + public bool CursorFollow; + public bool SelectedAllMouseLock; + public bool Edited; + public ImGuiInputTextFlags Flags; + + public ImVectorWrapper TextW => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + + public ImVectorWrapper TextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + + public ImVectorWrapper InitialTextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + + // See imgui_widgets.cpp: STB_TEXTEDIT_DELETECHARS + public void DeleteChars(int pos, int n) + { + var dst = this.TextW.Data + pos; + + // We maintain our buffer length in both UTF-8 and wchar formats + this.Edited = true; + this.CurLenA -= Encoding.UTF8.GetByteCount(dst, n); + this.CurLenW -= n; + + // Offset remaining text (FIXME-OPT: Use memmove) + var src = this.TextW.Data + pos + n; + int i; + for (i = 0; src[i] != 0; i++) + dst[i] = src[i]; + dst[i] = '\0'; + } + + // See imgui_widgets.cpp: STB_TEXTEDIT_INSERTCHARS + public bool InsertChars(int pos, ReadOnlySpan newText) + { + var isResizable = (this.Flags & ImGuiInputTextFlags.CallbackResize) != 0; + var textLen = this.CurLenW; + Debug.Assert(pos <= textLen, "pos <= text_len"); + + var newTextLenUtf8 = Encoding.UTF8.GetByteCount(newText); + if (!isResizable && newTextLenUtf8 + this.CurLenA + 1 > this.BufCapacityA) + return false; + + // Grow internal buffer if needed + if (newText.Length + textLen + 1 > this.TextW.Length) + { + if (!isResizable) + return false; + + Debug.Assert(textLen < this.TextW.Length, "text_len < this.TextW.Length"); + this.TextW.Resize(textLen + Math.Clamp(newText.Length * 4, 32, Math.Max(256, newText.Length)) + 1); + } + + var text = this.TextW.DataSpan; + if (pos != textLen) + text.Slice(pos, textLen - pos).CopyTo(text[(pos + newText.Length)..]); + newText.CopyTo(text[pos..]); + + this.Edited = true; + this.CurLenW += newText.Length; + this.CurLenA += newTextLenUtf8; + this.TextW[this.CurLenW] = '\0'; + + return true; + } + } +} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 1dcc5c0c7..95415659b 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -59,7 +59,7 @@ internal class DalamudInterface : IDisposable, IServiceType private readonly ComponentDemoWindow componentDemoWindow; private readonly DataWindow dataWindow; private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow; - private readonly ImeWindow imeWindow; + private readonly DalamudImeWindow imeWindow; private readonly ConsoleWindow consoleWindow; private readonly PluginStatWindow pluginStatWindow; private readonly PluginInstallerWindow pluginWindow; @@ -111,7 +111,7 @@ internal class DalamudInterface : IDisposable, IServiceType this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; this.dataWindow = new DataWindow() { IsOpen = false }; this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow() { IsOpen = false }; - this.imeWindow = new ImeWindow() { IsOpen = false }; + this.imeWindow = new DalamudImeWindow() { IsOpen = false }; this.consoleWindow = new ConsoleWindow(configuration) { IsOpen = configuration.LogOpenAtStartup }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; this.pluginWindow = new PluginInstallerWindow(pluginImageCache, configuration) { IsOpen = false }; @@ -256,7 +256,7 @@ internal class DalamudInterface : IDisposable, IServiceType public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true; /// - /// Opens the . + /// Opens the . /// public void OpenImeWindow() => this.imeWindow.IsOpen = true; @@ -356,7 +356,7 @@ internal class DalamudInterface : IDisposable, IServiceType #region Close /// - /// Closes the . + /// Closes the . /// public void CloseImeWindow() => this.imeWindow.IsOpen = false; @@ -408,7 +408,7 @@ internal class DalamudInterface : IDisposable, IServiceType public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle(); /// - /// Toggles the . + /// Toggles the . /// public void ToggleImeWindow() => this.imeWindow.Toggle(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 1b12fd853..d7ab5ba9d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -12,7 +12,6 @@ using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.ClientState.GamePad; using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.Gui.Internal; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Interface.GameFonts; @@ -73,12 +72,16 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly WndProcHookManager wndProcHookManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DalamudIme dalamudIme = Service.Get(); private readonly ManualResetEvent fontBuildSignal; private readonly SwapChainVtableResolver address; - private readonly Hook dispatchMessageWHook; private readonly Hook setCursorHook; - private Hook processMessageHook; private RawDX11Scene? scene; private Hook? presentHook; @@ -92,8 +95,6 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceConstructor] private InterfaceManager() { - this.dispatchMessageWHook = Hook.FromImport( - null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); @@ -111,12 +112,6 @@ internal class InterfaceManager : IDisposable, IServiceType [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr SetCursorDelegate(IntPtr hCursor); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate IntPtr DispatchMessageWDelegate(ref User32.MSG msg); - - [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr ProcessMessageDelegate(IntPtr hWnd, uint msg, ulong wParam, ulong lParam, IntPtr handeled); - /// /// This event gets called each frame to facilitate ImGui drawing. /// @@ -236,10 +231,9 @@ internal class InterfaceManager : IDisposable, IServiceType this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - this.dispatchMessageWHook.Dispose(); - this.processMessageHook?.Dispose(); }).Wait(); + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; this.scene?.Dispose(); } @@ -660,6 +654,20 @@ internal class InterfaceManager : IDisposable, IServiceType this.scene = newScene; Service.Provide(new(this)); + + this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; + } + + private unsafe void WndProcHookManagerOnPreWndProc(ref WndProcHookManager.WndProcOverrideEventArgs args) + { + var r = this.scene?.ProcessWndProcW(args.Hwnd, (User32.WindowMessage)args.Message, args.WParam, args.LParam); + if (r is not null) + { + args.ReturnValue = r.Value; + args.SuppressCall = true; + } + + this.dalamudIme.ProcessImeMessage(ref args); } /* @@ -1095,15 +1103,9 @@ internal class InterfaceManager : IDisposable, IServiceType Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - var wndProcAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 80 7C 24 ?? ?? 74 ?? B8"); - Log.Verbose($"WndProc address 0x{wndProcAddress.ToInt64():X}"); - this.processMessageHook = Hook.FromAddress(wndProcAddress, this.ProcessMessageDetour); - this.setCursorHook.Enable(); this.presentHook.Enable(); this.resizeBuffersHook.Enable(); - this.dispatchMessageWHook.Enable(); - this.processMessageHook.Enable(); }); } @@ -1124,25 +1126,6 @@ internal class InterfaceManager : IDisposable, IServiceType this.isRebuildingFonts = false; } - private unsafe IntPtr ProcessMessageDetour(IntPtr hWnd, uint msg, ulong wParam, ulong lParam, IntPtr handeled) - { - var ime = Service.GetNullable(); - var res = ime?.ProcessWndProcW(hWnd, (User32.WindowMessage)msg, (void*)wParam, (void*)lParam); - return this.processMessageHook.Original(hWnd, msg, wParam, lParam, handeled); - } - - private unsafe IntPtr DispatchMessageWDetour(ref User32.MSG msg) - { - if (msg.hwnd == this.GameWindowHandle && this.scene != null) - { - var res = this.scene.ProcessWndProcW(msg.hwnd, msg.message, (void*)msg.wParam, (void*)msg.lParam); - if (res != null) - return res.Value; - } - - return this.dispatchMessageWHook.IsDisposed ? User32.DispatchMessage(ref msg) : this.dispatchMessageWHook.Original(ref msg); - } - private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) { #if DEBUG diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs new file mode 100644 index 000000000..1819ed819 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs @@ -0,0 +1,223 @@ +using System.Numerics; + +using Dalamud.Interface.Windowing; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows; + +/// +/// A window for displaying IME details. +/// +internal unsafe class DalamudImeWindow : Window +{ + private const int ImePageSize = 9; + + /// + /// Initializes a new instance of the class. + /// + public DalamudImeWindow() + : base( + "Dalamud IME", + ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoBackground) + { + this.Size = default(Vector2); + + this.RespectCloseHotkey = false; + } + + /// + public override void Draw() + { + } + + /// + public override void PostDraw() + { + if (Service.GetNullable() is not { } ime) + return; + + var viewport = ime.AssociatedViewport; + if (viewport.NativePtr is null) + return; + + var drawCand = ime.ImmCand.Count != 0; + var drawConv = drawCand || ime.ShowPartialConversion; + var drawIme = ime.InputModeIcon != null; + + var pad = ImGui.GetStyle().WindowPadding; + var candTextSize = ImGui.CalcTextSize(ime.ImmComp == string.Empty ? " " : ime.ImmComp); + + var native = ime.ImmCandNative; + var totalIndex = native.dwSelection + 1; + var totalSize = native.dwCount; + + var pageStart = native.dwPageStart; + var pageIndex = (pageStart / ImePageSize) + 1; + var pageCount = (totalSize / ImePageSize) + 1; + var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})"; + + // Calc the window size. + var maxTextWidth = 0f; + for (var i = 0; i < ime.ImmCand.Count; i++) + { + var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.ImmCand[i]}"); + maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X; + } + + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X; + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.ImmComp).X + ? maxTextWidth + : ImGui.CalcTextSize(ime.ImmComp).X; + + var numEntries = (drawCand ? ime.ImmCand.Count + 1 : 0) + 1 + (drawIme ? 1 : 0); + var spaceY = ImGui.GetStyle().ItemSpacing.Y; + var imeWindowHeight = (spaceY * (numEntries - 1)) + (candTextSize.Y * numEntries); + var windowSize = new Vector2(maxTextWidth, imeWindowHeight) + (pad * 2); + + // 1. Figure out the expanding direction. + var expandUpward = ime.CursorPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y; + var windowPos = ime.CursorPos - pad; + if (expandUpward) + { + windowPos.Y -= windowSize.Y - candTextSize.Y - (pad.Y * 2); + if (drawIme) + windowPos.Y += candTextSize.Y + spaceY; + } + else + { + if (drawIme) + windowPos.Y -= candTextSize.Y + spaceY; + } + + // 2. Contain within the viewport. Do not use clamp, as the target window might be too small. + if (windowPos.X < viewport.WorkPos.X) + windowPos.X = viewport.WorkPos.X; + else if (windowPos.X + windowSize.X > viewport.WorkPos.X + viewport.WorkSize.X) + windowPos.X = (viewport.WorkPos.X + viewport.WorkSize.X) - windowSize.X; + if (windowPos.Y < viewport.WorkPos.Y) + windowPos.Y = viewport.WorkPos.Y; + else if (windowPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y) + windowPos.Y = (viewport.WorkPos.Y + viewport.WorkSize.Y) - windowSize.Y; + + var cursor = windowPos + pad; + + // Draw the ime window. + var drawList = ImGui.GetForegroundDrawList(viewport); + + // Draw the background rect for candidates. + if (drawCand) + { + Vector2 candRectLt, candRectRb; + if (!expandUpward) + { + candRectLt = windowPos + candTextSize with { X = 0 } + pad with { X = 0 }; + candRectRb = windowPos + windowSize; + if (drawIme) + candRectLt.Y += spaceY + candTextSize.Y; + } + else + { + candRectLt = windowPos; + candRectRb = windowPos + (windowSize - candTextSize with { X = 0 } - pad with { X = 0 }); + if (drawIme) + candRectRb.Y -= spaceY + candTextSize.Y; + } + + drawList.AddRectFilled( + candRectLt, + candRectRb, + ImGui.GetColorU32(ImGuiCol.WindowBg), + ImGui.GetStyle().WindowRounding); + } + + if (!expandUpward && drawIme) + { + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + cursor.Y += candTextSize.Y + spaceY; + } + + if (!expandUpward && drawConv) + { + DrawTextBeingConverted(); + cursor.Y += candTextSize.Y + spaceY; + + // Add a separator. + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + } + + if (drawCand) + { + // Add the candidate words. + for (var i = 0; i < ime.ImmCand.Count; i++) + { + var selected = i == (native.dwSelection % ImePageSize); + var color = ImGui.GetColorU32(ImGuiCol.Text); + if (selected) + color = ImGui.GetColorU32(ImGuiCol.NavHighlight); + + drawList.AddText(cursor, color, $"{i + 1}. {ime.ImmCand[i]}"); + cursor.Y += candTextSize.Y + spaceY; + } + + // Add a separator + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + + // Add the pages infomation. + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), pageInfo); + cursor.Y += candTextSize.Y + spaceY; + } + + if (expandUpward && drawConv) + { + // Add a separator. + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + + DrawTextBeingConverted(); + cursor.Y += candTextSize.Y + spaceY; + } + + if (expandUpward && drawIme) + { + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + } + + return; + + void DrawTextBeingConverted() + { + // Draw the text background. + drawList.AddRectFilled( + cursor - (pad / 2), + cursor + candTextSize + (pad / 2), + ImGui.GetColorU32(ImGuiCol.WindowBg)); + + // If only a part of the full text is marked for conversion, then draw background for the part being edited. + if (ime.PartialConversionFrom != 0 || ime.PartialConversionTo != ime.ImmComp.Length) + { + var part1 = ime.ImmComp[..ime.PartialConversionFrom]; + var part2 = ime.ImmComp[..ime.PartialConversionTo]; + var size1 = ImGui.CalcTextSize(part1); + var size2 = ImGui.CalcTextSize(part2); + drawList.AddRectFilled( + cursor + size1 with { Y = 0 }, + cursor + size2, + ImGui.GetColorU32(ImGuiCol.TextSelectedBg)); + } + + // Add the text being converted. + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.ImmComp); + + // Draw the caret inside the composition string. + if (DalamudIme.ShowCursorInInputText) + { + var partBeforeCaret = ime.ImmComp[..ime.CompositionCursorOffset]; + var sizeBeforeCaret = ImGui.CalcTextSize(partBeforeCaret); + drawList.AddLine( + cursor + sizeBeforeCaret with { Y = 0 }, + cursor + sizeBeforeCaret, + ImGui.GetColorU32(ImGuiCol.Text)); + } + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/IMEWindow.cs b/Dalamud/Interface/Internal/Windows/IMEWindow.cs deleted file mode 100644 index 80e03caf3..000000000 --- a/Dalamud/Interface/Internal/Windows/IMEWindow.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Numerics; - -using Dalamud.Game.ClientState.Keys; -using Dalamud.Game.Gui.Internal; -using Dalamud.Interface.Windowing; -using ImGuiNET; - -namespace Dalamud.Interface.Internal.Windows; - -/// -/// A window for displaying IME details. -/// -internal unsafe class ImeWindow : Window -{ - private const int ImePageSize = 9; - - /// - /// Initializes a new instance of the class. - /// - public ImeWindow() - : base("Dalamud IME", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoBackground) - { - this.Size = new Vector2(100, 200); - this.SizeCondition = ImGuiCond.FirstUseEver; - - this.RespectCloseHotkey = false; - } - - /// - public override void Draw() - { - if (this.IsOpen && Service.Get()[VirtualKey.SHIFT]) Service.Get().CloseImeWindow(); - var ime = Service.GetNullable(); - - if (ime == null || !ime.IsEnabled) - { - ImGui.Text("IME is unavailable."); - return; - } - - // ImGui.Text($"{ime.GetCursorPos()}"); - // ImGui.Text($"{ImGui.GetWindowViewport().WorkSize}"); - } - - /// - public override void PostDraw() - { - if (this.IsOpen && Service.Get()[VirtualKey.SHIFT]) Service.Get().CloseImeWindow(); - var ime = Service.GetNullable(); - - if (ime == null || !ime.IsEnabled) - return; - - var maxTextWidth = 0f; - var textHeight = ImGui.CalcTextSize(ime.ImmComp).Y; - - var native = ime.ImmCandNative; - var totalIndex = native.Selection + 1; - var totalSize = native.Count; - - var pageStart = native.PageStart; - var pageIndex = (pageStart / ImePageSize) + 1; - var pageCount = (totalSize / ImePageSize) + 1; - var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})"; - - // Calc the window size - for (var i = 0; i < ime.ImmCand.Count; i++) - { - var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.ImmCand[i]}"); - maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X; - } - - maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X; - maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.ImmComp).X ? maxTextWidth : ImGui.CalcTextSize(ime.ImmComp).X; - - var imeWindowWidth = maxTextWidth + (2 * ImGui.GetStyle().WindowPadding.X); - var imeWindowHeight = (textHeight * (ime.ImmCand.Count + 2)) + (5 * (ime.ImmCand.Count - 1)) + (2 * ImGui.GetStyle().WindowPadding.Y); - - // Calc the window pos - var cursorPos = ime.GetCursorPos(); - var imeWindowMinPos = new Vector2(cursorPos.X, cursorPos.Y); - var imeWindowMaxPos = new Vector2(imeWindowMinPos.X + imeWindowWidth, imeWindowMinPos.Y + imeWindowHeight); - var gameWindowSize = ImGui.GetWindowViewport().WorkSize; - - var offset = new Vector2( - imeWindowMaxPos.X - gameWindowSize.X > 0 ? imeWindowMaxPos.X - gameWindowSize.X : 0, - imeWindowMaxPos.Y - gameWindowSize.Y > 0 ? imeWindowMaxPos.Y - gameWindowSize.Y : 0); - imeWindowMinPos -= offset; - imeWindowMaxPos -= offset; - - var nextDrawPosY = imeWindowMinPos.Y; - var drawAreaPosX = imeWindowMinPos.X + ImGui.GetStyle().WindowPadding.X; - - // Draw the ime window - var drawList = ImGui.GetForegroundDrawList(); - // Draw the background rect - drawList.AddRectFilled(imeWindowMinPos, imeWindowMaxPos, ImGui.GetColorU32(ImGuiCol.WindowBg), ImGui.GetStyle().WindowRounding); - // Add component text - drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Text), ime.ImmComp); - nextDrawPosY += textHeight + ImGui.GetStyle().ItemSpacing.Y; - // Add separator - drawList.AddLine(new Vector2(drawAreaPosX, nextDrawPosY), new Vector2(drawAreaPosX + maxTextWidth, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Separator)); - // Add candidate words - for (var i = 0; i < ime.ImmCand.Count; i++) - { - var selected = i == (native.Selection % ImePageSize); - var color = ImGui.GetColorU32(ImGuiCol.Text); - if (selected) - color = ImGui.GetColorU32(ImGuiCol.NavHighlight); - - drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), color, $"{i + 1}. {ime.ImmCand[i]}"); - nextDrawPosY += textHeight + ImGui.GetStyle().ItemSpacing.Y; - } - - // Add separator - drawList.AddLine(new Vector2(drawAreaPosX, nextDrawPosY), new Vector2(drawAreaPosX + maxTextWidth, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Separator)); - // Add pages infomation - drawList.AddText(new Vector2(drawAreaPosX, nextDrawPosY), ImGui.GetColorU32(ImGuiCol.Text), pageInfo); - } -} diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs new file mode 100644 index 000000000..fcd90c95a --- /dev/null +++ b/Dalamud/Interface/Internal/WndProcHookManager.cs @@ -0,0 +1,273 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Hooking; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// +/// A manifestation of "I can't believe this is required". +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed class WndProcHookManager : IServiceType, IDisposable +{ + private static readonly ModuleLog Log = new("WPHM"); + + private readonly Hook dispatchMessageWHook; + private readonly Dictionary wndProcNextDict = new(); + private readonly WndProcDelegate wndProcDelegate; + private readonly uint unhookSelfMessage; + private bool disposed; + + [ServiceManager.ServiceConstructor] + private unsafe WndProcHookManager() + { + this.wndProcDelegate = this.WndProcDetour; + this.dispatchMessageWHook = Hook.FromImport( + null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); + this.dispatchMessageWHook.Enable(); + fixed (void* pMessageName = $"{nameof(WndProcHookManager)}.{nameof(this.unhookSelfMessage)}") + this.unhookSelfMessage = RegisterWindowMessageW((ushort*)pMessageName); + } + + /// + /// Finalizes an instance of the class. + /// + ~WndProcHookManager() => this.ReleaseUnmanagedResources(); + + /// + /// Delegate for overriding WndProc. + /// + /// The arguments. + public delegate void WndProcOverrideDelegate(ref WndProcOverrideEventArgs args); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate nint DispatchMessageWDelegate(ref MSG msg); + + /// + /// Called before WndProc. + /// + public event WndProcOverrideDelegate? PreWndProc; + + /// + /// Called after WndProc. + /// + public event WndProcOverrideDelegate? PostWndProc; + + /// + public void Dispose() + { + this.disposed = true; + this.dispatchMessageWHook.Dispose(); + this.ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + /// + /// Detour for . Used to discover new windows to hook. + /// + /// The message. + /// The original return value. + private unsafe nint DispatchMessageWDetour(ref MSG msg) + { + lock (this.wndProcNextDict) + { + if (!this.disposed && ImGuiHelpers.FindViewportId(msg.hwnd) >= 0 && + !this.wndProcNextDict.ContainsKey(msg.hwnd)) + { + this.wndProcNextDict[msg.hwnd] = SetWindowLongPtrW( + msg.hwnd, + GWLP.GWLP_WNDPROC, + Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate)); + } + } + + return this.dispatchMessageWHook.IsDisposed + ? DispatchMessageW((MSG*)Unsafe.AsPointer(ref msg)) + : this.dispatchMessageWHook.Original(ref msg); + } + + private unsafe LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + nint nextProc; + lock (this.wndProcNextDict) + { + if (!this.wndProcNextDict.TryGetValue(hwnd, out nextProc)) + { + // Something went wrong; prevent crash. Things will, regardless of the effort, break. + return DefWindowProcW(hwnd, uMsg, wParam, lParam); + } + } + + if (uMsg == this.unhookSelfMessage) + { + // Remove self from the chain. + SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); + lock (this.wndProcNextDict) + this.wndProcNextDict.Remove(hwnd); + + // Even though this message is dedicated for our processing, + // satisfy the expectations by calling the next window procedure. + return CallWindowProcW( + (delegate* unmanaged)nextProc, + hwnd, + uMsg, + wParam, + lParam); + } + + var arg = new WndProcOverrideEventArgs(hwnd, ref uMsg, ref wParam, ref lParam); + try + { + this.PreWndProc?.Invoke(ref arg); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PostWndProc)} error"); + } + + if (!arg.SuppressCall) + { + try + { + arg.ReturnValue = CallWindowProcW( + (delegate* unmanaged)nextProc, + hwnd, + uMsg, + wParam, + lParam); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(CallWindowProcW)} error; probably some other software's fault"); + } + + try + { + this.PostWndProc?.Invoke(ref arg); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PostWndProc)} error"); + } + } + + if (uMsg == WM.WM_NCDESTROY) + { + // The window will cease to exist, once we return. + SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); + lock (this.wndProcNextDict) + this.wndProcNextDict.Remove(hwnd); + } + + return arg.ReturnValue; + } + + private void ReleaseUnmanagedResources() + { + this.disposed = true; + + // As wndProcNextDict will be touched on each SendMessageW call, make a copy of window list first. + HWND[] windows; + lock (this.wndProcNextDict) + windows = this.wndProcNextDict.Keys.ToArray(); + + // Unregister our hook from all the windows we hooked. + foreach (var v in windows) + SendMessageW(v, this.unhookSelfMessage, default, default); + } + + /// + /// Parameters for . + /// + public ref struct WndProcOverrideEventArgs + { + /// + /// The handle of the target window of the message. + /// + public readonly HWND Hwnd; + + /// + /// The message. + /// + public ref uint Message; + + /// + /// The WPARAM. + /// + public ref WPARAM WParam; + + /// + /// The LPARAM. + /// + public ref LPARAM LParam; + + /// + /// Initializes a new instance of the struct. + /// + /// The handle of the target window of the message. + /// The message. + /// The WPARAM. + /// The LPARAM. + public WndProcOverrideEventArgs(HWND hwnd, ref uint msg, ref WPARAM wParam, ref LPARAM lParam) + { + this.Hwnd = hwnd; + this.LParam = ref lParam; + this.WParam = ref wParam; + this.Message = ref msg; + this.ViewportId = ImGuiHelpers.FindViewportId(hwnd); + } + + /// + /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
+ /// Does nothing if changed from . + ///
+ public bool SuppressCall { get; set; } + + /// + /// Gets or sets the return value.
+ /// Has the return value from next window procedure, if accessed from . + ///
+ public LRESULT ReturnValue { get; set; } + + /// + /// Gets the ImGui viewport ID. + /// + public int ViewportId { get; init; } + + /// + /// Gets a value indicating whether this message is for the game window (the first viewport). + /// + public bool IsGameWindow => this.ViewportId == 0; + + /// + /// Sets to true and sets . + /// + /// The new return value. + public void SuppressAndReturn(LRESULT returnValue) + { + this.ReturnValue = returnValue; + this.SuppressCall = true; + } + + /// + /// Sets to true and calls . + /// + public void SuppressWithDefault() + { + this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam); + this.SuppressCall = true; + } + } +} diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 579d93f86..85f81b203 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -426,6 +426,26 @@ public static class ImGuiHelpers /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; + + /// + /// Finds the corresponding ImGui viewport ID for the given window handle. + /// + /// The window handle. + /// The viewport ID, or -1 if not found. + internal static unsafe int FindViewportId(nint hwnd) + { + if (!IsImGuiInitialized) + return -1; + + var viewports = new ImVectorWrapper(&ImGui.GetPlatformIO().NativePtr->Viewports); + for (var i = 0; i < viewports.LengthUnsafe; i++) + { + if (viewports.DataUnsafe[i].PlatformHandle == hwnd) + return i; + } + + return -1; + } /// /// Get data needed for each new frame. From e089949a728893474d83d05393df5487ace96c0d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 02:33:18 +0900 Subject: [PATCH 380/585] fix minor things --- Dalamud/Interface/Internal/DalamudIme.cs | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 1fc70b0f6..6535228a7 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -184,6 +184,13 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType case WM.WM_IME_NOTIFY: // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); break; + + case WM.WM_LBUTTONDOWN: + case WM.WM_RBUTTONDOWN: + case WM.WM_MBUTTONDOWN: + case WM.WM_XBUTTONDOWN: + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); + break; } this.UpdateInputLanguage(hImc); @@ -299,9 +306,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType if (finalCommit) { - this.PartialConversionFrom = this.PartialConversionTo = 0; + this.ClearState(hImc); + return; } - else if ((comp & GCS.GCS_COMPATTR) != 0) + + if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); var attrPtr = stackalloc byte[attrLength]; @@ -331,14 +340,17 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.UpdateImeWindowStatus(hImc); } - private void ClearState() + private void ClearState(HIMC hImc) { this.ImmComp = string.Empty; this.PartialConversionFrom = this.PartialConversionTo = 0; + this.CompositionCursorOffset = 0; this.UpdateImeWindowStatus(default); ref var textState = ref TextState; textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; + + Log.Information($"{nameof(this.ClearState)}"); } private void LoadCand(HIMC hImc) @@ -386,15 +398,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data) { this.CursorPos = data.InputPos; - if (data.WantVisible) - { - this.AssociatedViewport = viewport; - } - else - { - this.AssociatedViewport = default; - this.ClearState(); - } + this.AssociatedViewport = data.WantVisible ? viewport : default; } [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] From f03552a2ab51aa3a5c1b24501028beb5127db354 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 02:39:11 +0900 Subject: [PATCH 381/585] Prevent Tab key from breaking input --- Dalamud/Interface/Internal/DalamudIme.cs | 54 +++++++++++++++++------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 6535228a7..718ec53e6 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -55,12 +55,12 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return textState.CursorAnim % 1.2f <= 0.8f; } } - + /// /// Gets the cursor position, in screen coordinates. /// internal Vector2 CursorPos { get; private set; } - + /// /// Gets the associated viewport. /// @@ -101,7 +101,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType /// internal bool ShowPartialConversion => this.PartialConversionFrom != 0 || this.PartialConversionTo != this.ImmComp.Length; - + /// /// Gets the input mode icon from . /// @@ -139,15 +139,17 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType switch (args.Message) { - case WM.WM_IME_NOTIFY when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: + case WM.WM_IME_NOTIFY + when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE + or IMN.IMN_CHANGECANDIDATE: this.UpdateImeWindowStatus(hImc); args.SuppressAndReturn(0); break; - + case WM.WM_IME_STARTCOMPOSITION: args.SuppressAndReturn(0); break; - + case WM.WM_IME_COMPOSITION: if (invalidTarget) ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); @@ -162,12 +164,12 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressAndReturn(0); break; - + case WM.WM_IME_CONTROL: // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressAndReturn(0); break; - + case WM.WM_IME_REQUEST: // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressAndReturn(0); @@ -180,11 +182,31 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); args.SuppressWithDefault(); break; - + case WM.WM_IME_NOTIFY: // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); break; + case WM.WM_KEYDOWN when (int)args.WParam is + VK.VK_TAB + or VK.VK_PRIOR + or VK.VK_NEXT + or VK.VK_END + or VK.VK_HOME + or VK.VK_LEFT + or VK.VK_UP + or VK.VK_RIGHT + or VK.VK_DOWN + or VK.VK_RETURN: + if (this.ImmCand.Count != 0) + { + TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); + args.WParam = VK.VK_PROCESSKEY; + } + + break; + case WM.WM_LBUTTONDOWN: case WM.WM_RBUTTONDOWN: case WM.WM_MBUTTONDOWN: @@ -192,7 +214,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); break; } - + this.UpdateInputLanguage(hImc); } finally @@ -220,7 +242,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ImmGetConversionStatus(hImc, &conv, &sent); var lang = GetKeyboardLayout(0); var open = ImmGetOpenStatus(hImc) != false; - + // Log.Verbose($"{nameof(this.UpdateInputLanguage)}: conv={conv:X} sent={sent:X} open={open} lang={lang:X}"); var native = (conv & 1) != 0; @@ -285,8 +307,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType (s, e) = (e, s); var newString = finalCommit - ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) - : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); + ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) + : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); if (s != e) textState.DeleteChars(s, e - s); @@ -303,13 +325,13 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType finalCommit ? 0 : ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); - + if (finalCommit) { this.ClearState(hImc); return; } - + if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); @@ -349,7 +371,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ref var textState = ref TextState; textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; - + Log.Information($"{nameof(this.ClearState)}"); } From 01b45c98ac0abc12a6c644d61a1a7f82525a5e8e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 02:42:54 +0900 Subject: [PATCH 382/585] w --- Dalamud/Interface/Internal/DalamudIme.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 718ec53e6..286197590 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -200,8 +200,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType or VK.VK_RETURN: if (this.ImmCand.Count != 0) { - TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; - ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); + this.ClearState(hImc); args.WParam = VK.VK_PROCESSKEY; } @@ -367,6 +366,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.ImmComp = string.Empty; this.PartialConversionFrom = this.PartialConversionTo = 0; this.CompositionCursorOffset = 0; + TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); this.UpdateImeWindowStatus(default); ref var textState = ref TextState; From 2c3139d8b7685d16b1db1268361a0176aab6efeb Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 15:41:37 +0900 Subject: [PATCH 383/585] Ensure borders on IME mode foreground icon --- Dalamud/Game/Text/SeIconChar.cs | 32 ++++++++++++++++--- Dalamud/Interface/Internal/DalamudIme.cs | 9 +++--- .../Internal/Windows/DalamudImeWindow.cs | 28 ++++++++++++++++ 3 files changed, 59 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/Text/SeIconChar.cs b/Dalamud/Game/Text/SeIconChar.cs index c1be00613..17924c671 100644 --- a/Dalamud/Game/Text/SeIconChar.cs +++ b/Dalamud/Game/Text/SeIconChar.cs @@ -611,29 +611,51 @@ public enum SeIconChar QuestRepeatable = 0xE0BF, /// - /// The IME hiragana icon unicode character. + /// The [あ] character indicating that the Japanese IME is in full-width Hiragana input mode. /// + /// + /// Half-width Hiragana exists as a Windows API constant, but the feature is unused, or at least unexposed to the end user via the IME. + /// ImeHiragana = 0xE020, /// - /// The IME katakana icon unicode character. + /// The [ア] character indicating that the Japanese IME is in full-width Katakana input mode. /// ImeKatakana = 0xE021, /// - /// The IME alphanumeric icon unicode character. + /// The [A] character indicating that Japanese or Korean IME is in full-width Latin character input mode. /// ImeAlphanumeric = 0xE022, /// - /// The IME katakana half-width icon unicode character. + /// The [_ア] character indicating that the Japanese IME is in half-width Katakana input mode. /// ImeKatakanaHalfWidth = 0xE023, /// - /// The IME alphanumeric half-width icon unicode character. + /// The [_A] character indicating that Japanese or Korean IME is in half-width Latin character input mode. /// ImeAlphanumericHalfWidth = 0xE024, + + /// + /// The [가] character indicating that the Korean IME is in Hangul input mode. + /// + /// + /// Use and for alphanumeric input mode, + /// toggled via Alt+=. + /// + ImeKoreanHangul = 0xE025, + + /// + /// The [中] character indicating that the Chinese IME is in Han character input mode. + /// + ImeChineseHan = 0xE026, + + /// + /// The [英] character indicating that the Chinese IME is in Latin character input mode. + /// + ImeChineseLatin = 0xE027, /// /// The instance (1) icon unicode character. diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 286197590..9e0466e66 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -19,7 +19,7 @@ using static TerraFX.Interop.Windows.Windows; namespace Dalamud.Interface.Internal; /// -/// This class handles IME for non-English users. +/// This class handles CJK IME. /// [ServiceManager.EarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType @@ -251,7 +251,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { case LANG.LANG_KOREAN: if (native) - this.InputModeIcon = "\uE025"; + this.InputModeIcon = $"{(char)SeIconChar.ImeKoreanHangul}"; else if (fullwidth) this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; else @@ -274,11 +274,10 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType break; case LANG.LANG_CHINESE: - // TODO: does Chinese IME also need "open" check? if (native) - this.InputModeIcon = "\uE026"; + this.InputModeIcon = $"{(char)SeIconChar.ImeChineseHan}"; else - this.InputModeIcon = "\uE027"; + this.InputModeIcon = $"{(char)SeIconChar.ImeChineseLatin}"; break; default: diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs index 1819ed819..7417afd91 100644 --- a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs @@ -133,6 +133,20 @@ internal unsafe class DalamudImeWindow : Window if (!expandUpward && drawIme) { + for (var dx = -2; dx <= 2; dx++) + { + for (var dy = -2; dy <= 2; dy++) + { + if (dx != 0 || dy != 0) + { + drawList.AddText( + cursor + new Vector2(dx, dy), + ImGui.GetColorU32(ImGuiCol.WindowBg), + ime.InputModeIcon); + } + } + } + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); cursor.Y += candTextSize.Y + spaceY; } @@ -179,6 +193,20 @@ internal unsafe class DalamudImeWindow : Window if (expandUpward && drawIme) { + for (var dx = -2; dx <= 2; dx++) + { + for (var dy = -2; dy <= 2; dy++) + { + if (dx != 0 || dy != 0) + { + drawList.AddText( + cursor + new Vector2(dx, dy), + ImGui.GetColorU32(ImGuiCol.WindowBg), + ime.InputModeIcon); + } + } + } + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); } From 806ecc0faf7b019d3a31093561778c2f4328fb0e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 8 Dec 2023 15:48:20 +0900 Subject: [PATCH 384/585] Use RenderChar instead of AddText --- Dalamud/Interface/Internal/DalamudIme.cs | 24 +++++++++--------- .../Internal/Windows/DalamudImeWindow.cs | 25 +++++++++++++++---- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 9e0466e66..f44c885ce 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -105,7 +105,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType /// /// Gets the input mode icon from . /// - internal string? InputModeIcon { get; private set; } + internal char InputModeIcon { get; private set; } private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588); @@ -251,37 +251,37 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { case LANG.LANG_KOREAN: if (native) - this.InputModeIcon = $"{(char)SeIconChar.ImeKoreanHangul}"; + this.InputModeIcon = (char)SeIconChar.ImeKoreanHangul; else if (fullwidth) - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_JAPANESE: // wtf // see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0 if (open && native && katakana && fullwidth) - this.InputModeIcon = $"{(char)SeIconChar.ImeKatakana}"; + this.InputModeIcon = (char)SeIconChar.ImeKatakana; else if (open && native && katakana) - this.InputModeIcon = $"{(char)SeIconChar.ImeKatakanaHalfWidth}"; + this.InputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth; else if (open && native) - this.InputModeIcon = $"{(char)SeIconChar.ImeHiragana}"; + this.InputModeIcon = (char)SeIconChar.ImeHiragana; else if (open && fullwidth) - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumeric}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = $"{(char)SeIconChar.ImeAlphanumericHalfWidth}"; + this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_CHINESE: if (native) - this.InputModeIcon = $"{(char)SeIconChar.ImeChineseHan}"; + this.InputModeIcon = (char)SeIconChar.ImeChineseHan; else - this.InputModeIcon = $"{(char)SeIconChar.ImeChineseLatin}"; + this.InputModeIcon = (char)SeIconChar.ImeChineseLatin; break; default: - this.InputModeIcon = null; + this.InputModeIcon = default; break; } diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs index 7417afd91..ecaa522e5 100644 --- a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs @@ -43,7 +43,8 @@ internal unsafe class DalamudImeWindow : Window var drawCand = ime.ImmCand.Count != 0; var drawConv = drawCand || ime.ShowPartialConversion; - var drawIme = ime.InputModeIcon != null; + var drawIme = ime.InputModeIcon != 0; + var imeIconFont = InterfaceManager.DefaultFont; var pad = ImGui.GetStyle().WindowPadding; var candTextSize = ImGui.CalcTextSize(ime.ImmComp == string.Empty ? " " : ime.ImmComp); @@ -139,7 +140,9 @@ internal unsafe class DalamudImeWindow : Window { if (dx != 0 || dy != 0) { - drawList.AddText( + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, cursor + new Vector2(dx, dy), ImGui.GetColorU32(ImGuiCol.WindowBg), ime.InputModeIcon); @@ -147,7 +150,12 @@ internal unsafe class DalamudImeWindow : Window } } - drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor, + ImGui.GetColorU32(ImGuiCol.Text), + ime.InputModeIcon); cursor.Y += candTextSize.Y + spaceY; } @@ -199,7 +207,9 @@ internal unsafe class DalamudImeWindow : Window { if (dx != 0 || dy != 0) { - drawList.AddText( + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, cursor + new Vector2(dx, dy), ImGui.GetColorU32(ImGuiCol.WindowBg), ime.InputModeIcon); @@ -207,7 +217,12 @@ internal unsafe class DalamudImeWindow : Window } } - drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.InputModeIcon); + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor, + ImGui.GetColorU32(ImGuiCol.Text), + ime.InputModeIcon); } return; From b910ebc014f4ac7fdefa0839d574c0626fde794c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 22:32:28 +0900 Subject: [PATCH 385/585] Auto-enable fonts depending on the character input --- Dalamud/Interface/Internal/DalamudIme.cs | 68 +++++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 56 ++++++++++++++- 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index f44c885ce..f8d7fb690 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -5,8 +5,10 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Text.Unicode; using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -26,6 +28,26 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { private static readonly ModuleLog Log = new("IME"); + private static readonly UnicodeRange[] HanRange = + { + UnicodeRanges.CjkRadicalsSupplement, + UnicodeRanges.CjkSymbolsandPunctuation, + UnicodeRanges.CjkUnifiedIdeographsExtensionA, + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkCompatibilityIdeographs, + UnicodeRanges.CjkCompatibilityForms, + // No more; Extension B~ are outside BMP range + }; + + private static readonly UnicodeRange[] HangulRange = + { + UnicodeRanges.HangulJamo, + UnicodeRanges.HangulSyllables, + UnicodeRanges.HangulCompatibilityJamo, + UnicodeRanges.HangulJamoExtendedA, + UnicodeRanges.HangulJamoExtendedB, + }; + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; [ServiceManager.ServiceConstructor] @@ -38,6 +60,16 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private delegate void ImGuiSetPlatformImeDataDelegate(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data); + /// + /// Gets a value indicating whether Han(Chinese) input has been detected. + /// + public bool EncounteredHan { get; private set; } + + /// + /// Gets a value indicating whether Hangul(Korean) input has been detected. + /// + public bool EncounteredHangul { get; private set; } + /// /// Gets a value indicating whether to display the cursor in input text. This also deals with blinking. /// @@ -116,6 +148,39 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType GC.SuppressFinalize(this); } + /// + /// Looks for the characters inside and enables fonts accordingly. + /// + /// The string. + public void ReflectCharacterEncounters(string str) + { + foreach (var chr in str) + { + if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + { + if (Service.Get() + .GetFdtReader(GameFontFamilyAndSize.Axis12) + ?.FindGlyph(chr) is null) + { + if (!this.EncounteredHan) + { + this.EncounteredHan = true; + Service.Get().RebuildFonts(); + } + } + } + + if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + { + if (!this.EncounteredHangul) + { + this.EncounteredHangul = true; + Service.Get().RebuildFonts(); + } + } + } + } + /// /// Processes window messages. /// @@ -308,6 +373,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); + this.ReflectCharacterEncounters(newString); + if (s != e) textState.DeleteChars(s, e - s); textState.InsertChars(s, newString); @@ -402,6 +469,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType (int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize))) { this.ImmCand.Add(new((char*)(pStorage + candlist.dwOffset[i]))); + this.ReflectCharacterEncounters(this.ImmCand[^1]); } } diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d7ab5ba9d..49dfdb248 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Text.Unicode; using System.Threading; using Dalamud.Configuration.Internal; @@ -786,10 +787,22 @@ internal class InterfaceManager : IDisposable, IServiceType var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); if (!File.Exists(fontPathKr)) fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); + if (!File.Exists(fontPathKr)) + fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); if (!File.Exists(fontPathKr)) fontPathKr = null; Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); + var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); + + var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); + // Default font Log.Verbose("[FONT] SetupFonts - Default font"); var fontInfo = new TargetFontModification( @@ -817,7 +830,8 @@ internal class InterfaceManager : IDisposable, IServiceType this.loadedFontInfo[DefaultFont] = fontInfo; } - if (fontPathKr != null && Service.Get().EffectiveLanguage == "ko") + if (fontPathKr != null + && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) { fontConfig.MergeMode = true; fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); @@ -826,6 +840,46 @@ internal class InterfaceManager : IDisposable, IServiceType fontConfig.MergeMode = false; } + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || this.dalamudIme.EncounteredHan)) + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + // FontAwesome icon font Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); { From 4be635be675204da1c0c59c6eb04e08e112754ce Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 12:40:33 +0900 Subject: [PATCH 386/585] Remove ClearState log --- Dalamud/Interface/Internal/DalamudIme.cs | 2 +- Dalamud/Interface/Internal/WndProcHookManager.cs | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index f8d7fb690..b3252546a 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -439,7 +439,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ref var textState = ref TextState; textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; - Log.Information($"{nameof(this.ClearState)}"); + // Log.Information($"{nameof(this.ClearState)}"); } private void LoadCand(HIMC hImc) diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs index fcd90c95a..1110ff387 100644 --- a/Dalamud/Interface/Internal/WndProcHookManager.cs +++ b/Dalamud/Interface/Internal/WndProcHookManager.cs @@ -112,19 +112,21 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable if (uMsg == this.unhookSelfMessage) { - // Remove self from the chain. - SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); - lock (this.wndProcNextDict) - this.wndProcNextDict.Remove(hwnd); - // Even though this message is dedicated for our processing, // satisfy the expectations by calling the next window procedure. - return CallWindowProcW( + var rv = CallWindowProcW( (delegate* unmanaged)nextProc, hwnd, uMsg, wParam, lParam); + + // Remove self from the chain. + SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); + lock (this.wndProcNextDict) + this.wndProcNextDict.Remove(hwnd); + + return rv; } var arg = new WndProcOverrideEventArgs(hwnd, ref uMsg, ref wParam, ref lParam); From 0afb3d2c8af5cb538fd88f0d9d113ee21ff036f6 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 13:33:40 +0900 Subject: [PATCH 387/585] Better WndProc handling --- .../Hooking/WndProcHook/WndProcEventArgs.cs | 144 +++++++++ .../WndProcHook/WndProcEventDelegate.cs | 7 + .../Hooking/WndProcHook/WndProcHookManager.cs | 115 ++++++++ Dalamud/Interface/Internal/DalamudIme.cs | 33 ++- .../Interface/Internal/InterfaceManager.cs | 10 +- .../Interface/Internal/WndProcHookManager.cs | 275 ------------------ 6 files changed, 294 insertions(+), 290 deletions(-) create mode 100644 Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs create mode 100644 Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs create mode 100644 Dalamud/Hooking/WndProcHook/WndProcHookManager.cs delete mode 100644 Dalamud/Interface/Internal/WndProcHookManager.cs diff --git a/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs b/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs new file mode 100644 index 000000000..b25df5d14 --- /dev/null +++ b/Dalamud/Hooking/WndProcHook/WndProcEventArgs.cs @@ -0,0 +1,144 @@ +using System.Runtime.InteropServices; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Hooking.WndProcHook; + +/// +/// Event arguments for , +/// and the manager for individual WndProc hook. +/// +internal sealed unsafe class WndProcEventArgs +{ + private readonly WndProcHookManager owner; + private readonly delegate* unmanaged oldWndProcW; + private readonly WndProcDelegate myWndProc; + + private GCHandle gcHandle; + private bool released; + + /// + /// Initializes a new instance of the class. + /// + /// The owner. + /// The handle of the target window of the message. + /// The viewport ID. + internal WndProcEventArgs(WndProcHookManager owner, HWND hwnd, int viewportId) + { + this.Hwnd = hwnd; + this.owner = owner; + this.ViewportId = viewportId; + this.myWndProc = this.WndProcDetour; + this.oldWndProcW = (delegate* unmanaged)SetWindowLongPtrW( + hwnd, + GWLP.GWLP_WNDPROC, + Marshal.GetFunctionPointerForDelegate(this.myWndProc)); + this.gcHandle = GCHandle.Alloc(this); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam); + + /// + /// Gets the handle of the target window of the message. + /// + public HWND Hwnd { get; } + + /// + /// Gets the ImGui viewport ID. + /// + public int ViewportId { get; } + + /// + /// Gets or sets the message. + /// + public uint Message { get; set; } + + /// + /// Gets or sets the WPARAM. + /// + public WPARAM WParam { get; set; } + + /// + /// Gets or sets the LPARAM. + /// + public LPARAM LParam { get; set; } + + /// + /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
+ /// Does nothing if changed from . + ///
+ public bool SuppressCall { get; set; } + + /// + /// Gets or sets the return value.
+ /// Has the return value from next window procedure, if accessed from . + ///
+ public LRESULT ReturnValue { get; set; } + + /// + /// Sets to true and sets . + /// + /// The new return value. + public void SuppressWithValue(LRESULT returnValue) + { + this.ReturnValue = returnValue; + this.SuppressCall = true; + } + + /// + /// Sets to true and sets from the result of + /// . + /// + public void SuppressWithDefault() + { + this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam); + this.SuppressCall = true; + } + + /// + internal void InternalRelease() + { + if (this.released) + return; + + this.released = true; + SendMessageW(this.Hwnd, WM.WM_NULL, 0, 0); + this.FinalRelease(); + } + + private void FinalRelease() + { + if (!this.gcHandle.IsAllocated) + return; + + this.gcHandle.Free(); + SetWindowLongPtrW(this.Hwnd, GWLP.GWLP_WNDPROC, (nint)this.oldWndProcW); + this.owner.OnHookedWindowRemoved(this); + } + + private LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + if (hwnd != this.Hwnd) + return CallWindowProcW(this.oldWndProcW, hwnd, uMsg, wParam, lParam); + + this.SuppressCall = false; + this.ReturnValue = 0; + this.Message = uMsg; + this.WParam = wParam; + this.LParam = lParam; + this.owner.InvokePreWndProc(this); + + if (!this.SuppressCall) + this.ReturnValue = CallWindowProcW(this.oldWndProcW, hwnd, uMsg, wParam, lParam); + + this.owner.InvokePostWndProc(this); + + if (uMsg == WM.WM_NCDESTROY || this.released) + this.FinalRelease(); + + return this.ReturnValue; + } +} diff --git a/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs b/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs new file mode 100644 index 000000000..f753f16cc --- /dev/null +++ b/Dalamud/Hooking/WndProcHook/WndProcEventDelegate.cs @@ -0,0 +1,7 @@ +namespace Dalamud.Hooking.WndProcHook; + +/// +/// Delegate for overriding WndProc. +/// +/// The arguments. +internal delegate void WndProcEventDelegate(WndProcEventArgs args); diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs new file mode 100644 index 000000000..00934f27f --- /dev/null +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -0,0 +1,115 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Hooking.WndProcHook; + +/// +/// Manages WndProc hooks for game main window and extra ImGui viewport windows. +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed class WndProcHookManager : IServiceType, IDisposable +{ + private static readonly ModuleLog Log = new(nameof(WndProcHookManager)); + + private readonly Hook dispatchMessageWHook; + private readonly Dictionary wndProcOverrides = new(); + + [ServiceManager.ServiceConstructor] + private unsafe WndProcHookManager() + { + this.dispatchMessageWHook = Hook.FromImport( + null, + "user32.dll", + "DispatchMessageW", + 0, + this.DispatchMessageWDetour); + this.dispatchMessageWHook.Enable(); + } + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + private unsafe delegate nint DispatchMessageWDelegate(MSG* msg); + + /// + /// Called before WndProc. + /// + public event WndProcEventDelegate? PreWndProc; + + /// + /// Called after WndProc. + /// + public event WndProcEventDelegate? PostWndProc; + + /// + public void Dispose() + { + this.dispatchMessageWHook.Dispose(); + foreach (var v in this.wndProcOverrides.Values) + v.InternalRelease(); + this.wndProcOverrides.Clear(); + } + + /// + /// Invokes . + /// + /// The arguments. + internal void InvokePreWndProc(WndProcEventArgs args) + { + try + { + this.PreWndProc?.Invoke(args); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PreWndProc)} error"); + } + } + + /// + /// Invokes . + /// + /// The arguments. + internal void InvokePostWndProc(WndProcEventArgs args) + { + try + { + this.PostWndProc?.Invoke(args); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.PostWndProc)} error"); + } + } + + /// + /// Removes from the list of known WndProc overrides. + /// + /// Object to remove. + internal void OnHookedWindowRemoved(WndProcEventArgs args) + { + if (!this.dispatchMessageWHook.IsDisposed) + this.wndProcOverrides.Remove(args.Hwnd); + } + + /// + /// Detour for . Used to discover new windows to hook. + /// + /// The message. + /// The original return value. + private unsafe nint DispatchMessageWDetour(MSG* msg) + { + if (!this.wndProcOverrides.ContainsKey(msg->hwnd) + && ImGuiHelpers.FindViewportId(msg->hwnd) is var vpid and >= 0) + { + this.wndProcOverrides[msg->hwnd] = new(this, msg->hwnd, vpid); + } + + return this.dispatchMessageWHook.Original(msg); + } +} diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index b3252546a..9bd9a2498 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -8,6 +8,7 @@ using System.Text; using System.Text.Unicode; using Dalamud.Game.Text; +using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -77,6 +78,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { get { + if (!ImGuiHelpers.IsImGuiInitialized) + return true; if (!ImGui.GetIO().ConfigInputTextCursorBlink) return true; ref var textState = ref TextState; @@ -185,7 +188,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType /// Processes window messages. ///
/// The arguments. - public void ProcessImeMessage(ref WndProcHookManager.WndProcOverrideEventArgs args) + public void ProcessImeMessage(WndProcEventArgs args) { if (!ImGuiHelpers.IsImGuiInitialized) return; @@ -208,11 +211,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: this.UpdateImeWindowStatus(hImc); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_STARTCOMPOSITION: - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_COMPOSITION: @@ -222,22 +225,22 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.ReplaceCompositionString(hImc, (uint)args.LParam); // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_ENDCOMPOSITION: // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_CONTROL: // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_REQUEST: // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); - args.SuppressAndReturn(0); + args.SuppressWithValue(0); break; case WM.WM_IME_SETCONTEXT: @@ -298,7 +301,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return new(data, 0, numBytes / 2); } - private void ReleaseUnmanagedResources() => ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + private void ReleaseUnmanagedResources() + { + if (ImGuiHelpers.IsImGuiInitialized) + ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + } private void UpdateInputLanguage(HIMC hImc) { @@ -492,8 +499,16 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] - private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) => + private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) + { + if (!ImGuiHelpers.IsImGuiInitialized) + { + throw new InvalidOperationException( + $"Expected {nameof(InterfaceManager.InterfaceManagerWithScene)} to have initialized ImGui."); + } + ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + } /// /// Ported from imstb_textedit.h. diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 49dfdb248..48157fa86 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -15,6 +15,7 @@ using Dalamud.Game.ClientState.GamePad; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; +using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; @@ -659,16 +660,13 @@ internal class InterfaceManager : IDisposable, IServiceType this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; } - private unsafe void WndProcHookManagerOnPreWndProc(ref WndProcHookManager.WndProcOverrideEventArgs args) + private unsafe void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) { var r = this.scene?.ProcessWndProcW(args.Hwnd, (User32.WindowMessage)args.Message, args.WParam, args.LParam); if (r is not null) - { - args.ReturnValue = r.Value; - args.SuppressCall = true; - } + args.SuppressWithValue(r.Value); - this.dalamudIme.ProcessImeMessage(ref args); + this.dalamudIme.ProcessImeMessage(args); } /* diff --git a/Dalamud/Interface/Internal/WndProcHookManager.cs b/Dalamud/Interface/Internal/WndProcHookManager.cs deleted file mode 100644 index 1110ff387..000000000 --- a/Dalamud/Interface/Internal/WndProcHookManager.cs +++ /dev/null @@ -1,275 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -using Dalamud.Hooking; -using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; - -using TerraFX.Interop.Windows; - -using static TerraFX.Interop.Windows.Windows; - -namespace Dalamud.Interface.Internal; - -/// -/// A manifestation of "I can't believe this is required". -/// -[ServiceManager.BlockingEarlyLoadedService] -internal sealed class WndProcHookManager : IServiceType, IDisposable -{ - private static readonly ModuleLog Log = new("WPHM"); - - private readonly Hook dispatchMessageWHook; - private readonly Dictionary wndProcNextDict = new(); - private readonly WndProcDelegate wndProcDelegate; - private readonly uint unhookSelfMessage; - private bool disposed; - - [ServiceManager.ServiceConstructor] - private unsafe WndProcHookManager() - { - this.wndProcDelegate = this.WndProcDetour; - this.dispatchMessageWHook = Hook.FromImport( - null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); - this.dispatchMessageWHook.Enable(); - fixed (void* pMessageName = $"{nameof(WndProcHookManager)}.{nameof(this.unhookSelfMessage)}") - this.unhookSelfMessage = RegisterWindowMessageW((ushort*)pMessageName); - } - - /// - /// Finalizes an instance of the class. - /// - ~WndProcHookManager() => this.ReleaseUnmanagedResources(); - - /// - /// Delegate for overriding WndProc. - /// - /// The arguments. - public delegate void WndProcOverrideDelegate(ref WndProcOverrideEventArgs args); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate LRESULT WndProcDelegate(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate nint DispatchMessageWDelegate(ref MSG msg); - - /// - /// Called before WndProc. - /// - public event WndProcOverrideDelegate? PreWndProc; - - /// - /// Called after WndProc. - /// - public event WndProcOverrideDelegate? PostWndProc; - - /// - public void Dispose() - { - this.disposed = true; - this.dispatchMessageWHook.Dispose(); - this.ReleaseUnmanagedResources(); - GC.SuppressFinalize(this); - } - - /// - /// Detour for . Used to discover new windows to hook. - /// - /// The message. - /// The original return value. - private unsafe nint DispatchMessageWDetour(ref MSG msg) - { - lock (this.wndProcNextDict) - { - if (!this.disposed && ImGuiHelpers.FindViewportId(msg.hwnd) >= 0 && - !this.wndProcNextDict.ContainsKey(msg.hwnd)) - { - this.wndProcNextDict[msg.hwnd] = SetWindowLongPtrW( - msg.hwnd, - GWLP.GWLP_WNDPROC, - Marshal.GetFunctionPointerForDelegate(this.wndProcDelegate)); - } - } - - return this.dispatchMessageWHook.IsDisposed - ? DispatchMessageW((MSG*)Unsafe.AsPointer(ref msg)) - : this.dispatchMessageWHook.Original(ref msg); - } - - private unsafe LRESULT WndProcDetour(HWND hwnd, uint uMsg, WPARAM wParam, LPARAM lParam) - { - nint nextProc; - lock (this.wndProcNextDict) - { - if (!this.wndProcNextDict.TryGetValue(hwnd, out nextProc)) - { - // Something went wrong; prevent crash. Things will, regardless of the effort, break. - return DefWindowProcW(hwnd, uMsg, wParam, lParam); - } - } - - if (uMsg == this.unhookSelfMessage) - { - // Even though this message is dedicated for our processing, - // satisfy the expectations by calling the next window procedure. - var rv = CallWindowProcW( - (delegate* unmanaged)nextProc, - hwnd, - uMsg, - wParam, - lParam); - - // Remove self from the chain. - SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); - lock (this.wndProcNextDict) - this.wndProcNextDict.Remove(hwnd); - - return rv; - } - - var arg = new WndProcOverrideEventArgs(hwnd, ref uMsg, ref wParam, ref lParam); - try - { - this.PreWndProc?.Invoke(ref arg); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(this.PostWndProc)} error"); - } - - if (!arg.SuppressCall) - { - try - { - arg.ReturnValue = CallWindowProcW( - (delegate* unmanaged)nextProc, - hwnd, - uMsg, - wParam, - lParam); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(CallWindowProcW)} error; probably some other software's fault"); - } - - try - { - this.PostWndProc?.Invoke(ref arg); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(this.PostWndProc)} error"); - } - } - - if (uMsg == WM.WM_NCDESTROY) - { - // The window will cease to exist, once we return. - SetWindowLongPtrW(hwnd, GWLP.GWLP_WNDPROC, nextProc); - lock (this.wndProcNextDict) - this.wndProcNextDict.Remove(hwnd); - } - - return arg.ReturnValue; - } - - private void ReleaseUnmanagedResources() - { - this.disposed = true; - - // As wndProcNextDict will be touched on each SendMessageW call, make a copy of window list first. - HWND[] windows; - lock (this.wndProcNextDict) - windows = this.wndProcNextDict.Keys.ToArray(); - - // Unregister our hook from all the windows we hooked. - foreach (var v in windows) - SendMessageW(v, this.unhookSelfMessage, default, default); - } - - /// - /// Parameters for . - /// - public ref struct WndProcOverrideEventArgs - { - /// - /// The handle of the target window of the message. - /// - public readonly HWND Hwnd; - - /// - /// The message. - /// - public ref uint Message; - - /// - /// The WPARAM. - /// - public ref WPARAM WParam; - - /// - /// The LPARAM. - /// - public ref LPARAM LParam; - - /// - /// Initializes a new instance of the struct. - /// - /// The handle of the target window of the message. - /// The message. - /// The WPARAM. - /// The LPARAM. - public WndProcOverrideEventArgs(HWND hwnd, ref uint msg, ref WPARAM wParam, ref LPARAM lParam) - { - this.Hwnd = hwnd; - this.LParam = ref lParam; - this.WParam = ref wParam; - this.Message = ref msg; - this.ViewportId = ImGuiHelpers.FindViewportId(hwnd); - } - - /// - /// Gets or sets a value indicating whether to suppress calling the next WndProc in the chain.
- /// Does nothing if changed from . - ///
- public bool SuppressCall { get; set; } - - /// - /// Gets or sets the return value.
- /// Has the return value from next window procedure, if accessed from . - ///
- public LRESULT ReturnValue { get; set; } - - /// - /// Gets the ImGui viewport ID. - /// - public int ViewportId { get; init; } - - /// - /// Gets a value indicating whether this message is for the game window (the first viewport). - /// - public bool IsGameWindow => this.ViewportId == 0; - - /// - /// Sets to true and sets . - /// - /// The new return value. - public void SuppressAndReturn(LRESULT returnValue) - { - this.ReturnValue = returnValue; - this.SuppressCall = true; - } - - /// - /// Sets to true and calls . - /// - public void SuppressWithDefault() - { - this.ReturnValue = DefWindowProcW(this.Hwnd, this.Message, this.WParam, this.LParam); - this.SuppressCall = true; - } - } -} From 6fefc3bee0692310ed14babecba948e3f9777c69 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 17 Dec 2023 14:09:38 +0900 Subject: [PATCH 388/585] Safer unload --- .../Hooking/WndProcHook/WndProcHookManager.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs index 00934f27f..91020f898 100644 --- a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Runtime.InteropServices; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -21,6 +22,8 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable private readonly Hook dispatchMessageWHook; private readonly Dictionary wndProcOverrides = new(); + private HWND mainWindowHwnd; + [ServiceManager.ServiceConstructor] private unsafe WndProcHookManager() { @@ -31,6 +34,12 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable 0, this.DispatchMessageWDetour); this.dispatchMessageWHook.Enable(); + + // Capture the game main window handle, + // so that no guarantees would have to be made on the service dispose order. + Service + .GetAsync() + .ContinueWith(r => this.mainWindowHwnd = (HWND)r.Result.Manager.GameWindowHandle); } [UnmanagedFunctionPointer(CallingConvention.StdCall)] @@ -49,7 +58,19 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable /// public void Dispose() { + if (this.dispatchMessageWHook.IsDisposed) + return; + this.dispatchMessageWHook.Dispose(); + + // Ensure that either we're on the main thread, or DispatchMessage is executed at least once. + // The game calls DispatchMessageW only from its main thread, so if we're already on one, + // this line does nothing; if not, it will require a cycle of GetMessage ... DispatchMessageW, + // which at the point of returning from DispatchMessageW(=point of returning from SendMessageW), + // the hook would be guaranteed to be fully disabled and detour delegate would be safe to be released. + SendMessageW(this.mainWindowHwnd, WM.WM_NULL, 0, 0); + + // Now this.wndProcOverrides cannot be touched from other thread. foreach (var v in this.wndProcOverrides.Values) v.InternalRelease(); this.wndProcOverrides.Clear(); From 9ca2d34f956c2a904c325fcb5816b6c00165f1c9 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 19 Dec 2023 09:07:45 +0100 Subject: [PATCH 389/585] Update ClientStructs (#1577) 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 3364dfea7..edc754348 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 3364dfea769b79e43aebaa955b6b98ec1d6eb458 +Subproject commit edc754348a3ed8fd49da6695248bfebe7ba89c12 From 6eb8153a99acb83cc321474ac22251b59e076a27 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 22 Dec 2023 10:56:01 +0900 Subject: [PATCH 390/585] Add missing EmptyClipboard (#1584) * Add missing EmptyClipboard * Fix missing GlobalUnlock --- Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index fd07d824f..1746fb1c4 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -131,6 +131,7 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis ptr[str.Length] = default; GlobalUnlock(hMem); + EmptyClipboard(); SetClipboardData(CF.CF_UNICODETEXT, hMem); } catch (Exception e) @@ -158,9 +159,9 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis return this.clipboardData.Data; } + var hMem = (HGLOBAL)GetClipboardData(CF.CF_UNICODETEXT); try { - var hMem = (HGLOBAL)GetClipboardData(CF.CF_UNICODETEXT); if (hMem != default) { var ptr = (char*)GlobalLock(hMem); @@ -191,6 +192,8 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis } finally { + if (hMem != default) + GlobalUnlock(hMem); CloseClipboard(); } From e015da0447ae20216617913d10db77e9bb18bb6e Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:10:44 -0800 Subject: [PATCH 391/585] Improve Dalamud ConsoleWindow plugin search (#1582) * Improve Dalamud ConsoleWindow plugin search * Improve Dalamud ConsoleWindow plugin search * Add `no results` message to plugin filter --- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 89dd153cc..770582a30 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -39,6 +39,7 @@ internal class ConsoleWindow : Window, IDisposable private string commandText = string.Empty; private string textFilter = string.Empty; private string selectedSource = "DalamudInternal"; + private string pluginFilter = string.Empty; private bool filterShowUncaughtExceptions; private bool showFilterToolbar; @@ -475,14 +476,24 @@ internal class ConsoleWindow : Window, IDisposable ImGui.TableNextColumn(); ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.BeginCombo("##Sources", this.selectedSource)) + if (ImGui.BeginCombo("##Sources", this.selectedSource, ImGuiComboFlags.HeightLarge)) { var sourceNames = Service.Get().InstalledPlugins .Select(p => p.Manifest.InternalName) .OrderBy(s => s) .Prepend("DalamudInternal") + .Where(name => this.pluginFilter is "" || new FuzzyMatcher(this.pluginFilter.ToLowerInvariant(), MatchMode.Fuzzy).Matches(name.ToLowerInvariant()) != 0) .ToList(); + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.InputTextWithHint("##PluginSearchFilter", "Filter Plugin List", ref this.pluginFilter, 2048); + ImGui.Separator(); + + if (!sourceNames.Any()) + { + ImGui.TextColored(KnownColor.OrangeRed.Vector(), "No Results"); + } + foreach (var selectable in sourceNames) { if (ImGui.Selectable(selectable, this.selectedSource == selectable)) From c0bb3aebc278a983b01bb77ef9cecaecb5efe8bd Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 23 Dec 2023 19:24:41 +0900 Subject: [PATCH 392/585] Fix crashes from Ctrl+Z when having IME activated (#1587) --- Dalamud/Interface/Internal/DalamudIme.cs | 166 +++++++++++++++++------ 1 file changed, 122 insertions(+), 44 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 9bd9a2498..70e230c5f 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Runtime.CompilerServices; @@ -11,7 +12,6 @@ using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; using ImGuiNET; @@ -27,7 +27,9 @@ namespace Dalamud.Interface.Internal; [ServiceManager.EarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType { - private static readonly ModuleLog Log = new("IME"); + private const int ImGuiContextTextStateOffset = 0x4588; + private const int CImGuiStbTextCreateUndoOffset = 0xB57A0; + private const int CImGuiStbTextUndoOffset = 0xB59C0; private static readonly UnicodeRange[] HanRange = { @@ -49,8 +51,40 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType UnicodeRanges.HangulJamoExtendedB, }; + private static readonly delegate* unmanaged + StbTextMakeUndoReplace; + + private static readonly delegate* unmanaged StbTextUndo; + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; + private (int Start, int End, int Cursor)? temporaryUndoSelection; + + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1003:Symbols should be spaced correctly", Justification = ".")] + static DalamudIme() + { + nint cimgui; + try + { + _ = ImGui.GetCurrentContext(); + + cimgui = Process.GetCurrentProcess().Modules.Cast() + .First(x => x.ModuleName == "cimgui.dll") + .BaseAddress; + } + catch + { + return; + } + + StbTextMakeUndoReplace = + (delegate* unmanaged) + (cimgui + CImGuiStbTextCreateUndoOffset); + StbTextUndo = + (delegate* unmanaged) + (cimgui + CImGuiStbTextUndoOffset); + } + [ServiceManager.ServiceConstructor] private DalamudIme() => this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData; @@ -82,12 +116,12 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return true; if (!ImGui.GetIO().ConfigInputTextCursorBlink) return true; - ref var textState = ref TextState; - if (textState.Id == 0 || (textState.Flags & ImGuiInputTextFlags.ReadOnly) != 0) + var textState = TextState; + if (textState->Id == 0 || (textState->Flags & ImGuiInputTextFlags.ReadOnly) != 0) return true; - if (textState.CursorAnim <= 0) + if (textState->CursorAnim <= 0) return true; - return textState.CursorAnim % 1.2f <= 0.8f; + return textState->CursorAnim % 1.2f <= 0.8f; } } @@ -142,7 +176,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ///
internal char InputModeIcon { get; private set; } - private static ref ImGuiInputTextState TextState => ref *(ImGuiInputTextState*)(ImGui.GetCurrentContext() + 0x4588); + private static ImGuiInputTextState* TextState => + (ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextTextStateOffset); /// public void Dispose() @@ -203,7 +238,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType try { - var invalidTarget = TextState.Id == 0 || (TextState.Flags & ImGuiInputTextFlags.ReadOnly) != 0; + var invalidTarget = TextState->Id == 0 || (TextState->Flags & ImGuiInputTextFlags.ReadOnly) != 0; switch (args.Message) { @@ -362,41 +397,26 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private void ReplaceCompositionString(HIMC hImc, uint comp) { - ref var textState = ref TextState; var finalCommit = (comp & GCS.GCS_RESULTSTR) != 0; - - ref var s = ref textState.Stb.SelectStart; - ref var e = ref textState.Stb.SelectEnd; - ref var c = ref textState.Stb.Cursor; - s = Math.Clamp(s, 0, textState.CurLenW); - e = Math.Clamp(e, 0, textState.CurLenW); - c = Math.Clamp(c, 0, textState.CurLenW); - if (s == e) - s = e = c; - if (s > e) - (s, e) = (e, s); - var newString = finalCommit ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); this.ReflectCharacterEncounters(newString); - if (s != e) - textState.DeleteChars(s, e - s); - textState.InsertChars(s, newString); + if (this.temporaryUndoSelection is not null) + { + TextState->Undo(); + TextState->SelectionTuple = this.temporaryUndoSelection.Value; + this.temporaryUndoSelection = null; + } - if (finalCommit) - s = e = s + newString.Length; - else - e = s + newString.Length; + TextState->SanitizeSelectionRange(); + if (TextState->ReplaceSelectionAndPushUndo(newString)) + this.temporaryUndoSelection = TextState->SelectionTuple; - this.ImmComp = finalCommit ? string.Empty : newString; - - this.CompositionCursorOffset = - finalCommit - ? 0 - : ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); + // Put the cursor at the beginning, so that the candidate window appears aligned with the text. + TextState->SetSelectionRange(TextState->SelectionTuple.Start, newString.Length, 0); if (finalCommit) { @@ -404,6 +424,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return; } + this.ImmComp = newString; + this.CompositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); + if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); @@ -429,8 +452,6 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.PartialConversionTo = this.ImmComp.Length; } - // Put the cursor at the beginning, so that the candidate window appears aligned with the text. - c = s; this.UpdateImeWindowStatus(hImc); } @@ -439,13 +460,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.ImmComp = string.Empty; this.PartialConversionFrom = this.PartialConversionTo = 0; this.CompositionCursorOffset = 0; - TextState.Stb.SelectStart = TextState.Stb.Cursor = TextState.Stb.SelectEnd; + this.temporaryUndoSelection = null; + TextState->Stb.SelectStart = TextState->Stb.Cursor = TextState->Stb.SelectEnd; ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); this.UpdateImeWindowStatus(default); - ref var textState = ref TextState; - textState.Stb.Cursor = textState.Stb.SelectStart = textState.Stb.SelectEnd; - // Log.Information($"{nameof(this.ClearState)}"); } @@ -498,7 +517,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.AssociatedViewport = data.WantVisible ? viewport : default; } - [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui.dll to become available.")] + [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui context initialization.")] private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) { if (!ImGuiHelpers.IsImGuiInitialized) @@ -569,15 +588,71 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType public bool Edited; public ImGuiInputTextFlags Flags; - public ImVectorWrapper TextW => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + public ImVectorWrapper TextW => new((ImVector*)&this.ThisPtr->TextWRaw); - public ImVectorWrapper TextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + public (int Start, int End, int Cursor) SelectionTuple + { + get => (this.Stb.SelectStart, this.Stb.SelectEnd, this.Stb.Cursor); + set => (this.Stb.SelectStart, this.Stb.SelectEnd, this.Stb.Cursor) = value; + } - public ImVectorWrapper InitialTextA => new((ImVector*)Unsafe.AsPointer(ref this.TextWRaw)); + private ImGuiInputTextState* ThisPtr => (ImGuiInputTextState*)Unsafe.AsPointer(ref this); + + public void SetSelectionRange(int offset, int length, int relativeCursorOffset) + { + this.Stb.SelectStart = offset; + this.Stb.SelectEnd = offset + length; + if (relativeCursorOffset >= 0) + this.Stb.Cursor = this.Stb.SelectStart + relativeCursorOffset; + else + this.Stb.Cursor = this.Stb.SelectEnd + 1 + relativeCursorOffset; + this.SanitizeSelectionRange(); + } + + public void SanitizeSelectionRange() + { + ref var s = ref this.Stb.SelectStart; + ref var e = ref this.Stb.SelectEnd; + ref var c = ref this.Stb.Cursor; + s = Math.Clamp(s, 0, this.CurLenW); + e = Math.Clamp(e, 0, this.CurLenW); + c = Math.Clamp(c, 0, this.CurLenW); + if (s == e) + s = e = c; + if (s > e) + (s, e) = (e, s); + } + + public void Undo() => StbTextUndo(this.ThisPtr, &this.ThisPtr->Stb); + + public bool MakeUndoReplace(int offset, int oldLength, int newLength) + { + if (oldLength == 0 && newLength == 0) + return false; + + StbTextMakeUndoReplace(this.ThisPtr, &this.ThisPtr->Stb, offset, oldLength, newLength); + return true; + } + + public bool ReplaceSelectionAndPushUndo(ReadOnlySpan newText) + { + var off = this.Stb.SelectStart; + var len = this.Stb.SelectEnd - this.Stb.SelectStart; + return this.MakeUndoReplace(off, len, newText.Length) && this.ReplaceChars(off, len, newText); + } + + public bool ReplaceChars(int pos, int len, ReadOnlySpan newText) + { + this.DeleteChars(pos, len); + return this.InsertChars(pos, newText); + } // See imgui_widgets.cpp: STB_TEXTEDIT_DELETECHARS public void DeleteChars(int pos, int n) { + if (n == 0) + return; + var dst = this.TextW.Data + pos; // We maintain our buffer length in both UTF-8 and wchar formats @@ -596,6 +671,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // See imgui_widgets.cpp: STB_TEXTEDIT_INSERTCHARS public bool InsertChars(int pos, ReadOnlySpan newText) { + if (newText.Length == 0) + return true; + var isResizable = (this.Flags & ImGuiInputTextFlags.CallbackResize) != 0; var textLen = this.CurLenW; Debug.Assert(pos <= textLen, "pos <= text_len"); From b55875255837220c24a115468c5dc160977082d9 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 23 Dec 2023 02:25:08 -0800 Subject: [PATCH 393/585] chore: Consolidate on ImGuiColors (#1585) Uses ImGuiColors over KnownColor in most places so that themes override things properly. --- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 4 ++-- .../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 3 ++- .../Internal/Windows/Data/Widgets/IconBrowserWidget.cs | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 770582a30..53821d9df 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -150,7 +150,7 @@ internal class ConsoleWindow : Window, IDisposable { const string regexErrorString = "Regex Filter Error"; ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f); - ImGui.TextColored(KnownColor.OrangeRed.Vector(), regexErrorString); + ImGui.TextColored(ImGuiColors.DalamudRed, regexErrorString); } ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55 * ImGuiHelpers.GlobalScale), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); @@ -491,7 +491,7 @@ internal class ConsoleWindow : Window, IDisposable if (!sourceNames.Any()) { - ImGui.TextColored(KnownColor.OrangeRed.Vector(), "No Results"); + ImGui.TextColored(ImGuiColors.DalamudRed, "No Results"); } foreach (var selectable in sourceNames) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 0e654d316..5b2855298 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -3,6 +3,7 @@ using System.Drawing; using System.Linq; using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using ImGuiNET; @@ -130,7 +131,7 @@ public class AddonLifecycleWidget : IDataWindowWidget } else { - var color = receiveEventListener.Hook.IsEnabled ? KnownColor.Green.Vector() : KnownColor.OrangeRed.Vector(); + var color = receiveEventListener.Hook.IsEnabled ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed; var text = receiveEventListener.Hook.IsEnabled ? "Enabled" : "Disabled"; ImGui.TextColored(color, text); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs index dcae6e689..06c691cc9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/IconBrowserWidget.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using Dalamud.Data; +using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -154,7 +155,7 @@ public class IconBrowserWidget : IDataWindowWidget this.nullValues.Add(iconId); } - ImGui.GetWindowDrawList().AddRect(cursor, cursor + this.iconSize, ImGui.GetColorU32(KnownColor.White.Vector())); + ImGui.GetWindowDrawList().AddRect(cursor, cursor + this.iconSize, ImGui.GetColorU32(ImGuiColors.DalamudWhite)); } catch (Exception) { From c17897c6fbb0be5c2303e19d3b273284186f9819 Mon Sep 17 00:00:00 2001 From: marzent Date: Sat, 23 Dec 2023 11:27:24 +0100 Subject: [PATCH 394/585] Mark DalamudIme as BlockingEarlyLoadedService (#1579) --- Dalamud/Interface/Internal/DalamudIme.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 70e230c5f..e030b4e50 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -24,7 +24,7 @@ namespace Dalamud.Interface.Internal; /// /// This class handles CJK IME. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType { private const int ImGuiContextTextStateOffset = 0x4588; From a6b802b577f9ad3f2a4d3b435310f7a0f9e1aeed Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 24 Dec 2023 23:05:13 +0100 Subject: [PATCH 395/585] Update ClientStructs (#1586) 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 edc754348..97b814ca1 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit edc754348a3ed8fd49da6695248bfebe7ba89c12 +Subproject commit 97b814ca15d147911cdac3059623185a57984e0a From 02b1f6e42690d1c053a1c4b243131f97f5d331d2 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 31 Dec 2023 14:30:21 -0800 Subject: [PATCH 396/585] [AddonEventManager] Actually Ensure Thread Safety (#1589) * Actually make AddonEventManager thread safe * Ensure AddonEventHandlers are also thread safe Additionally, use Guid instead of strings * Make DalamudInternalKey readonly * Properly use ConcurrentDict features Fixes GUID not working --- .../Game/Addon/Events/AddonEventListener.cs | 10 ++- .../Game/Addon/Events/AddonEventManager.cs | 79 ++++++++----------- .../Addon/Events/PluginEventController.cs | 12 +-- 3 files changed, 43 insertions(+), 58 deletions(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventListener.cs b/Dalamud/Game/Addon/Events/AddonEventListener.cs index ceac38108..a2498d5a7 100644 --- a/Dalamud/Game/Addon/Events/AddonEventListener.cs +++ b/Dalamud/Game/Addon/Events/AddonEventListener.cs @@ -67,7 +67,10 @@ internal unsafe class AddonEventListener : IDisposable { if (node is null) return; - node->AddEvent(eventType, param, this.eventListener, (AtkResNode*)addon, false); + Service.Get().RunOnFrameworkThread(() => + { + node->AddEvent(eventType, param, this.eventListener, (AtkResNode*)addon, false); + }); } /// @@ -80,7 +83,10 @@ internal unsafe class AddonEventListener : IDisposable { if (node is null) return; - node->RemoveEvent(eventType, param, this.eventListener, false); + Service.Get().RunOnFrameworkThread(() => + { + node->RemoveEvent(eventType, param, this.eventListener, false); + }); } [UnmanagedCallersOnly] diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index af713a771..4231b0d09 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using System.Linq; +using System.Collections.Concurrent; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -9,7 +8,6 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -26,22 +24,19 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// /// PluginName for Dalamud Internal use. /// - public const string DalamudInternalKey = "Dalamud.Internal"; + public static readonly Guid DalamudInternalKey = Guid.NewGuid(); private static readonly ModuleLog Log = new("AddonEventManager"); [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycle = Service.Get(); - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - private readonly AddonLifecycleEventListener finalizeEventListener; private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; - private readonly List pluginEventControllers; + private readonly ConcurrentDictionary pluginEventControllers; private AddonCursorType? cursorOverride; @@ -51,10 +46,8 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType this.address = new AddonEventManagerAddressResolver(); this.address.Setup(sigScanner); - this.pluginEventControllers = new List - { - new(DalamudInternalKey), // Create entry for Dalamud's Internal Use. - }; + this.pluginEventControllers = new ConcurrentDictionary(); + this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController()); this.cursorOverride = null; @@ -73,7 +66,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType { this.onUpdateCursor.Dispose(); - foreach (var pluginEventController in this.pluginEventControllers) + foreach (var (_, pluginEventController) in this.pluginEventControllers) { pluginEventController.Dispose(); } @@ -90,16 +83,17 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// The event type for this event. /// The handler to call when event is triggered. /// IAddonEventHandle used to remove the event. - internal IAddonEventHandle? AddEvent(string pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + internal IAddonEventHandle? AddEvent(Guid pluginId, IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) { - if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported."); - - if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) + if (this.pluginEventControllers.TryGetValue(pluginId, out var controller)) { - return eventController.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler); + return controller.AddEvent(atkUnitBase, atkResNode, eventType, eventHandler); + } + else + { + Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); } - Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); return null; } @@ -108,13 +102,11 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// /// Unique ID for this plugin. /// The Unique Id for this event. - internal void RemoveEvent(string pluginId, IAddonEventHandle eventHandle) + internal void RemoveEvent(Guid pluginId, IAddonEventHandle eventHandle) { - if (!ThreadSafety.IsMainThread) throw new InvalidOperationException("This should be done only from the main thread. Modifying active native code on non-main thread is not supported."); - - if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } eventController) + if (this.pluginEventControllers.TryGetValue(pluginId, out var controller)) { - eventController.RemoveEvent(eventHandle); + controller.RemoveEvent(eventHandle); } else { @@ -137,33 +129,28 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType /// Adds a new managed event controller if one doesn't already exist for this pluginId. ///
/// Unique ID for this plugin. - internal void AddPluginEventController(string pluginId) + internal void AddPluginEventController(Guid pluginId) { - this.framework.RunOnFrameworkThread(() => - { - if (this.pluginEventControllers.All(entry => entry.PluginId != pluginId)) + this.pluginEventControllers.GetOrAdd( + pluginId, + key => { - Log.Verbose($"Creating new PluginEventController for: {pluginId}"); - this.pluginEventControllers.Add(new PluginEventController(pluginId)); - } - }); + Log.Verbose($"Creating new PluginEventController for: {key}"); + return new PluginEventController(); + }); } /// /// Removes an existing managed event controller for the specified plugin. /// /// Unique ID for this plugin. - internal void RemovePluginEventController(string pluginId) + internal void RemovePluginEventController(Guid pluginId) { - this.framework.RunOnFrameworkThread(() => + if (this.pluginEventControllers.TryRemove(pluginId, out var controller)) { - if (this.pluginEventControllers.FirstOrDefault(entry => entry.PluginId == pluginId) is { } controller) - { - Log.Verbose($"Removing PluginEventController for: {pluginId}"); - this.pluginEventControllers.Remove(controller); - controller.Dispose(); - } - }); + Log.Verbose($"Removing PluginEventController for: {pluginId}"); + controller.Dispose(); + } } /// @@ -178,7 +165,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType foreach (var pluginList in this.pluginEventControllers) { - pluginList.RemoveForAddon(addonInfo.AddonName); + pluginList.Value.RemoveForAddon(addonInfo.AddonName); } } @@ -234,7 +221,7 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon { this.plugin = plugin; - this.eventManagerService.AddPluginEventController(plugin.Manifest.WorkingPluginId.ToString()); + this.eventManagerService.AddPluginEventController(plugin.Manifest.WorkingPluginId); } /// @@ -246,16 +233,16 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon this.eventManagerService.ResetCursor(); } - this.eventManagerService.RemovePluginEventController(this.plugin.Manifest.WorkingPluginId.ToString()); + this.eventManagerService.RemovePluginEventController(this.plugin.Manifest.WorkingPluginId); } /// public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) - => this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId.ToString(), atkUnitBase, atkResNode, eventType, eventHandler); + => this.eventManagerService.AddEvent(this.plugin.Manifest.WorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler); /// public void RemoveEvent(IAddonEventHandle eventHandle) - => this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId.ToString(), eventHandle); + => this.eventManagerService.RemoveEvent(this.plugin.Manifest.WorkingPluginId, eventHandle); /// public void SetCursor(AddonCursorType cursor) diff --git a/Dalamud/Game/Addon/Events/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs index 7847dd482..3ba067a6d 100644 --- a/Dalamud/Game/Addon/Events/PluginEventController.cs +++ b/Dalamud/Game/Addon/Events/PluginEventController.cs @@ -19,19 +19,11 @@ internal unsafe class PluginEventController : IDisposable /// /// Initializes a new instance of the class. /// - /// The Unique ID for this plugin. - public PluginEventController(string pluginId) + public PluginEventController() { - this.PluginId = pluginId; - this.EventListener = new AddonEventListener(this.PluginEventListHandler); } - /// - /// Gets the unique ID for this PluginEventList. - /// - public string PluginId { get; init; } - private AddonEventListener EventListener { get; init; } private List Events { get; } = new(); @@ -125,7 +117,7 @@ internal unsafe class PluginEventController : IDisposable if (this.Events.All(registeredEvent => registeredEvent.ParamKey != i)) return i; } - throw new OverflowException($"uint.MaxValue number of ParamKeys used for {this.PluginId}"); + throw new OverflowException($"uint.MaxValue number of ParamKeys used for this event controller."); } /// From 69096c440a8b2bc0e499bd872f86930058e39085 Mon Sep 17 00:00:00 2001 From: marzent Date: Mon, 1 Jan 2024 01:20:00 +0100 Subject: [PATCH 397/585] Allow plugins to load Dalamud dependency assemblies (#1580) --- Dalamud/Plugin/Internal/Loader/PluginLoader.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs index 5c03c32b8..53aec60ef 100644 --- a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs +++ b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs @@ -1,7 +1,7 @@ // Copyright (c) Nate McMaster, Dalamud team. // Licensed under the Apache License, Version 2.0. See License.txt in the Loader root for license information. -using System; +using System.IO; using System.Reflection; using System.Runtime.Loader; @@ -151,6 +151,14 @@ internal class PluginLoader : IDisposable builder.PreferDefaultLoadContextAssembly(assemblyName); } + // This allows plugins to search for dependencies in the Dalamud directory when their assembly + // load would otherwise fail, allowing them to resolve assemblies not already loaded by Dalamud + // itself yet. + builder.AddProbingPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); + + // Also make sure that plugins do not load their own Dalamud assembly. + builder.PreferDefaultLoadContextAssembly(Assembly.GetExecutingAssembly().GetName()); + return builder; } From 01cde50a468ebd80a1d05e336c9e05ef81c597a4 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Mon, 1 Jan 2024 07:11:09 -0800 Subject: [PATCH 398/585] chore: Suppress expected load errors (#1593) - Add new `PluginPreconditionFailedException` to track cases where a plugin could not be loaded due to a precondition not being met. - Make `BannedPluginException` inherit from this - Make `PluginPreconditionFailedException`s show as warnings in the log. --- .../Internal/Exceptions/BannedPluginException.cs | 9 ++------- .../PluginPreconditionFailedException.cs | 16 ++++++++++++++++ Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 15 +++++++++------ 3 files changed, 27 insertions(+), 13 deletions(-) create mode 100644 Dalamud/Plugin/Internal/Exceptions/PluginPreconditionFailedException.cs diff --git a/Dalamud/Plugin/Internal/Exceptions/BannedPluginException.cs b/Dalamud/Plugin/Internal/Exceptions/BannedPluginException.cs index 851e5be33..1119a8c4e 100644 --- a/Dalamud/Plugin/Internal/Exceptions/BannedPluginException.cs +++ b/Dalamud/Plugin/Internal/Exceptions/BannedPluginException.cs @@ -3,19 +3,14 @@ namespace Dalamud.Plugin.Internal.Exceptions; /// /// This represents a banned plugin that attempted an operation. /// -internal class BannedPluginException : PluginException +internal class BannedPluginException : PluginPreconditionFailedException { /// /// Initializes a new instance of the class. /// /// The message describing the invalid operation. public BannedPluginException(string message) + : base(message) { - this.Message = message; } - - /// - /// Gets the message describing the invalid operation. - /// - public override string Message { get; } } diff --git a/Dalamud/Plugin/Internal/Exceptions/PluginPreconditionFailedException.cs b/Dalamud/Plugin/Internal/Exceptions/PluginPreconditionFailedException.cs new file mode 100644 index 000000000..c1bb58d0d --- /dev/null +++ b/Dalamud/Plugin/Internal/Exceptions/PluginPreconditionFailedException.cs @@ -0,0 +1,16 @@ +namespace Dalamud.Plugin.Internal.Exceptions; + +/// +/// An exception to be thrown when policy blocks a plugin from loading. +/// +internal class PluginPreconditionFailedException : InvalidPluginOperationException +{ + /// + /// Initializes a new instance of the class. + /// + /// The message to associate with this exception. + public PluginPreconditionFailedException(string message) + : base(message) + { + } +} diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 91f1625a7..aff9a8b43 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -315,23 +315,23 @@ internal class LocalPlugin : IDisposable } if (pluginManager.IsManifestBanned(this.manifest) && !this.IsDev) - throw new BannedPluginException($"Unable to load {this.Name}, banned"); + throw new BannedPluginException($"Unable to load {this.Name} as it was banned"); if (this.manifest.ApplicableVersion < dalamud.StartInfo.GameVersion) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version"); + throw new PluginPreconditionFailedException($"Unable to load {this.Name}, game is newer than applicable version {this.manifest.ApplicableVersion}"); if (this.manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !pluginManager.LoadAllApiLevels) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level"); + throw new PluginPreconditionFailedException($"Unable to load {this.Name}, incompatible API level {this.manifest.DalamudApiLevel}"); // We might want to throw here? if (!this.IsWantedByAnyProfile) Log.Warning("{Name} is loading, but isn't wanted by any profile", this.Name); if (this.IsOrphaned) - throw new InvalidPluginOperationException($"Plugin {this.Name} had no associated repo."); + throw new PluginPreconditionFailedException($"Plugin {this.Name} had no associated repo"); if (!this.CheckPolicy()) - throw new InvalidPluginOperationException("Plugin was not loaded as per policy"); + throw new PluginPreconditionFailedException($"Unable to load {this.Name} as a load policy forbids it"); this.State = PluginState.Loading; Log.Information($"Loading {this.DllFile.Name}"); @@ -439,7 +439,10 @@ internal class LocalPlugin : IDisposable { this.State = PluginState.LoadError; - if (ex is not BannedPluginException) + // If a precondition fails, don't record it as an error, as it isn't really. + if (ex is PluginPreconditionFailedException) + Log.Warning(ex.Message); + else Log.Error(ex, $"Error while loading {this.Name}"); throw; From 8bdab4d2c8368edb37acdc4f0f7d17d75c40f753 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 21 Nov 2023 15:09:38 +0900 Subject: [PATCH 399/585] Implement DalamudFontAtlas --- Dalamud/Interface/GameFonts/FdtFileView.cs | 159 ++ .../GameFonts/GameFontFamilyAndSize.cs | 25 +- .../GameFontFamilyAndSizeAttribute.cs | 37 + Dalamud/Interface/GameFonts/GameFontHandle.cs | 83 +- .../Interface/GameFonts/GameFontManager.cs | 507 ------ Dalamud/Interface/GameFonts/GameFontStyle.cs | 2 +- .../Interface/Internal/DalamudInterface.cs | 13 +- .../Interface/Internal/InterfaceManager.cs | 932 +++-------- .../Internal/Windows/ChangelogWindow.cs | 62 +- .../Internal/Windows/Data/DataWindow.cs | 8 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 186 +++ .../Windows/Settings/SettingsWindow.cs | 19 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 30 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 63 +- .../FontAtlasAutoRebuildMode.cs | 22 + .../ManagedFontAtlas/FontAtlasBuildStep.cs | 38 + .../FontAtlasBuildStepDelegate.cs | 15 + .../FontAtlasBuildToolkitUtilities.cs | 111 ++ .../Interface/ManagedFontAtlas/IFontAtlas.cs | 84 + .../IFontAtlasBuildToolkit.cs | 67 + .../IFontAtlasBuildToolkitPostBuild.cs | 26 + .../IFontAtlasBuildToolkitPostPromotion.cs | 33 + .../IFontAtlasBuildToolkitPreBuild.cs | 164 ++ .../Interface/ManagedFontAtlas/IFontHandle.cs | 42 + .../Internals/DelegateFontHandle.cs | 331 ++++ .../FontAtlasFactory.BuildToolkit.cs | 647 ++++++++ .../FontAtlasFactory.Implementation.cs | 711 +++++++++ .../Internals/FontAtlasFactory.cs | 379 +++++ .../Internals/GamePrebakedFontHandle.cs | 692 ++++++++ .../Internals/IFontHandleManager.cs | 34 + .../Internals/IFontHandleSubstance.cs | 47 + .../Internals/TrueType.Common.cs | 203 +++ .../Internals/TrueType.Enums.cs | 84 + .../Internals/TrueType.Files.cs | 148 ++ .../Internals/TrueType.GposGsub.cs | 259 +++ .../Internals/TrueType.PointerSpan.cs | 443 ++++++ .../Internals/TrueType.Tables.cs | 1391 +++++++++++++++++ .../ManagedFontAtlas/Internals/TrueType.cs | 135 ++ .../ManagedFontAtlas/SafeFontConfig.cs | 291 ++++ Dalamud/Interface/UiBuilder.cs | 203 ++- Dalamud/Interface/Utility/ImGuiHelpers.cs | 253 ++- 41 files changed, 7551 insertions(+), 1428 deletions(-) create mode 100644 Dalamud/Interface/GameFonts/FdtFileView.cs create mode 100644 Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs delete mode 100644 Dalamud/Interface/GameFonts/GameFontManager.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs diff --git a/Dalamud/Interface/GameFonts/FdtFileView.cs b/Dalamud/Interface/GameFonts/FdtFileView.cs new file mode 100644 index 000000000..78b2e22f3 --- /dev/null +++ b/Dalamud/Interface/GameFonts/FdtFileView.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.IO; + +namespace Dalamud.Interface.GameFonts; + +/// +/// Reference member view of a .fdt file data. +/// +internal readonly unsafe ref struct FdtFileView +{ + private readonly byte* ptr; + + /// + /// Initializes a new instance of the struct. + /// + /// Pointer to the data. + /// Length of the data. + public FdtFileView(void* ptr, int length) + { + this.ptr = (byte*)ptr; + if (length < sizeof(FdtReader.FdtHeader)) + throw new InvalidDataException("Not enough space for a FdtHeader"); + + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader)) + throw new InvalidDataException("Not enough space for a FontTableHeader"); + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader) + + (sizeof(FdtReader.FontTableEntry) * this.FontHeader.FontTableEntryCount)) + throw new InvalidDataException("Not enough space for all the FontTableEntry"); + + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader)) + throw new InvalidDataException("Not enough space for a KerningTableHeader"); + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader) + + (sizeof(FdtReader.KerningTableEntry) * this.KerningEntryCount)) + throw new InvalidDataException("Not enough space for all the KerningTableEntry"); + } + + /// + /// Gets the file header. + /// + public ref FdtReader.FdtHeader FileHeader => ref *(FdtReader.FdtHeader*)this.ptr; + + /// + /// Gets the font header. + /// + public ref FdtReader.FontTableHeader FontHeader => + ref *(FdtReader.FontTableHeader*)((nint)this.ptr + this.FileHeader.FontTableHeaderOffset); + + /// + /// Gets the glyphs. + /// + public Span Glyphs => new(this.GlyphsUnsafe, this.FontHeader.FontTableEntryCount); + + /// + /// Gets the kerning header. + /// + public ref FdtReader.KerningTableHeader KerningHeader => + ref *(FdtReader.KerningTableHeader*)((nint)this.ptr + this.FileHeader.KerningTableHeaderOffset); + + /// + /// Gets the number of kerning entries. + /// + public int KerningEntryCount => Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); + + /// + /// Gets the kerning entries. + /// + public Span PairAdjustments => new( + this.ptr + this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader), + this.KerningEntryCount); + + /// + /// Gets the maximum texture index. + /// + public int MaxTextureIndex + { + get + { + var i = 0; + foreach (ref var g in this.Glyphs) + { + if (g.TextureIndex > i) + i = g.TextureIndex; + } + + return i; + } + } + + private FdtReader.FontTableEntry* GlyphsUnsafe => + (FdtReader.FontTableEntry*)(this.ptr + this.FileHeader.FontTableHeaderOffset + + sizeof(FdtReader.FontTableHeader)); + + /// + /// Finds the glyph index for the corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding index, or a negative number according to . + public int FindGlyphIndex(int codepoint) + { + var comp = FdtReader.CodePointToUtf8Int32(codepoint); + + var glyphs = this.GlyphsUnsafe; + var lo = 0; + var hi = this.FontHeader.FontTableEntryCount - 1; + while (lo <= hi) + { + var i = (int)(((uint)hi + (uint)lo) >> 1); + switch (comp.CompareTo(glyphs[i].CharUtf8)) + { + case 0: + return i; + case > 0: + lo = i + 1; + break; + default: + hi = i - 1; + break; + } + } + + return ~lo; + } + + /// + /// Create a glyph range for use with . + /// + /// Merge two ranges into one if distance is below the value specified in this parameter. + /// Glyph ranges. + public ushort[] ToGlyphRanges(int mergeDistance = 8) + { + var glyphs = this.Glyphs; + var ranges = new List(glyphs.Length) + { + checked((ushort)glyphs[0].CharInt), + checked((ushort)glyphs[0].CharInt), + }; + + foreach (ref var glyph in glyphs[1..]) + { + var c32 = glyph.CharInt; + if (c32 >= 0x10000) + break; + + var c16 = unchecked((ushort)c32); + if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) + { + ranges[^1] = c16; + } + else if (ranges[^1] + 1 < c16) + { + ranges.Add(c16); + ranges.Add(c16); + } + } + + ranges.Add(0); + return ranges.ToArray(); + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs index dd78baf87..6e66cf19b 100644 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Enum of available game fonts in specific sizes. /// -public enum GameFontFamilyAndSize : int +public enum GameFontFamilyAndSize { /// /// Placeholder meaning unused. @@ -15,6 +15,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_96.fdt", "common/font/font{0}.tex", -1)] Axis96, /// @@ -22,6 +23,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_12.fdt", "common/font/font{0}.tex", -1)] Axis12, /// @@ -29,6 +31,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_14.fdt", "common/font/font{0}.tex", -1)] Axis14, /// @@ -36,6 +39,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_18.fdt", "common/font/font{0}.tex", -1)] Axis18, /// @@ -43,6 +47,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_36.fdt", "common/font/font{0}.tex", -4)] Axis36, /// @@ -50,6 +55,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_16.fdt", "common/font/font{0}.tex", -1)] Jupiter16, /// @@ -57,6 +63,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_20.fdt", "common/font/font{0}.tex", -1)] Jupiter20, /// @@ -64,6 +71,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_23.fdt", "common/font/font{0}.tex", -1)] Jupiter23, /// @@ -71,6 +79,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)] Jupiter45, /// @@ -78,6 +87,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_46.fdt", "common/font/font{0}.tex", -2)] Jupiter46, /// @@ -85,6 +95,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)] Jupiter90, /// @@ -92,6 +103,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_16.fdt", "common/font/font{0}.tex", -1)] Meidinger16, /// @@ -99,6 +111,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_20.fdt", "common/font/font{0}.tex", -1)] Meidinger20, /// @@ -106,6 +119,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_40.fdt", "common/font/font{0}.tex", -4)] Meidinger40, /// @@ -113,6 +127,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)] MiedingerMid10, /// @@ -120,6 +135,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)] MiedingerMid12, /// @@ -127,6 +143,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)] MiedingerMid14, /// @@ -134,6 +151,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)] MiedingerMid18, /// @@ -141,6 +159,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)] MiedingerMid36, /// @@ -148,6 +167,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)] TrumpGothic184, /// @@ -155,6 +175,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)] TrumpGothic23, /// @@ -162,6 +183,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)] TrumpGothic34, /// @@ -169,5 +191,6 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_68.fdt", "common/font/font{0}.tex", -3)] TrumpGothic68, } diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs new file mode 100644 index 000000000..f5260e4bc --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Interface.GameFonts; + +/// +/// Marks the path for an enum value. +/// +[AttributeUsage(AttributeTargets.Field)] +internal class GameFontFamilyAndSizeAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Inner path of the file. + /// the file path format for the relevant .tex files. + /// Horizontal offset of the corresponding font. + public GameFontFamilyAndSizeAttribute(string path, string texPathFormat, int horizontalOffset) + { + this.Path = path; + this.TexPathFormat = texPathFormat; + this.HorizontalOffset = horizontalOffset; + } + + /// + /// Gets the path. + /// + public string Path { get; } + + /// + /// Gets the file path format for the relevant .tex files.
+ /// Used for (, ). + ///
+ public string TexPathFormat { get; } + + /// + /// Gets the horizontal offset of the corresponding font. + /// + public int HorizontalOffset { get; } +} diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index d71e725c5..77461aa0a 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,75 +1,76 @@ -using System; using System.Numerics; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; + using ImGuiNET; namespace Dalamud.Interface.GameFonts; /// -/// Prepare and keep game font loaded for use in OnDraw. +/// ABI-compatible wrapper for . /// -public class GameFontHandle : IDisposable +public sealed class GameFontHandle : IFontHandle { - private readonly GameFontManager manager; - private readonly GameFontStyle fontStyle; + private readonly IFontHandle.IInternal fontHandle; + private readonly FontAtlasFactory fontAtlasFactory; /// /// Initializes a new instance of the class. /// - /// GameFontManager instance. - /// Font to use. - internal GameFontHandle(GameFontManager manager, GameFontStyle font) + /// The wrapped . + /// An instance of . + internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) { - this.manager = manager; - this.fontStyle = font; + this.fontHandle = fontHandle; + this.fontAtlasFactory = fontAtlasFactory; } - /// - /// Gets the font style. - /// - public GameFontStyle Style => this.fontStyle; + /// + public Exception? LoadException => this.fontHandle.LoadException; + + /// + public bool Available => this.fontHandle.Available; + + /// + [Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)] + public ImFontPtr ImFont => this.fontHandle.ImFont; /// - /// Gets a value indicating whether this font is ready for use. + /// Gets the font style. Only applicable for . /// - public bool Available - { - get - { - unsafe - { - return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null; - } - } - } + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle; /// - /// Gets the font. + /// Gets the relevant .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
- public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!; + + /// + public void Dispose() => this.fontHandle.Dispose(); + + /// + public IDisposable Push() => this.fontHandle.Push(); /// - /// Gets the FdtReader. - /// - public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); - - /// - /// Creates a new GameFontLayoutPlan.Builder. + /// Creates a new .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
/// Text. /// A new builder for GameFontLayoutPlan. - public GameFontLayoutPlan.Builder LayoutBuilder(string text) - { - return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); - } - - /// - public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text); /// /// Draws text. /// /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void Text(string text) { if (!this.Available) @@ -93,6 +94,7 @@ public class GameFontHandle : IDisposable ///
/// Color. /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextColored(Vector4 col, string text) { ImGui.PushStyleColor(ImGuiCol.Text, col); @@ -104,6 +106,7 @@ public class GameFontHandle : IDisposable /// Draws disabled text. ///
/// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextDisabled(string text) { unsafe diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs deleted file mode 100644 index b3454e085..000000000 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ /dev/null @@ -1,507 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Utility.Timing; -using ImGuiNET; -using Lumina.Data.Files; -using Serilog; - -using static Dalamud.Interface.Utility.ImGuiHelpers; - -namespace Dalamud.Interface.GameFonts; - -/// -/// Loads game font for use in ImGui. -/// -[ServiceManager.BlockingEarlyLoadedService] -internal class GameFontManager : IServiceType -{ - private static readonly string?[] FontNames = - { - null, - "AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36", - "Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90", - "Meidinger_16", "Meidinger_20", "Meidinger_40", - "MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36", - "TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68", - }; - - private readonly object syncRoot = new(); - - private readonly FdtReader?[] fdts; - private readonly List texturePixels; - private readonly Dictionary fonts = new(); - private readonly Dictionary fontUseCounter = new(); - private readonly Dictionary>> glyphRectIds = new(); - -#pragma warning disable CS0414 - private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; -#pragma warning restore CS0414 - - [ServiceManager.ServiceConstructor] - private GameFontManager(DataManager dataManager) - { - using (Timings.Start("Getting fdt data")) - { - this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray(); - } - - using (Timings.Start("Getting texture data")) - { - var texTasks = Enumerable - .Range(1, 1 + this.fdts - .Where(x => x != null) - .Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max()) - .Max()) - .Select(x => dataManager.GetFile($"common/font/font{x}.tex")!) - .Select(x => new Task(Timings.AttachTimingHandle(() => x.ImageData!))) - .ToArray(); - foreach (var task in texTasks) - task.Start(); - this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList(); - } - } - - /// - /// Describe font into a string. - /// - /// Font to describe. - /// A string in a form of "FontName (NNNpt)". - public static string DescribeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Undefined => "-", - GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)", - GameFontFamilyAndSize.Axis12 => "AXIS (12pt)", - GameFontFamilyAndSize.Axis14 => "AXIS (14pt)", - GameFontFamilyAndSize.Axis18 => "AXIS (18pt)", - GameFontFamilyAndSize.Axis36 => "AXIS (36pt)", - GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)", - GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)", - GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)", - GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)", - GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)", - GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)", - GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)", - GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)", - GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)", - GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)", - GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)", - GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)", - GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)", - GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)", - GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)", - GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)", - GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)", - GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)", - _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), - }; - } - - /// - /// Determines whether a font should be able to display most of stuff. - /// - /// Font to check. - /// True if it can. - public static bool IsGenericPurposeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Axis96 => true, - GameFontFamilyAndSize.Axis12 => true, - GameFontFamilyAndSize.Axis14 => true, - GameFontFamilyAndSize.Axis18 => true, - GameFontFamilyAndSize.Axis36 => true, - _ => false, - }; - } - - /// - /// Unscales fonts after they have been rendered onto atlas. - /// - /// Font to unscale. - /// Scale factor. - /// Whether to call target.BuildLookupTable(). - public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true) - { - if (fontScale == 1) - return; - - unsafe - { - var font = fontPtr.NativePtr; - for (int i = 0, i_ = font->IndexedHotData.Size; i < i_; ++i) - { - font->IndexedHotData.Ref(i).AdvanceX /= fontScale; - font->IndexedHotData.Ref(i).OccupiedWidth /= fontScale; - } - - font->FontSize /= fontScale; - font->Ascent /= fontScale; - font->Descent /= fontScale; - if (font->ConfigData != null) - font->ConfigData->SizePixels /= fontScale; - var glyphs = (ImFontGlyphReal*)font->Glyphs.Data; - for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) - { - var glyph = &glyphs[i]; - glyph->X0 /= fontScale; - glyph->X1 /= fontScale; - glyph->Y0 /= fontScale; - glyph->Y1 /= fontScale; - glyph->AdvanceX /= fontScale; - } - - for (int i = 0, i_ = font->KerningPairs.Size; i < i_; i++) - font->KerningPairs.Ref(i).AdvanceXAdjustment /= fontScale; - for (int i = 0, i_ = font->FrequentKerningPairs.Size; i < i_; i++) - font->FrequentKerningPairs.Ref(i) /= fontScale; - } - - if (rebuildLookupTable && fontPtr.Glyphs.Size > 0) - fontPtr.BuildLookupTableNonstandard(); - } - - /// - /// Create a glyph range for use with ImGui AddFont. - /// - /// Font family and size. - /// Merge two ranges into one if distance is below the value specified in this parameter. - /// Glyph ranges. - public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8) - { - var fdt = this.fdts[(int)family]!; - var ranges = new List(fdt.Glyphs.Count) - { - checked((ushort)fdt.Glyphs[0].CharInt), - checked((ushort)fdt.Glyphs[0].CharInt), - }; - - foreach (var glyph in fdt.Glyphs.Skip(1)) - { - var c32 = glyph.CharInt; - if (c32 >= 0x10000) - break; - - var c16 = unchecked((ushort)c32); - if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) - { - ranges[^1] = c16; - } - else if (ranges[^1] + 1 < c16) - { - ranges.Add(c16); - ranges.Add(c16); - } - } - - return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned); - } - - /// - /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. - /// - /// Font to use. - /// Handle to game font that may or may not be ready yet. - public GameFontHandle NewFontRef(GameFontStyle style) - { - var interfaceManager = Service.Get(); - var needRebuild = false; - - lock (this.syncRoot) - { - this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1; - } - - needRebuild = !this.fonts.ContainsKey(style); - if (needRebuild) - { - Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString()); - Service.GetAsync() - .ContinueWith(task => task.Result.RunOnTick(() => interfaceManager.RebuildFonts())); - } - - return new(this, style); - } - - /// - /// Gets the font. - /// - /// Font to get. - /// Corresponding font or null. - public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null); - - /// - /// Gets the corresponding FdtReader. - /// - /// Font to get. - /// Corresponding FdtReader or null. - public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family]; - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Build fonts before plugins do something more. To be called from InterfaceManager. - /// - public void BuildFonts() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true; - - this.glyphRectIds.Clear(); - this.fonts.Clear(); - - lock (this.syncRoot) - { - foreach (var style in this.fontUseCounter.Keys) - this.EnsureFont(style); - } - } - - /// - /// Record that ImGui.GetIO().Fonts.Build() has been called. - /// - public void AfterIoFontsBuild() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; - } - - /// - /// Checks whether GameFontMamager owns an ImFont. - /// - /// ImFontPtr to check. - /// Whether it owns. - public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr); - - /// - /// Post-build fonts before plugins do something more. To be called from InterfaceManager. - /// - public unsafe void AfterBuildFonts() - { - var interfaceManager = Service.Get(); - var ioFonts = ImGui.GetIO().Fonts; - var fontGamma = interfaceManager.FontGamma; - - var pixels8s = new byte*[ioFonts.Textures.Size]; - var pixels32s = new uint*[ioFonts.Textures.Size]; - var widths = new int[ioFonts.Textures.Size]; - var heights = new int[ioFonts.Textures.Size]; - for (var i = 0; i < pixels8s.Length; i++) - { - ioFonts.GetTexDataAsRGBA32(i, out pixels8s[i], out widths[i], out heights[i]); - pixels32s[i] = (uint*)pixels8s[i]; - } - - foreach (var (style, font) in this.fonts) - { - var fdt = this.fdts[(int)style.FamilyAndSize]; - var scale = style.SizePt / fdt.FontHeader.Size; - var fontPtr = font.NativePtr; - - Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale); - - fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; - if (fontPtr->ConfigData != null) - fontPtr->ConfigData->SizePixels = fontPtr->FontSize; - fontPtr->Ascent = fdt.FontHeader.Ascent; - fontPtr->Descent = fdt.FontHeader.Descent; - fontPtr->EllipsisChar = '…'; - foreach (var fallbackCharCandidate in "〓?!") - { - var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); - if ((IntPtr)glyph.NativePtr != IntPtr.Zero) - { - var ptr = font.NativePtr; - ptr->FallbackChar = fallbackCharCandidate; - ptr->FallbackGlyph = glyph.NativePtr; - ptr->FallbackHotData = (ImFontGlyphHotData*)ptr->IndexedHotData.Address(fallbackCharCandidate); - break; - } - } - - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) - { - var rc = (ImFontAtlasCustomRectReal*)ioFonts.GetCustomRectByIndex(rectId).NativePtr; - var pixels8 = pixels8s[rc->TextureIndex]; - var pixels32 = pixels32s[rc->TextureIndex]; - var width = widths[rc->TextureIndex]; - var height = heights[rc->TextureIndex]; - var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; - var sourceBufferDelta = glyph.TextureChannelByteIndex; - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - if (widthAdjustment == 0) - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; - pixels32[((rc->Y + y) * width) + rc->X + x] = (uint)(a << 24) | 0xFFFFFFu; - } - } - } - else - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) - pixels32[((rc->Y + y) * width) + rc->X + x] = 0xFFFFFFu; - } - - for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++) - { - var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); - for (var y = 0; y < glyph.BoundingHeight; y++) - { - float xDelta = xbold; - if (style.BaseSkewStrength > 0) - xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; - else if (style.BaseSkewStrength < 0) - xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; - var xDeltaInt = (int)Math.Floor(xDelta); - var xness = xDelta - xDeltaInt; - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x; - var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; - var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))]; - var n = (a1 * xness) + (a2 * (1 - xness)); - var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; - pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n)); - } - } - } - } - - if (Math.Abs(fontGamma - 1.4f) >= 0.001) - { - // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) - for (int y = rc->Y, y_ = rc->Y + rc->Height; y < y_; y++) - { - for (int x = rc->X, x_ = rc->X + rc->Width; x < x_; x++) - { - var i = (((y * width) + x) * 4) + 3; - pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); - } - } - } - } - - UnscaleFont(font, 1 / scale, false); - } - } - - /// - /// Decrease font reference counter. - /// - /// Font to release. - internal void DecreaseFontRef(GameFontStyle style) - { - lock (this.syncRoot) - { - if (!this.fontUseCounter.ContainsKey(style)) - return; - - if ((this.fontUseCounter[style] -= 1) == 0) - this.fontUseCounter.Remove(style); - } - } - - private unsafe void EnsureFont(GameFontStyle style) - { - var rectIds = this.glyphRectIds[style] = new(); - - var fdt = this.fdts[(int)style.FamilyAndSize]; - if (fdt == null) - return; - - ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - fontConfig.PixelSnapH = false; - - var io = ImGui.GetIO(); - var font = io.Fonts.AddFontDefault(fontConfig); - - fontConfig.Destroy(); - - this.fonts[style] = font; - foreach (var glyph in fdt.Glyphs) - { - var c = glyph.Char; - if (c < 32 || c >= 0xFFFF) - continue; - - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - rectIds[c] = Tuple.Create( - io.Fonts.AddCustomRectFontGlyph( - font, - c, - glyph.BoundingWidth + widthAdjustment, - glyph.BoundingHeight, - glyph.AdvanceWidth, - new Vector2(0, glyph.CurrentOffsetY)), - glyph); - } - - foreach (var kernPair in fdt.Distances) - font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); - } -} diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index 946473df4..e219670b8 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -175,7 +175,7 @@ public struct GameFontStyle public bool Italic { get => this.SkewStrength != 0; - set => this.SkewStrength = value ? this.SizePx / 7 : 0; + set => this.SkewStrength = value ? this.SizePx / 6 : 0; } /// diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 95415659b..60c1f9957 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -93,7 +94,8 @@ internal class DalamudInterface : IDisposable, IServiceType private DalamudInterface( Dalamud dalamud, DalamudConfiguration configuration, - InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, + FontAtlasFactory fontAtlasFactory, + InterfaceManager interfaceManager, PluginImageCache pluginImageCache, DalamudAssetManager dalamudAssetManager, Game.Framework framework, @@ -103,7 +105,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.dalamud = dalamud; this.configuration = configuration; - this.interfaceManager = interfaceManagerWithScene.Manager; + this.interfaceManager = interfaceManager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -122,10 +124,14 @@ internal class DalamudInterface : IDisposable, IServiceType clientState, configuration, dalamudAssetManager, + fontAtlasFactory, framework, gameGui, titleScreenMenu) { IsOpen = false }; - this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; + this.changelogWindow = new ChangelogWindow( + this.titleScreenMenuWindow, + fontAtlasFactory, + dalamudAssetManager) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false }; @@ -207,6 +213,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.interfaceManager.Draw -= this.OnDraw; + this.WindowSystem.Windows.OfType().AggregateToDisposable().Dispose(); this.WindowSystem.RemoveAllWindows(); this.changelogWindow.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 48157fa86..5a6a2cbdb 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -19,10 +18,13 @@ using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Storage.Assets; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -64,11 +66,9 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. - private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. - - private readonly HashSet glyphRequests = new(); - private readonly Dictionary loadedFontInfo = new(); + private const int NonMainThreadFontAccessWarningCheckInterval = 10000; + private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); + private static long nextNonMainThreadFontAccessWarningCheck; private readonly List deferredDisposeTextures = new(); @@ -81,28 +81,28 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly ManualResetEvent fontBuildSignal; - private readonly SwapChainVtableResolver address; + private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; private RawDX11Scene? scene; private Hook? presentHook; private Hook? resizeBuffersHook; + private IFontAtlas? dalamudAtlas; + private IFontHandle.IInternal? defaultFontHandle; + private IFontHandle.IInternal? iconFontHandle; + private IFontHandle.IInternal? monoFontHandle; + // can't access imgui IO before first present call private bool lastWantCapture = false; - private bool isRebuildingFonts = false; private bool isOverrideGameCursor = true; + private IntPtr gameWindowHandle; [ServiceManager.ServiceConstructor] private InterfaceManager() { this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); - - this.fontBuildSignal = new ManualResetEvent(false); - - this.address = new SwapChainVtableResolver(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -117,43 +117,46 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// This event gets called each frame to facilitate ImGui drawing. /// - public event RawDX11Scene.BuildUIDelegate Draw; + public event RawDX11Scene.BuildUIDelegate? Draw; /// /// This event gets called when ResizeBuffers is called. /// - public event Action ResizeBuffers; - - /// - /// Gets or sets an action that is executed right before fonts are rebuilt. - /// - public event Action BuildFonts; + public event Action? ResizeBuffers; /// /// Gets or sets an action that is executed right after fonts are rebuilt. /// - public event Action AfterBuildFonts; + public event Action? AfterBuildFonts; /// - /// Gets the default ImGui font. + /// Gets the default ImGui font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont { get; private set; } + public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included FontAwesome icon font. + /// Gets an included FontAwesome icon font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont { get; private set; } + public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included monospaced font. + /// Gets an included monospaced font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont { get; private set; } + public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. /// public ImGuiIOPtr LastImGuiIoPtr { get; set; } + /// + /// Gets the DX11 scene. + /// + public RawDX11Scene? Scene => this.scene; + /// /// Gets the D3D11 device instance. /// @@ -178,11 +181,6 @@ internal class InterfaceManager : IDisposable, IServiceType } } - /// - /// Gets or sets a value indicating whether the fonts are built and ready to use. - /// - public bool FontsReady { get; set; } = false; - /// /// Gets a value indicating whether the Dalamud interface ready to use. /// @@ -213,30 +211,52 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGammaLevel)); - /// - /// Gets a value indicating whether we're building fonts but haven't generated atlas yet. - /// - public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0); - /// /// Gets a value indicating the native handle of the game main window. /// - public IntPtr GameWindowHandle { get; private set; } + public IntPtr GameWindowHandle + { + get + { + if (this.gameWindowHandle == 0) + { + nint gwh = 0; + while ((gwh = NativeFunctions.FindWindowEx(0, gwh, "FFXIVGAME", 0)) != 0) + { + _ = User32.GetWindowThreadProcessId(gwh, out var pid); + if (pid == Environment.ProcessId && User32.IsWindowVisible(gwh)) + { + this.gameWindowHandle = gwh; + break; + } + } + } + + return this.gameWindowHandle; + } + } /// /// Dispose of managed and unmanaged resources. /// public void Dispose() { - this.framework.RunOnFrameworkThread(() => + if (Service.GetNullable() is { } framework) + framework.RunOnFrameworkThread(Disposer).Wait(); + else + Disposer(); + + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.dalamudAtlas?.Dispose(); + this.scene?.Dispose(); + return; + + void Disposer() { this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - }).Wait(); - - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.scene?.Dispose(); + } } #nullable enable @@ -376,93 +396,8 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public void RebuildFonts() { - if (this.scene == null) - { - Log.Verbose("[FONT] RebuildFonts(): scene not ready, doing nothing"); - return; - } - Log.Verbose("[FONT] RebuildFonts() called"); - - // don't invoke this multiple times per frame, in case multiple plugins call it - if (!this.isRebuildingFonts) - { - Log.Verbose("[FONT] RebuildFonts() trigger"); - this.isRebuildingFonts = true; - this.scene.OnNewRenderFrame += this.RebuildFontsInternal; - } - } - - /// - /// Wait for the rebuilding fonts to complete. - /// - public void WaitForFontRebuild() - { - this.fontBuildSignal.WaitOne(); - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Ranges of glyphs. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, List> ranges) - { - var allContained = false; - var fonts = ImGui.GetIO().Fonts.Fonts; - ImFontPtr foundFont = null; - unsafe - { - for (int i = 0, i_ = fonts.Size; i < i_; i++) - { - if (!this.glyphRequests.Any(x => x.FontInternal.NativePtr == fonts[i].NativePtr)) - continue; - - allContained = true; - foreach (var range in ranges) - { - if (!allContained) - break; - - for (var j = range.Item1; j <= range.Item2 && allContained; j++) - allContained &= fonts[i].FindGlyphNoFallback(j).NativePtr != null; - } - - if (allContained) - foundFont = fonts[i]; - - break; - } - } - - var req = new SpecialGlyphRequest(this, size, ranges); - req.FontInternal = foundFont; - - if (!allContained) - this.RebuildFonts(); - - return req; - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Text to calculate glyph ranges from. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, string text) - { - List> ranges = new(); - foreach (var c in new SortedSet(text.ToHashSet())) - { - if (ranges.Any() && ranges[^1].Item2 + 1 == c) - ranges[^1] = Tuple.Create(ranges[^1].Item1, c); - else - ranges.Add(Tuple.Create(c, c)); - } - - return this.NewFontSizeRef(size, ranges); + this.dalamudAtlas?.BuildFontsAsync(); } /// @@ -486,11 +421,11 @@ internal class InterfaceManager : IDisposable, IServiceType try { var dxgiDev = this.Device.QueryInterfaceOrNull(); - var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); + var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); if (dxgiAdapter == null) return null; - var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local); + var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local); return (memInfo.CurrentUsage, memInfo.CurrentReservation); } catch @@ -516,20 +451,65 @@ internal class InterfaceManager : IDisposable, IServiceType /// Value. internal void SetImmersiveMode(bool enabled) { - if (this.GameWindowHandle == nint.Zero) - return; - - int value = enabled ? 1 : 0; - var hr = NativeFunctions.DwmSetWindowAttribute( - this.GameWindowHandle, - NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - ref value, - sizeof(int)); + if (this.GameWindowHandle == 0) + throw new InvalidOperationException("Game window is not yet ready."); + var value = enabled ? 1 : 0; + ((Result)NativeFunctions.DwmSetWindowAttribute( + this.GameWindowHandle, + NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + ref value, + sizeof(int))).CheckError(); } - private static void ShowFontError(string path) + private static InterfaceManager WhenFontsReady() { - Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); + var im = Service.GetNullable(); + if (im?.dalamudAtlas is not { } atlas) + throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call."); + + if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) + { + nextNonMainThreadFontAccessWarningCheck = + Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; + var stack = new StackTrace(); + if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) + { + if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) + { + NonMainThreadFontAccessWarning.Add(plugin, new()); + Log.Warning( + "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", + plugin.Name, + stack); + } + } + else + { + // Dalamud internal should be made safe right now + throw new InvalidOperationException("Attempted to access fonts outside the main thread."); + } + } + + if (!atlas.HasBuiltAtlas) + atlas.BuildTask.GetAwaiter().GetResult(); + return im; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RenderImGui(RawDX11Scene scene) + { + var conf = Service.Get(); + + // Process information needed by ImGuiHelpers each frame. + ImGuiHelpers.NewFrame(); + + // Enable viewports if there are no issues. + if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; + else + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + + scene.Render(); } private void InitScene(IntPtr swapChain) @@ -546,7 +526,7 @@ internal class InterfaceManager : IDisposable, IServiceType Service.ProvideException(ex); Log.Error(ex, "Could not load ImGui dependencies."); - var res = PInvoke.User32.MessageBox( + var res = User32.MessageBox( IntPtr.Zero, "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", "Dalamud Error", @@ -578,7 +558,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (iniFileInfo.Length > 1200000) { Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName!, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); iniFileInfo.Delete(); } } @@ -623,8 +603,6 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - this.SetupFonts(); - if (!configuration.IsDocking) { ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; @@ -675,26 +653,34 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { + Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); + Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + if (this.scene != null && swapChain != this.scene.SwapChain.NativePointer) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); if (this.scene == null) this.InitScene(swapChain); + Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); + + if (!this.dalamudAtlas!.HasBuiltAtlas) + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + if (this.address.IsReshade) { - var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags); + var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); return pRes; } - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); - return this.presentHook.Original(swapChain, syncInterval, presentFlags); + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } private void DisposeTextures() @@ -711,471 +697,70 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RenderImGui() + [ServiceManager.CallWhenServicesReady( + "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] + private void ContinueConstruction( + TargetSigScanner sigScanner, + Framework framework, + FontAtlasFactory fontAtlasFactory) { - // Process information needed by ImGuiHelpers each frame. - ImGuiHelpers.NewFrame(); + this.dalamudAtlas = fontAtlasFactory + .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); + this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); + this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont( + new() + { + SizePx = DefaultFontSizePx, + GlyphMinAdvanceX = DefaultFontSizePx, + GlyphMaxAdvanceX = DefaultFontSizePx, + }))); + this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddDalamudAssetFont( + DalamudAsset.InconsolataRegular, + new() { SizePx = DefaultFontSizePx }))); + this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( + tk => + { + // Note: the first call of this function is done outside the main thread; this is expected. + // Do not use DefaultFont, IconFont, and MonoFont. + // Use font handles directly. - // Check if we can still enable viewports without any issues. - this.CheckViewportState(); + // Fill missing glyphs in MonoFont from DefaultFont + tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); - this.scene.Render(); - } + // Broadcast to auto-rebuilding instances + this.AfterBuildFonts?.Invoke(); + }); - private void CheckViewportState() - { - var configuration = Service.Get(); + // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. + _ = this.dalamudAtlas.BuildFontsAsync(false); - if (configuration.IsDisableViewport || this.scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) - { - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; - return; - } - - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; - } - - /// - /// Loads font for use in ImGui text functions. - /// - private unsafe void SetupFonts() - { - using var setupFontsTimings = Timings.Start("IM SetupFonts"); - - var gameFontManager = Service.Get(); - var dalamud = Service.Get(); - var io = ImGui.GetIO(); - var ioFonts = io.Fonts; - - var fontGamma = this.FontGamma; - - this.fontBuildSignal.Reset(); - ioFonts.Clear(); - ioFonts.TexDesiredWidth = 4096; - - Log.Verbose("[FONT] SetupFonts - 1"); - - foreach (var v in this.loadedFontInfo) - v.Value.Dispose(); - - this.loadedFontInfo.Clear(); - - Log.Verbose("[FONT] SetupFonts - 2"); - - ImFontConfigPtr fontConfig = null; - List garbageList = new(); + this.address.Setup(sigScanner); try { - var dummyRangeHandle = GCHandle.Alloc(new ushort[] { '0', '0', 0 }, GCHandleType.Pinned); - garbageList.Add(dummyRangeHandle); - - fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - - var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Regular.otf"); - if (!File.Exists(fontPathJp)) - fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); - if (!File.Exists(fontPathJp)) - ShowFontError(fontPathJp); - Log.Verbose("[FONT] fontPathJp = {0}", fontPathJp); - - var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); - if (!File.Exists(fontPathKr)) - fontPathKr = null; - Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); - - var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); - if (!File.Exists(fontPathChs)) - fontPathChs = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); - - var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); - if (!File.Exists(fontPathCht)) - fontPathCht = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); - - // Default font - Log.Verbose("[FONT] SetupFonts - Default font"); - var fontInfo = new TargetFontModification( - "Default", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - this.UseAxis ? DefaultFontSizePx : DefaultFontSizePx + 1, - io.FontGlobalScale); - Log.Verbose("[FONT] SetupFonts - Default corresponding AXIS size: {0}pt ({1}px)", fontInfo.SourceAxis.Style.BaseSizePt, fontInfo.SourceAxis.Style.BaseSizePx); - fontConfig.SizePixels = fontInfo.TargetSizePx * io.FontGlobalScale; - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = false; - DefaultFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - else - { - var rangeHandle = gameFontManager.ToGlyphRanges(GameFontFamilyAndSize.Axis12); - garbageList.Add(rangeHandle); - - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - - if (fontPathKr != null - && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) - { - fontConfig.MergeMode = true; - fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathKr, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" - || this.dalamudIme.EncounteredHan)) - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - // FontAwesome icon font - Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); - { - var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesomeFreeSolid.otf"); - if (!File.Exists(fontPathIcon)) - ShowFontError(fontPathIcon); - - var iconRangeHandle = GCHandle.Alloc(new ushort[] { 0xE000, 0xF8FF, 0, }, GCHandleType.Pinned); - garbageList.Add(iconRangeHandle); - - fontConfig.GlyphRanges = iconRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[IconFont] = new("Icon", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Monospace font - Log.Verbose("[FONT] SetupFonts - Monospace font"); - { - var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf"); - if (!File.Exists(fontPathMono)) - ShowFontError(fontPathMono); - - fontConfig.GlyphRanges = IntPtr.Zero; - fontConfig.PixelSnapH = true; - MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[MonoFont] = new("Mono", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Default font but in requested size for requested glyphs - Log.Verbose("[FONT] SetupFonts - Default font but in requested size for requested glyphs"); - { - Dictionary> extraFontRequests = new(); - foreach (var extraFontRequest in this.glyphRequests) - { - if (!extraFontRequests.ContainsKey(extraFontRequest.Size)) - extraFontRequests[extraFontRequest.Size] = new(); - extraFontRequests[extraFontRequest.Size].Add(extraFontRequest); - } - - foreach (var (fontSize, requests) in extraFontRequests) - { - List<(ushort, ushort)> codepointRanges = new(4 + requests.Sum(x => x.CodepointRanges.Count)) - { - new(Fallback1Codepoint, Fallback1Codepoint), - new(Fallback2Codepoint, Fallback2Codepoint), - // ImGui default ellipsis characters - new(0x2026, 0x2026), - new(0x0085, 0x0085), - }; - - foreach (var request in requests) - codepointRanges.AddRange(request.CodepointRanges.Select(x => (From: x.Item1, To: x.Item2))); - - codepointRanges.Sort(); - List flattenedRanges = new(); - foreach (var range in codepointRanges) - { - if (flattenedRanges.Any() && flattenedRanges[^1] >= range.Item1 - 1) - { - flattenedRanges[^1] = Math.Max(flattenedRanges[^1], range.Item2); - } - else - { - flattenedRanges.Add(range.Item1); - flattenedRanges.Add(range.Item2); - } - } - - flattenedRanges.Add(0); - - fontInfo = new( - $"Requested({fontSize}px)", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - fontSize, - io.FontGlobalScale); - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.SizePixels = fontInfo.SourceAxis.Style.BaseSizePx; - fontConfig.PixelSnapH = false; - - var sizedFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - else - { - var rangeHandle = GCHandle.Alloc(flattenedRanges.ToArray(), GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.PixelSnapH = true; - - var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontSize * io.FontGlobalScale, fontConfig, rangeHandle.AddrOfPinnedObject()); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - } - } - - gameFontManager.BuildFonts(); - - var customFontFirstConfigIndex = ioFonts.ConfigData.Size; - - Log.Verbose("[FONT] Invoke OnBuildFonts"); - this.BuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnBuildFonts OK!"); - - for (int i = customFontFirstConfigIndex, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - if (gameFontManager.OwnsFont(config.DstFont)) - continue; - - config.OversampleH = 1; - config.OversampleV = 1; - - var name = Encoding.UTF8.GetString((byte*)config.Name.Data, config.Name.Count).TrimEnd('\0'); - if (name.IsNullOrEmpty()) - name = $"{config.SizePixels}px"; - - // ImFont information is reflected only if corresponding ImFontConfig has MergeMode not set. - if (config.MergeMode) - { - if (!this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode specified for {0} but not found in loadedFontInfo. Skipping.", name); - continue; - } - } - else - { - if (this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode not specified for {0} but found in loadedFontInfo. Skipping.", name); - continue; - } - - // While the font will be loaded in the scaled size after FontScale is applied, the font will be treated as having the requested size when used from plugins. - this.loadedFontInfo[config.DstFont.NativePtr] = new($"PlReq({name})", config.SizePixels); - } - - config.SizePixels = config.SizePixels * io.FontGlobalScale; - } - - for (int i = 0, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - config.RasterizerGamma *= fontGamma; - } - - Log.Verbose("[FONT] ImGui.IO.Build will be called."); - ioFonts.Build(); - gameFontManager.AfterIoFontsBuild(); - this.ClearStacks(); - Log.Verbose("[FONT] ImGui.IO.Build OK!"); - - gameFontManager.AfterBuildFonts(); - - foreach (var (font, mod) in this.loadedFontInfo) - { - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes($"{mod.Name}\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - Log.Verbose("[FONT] {0}: Unscale with scale value of {1}", mod.Name, mod.Scale); - GameFontManager.UnscaleFont(font, mod.Scale, false); - - if (mod.Axis == TargetFontModification.AxisMode.Overwrite) - { - Log.Verbose("[FONT] {0}: Overwrite from AXIS of size {1}px (was {2}px)", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - GameFontManager.UnscaleFont(font, font.FontSize / mod.SourceAxis.ImFont.FontSize, false); - var ascentDiff = mod.SourceAxis.ImFont.Ascent - font.Ascent; - font.Ascent += ascentDiff; - font.Descent = ascentDiff; - font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar; - font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); - } - else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly) - { - Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize -= 1; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize += 1; - } - - Log.Verbose("[FONT] {0}: Resize from {1}px to {2}px", mod.Name, font.FontSize, mod.TargetSizePx); - GameFontManager.UnscaleFont(font, font.FontSize / mod.TargetSizePx, false); - } - - // Fill missing glyphs in MonoFont from DefaultFont - ImGuiHelpers.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); - - for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++) - { - var font = ioFonts.Fonts[i]; - if (font.Glyphs.Size == 0) - { - Log.Warning("[FONT] Font has no glyph: {0}", font.GetDebugName()); - continue; - } - - if (font.FindGlyphNoFallback(Fallback1Codepoint).NativePtr != null) - font.FallbackChar = Fallback1Codepoint; - - font.BuildLookupTableNonstandard(); - } - - Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); - this.AfterBuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnAfterBuildFonts OK!"); - - if (ioFonts.Fonts[0].NativePtr != DefaultFont.NativePtr) - Log.Warning("[FONT] First font is not DefaultFont"); - - Log.Verbose("[FONT] Fonts built!"); - - this.fontBuildSignal.Set(); - - this.FontsReady = true; + if (Service.Get().WindowIsImmersive) + this.SetImmersiveMode(true); } - finally + catch (Exception ex) { - if (fontConfig.NativePtr != null) - fontConfig.Destroy(); - - foreach (var garbage in garbageList) - garbage.Free(); + Log.Error(ex, "Could not enable immersive mode"); } - } - [ServiceManager.CallWhenServicesReady( - "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] - private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration) - { - this.address.Setup(sigScanner); - this.framework.RunOnFrameworkThread(() => - { - while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) - { - _ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid); + this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); + this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle)) - break; - } + Log.Verbose("===== S W A P C H A I N ====="); + Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); + Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - try - { - if (configuration.WindowIsImmersive) - this.SetImmersiveMode(true); - } - catch (Exception ex) - { - Log.Error(ex, "Could not enable immersive mode"); - } - - this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - - Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); - Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - - this.setCursorHook.Enable(); - this.presentHook.Enable(); - this.resizeBuffersHook.Enable(); - }); - } - - // This is intended to only be called as a handler attached to scene.OnNewRenderFrame - private void RebuildFontsInternal() - { - Log.Verbose("[FONT] RebuildFontsInternal() called"); - this.SetupFonts(); - - Log.Verbose("[FONT] RebuildFontsInternal() detaching"); - this.scene!.OnNewRenderFrame -= this.RebuildFontsInternal; - - Log.Verbose("[FONT] Calling InvalidateFonts"); - this.scene.InvalidateFonts(); - - Log.Verbose("[FONT] Font Rebuild OK!"); - - this.isRebuildingFonts = false; + this.setCursorHook.Enable(); + this.presentHook.Enable(); + this.resizeBuffersHook.Enable(); } private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) @@ -1206,14 +791,17 @@ internal class InterfaceManager : IDisposable, IServiceType private IntPtr SetCursorDetour(IntPtr hCursor) { - if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) + if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); + return this.setCursorHook.IsDisposed + ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() + : this.setCursorHook.Original(hCursor); } private void OnNewInputFrame() { + var io = ImGui.GetIO(); var dalamudInterface = Service.GetNullable(); var gamepadState = Service.GetNullable(); var keyState = Service.GetNullable(); @@ -1221,18 +809,21 @@ internal class InterfaceManager : IDisposable, IServiceType if (dalamudInterface == null || gamepadState == null || keyState == null) return; + // Prevent setting the footgun from ImGui Demo; the Space key isn't removing the flag at the moment. + io.ConfigFlags &= ~ImGuiConfigFlags.NoMouse; + // fix for keys in game getting stuck, if you were holding a game key (like run) // and then clicked on an imgui textbox - imgui would swallow the keyup event, // so the game would think the key remained pressed continuously until you left // imgui and pressed and released the key again - if (ImGui.GetIO().WantTextInput) + if (io.WantTextInput) { keyState.ClearAll(); } // TODO: mouse state? - var gamepadEnabled = (ImGui.GetIO().BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; + var gamepadEnabled = (io.BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; // NOTE (Chiv) Activate ImGui navigation via L1+L3 press // (mimicking how mouse navigation is activated via L1+R3 press in game). @@ -1240,12 +831,12 @@ internal class InterfaceManager : IDisposable, IServiceType && gamepadState.Raw(GamepadButtons.L1) > 0 && gamepadState.Pressed(GamepadButtons.L3) > 0) { - ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; + io.ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; gamepadState.NavEnableGamepad ^= true; dalamudInterface.ToggleGamepadModeNotifierWindow(); } - if (gamepadEnabled && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) + if (gamepadEnabled && (io.ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) { var northButton = gamepadState.Raw(GamepadButtons.North) != 0; var eastButton = gamepadState.Raw(GamepadButtons.East) != 0; @@ -1264,7 +855,6 @@ internal class InterfaceManager : IDisposable, IServiceType var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0; var r2Button = gamepadState.Raw(GamepadButtons.R2) != 0; - var io = ImGui.GetIO(); io.AddKeyEvent(ImGuiKey.GamepadFaceUp, northButton); io.AddKeyEvent(ImGuiKey.GamepadFaceRight, eastButton); io.AddKeyEvent(ImGuiKey.GamepadFaceDown, southButton); @@ -1312,7 +902,10 @@ internal class InterfaceManager : IDisposable, IServiceType var snap = ImGuiManagedAsserts.GetSnapshot(); if (this.IsDispatchingEvents) - this.Draw?.Invoke(); + { + using (this.defaultFontHandle?.Push()) + this.Draw?.Invoke(); + } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); @@ -1339,123 +932,4 @@ internal class InterfaceManager : IDisposable, IServiceType /// public InterfaceManager Manager { get; init; } } - - /// - /// Represents a glyph request. - /// - public class SpecialGlyphRequest : IDisposable - { - /// - /// Initializes a new instance of the class. - /// - /// InterfaceManager to associate. - /// Font size in pixels. - /// Codepoint ranges. - internal SpecialGlyphRequest(InterfaceManager manager, float size, List> ranges) - { - this.Manager = manager; - this.Size = size; - this.CodepointRanges = ranges; - this.Manager.glyphRequests.Add(this); - } - - /// - /// Gets the font of specified size, or DefaultFont if it's not ready yet. - /// - public ImFontPtr Font - { - get - { - unsafe - { - return this.FontInternal.NativePtr == null ? DefaultFont : this.FontInternal; - } - } - } - - /// - /// Gets or sets the associated ImFont. - /// - internal ImFontPtr FontInternal { get; set; } - - /// - /// Gets associated InterfaceManager. - /// - internal InterfaceManager Manager { get; init; } - - /// - /// Gets font size. - /// - internal float Size { get; init; } - - /// - /// Gets codepoint ranges. - /// - internal List> CodepointRanges { get; init; } - - /// - public void Dispose() - { - this.Manager.glyphRequests.Remove(this); - } - } - - private unsafe class TargetFontModification : IDisposable - { - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information, assuming that AXIS fonts will not be applied. - /// - /// Name of the font to write to ImGui font information. - /// Target font size in pixels, which will not be considered for further scaling. - internal TargetFontModification(string name, float sizePx) - { - this.Name = name; - this.Axis = AxisMode.Suppress; - this.TargetSizePx = sizePx; - this.Scale = 1; - this.SourceAxis = null; - } - - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information. - /// - /// Name of the font to write to ImGui font information. - /// Whether and how to use AXIS fonts. - /// Target font size in pixels, which will not be considered for further scaling. - /// Font scale to be referred for loading AXIS font of appropriate size. - internal TargetFontModification(string name, AxisMode axis, float sizePx, float globalFontScale) - { - this.Name = name; - this.Axis = axis; - this.TargetSizePx = sizePx; - this.Scale = globalFontScale; - this.SourceAxis = Service.Get().NewFontRef(new(GameFontFamily.Axis, this.TargetSizePx * this.Scale)); - } - - internal enum AxisMode - { - Suppress, - GameGlyphsOnly, - Overwrite, - } - - internal string Name { get; private init; } - - internal AxisMode Axis { get; private init; } - - internal float TargetSizePx { get; private init; } - - internal float Scale { get; private init; } - - internal GameFontHandle? SourceAxis { get; private init; } - - internal bool SourceAxisAvailable => this.SourceAxis != null && this.SourceAxis.ImFont.NativePtr != null; - - public void Dispose() - { - this.SourceAxis?.Dispose(); - } - } } diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index b9e7ab686..9b0416583 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Linq; using System.Numerics; @@ -7,6 +6,8 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -31,8 +32,14 @@ internal sealed class ChangelogWindow : Window, IDisposable • Plugins can now add tooltips and interaction to the server info bar • The Dalamud/plugin installer UI has been refreshed "; - + private readonly TitleScreenMenuWindow tsmWindow; + + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy bannerFont; + private readonly Lazy apiBumpExplainerTexture; + private readonly Lazy logoTexture; private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) { @@ -46,27 +53,36 @@ internal sealed class ChangelogWindow : Window, IDisposable Point2 = Vector2.One, }; - private IDalamudTextureWrap? apiBumpExplainerTexture; - private IDalamudTextureWrap? logoTexture; - private GameFontHandle? bannerFont; - private State state = State.WindowFadeIn; private bool needFadeRestart = false; - + /// /// Initializes a new instance of the class. /// /// TSM window. - public ChangelogWindow(TitleScreenMenuWindow tsmWindow) + /// An instance of . + /// An instance of . + public ChangelogWindow( + TitleScreenMenuWindow tsmWindow, + FontAtlasFactory fontAtlasFactory, + DalamudAssetManager assets) : base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true) { this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; + this.privateAtlas = this.scopedFinalizer.Add( + fontAtlasFactory.CreateFontAtlas(this.Namespace, FontAtlasAutoRebuildMode.Async)); + this.bannerFont = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.MiedingerMid18)))); + + this.apiBumpExplainerTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.ChangelogApiBumpIcon)); + this.logoTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.Logo)); // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); + _ = this.bannerFont; } private enum State @@ -97,20 +113,12 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - this.MakeFont(Service.Get()); + _ = this.bannerFont; this.state = State.WindowFadeIn; this.windowFade.Reset(); this.bodyFade.Reset(); this.needFadeRestart = true; - - if (this.apiBumpExplainerTexture == null) - { - var dalamud = Service.Get(); - var tm = Service.Get(); - this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png"))) - ?? throw new Exception("Could not load api bump explainer."); - } base.OnOpen(); } @@ -186,10 +194,7 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2)); using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) - { - this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); - ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); - } + ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize); } ImGui.SameLine(); @@ -205,7 +210,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) { - using var font = ImRaii.PushFont(this.bannerFont!.ImFont); + using var font = this.bannerFont.Value.Push(); switch (this.state) { @@ -275,9 +280,11 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.TextWrapped("If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."); ImGuiHelpers.ScaledDummy(15); - - ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width); - ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size); + + ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture.Value.Width); + ImGui.Image( + this.apiBumpExplainerTexture.Value.ImGuiHandle, + this.apiBumpExplainerTexture.Value.Size); DrawNextButton(State.Links); break; @@ -377,7 +384,4 @@ internal sealed class ChangelogWindow : Window, IDisposable public void Dispose() { } - - private void MakeFont(GameFontManager gfm) => - this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 20c3d6d01..951d3d91c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -6,6 +6,8 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; +using Dalamud.Utility; + using ImGuiNET; using Serilog; @@ -14,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class responsible for drawing the data/debug window. /// -internal class DataWindow : Window +internal class DataWindow : Window, IDisposable { private readonly IDataWindowWidget[] modules = { @@ -34,6 +36,7 @@ internal class DataWindow : Window new FlyTextWidget(), new FontAwesomeTestWidget(), new GameInventoryTestWidget(), + new GamePrebakedFontsTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), @@ -76,6 +79,9 @@ internal class DataWindow : Window this.Load(); } + /// + public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose(); + /// public override void OnOpen() { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs new file mode 100644 index 000000000..12749114b --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -0,0 +1,186 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget for testing game prebaked fonts. +/// +internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable +{ + private ImVectorWrapper testStringBuffer; + private IFontAtlas? privateAtlas; + private IReadOnlyDictionary Handle)[]>? fontHandles; + private bool useGlobalScale; + private bool useWordWrap; + private bool useItalic; + private bool useBold; + + /// + public string[]? CommandShortcuts { get; init; } + + /// + public string DisplayName { get; init; } = "Game Prebaked Fonts"; + + /// + public bool Ready { get; set; } + + /// + public void Load() => this.Ready = true; + + /// + public unsafe void Draw() + { + ImGui.AlignTextToFramePadding(); + fixed (byte* labelPtr = "Global Scale"u8) + { + var v = (byte)(this.useGlobalScale ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useGlobalScale = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Word Wrap"u8) + { + var v = (byte)(this.useWordWrap ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + this.useWordWrap = v != 0; + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Italic"u8) + { + var v = (byte)(this.useItalic ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useItalic = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Bold"u8) + { + var v = (byte)(this.useBold ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useBold = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed) + { + this.testStringBuffer.Dispose(); + this.testStringBuffer = ImVectorWrapper.CreateFromSpan( + "(Game)-[Font] {Test}. 0123456789!! <氣気气きキ기>。"u8, + minCapacity: 1024); + } + + this.privateAtlas ??= + Service.Get().CreateFontAtlas( + nameof(GamePrebakedFontsTestWidget), + FontAtlasAutoRebuildMode.Async, + this.useGlobalScale); + this.fontHandles ??= + Enum.GetValues() + .Where(x => x.GetAttribute() is not null) + .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) + .GroupBy(x => x.Family) + .ToImmutableDictionary( + x => x.Key, + x => x.Select(y => (y, new Lazy(() => this.privateAtlas.NewGameFontHandle(y)))) + .ToArray()); + + fixed (byte* labelPtr = "Test Input"u8) + { + if (ImGuiNative.igInputTextMultiline( + labelPtr, + this.testStringBuffer.Data, + (uint)this.testStringBuffer.Capacity, + new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale), + 0, + null, + null) != 0) + { + var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); + if (len + 4 >= this.testStringBuffer.Capacity) + this.testStringBuffer.EnsureCapacityExponential(len + 4); + if (len < this.testStringBuffer.Capacity) + { + this.testStringBuffer.LengthUnsafe = len; + this.testStringBuffer.StorageSpan[len] = default; + } + } + } + + var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); + foreach (var (family, items) in this.fontHandles) + { + if (!ImGui.CollapsingHeader($"{family} Family")) + continue; + + foreach (var (gfs, handle) in items) + { + ImGui.TextUnformatted($"{gfs.SizePt}pt"); + ImGui.SameLine(offsetX); + ImGuiNative.igPushTextWrapPos(this.useWordWrap ? 0f : -1f); + try + { + if (handle.Value.LoadException is { } exc) + { + ImGui.TextUnformatted(exc.ToString()); + } + else if (!handle.Value.Available) + { + fixed (byte* labelPtr = "Loading..."u8) + ImGuiNative.igTextUnformatted(labelPtr, labelPtr + 8 + ((Environment.TickCount / 200) % 3)); + } + else + { + if (!this.useGlobalScale) + ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); + using var pushPop = handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + } + } + finally + { + ImGuiNative.igPopTextWrapPos(); + ImGuiNative.igSetWindowFontScale(1); + } + } + } + } + + /// + public void Dispose() + { + this.ClearAtlas(); + this.testStringBuffer.Dispose(); + } + + private void ClearAtlas() + { + this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value)) + .AggregateToDisposable().Dispose(); + this.fontHandles = null; + this.privateAtlas?.Dispose(); + this.privateAtlas = null; + } +} diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 7d4489f8d..414eabd22 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -8,7 +8,6 @@ using Dalamud.Interface.Internal.Windows.Settings.Tabs; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Internal; using Dalamud.Utility; using ImGuiNET; @@ -19,14 +18,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings; /// internal class SettingsWindow : Window { - private readonly SettingsTab[] tabs = - { - new SettingsTabGeneral(), - new SettingsTabLook(), - new SettingsTabDtr(), - new SettingsTabExperimental(), - new SettingsTabAbout(), - }; + private SettingsTab[]? tabs; private string searchInput = string.Empty; @@ -49,6 +41,15 @@ internal class SettingsWindow : Window /// public override void OnOpen() { + this.tabs ??= new SettingsTab[] + { + new SettingsTabGeneral(), + new SettingsTabLook(), + new SettingsTabDtr(), + new SettingsTabExperimental(), + new SettingsTabAbout(), + }; + foreach (var settingsTab in this.tabs) { settingsTab.Load(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 5b6f6b02f..8714fd666 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -1,13 +1,13 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Numerics; using CheapLoc; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; @@ -15,7 +15,6 @@ using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -173,16 +172,21 @@ Contribute at: https://github.com/goatcorp/Dalamud "; private readonly Stopwatch creditsThrottler; + private readonly IFontAtlas privateAtlas; private string creditsText; private bool resetNow = false; private IDalamudTextureWrap? logoTexture; - private GameFontHandle? thankYouFont; + private IFontHandle? thankYouFont; public SettingsTabAbout() { this.creditsThrottler = new(); + + this.privateAtlas = Service + .Get() + .CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async); } public override SettingsEntry[] Entries { get; } = { }; @@ -207,11 +211,7 @@ Contribute at: https://github.com/goatcorp/Dalamud this.creditsThrottler.Restart(); - if (this.thankYouFont == null) - { - var gfm = Service.Get(); - this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34)); - } + this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34)); this.resetNow = true; @@ -269,14 +269,12 @@ Contribute at: https://github.com/goatcorp/Dalamud if (this.thankYouFont != null) { - ImGui.PushFont(this.thankYouFont.ImFont); + using var fontPush = this.thankYouFont.Push(); var thankYouLenX = ImGui.CalcTextSize(ThankYouText).X; ImGui.Dummy(new Vector2((windowX / 2) - (thankYouLenX / 2), 0f)); ImGui.SameLine(); ImGui.TextUnformatted(ThankYouText); - - ImGui.PopFont(); } ImGuiHelpers.ScaledDummy(0, windowSize.Y + 50f); @@ -305,9 +303,5 @@ Contribute at: https://github.com/goatcorp/Dalamud /// /// Disposes of managed and unmanaged resources. /// - public override void Dispose() - { - this.logoTexture?.Dispose(); - this.thankYouFont?.Dispose(); - } + public override void Dispose() => this.privateAtlas.Dispose(); } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 42bca89ff..9c385a99c 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -7,11 +7,14 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; +using Dalamud.Utility; using ImGuiNET; @@ -27,16 +30,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly ClientState clientState; private readonly DalamudConfiguration configuration; - private readonly Framework framework; private readonly GameGui gameGui; private readonly TitleScreenMenu titleScreenMenu; + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy myFontHandle; private readonly Lazy shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); private readonly Dictionary logoEasings = new(); - private readonly Dictionary specialGlyphRequests = new(); private InOutCubic? fadeOutEasing; @@ -48,6 +52,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// An instance of . /// An instance of . /// An instance of . + /// An instance of . /// An instance of . /// An instance of . /// An instance of . @@ -55,6 +60,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable ClientState clientState, DalamudConfiguration configuration, DalamudAssetManager dalamudAssetManager, + FontAtlasFactory fontAtlasFactory, Framework framework, GameGui gameGui, TitleScreenMenu titleScreenMenu) @@ -65,7 +71,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable { this.clientState = clientState; this.configuration = configuration; - this.framework = framework; this.gameGui = gameGui; this.titleScreenMenu = titleScreenMenu; @@ -77,9 +82,25 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); + this.privateAtlas = fontAtlasFactory.CreateFontAtlas(this.WindowName, FontAtlasAutoRebuildMode.Async); + this.scopedFinalizer.Add(this.privateAtlas); + + this.myFontHandle = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + toolkit => toolkit.AddDalamudDefaultFont( + TargetFontSizePx, + titleScreenMenu.Entries.SelectMany(x => x.Name).ToGlyphRange()))))); + + titleScreenMenu.EntryListChange += this.TitleScreenMenuEntryListChange; + this.scopedFinalizer.Add(() => titleScreenMenu.EntryListChange -= this.TitleScreenMenuEntryListChange); + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); framework.Update += this.FrameworkOnUpdate; + this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate); } private enum State @@ -94,6 +115,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public bool AllowDrawing { get; set; } = true; + /// + public void Dispose() => this.scopedFinalizer.Dispose(); + /// public override void PreDraw() { @@ -109,12 +133,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable base.PostDraw(); } - /// - public void Dispose() - { - this.framework.Update -= this.FrameworkOnUpdate; - } - /// public override void Draw() { @@ -246,33 +264,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable break; } } - - var srcText = entries.Select(e => e.Name).ToHashSet(); - var keys = this.specialGlyphRequests.Keys.ToHashSet(); - keys.RemoveWhere(x => srcText.Contains(x)); - foreach (var key in keys) - { - this.specialGlyphRequests[key].Dispose(); - this.specialGlyphRequests.Remove(key); - } } private bool DrawEntry( TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) { - InterfaceManager.SpecialGlyphRequest fontHandle; - if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) - { - fontHandle.Dispose(); - this.specialGlyphRequests.Remove(entry.Name); - fontHandle = null; - } - - if (fontHandle == null) - this.specialGlyphRequests[entry.Name] = fontHandle = Service.Get().NewFontSizeRef(TargetFontSizePx, entry.Name); - - ImGui.PushFont(fontHandle.Font); - ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size); + using var fontScopeDispose = this.myFontHandle.Value.Push(); var scale = ImGui.GetIO().FontGlobalScale; @@ -383,8 +380,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable initialCursor.Y += entry.Texture.Height * scale; ImGui.SetCursorPos(initialCursor); - ImGui.PopFont(); - return isHover; } @@ -401,4 +396,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } + + private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs new file mode 100644 index 000000000..50e591390 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// How to rebuild . +/// +public enum FontAtlasAutoRebuildMode +{ + /// + /// Do not rebuild. + /// + Disable, + + /// + /// Rebuild on new frame. + /// + OnNewFrame, + + /// + /// Rebuild asynchronously. + /// + Async, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs new file mode 100644 index 000000000..345ab729d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs @@ -0,0 +1,38 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Build step for . +/// +public enum FontAtlasBuildStep +{ + /// + /// An invalid value. This should never be passed through event callbacks. + /// + Invalid, + + /// + /// Called before calling .
+ /// Expect to be passed. + ///
+ PreBuild, + + /// + /// Called after calling .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostBuild, + + /// + /// Called after promoting staging font atlas to the actual atlas for .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostPromotion, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs new file mode 100644 index 000000000..4f5b34061 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs @@ -0,0 +1,15 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Delegate to be called when a font needs to be built. +/// +/// A toolkit that may help you for font building steps. +/// +/// An implementation of may implement all of +/// , , and +/// .
+/// Either use to identify the build step, or use +/// , , +/// and for routing. +///
+public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs new file mode 100644 index 000000000..d12409d51 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -0,0 +1,111 @@ +using System.Collections.Generic; + +using Dalamud.Interface.Utility; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Convenience function for building fonts through . +/// +public static class FontAtlasBuildToolkitUtilities +{ + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this IEnumerable enumerable, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in enumerable) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this ReadOnlySpan span, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in span) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given string into an array of containing ImGui glyph ranges. + /// + /// The string. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this string @string, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) => + @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// This, for method chaining. + public static IFontAtlasBuildToolkit OnPreBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PreBuild) + action.Invoke((IFontAtlasBuildToolkitPreBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostBuild) + action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostPromotion( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion) + action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit); + return toolkit; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs new file mode 100644 index 000000000..6d971dc02 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -0,0 +1,84 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas.Internals; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Wrapper for . +/// +public interface IFontAtlas : IDisposable +{ + /// + /// Event to be called on build step changes.
+ /// is meaningless for this event. + ///
+ event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + /// Event fired when a font rebuild operation is suggested.
+ /// This will be invoked from the main thread. + ///
+ event Action? RebuildRecommend; + + /// + /// Gets the name of the atlas. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets a value how the atlas should be rebuilt when the relevant Dalamud Configuration changes. + /// + FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + /// Gets the font atlas. Might be empty. + /// + ImFontAtlasPtr ImAtlas { get; } + + /// + /// Gets the task that represents the current font rebuild state. + /// + Task BuildTask { get; } + + /// + /// Gets a value indicating whether there exists any built atlas, regardless of . + /// + bool HasBuiltAtlas { get; } + + /// + /// Gets a value indicating whether this font atlas is under the effect of global scale. + /// + bool IsGlobalScaled { get; } + + /// + public IFontHandle NewGameFontHandle(GameFontStyle style); + + /// + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate @delegate); + + /// + public void FreeFontHandle(IFontHandle handle); + + /// + /// Queues rebuilding fonts, on the main thread.
+ /// Note that would not necessarily get changed from calling this function. + ///
+ void BuildFontsOnNextFrame(); + + /// + /// Rebuilds fonts immediately, on the current thread.
+ /// Even the callback for will be called on the same thread. + ///
+ void BuildFontsImmediately(); + + /// + /// Rebuilds fonts asynchronously, on any thread. + /// + /// Call on the main thread. + /// The task. + Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs new file mode 100644 index 000000000..4b016bbb2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -0,0 +1,67 @@ +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Common stuff for and . +/// +public interface IFontAtlasBuildToolkit +{ + /// + /// Gets or sets the font relevant to the call. + /// + ImFontPtr Font { get; set; } + + /// + /// Gets the current scale this font atlas is being built with. + /// + float Scale { get; } + + /// + /// Gets a value indicating whether the current build operation is asynchronous. + /// + bool IsAsyncBuildOperation { get; } + + /// + /// Gets the current build step. + /// + FontAtlasBuildStep BuildStep { get; } + + /// + /// Gets the font atlas being built. + /// + ImFontAtlasPtr NewImAtlas { get; } + + /// + /// Gets the wrapper for of .
+ /// This does not need to be disposed. Calling does nothing.- + ///
+ /// Modification of this vector may result in undefined behaviors. + ///
+ ImVectorWrapper Fonts { get; } + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeWithAtlas(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeWithAtlas(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The action to run on dispose. + void DisposeWithAtlas(Action action); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs new file mode 100644 index 000000000..3c14197e0 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -0,0 +1,26 @@ +using Dalamud.Interface.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit +{ + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + + /// + /// Stores a texture to be managed with the atlas. + /// + /// The texture wrap. + /// Dispose the wrap on error. + /// The texture index. + int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs new file mode 100644 index 000000000..8c3c91624 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs @@ -0,0 +1,33 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit +{ + /// + /// Copies glyphs across fonts, in a safer way.
+ /// If the font does not belong to the current atlas, this function is a no-op. + ///
+ /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE'); + + /// + /// Calls , with some fixups. + /// + /// The font. + void BuildLookupTable(ImFontPtr font); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs new file mode 100644 index 000000000..e8f11aec3 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -0,0 +1,164 @@ +using System.IO; +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is .
+///
+/// After returns, +/// either must be set, +/// or at least one font must have been added to the atlas using one of AddFont... functions. +///
+public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit +{ + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeAfterBuild(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeAfterBuild(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The action to run on dispose. + void DisposeAfterBuild(Action action); + + /// + /// Excludes given font from global scaling. + /// + /// The font. + /// Same with . + ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// + /// Do NOT call on the once this function has + /// been called, unless is set and the function has thrown an error. + /// + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + nint dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + => this.AddFontFromImGuiHeapAllocatedMemory( + (void*)dataPointer, + dataSize, + fontConfig, + freeOnException, + debugTag); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// Do NOT call on the once this + /// function has been called. + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag); + + /// + /// Adds a font from a file. + /// + /// The file path to create a new font from. + /// The font config. + /// The newly added font. + ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig); + + /// + /// Adds a font from a stream. + /// + /// The stream to create a new font from. + /// The font config. + /// Dispose when this function returns or throws. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromStream(Stream stream, in SafeFontConfig fontConfig, bool leaveOpen, string debugTag); + + /// + /// Adds a font from memory. + /// + /// The span to create from. + /// The font config. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromMemory(ReadOnlySpan span, in SafeFontConfig fontConfig, string debugTag); + + /// + /// Adds the default font known to the current font atlas.
+ ///
+ /// Default font includes and . + ///
+ /// Font size in pixels. + /// The glyph ranges. Use .ToGlyphRange to build. + /// A font returned from . + ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null); + + /// + /// Adds a font that is shipped with Dalamud.
+ ///
+ /// Note: if game symbols font file is requested but is unavailable, + /// then it will take the glyphs from game's built-in fonts, and everything in + /// will be ignored but and . + ///
+ /// The font type. + /// The font config. + /// The added font. + ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig); + + /// + /// Same with (, ...), + /// but using only FontAwesome icon ranges.
+ /// will be ignored. + ///
+ /// The font config. + /// The added font. + ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig); + + /// + /// Adds the game's symbols into the provided font.
+ /// will be ignored. + ///
+ /// The font config. + void AddGameSymbol(in SafeFontConfig fontConfig); + + /// + /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
+ /// will be ignored. + ///
+ /// The font config. + void AddExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs new file mode 100644 index 000000000..854594663 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -0,0 +1,42 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Represents a reference counting handle for fonts. +/// +public interface IFontHandle : IDisposable +{ + /// + /// Represents a reference counting handle for fonts. Dalamud internal use only. + /// + internal interface IInternal : IFontHandle + { + /// + /// Gets the font.
+ /// Use of this properly is safe only from the UI thread.
+ /// Use if the intended purpose of this property is .
+ /// Futures changes may make simple not enough. + ///
+ ImFontPtr ImFont { get; } + } + + /// + /// Gets the load exception, if it failed to load. Otherwise, it is null. + /// + Exception? LoadException { get; } + + /// + /// Gets a value indicating whether this font is ready for use.
+ /// Use directly if you want to keep the current ImGui font if the font is not ready. + ///
+ bool Available { get; } + + /// + /// Pushes the current font into ImGui font stack using , if available.
+ /// Use to access the current font.
+ /// You may not access the font once you dispose this object. + ///
+ /// A disposable object that will call (1) on dispose. + IDisposable Push(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs new file mode 100644 index 000000000..bc48ddcc1 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -0,0 +1,331 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle representing a user-callback generated font. +/// +internal class DelegateFontHandle : IFontHandle.IInternal +{ + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Callback for . + public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) + { + this.manager = manager; + this.CallOnBuildStepChange = callOnBuildStepChange; + } + + /// + /// Gets the function to be called on build step changes. + /// + public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly HashSet handles = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager"; + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + public void Dispose() + { + lock (this.syncRoot) + { + this.handles.Clear(); + this.Substance?.Dispose(); + this.Substance = null; + } + } + + /// + /// Creates a new IFontHandle using your own callbacks. + /// + /// Callback for . + /// Handle to a font that may or may not be ready yet. + public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate callOnBuildStepChange) + { + var key = new DelegateFontHandle(this, callOnBuildStepChange); + lock (this.syncRoot) + this.handles.Add(key); + this.RebuildRecommend?.Invoke(); + return key; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not DelegateFontHandle cgfh) + return; + + lock (this.syncRoot) + this.handles.Remove(cgfh); + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.handles.ToArray()); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); + + // Not owned by this class. Do not dispose. + private readonly DelegateFontHandle[] relevantHandles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The relevant handles. + public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) + { + this.Manager = manager; + this.relevantHandles = relevantHandles; + } + + /// + public IFontHandleManager Manager { get; } + + /// + public void Dispose() + { + this.fonts.Clear(); + this.buildExceptions.Clear(); + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + var fontsVector = toolkitPreBuild.Fonts; + foreach (var k in this.relevantHandles) + { + var fontCountPrevious = fontsVector.Length; + + try + { + toolkitPreBuild.Font = default; + k.CallOnBuildStepChange(toolkitPreBuild); + if (toolkitPreBuild.Font.IsNull()) + { + if (fontCountPrevious == fontsVector.Length) + { + throw new InvalidOperationException( + $"{nameof(FontAtlasBuildStepDelegate)} must either set the " + + $"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font."); + } + + toolkitPreBuild.Font = fontsVector[^1]; + } + else + { + var found = false; + unsafe + { + for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++) + { + if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr) + found = true; + } + } + + if (!found) + { + throw new InvalidOperationException( + "The font does not exist in the atlas' font array. If you need an empty font, try" + + "adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" + + "glyph range."); + } + } + + if (fontsVector.Length - fontCountPrevious != 1) + { + Log.Warning( + "[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " + + "did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", + this.Manager.Name, + fontsVector.Length - fontCountPrevious, + nameof(FontAtlasBuildStepDelegate), + nameof(SafeFontConfig), + nameof(SafeFontConfig.MergeFont), + nameof(ImFontConfigPtr), + nameof(ImFontConfigPtr.MergeMode)); + } + + for (var i = fontCountPrevious; i < fontsVector.Length; i++) + { + if (fontsVector[i].ValidateUnsafe() is { } ex) + { + throw new InvalidOperationException( + "One of the newly added fonts seem to be pointing to an invalid memory address.", + ex); + } + } + + // Check for duplicate entries; duplicates will result in free-after-free + for (var i = 0; i < fontCountPrevious; i++) + { + for (var j = fontCountPrevious; j < fontsVector.Length; j++) + { + unsafe + { + if (fontsVector[i].NativePtr == fontsVector[j].NativePtr) + throw new InvalidOperationException("An already added font has been added again."); + } + } + } + + this.fonts[k] = toolkitPreBuild.Font; + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PreBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + + // Sanitization, in a futile attempt to prevent crashes on invalid parameters + unsafe + { + var distinct = + fontsVector + .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates + .Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them + .ToArray(); + + // We're adding the contents back; do not destroy the contents + fontsVector.Clear(true); + fontsVector.AddRange(distinct.AsSpan()); + } + } + } + } + + /// + public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostBuild.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostBuild); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}] An error has occurred while during {delegate} PostBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostPromotion.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostPromotion); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs new file mode 100644 index 000000000..9ebf20fc7 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -0,0 +1,647 @@ +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Unicode; + +using Dalamud.Configuration.Internal; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + private static readonly Dictionary> PairAdjustmentsCache = + new(); + + /// + /// Implementations for and + /// . + /// + private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable + { + private static readonly ushort FontAwesomeIconMin = + (ushort)Enum.GetValues().Where(x => x > 0).Min(); + + private static readonly ushort FontAwesomeIconMax = + (ushort)Enum.GetValues().Where(x => x > 0).Max(); + + private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new(); + private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance; + private readonly FontAtlasFactory factory; + private readonly FontAtlasBuiltData data; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// New atlas. + /// An instance of . + /// Specify whether the current build operation is an asynchronous one. + public BuildToolkit( + FontAtlasFactory factory, + FontAtlasBuiltData data, + GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance, + bool isAsync) + { + this.data = data; + this.gameFontHandleSubstance = gameFontHandleSubstance; + this.IsAsyncBuildOperation = isAsync; + this.factory = factory; + } + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.data.Scale; + + /// + public bool IsAsyncBuildOperation { get; } + + /// + public FontAtlasBuildStep BuildStep { get; set; } + + /// + public ImFontAtlasPtr NewImAtlas => this.data.Atlas; + + /// + public ImVectorWrapper Fonts => this.data.Fonts; + + /// + /// Gets the list of fonts to ignore global scale. + /// + public List GlobalScaleExclusions { get; } = new(); + + /// + public void Dispose() => this.disposeAfterBuild.Dispose(); + + /// + public T2 DisposeAfterBuild(T2 disposable) where T2 : IDisposable => + this.disposeAfterBuild.Add(disposable); + + /// + public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle); + + /// + public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action); + + /// + public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) + { + this.GlobalScaleExclusions.Add(fontPtr); + return fontPtr; + } + + /// + public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => + this.GlobalScaleExclusions.Contains(fontPtr); + + /// + public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => + this.data.AddNewTexture(textureWrap, disposeOnError); + + /// + public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + { + Log.Verbose( + "[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}", + this.data.Owner?.Name ?? "(error)", + (nint)this.NewImAtlas.NativePtr, + nameof(this.AddFontFromImGuiHeapAllocatedMemory), + (nint)dataPointer, + dataSize, + debugTag); + + try + { + fontConfig.ThrowOnInvalidValues(); + + var raw = fontConfig.Raw with + { + FontData = dataPointer, + FontDataSize = dataSize, + }; + + if (fontConfig.GlyphRanges is not { Length: > 0 } ranges) + ranges = new ushort[] { 1, 0xFFFE, 0 }; + + raw.GlyphRanges = (ushort*)this.DisposeAfterBuild( + GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject(); + + TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw); + + var font = this.NewImAtlas.AddFont(&raw); + + var dataHash = default(HashCode); + dataHash.AddBytes(new(dataPointer, dataSize)); + var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32); + + List<(char Left, char Right, float Distance)> pairAdjustments; + lock (PairAdjustmentsCache) + { + if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments)) + { + PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new()); + try + { + pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray()); + } + catch + { + // don't care + } + } + } + + foreach (var pair in pairAdjustments) + { + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges)) + continue; + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges)) + continue; + + font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels); + } + + return font; + } + catch + { + if (freeOnException) + ImGuiNative.igMemFree(dataPointer); + throw; + } + } + + /// + public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig) + { + return this.AddFontFromStream( + File.OpenRead(path), + fontConfig, + false, + $"{nameof(this.AddFontFromFile)}({path})"); + } + + /// + public unsafe ImFontPtr AddFontFromStream( + Stream stream, + in SafeFontConfig fontConfig, + bool leaveOpen, + string debugTag) + { + using var streamCloser = leaveOpen ? null : stream; + if (!stream.CanSeek) + { + // There is no need to dispose a MemoryStream. + var ms = new MemoryStream(); + stream.CopyTo(ms); + stream = ms; + } + + var length = checked((int)(uint)stream.Length); + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + stream.ReadExactly(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromStream)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public unsafe ImFontPtr AddFontFromMemory( + ReadOnlySpan span, + in SafeFontConfig fontConfig, + string debugTag) + { + var length = span.Length; + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + span.CopyTo(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromMemory)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) + { + if (Service.Get().UseAxis) + { + return this.gameFontHandleSubstance.GetOrCreateFont( + new(GameFontFamily.Axis, sizePx), + this); + } + + glyphRanges ??= this.factory.DefaultGlyphRanges; + + var fontConfig = new SafeFontConfig + { + SizePx = sizePx, + GlyphRanges = glyphRanges, + }; + + var font = this.AddDalamudAssetFont(DalamudAsset.NotoSansJpMedium, fontConfig); + this.AddExtraGlyphsForDalamudLanguage(fontConfig with { MergeFont = font }); + this.AddGameSymbol(fontConfig with { MergeFont = font }); + return font; + } + + /// + public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig) + { + if (asset.GetPurpose() != DalamudAssetPurpose.Font) + throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font."); + + switch (asset) + { + case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + SizePx = (fontConfig.SizePx * 3) / 2, + }); + + case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: + return this.gameFontHandleSubstance.AttachGameSymbols( + this, + fontConfig.MergeFont, + fontConfig.SizePx); + + default: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + }); + } + } + + /// + public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( + DalamudAsset.FontAwesomeFreeSolid, + fontConfig with + { + GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 }, + }); + + /// + public void AddGameSymbol(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( + DalamudAsset.LodestoneGameSymbol, + fontConfig with + { + GlyphRanges = new ushort[] + { + GamePrebakedFontHandle.SeIconCharMin, + GamePrebakedFontHandle.SeIconCharMax, + 0, + }, + }); + + /// + public void AddExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) + { + var dalamudConfiguration = Service.Get(); + if (dalamudConfiguration.EffectiveLanguage == "ko") + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansKrRegular, + fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.HangulJamo, + UnicodeRanges.HangulCompatibilityJamo, + UnicodeRanges.HangulSyllables, + UnicodeRanges.HangulJamoExtendedA, + UnicodeRanges.HangulJamoExtendedB), + }); + } + } + + public void PreBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPreBuild(this); + } + + public unsafe void PreBuild() + { + var gamma = this.factory.InterfaceManager.FontGamma; + var configData = this.data.ConfigData; + foreach (ref var config in configData.DataSpan) + { + if (this.GlobalScaleExclusions.Contains(new(config.DstFont))) + continue; + + config.SizePixels *= this.Scale; + + config.GlyphMaxAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMaxAdvanceX)) + config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphMinAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMinAdvanceX)) + config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphOffset *= this.Scale; + + config.RasterizerGamma *= gamma; + } + } + + public void DoBuild() + { + // ImGui will call AddFontDefault() on Build() call. + // AddFontDefault() will reliably crash, when invoked multithreaded. + // We add a dummy font to prevent that. + if (this.data.ConfigData.Length == 0) + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 }); + } + + if (!this.NewImAtlas.Build()) + throw new InvalidOperationException("ImFontAtlas.Build failed"); + + this.BuildStep = FontAtlasBuildStep.PostBuild; + } + + public unsafe void PostBuild() + { + var scale = this.Scale; + foreach (ref var font in this.Fonts.DataSpan) + { + if (!this.GlobalScaleExclusions.Contains(font) && Math.Abs(scale - 1f) > 0f) + font.AdjustGlyphMetrics(1 / scale, scale); + + foreach (var c in FallbackCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.UpdateFallbackChar(c); + break; + } + + foreach (var c in EllipsisCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.EllipsisChar = c; + break; + } + } + } + + public void PostBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPostBuild(this); + } + + public unsafe void UploadTextures() + { + var buf = Array.Empty(); + try + { + var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = use4 ? 2 : 4; + var width = this.NewImAtlas.TexWidth; + var height = this.NewImAtlas.TexHeight; + foreach (ref var texture in this.data.ImTextures.DataSpan) + { + if (texture.TexID != 0) + { + // Nothing to do + } + else if (texture.TexPixelsRGBA32 is not null) + { + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + new(texture.TexPixelsRGBA32, width * height * 4), + width * 4, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + } + else if (texture.TexPixelsAlpha8 is not null) + { + var numPixels = width * height; + if (buf.Length < numPixels * bpp) + { + ArrayPool.Shared.Return(buf); + buf = ArrayPool.Shared.Rent(numPixels * bpp); + } + + fixed (void* pBuf = buf) + { + var sourcePtr = texture.TexPixelsAlpha8; + if (use4) + { + var target = (ushort*)pBuf; + while (numPixels-- > 0) + { + *target = (ushort)((*sourcePtr << 8) | 0x0FFF); + target++; + sourcePtr++; + } + } + else + { + var target = (uint*)pBuf; + while (numPixels-- > 0) + { + *target = (uint)((*sourcePtr << 24) | 0x00FFFFFF); + target++; + sourcePtr++; + } + } + } + + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + buf, + width * bpp, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + continue; + } + else + { + Log.Warning( + "[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null", + this.data.Owner?.Name ?? "(error)"); + } + + if (texture.TexPixelsRGBA32 is not null) + ImGuiNative.igMemFree(texture.TexPixelsRGBA32); + if (texture.TexPixelsAlpha8 is not null) + ImGuiNative.igMemFree(texture.TexPixelsAlpha8); + texture.TexPixelsRGBA32 = null; + texture.TexPixelsAlpha8 = null; + } + } + finally + { + ArrayPool.Shared.Return(buf); + } + } + } + + /// + /// Implementations for . + /// + private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion + { + private readonly FontAtlasBuiltData builtData; + + /// + /// Initializes a new instance of the class. + /// + /// The built data. + public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.builtData.Scale; + + /// + public bool IsAsyncBuildOperation => true; + + /// + public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; + + /// + public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; + + /// + public unsafe ImVectorWrapper Fonts => new( + &this.NewImAtlas.NativePtr->Fonts, + x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); + + /// + public unsafe void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE') + { + var sourceFound = false; + var targetFound = false; + foreach (var f in this.Fonts) + { + sourceFound |= f.NativePtr == source.NativePtr; + targetFound |= f.NativePtr == target.NativePtr; + } + + if (sourceFound && targetFound) + { + ImGuiHelpers.CopyGlyphsAcrossFonts( + source, + target, + missingOnly, + false, + rangeLow, + rangeHigh); + if (rebuildLookupTable) + this.BuildLookupTable(target); + } + } + + /// + public unsafe void BuildLookupTable(ImFontPtr font) + { + // Need to clear previous Fallback pointers before BuildLookupTable, or it may crash + font.NativePtr->FallbackGlyph = null; + font.NativePtr->FallbackHotData = null; + font.BuildLookupTable(); + + // Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking + // Codepoint < FallbackHotData.size always means that it's not fallback char. + // Otherwise, having a fallback character in ImGui.InputText gets strange. + var indexedHotData = font.IndexedHotDataWrapped(); + var indexLookup = font.IndexLookupWrapped(); + ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.NativePtr->FallbackHotData; + for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++) + { + if (indexLookup[codepoint] == ushort.MaxValue) + { + indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX; + indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth; + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs new file mode 100644 index 000000000..3f0b5b22e --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -0,0 +1,711 @@ +// #define VeryVerboseLog + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +using ImGuiNET; + +using JetBrains.Annotations; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + /// + /// Fallback codepoints for ImFont. + /// + public const string FallbackCodepoints = "\u3013\uFFFD?-"; + + /// + /// Ellipsis codepoints for ImFont. + /// + public const string EllipsisCodepoints = "\u2026\u0085"; + + /// + /// If set, disables concurrent font build operation. + /// + private static readonly object? NoConcurrentBuildOperationLock = null; // new(); + + private static readonly ModuleLog Log = new(nameof(FontAtlasFactory)); + + private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); + + private struct FontAtlasBuiltData : IDisposable + { + public readonly DalamudFontAtlas? Owner; + public readonly ImFontAtlasPtr Atlas; + public readonly float Scale; + + public bool IsBuildInProgress; + + private readonly List? wraps; + private readonly List? substances; + private readonly DisposeSafety.ScopedFinalizer? garbage; + + public unsafe FontAtlasBuiltData( + DalamudFontAtlas owner, + IEnumerable substances, + float scale) + { + this.Owner = owner; + this.Scale = scale; + this.garbage = new(); + + try + { + var substancesList = this.substances = new(); + foreach (var s in substances) + substancesList.Add(this.garbage.Add(s)); + this.garbage.Add(() => substancesList.Clear()); + + var wrapsCopy = this.wraps = new(); + this.garbage.Add(() => wrapsCopy.Clear()); + + var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); + this.Atlas = atlasPtr; + if (this.Atlas.NativePtr is null) + throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); + + this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); + this.IsBuildInProgress = true; + } + catch + { + this.garbage.Dispose(); + throw; + } + } + + public readonly DisposeSafety.ScopedFinalizer Garbage => + this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); + + public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + + public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + + public readonly IReadOnlyList Wraps => + (IReadOnlyList?)this.wraps ?? Array.Empty(); + + public readonly IReadOnlyList Substances => + (IReadOnlyList?)this.substances ?? Array.Empty(); + + public readonly void AddExistingTexture(IDalamudTextureWrap wrap) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + this.wraps.Add(this.Garbage.Add(wrap)); + } + + public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + var handle = wrap.ImGuiHandle; + var index = this.ImTextures.IndexOf(x => x.TexID == handle); + if (index == -1) + { + try + { + this.wraps.EnsureCapacity(this.wraps.Count + 1); + this.ImTextures.EnsureCapacityExponential(this.ImTextures.Length + 1); + + index = this.ImTextures.Length; + this.wraps.Add(this.Garbage.Add(wrap)); + this.ImTextures.Add(new() { TexID = handle }); + } + catch (Exception e) + { + if (disposeOnError) + wrap.Dispose(); + + if (this.wraps.Count != this.ImTextures.Length) + { + Log.Error( + e, + "{name} failed, and {wraps} and {imtextures} have different number of items", + nameof(this.AddNewTexture), + nameof(this.Wraps), + nameof(this.ImTextures)); + + if (this.wraps.Count > 0 && this.wraps[^1] == wrap) + this.wraps.RemoveAt(this.wraps.Count - 1); + if (this.ImTextures.Length > 0 && this.ImTextures[^1].TexID == handle) + this.ImTextures.RemoveAt(this.ImTextures.Length - 1); + + if (this.wraps.Count != this.ImTextures.Length) + Log.Fatal("^ Failed to undo due to an internal inconsistency; embrace for a crash"); + } + + throw; + } + } + + return index; + } + + public unsafe void Dispose() + { + if (this.garbage is null) + return; + + if (this.IsBuildInProgress) + { + Log.Error( + "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + + "Stack:\n{trace}", + this.Owner?.Name ?? "", + (nint)this.Atlas.NativePtr, + new StackTrace()); + while (this.IsBuildInProgress) + Thread.Sleep(100); + } + +#if VeryVerboseLog + Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); +#endif + this.garbage.Dispose(); + } + + public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) + { + var axisSubstance = this.Substances.OfType().Single(); + return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; + } + } + + private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback + { + private readonly DisposeSafety.ScopedFinalizer disposables = new(); + private readonly FontAtlasFactory factory; + private readonly DelegateFontHandle.HandleManager delegateFontHandleManager; + private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; + private readonly IFontHandleManager[] fontHandleManagers; + + private readonly object syncRootPostPromotion = new(); + private readonly object syncRoot = new(); + + private Task buildTask = EmptyTask; + private FontAtlasBuiltData builtData; + + private int buildIndex; + private bool buildQueued; + private bool disposed = false; + + /// + /// Initializes a new instance of the class. + /// + /// The factory. + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas are under the effect of global scale. + public DalamudFontAtlas( + FontAtlasFactory factory, + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled) + { + this.IsGlobalScaled = isGlobalScaled; + try + { + this.factory = factory; + this.AutoRebuildMode = autoRebuildMode; + this.Name = atlasName; + + this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend; + this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend); + + this.fontHandleManagers = new IFontHandleManager[] + { + this.delegateFontHandleManager = this.disposables.Add( + new DelegateFontHandle.HandleManager(atlasName)), + this.gameFontHandleManager = this.disposables.Add( + new GamePrebakedFontHandle.HandleManager(atlasName, factory)), + }; + foreach (var fhm in this.fontHandleManagers) + fhm.RebuildRecommend += this.OnRebuildRecommend; + } + catch + { + this.disposables.Dispose(); + throw; + } + + this.factory.SceneTask.ContinueWith( + r => + { + lock (this.syncRoot) + { + if (this.disposed) + return; + + r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame; + this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame); + } + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + this.BuildFontsOnNextFrame(); + }); + } + + /// + /// Finalizes an instance of the class. + /// + ~DalamudFontAtlas() + { + lock (this.syncRoot) + { + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.builtData.Dispose(); + } + } + + /// + public event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + public event Action? RebuildRecommend; + + /// + public event Action? BeforeDispose; + + /// + public event Action? AfterDispose; + + /// + public string Name { get; } + + /// + public FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + public ImFontAtlasPtr ImAtlas + { + get + { + lock (this.syncRoot) + return this.builtData.Atlas; + } + } + + /// + public Task BuildTask => this.buildTask; + + /// + public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); + + /// + public bool IsGlobalScaled { get; } + + /// + public void Dispose() + { + if (this.disposed) + return; + + this.BeforeDispose?.InvokeSafely(this); + + try + { + lock (this.syncRoot) + { + this.disposed = true; + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.buildTask = EmptyTask; + this.disposables.Add(this.builtData); + this.builtData = default; + this.disposables.Dispose(); + } + + try + { + this.AfterDispose?.Invoke(this, null); + } + catch + { + // ignore + } + } + catch (Exception e) + { + try + { + this.AfterDispose?.Invoke(this, e); + } + catch + { + // ignore + } + } + + GC.SuppressFinalize(this); + } + + /// + public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); + + /// + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate @delegate) => + this.delegateFontHandleManager.NewFontHandle(@delegate); + + /// + public void FreeFontHandle(IFontHandle handle) + { + foreach (var manager in this.fontHandleManagers) + { + manager.FreeFontHandle(handle); + } + } + + /// + public void BuildFontsOnNextFrame() + { + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsOnNextFrame)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + if (!this.buildTask.IsCompleted || this.buildQueued) + return; + +#if VeryVerboseLog + Log.Verbose("[{name}] Queueing from {source}.", this.Name, nameof(this.BuildFontsOnNextFrame)); +#endif + + this.buildQueued = true; + } + + /// + public void BuildFontsImmediately() + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsImmediately)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + var tcs = new TaskCompletionSource(); + int rebuildIndex; + try + { + rebuildIndex = ++this.buildIndex; + lock (this.syncRoot) + { + if (!this.buildTask.IsCompleted) + throw new InvalidOperationException("Font rebuild is already in progress."); + + this.buildTask = tcs.Task; + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var r = this.RebuildFontsPrivate(false, scale); + r.Wait(); + if (r.IsCompletedSuccessfully) + tcs.SetResult(r.Result); + else if (r.Exception is not null) + tcs.SetException(r.Exception); + else + tcs.SetCanceled(); + } + catch (Exception e) + { + tcs.SetException(e); + Log.Error(e, "[{name}] Failed to build fonts.", this.Name); + throw; + } + + this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately)); + } + + /// + public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true) + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsAsync)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.OnNewFrame)}."); + } + + lock (this.syncRoot) + { + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var rebuildIndex = ++this.buildIndex; + return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); + + async Task BuildInner(Task unused) + { + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + return default; + } + + var res = await this.RebuildFontsPrivate(true, scale); + if (res.Atlas.IsNull()) + return res; + + if (callPostPromotionOnMainThread) + { + await this.factory.Framework.RunOnFrameworkThread( + () => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync))); + } + else + { + this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); + } + + return res; + } + } + } + + private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) + { + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + this.builtData.ExplicitDisposeIgnoreExceptions(); + this.builtData = data; + this.buildTask = EmptyTask; + foreach (var substance in data.Substances) + substance.Manager.Substance = substance; + } + + lock (this.syncRootPostPromotion) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + var toolkit = new BuildToolkitPostPromotion(data); + + try + { + this.BuildStepChange?.Invoke(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {delegateName} PostPromotion error", + this.Name, + nameof(FontAtlasBuildStepDelegate)); + } + + foreach (var substance in data.Substances) + { + try + { + substance.OnPostPromotion(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {substance} PostPromotion error", + this.Name, + substance.GetType().FullName ?? substance.GetType().Name); + } + } + + foreach (var font in toolkit.Fonts) + { + try + { + toolkit.BuildLookupTable(font); + } + catch (Exception e) + { + Log.Error(e, "[{name}] BuildLookupTable error", this.Name); + } + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Built from {source}.", this.Name, source); +#endif + } + } + + private void ImGuiSceneOnNewRenderFrame() + { + if (!this.buildQueued) + return; + + try + { + if (this.AutoRebuildMode != FontAtlasAutoRebuildMode.Async) + this.BuildFontsImmediately(); + } + finally + { + this.buildQueued = false; + } + } + + private Task RebuildFontsPrivate(bool isAsync, float scale) + { + if (NoConcurrentBuildOperationLock is null) + return this.RebuildFontsPrivateReal(isAsync, scale); + lock (NoConcurrentBuildOperationLock) + return this.RebuildFontsPrivateReal(isAsync, scale); + } + + private async Task RebuildFontsPrivateReal(bool isAsync, float scale) + { + lock (this.syncRoot) + { + // this lock ensures that this.buildTask is properly set. + } + + var sw = new Stopwatch(); + sw.Start(); + + var res = default(FontAtlasBuiltData); + nint atlasPtr = 0; + try + { + res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + unsafe + { + atlasPtr = (nint)res.Atlas.NativePtr; + } + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: PreBuild (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + using var toolkit = res.CreateToolkit(this.factory, isAsync); + this.BuildStepChange?.Invoke(toolkit); + toolkit.PreBuildSubstances(); + toolkit.PreBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.DoBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: PostBuild (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.PostBuild(); + toolkit.PostBuildSubstances(); + this.BuildStepChange?.Invoke(toolkit); + + if (this.factory.SceneTask is { IsCompleted: false } sceneTask) + { + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + await sceneTask.ConfigureAwait(!isAsync); + } + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: UploadTextures (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + toolkit.UploadTextures(); + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: Complete (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + res.IsBuildInProgress = false; + return res; + } + catch (Exception e) + { + Log.Error( + e, + "[{name}:{functionname}] 0x{ptr:X}: Failed (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + res.IsBuildInProgress = false; + res.Dispose(); + throw; + } + finally + { + this.buildQueued = false; + } + } + + private void OnRebuildRecommend() + { + if (this.disposed) + return; + + this.factory.Framework.RunOnFrameworkThread( + () => + { + this.RebuildRecommend?.InvokeSafely(); + + switch (this.AutoRebuildMode) + { + case FontAtlasAutoRebuildMode.Async: + _ = this.BuildFontsAsync(); + break; + case FontAtlasAutoRebuildMode.OnNewFrame: + this.BuildFontsOnNextFrame(); + break; + case FontAtlasAutoRebuildMode.Disable: + default: + break; + } + }); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs new file mode 100644 index 000000000..7a1926a9d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -0,0 +1,379 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using ImGuiScene; + +using Lumina.Data.Files; + +using SharpDX; +using SharpDX.Direct3D11; +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Factory for the implementation of . +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed partial class FontAtlasFactory + : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable +{ + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly IReadOnlyDictionary> fdtFiles; + private readonly IReadOnlyDictionary[]>> texFiles; + private readonly IReadOnlyDictionary> prebakedTextureWraps; + private readonly Task defaultGlyphRanges; + private readonly DalamudAssetManager dalamudAssetManager; + + private float lastBuildGamma = -1f; + + [ServiceManager.ServiceConstructor] + private FontAtlasFactory( + DataManager dataManager, + Framework framework, + InterfaceManager interfaceManager, + DalamudAssetManager dalamudAssetManager) + { + this.Framework = framework; + this.InterfaceManager = interfaceManager; + this.dalamudAssetManager = dalamudAssetManager; + this.SceneTask = Service + .GetAsync() + .ContinueWith(r => r.Result.Manager.Scene); + + var gffasInfo = Enum.GetValues() + .Select( + x => + ( + Font: x, + Attr: x.GetAttribute())) + .Where(x => x.Attr is not null) + .ToArray(); + var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray(); + + this.fdtFiles = gffasInfo.ToImmutableDictionary( + x => x.Font, + x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); + var channelCountsTask = texPaths.ToImmutableDictionary( + x => x, + x => Task.WhenAll( + gffasInfo.Where(y => y.Attr.TexPathFormat == x) + .Select(y => this.fdtFiles[y.Font])) + .ContinueWith( + files => 1 + files.Result.Max( + file => + { + unsafe + { + using var pin = file.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Length); + return fdt.MaxTextureIndex; + } + }))); + this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result])); + this.texFiles = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith( + y => Enumerable + .Range(1, 1 + ((y.Result - 1) / 4)) + .Select(z => Task.Run(() => dataManager.GetFile(string.Format(x.Key, z))!)) + .ToArray())); + this.defaultGlyphRanges = + this.fdtFiles[GameFontFamilyAndSize.Axis12] + .ContinueWith( + file => + { + unsafe + { + using var pin = file.Result.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Result.Length); + return fdt.ToGlyphRanges(); + } + }); + } + + /// + /// Gets the service instance of . + /// + public Framework Framework { get; } + + /// + /// Gets the service instance of .
+ /// may not yet be available. + ///
+ public InterfaceManager InterfaceManager { get; } + + /// + /// Gets the async task for inside . + /// + public Task SceneTask { get; } + + /// + /// Gets the default glyph ranges (glyph ranges of ). + /// + public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges); + + /// + /// Gets a value indicating whether game symbol font file is available. + /// + public bool HasGameSymbolsFontFile => + this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); + + /// + public void Dispose() + { + this.cancellationTokenSource.Cancel(); + this.scopedFinalizer.Dispose(); + this.cancellationTokenSource.Dispose(); + } + + /// + /// Creates a new instance of a class that implements the interface. + /// + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas is global scaled. + /// The new font atlas. + public IFontAtlas CreateFontAtlas( + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true) => + new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled); + + /// + /// Adds the font from Dalamud Assets. + /// + /// The toolkitPostBuild. + /// The font. + /// The font config. + /// The address and size. + public ImFontPtr AddFont( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + DalamudAsset asset, + in SafeFontConfig fontConfig) => + toolkitPreBuild.AddFontFromStream( + this.dalamudAssetManager.CreateStream(asset), + fontConfig, + false, + $"Asset({asset})"); + + /// + /// Gets the for the . + /// + /// The font family and size. + /// The . + public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); + + /// + public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) + { + var arr = ExtractResult(this.fdtFiles[gffas]); + var handle = arr.AsMemory().Pin(); + try + { + fdtFileView = new(handle.Pointer, arr.Length); + return handle; + } + catch + { + handle.Dispose(); + throw; + } + } + + /// + public int GetFontTextureCount(string texPathFormat) => + ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length; + + /// + public TexFile GetTexFile(string texPathFormat, int index) => + ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]); + + /// + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex) + { + lock (this.prebakedTextureWraps[texPathFormat]) + { + var gamma = this.InterfaceManager.FontGamma; + var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]); + if (Math.Abs(this.lastBuildGamma - gamma) > 0.0001f) + { + this.lastBuildGamma = gamma; + wraps.AggregateToDisposable().Dispose(); + wraps.AsSpan().Clear(); + } + + var fileIndex = textureIndex / 4; + var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; + wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex, gamma); + return CloneTextureWrap(wraps[textureIndex]); + } + } + + private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); + + private static unsafe void ExtractChannelFromB8G8R8A8( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4, + float gamma) + { + var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + var gammaTable = stackalloc byte[256]; + for (var i = 0; i < 256; i++) + gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 255f, 0, 1), 1.4f / gamma) * 255); + + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + channelIndex; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)((gammaTable[*rptr] << 8) | 0x0FFF); + wptr++; + rptr += 4; + } + } + else + { + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (uint)((gammaTable[*rptr] << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + /// + /// Clones a texture wrap, by getting a new reference to the underlying and the + /// texture behind. + /// + /// The to clone from. + /// The cloned . + private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) + { + var srv = CppObject.FromPointer(wrap.ImGuiHandle); + using var res = srv.Resource; + using var tex2D = res.QueryInterface(); + var description = tex2D.Description; + return new DalamudTextureWrap( + new D3DTextureWrap( + srv.QueryInterface(), + description.Width, + description.Height)); + } + + private static unsafe void ExtractChannelFromB4G4R4A4( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4, + float gamma) + { + var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + (channelIndex / 2); + var rshift = (channelIndex & 1) == 0 ? 0 : 4; + var gammaTable = stackalloc byte[256]; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + for (var i = 0; i < 16; i++) + gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 15f, 0, 1), 1.4f / gamma) * 15); + + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)((gammaTable[(*rptr >> rshift) & 0xF] << 12) | 0x0FFF); + wptr++; + rptr += 2; + } + } + else + { + for (var i = 0; i < 256; i++) + gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 255f, 0, 1), 1.4f / gamma) * 255); + + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + var v = (*rptr >> rshift) & 0xF; + v |= v << 4; + *wptr = (uint)((gammaTable[v] << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex, float gamma) + { + var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); + var numPixels = texFile.Header.Width * texFile.Header.Height; + + _ = Service.Get(); + var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = targetIsB4G4R4A4 ? 2 : 4; + var buffer = ArrayPool.Shared.Rent(numPixels * bpp); + try + { + var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _); + switch (texFile.Header.Format) + { + case TexFile.TextureFormat.B4G4R4A4: + // Game ships with this format. + ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4, gamma); + break; + case TexFile.TextureFormat.B8G8R8A8: + // In case of modded font textures. + ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4, gamma); + break; + default: + // Unlikely. + ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4, gamma); + break; + } + + return this.scopedFinalizer.Add( + this.InterfaceManager.LoadImageFromDxgiFormat( + buffer, + texFile.Header.Width * bpp, + texFile.Header.Width, + texFile.Header.Height, + targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs new file mode 100644 index 000000000..012613a38 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -0,0 +1,692 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; + +using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; + +using ImGuiNET; + +using Lumina.Data.Files; + +using Vector4 = System.Numerics.Vector4; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle that uses the game's built-in fonts, optionally with some styling. +/// +internal class GamePrebakedFontHandle : IFontHandle.IInternal +{ + /// + /// The smallest value of . + /// + public static readonly char SeIconCharMin = (char)Enum.GetValues().Min(); + + /// + /// The largest value of . + /// + public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); + + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Font to use. + public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style) + { + if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined) + throw new ArgumentOutOfRangeException(nameof(style), style, null); + + if (style.SizePt <= 0) + throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style)); + + this.manager = manager; + this.FontStyle = style; + } + + /// + /// Provider for for `common/font/fontNN.tex`. + /// + public interface IGameFontTextureProvider + { + /// + /// Creates the for the .
+ /// Dispose after use. + ///
+ /// The font family and size. + /// The view. + /// Dispose this after use.. + public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView); + + /// + /// Gets the number of font textures. + /// + /// Format of .tex path. + /// The number of textures. + public int GetFontTextureCount(string texPathFormat); + + /// + /// Gets the for the given index of a font. + /// + /// Format of .tex path. + /// The index of .tex file. + /// The . + public TexFile GetTexFile(string texPathFormat, int index); + + /// + /// Gets a new reference of the font texture. + /// + /// Format of .tex path. + /// Texture index. + /// The texture. + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex); + } + + /// + /// Gets the font style. + /// + public GameFontStyle FontStyle { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly Dictionary gameFontsRc = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + /// An instance of . + public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider) + { + this.GameFontTextureProvider = gameFontTextureProvider; + this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager"; + } + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + /// Gets an instance of . + /// + public IGameFontTextureProvider GameFontTextureProvider { get; } + + /// + public void Dispose() + { + this.Substance?.Dispose(); + this.Substance = null; + } + + /// + /// Creates a new from game's built-in fonts. + /// + /// Font to use. + /// Handle to a font that may or may not be ready yet. + public IFontHandle NewFontHandle(GameFontStyle style) + { + var handle = new GamePrebakedFontHandle(this, style); + bool suggestRebuild; + lock (this.syncRoot) + { + this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; + suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; + } + + if (suggestRebuild) + this.RebuildRecommend?.Invoke(); + + return handle; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not GamePrebakedFontHandle ggfh) + return; + + lock (this.syncRoot) + { + if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) + return; + + if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0) + this.gameFontsRc.Remove(ggfh.FontStyle); + } + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.gameFontsRc.Keys); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private readonly HandleManager handleManager; + private readonly InterfaceManager interfaceManager; + private readonly HashSet gameFontStyles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + + private readonly Dictionary fontsSymbolsOnly = new(); + private readonly Dictionary> symbolsCopyTargets = new(); + + private readonly HashSet templatedFonts = new(); + private readonly Dictionary> lateBuildRanges = new(); + + private readonly Dictionary> glyphRectIds = + new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The game font styles. + public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) + { + this.handleManager = manager; + this.interfaceManager = Service.Get(); + this.gameFontStyles = new(gameFontStyles); + } + + /// + public IFontHandleManager Manager => this.handleManager; + + /// + public void Dispose() + { + } + + /// + /// Attaches game symbols to the given font. + /// + /// The toolkitPostBuild. + /// The font to attach to. + /// The font size in pixels. + /// if it is not empty; otherwise a new font. + public ImFontPtr AttachGameSymbols(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, ImFontPtr font, float sizePx) + { + var style = new GameFontStyle(GameFontFamily.Axis, sizePx); + if (!this.fontsSymbolsOnly.TryGetValue(style, out var symbolFont)) + { + symbolFont = this.CreateFontPrivate(style, toolkitPreBuild, ' ', '\uFFFE', true); + this.fontsSymbolsOnly.Add(style, symbolFont); + } + + if (font.IsNull()) + font = this.CreateTemplateFont(style, toolkitPreBuild); + + if (!this.symbolsCopyTargets.TryGetValue(symbolFont, out var set)) + this.symbolsCopyTargets[symbolFont] = set = new(); + + set.Add(font); + return font; + } + + /// + /// Creates or gets a relevant for the given . + /// + /// The game font style. + /// The toolkitPostBuild. + /// The font. + public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + if (this.fonts.TryGetValue(style, out var font)) + return font; + + try + { + font = this.CreateFontPrivate(style, toolkitPreBuild, ' ', '\uFFFE', true); + this.fonts.Add(style, font); + return font; + } + catch (Exception e) + { + this.buildExceptions[style] = e; + throw; + } + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh ? this.fonts.GetValueOrDefault(ggfh.FontStyle) : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var style in this.gameFontStyles) + { + if (this.fonts.ContainsKey(style)) + continue; + + try + { + _ = this.GetOrCreateFont(style, toolkitPreBuild); + } + catch + { + // ignore; it should have been recorded from the call + } + } + } + + /// + public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + var allTextureIndices = new Dictionary(); + var allTexFiles = new Dictionary(); + using var rentReturn = Disposable.Create( + () => + { + foreach (var x in allTextureIndices.Values) + ArrayPool.Shared.Return(x); + foreach (var x in allTexFiles.Values) + ArrayPool.Shared.Return(x); + }); + + var fontGamma = this.interfaceManager.FontGamma; + var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; + var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; + var heights = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; + for (var i = 0; i < pixels8Array.Length; i++) + toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out heights[i]); + + foreach (var (style, font) in this.fonts.Concat(this.fontsSymbolsOnly)) + { + try + { + var fas = GameFontStyle.GetRecommendedFamilyAndSize( + style.Family, + style.SizePt * toolkitPostBuild.Scale); + var attr = fas.GetAttribute(); + var horizontalOffset = attr?.HorizontalOffset ?? 0; + var texCount = this.handleManager.GameFontTextureProvider.GetFontTextureCount(attr.TexPathFormat); + using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); + ref var fdtFontHeader = ref fdt.FontHeader; + var fdtGlyphs = fdt.Glyphs; + var fontPtr = font.NativePtr; + + fontPtr->FontSize = (fdtFontHeader.Size * 4) / 3; + if (fontPtr->ConfigData != null) + fontPtr->ConfigData->SizePixels = fontPtr->FontSize; + fontPtr->Ascent = fdtFontHeader.Ascent; + fontPtr->Descent = fdtFontHeader.Descent; + fontPtr->EllipsisChar = '…'; + + if (!allTexFiles.TryGetValue(attr.TexPathFormat, out var texFiles)) + allTexFiles.Add(attr.TexPathFormat, texFiles = ArrayPool.Shared.Rent(texCount)); + + if (this.glyphRectIds.TryGetValue(style, out var rectIdToGlyphs)) + { + this.glyphRectIds.Remove(style); + + foreach (var (rectId, fdtGlyphIndex) in rectIdToGlyphs.Values) + { + ref var glyph = ref fdtGlyphs[fdtGlyphIndex]; + var rc = (ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas + .GetCustomRectByIndex(rectId) + .NativePtr; + var pixels8 = pixels8Array[rc->TextureIndex]; + var width = widths[rc->TextureIndex]; + texFiles[glyph.TextureFileIndex] ??= + this.handleManager + .GameFontTextureProvider + .GetTexFile(attr.TexPathFormat, glyph.TextureFileIndex); + var sourceBuffer = texFiles[glyph.TextureFileIndex].ImageData; + var sourceBufferDelta = glyph.TextureChannelByteIndex; + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdtFontHeader, glyph); + if (widthAdjustment == 0) + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var a = sourceBuffer[ + sourceBufferDelta + + (4 * (((glyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + + glyph.TextureOffsetX + x))]; + pixels8[((rc->Y + y) * width) + rc->X + x] = a; + } + } + } + else + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) + pixels8[((rc->Y + y) * width) + rc->X + x] = 0; + } + + for (int xbold = 0, xboldTo = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); + xbold < xboldTo; + xbold++) + { + var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); + for (var y = 0; y < glyph.BoundingHeight; y++) + { + float xDelta = xbold; + if (style.BaseSkewStrength > 0) + { + xDelta += style.BaseSkewStrength * + (fdtFontHeader.LineHeight - glyph.CurrentOffsetY - y) / + fdtFontHeader.LineHeight; + } + else if (style.BaseSkewStrength < 0) + { + xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / + fdtFontHeader.LineHeight; + } + + var xDeltaInt = (int)Math.Floor(xDelta); + var xness = xDelta - xDeltaInt; + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var sourcePixelIndex = + ((glyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + + glyph.TextureOffsetX + x; + var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; + var a2 = x == glyph.BoundingWidth - 1 + ? 0 + : sourceBuffer[sourceBufferDelta + + (4 * (sourcePixelIndex + 1))]; + var n = (a1 * xness) + (a2 * (1 - xness)); + var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; + pixels8[targetOffset] = + Math.Max(pixels8[targetOffset], (byte)(boldStrength * n)); + } + } + } + } + + if (Math.Abs(fontGamma - 1.4f) >= 0.001) + { + // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) + var xTo = rc->X + rc->Width; + var yTo = rc->Y + rc->Height; + for (int y = rc->Y; y < yTo; y++) + { + for (int x = rc->X; x < xTo; x++) + { + var i = (y * width) + x; + pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); + } + } + } + } + } + else if (this.lateBuildRanges.TryGetValue(font, out var buildRanges)) + { + buildRanges.Sort(); + for (var i = 0; i < buildRanges.Count; i++) + { + var current = buildRanges[i]; + if (current.From > current.To) + buildRanges[i] = (From: current.To, To: current.From); + } + + for (var i = 0; i < buildRanges.Count - 1; i++) + { + var current = buildRanges[i]; + var next = buildRanges[i + 1]; + if (next.From <= current.To) + { + buildRanges[i] = current with { To = next.To }; + buildRanges.RemoveAt(i + 1); + i--; + } + } + + var fdtTexSize = new Vector4( + fdtFontHeader.TextureWidth, + fdtFontHeader.TextureHeight, + fdtFontHeader.TextureWidth, + fdtFontHeader.TextureHeight); + + if (!allTextureIndices.TryGetValue(attr.TexPathFormat, out var textureIndices)) + { + allTextureIndices.Add( + attr.TexPathFormat, + textureIndices = ArrayPool.Shared.Rent(texCount)); + textureIndices.AsSpan(0, texCount).Fill(-1); + } + + var glyphs = font.GlyphsWrapped(); + glyphs.EnsureCapacity(glyphs.Length + buildRanges.Sum(x => (x.To - x.From) + 1)); + foreach (var (rangeMin, rangeMax) in buildRanges) + { + var glyphIndex = fdt.FindGlyphIndex(rangeMin); + if (glyphIndex < 0) + glyphIndex = ~glyphIndex; + var endIndex = fdt.FindGlyphIndex(rangeMax); + if (endIndex < 0) + endIndex = ~endIndex - 1; + for (; glyphIndex <= endIndex; glyphIndex++) + { + var fdtg = fdtGlyphs[glyphIndex]; + + // If the glyph already exists in the target font, we do not overwrite. + if ( + !(fdtg.Char == ' ' && this.templatedFonts.Contains(font)) + && font.FindGlyphNoFallback(fdtg.Char).NativePtr is not null) + { + continue; + } + + ref var textureIndex = ref textureIndices[fdtg.TextureIndex]; + if (textureIndex == -1) + { + textureIndex = toolkitPostBuild.StoreTexture( + this.handleManager + .GameFontTextureProvider + .NewFontTextureRef(attr.TexPathFormat, fdtg.TextureIndex), + true); + } + + var glyph = new ImGuiHelpers.ImFontGlyphReal + { + AdvanceX = fdtg.AdvanceWidth, + Codepoint = fdtg.Char, + Colored = false, + TextureIndex = textureIndex, + Visible = true, + X0 = horizontalOffset, + Y0 = fdtg.CurrentOffsetY, + U0 = fdtg.TextureOffsetX, + V0 = fdtg.TextureOffsetY, + U1 = fdtg.BoundingWidth, + V1 = fdtg.BoundingHeight, + }; + + glyph.XY1 = glyph.XY0 + glyph.UV1; + glyph.UV1 += glyph.UV0; + glyph.UV /= fdtTexSize; + + glyphs.Add(glyph); + } + } + + font.NativePtr->FallbackGlyph = null; + + font.BuildLookupTable(); + } + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); + if ((IntPtr)glyph.NativePtr != IntPtr.Zero) + { + var ptr = font.NativePtr; + ptr->FallbackChar = fallbackCharCandidate; + ptr->FallbackGlyph = glyph.NativePtr; + ptr->FallbackHotData = + (ImFontGlyphHotData*)ptr->IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + + font.AdjustGlyphMetrics(style.SizePt / fdtFontHeader.Size, toolkitPostBuild.Scale); + } + catch (Exception e) + { + this.buildExceptions[style] = e; + this.fonts[style] = default; + } + } + + foreach (var (source, targets) in this.symbolsCopyTargets) + { + foreach (var target in targets) + ImGuiHelpers.CopyGlyphsAcrossFonts(source, target, true, true, SeIconCharMin, SeIconCharMax); + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + // Irrelevant + } + + /// + /// Creates a relevant for the given . + /// + /// The game font style. + /// The toolkitPostBuild. + /// Min range. + /// Max range. + /// Add extra language glyphs. + /// The font. + private ImFontPtr CreateFontPrivate( + GameFontStyle style, + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + char minRange, + char maxRange, + bool addExtraLanguageGlyphs) + { + var font = toolkitPreBuild.IgnoreGlobalScale(this.CreateTemplateFont(style, toolkitPreBuild)); + + if (addExtraLanguageGlyphs) + toolkitPreBuild.AddExtraGlyphsForDalamudLanguage(new() { MergeFont = font }); + + var fas = GameFontStyle.GetRecommendedFamilyAndSize(style.Family, style.SizePt * toolkitPreBuild.Scale); + var horizontalOffset = fas.GetAttribute()?.HorizontalOffset ?? 0; + using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); + ref var fdtFontHeader = ref fdt.FontHeader; + var existing = new SortedSet(); + + if (style is { Bold: false, Italic: false }) + { + if (!this.lateBuildRanges.TryGetValue(font, out var ranges)) + this.lateBuildRanges[font] = ranges = new(); + + ranges.Add((minRange, maxRange)); + } + else + { + if (this.glyphRectIds.TryGetValue(style, out var rectIds)) + existing.UnionWith(rectIds.Keys); + else + rectIds = this.glyphRectIds[style] = new(); + + var glyphs = fdt.Glyphs; + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint < minRange || cint > maxRange) + continue; + + var c = (char)cint; + if (existing.Contains(c)) + continue; + + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdtFontHeader, glyph); + rectIds[c] = ( + toolkitPreBuild.NewImAtlas.AddCustomRectFontGlyph( + font, + c, + glyph.BoundingWidth + widthAdjustment, + glyph.BoundingHeight, + glyph.AdvanceWidth, + new(horizontalOffset, glyph.CurrentOffsetY)), + fdtGlyphIndex); + } + } + + foreach (ref var kernPair in fdt.PairAdjustments) + font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); + + return font; + } + + /// + /// Creates a new template font. + /// + /// The game font style. + /// The toolkitPostBuild. + /// The font. + private ImFontPtr CreateTemplateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + var font = toolkitPreBuild.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() + { + GlyphRanges = new ushort[] { ' ', ' ', '\0' }, + SizePx = style.SizePx * toolkitPreBuild.Scale, + }); + this.templatedFonts.Add(font); + return font; + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs new file mode 100644 index 000000000..795ca61fc --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -0,0 +1,34 @@ +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Manager for . +/// +internal interface IFontHandleManager : IDisposable +{ + /// + /// Event fired when a font rebuild operation is suggested. + /// + event Action? RebuildRecommend; + + /// + /// Gets the name of the font handle manager. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets or sets the active font handle substance. + /// + IFontHandleSubstance? Substance { get; set; } + + /// + /// Decrease font reference counter. + /// + /// Handle being released. + void FreeFontHandle(IFontHandle handle); + + /// + /// Creates a new substance of the font atlas. + /// + /// The new substance. + IFontHandleSubstance NewSubstance(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs new file mode 100644 index 000000000..fbfa2d12e --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -0,0 +1,47 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Substance of a font. +/// +internal interface IFontHandleSubstance : IDisposable +{ + /// + /// Gets the manager relevant to this instance of . + /// + IFontHandleManager Manager { get; } + + /// + /// Gets the font. + /// + /// The handle to get from. + /// Corresponding font or null. + ImFontPtr GetFontPtr(IFontHandle handle); + + /// + /// Gets the exception happened while loading for the font. + /// + /// The handle to get from. + /// Corresponding font or null. + Exception? GetBuildException(IFontHandle handle); + + /// + /// Called before call. + /// + /// The toolkit. + void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called after call. + /// + /// The toolkit. + void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild); + + /// + /// Called on the specific thread depending on after + /// promoting the staging atlas to direct use with . + /// + /// The toolkit. + void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs new file mode 100644 index 000000000..8e7149853 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs @@ -0,0 +1,203 @@ +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private struct Fixed : IComparable + { + public ushort Major; + public ushort Minor; + + public Fixed(ushort major, ushort minor) + { + this.Major = major; + this.Minor = minor; + } + + public Fixed(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Major); + span.ReadBig(ref offset, out this.Minor); + } + + public int CompareTo(Fixed other) + { + var majorComparison = this.Major.CompareTo(other.Major); + return majorComparison != 0 ? majorComparison : this.Minor.CompareTo(other.Minor); + } + } + + private struct KerningPair : IEquatable + { + public ushort Left; + public ushort Right; + public short Value; + + public KerningPair(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Left); + span.ReadBig(ref offset, out this.Right); + span.ReadBig(ref offset, out this.Value); + } + + public KerningPair(ushort left, ushort right, short value) + { + this.Left = left; + this.Right = right; + this.Value = value; + } + + public static bool operator ==(KerningPair left, KerningPair right) => left.Equals(right); + + public static bool operator !=(KerningPair left, KerningPair right) => !left.Equals(right); + + public static KerningPair ReverseEndianness(KerningPair pair) => new() + { + Left = BinaryPrimitives.ReverseEndianness(pair.Left), + Right = BinaryPrimitives.ReverseEndianness(pair.Right), + Value = BinaryPrimitives.ReverseEndianness(pair.Value), + }; + + public bool Equals(KerningPair other) => + this.Left == other.Left && this.Right == other.Right && this.Value == other.Value; + + public override bool Equals(object? obj) => obj is KerningPair other && this.Equals(other); + + public override int GetHashCode() => HashCode.Combine(this.Left, this.Right, this.Value); + + public override string ToString() => $"KerningPair[{this.Left}, {this.Right}] = {this.Value}"; + } + + [StructLayout(LayoutKind.Explicit, Size = 4)] + private struct PlatformAndEncoding + { + [FieldOffset(0)] + public PlatformId Platform; + + [FieldOffset(2)] + public UnicodeEncodingId UnicodeEncoding; + + [FieldOffset(2)] + public MacintoshEncodingId MacintoshEncoding; + + [FieldOffset(2)] + public IsoEncodingId IsoEncoding; + + [FieldOffset(2)] + public WindowsEncodingId WindowsEncoding; + + public PlatformAndEncoding(PointerSpan source) + { + var offset = 0; + source.ReadBig(ref offset, out this.Platform); + source.ReadBig(ref offset, out this.UnicodeEncoding); + } + + public static PlatformAndEncoding ReverseEndianness(PlatformAndEncoding value) => new() + { + Platform = (PlatformId)BinaryPrimitives.ReverseEndianness((ushort)value.Platform), + UnicodeEncoding = (UnicodeEncodingId)BinaryPrimitives.ReverseEndianness((ushort)value.UnicodeEncoding), + }; + + public readonly string Decode(Span data) + { + switch (this.Platform) + { + case PlatformId.Unicode: + switch (this.UnicodeEncoding) + { + case UnicodeEncodingId.Unicode_2_0_Bmp: + case UnicodeEncodingId.Unicode_2_0_Full: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + + case PlatformId.Macintosh: + switch (this.MacintoshEncoding) + { + case MacintoshEncodingId.Roman: + return Encoding.ASCII.GetString(data); + } + + break; + + case PlatformId.Windows: + switch (this.WindowsEncoding) + { + case WindowsEncodingId.Symbol: + case WindowsEncodingId.UnicodeBmp: + case WindowsEncodingId.UnicodeFullRepertoire: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + } + + throw new NotSupportedException(); + } + } + + [StructLayout(LayoutKind.Explicit)] + private struct TagStruct : IEquatable, IComparable + { + [FieldOffset(0)] + public unsafe fixed byte Tag[4]; + + [FieldOffset(0)] + public uint NativeValue; + + public unsafe TagStruct(char c1, char c2, char c3, char c4) + { + this.Tag[0] = checked((byte)c1); + this.Tag[1] = checked((byte)c2); + this.Tag[2] = checked((byte)c3); + this.Tag[3] = checked((byte)c4); + } + + public unsafe TagStruct(PointerSpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe TagStruct(ReadOnlySpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe byte this[int index] + { + get => this.Tag[index]; + set => this.Tag[index] = value; + } + + public static bool operator ==(TagStruct left, TagStruct right) => left.Equals(right); + + public static bool operator !=(TagStruct left, TagStruct right) => !left.Equals(right); + + public bool Equals(TagStruct other) => this.NativeValue == other.NativeValue; + + public override bool Equals(object? obj) => obj is TagStruct other && this.Equals(other); + + public override int GetHashCode() => (int)this.NativeValue; + + public int CompareTo(TagStruct other) => this.NativeValue.CompareTo(other.NativeValue); + + public override unsafe string ToString() => + $"0x{this.NativeValue:08X} \"{(char)this.Tag[0]}{(char)this.Tag[1]}{(char)this.Tag[2]}{(char)this.Tag[3]}\""; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs new file mode 100644 index 000000000..f6a653a51 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum IsoEncodingId : ushort + { + Ascii = 0, + Iso_10646 = 1, + Iso_8859_1 = 2, + } + + private enum MacintoshEncodingId : ushort + { + Roman = 0, + } + + private enum NameId : ushort + { + CopyrightNotice = 0, + FamilyName = 1, + SubfamilyName = 2, + UniqueId = 3, + FullFontName = 4, + VersionString = 5, + PostScriptName = 6, + Trademark = 7, + Manufacturer = 8, + Designer = 9, + Description = 10, + UrlVendor = 11, + UrlDesigner = 12, + LicenseDescription = 13, + LicenseInfoUrl = 14, + TypographicFamilyName = 16, + TypographicSubfamilyName = 17, + CompatibleFullMac = 18, + SampleText = 19, + PoscSriptCidFindFontName = 20, + WwsFamilyName = 21, + WwsSubfamilyName = 22, + LightBackgroundPalette = 23, + DarkBackgroundPalette = 24, + VariationPostScriptNamePrefix = 25, + } + + private enum PlatformId : ushort + { + Unicode = 0, + Macintosh = 1, // discouraged + Iso = 2, // deprecated + Windows = 3, + Custom = 4, // OTF Windows NT compatibility mapping + } + + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum UnicodeEncodingId : ushort + { + Unicode_1_0 = 0, // deprecated + Unicode_1_1 = 1, // deprecated + IsoIec_10646 = 2, // deprecated + Unicode_2_0_Bmp = 3, + Unicode_2_0_Full = 4, + UnicodeVariationSequences = 5, + UnicodeFullRepertoire = 6, + } + + private enum WindowsEncodingId : ushort + { + Symbol = 0, + UnicodeBmp = 1, + ShiftJis = 2, + Prc = 3, + Big5 = 4, + Wansung = 5, + Johab = 6, + UnicodeFullRepertoire = 10, + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs new file mode 100644 index 000000000..3d89dd806 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs @@ -0,0 +1,148 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +[SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1310:Field names should not contain underscore", + Justification = "Version name")] +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name")] +internal static partial class TrueTypeUtils +{ + private readonly struct SfntFile : IReadOnlyDictionary> + { + // http://formats.kaitai.io/ttf/ttf.svg + + public static readonly TagStruct FileTagTrueType1 = new('1', '\0', '\0', '\0'); + public static readonly TagStruct FileTagType1 = new('t', 'y', 'p', '1'); + public static readonly TagStruct FileTagOpenTypeWithCff = new('O', 'T', 'T', 'O'); + public static readonly TagStruct FileTagOpenType1_0 = new('\0', '\x01', '\0', '\0'); + public static readonly TagStruct FileTagTrueTypeApple = new('t', 'r', 'u', 'e'); + + public readonly PointerSpan Memory; + public readonly int OffsetInCollection; + public readonly ushort TableCount; + + public SfntFile(PointerSpan memory, int offsetInCollection = 0) + { + var span = memory.Span; + this.Memory = memory; + this.OffsetInCollection = offsetInCollection; + this.TableCount = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + } + + public int Count => this.TableCount; + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable> Values => this.Select(x => x.Value); + + public PointerSpan this[TagStruct key] => this.First(x => x.Key == key).Value; + + public IEnumerator>> GetEnumerator() + { + var offset = 12; + for (var i = 0; i < this.TableCount; i++) + { + var dte = new DirectoryTableEntry(this.Memory[offset..]); + yield return new(dte.Tag, this.Memory.Slice(dte.Offset - this.OffsetInCollection, dte.Length)); + + offset += Unsafe.SizeOf(); + } + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(TagStruct key) => this.Any(x => x.Key == key); + + public bool TryGetValue(TagStruct key, out PointerSpan value) + { + foreach (var (k, v) in this) + { + if (k == key) + { + value = v; + return true; + } + } + + value = default; + return false; + } + + public readonly struct DirectoryTableEntry + { + public readonly PointerSpan Memory; + + public DirectoryTableEntry(PointerSpan span) => this.Memory = span; + + public TagStruct Tag => new(this.Memory); + + public uint Checksum => this.Memory.ReadU32Big(4); + + public int Offset => this.Memory.ReadI32Big(8); + + public int Length => this.Memory.ReadI32Big(12); + } + } + + private readonly struct TtcFile : IReadOnlyList + { + public static readonly TagStruct FileTag = new('t', 't', 'c', 'f'); + + public readonly PointerSpan Memory; + public readonly TagStruct Tag; + public readonly ushort MajorVersion; + public readonly ushort MinorVersion; + public readonly int FontCount; + + public TtcFile(PointerSpan memory) + { + var span = memory.Span; + this.Memory = memory; + this.Tag = new(span); + if (this.Tag != FileTag) + throw new InvalidOperationException(); + + this.MajorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + this.MinorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[6..]); + this.FontCount = BinaryPrimitives.ReadInt32BigEndian(span[8..]); + } + + public int Count => this.FontCount; + + public SfntFile this[int index] + { + get + { + if (index < 0 || index >= this.FontCount) + { + throw new IndexOutOfRangeException( + $"The requested font #{index} does not exist in this .ttc file."); + } + + var offset = BinaryPrimitives.ReadInt32BigEndian(this.Memory.Span[(12 + 4 * index)..]); + return new(this.Memory[offset..], offset); + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.FontCount; i++) + yield return this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs new file mode 100644 index 000000000..d200de47b --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs @@ -0,0 +1,259 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [Flags] + private enum LookupFlags : byte + { + RightToLeft = 1 << 0, + IgnoreBaseGlyphs = 1 << 1, + IgnoreLigatures = 1 << 2, + IgnoreMarks = 1 << 3, + UseMarkFilteringSet = 1 << 4, + } + + private enum LookupType : ushort + { + SingleAdjustment = 1, + PairAdjustment = 2, + CursiveAttachment = 3, + MarkToBaseAttachment = 4, + MarkToLigatureAttachment = 5, + MarkToMarkAttachment = 6, + ContextPositioning = 7, + ChainedContextPositioning = 8, + ExtensionPositioning = 9, + } + + private readonly struct ClassDefTable + { + public readonly PointerSpan Memory; + + public ClassDefTable(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public Format1ClassArray Format1 => new(this.Memory); + + public Format2ClassRanges Format2 => new(this.Memory); + + public IEnumerable<(ushort Class, ushort GlyphId)> Enumerate() + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + var count = format1.GlyphCount; + var classes = format1.ClassValueArray; + for (var i = 0; i < count; i++) + yield return (classes[i], (ushort)(i + startId)); + + break; + } + + case 2: + { + foreach (var range in this.Format2.ClassValueArray) + { + var @class = range.Class; + var startId = range.StartGlyphId; + var count = range.EndGlyphId - startId + 1; + for (var i = 0; i < count; i++) + yield return (@class, (ushort)(startId + i)); + } + + break; + } + } + } + + [Pure] + public ushort GetClass(ushort glyphId) + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + if (startId <= glyphId && glyphId < startId + format1.GlyphCount) + return this.Format1.ClassValueArray[glyphId - startId]; + + break; + } + + case 2: + { + var rangeSpan = this.Format2.ClassValueArray; + var i = rangeSpan.BinarySearch(new Format2ClassRanges.ClassRangeRecord { EndGlyphId = glyphId }); + if (i >= 0 && rangeSpan[i].ContainsGlyph(glyphId)) + return rangeSpan[i].Class; + + break; + } + } + + return 0; + } + + public readonly struct Format1ClassArray + { + public readonly PointerSpan Memory; + + public Format1ClassArray(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort StartGlyphId => this.Memory.ReadU16Big(2); + + public ushort GlyphCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[6..].As(this.GlyphCount), + BinaryPrimitives.ReverseEndianness); + } + + public readonly struct Format2ClassRanges + { + public readonly PointerSpan Memory; + + public Format2ClassRanges(PointerSpan memory) => this.Memory = memory; + + public ushort ClassRangeCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[4..].As(this.ClassRangeCount), + ClassRangeRecord.ReverseEndianness); + + public struct ClassRangeRecord : IComparable + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort Class; + + public static ClassRangeRecord ReverseEndianness(ClassRangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + Class = BinaryPrimitives.ReverseEndianness(value.Class), + }; + + public int CompareTo(ClassRangeRecord other) => this.EndGlyphId.CompareTo(other.EndGlyphId); + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + } + + private readonly struct CoverageTable + { + public readonly PointerSpan Memory; + + public CoverageTable(PointerSpan memory) => this.Memory = memory; + + public enum CoverageFormat : ushort + { + Glyphs = 1, + RangeRecords = 2, + } + + public CoverageFormat Format => this.Memory.ReadEnumBig(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Glyphs => + this.Format == CoverageFormat.Glyphs + ? new(this.Memory[4..].As(this.Count), BinaryPrimitives.ReverseEndianness) + : default(BigEndianPointerSpan); + + public BigEndianPointerSpan RangeRecords => + this.Format == CoverageFormat.RangeRecords + ? new(this.Memory[4..].As(this.Count), RangeRecord.ReverseEndianness) + : default(BigEndianPointerSpan); + + public int GetCoverageIndex(ushort glyphId) + { + switch (this.Format) + { + case CoverageFormat.Glyphs: + return this.Glyphs.BinarySearch(glyphId); + + case CoverageFormat.RangeRecords: + { + var index = this.RangeRecords.BinarySearch( + (in RangeRecord record) => glyphId.CompareTo(record.EndGlyphId)); + + if (index >= 0 && this.RangeRecords[index].ContainsGlyph(glyphId)) + return index; + + return -1; + } + + default: + return -1; + } + } + + public struct RangeRecord + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort StartCoverageIndex; + + public static RangeRecord ReverseEndianness(RangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + StartCoverageIndex = BinaryPrimitives.ReverseEndianness(value.StartCoverageIndex), + }; + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + + private readonly struct LookupTable : IEnumerable> + { + public readonly PointerSpan Memory; + + public LookupTable(PointerSpan memory) => this.Memory = memory; + + public LookupType Type => this.Memory.ReadEnumBig(0); + + public byte MarkAttachmentType => this.Memory[2]; + + public LookupFlags Flags => (LookupFlags)this.Memory[3]; + + public ushort SubtableCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubtableOffsets => new( + this.Memory[6..].As(this.SubtableCount), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan this[int index] => this.Memory[this.SubtableOffsets[this.EnsureIndex(index)] ..]; + + public IEnumerator> GetEnumerator() + { + foreach (var i in Enumerable.Range(0, this.SubtableCount)) + yield return this.Memory[this.SubtableOffsets[i] ..]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => index >= 0 && index < this.SubtableCount + ? index + : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs new file mode 100644 index 000000000..c91df4ff2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs @@ -0,0 +1,443 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private delegate int BinarySearchComparer(in T value); + + private static IDisposable CreatePointerSpan(this T[] data, out PointerSpan pointerSpan) + where T : unmanaged + { + var gchandle = GCHandle.Alloc(data, GCHandleType.Pinned); + pointerSpan = new(gchandle.AddrOfPinnedObject(), data.Length); + return Disposable.Create(() => gchandle.Free()); + } + + private static int BinarySearch(this IReadOnlyList span, in T value) + where T : unmanaged, IComparable + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = value.CompareTo(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static int BinarySearch(this IReadOnlyList span, BinarySearchComparer comparer) + where T : unmanaged + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = comparer(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static short ReadI16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static int ReadI32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static long ReadI64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static ushort ReadU16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static uint ReadU32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static ulong ReadU64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static Half ReadF16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static float ReadF32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static double ReadF64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out short value) => + value = BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out int value) => + value = BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out long value) => + value = BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ushort value) => + value = BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out uint value) => + value = BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ulong value) => + value = BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out Half value) => + value = BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out float value) => + value = BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out double value) => + value = BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, ref int offset, out short value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out int value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out long value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ushort value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out uint value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ulong value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out Half value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out float value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out double value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static unsafe T ReadEnumBig(this PointerSpan ps, int offset) where T : unmanaged, Enum + { + switch (Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T)))) + { + case 1: + var b1 = ps.Span[offset]; + return *(T*)&b1; + case 2: + var b2 = ps.ReadU16Big(offset); + return *(T*)&b2; + case 4: + var b4 = ps.ReadU32Big(offset); + return *(T*)&b4; + case 8: + var b8 = ps.ReadU64Big(offset); + return *(T*)&b8; + default: + throw new ArgumentException("Enum is not of size 1, 2, 4, or 8.", nameof(T), null); + } + } + + private static void ReadBig(this PointerSpan ps, int offset, out T value) where T : unmanaged, Enum => + value = ps.ReadEnumBig(offset); + + private static void ReadBig(this PointerSpan ps, ref int offset, out T value) where T : unmanaged, Enum + { + value = ps.ReadEnumBig(offset); + offset += Unsafe.SizeOf(); + } + + private readonly unsafe struct PointerSpan : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + public PointerSpan(T* pointer, int count) + { + this.Pointer = pointer; + this.Count = count; + } + + public PointerSpan(nint pointer, int count) + : this((T*)pointer, count) + { + } + + public Span Span => new(this.Pointer, this.Count); + + public bool IsEmpty => this.Count == 0; + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => this; + + bool ICollection.IsReadOnly => false; + + public ref T this[int index] => ref this.Pointer[this.EnsureIndex(index)]; + + public PointerSpan this[Range range] => this.Slice(range.GetOffsetAndLength(this.Count)); + + T IList.this[int index] + { + get => this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = value; + } + + T IReadOnlyList.this[int index] => this.Pointer[this.EnsureIndex(index)]; + + public bool ContainsPointer(T2* obj) where T2 : unmanaged => + (T*)obj >= this.Pointer && (T*)(obj + 1) <= this.Pointer + this.Count; + + public PointerSpan Slice(int offset, int count) => new(this.Pointer + offset, count); + + public PointerSpan Slice((int Offset, int Count) offsetAndCount) + => this.Slice(offsetAndCount.Offset, offsetAndCount.Count); + + public PointerSpan As(int count) + where T2 : unmanaged => + count > this.Count / sizeof(T2) + ? throw new ArgumentOutOfRangeException( + nameof(count), + count, + $"Wanted {count} items; had {this.Count / sizeof(T2)} items") + : new((T2*)this.Pointer, count); + + public PointerSpan As() + where T2 : unmanaged => + new((T2*)this.Pointer, this.Count / sizeof(T2)); + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return true; + } + + return false; + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this.Pointer[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this.Pointer[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } + + private readonly unsafe struct BigEndianPointerSpan + : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + private readonly Func reverseEndianness; + + public BigEndianPointerSpan(PointerSpan pointerSpan, Func reverseEndianness) + { + this.reverseEndianness = reverseEndianness; + this.Pointer = pointerSpan.Pointer; + this.Count = pointerSpan.Count; + } + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + public bool IsSynchronized => true; + + public object SyncRoot => this; + + public bool IsReadOnly => true; + + public T this[int index] + { + get => + BitConverter.IsLittleEndian + ? this.reverseEndianness(this.Pointer[this.EnsureIndex(index)]) + : this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = + BitConverter.IsLittleEndian + ? this.reverseEndianness(value) + : value; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) => throw new NotSupportedException(); + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs new file mode 100644 index 000000000..80cf4b7da --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs @@ -0,0 +1,1391 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +internal static partial class TrueTypeUtils +{ + [Flags] + private enum ValueFormat : ushort + { + PlacementX = 1 << 0, + PlacementY = 1 << 1, + AdvanceX = 1 << 2, + AdvanceY = 1 << 3, + PlacementDeviceOffsetX = 1 << 4, + PlacementDeviceOffsetY = 1 << 5, + AdvanceDeviceOffsetX = 1 << 6, + AdvanceDeviceOffsetY = 1 << 7, + + ValidBits = 0 + | PlacementX | PlacementY + | AdvanceX | AdvanceY + | PlacementDeviceOffsetX | PlacementDeviceOffsetY + | AdvanceDeviceOffsetX | AdvanceDeviceOffsetY, + } + + private static int NumBytes(this ValueFormat value) => + ushort.PopCount((ushort)(value & ValueFormat.ValidBits)) * 2; + + private readonly struct Cmap + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/cmap + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html + + public static readonly TagStruct DirectoryTableTag = new('c', 'm', 'a', 'p'); + + public readonly PointerSpan Memory; + + public Cmap(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Cmap(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort RecordCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Records => new( + this.Memory[4..].As(this.RecordCount), + EncodingRecord.ReverseEndianness); + + public EncodingRecord? UnicodeEncodingRecord => + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Bmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Full }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.UnicodeFullRepertoire }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeBmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeFullRepertoire }); + + public CmapFormat? UnicodeTable => this.GetTable(this.UnicodeEncodingRecord); + + public CmapFormat? GetTable(EncodingRecord? encodingRecord) => + encodingRecord is { } record + ? this.Memory.ReadU16Big(record.SubtableOffset) switch + { + 0 => new CmapFormat0(this.Memory[record.SubtableOffset..]), + 2 => new CmapFormat2(this.Memory[record.SubtableOffset..]), + 4 => new CmapFormat4(this.Memory[record.SubtableOffset..]), + 6 => new CmapFormat6(this.Memory[record.SubtableOffset..]), + 8 => new CmapFormat8(this.Memory[record.SubtableOffset..]), + 10 => new CmapFormat10(this.Memory[record.SubtableOffset..]), + 12 or 13 => new CmapFormat12And13(this.Memory[record.SubtableOffset..]), + _ => null, + } + : null; + + public struct EncodingRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public int SubtableOffset; + + public EncodingRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.SubtableOffset); + } + + public static EncodingRecord ReverseEndianness(EncodingRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + SubtableOffset = BinaryPrimitives.ReverseEndianness(value.SubtableOffset), + }; + } + + public struct MapGroup : IComparable + { + public int StartCharCode; + public int EndCharCode; + public int GlyphId; + + public MapGroup(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.StartCharCode); + span.ReadBig(ref offset, out this.EndCharCode); + span.ReadBig(ref offset, out this.GlyphId); + } + + public static MapGroup ReverseEndianness(MapGroup obj) => new() + { + StartCharCode = BinaryPrimitives.ReverseEndianness(obj.StartCharCode), + EndCharCode = BinaryPrimitives.ReverseEndianness(obj.EndCharCode), + GlyphId = BinaryPrimitives.ReverseEndianness(obj.GlyphId), + }; + + public int CompareTo(MapGroup other) + { + var endCharCodeComparison = this.EndCharCode.CompareTo(other.EndCharCode); + if (endCharCodeComparison != 0) return endCharCodeComparison; + + var startCharCodeComparison = this.StartCharCode.CompareTo(other.StartCharCode); + if (startCharCodeComparison != 0) return startCharCodeComparison; + + return this.GlyphId.CompareTo(other.GlyphId); + } + } + + public abstract class CmapFormat : IReadOnlyDictionary + { + public int Count => this.Count(x => x.Value != 0); + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable Values => this.Select(x => x.Value); + + public ushort this[int key] => throw new NotImplementedException(); + + public abstract ushort CharToGlyph(int c); + + public abstract IEnumerator> GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(int key) => this.CharToGlyph(key) != 0; + + public bool TryGetValue(int key, out ushort value) + { + value = this.CharToGlyph(key); + return value != 0; + } + } + + public class CmapFormat0 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat0(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public PointerSpan GlyphIdArray => this.Memory.Slice(6, 256); + + public override ushort CharToGlyph(int c) => c is >= 0 and < 256 ? this.GlyphIdArray[c] : (byte)0; + + public override IEnumerator> GetEnumerator() + { + for (var codepoint = 0; codepoint < 256; codepoint++) + { + if (this.GlyphIdArray[codepoint] is var glyphId and not 0) + yield return new(codepoint, glyphId); + } + } + } + + public class CmapFormat2 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat2(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubHeaderKeys => new( + this.Memory[6..].As(256), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan Data => this.Memory[518..]; + + public bool TryGetSubHeader( + int keyIndex, out SubHeader subheader, out BigEndianPointerSpan glyphSpan) + { + if (keyIndex < 0 || keyIndex >= this.SubHeaderKeys.Count) + { + subheader = default; + glyphSpan = default; + return false; + } + + var offset = this.SubHeaderKeys[keyIndex]; + if (offset + Unsafe.SizeOf() > this.Data.Length) + { + subheader = default; + glyphSpan = default; + return false; + } + + subheader = new(this.Data[offset..]); + glyphSpan = new( + this.Data[(offset + Unsafe.SizeOf() + subheader.IdRangeOffset)..] + .As(subheader.EntryCount), + BinaryPrimitives.ReverseEndianness); + + return true; + } + + public override ushort CharToGlyph(int c) + { + if (!this.TryGetSubHeader(c >> 8, out var sh, out var glyphSpan)) + return 0; + + c = (c & 0xFF) - sh.FirstCode; + if (c > 0 || c >= glyphSpan.Count) + return 0; + + var res = glyphSpan[c]; + return res == 0 ? (ushort)0 : unchecked((ushort)(res + sh.IdDelta)); + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.SubHeaderKeys.Count; i++) + { + if (!this.TryGetSubHeader(i, out var sh, out var glyphSpan)) + continue; + + for (var j = 0; j < glyphSpan.Count; j++) + { + var res = glyphSpan[j]; + if (res == 0) + continue; + + var glyphId = unchecked((ushort)(res + sh.IdDelta)); + if (glyphId == 0) + continue; + + var codepoint = (i << 8) | (sh.FirstCode + j); + yield return new(codepoint, glyphId); + } + } + } + + public struct SubHeader + { + public ushort FirstCode; + public ushort EntryCount; + public ushort IdDelta; + public ushort IdRangeOffset; + + public SubHeader(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.FirstCode); + span.ReadBig(ref offset, out this.EntryCount); + span.ReadBig(ref offset, out this.IdDelta); + span.ReadBig(ref offset, out this.IdRangeOffset); + } + } + } + + public class CmapFormat4 : CmapFormat + { + public const int EndCodesOffset = 14; + + public readonly PointerSpan Memory; + + public CmapFormat4(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort SegCountX2 => this.Memory.ReadU16Big(6); + + public ushort SearchRange => this.Memory.ReadU16Big(8); + + public ushort EntrySelector => this.Memory.ReadU16Big(10); + + public ushort RangeShift => this.Memory.ReadU16Big(12); + + public BigEndianPointerSpan EndCodes => new( + this.Memory.Slice(EndCodesOffset, this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan StartCodes => new( + this.Memory.Slice(EndCodesOffset + 2 + (1 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdDeltas => new( + this.Memory.Slice(EndCodesOffset + 2 + (2 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdRangeOffsets => new( + this.Memory.Slice(EndCodesOffset + 2 + (3 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory.Slice(EndCodesOffset + 2 + (4 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c is < 0 or >= 0x10000) + return 0; + + var i = this.EndCodes.BinarySearch((ushort)c); + if (i < 0) + return 0; + + var startCode = this.StartCodes[i]; + var endCode = this.EndCodes[i]; + if (c < startCode || c > endCode) + return 0; + + var idRangeOffset = this.IdRangeOffsets[i]; + var idDelta = this.IdDeltas[i]; + if (idRangeOffset == 0) + return unchecked((ushort)(c + idDelta)); + + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr > this.Memory.Length) + return 0; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + var glyph = glyphs[c - startCode]; + return unchecked(glyph == 0 ? (ushort)0 : (ushort)(idDelta + glyph)); + } + + public override IEnumerator> GetEnumerator() + { + var startCodes = this.StartCodes; + var endCodes = this.EndCodes; + var idDeltas = this.IdDeltas; + var idRangeOffsets = this.IdRangeOffsets; + + for (var i = 0; i < this.SegCountX2 / 2; i++) + { + var startCode = startCodes[i]; + var endCode = endCodes[i]; + var idRangeOffset = idRangeOffsets[i]; + var idDelta = idDeltas[i]; + + if (idRangeOffset == 0) + { + for (var c = (int)startCode; c <= endCode; c++) + yield return new(c, (ushort)(c + idDelta)); + } + else + { + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr >= this.Memory.Length) + continue; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + for (var j = 0; j < glyphs.Count; j++) + { + var glyphId = glyphs[j]; + if (glyphId == 0) + continue; + + glyphId += idDelta; + if (glyphId == 0) + continue; + + yield return new(startCode + j, glyphId); + } + } + } + } + } + + public class CmapFormat6 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat6(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort FirstCode => this.Memory.ReadU16Big(6); + + public ushort EntryCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory[10..].As(this.EntryCount), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var glyphIds = this.GlyphIds; + if (c < this.FirstCode || c >= this.FirstCode + this.GlyphIds.Count) + return 0; + + return glyphIds[c - this.FirstCode]; + } + + public override IEnumerator> GetEnumerator() + { + var glyphIds = this.GlyphIds; + for (var i = 0; i < this.GlyphIds.Length; i++) + { + var g = glyphIds[i]; + if (g != 0) + yield return new(this.FirstCode + i, g); + } + } + } + + public class CmapFormat8 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat8(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public PointerSpan Is32 => this.Memory.Slice(12, 8192); + + public int NumGroups => this.Memory.ReadI32Big(8204); + + public BigEndianPointerSpan Groups => + new(this.Memory[8208..].As(), MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch((in MapGroup value) => c.CompareTo(value.EndCharCode)); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + return unchecked((ushort)(group.GlyphId + c - group.StartCharCode)); + } + + public override IEnumerator> GetEnumerator() + { + foreach (var group in this.Groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + } + + public class CmapFormat10 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat10(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int StartCharCode => this.Memory.ReadI32Big(12); + + public int NumChars => this.Memory.ReadI32Big(16); + + public BigEndianPointerSpan GlyphIdArray => new( + this.Memory.Slice(20, this.NumChars * 2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c < this.StartCharCode || c >= this.StartCharCode + this.GlyphIdArray.Count) + return 0; + + return this.GlyphIdArray[c]; + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.GlyphIdArray.Count; i++) + { + var glyph = this.GlyphIdArray[i]; + if (glyph != 0) + yield return new(this.StartCharCode + i, glyph); + } + } + } + + public class CmapFormat12And13 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat12And13(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int NumGroups => this.Memory.ReadI32Big(12); + + public BigEndianPointerSpan Groups => new( + this.Memory[16..].As(this.NumGroups), + MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch(new MapGroup() { EndCharCode = c }); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + if (this.Format == 12) + return (ushort)(group.GlyphId + c - group.StartCharCode); + else + return (ushort)group.GlyphId; + } + + public override IEnumerator> GetEnumerator() + { + var groups = this.Groups; + if (this.Format == 12) + { + foreach (var group in groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + else + { + foreach (var group in groups) + { + if (group.GlyphId == 0) + continue; + + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + yield return new(j, (ushort)group.GlyphId); + } + } + } + } + } + + private readonly struct Gpos + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos + + public static readonly TagStruct DirectoryTableTag = new('G', 'P', 'O', 'S'); + + public readonly PointerSpan Memory; + + public Gpos(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Gpos(PointerSpan memory) => this.Memory = memory; + + public Fixed Version => new(this.Memory); + + public ushort ScriptListOffset => this.Memory.ReadU16Big(4); + + public ushort FeatureListOffset => this.Memory.ReadU16Big(6); + + public ushort LookupListOffset => this.Memory.ReadU16Big(8); + + public uint FeatureVariationsOffset => this.Version.CompareTo(new(1, 1)) >= 0 + ? this.Memory.ReadU32Big(10) + : 0; + + public BigEndianPointerSpan LookupOffsetList => new( + this.Memory[(this.LookupListOffset + 2)..].As( + this.Memory.ReadU16Big(this.LookupListOffset)), + BinaryPrimitives.ReverseEndianness); + + public IEnumerable EnumerateLookupTables() + { + foreach (var offset in this.LookupOffsetList) + yield return new(this.Memory[(this.LookupListOffset + offset)..]); + } + + public IEnumerable ExtractAdvanceX() => + this.EnumerateLookupTables() + .SelectMany( + lookupTable => lookupTable.Type switch + { + LookupType.PairAdjustment => + lookupTable.SelectMany(y => new PairAdjustmentPositioning(y).ExtractAdvanceX()), + LookupType.ExtensionPositioning => + lookupTable + .Where(y => y.ReadU16Big(0) == 1) + .Select(y => new ExtensionPositioningSubtableFormat1(y)) + .Where(y => y.ExtensionLookupType == LookupType.PairAdjustment) + .SelectMany(y => new PairAdjustmentPositioning(y.ExtensionData).ExtractAdvanceX()), + _ => Array.Empty(), + }); + + public struct ValueRecord + { + public short PlacementX; + public short PlacementY; + public short AdvanceX; + public short AdvanceY; + public short PlacementDeviceOffsetX; + public short PlacementDeviceOffsetY; + public short AdvanceDeviceOffsetX; + public short AdvanceDeviceOffsetY; + + public ValueRecord(PointerSpan pointerSpan, ValueFormat valueFormat) + { + var offset = 0; + if ((valueFormat & ValueFormat.PlacementX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementX); + + if ((valueFormat & ValueFormat.PlacementY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementY); + + if ((valueFormat & ValueFormat.AdvanceX) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceX); + if ((valueFormat & ValueFormat.AdvanceY) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceY); + if ((valueFormat & ValueFormat.PlacementDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetX); + + if ((valueFormat & ValueFormat.PlacementDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetY); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetX); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetY); + } + } + + public readonly struct PairAdjustmentPositioning + { + public readonly PointerSpan Memory; + + public PairAdjustmentPositioning(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public IEnumerable ExtractAdvanceX() => this.Format switch + { + 1 => new Format1(this.Memory).ExtractAdvanceX(), + 2 => new Format2(this.Memory).ExtractAdvanceX(), + _ => Array.Empty(), + }; + + public readonly struct Format1 + { + public readonly PointerSpan Memory; + + public Format1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort PairSetCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan PairSetOffsets => new( + this.Memory[10..].As(this.PairSetCount), + BinaryPrimitives.ReverseEndianness); + + public CoverageTable CoverageTable => new(this.Memory[this.CoverageOffset..]); + + public PairSet this[int index] => new( + this.Memory[this.PairSetOffsets[index] ..], + this.ValueFormat1, + this.ValueFormat2); + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var coverageTable = this.CoverageTable; + switch (coverageTable.Format) + { + case CoverageTable.CoverageFormat.Glyphs: + { + var glyphSpan = coverageTable.Glyphs; + foreach (var coverageIndex in Enumerable.Range(0, glyphSpan.Count)) + { + var glyph1Id = glyphSpan[coverageIndex]; + PairSet pairSetView; + try + { + pairSetView = this[coverageIndex]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj >= 10000) + System.Diagnostics.Debugger.Break(); + + if (adj != 0) + yield return new(glyph1Id, pair.SecondGlyph, adj); + } + } + + break; + } + + case CoverageTable.CoverageFormat.RangeRecords: + { + foreach (var rangeRecord in coverageTable.RangeRecords) + { + var startGlyphId = rangeRecord.StartGlyphId; + var endGlyphId = rangeRecord.EndGlyphId; + var startCoverageIndex = rangeRecord.StartCoverageIndex; + var glyphCount = endGlyphId - startGlyphId + 1; + foreach (var glyph1Id in Enumerable.Range(startGlyphId, glyphCount)) + { + PairSet pairSetView; + try + { + pairSetView = this[startCoverageIndex + glyph1Id - startGlyphId]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj != 0) + yield return new((ushort)glyph1Id, pair.SecondGlyph, adj); + } + } + } + + break; + } + } + } + + public readonly struct PairSet + { + public readonly PointerSpan Memory; + public readonly ValueFormat ValueFormat1; + public readonly ValueFormat ValueFormat2; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public PairSet( + PointerSpan memory, + ValueFormat valueFormat1, + ValueFormat valueFormat2) + { + this.Memory = memory; + this.ValueFormat1 = valueFormat1; + this.ValueFormat2 = valueFormat2; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = 2 + this.PairValue1Size + this.PairValue2Size; + } + + public ushort Count => this.Memory.ReadU16Big(0); + + public PairValueRecord this[int index] + { + get + { + var pvr = this.Memory.Slice(2 + (this.PairSize * index), this.PairSize); + return new() + { + SecondGlyph = pvr.ReadU16Big(0), + Record1 = new(pvr.Slice(2, this.PairValue1Size), this.ValueFormat1), + Record2 = new( + pvr.Slice(2 + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2), + }; + } + } + + public struct PairValueRecord + { + public ushort SecondGlyph; + public ValueRecord Record1; + public ValueRecord Record2; + } + } + } + + public readonly struct Format2 + { + public readonly PointerSpan Memory; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public Format2(PointerSpan memory) + { + this.Memory = memory; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = this.PairValue1Size + this.PairValue2Size; + } + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort ClassDef1Offset => this.Memory.ReadU16Big(8); + + public ushort ClassDef2Offset => this.Memory.ReadU16Big(10); + + public ushort Class1Count => this.Memory.ReadU16Big(12); + + public ushort Class2Count => this.Memory.ReadU16Big(14); + + public ClassDefTable ClassDefTable1 => new(this.Memory[this.ClassDef1Offset..]); + + public ClassDefTable ClassDefTable2 => new(this.Memory[this.ClassDef2Offset..]); + + public (ValueRecord Record1, ValueRecord Record2) this[(int Class1Index, int Class2Index) v] => + this[v.Class1Index, v.Class2Index]; + + public (ValueRecord Record1, ValueRecord Record2) this[int class1Index, int class2Index] + { + get + { + if (class1Index < 0 || class1Index >= this.Class1Count) + throw new IndexOutOfRangeException(); + + if (class2Index < 0 || class2Index >= this.Class2Count) + throw new IndexOutOfRangeException(); + + var offset = 16 + (this.PairSize * ((class1Index * this.Class2Count) + class2Index)); + return ( + new(this.Memory.Slice(offset, this.PairValue1Size), this.ValueFormat1), + new( + this.Memory.Slice(offset + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2)); + } + } + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var classes1 = this.ClassDefTable1.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + var classes2 = this.ClassDefTable2.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + foreach (var class1 in Enumerable.Range(0, this.Class1Count)) + { + if (!classes1.TryGetValue((ushort)class1, out var glyphs1)) + continue; + + foreach (var class2 in Enumerable.Range(0, this.Class2Count)) + { + if (!classes2.TryGetValue((ushort)class2, out var glyphs2)) + continue; + + (ValueRecord, ValueRecord) record; + try + { + record = this[class1, class2]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + var val = record.Item1.AdvanceX + record.Item2.PlacementX; + if (val == 0) + continue; + + foreach (var glyph1 in glyphs1) + { + foreach (var glyph2 in glyphs2) + { + yield return new(glyph1, glyph2, (short)val); + } + } + } + } + } + } + } + + public readonly struct ExtensionPositioningSubtableFormat1 + { + public readonly PointerSpan Memory; + + public ExtensionPositioningSubtableFormat1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public LookupType ExtensionLookupType => this.Memory.ReadEnumBig(2); + + public int ExtensionOffset => this.Memory.ReadI32Big(4); + + public PointerSpan ExtensionData => this.Memory[this.ExtensionOffset..]; + } + } + + private readonly struct Head + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/head + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html + + public const uint MagicNumberValue = 0x5F0F3CF5; + public static readonly TagStruct DirectoryTableTag = new('h', 'e', 'a', 'd'); + + public readonly PointerSpan Memory; + + public Head(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Head(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum HeadFlags : ushort + { + BaselineForFontAtZeroY = 1 << 0, + LeftSideBearingAtZeroX = 1 << 1, + InstructionsDependOnPointSize = 1 << 2, + ForcePpemsInteger = 1 << 3, + InstructionsAlterAdvanceWidth = 1 << 4, + VerticalLayout = 1 << 5, + Reserved6 = 1 << 6, + RequiresLayoutForCorrectLinguisticRendering = 1 << 7, + IsAatFont = 1 << 8, + ContainsRtlGlyph = 1 << 9, + ContainsIndicStyleRearrangementEffects = 1 << 10, + Lossless = 1 << 11, + ProduceCompatibleMetrics = 1 << 12, + OptimizedForClearType = 1 << 13, + IsLastResortFont = 1 << 14, + Reserved15 = 1 << 15, + } + + [Flags] + public enum MacStyleFlags : ushort + { + Bold = 1 << 0, + Italic = 1 << 1, + Underline = 1 << 2, + Outline = 1 << 3, + Shadow = 1 << 4, + Condensed = 1 << 5, + Extended = 1 << 6, + } + + public Fixed Version => new(this.Memory); + + public Fixed FontRevision => new(this.Memory[4..]); + + public uint ChecksumAdjustment => this.Memory.ReadU32Big(8); + + public uint MagicNumber => this.Memory.ReadU32Big(12); + + public HeadFlags Flags => this.Memory.ReadEnumBig(16); + + public ushort UnitsPerEm => this.Memory.ReadU16Big(18); + + public ulong CreatedTimestamp => this.Memory.ReadU64Big(20); + + public ulong ModifiedTimestamp => this.Memory.ReadU64Big(28); + + public ushort MinX => this.Memory.ReadU16Big(36); + + public ushort MinY => this.Memory.ReadU16Big(38); + + public ushort MaxX => this.Memory.ReadU16Big(40); + + public ushort MaxY => this.Memory.ReadU16Big(42); + + public MacStyleFlags MacStyle => this.Memory.ReadEnumBig(44); + + public ushort LowestRecommendedPpem => this.Memory.ReadU16Big(46); + + public ushort FontDirectionHint => this.Memory.ReadU16Big(48); + + public ushort IndexToLocFormat => this.Memory.ReadU16Big(50); + + public ushort GlyphDataFormat => this.Memory.ReadU16Big(52); + } + + private readonly struct Kern + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/kern + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html + + public static readonly TagStruct DirectoryTableTag = new('k', 'e', 'r', 'n'); + + public readonly PointerSpan Memory; + + public Kern(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Kern(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public IEnumerable EnumerateHorizontalPairs() => this.Version switch + { + 0 => new Version0(this.Memory).EnumerateHorizontalPairs(), + 1 => new Version1(this.Memory).EnumerateHorizontalPairs(), + _ => Array.Empty(), + }; + + public readonly struct Format0 + { + public readonly PointerSpan Memory; + + public Format0(PointerSpan memory) => this.Memory = memory; + + public ushort PairCount => this.Memory.ReadU16Big(0); + + public ushort SearchRange => this.Memory.ReadU16Big(2); + + public ushort EntrySelector => this.Memory.ReadU16Big(4); + + public ushort RangeShift => this.Memory.ReadU16Big(6); + + public BigEndianPointerSpan Pairs => new( + this.Memory[8..].As(this.PairCount), + KerningPair.ReverseEndianness); + } + + public readonly struct Version0 + { + public readonly PointerSpan Memory; + + public Version0(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Horizontal = 1 << 0, + Minimum = 1 << 1, + CrossStream = 1 << 2, + Override = 1 << 3, + } + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort NumSubtables => this.Memory.ReadU16Big(2); + + public PointerSpan Data => this.Memory[4..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() + { + var accumulator = new Dictionary<(ushort Left, ushort Right), short>(); + foreach (var subtable in this.EnumerateSubtables()) + { + var isOverride = (subtable.Flags & CoverageFlags.Override) != 0; + var isMinimum = (subtable.Flags & CoverageFlags.Minimum) != 0; + foreach (var t in subtable.EnumeratePairs()) + { + if (isOverride) + { + accumulator[(t.Left, t.Right)] = t.Value; + } + else if (isMinimum) + { + accumulator[(t.Left, t.Right)] = Math.Max( + accumulator.GetValueOrDefault((t.Left, t.Right), t.Value), + t.Value); + } + else + { + accumulator[(t.Left, t.Right)] = (short)( + accumulator.GetValueOrDefault( + (t.Left, t.Right)) + t.Value); + } + } + } + + return accumulator.Select( + x => new KerningPair { Left = x.Key.Left, Right = x.Key.Right, Value = x.Value }); + } + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public PointerSpan Data => this.Memory[6..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + + public readonly struct Version1 + { + public readonly PointerSpan Memory; + + public Version1(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Vertical = 1 << 0, + CrossStream = 1 << 1, + Variation = 1 << 2, + } + + public Fixed Version => new(this.Memory); + + public int NumSubtables => this.Memory.ReadI16Big(4); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() => this + .EnumerateSubtables() + .Where(x => x.Flags == 0) + .SelectMany(x => x.EnumeratePairs()); + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public int Length => this.Memory.ReadI32Big(0); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public ushort TupleIndex => this.Memory.ReadU16Big(6); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + } + + private readonly struct Name + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/name + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html + + public static readonly TagStruct DirectoryTableTag = new('n', 'a', 'm', 'e'); + + public readonly PointerSpan Memory; + + public Name(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Name(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public ushort StorageOffset => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan NameRecords => new( + this.Memory[6..].As(this.Count), + NameRecord.ReverseEndianness); + + public ushort LanguageCount => + this.Version == 0 ? (ushort)0 : this.Memory.ReadU16Big(6 + this.NameRecords.ByteCount); + + public BigEndianPointerSpan LanguageRecords => this.Version == 0 + ? default + : new( + this.Memory[ + (8 + this.NameRecords + .ByteCount)..] + .As( + this.LanguageCount), + LanguageRecord.ReverseEndianness); + + public PointerSpan Storage => this.Memory[this.StorageOffset..]; + + public string this[in NameRecord record] => + record.PlatformAndEncoding.Decode(this.Storage.Span.Slice(record.StringOffset, record.Length)); + + public string this[in LanguageRecord record] => + Encoding.ASCII.GetString(this.Storage.Span.Slice(record.LanguageTagOffset, record.Length)); + + public struct NameRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public ushort LanguageId; + public NameId NameId; + public ushort Length; + public ushort StringOffset; + + public NameRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.LanguageId); + span.ReadBig(ref offset, out this.NameId); + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.StringOffset); + } + + public static NameRecord ReverseEndianness(NameRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + LanguageId = BinaryPrimitives.ReverseEndianness(value.LanguageId), + NameId = (NameId)BinaryPrimitives.ReverseEndianness((ushort)value.NameId), + Length = BinaryPrimitives.ReverseEndianness(value.Length), + StringOffset = BinaryPrimitives.ReverseEndianness(value.StringOffset), + }; + } + + public struct LanguageRecord + { + public ushort Length; + public ushort LanguageTagOffset; + + public LanguageRecord(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.LanguageTagOffset); + } + + public static LanguageRecord ReverseEndianness(LanguageRecord value) => new() + { + Length = BinaryPrimitives.ReverseEndianness(value.Length), + LanguageTagOffset = BinaryPrimitives.ReverseEndianness(value.LanguageTagOffset), + }; + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs new file mode 100644 index 000000000..1d437d56d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs @@ -0,0 +1,135 @@ +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + /// + /// Checks whether the given will fail in , + /// and throws an appropriate exception if it is the case. + /// + /// The font config. + public static unsafe void CheckImGuiCompatibleOrThrow(in ImFontConfig fontConfig) + { + var ranges = fontConfig.GlyphRanges; + var sfnt = AsSfntFile(fontConfig); + var cmap = new Cmap(sfnt); + if (cmap.UnicodeTable is not { } unicodeTable) + throw new NotSupportedException("The font does not have a compatible Unicode character mapping table."); + if (unicodeTable.All(x => !ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(x.Key, ranges))) + throw new NotSupportedException("The font does not have any glyph that falls under the requested range."); + } + + /// + /// Enumerates through horizontal pair adjustments of a kern and gpos tables. + /// + /// The font config. + /// The enumerable of pair adjustments. Distance values need to be multiplied by font size in pixels. + public static IEnumerable<(char Left, char Right, float Distance)> ExtractHorizontalPairAdjustments( + ImFontConfig fontConfig) + { + float multiplier; + Dictionary glyphToCodepoints; + Gpos gpos = default; + Kern kern = default; + + try + { + var sfnt = AsSfntFile(fontConfig); + var head = new Head(sfnt); + multiplier = 3f / 4 / head.UnitsPerEm; + + if (new Cmap(sfnt).UnicodeTable is not { } table) + yield break; + + if (sfnt.ContainsKey(Kern.DirectoryTableTag)) + kern = new(sfnt); + else if (sfnt.ContainsKey(Gpos.DirectoryTableTag)) + gpos = new(sfnt); + else + yield break; + + glyphToCodepoints = table + .GroupBy(x => x.Value, x => x.Key) + .OrderBy(x => x.Key) + .ToDictionary( + x => x.Key, + x => x.Where(y => y <= ushort.MaxValue) + .Select(y => (char)y) + .ToArray()); + } + catch + { + // don't care; give up + yield break; + } + + if (kern.Memory.Count != 0) + { + foreach (var pair in kern.EnumerateHorizontalPairs()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + else if (gpos.Memory.Count != 0) + { + foreach (var pair in gpos.ExtractAdvanceX()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + } + + private static unsafe SfntFile AsSfntFile(in ImFontConfig fontConfig) + { + var memory = new PointerSpan((byte*)fontConfig.FontData, fontConfig.FontDataSize); + if (memory.Length < 4) + throw new NotSupportedException("File is too short to even have a magic."); + + var magic = memory.ReadU32Big(0); + if (BitConverter.IsLittleEndian) + magic = BinaryPrimitives.ReverseEndianness(magic); + + if (magic == SfntFile.FileTagTrueType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenTypeWithCff.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenType1_0.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagTrueTypeApple.NativeValue) + return new(memory); + if (magic == TtcFile.FileTag.NativeValue) + return new TtcFile(memory)[fontConfig.FontNo]; + + throw new NotSupportedException($"The given file with the magic 0x{magic:X08} is not supported."); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs new file mode 100644 index 000000000..812608973 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -0,0 +1,291 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Managed version of , to avoid unnecessary heap allocation and use of unsafe blocks. +/// +public struct SafeFontConfig +{ + /// + /// The raw config. + /// + public ImFontConfig Raw; + + /// + /// Initializes a new instance of the struct. + /// + public SafeFontConfig() + { + this.OversampleH = 1; + this.OversampleV = 1; + this.PixelSnapH = true; + this.GlyphMaxAdvanceX = float.MaxValue; + this.RasterizerMultiply = 1f; + this.RasterizerGamma = 1.4f; + this.EllipsisChar = unchecked((char)-1); + this.Raw.FontDataOwnedByAtlas = 1; + } + + /// + /// Gets or sets the index of font within a TTF/OTF file. + /// + public int FontNo + { + get => this.Raw.FontNo; + set => this.Raw.FontNo = EnsureRange(value, 0, int.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in pixels.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePx + { + get => this.Raw.SizePixels; + set => this.Raw.SizePixels = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in points.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePt + { + get => (this.Raw.SizePixels * 3) / 4; + set => this.Raw.SizePixels = EnsureRange((value * 4) / 3, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the horizontal oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.
+ /// Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + ///
+ public int OversampleH + { + get => this.Raw.OversampleH; + set => this.Raw.OversampleH = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets the vertical oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// This is not really useful as we don't use sub-pixel positions on the Y axis. + ///
+ public int OversampleV + { + get => this.Raw.OversampleV; + set => this.Raw.OversampleV = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets a value indicating whether to align every glyph to pixel boundary.
+ /// Useful e.g. if you are merging a non-pixel aligned font with the default font.
+ /// If enabled, you can set and to 1. + ///
+ public bool PixelSnapH + { + get => this.Raw.PixelSnapH != 0; + set => this.Raw.PixelSnapH = value ? (byte)1 : (byte)0; + } + + /// + /// Gets or sets the extra spacing (in pixels) between glyphs.
+ /// Only X axis is supported for now.
+ /// Effectively, it is the letter spacing. + ///
+ public Vector2 GlyphExtraSpacing + { + get => this.Raw.GlyphExtraSpacing; + set => this.Raw.GlyphExtraSpacing = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the offset all glyphs from this font input.
+ /// Use this to offset fonts vertically when merging multiple fonts. + ///
+ public Vector2 GlyphOffset + { + get => this.Raw.GlyphOffset; + set => this.Raw.GlyphOffset = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the glyph ranges, which is a user-provided list of Unicode range. + /// Each range has 2 values, and values are inclusive.
+ /// The list must be zero-terminated.
+ /// If empty or null, then all the glyphs from the font that is in the range of UCS-2 will be added. + ///
+ public ushort[]? GlyphRanges { get; set; } + + /// + /// Gets or sets the minimum AdvanceX for glyphs.
+ /// Set only to align font icons.
+ /// Set both / to enforce mono-space font. + ///
+ public float GlyphMinAdvanceX + { + get => this.Raw.GlyphMinAdvanceX; + set => this.Raw.GlyphMinAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets the maximum AdvanceX for glyphs. + /// + public float GlyphMaxAdvanceX + { + get => this.Raw.GlyphMaxAdvanceX; + set => this.Raw.GlyphMaxAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets a value that either brightens (>1.0f) or darkens (<1.0f) the font output.
+ /// Brightening small fonts may be a good workaround to make them more readable. + ///
+ public float RasterizerMultiply + { + get => this.Raw.RasterizerMultiply; + set => this.Raw.RasterizerMultiply = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the gamma value for fonts. + /// + public float RasterizerGamma + { + get => this.Raw.RasterizerGamma; + set => this.Raw.RasterizerGamma = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets a value explicitly specifying unicode codepoint of the ellipsis character.
+ /// When fonts are being merged first specified ellipsis will be used. + ///
+ public char EllipsisChar + { + get => (char)this.Raw.EllipsisChar; + set => this.Raw.EllipsisChar = value; + } + + /// + /// Gets or sets the desired name of the new font. Names longer than 40 bytes will be partially lost. + /// + public unsafe string Name + { + get + { + fixed (void* pName = this.Raw.Name) + { + var span = new ReadOnlySpan(pName, 40); + var firstNull = span.IndexOf((byte)0); + if (firstNull != -1) + span = span[..firstNull]; + return Encoding.UTF8.GetString(span); + } + } + + set + { + fixed (void* pName = this.Raw.Name) + { + var span = new Span(pName, 40); + Encoding.UTF8.GetBytes(value, span); + } + } + } + + /// + /// Gets or sets the desired font to merge with, if set. + /// + public unsafe ImFontPtr MergeFont + { + get => this.Raw.DstFont is not null ? this.Raw.DstFont : default; + set + { + this.Raw.MergeMode = value.NativePtr is null ? (byte)0 : (byte)1; + this.Raw.DstFont = value.NativePtr is null ? default : value.NativePtr; + } + } + + /// + /// Throws with appropriate messages, + /// if this has invalid values. + /// + public readonly void ThrowOnInvalidValues() + { + if (!(this.Raw.FontNo >= 0)) + throw new ArgumentException($"{nameof(this.FontNo)} must not be a negative number."); + + if (!(this.Raw.SizePixels > 0)) + throw new ArgumentException($"{nameof(this.SizePx)} must be a positive number."); + + if (!(this.Raw.OversampleH >= 1)) + throw new ArgumentException($"{nameof(this.OversampleH)} must be a negative number."); + + if (!(this.Raw.OversampleV >= 1)) + throw new ArgumentException($"{nameof(this.OversampleV)} must be a negative number."); + + if (!float.IsFinite(this.Raw.GlyphMinAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + + if (!float.IsFinite(this.Raw.GlyphMaxAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + + if (!(this.Raw.RasterizerMultiply > 0)) + throw new ArgumentException($"{nameof(this.RasterizerMultiply)} must be a positive number."); + + if (!(this.Raw.RasterizerGamma > 0)) + throw new ArgumentException($"{nameof(this.RasterizerGamma)} must be a positive number."); + + if (this.GlyphRanges is { Length: > 0 } ranges) + { + if (ranges[0] == 0) + { + throw new ArgumentException( + "Font ranges cannot start with 0.", + nameof(this.GlyphRanges)); + } + + if (ranges[(ranges.Length - 1) & ~1] != 0) + { + throw new ArgumentException( + "Font ranges must terminate with a zero at even indices.", + nameof(this.GlyphRanges)); + } + } + } + + private static T EnsureRange(T value, T min, T max, [CallerMemberName] string callerName = "") + where T : INumber + { + if (value < min) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be less than {min}."); + if (value > max) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be more than {max}."); + + return value; + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index dd2e5bad3..f7beb22fa 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -12,6 +11,8 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -30,11 +31,14 @@ public sealed class UiBuilder : IDisposable private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); - private readonly GameFontManager gameFontManager = Service.Get(); + private readonly Framework framework = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -45,14 +49,32 @@ public sealed class UiBuilder : IDisposable /// The plugin namespace. internal UiBuilder(string namespaceName) { - this.stopwatch = new Stopwatch(); - this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); - this.namespaceName = namespaceName; + try + { + this.stopwatch = new Stopwatch(); + this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); + this.namespaceName = namespaceName; - this.interfaceManager.Draw += this.OnDraw; - this.interfaceManager.BuildFonts += this.OnBuildFonts; - this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; - this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.interfaceManager.Draw += this.OnDraw; + this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw); + + this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); + + this.privateAtlas = + this.scopedFinalizer + .Add( + Service + .Get() + .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); + this.privateAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; + this.privateAtlas.RebuildRecommend += this.RebuildFonts; + } + catch + { + this.scopedFinalizer.Dispose(); + throw; + } } /// @@ -80,19 +102,19 @@ public sealed class UiBuilder : IDisposable /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action BuildFonts; + [Obsolete($"Use {nameof(NewDelegateFontHandle)} instead.", false)] + public event Action? BuildFonts; /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action AfterBuildFonts; + [Obsolete($"Use {nameof(NewDelegateFontHandle)} instead.", false)] + public event Action? AfterBuildFonts; /// /// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown. @@ -107,18 +129,57 @@ public sealed class UiBuilder : IDisposable public event Action HideUi; /// - /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. + /// Gets the default Dalamud font size in points. /// + public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt; + + /// + /// Gets the default Dalamud font size in pixels. + /// + public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx; + + /// + /// Gets the default Dalamud font - supporting all game languages and icons.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); + /// + /// public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr IconFont => InterfaceManager.IconFont; /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudAssetFont( + /// DalamudAsset.InconsolataRegular, + /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr MonoFont => InterfaceManager.MonoFont; /// @@ -319,7 +380,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -341,7 +402,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -357,19 +418,74 @@ public sealed class UiBuilder : IDisposable /// /// Font to get. /// Handle to the game font which may or may not be available for use yet. - public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style); + [Obsolete($"Use {nameof(NewGameFontHandle)} instead.", false)] + public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( + (IFontHandle.IInternal)this.NewGameFontHandle(style), + Service.Get()); + + /// + public IFontHandle NewGameFontHandle(GameFontStyle style) => this.privateAtlas.NewGameFontHandle(style); + + /// + /// + /// On initialization: + /// + /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => { + /// var config = new SafeFontConfig { SizePx = 16 }; + /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); + /// tk.AddGameSymbol(config); + /// tk.AddExtraGlyphsForDalamudLanguage(config); + /// // optional: tk.Font = config.MergeFont; + /// })); + /// + /// + /// On use: + /// + /// using (this.fontHandle.Push()) + /// ImGui.TextUnformatted("Example"); + /// + /// + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => + this.privateAtlas.NewDelegateFontHandle(buildStepDelegate); /// /// Call this to queue a rebuild of the font atlas.
- /// This will invoke any handlers and ensure that any loaded fonts are - /// ready to be used on the next UI frame. + /// This will invoke any and handlers and ensure that any + /// loaded fonts are ready to be used on the next UI frame. ///
public void RebuildFonts() { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); - this.interfaceManager.RebuildFonts(); + if (this.AfterBuildFonts is null && this.BuildFonts is null) + this.privateAtlas.BuildFontsAsync(); + else + this.privateAtlas.BuildFontsOnNextFrame(); } + /// + /// Creates an isolated . + /// + /// Specify when and how to rebuild this atlas. + /// Whether the fonts in the atlas is global scaled. + /// Name for debugging purposes. + /// A new instance of . + /// + /// Use this to create extra font atlases, if you want to create and dispose fonts without having to rebuild all + /// other fonts together.
+ /// If is not , + /// the font rebuilding functions must be called manually. + ///
+ public IFontAtlas CreateFontAtlas( + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true, + string? debugName = null) => + this.scopedFinalizer.Add(Service + .Get() + .CreateFontAtlas( + this.namespaceName + ":" + (debugName ?? "custom"), + autoRebuildMode, + isGlobalScaled)); + /// /// Add a notification to the notification queue. /// @@ -392,12 +508,7 @@ public sealed class UiBuilder : IDisposable /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() - { - this.interfaceManager.Draw -= this.OnDraw; - this.interfaceManager.BuildFonts -= this.OnBuildFonts; - this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; - } + void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); /// /// Open the registered configuration UI, if it exists. @@ -463,8 +574,12 @@ public sealed class UiBuilder : IDisposable this.ShowUi?.InvokeSafely(); } - if (!this.interfaceManager.FontsReady) + // just in case, if something goes wrong, prevent drawing; otherwise it probably will crash. + if (!this.privateAtlas.BuildTask.IsCompletedSuccessfully + && (this.BuildFonts is not null || this.AfterBuildFonts is not null)) + { return; + } ImGui.PushID(this.namespaceName); if (DoStats) @@ -526,14 +641,28 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } - private void OnBuildFonts() + private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) { - this.BuildFonts?.InvokeSafely(); - } + if (e.IsAsyncBuildOperation) + return; - private void OnAfterBuildFonts() - { - this.AfterBuildFonts?.InvokeSafely(); + e.OnPreBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.BuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); + + e.OnPostBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.AfterBuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); } private void OnResizeBuffers() diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 85f81b203..80329f558 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,10 +1,15 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Numerics; +using System.Reactive.Disposables; using System.Runtime.InteropServices; +using System.Text.Unicode; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; @@ -31,7 +36,7 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null; /// /// Gets the global Dalamud scale; even available before drawing is ready.
@@ -342,25 +347,18 @@ public static class ImGuiHelpers } if (changed && rebuildLookupTable) - target.BuildLookupTableNonstandard(); - } + { + // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. + // FallbackGlyph is resolved after resolving ' '. + // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, + // making FindGlyph return nullptr. + // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, + // making ImGui attempt to treat whatever was there as a ' '. + // This may cause random glyphs to be sized randomly, if not an access violation exception. + target.NativePtr->FallbackGlyph = null; - /// - /// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions. - /// - /// The font. - public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font) - { - // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. - // FallbackGlyph is resolved after resolving ' '. - // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, - // making FindGlyph return nullptr. - // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, - // making ImGui attempt to treat whatever was there as a ' '. - // This may cause random glyphs to be sized randomly, if not an access violation exception. - font.NativePtr->FallbackGlyph = null; - - font.BuildLookupTable(); + target.BuildLookupTable(); + } } /// @@ -406,6 +404,129 @@ public static class ImGuiHelpers public static void CenterCursorFor(float itemWidth) => ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + /// + /// Allocates memory on the heap using
+ /// Memory must be freed using . + ///
+ /// Note that null is a valid return value when is 0. + ///
+ /// The length of allocated memory. + /// The allocated memory. + /// If returns null. + public static unsafe void* AllocateMemory(int length) + { + switch (length) + { + case 0: + return null; + case < 0: + throw new ArgumentOutOfRangeException( + nameof(length), + length, + $"{nameof(length)} cannot be a negative number."); + default: + var memory = ImGuiNative.igMemAlloc((uint)length); + if (memory is null) + { + throw new OutOfMemoryException( + $"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}"); + } + + return memory; + } + } + + /// + /// Mark 4K page as used, after adding a codepoint to a font. + /// + /// The font. + /// The codepoint. + public static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) + { + // Mark 4K page as used + var pageIndex = unchecked((ushort)(codepoint / 4096)); + font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); + } + + /// + /// Creates a new instance of with a natively backed memory. + /// + /// The created instance. + /// Disposable you can call. + public static unsafe IDisposable NewFontAtlasPtrScoped(out ImFontAtlasPtr font) + { + font = new(ImGuiNative.ImFontAtlas_ImFontAtlas()); + var ptr = font.NativePtr; + return Disposable.Create(() => + { + if (ptr != null) + ImGuiNative.ImFontAtlas_destroy(ptr); + ptr = null; + }); + } + + /// + /// Creates a new instance of with a natively backed memory. + /// + /// The created instance. + /// Disposable you can call. + public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder) + { + builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()); + var ptr = builder.NativePtr; + return Disposable.Create(() => + { + if (ptr != null) + ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr); + ptr = null; + }); + } + + /// + /// Builds ImGui Glyph Ranges for use with . + /// + /// The builder. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// When disposed, the resource allocated for the range will be freed. + public static unsafe ushort[] BuildRangesToArray( + this ImFontGlyphRangesBuilderPtr builder, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + if (addFallbackCodepoints) + builder.AddText(FontAtlasFactory.FallbackCodepoints); + if (addEllipsisCodepoints) + { + builder.AddText(FontAtlasFactory.EllipsisCodepoints); + builder.AddChar('.'); + } + + builder.BuildRanges(out var vec); + return new ReadOnlySpan((void*)vec.Data, vec.Size).ToArray(); + } + + /// + public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges) + => CreateImGuiRangesFrom((IEnumerable)ranges); + + /// + /// Creates glyph ranges from .
+ /// Use values from . + ///
+ /// The unicode ranges. + /// The range array that can be used for . + public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => + ranges + .Where(x => x.FirstCodePoint <= ushort.MaxValue) + .SelectMany( + x => new[] + { + (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), + (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), + }) + .ToArray(); + /// /// Determines whether is empty. /// @@ -414,7 +535,7 @@ public static class ImGuiHelpers public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; /// - /// Determines whether is not null and loaded. + /// Determines whether is empty. /// /// The pointer. /// Whether it is empty. @@ -447,6 +568,98 @@ public static class ImGuiHelpers return -1; } + /// + /// If is default, then returns . + /// + /// The self. + /// The other. + /// if it is not default; otherwise, . + public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => + self.NativePtr is null ? other : self; + + /// + /// Attempts to validate that is valid. + /// + /// The font pointer. + /// The exception, if any occurred during validation. + internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr) + { + try + { + var font = fontPtr.NativePtr; + if (font is null) + throw new NullReferenceException("The font is null."); + + _ = Marshal.ReadIntPtr((nint)font); + if (font->IndexedHotData.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexedHotData.Data); + if (font->FrequentKerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data); + if (font->IndexLookup.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexLookup.Data); + if (font->Glyphs.Data != 0) + _ = Marshal.ReadIntPtr(font->Glyphs.Data); + if (font->KerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->KerningPairs.Data); + if (font->ConfigDataCount == 0 && font->ConfigData is not null) + throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?"); + if (font->ConfigDataCount != 0 && font->ConfigData is null) + throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?"); + if (font->ConfigData is not null) + _ = Marshal.ReadIntPtr((nint)font->ConfigData); + if (font->FallbackGlyph is not null + && ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->FallbackHotData is not null + && ((nint)font->FallbackHotData < font->IndexedHotData.Data + || (nint)font->FallbackHotData >= font->IndexedHotData.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->ContainerAtlas is not null) + _ = Marshal.ReadIntPtr((nint)font->ContainerAtlas); + } + catch (Exception e) + { + return e; + } + + return null; + } + + /// + /// Updates the fallback char of . + /// + /// The font. + /// The fallback character. + internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c) + { + font.FallbackChar = c; + font.NativePtr->FallbackHotData = + (ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar); + } + + /// + /// Determines if the supplied codepoint is inside the given range, + /// in format of . + /// + /// The codepoint. + /// The ranges. + /// Whether it is the case. + internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr) + { + if (codepoint is <= 0 or >= ushort.MaxValue) + return false; + + while (*rangePtr != 0) + { + var from = *rangePtr++; + var to = *rangePtr++; + if (from <= codepoint && codepoint <= to) + return true; + } + + return false; + } + /// /// Get data needed for each new frame. /// From f8e6df1172e2b4eb87ee094aa94e219fa0f1ac99 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 29 Nov 2023 14:12:36 +0900 Subject: [PATCH 400/585] Add font build status display to Settings window --- .../Interface/Internal/InterfaceManager.cs | 9 ++++-- .../Windows/Settings/Tabs/SettingsTabLook.cs | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 5a6a2cbdb..e21a22fa2 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -4,9 +4,7 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; -using System.Text.Unicode; -using System.Threading; +using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -236,6 +234,11 @@ internal class InterfaceManager : IDisposable, IServiceType } } + /// + /// Gets the font build task. + /// + public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; + /// /// Dispose of managed and unmanaged resources. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 02e8ce789..ec140890f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,6 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using System.Text; using CheapLoc; using Dalamud.Configuration.Internal; @@ -145,6 +146,7 @@ public class SettingsTabLook : SettingsTab public override void Draw() { var interfaceManager = Service.Get(); + var fontBuildTask = interfaceManager.FontBuildTask; ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); @@ -164,6 +166,19 @@ public class SettingsTabLook : SettingsTab } } + if (!fontBuildTask.IsCompleted) + { + ImGui.SameLine(); + var buildingFonts = Loc.Localize("DalamudSettingsFontBuildInProgressWithEndingThreeDots", "Building fonts..."); + unsafe + { + var len = Encoding.UTF8.GetByteCount(buildingFonts); + var p = stackalloc byte[len]; + Encoding.UTF8.GetBytes(buildingFonts, new(p, len)); + ImGuiNative.igTextUnformatted(p, (p + len + ((Environment.TickCount / 200) % 3)) - 2); + } + } + var globalUiScaleInPt = 12f * this.globalUiScale; if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp)) { @@ -174,6 +189,19 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays.")); + if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled) + { + ImGui.TextColored( + ImGuiColors.DalamudRed, + Loc.Localize("DalamudSettingsFontBuildFaulted", "Failed to load fonts as requested.")); + if (fontBuildTask.Exception is not null + && ImGui.CollapsingHeader("##DalamudSetingsFontBuildFaultReason")) + { + foreach (var e in fontBuildTask.Exception.InnerExceptions) + ImGui.TextUnformatted(e.ToString()); + } + } + ImGuiHelpers.ScaledDummy(5); ImGui.AlignTextToFramePadding(); @@ -208,6 +236,7 @@ public class SettingsTabLook : SettingsTab public override void Save() { Service.Get().GlobalUiScale = this.globalUiScale; + Service.Get().FontGammaLevel = this.fontGamma; base.Save(); } From 701d006db831270bbb8ff85f3652e06886b84d1d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 29 Nov 2023 14:13:11 +0900 Subject: [PATCH 401/585] Fix AddDalamudDefaultFont on not using lodestone symbol fonts file --- .../IFontAtlasBuildToolkitPreBuild.cs | 7 ++- .../Internals/DelegateFontHandle.cs | 5 +- .../FontAtlasFactory.BuildToolkit.cs | 5 +- .../Internals/GamePrebakedFontHandle.cs | 49 +++++++++++++------ 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index e8f11aec3..dbe8626e9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -120,7 +120,9 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds the default font known to the current font atlas.
///
- /// Default font includes and . + /// Includes and .
+ /// As this involves adding multiple fonts, calling this function will set + /// as the return value of this function, if it was empty before. ///
/// Font size in pixels. /// The glyph ranges. Use .ToGlyphRange to build. @@ -132,7 +134,8 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit ///
/// Note: if game symbols font file is requested but is unavailable, /// then it will take the glyphs from game's built-in fonts, and everything in - /// will be ignored but and . + /// will be ignored but , , + /// and . ///
/// The font type. /// The font config. diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index bc48ddcc1..142bd73da 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -211,7 +211,8 @@ internal class DelegateFontHandle : IFontHandle.IInternal { Log.Warning( "[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " + - "did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", + "Using the most recently added font. " + + "Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", this.Manager.Name, fontsVector.Length - fontCountPrevious, nameof(FontAtlasBuildStepDelegate), @@ -262,7 +263,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal { var distinct = fontsVector - .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates + .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates .Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them .ToArray(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 9ebf20fc7..8e115c126 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -291,6 +291,8 @@ internal sealed partial class FontAtlasFactory var font = this.AddDalamudAssetFont(DalamudAsset.NotoSansJpMedium, fontConfig); this.AddExtraGlyphsForDalamudLanguage(fontConfig with { MergeFont = font }); this.AddGameSymbol(fontConfig with { MergeFont = font }); + if (this.Font.IsNull()) + this.Font = font; return font; } @@ -316,7 +318,8 @@ internal sealed partial class FontAtlasFactory return this.gameFontHandleSubstance.AttachGameSymbols( this, fontConfig.MergeFont, - fontConfig.SizePx); + fontConfig.SizePx, + fontConfig.GlyphRanges); default: return this.factory.AddFont( diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 012613a38..37266f39b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -214,9 +214,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal // Owned by this class, but ImFontPtr values still do not belong to this. private readonly Dictionary fonts = new(); private readonly Dictionary buildExceptions = new(); - - private readonly Dictionary fontsSymbolsOnly = new(); - private readonly Dictionary> symbolsCopyTargets = new(); + private readonly Dictionary> fontCopyTargets = new(); private readonly HashSet templatedFonts = new(); private readonly Dictionary> lateBuildRanges = new(); @@ -250,23 +248,24 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// The toolkitPostBuild. /// The font to attach to. /// The font size in pixels. + /// The intended glyph ranges. /// if it is not empty; otherwise a new font. - public ImFontPtr AttachGameSymbols(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, ImFontPtr font, float sizePx) + public ImFontPtr AttachGameSymbols( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + ImFontPtr font, + float sizePx, + ushort[]? glyphRanges) { var style = new GameFontStyle(GameFontFamily.Axis, sizePx); - if (!this.fontsSymbolsOnly.TryGetValue(style, out var symbolFont)) - { - symbolFont = this.CreateFontPrivate(style, toolkitPreBuild, ' ', '\uFFFE', true); - this.fontsSymbolsOnly.Add(style, symbolFont); - } + var referenceFont = this.GetOrCreateFont(style, toolkitPreBuild); if (font.IsNull()) font = this.CreateTemplateFont(style, toolkitPreBuild); - if (!this.symbolsCopyTargets.TryGetValue(symbolFont, out var set)) - this.symbolsCopyTargets[symbolFont] = set = new(); + if (!this.fontCopyTargets.TryGetValue(referenceFont, out var copyTargets)) + this.fontCopyTargets[referenceFont] = copyTargets = new(); - set.Add(font); + copyTargets.Add((font, glyphRanges)); return font; } @@ -342,7 +341,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal for (var i = 0; i < pixels8Array.Length; i++) toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out heights[i]); - foreach (var (style, font) in this.fonts.Concat(this.fontsSymbolsOnly)) + foreach (var (style, font) in this.fonts) { try { @@ -585,10 +584,30 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } - foreach (var (source, targets) in this.symbolsCopyTargets) + foreach (var (source, targets) in this.fontCopyTargets) { foreach (var target in targets) - ImGuiHelpers.CopyGlyphsAcrossFonts(source, target, true, true, SeIconCharMin, SeIconCharMax); + { + if (target.Ranges is null) + { + ImGuiHelpers.CopyGlyphsAcrossFonts(source, target.Font, missingOnly: true); + } + else + { + for (var i = 0; i < target.Ranges.Length; i += 2) + { + if (target.Ranges[i] == 0) + break; + ImGuiHelpers.CopyGlyphsAcrossFonts( + source, + target.Font, + true, + true, + target.Ranges[i], + target.Ranges[i + 1]); + } + } + } } } From e86c5458a20582d6dbad3e15de89825c4bdddaf9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 21:45:19 +0900 Subject: [PATCH 402/585] Remove font gamma configuration --- .../Internal/DalamudConfiguration.cs | 7 +-- .../Interface/Internal/InterfaceManager.cs | 10 ----- .../Windows/Settings/SettingsWindow.cs | 6 +-- .../Windows/Settings/Tabs/SettingsTabLook.cs | 23 ---------- .../FontAtlasFactory.BuildToolkit.cs | 3 +- .../Internals/FontAtlasFactory.cs | 44 +++++-------------- .../Internals/GamePrebakedFontHandle.cs | 16 ------- 7 files changed, 15 insertions(+), 94 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 76c8f3603..66c2745c5 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -148,12 +148,9 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable public bool UseAxisFontsFromGame { get; set; } = false; /// - /// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness. - /// - /// Before gamma is applied... - /// * ...TTF fonts loaded with stb or FreeType are in linear space. - /// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4. + /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. /// + [Obsolete("It happens that nobody touched this setting", true)] public float FontGammaLevel { get; set; } = 1.4f; /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index e21a22fa2..46d37fe90 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -199,16 +199,6 @@ internal class InterfaceManager : IDisposable, IServiceType /// public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; - /// - /// Gets or sets the overrided font gamma value, instead of using the value from configuration. - /// - public float? FontGammaOverride { get; set; } = null; - - /// - /// Gets the font gamma value to use. - /// - public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGammaLevel)); - /// /// Gets a value indicating the native handle of the game main window. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 414eabd22..20ffc781c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -66,13 +66,9 @@ internal class SettingsWindow : Window var configuration = Service.Get(); var interfaceManager = Service.Get(); - var rebuildFont = - ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale || - interfaceManager.FontGamma != configuration.FontGammaLevel || - interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - interfaceManager.FontGammaOverride = null; interfaceManager.UseAxisOverride = null; if (rebuildFont) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index ec140890f..35f307655 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -29,7 +29,6 @@ public class SettingsTabLook : SettingsTab }; private float globalUiScale; - private float fontGamma; public override SettingsEntry[] Entries { get; } = { @@ -202,33 +201,12 @@ public class SettingsTabLook : SettingsTab } } - ImGuiHelpers.ScaledDummy(5); - - ImGui.AlignTextToFramePadding(); - ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); - ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) - { - this.fontGamma = 1.4f; - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); - } - - if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp)) - { - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); - } - - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text.")); - base.Draw(); } public override void Load() { this.globalUiScale = Service.Get().GlobalUiScale; - this.fontGamma = Service.Get().FontGammaLevel; base.Load(); } @@ -236,7 +214,6 @@ public class SettingsTabLook : SettingsTab public override void Save() { Service.Get().GlobalUiScale = this.globalUiScale; - Service.Get().FontGammaLevel = this.fontGamma; base.Save(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 8e115c126..4403d6400 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -381,7 +381,6 @@ internal sealed partial class FontAtlasFactory public unsafe void PreBuild() { - var gamma = this.factory.InterfaceManager.FontGamma; var configData = this.data.ConfigData; foreach (ref var config in configData.DataSpan) { @@ -400,7 +399,7 @@ internal sealed partial class FontAtlasFactory config.GlyphOffset *= this.Scale; - config.RasterizerGamma *= gamma; + config.RasterizerGamma *= 1.4f; } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 7a1926a9d..fc199ef5a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -39,8 +39,6 @@ internal sealed partial class FontAtlasFactory private readonly Task defaultGlyphRanges; private readonly DalamudAssetManager dalamudAssetManager; - private float lastBuildGamma = -1f; - [ServiceManager.ServiceConstructor] private FontAtlasFactory( DataManager dataManager, @@ -210,18 +208,10 @@ internal sealed partial class FontAtlasFactory { lock (this.prebakedTextureWraps[texPathFormat]) { - var gamma = this.InterfaceManager.FontGamma; var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]); - if (Math.Abs(this.lastBuildGamma - gamma) > 0.0001f) - { - this.lastBuildGamma = gamma; - wraps.AggregateToDisposable().Dispose(); - wraps.AsSpan().Clear(); - } - var fileIndex = textureIndex / 4; var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; - wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex, gamma); + wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex); return CloneTextureWrap(wraps[textureIndex]); } } @@ -232,13 +222,9 @@ internal sealed partial class FontAtlasFactory Span target, ReadOnlySpan source, int channelIndex, - bool targetIsB4G4R4A4, - float gamma) + bool targetIsB4G4R4A4) { var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); - var gammaTable = stackalloc byte[256]; - for (var i = 0; i < 256; i++) - gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 255f, 0, 1), 1.4f / gamma) * 255); fixed (byte* sourcePtrImmutable = source) { @@ -250,7 +236,7 @@ internal sealed partial class FontAtlasFactory var wptr = (ushort*)targetPtr; while (numPixels-- > 0) { - *wptr = (ushort)((gammaTable[*rptr] << 8) | 0x0FFF); + *wptr = (ushort)((*rptr << 8) | 0x0FFF); wptr++; rptr += 4; } @@ -260,7 +246,7 @@ internal sealed partial class FontAtlasFactory var wptr = (uint*)targetPtr; while (numPixels-- > 0) { - *wptr = (uint)((gammaTable[*rptr] << 24) | 0x00FFFFFF); + *wptr = (uint)((*rptr << 24) | 0x00FFFFFF); wptr++; rptr += 4; } @@ -292,41 +278,33 @@ internal sealed partial class FontAtlasFactory Span target, ReadOnlySpan source, int channelIndex, - bool targetIsB4G4R4A4, - float gamma) + bool targetIsB4G4R4A4) { var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); fixed (byte* sourcePtrImmutable = source) { var rptr = sourcePtrImmutable + (channelIndex / 2); var rshift = (channelIndex & 1) == 0 ? 0 : 4; - var gammaTable = stackalloc byte[256]; fixed (void* targetPtr = target) { if (targetIsB4G4R4A4) { - for (var i = 0; i < 16; i++) - gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 15f, 0, 1), 1.4f / gamma) * 15); - var wptr = (ushort*)targetPtr; while (numPixels-- > 0) { - *wptr = (ushort)((gammaTable[(*rptr >> rshift) & 0xF] << 12) | 0x0FFF); + *wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF); wptr++; rptr += 2; } } else { - for (var i = 0; i < 256; i++) - gammaTable[i] = (byte)(MathF.Pow(Math.Clamp(i / 255f, 0, 1), 1.4f / gamma) * 255); - var wptr = (uint*)targetPtr; while (numPixels-- > 0) { var v = (*rptr >> rshift) & 0xF; v |= v << 4; - *wptr = (uint)((gammaTable[v] << 24) | 0x00FFFFFF); + *wptr = (uint)((v << 24) | 0x00FFFFFF); wptr++; rptr += 4; } @@ -335,7 +313,7 @@ internal sealed partial class FontAtlasFactory } } - private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex, float gamma) + private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) { var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); var numPixels = texFile.Header.Width * texFile.Header.Height; @@ -351,15 +329,15 @@ internal sealed partial class FontAtlasFactory { case TexFile.TextureFormat.B4G4R4A4: // Game ships with this format. - ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4, gamma); + ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); break; case TexFile.TextureFormat.B8G8R8A8: // In case of modded font textures. - ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4, gamma); + ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); break; default: // Unlikely. - ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4, gamma); + ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4); break; } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 37266f39b..c40302f6c 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -334,7 +334,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal ArrayPool.Shared.Return(x); }); - var fontGamma = this.interfaceManager.FontGamma; var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; var heights = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; @@ -447,21 +446,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } } - - if (Math.Abs(fontGamma - 1.4f) >= 0.001) - { - // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) - var xTo = rc->X + rc->Width; - var yTo = rc->Y + rc->Height; - for (int y = rc->Y; y < yTo; y++) - { - for (int x = rc->X; x < xTo; x++) - { - var i = (y * width) + x; - pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); - } - } - } } } else if (this.lateBuildRanges.TryGetValue(font, out var buildRanges)) From aa3b991932d48b999c84bd6c2cd9a8f3b2ae69aa Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 21:48:50 +0900 Subject: [PATCH 403/585] Minor fix --- Dalamud/Interface/Internal/Windows/ChangelogWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 9b0416583..ae59db36a 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -82,7 +82,7 @@ internal sealed class ChangelogWindow : Window, IDisposable // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - _ = this.bannerFont; + _ = this.bannerFont.Value; } private enum State From 47902f977040014586d93b64c126ffe6c7166089 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 21:53:45 +0900 Subject: [PATCH 404/585] Guarantee rounding advanceX/kerning pair distances --- .../Internals/FontAtlasFactory.BuildToolkit.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 4403d6400..4fa4c6a9e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -426,7 +426,9 @@ internal sealed partial class FontAtlasFactory var scale = this.Scale; foreach (ref var font in this.Fonts.DataSpan) { - if (!this.GlobalScaleExclusions.Contains(font) && Math.Abs(scale - 1f) > 0f) + if (this.GlobalScaleExclusions.Contains(font)) + font.AdjustGlyphMetrics(1f, 1f); // we still need to round advanceX and kerning + else font.AdjustGlyphMetrics(1 / scale, scale); foreach (var c in FallbackCodepoints) From 7eb4bf8ab46a7d2e4b9528b5528d2621a7595e50 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 30 Nov 2023 22:16:12 +0900 Subject: [PATCH 405/585] Add some more examples to doc comments --- Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs | 2 +- .../ManagedFontAtlas/Internals/DelegateFontHandle.cs | 6 +++--- .../Internals/FontAtlasFactory.Implementation.cs | 4 ++-- Dalamud/Interface/UiBuilder.cs | 12 ++++++++---- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index 6d971dc02..0a50d6070 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -58,7 +58,7 @@ public interface IFontAtlas : IDisposable public IFontHandle NewGameFontHandle(GameFontStyle style); /// - public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate @delegate); + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate); /// public void FreeFontHandle(IFontHandle handle); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index 142bd73da..f9f2c0ef1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -91,11 +91,11 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// /// Creates a new IFontHandle using your own callbacks. /// - /// Callback for . + /// Callback for . /// Handle to a font that may or may not be ready yet. - public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate callOnBuildStepChange) + public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) { - var key = new DelegateFontHandle(this, callOnBuildStepChange); + var key = new DelegateFontHandle(this, buildStepDelegate); lock (this.syncRoot) this.handles.Add(key); this.RebuildRecommend?.Invoke(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 3f0b5b22e..52d77b963 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -360,8 +360,8 @@ internal sealed partial class FontAtlasFactory public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); /// - public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate @delegate) => - this.delegateFontHandleManager.NewFontHandle(@delegate); + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => + this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); /// public void FreeFontHandle(IFontHandle handle) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index f7beb22fa..5d0810009 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -428,18 +428,22 @@ public sealed class UiBuilder : IDisposable /// /// - /// On initialization: + /// On initialization: /// /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => { /// var config = new SafeFontConfig { SizePx = 16 }; /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); /// tk.AddGameSymbol(config); /// tk.AddExtraGlyphsForDalamudLanguage(config); - /// // optional: tk.Font = config.MergeFont; + /// // optionally do the following if you have to add more than one font here, + /// // to specify which font added during this delegate is the final font to use. + /// tk.Font = config.MergeFont; /// })); + /// // or + /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); /// - /// - /// On use: + ///
+ /// On use: /// /// using (this.fontHandle.Push()) /// ImGui.TextUnformatted("Example"); From e7c7cdaa2975c966b1e3e9c682509bd2c8fb50cd Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 14:39:22 +0900 Subject: [PATCH 406/585] Update docs and exposed API --- .../Interface/Internal/InterfaceManager.cs | 57 +++++++++-------- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 60 ++++++++++++++++-- .../Internals/DelegateFontHandle.cs | 6 +- .../FontAtlasFactory.Implementation.cs | 33 +++++++--- .../Internals/GamePrebakedFontHandle.cs | 6 +- Dalamud/Interface/UiBuilder.cs | 61 ++++++------------- 6 files changed, 128 insertions(+), 95 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 46d37fe90..d252321db 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -699,35 +699,38 @@ internal class InterfaceManager : IDisposable, IServiceType { this.dalamudAtlas = fontAtlasFactory .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); - this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); - this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - tk => tk.AddFontAwesomeIconFont( - new() - { - SizePx = DefaultFontSizePx, - GlyphMinAdvanceX = DefaultFontSizePx, - GlyphMaxAdvanceX = DefaultFontSizePx, - }))); - this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - tk => tk.AddDalamudAssetFont( - DalamudAsset.InconsolataRegular, - new() { SizePx = DefaultFontSizePx }))); - this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( - tk => - { - // Note: the first call of this function is done outside the main thread; this is expected. - // Do not use DefaultFont, IconFont, and MonoFont. - // Use font handles directly. + using (this.dalamudAtlas.SuppressAutoRebuild()) + { + this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); + this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont( + new() + { + SizePx = DefaultFontSizePx, + GlyphMinAdvanceX = DefaultFontSizePx, + GlyphMaxAdvanceX = DefaultFontSizePx, + }))); + this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddDalamudAssetFont( + DalamudAsset.InconsolataRegular, + new() { SizePx = DefaultFontSizePx }))); + this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( + tk => + { + // Note: the first call of this function is done outside the main thread; this is expected. + // Do not use DefaultFont, IconFont, and MonoFont. + // Use font handles directly. - // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + // Fill missing glyphs in MonoFont from DefaultFont + tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); - // Broadcast to auto-rebuilding instances - this.AfterBuildFonts?.Invoke(); - }); + // Broadcast to auto-rebuilding instances + this.AfterBuildFonts?.Invoke(); + }); + } // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. _ = this.dalamudAtlas.BuildFontsAsync(false); diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index 0a50d6070..d32adc1eb 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -1,7 +1,6 @@ using System.Threading.Tasks; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas.Internals; using ImGuiNET; @@ -54,25 +53,73 @@ public interface IFontAtlas : IDisposable ///
bool IsGlobalScaled { get; } - /// + /// + /// Suppresses automatically rebuilding fonts for the scope. + /// + /// An instance of that will release the suppression. + /// + /// Use when you will be creating multiple new handles, and want rebuild to trigger only when you're done doing so. + /// This function will effectively do nothing, if is set to + /// . + /// + /// + /// + /// using (atlas.SuppressBuild()) { + /// this.font1 = atlas.NewGameFontHandle(...); + /// this.font2 = atlas.NewDelegateFontHandle(...); + /// } + /// + /// + public IDisposable SuppressAutoRebuild(); + + /// + /// Creates a new from game's built-in fonts. + /// + /// Font to use. + /// Handle to a font that may or may not be ready yet. public IFontHandle NewGameFontHandle(GameFontStyle style); - /// + /// + /// Creates a new IFontHandle using your own callbacks. + /// + /// Callback for . + /// Handle to a font that may or may not be ready yet. + /// + /// On initialization: + /// + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => { + /// var config = new SafeFontConfig { SizePx = 16 }; + /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); + /// tk.AddGameSymbol(config); + /// tk.AddExtraGlyphsForDalamudLanguage(config); + /// // optionally do the following if you have to add more than one font here, + /// // to specify which font added during this delegate is the final font to use. + /// tk.Font = config.MergeFont; + /// })); + /// // or + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); + /// + ///
+ /// On use: + /// + /// using (this.fontHandle.Push()) + /// ImGui.TextUnformatted("Example"); + /// + ///
public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate); - /// - public void FreeFontHandle(IFontHandle handle); - /// /// Queues rebuilding fonts, on the main thread.
/// Note that would not necessarily get changed from calling this function. ///
+ /// If is . void BuildFontsOnNextFrame(); /// /// Rebuilds fonts immediately, on the current thread.
/// Even the callback for will be called on the same thread. ///
+ /// If is . void BuildFontsImmediately(); /// @@ -80,5 +127,6 @@ public interface IFontAtlas : IDisposable /// /// Call on the main thread. /// The task. + /// If is . Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index f9f2c0ef1..b6ec720dc 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -88,11 +88,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal } } - /// - /// Creates a new IFontHandle using your own callbacks. - /// - /// Callback for . - /// Handle to a font that may or may not be ready yet. + /// public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) { var key = new DelegateFontHandle(this, buildStepDelegate); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 52d77b963..5656fc673 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; @@ -203,6 +204,9 @@ internal sealed partial class FontAtlasFactory private Task buildTask = EmptyTask; private FontAtlasBuiltData builtData; + private int buildSuppressionCounter; + private bool buildSuppressionSuppressed; + private int buildIndex; private bool buildQueued; private bool disposed = false; @@ -356,6 +360,19 @@ internal sealed partial class FontAtlasFactory GC.SuppressFinalize(this); } + /// + public IDisposable SuppressAutoRebuild() + { + this.buildSuppressionCounter++; + return Disposable.Create( + () => + { + this.buildSuppressionCounter--; + if (this.buildSuppressionSuppressed) + this.OnRebuildRecommend(); + }); + } + /// public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); @@ -363,15 +380,6 @@ internal sealed partial class FontAtlasFactory public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); - /// - public void FreeFontHandle(IFontHandle handle) - { - foreach (var manager in this.fontHandleManagers) - { - manager.FreeFontHandle(handle); - } - } - /// public void BuildFontsOnNextFrame() { @@ -688,6 +696,13 @@ internal sealed partial class FontAtlasFactory if (this.disposed) return; + if (this.buildSuppressionCounter > 0) + { + this.buildSuppressionSuppressed = true; + return; + } + + this.buildSuppressionSuppressed = false; this.factory.Framework.RunOnFrameworkThread( () => { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index c40302f6c..2739ed2da 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -157,11 +157,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal this.Substance = null; } - /// - /// Creates a new from game's built-in fonts. - /// - /// Font to use. - /// Handle to a font that may or may not be ready yet. + /// public IFontHandle NewFontHandle(GameFontStyle style) { var handle = new GamePrebakedFontHandle(this, style); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 5d0810009..a477ec09e 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -37,7 +37,6 @@ public sealed class UiBuilder : IDisposable private readonly DalamudConfiguration configuration = Service.Get(); private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private readonly IFontAtlas privateAtlas; private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -61,14 +60,14 @@ public sealed class UiBuilder : IDisposable this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); - this.privateAtlas = + this.FontAtlas = this.scopedFinalizer .Add( Service .Get() .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); - this.privateAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; - this.privateAtlas.RebuildRecommend += this.RebuildFonts; + this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; + this.FontAtlas.RebuildRecommend += this.RebuildFonts; } catch { @@ -104,7 +103,7 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
- [Obsolete($"Use {nameof(NewDelegateFontHandle)} instead.", false)] + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] public event Action? BuildFonts; /// @@ -113,7 +112,7 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. /// - [Obsolete($"Use {nameof(NewDelegateFontHandle)} instead.", false)] + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] public event Action? AfterBuildFonts; /// @@ -145,7 +144,7 @@ public sealed class UiBuilder : IDisposable /// /// A font handle corresponding to this font can be obtained with: /// - /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// fontAtlas.NewDelegateFontHandle( /// e => e.OnPreBuild( /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); /// @@ -159,7 +158,7 @@ public sealed class UiBuilder : IDisposable /// /// A font handle corresponding to this font can be obtained with: /// - /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// fontAtlas.NewDelegateFontHandle( /// e => e.OnPreBuild( /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); /// @@ -173,7 +172,7 @@ public sealed class UiBuilder : IDisposable /// /// A font handle corresponding to this font can be obtained with: /// - /// uiBuilderOrFontAtlas.NewDelegateFontHandle( + /// fontAtlas.NewDelegateFontHandle( /// e => e.OnPreBuild( /// tk => tk.AddDalamudAssetFont( /// DalamudAsset.InconsolataRegular, @@ -251,6 +250,11 @@ public sealed class UiBuilder : IDisposable /// public bool UiPrepared => Service.GetNullable() != null; + /// + /// Gets the plugin-private font atlas. + /// + public IFontAtlas FontAtlas { get; } + /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// @@ -418,40 +422,11 @@ public sealed class UiBuilder : IDisposable /// /// Font to get. /// Handle to the game font which may or may not be available for use yet. - [Obsolete($"Use {nameof(NewGameFontHandle)} instead.", false)] + [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (IFontHandle.IInternal)this.NewGameFontHandle(style), + (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), Service.Get()); - /// - public IFontHandle NewGameFontHandle(GameFontStyle style) => this.privateAtlas.NewGameFontHandle(style); - - /// - /// - /// On initialization: - /// - /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => { - /// var config = new SafeFontConfig { SizePx = 16 }; - /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); - /// tk.AddGameSymbol(config); - /// tk.AddExtraGlyphsForDalamudLanguage(config); - /// // optionally do the following if you have to add more than one font here, - /// // to specify which font added during this delegate is the final font to use. - /// tk.Font = config.MergeFont; - /// })); - /// // or - /// this.fontHandle = uiBuilder.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); - /// - ///
- /// On use: - /// - /// using (this.fontHandle.Push()) - /// ImGui.TextUnformatted("Example"); - /// - ///
- public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => - this.privateAtlas.NewDelegateFontHandle(buildStepDelegate); - /// /// Call this to queue a rebuild of the font atlas.
/// This will invoke any and handlers and ensure that any @@ -461,9 +436,9 @@ public sealed class UiBuilder : IDisposable { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); if (this.AfterBuildFonts is null && this.BuildFonts is null) - this.privateAtlas.BuildFontsAsync(); + this.FontAtlas.BuildFontsAsync(); else - this.privateAtlas.BuildFontsOnNextFrame(); + this.FontAtlas.BuildFontsOnNextFrame(); } /// @@ -579,7 +554,7 @@ public sealed class UiBuilder : IDisposable } // just in case, if something goes wrong, prevent drawing; otherwise it probably will crash. - if (!this.privateAtlas.BuildTask.IsCompletedSuccessfully + if (!this.FontAtlas.BuildTask.IsCompletedSuccessfully && (this.BuildFonts is not null || this.AfterBuildFonts is not null)) { return; From 77536429d641a897e36f858da77ec9782af791ec Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 15:25:12 +0900 Subject: [PATCH 407/585] Fix adding supplemental language fonts for GamePrebakedFontHandle --- .../FontAtlasBuildToolkitUtilities.cs | 22 ++++++++++++ .../Internals/GamePrebakedFontHandle.cs | 34 ++++++++++++------- .../ManagedFontAtlas/SafeFontConfig.cs | 11 ++++++ Dalamud/Interface/Utility/ImGuiHelpers.cs | 1 + 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs index d12409d51..586887a3b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; +using System.Runtime.CompilerServices; using Dalamud.Interface.Utility; +using ImGuiNET; + namespace Dalamud.Interface.ManagedFontAtlas; /// @@ -58,6 +61,25 @@ public static class FontAtlasBuildToolkitUtilities bool addEllipsisCodepoints = true) => @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); + /// + /// Finds the corresponding in + /// . that corresponds to the + /// specified font . + /// + /// The toolkit. + /// The font. + /// The relevant config pointer, or empty config pointer if not found. + public static unsafe ImFontConfigPtr FindConfigPtr(this IFontAtlasBuildToolkit toolkit, ImFontPtr fontPtr) + { + foreach (ref var c in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan) + { + if (c.DstFont == fontPtr.NativePtr) + return new((nint)Unsafe.AsPointer(ref c)); + } + + return default; + } + /// /// Invokes /// if of diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 2739ed2da..7e9ef9019 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -204,7 +204,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal internal sealed class HandleSubstance : IFontHandleSubstance { private readonly HandleManager handleManager; - private readonly InterfaceManager interfaceManager; private readonly HashSet gameFontStyles; // Owned by this class, but ImFontPtr values still do not belong to this. @@ -226,7 +225,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) { this.handleManager = manager; - this.interfaceManager = Service.Get(); + Service.Get(); this.gameFontStyles = new(gameFontStyles); } @@ -351,20 +350,21 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal var fdtGlyphs = fdt.Glyphs; var fontPtr = font.NativePtr; - fontPtr->FontSize = (fdtFontHeader.Size * 4) / 3; + var glyphs = font.GlyphsWrapped(); + var scale = toolkitPostBuild.Scale * (style.SizePt / fdtFontHeader.Size); + + fontPtr->FontSize = toolkitPostBuild.Scale * style.SizePx; if (fontPtr->ConfigData != null) fontPtr->ConfigData->SizePixels = fontPtr->FontSize; - fontPtr->Ascent = fdtFontHeader.Ascent; - fontPtr->Descent = fdtFontHeader.Descent; + fontPtr->Ascent = fdtFontHeader.Ascent * scale; + fontPtr->Descent = fdtFontHeader.Descent * scale; fontPtr->EllipsisChar = '…'; if (!allTexFiles.TryGetValue(attr.TexPathFormat, out var texFiles)) allTexFiles.Add(attr.TexPathFormat, texFiles = ArrayPool.Shared.Rent(texCount)); - + if (this.glyphRectIds.TryGetValue(style, out var rectIdToGlyphs)) { - this.glyphRectIds.Remove(style); - foreach (var (rectId, fdtGlyphIndex) in rectIdToGlyphs.Values) { ref var glyph = ref fdtGlyphs[fdtGlyphIndex]; @@ -442,6 +442,9 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } } + + glyphs[rc->GlyphId].XY *= scale; + glyphs[rc->GlyphId].AdvanceX *= scale; } } else if (this.lateBuildRanges.TryGetValue(font, out var buildRanges)) @@ -480,7 +483,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal textureIndices.AsSpan(0, texCount).Fill(-1); } - var glyphs = font.GlyphsWrapped(); glyphs.EnsureCapacity(glyphs.Length + buildRanges.Sum(x => (x.To - x.From) + 1)); foreach (var (rangeMin, rangeMax) in buildRanges) { @@ -530,6 +532,8 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal glyph.XY1 = glyph.XY0 + glyph.UV1; glyph.UV1 += glyph.UV0; glyph.UV /= fdtTexSize; + glyph.XY *= scale; + glyph.AdvanceX *= scale; glyphs.Add(glyph); } @@ -555,7 +559,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } - font.AdjustGlyphMetrics(style.SizePt / fdtFontHeader.Size, toolkitPostBuild.Scale); + font.AdjustGlyphMetrics(1 / toolkitPostBuild.Scale, toolkitPostBuild.Scale); } catch (Exception e) { @@ -616,7 +620,10 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal var font = toolkitPreBuild.IgnoreGlobalScale(this.CreateTemplateFont(style, toolkitPreBuild)); if (addExtraLanguageGlyphs) - toolkitPreBuild.AddExtraGlyphsForDalamudLanguage(new() { MergeFont = font }); + { + toolkitPreBuild.AddExtraGlyphsForDalamudLanguage( + new(toolkitPreBuild.FindConfigPtr(font)) { MergeFont = font }); + } var fas = GameFontStyle.GetRecommendedFamilyAndSize(style.Family, style.SizePt * toolkitPreBuild.Scale); var horizontalOffset = fas.GetAttribute()?.HorizontalOffset ?? 0; @@ -662,9 +669,10 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal fdtGlyphIndex); } } - + + var scale = toolkitPreBuild.Scale * (style.SizePt / fdt.FontHeader.Size); foreach (ref var kernPair in fdt.PairAdjustments) - font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); + font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset * scale); return font; } diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs index 812608973..cd840e5ed 100644 --- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -31,6 +31,17 @@ public struct SafeFontConfig this.Raw.FontDataOwnedByAtlas = 1; } + /// + /// Initializes a new instance of the struct, + /// copying applicable values from an existing instance of . + /// + /// Config to copy from. + public unsafe SafeFontConfig(ImFontConfigPtr config) + { + this.Raw = *config.NativePtr; + this.Raw.GlyphRanges = null; + } + /// /// Gets or sets the index of font within a TTF/OTF file. /// diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 80329f558..ed6ad1dfe 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -525,6 +525,7 @@ public static class ImGuiHelpers (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), }) + .Append((ushort)0) .ToArray(); /// From 3d576a0654bb947cc6dbaa43fc4a7ff6610913cf Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 15:40:58 +0900 Subject: [PATCH 408/585] Fix inconsistencies --- .../Internals/FontAtlasFactory.BuildToolkit.cs | 6 +----- .../ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 4fa4c6a9e..a3abc6681 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -398,8 +398,6 @@ internal sealed partial class FontAtlasFactory config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; config.GlyphOffset *= this.Scale; - - config.RasterizerGamma *= 1.4f; } } @@ -426,9 +424,7 @@ internal sealed partial class FontAtlasFactory var scale = this.Scale; foreach (ref var font in this.Fonts.DataSpan) { - if (this.GlobalScaleExclusions.Contains(font)) - font.AdjustGlyphMetrics(1f, 1f); // we still need to round advanceX and kerning - else + if (!this.GlobalScaleExclusions.Contains(font)) font.AdjustGlyphMetrics(1 / scale, scale); foreach (var c in FallbackCodepoints) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 7e9ef9019..040f9a743 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -621,8 +621,12 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal if (addExtraLanguageGlyphs) { - toolkitPreBuild.AddExtraGlyphsForDalamudLanguage( - new(toolkitPreBuild.FindConfigPtr(font)) { MergeFont = font }); + var cfg = toolkitPreBuild.FindConfigPtr(font); + toolkitPreBuild.AddExtraGlyphsForDalamudLanguage(new() + { + MergeFont = cfg.DstFont, + SizePx = cfg.SizePixels, + }); } var fas = GameFontStyle.GetRecommendedFamilyAndSize(style.Family, style.SizePt * toolkitPreBuild.Scale); From d78667900f4f509b1cdd0fa964f3bd0346385aca Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 21:08:35 +0900 Subject: [PATCH 409/585] Make it possible to attach arbitrary game font from delegate font --- Dalamud/Interface/GameFonts/FdtFileView.cs | 2 +- Dalamud/Interface/GameFonts/GameFontStyle.cs | 35 +- .../IFontAtlasBuildToolkitPreBuild.cs | 27 +- .../Internals/DelegateFontHandle.cs | 6 + .../FontAtlasFactory.BuildToolkit.cs | 65 +- .../Internals/GamePrebakedFontHandle.cs | 801 ++++++++++-------- .../Internals/IFontHandleSubstance.cs | 7 + .../ManagedFontAtlas/SafeFontConfig.cs | 8 +- 8 files changed, 544 insertions(+), 407 deletions(-) diff --git a/Dalamud/Interface/GameFonts/FdtFileView.cs b/Dalamud/Interface/GameFonts/FdtFileView.cs index 78b2e22f3..896a6dbb4 100644 --- a/Dalamud/Interface/GameFonts/FdtFileView.cs +++ b/Dalamud/Interface/GameFonts/FdtFileView.cs @@ -6,7 +6,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Reference member view of a .fdt file data. /// -internal readonly unsafe ref struct FdtFileView +internal readonly unsafe struct FdtFileView { private readonly byte* ptr; diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index e219670b8..fbaf9de07 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -64,7 +64,7 @@ public struct GameFontStyle /// public float SizePt { - get => this.SizePx * 3 / 4; + readonly get => this.SizePx * 3 / 4; set => this.SizePx = value * 4 / 3; } @@ -73,14 +73,14 @@ public struct GameFontStyle /// public float BaseSkewStrength { - get => this.SkewStrength * this.BaseSizePx / this.SizePx; + readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx; set => this.SkewStrength = value * this.SizePx / this.BaseSizePx; } /// /// Gets the font family. /// - public GameFontFamily Family => this.FamilyAndSize switch + public readonly GameFontFamily Family => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined, GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis, @@ -112,7 +112,7 @@ public struct GameFontStyle /// /// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes. /// - public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch + public readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch { GameFontFamily.Axis => GameFontFamilyAndSize.Axis96, GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16, @@ -126,7 +126,7 @@ public struct GameFontStyle /// /// Gets the base font size in point unit. /// - public float BaseSizePt => this.FamilyAndSize switch + public readonly float BaseSizePt => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => 0, GameFontFamilyAndSize.Axis96 => 9.6f, @@ -158,14 +158,14 @@ public struct GameFontStyle /// /// Gets the base font size in pixel unit. /// - public float BaseSizePx => this.BaseSizePt * 4 / 3; + public readonly float BaseSizePx => this.BaseSizePt * 4 / 3; /// /// Gets or sets a value indicating whether this font is bold. /// public bool Bold { - get => this.Weight > 0f; + readonly get => this.Weight > 0f; set => this.Weight = value ? 1f : 0f; } @@ -174,7 +174,7 @@ public struct GameFontStyle /// public bool Italic { - get => this.SkewStrength != 0; + readonly get => this.SkewStrength != 0; set => this.SkewStrength = value ? this.SizePx / 6 : 0; } @@ -233,13 +233,26 @@ public struct GameFontStyle _ => GameFontFamilyAndSize.Undefined, }; + /// + /// Creates a new scaled instance of struct. + /// + /// The scale. + /// The scaled instance. + public readonly GameFontStyle Scale(float scale) => new() + { + FamilyAndSize = GetRecommendedFamilyAndSize(this.Family, this.SizePt * scale), + SizePx = this.SizePx * scale, + Weight = this.Weight, + SkewStrength = this.SkewStrength * scale, + }; + /// /// Calculates the adjustment to width resulting fron Weight and SkewStrength. /// /// Font header. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + public readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) { var widthDelta = this.Weight; switch (this.BaseSkewStrength) @@ -263,11 +276,11 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// - public override string ToString() + public override readonly string ToString() { return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})"; } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index dbe8626e9..cb8a27a54 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -1,6 +1,7 @@ using System.IO; using System.Runtime.InteropServices; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using ImGuiNET; @@ -44,6 +45,13 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// Same with . ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + /// /// Adds a font from memory region allocated using .
/// It WILL crash if you try to use a memory pointer allocated in some other way.
@@ -120,7 +128,7 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds the default font known to the current font atlas.
///
- /// Includes and .
+ /// Includes and .
/// As this involves adding multiple fonts, calling this function will set /// as the return value of this function, if it was empty before. ///
@@ -153,15 +161,26 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds the game's symbols into the provided font.
- /// will be ignored. + /// will be ignored.
+ /// If the game symbol font file is unavailable, only will be honored. ///
/// The font config. - void AddGameSymbol(in SafeFontConfig fontConfig); + /// The added font. + ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig); + + /// + /// Adds the game glyphs to the font. + /// + /// The font style. + /// The glyph ranges. + /// The font to merge to. If empty, then a new font will be created. + /// The added font. + ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont); /// /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
/// will be ignored. ///
/// The font config. - void AddExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); + void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index b6ec720dc..f0ed09155 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -271,6 +271,12 @@ internal class DelegateFontHandle : IFontHandle.IInternal } } + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + // irrelevant + } + /// public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index a3abc6681..46fb3f63d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -273,24 +273,21 @@ internal sealed partial class FontAtlasFactory /// public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) { + ImFontPtr font; + glyphRanges ??= this.factory.DefaultGlyphRanges; if (Service.Get().UseAxis) { - return this.gameFontHandleSubstance.GetOrCreateFont( - new(GameFontFamily.Axis, sizePx), - this); + font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); + } + else + { + font = this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { SizePx = sizePx, GlyphRanges = glyphRanges }); + this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font }); } - glyphRanges ??= this.factory.DefaultGlyphRanges; - - var fontConfig = new SafeFontConfig - { - SizePx = sizePx, - GlyphRanges = glyphRanges, - }; - - var font = this.AddDalamudAssetFont(DalamudAsset.NotoSansJpMedium, fontConfig); - this.AddExtraGlyphsForDalamudLanguage(fontConfig with { MergeFont = font }); - this.AddGameSymbol(fontConfig with { MergeFont = font }); + this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font }); if (this.Font.IsNull()) this.Font = font; return font; @@ -315,11 +312,12 @@ internal sealed partial class FontAtlasFactory }); case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: - return this.gameFontHandleSubstance.AttachGameSymbols( - this, - fontConfig.MergeFont, - fontConfig.SizePx, - fontConfig.GlyphRanges); + { + return this.AddGameGlyphs( + new(GameFontFamily.Axis, fontConfig.SizePx), + fontConfig.GlyphRanges, + fontConfig.MergeFont); + } default: return this.factory.AddFont( @@ -341,20 +339,25 @@ internal sealed partial class FontAtlasFactory }); /// - public void AddGameSymbol(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( - DalamudAsset.LodestoneGameSymbol, - fontConfig with - { - GlyphRanges = new ushort[] + public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) => + this.AddDalamudAssetFont( + DalamudAsset.LodestoneGameSymbol, + fontConfig with { - GamePrebakedFontHandle.SeIconCharMin, - GamePrebakedFontHandle.SeIconCharMax, - 0, - }, - }); + GlyphRanges = new ushort[] + { + GamePrebakedFontHandle.SeIconCharMin, + GamePrebakedFontHandle.SeIconCharMax, + 0, + }, + }); /// - public void AddExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) + public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) => + this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges); + + /// + public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) { var dalamudConfiguration = Service.Get(); if (dalamudConfiguration.EffectiveLanguage == "ko") @@ -377,6 +380,8 @@ internal sealed partial class FontAtlasFactory { foreach (var substance in this.data.Substances) substance.OnPreBuild(this); + foreach (var substance in this.data.Substances) + substance.OnPreBuildCleanup(this); } public unsafe void PreBuild() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 040f9a743..1ac6fdbce 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -1,5 +1,7 @@ using System.Buffers; +using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; @@ -207,15 +209,11 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal private readonly HashSet gameFontStyles; // Owned by this class, but ImFontPtr values still do not belong to this. - private readonly Dictionary fonts = new(); + private readonly Dictionary fonts = new(); private readonly Dictionary buildExceptions = new(); - private readonly Dictionary> fontCopyTargets = new(); + private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new(); private readonly HashSet templatedFonts = new(); - private readonly Dictionary> lateBuildRanges = new(); - - private readonly Dictionary> glyphRectIds = - new(); /// /// Initializes a new instance of the class. @@ -238,29 +236,22 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - /// Attaches game symbols to the given font. + /// Attaches game symbols to the given font. If font is null, it will be created. /// /// The toolkitPostBuild. /// The font to attach to. - /// The font size in pixels. + /// The game font style. /// The intended glyph ranges. /// if it is not empty; otherwise a new font. - public ImFontPtr AttachGameSymbols( + public ImFontPtr AttachGameGlyphs( IFontAtlasBuildToolkitPreBuild toolkitPreBuild, ImFontPtr font, - float sizePx, - ushort[]? glyphRanges) + GameFontStyle style, + ushort[]? glyphRanges = null) { - var style = new GameFontStyle(GameFontFamily.Axis, sizePx); - var referenceFont = this.GetOrCreateFont(style, toolkitPreBuild); - if (font.IsNull()) - font = this.CreateTemplateFont(style, toolkitPreBuild); - - if (!this.fontCopyTargets.TryGetValue(referenceFont, out var copyTargets)) - this.fontCopyTargets[referenceFont] = copyTargets = new(); - - copyTargets.Add((font, glyphRanges)); + font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx); + this.attachments.Add((font, style, glyphRanges)); return font; } @@ -272,14 +263,20 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// The font. public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) { - if (this.fonts.TryGetValue(style, out var font)) - return font; - try { - font = this.CreateFontPrivate(style, toolkitPreBuild, ' ', '\uFFFE', true); - this.fonts.Add(style, font); - return font; + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + style, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(plan.FullRangeFont); + return plan.FullRangeFont; } catch (Exception e) { @@ -290,7 +287,9 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public ImFontPtr GetFontPtr(IFontHandle handle) => - handle is GamePrebakedFontHandle ggfh ? this.fonts.GetValueOrDefault(ggfh.FontStyle) : default; + handle is GamePrebakedFontHandle ggfh + ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default + : default; /// public Exception? GetBuildException(IFontHandle handle) => @@ -315,6 +314,34 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var (font, style, ranges) in this.attachments) + { + var effectiveStyle = + toolkitPreBuild.IsGlobalScaleIgnored(font) + ? style.Scale(1 / toolkitPreBuild.Scale) + : style; + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + effectiveStyle, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(font, ranges); + } + + foreach (var plan in this.fonts.Values) + { + plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas); + } + } + /// public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { @@ -331,235 +358,19 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; - var heights = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; for (var i = 0; i < pixels8Array.Length; i++) - toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out heights[i]); + toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _); - foreach (var (style, font) in this.fonts) + foreach (var (style, plan) in this.fonts) { try { - var fas = GameFontStyle.GetRecommendedFamilyAndSize( - style.Family, - style.SizePt * toolkitPostBuild.Scale); - var attr = fas.GetAttribute(); - var horizontalOffset = attr?.HorizontalOffset ?? 0; - var texCount = this.handleManager.GameFontTextureProvider.GetFontTextureCount(attr.TexPathFormat); - using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); - ref var fdtFontHeader = ref fdt.FontHeader; - var fdtGlyphs = fdt.Glyphs; - var fontPtr = font.NativePtr; + foreach (var font in plan.Ranges.Keys) + this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale); - var glyphs = font.GlyphsWrapped(); - var scale = toolkitPostBuild.Scale * (style.SizePt / fdtFontHeader.Size); - - fontPtr->FontSize = toolkitPostBuild.Scale * style.SizePx; - if (fontPtr->ConfigData != null) - fontPtr->ConfigData->SizePixels = fontPtr->FontSize; - fontPtr->Ascent = fdtFontHeader.Ascent * scale; - fontPtr->Descent = fdtFontHeader.Descent * scale; - fontPtr->EllipsisChar = '…'; - - if (!allTexFiles.TryGetValue(attr.TexPathFormat, out var texFiles)) - allTexFiles.Add(attr.TexPathFormat, texFiles = ArrayPool.Shared.Rent(texCount)); - - if (this.glyphRectIds.TryGetValue(style, out var rectIdToGlyphs)) - { - foreach (var (rectId, fdtGlyphIndex) in rectIdToGlyphs.Values) - { - ref var glyph = ref fdtGlyphs[fdtGlyphIndex]; - var rc = (ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas - .GetCustomRectByIndex(rectId) - .NativePtr; - var pixels8 = pixels8Array[rc->TextureIndex]; - var width = widths[rc->TextureIndex]; - texFiles[glyph.TextureFileIndex] ??= - this.handleManager - .GameFontTextureProvider - .GetTexFile(attr.TexPathFormat, glyph.TextureFileIndex); - var sourceBuffer = texFiles[glyph.TextureFileIndex].ImageData; - var sourceBufferDelta = glyph.TextureChannelByteIndex; - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdtFontHeader, glyph); - if (widthAdjustment == 0) - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var a = sourceBuffer[ - sourceBufferDelta + - (4 * (((glyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + - glyph.TextureOffsetX + x))]; - pixels8[((rc->Y + y) * width) + rc->X + x] = a; - } - } - } - else - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) - pixels8[((rc->Y + y) * width) + rc->X + x] = 0; - } - - for (int xbold = 0, xboldTo = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); - xbold < xboldTo; - xbold++) - { - var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); - for (var y = 0; y < glyph.BoundingHeight; y++) - { - float xDelta = xbold; - if (style.BaseSkewStrength > 0) - { - xDelta += style.BaseSkewStrength * - (fdtFontHeader.LineHeight - glyph.CurrentOffsetY - y) / - fdtFontHeader.LineHeight; - } - else if (style.BaseSkewStrength < 0) - { - xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / - fdtFontHeader.LineHeight; - } - - var xDeltaInt = (int)Math.Floor(xDelta); - var xness = xDelta - xDeltaInt; - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var sourcePixelIndex = - ((glyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + - glyph.TextureOffsetX + x; - var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; - var a2 = x == glyph.BoundingWidth - 1 - ? 0 - : sourceBuffer[sourceBufferDelta - + (4 * (sourcePixelIndex + 1))]; - var n = (a1 * xness) + (a2 * (1 - xness)); - var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; - pixels8[targetOffset] = - Math.Max(pixels8[targetOffset], (byte)(boldStrength * n)); - } - } - } - } - - glyphs[rc->GlyphId].XY *= scale; - glyphs[rc->GlyphId].AdvanceX *= scale; - } - } - else if (this.lateBuildRanges.TryGetValue(font, out var buildRanges)) - { - buildRanges.Sort(); - for (var i = 0; i < buildRanges.Count; i++) - { - var current = buildRanges[i]; - if (current.From > current.To) - buildRanges[i] = (From: current.To, To: current.From); - } - - for (var i = 0; i < buildRanges.Count - 1; i++) - { - var current = buildRanges[i]; - var next = buildRanges[i + 1]; - if (next.From <= current.To) - { - buildRanges[i] = current with { To = next.To }; - buildRanges.RemoveAt(i + 1); - i--; - } - } - - var fdtTexSize = new Vector4( - fdtFontHeader.TextureWidth, - fdtFontHeader.TextureHeight, - fdtFontHeader.TextureWidth, - fdtFontHeader.TextureHeight); - - if (!allTextureIndices.TryGetValue(attr.TexPathFormat, out var textureIndices)) - { - allTextureIndices.Add( - attr.TexPathFormat, - textureIndices = ArrayPool.Shared.Rent(texCount)); - textureIndices.AsSpan(0, texCount).Fill(-1); - } - - glyphs.EnsureCapacity(glyphs.Length + buildRanges.Sum(x => (x.To - x.From) + 1)); - foreach (var (rangeMin, rangeMax) in buildRanges) - { - var glyphIndex = fdt.FindGlyphIndex(rangeMin); - if (glyphIndex < 0) - glyphIndex = ~glyphIndex; - var endIndex = fdt.FindGlyphIndex(rangeMax); - if (endIndex < 0) - endIndex = ~endIndex - 1; - for (; glyphIndex <= endIndex; glyphIndex++) - { - var fdtg = fdtGlyphs[glyphIndex]; - - // If the glyph already exists in the target font, we do not overwrite. - if ( - !(fdtg.Char == ' ' && this.templatedFonts.Contains(font)) - && font.FindGlyphNoFallback(fdtg.Char).NativePtr is not null) - { - continue; - } - - ref var textureIndex = ref textureIndices[fdtg.TextureIndex]; - if (textureIndex == -1) - { - textureIndex = toolkitPostBuild.StoreTexture( - this.handleManager - .GameFontTextureProvider - .NewFontTextureRef(attr.TexPathFormat, fdtg.TextureIndex), - true); - } - - var glyph = new ImGuiHelpers.ImFontGlyphReal - { - AdvanceX = fdtg.AdvanceWidth, - Codepoint = fdtg.Char, - Colored = false, - TextureIndex = textureIndex, - Visible = true, - X0 = horizontalOffset, - Y0 = fdtg.CurrentOffsetY, - U0 = fdtg.TextureOffsetX, - V0 = fdtg.TextureOffsetY, - U1 = fdtg.BoundingWidth, - V1 = fdtg.BoundingHeight, - }; - - glyph.XY1 = glyph.XY0 + glyph.UV1; - glyph.UV1 += glyph.UV0; - glyph.UV /= fdtTexSize; - glyph.XY *= scale; - glyph.AdvanceX *= scale; - - glyphs.Add(glyph); - } - } - - font.NativePtr->FallbackGlyph = null; - - font.BuildLookupTable(); - } - - foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) - { - var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); - if ((IntPtr)glyph.NativePtr != IntPtr.Zero) - { - var ptr = font.NativePtr; - ptr->FallbackChar = fallbackCharCandidate; - ptr->FallbackGlyph = glyph.NativePtr; - ptr->FallbackHotData = - (ImFontGlyphHotData*)ptr->IndexedHotData.Address( - fallbackCharCandidate); - break; - } - } - - font.AdjustGlyphMetrics(1 / toolkitPostBuild.Scale, toolkitPostBuild.Scale); + plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths); + plan.PostProcessFullRangeFont(); + plan.CopyGlyphsToRanges(); } catch (Exception e) { @@ -567,32 +378,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal this.fonts[style] = default; } } - - foreach (var (source, targets) in this.fontCopyTargets) - { - foreach (var target in targets) - { - if (target.Ranges is null) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(source, target.Font, missingOnly: true); - } - else - { - for (var i = 0; i < target.Ranges.Length; i += 2) - { - if (target.Ranges[i] == 0) - break; - ImGuiHelpers.CopyGlyphsAcrossFonts( - source, - target.Font, - true, - true, - target.Ranges[i], - target.Ranges[i + 1]); - } - } - } - } } /// @@ -601,103 +386,401 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal // Irrelevant } - /// - /// Creates a relevant for the given . - /// - /// The game font style. - /// The toolkitPostBuild. - /// Min range. - /// Max range. - /// Add extra language glyphs. - /// The font. - private ImFontPtr CreateFontPrivate( - GameFontStyle style, - IFontAtlasBuildToolkitPreBuild toolkitPreBuild, - char minRange, - char maxRange, - bool addExtraLanguageGlyphs) - { - var font = toolkitPreBuild.IgnoreGlobalScale(this.CreateTemplateFont(style, toolkitPreBuild)); - - if (addExtraLanguageGlyphs) - { - var cfg = toolkitPreBuild.FindConfigPtr(font); - toolkitPreBuild.AddExtraGlyphsForDalamudLanguage(new() - { - MergeFont = cfg.DstFont, - SizePx = cfg.SizePixels, - }); - } - - var fas = GameFontStyle.GetRecommendedFamilyAndSize(style.Family, style.SizePt * toolkitPreBuild.Scale); - var horizontalOffset = fas.GetAttribute()?.HorizontalOffset ?? 0; - using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); - ref var fdtFontHeader = ref fdt.FontHeader; - var existing = new SortedSet(); - - if (style is { Bold: false, Italic: false }) - { - if (!this.lateBuildRanges.TryGetValue(font, out var ranges)) - this.lateBuildRanges[font] = ranges = new(); - - ranges.Add((minRange, maxRange)); - } - else - { - if (this.glyphRectIds.TryGetValue(style, out var rectIds)) - existing.UnionWith(rectIds.Keys); - else - rectIds = this.glyphRectIds[style] = new(); - - var glyphs = fdt.Glyphs; - for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) - { - ref var glyph = ref glyphs[fdtGlyphIndex]; - var cint = glyph.CharInt; - if (cint < minRange || cint > maxRange) - continue; - - var c = (char)cint; - if (existing.Contains(c)) - continue; - - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdtFontHeader, glyph); - rectIds[c] = ( - toolkitPreBuild.NewImAtlas.AddCustomRectFontGlyph( - font, - c, - glyph.BoundingWidth + widthAdjustment, - glyph.BoundingHeight, - glyph.AdvanceWidth, - new(horizontalOffset, glyph.CurrentOffsetY)), - fdtGlyphIndex); - } - } - - var scale = toolkitPreBuild.Scale * (style.SizePt / fdt.FontHeader.Size); - foreach (ref var kernPair in fdt.PairAdjustments) - font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset * scale); - - return font; - } - /// /// Creates a new template font. /// - /// The game font style. /// The toolkitPostBuild. + /// The size of the font. /// The font. - private ImFontPtr CreateTemplateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx) { var font = toolkitPreBuild.AddDalamudAssetFont( DalamudAsset.NotoSansJpMedium, new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, - SizePx = style.SizePx * toolkitPreBuild.Scale, + SizePx = sizePx, }); this.templatedFonts.Add(font); return font; } + + private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale) + { + if (!this.templatedFonts.Contains(font)) + return; + + var fas = style.Scale(atlasScale).FamilyAndSize; + using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); + ref var fdtFontHeader = ref fdt.FontHeader; + var fontPtr = font.NativePtr; + + var scale = style.SizePt / fdtFontHeader.Size; + fontPtr->Ascent = fdtFontHeader.Ascent * scale; + fontPtr->Descent = fdtFontHeader.Descent * scale; + fontPtr->EllipsisChar = '…'; + } + } + + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Internal")] + private sealed class FontDrawPlan : IDisposable + { + public readonly GameFontStyle Style; + public readonly GameFontStyle BaseStyle; + public readonly GameFontFamilyAndSizeAttribute BaseAttr; + public readonly int TexCount; + public readonly Dictionary Ranges = new(); + public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new(); + public readonly ushort[] RectLookup = new ushort[0x10000]; + public readonly FdtFileView Fdt; + public readonly ImFontPtr FullRangeFont; + + private readonly IDisposable fdtHandle; + private readonly IGameFontTextureProvider gftp; + + public FontDrawPlan( + GameFontStyle style, + float scale, + IGameFontTextureProvider gameFontTextureProvider, + ImFontPtr fullRangeFont) + { + this.Style = style; + this.BaseStyle = style.Scale(scale); + this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute()!; + this.gftp = gameFontTextureProvider; + this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat); + this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt); + this.RectLookup.AsSpan().Fill(ushort.MaxValue); + this.FullRangeFont = fullRangeFont; + this.Ranges[fullRangeFont] = new(0x10000); + } + + public void Dispose() + { + this.fdtHandle.Dispose(); + } + + public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null) + { + if (!this.Ranges.TryGetValue(font, out var rangeBitArray)) + rangeBitArray = this.Ranges[font] = new(0x10000); + + if (glyphRanges is null) + { + foreach (ref var g in this.Fdt.Glyphs) + { + var c = g.CharInt; + if (c is >= 0x20 and <= 0xFFFE) + rangeBitArray[c] = true; + } + + return; + } + + for (var i = 0; i < glyphRanges.Length - 1; i += 2) + { + if (glyphRanges[i] == 0) + break; + var from = (int)glyphRanges[i]; + var to = (int)glyphRanges[i + 1]; + for (var j = from; j <= to; j++) + rangeBitArray[j] = true; + } + } + + public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas) + { + var glyphs = this.Fdt.Glyphs; + var ranges = this.Ranges[this.FullRangeFont]; + foreach (var (font, extraRange) in this.Ranges) + { + if (font.NativePtr != this.FullRangeFont.NativePtr) + ranges.Or(extraRange); + } + + if (this.Style is not { Weight: 0, SkewStrength: 0 }) + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph); + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add( + ( + atlas.AddCustomRectFontGlyph( + this.FullRangeFont, + (char)cint, + glyph.BoundingWidth + widthAdjustment, + glyph.BoundingHeight, + glyph.AdvanceWidth, + new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)), + fdtGlyphIndex)); + } + } + else + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add((-1, fdtGlyphIndex)); + } + } + } + + public unsafe void PostProcessFullRangeFont() + { + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + g.XY *= scale; + g.AdvanceX *= scale; + } + + var pfrf = this.FullRangeFont.NativePtr; + ref var frf = ref *pfrf; + pfrf->FallbackGlyph = null; + ImGuiNative.ImFont_BuildLookupTable(pfrf); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate); + if ((nint)glyph == IntPtr.Zero) + continue; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + + public unsafe void CopyGlyphsToRanges() + { + foreach (var (font, rangeBits) in this.Ranges) + { + if (font.NativePtr == this.FullRangeFont.NativePtr) + continue; + + var lookup = font.IndexLookupWrapped(); + var glyphs = font.GlyphsWrapped(); + foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + if (!rangeBits[sourceGlyph.Codepoint]) + continue; + + var glyphIndex = ushort.MaxValue; + if (sourceGlyph.Codepoint < lookup.Length) + glyphIndex = lookup[sourceGlyph.Codepoint]; + + if (glyphIndex == ushort.MaxValue) + glyphs.Add(sourceGlyph); + else + glyphs[glyphIndex] = sourceGlyph; + } + + font.NativePtr->FallbackGlyph = null; + font.BuildLookupTable(); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr; + if ((nint)glyph == IntPtr.Zero) + continue; + + ref var frf = ref *font.NativePtr; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + } + + public unsafe void SetFullRangeFontGlyphs( + IFontAtlasBuildToolkitPostBuild toolkitPostBuild, + Dictionary allTexFiles, + Dictionary allTextureIndices, + byte*[] pixels8Array, + int[] widths) + { + var glyphs = this.FullRangeFont.GlyphsWrapped(); + var lookups = this.FullRangeFont.IndexLookupWrapped(); + + ref var fdtFontHeader = ref this.Fdt.FontHeader; + var fdtGlyphs = this.Fdt.Glyphs; + var fdtTexSize = new Vector4( + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight, + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight); + + if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles)) + { + allTexFiles.Add( + this.BaseAttr.TexPathFormat, + texFiles = ArrayPool.Shared.Rent(this.TexCount)); + } + + if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices)) + { + allTextureIndices.Add( + this.BaseAttr.TexPathFormat, + textureIndices = ArrayPool.Shared.Rent(this.TexCount)); + textureIndices.AsSpan(0, this.TexCount).Fill(-1); + } + + var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1)); + var pixelStrength = stackalloc byte[pixelWidth]; + for (var i = 0; i < pixelWidth; i++) + pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i)); + + var minGlyphY = 0; + var maxGlyphY = 0; + foreach (ref var g in fdtGlyphs) + { + minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY); + maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY); + } + + var horzShift = stackalloc int[maxGlyphY - minGlyphY]; + var horzBlend = stackalloc byte[maxGlyphY - minGlyphY]; + horzShift -= minGlyphY; + horzBlend -= minGlyphY; + if (this.BaseStyle.BaseSkewStrength != 0) + { + for (var i = minGlyphY; i < maxGlyphY; i++) + { + float blend = this.BaseStyle.BaseSkewStrength switch + { + > 0 => fdtFontHeader.LineHeight - i, + < 0 => -i, + _ => throw new InvalidOperationException(), + }; + blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight; + horzShift[i] = (int)MathF.Floor(blend); + horzBlend[i] = (byte)(255 * (blend - horzShift[i])); + } + } + + foreach (var (rectId, fdtGlyphIndex) in this.Rects) + { + ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex]; + if (rectId == -1) + { + ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex]; + if (textureIndex == -1) + { + textureIndex = toolkitPostBuild.StoreTexture( + this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex), + true); + } + + var glyph = new ImGuiHelpers.ImFontGlyphReal + { + AdvanceX = fdtGlyph.AdvanceWidth, + Codepoint = fdtGlyph.Char, + Colored = false, + TextureIndex = textureIndex, + Visible = true, + X0 = this.BaseAttr.HorizontalOffset, + Y0 = fdtGlyph.CurrentOffsetY, + U0 = fdtGlyph.TextureOffsetX, + V0 = fdtGlyph.TextureOffsetY, + U1 = fdtGlyph.BoundingWidth, + V1 = fdtGlyph.BoundingHeight, + }; + + glyph.XY1 = glyph.XY0 + glyph.UV1; + glyph.UV1 += glyph.UV0; + glyph.UV /= fdtTexSize; + + glyphs.Add(glyph); + } + else + { + ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas + .GetCustomRectByIndex(rectId) + .NativePtr; + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph); + + // Glyph is scaled at this point; undo that. + ref var glyph = ref glyphs[lookups[rc.GlyphId]]; + glyph.X0 = this.BaseAttr.HorizontalOffset; + glyph.Y0 = fdtGlyph.CurrentOffsetY; + glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment; + glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight; + glyph.AdvanceX = fdtGlyph.AdvanceWidth; + + var pixels8 = pixels8Array[rc.TextureIndex]; + var width = widths[rc.TextureIndex]; + texFiles[fdtGlyph.TextureFileIndex] ??= + this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex); + var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData; + var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex; + + for (var y = 0; y < fdtGlyph.BoundingHeight; y++) + { + var sourcePixelIndex = + ((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX; + sourcePixelIndex *= 4; + sourcePixelIndex += sourceBufferDelta; + var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y]; + + var targetOffset = ((rc.Y + y) * width) + rc.X; + for (var x = 0; x < rc.Width; x++) + pixels8[targetOffset + x] = 0; + + targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y]; + if (blend1 == 0) + { + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var n = sourceBuffer[sourcePixelIndex + 4]; + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255)); + } + } + } + else + { + var blend2 = 255 - blend1; + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var a1 = sourceBuffer[sourcePixelIndex]; + var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4]; + var n = (a1 * blend1) + (a2 * blend2); + + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255)); + } + } + } + } + } + } + } } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs index fbfa2d12e..f6c5c6591 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -31,6 +31,13 @@ internal interface IFontHandleSubstance : IDisposable /// /// The toolkit. void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called between and calls.
+ /// Any further modification to will result in undefined behavior. + ///
+ /// The toolkit. + void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); /// /// Called after call. diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs index cd840e5ed..cb7f7c65a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -37,9 +37,13 @@ public struct SafeFontConfig /// /// Config to copy from. public unsafe SafeFontConfig(ImFontConfigPtr config) + : this() { - this.Raw = *config.NativePtr; - this.Raw.GlyphRanges = null; + if (config.NativePtr is not null) + { + this.Raw = *config.NativePtr; + this.Raw.GlyphRanges = null; + } } /// From 2bfddaae168bfacfd29a46d74a728aff96c4cfbc Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 21:21:14 +0900 Subject: [PATCH 410/585] Add minimum range rebuild test --- .../Widgets/GamePrebakedFontsTestWidget.cs | 57 ++++++++++++++----- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index 12749114b..dba293e8b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Text; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; @@ -24,6 +25,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable private bool useWordWrap; private bool useItalic; private bool useBold; + private bool useMinimumBuild; /// public string[]? CommandShortcuts { get; init; } @@ -80,6 +82,17 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable this.ClearAtlas(); } } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Minimum Range"u8) + { + var v = (byte)(this.useMinimumBuild ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useMinimumBuild = v != 0; + this.ClearAtlas(); + } + } ImGui.SameLine(); if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed) @@ -90,21 +103,6 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable minCapacity: 1024); } - this.privateAtlas ??= - Service.Get().CreateFontAtlas( - nameof(GamePrebakedFontsTestWidget), - FontAtlasAutoRebuildMode.Async, - this.useGlobalScale); - this.fontHandles ??= - Enum.GetValues() - .Where(x => x.GetAttribute() is not null) - .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) - .GroupBy(x => x.Family) - .ToImmutableDictionary( - x => x.Key, - x => x.Select(y => (y, new Lazy(() => this.privateAtlas.NewGameFontHandle(y)))) - .ToArray()); - fixed (byte* labelPtr = "Test Input"u8) { if (ImGuiNative.igInputTextMultiline( @@ -124,9 +122,38 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable this.testStringBuffer.LengthUnsafe = len; this.testStringBuffer.StorageSpan[len] = default; } + + if (this.useMinimumBuild) + _ = this.privateAtlas?.BuildFontsAsync(); } } + this.privateAtlas ??= + Service.Get().CreateFontAtlas( + nameof(GamePrebakedFontsTestWidget), + FontAtlasAutoRebuildMode.Async, + this.useGlobalScale); + this.fontHandles ??= + Enum.GetValues() + .Where(x => x.GetAttribute() is not null) + .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) + .GroupBy(x => x.Family) + .ToImmutableDictionary( + x => x.Key, + x => x.Select( + y => (y, new Lazy( + () => this.useMinimumBuild + ? this.privateAtlas.NewDelegateFontHandle( + e => + e.OnPreBuild( + tk => tk.AddGameGlyphs( + y, + Encoding.UTF8.GetString( + this.testStringBuffer.DataSpan).ToGlyphRange(), + default))) + : this.privateAtlas.NewGameFontHandle(y)))) + .ToArray()); + var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); foreach (var (family, items) in this.fontHandles) { From f172ee2308def7fb1e5567a8505b1b5b1f176db2 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 21:46:10 +0900 Subject: [PATCH 411/585] Better rounding --- .../FontAtlasFactory.BuildToolkit.cs | 2 +- .../Internals/GamePrebakedFontHandle.cs | 91 +++++++++++++++++-- Dalamud/Interface/Utility/ImGuiHelpers.cs | 2 +- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 46fb3f63d..fdef499dd 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -430,7 +430,7 @@ internal sealed partial class FontAtlasFactory foreach (ref var font in this.Fonts.DataSpan) { if (!this.GlobalScaleExclusions.Contains(font)) - font.AdjustGlyphMetrics(1 / scale, scale); + font.AdjustGlyphMetrics(1 / scale, 1 / scale); foreach (var c in FallbackCodepoints) { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 1ac6fdbce..99c817a91 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -369,8 +369,8 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale); plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths); - plan.PostProcessFullRangeFont(); - plan.CopyGlyphsToRanges(); + plan.CopyGlyphsToRanges(toolkitPostBuild); + plan.PostProcessFullRangeFont(toolkitPostBuild.Scale); } catch (Exception e) { @@ -543,17 +543,43 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } - public unsafe void PostProcessFullRangeFont() + public unsafe void PostProcessFullRangeFont(float atlasScale) { + var round = 1 / atlasScale; + var pfrf = this.FullRangeFont.NativePtr; + ref var frf = ref *pfrf; + + frf.FontSize = MathF.Round(frf.FontSize / round) * round; + frf.Ascent = MathF.Round(frf.Ascent / round) * round; + frf.Descent = MathF.Round(frf.Descent / round) * round; + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan) { - g.XY *= scale; - g.AdvanceX *= scale; + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + + var fullRange = this.Ranges[this.FullRangeFont]; + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!fullRange[leftInt] || !fullRange[rightInt]) + continue; + ImGuiNative.ImFont_AddKerningPair( + pfrf, + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); } - var pfrf = this.FullRangeFont.NativePtr; - ref var frf = ref *pfrf; pfrf->FallbackGlyph = null; ImGuiNative.ImFont_BuildLookupTable(pfrf); @@ -571,13 +597,19 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } - public unsafe void CopyGlyphsToRanges() + public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + var atlasScale = toolkitPostBuild.Scale; + var round = 1 / atlasScale; + foreach (var (font, rangeBits) in this.Ranges) { if (font.NativePtr == this.FullRangeFont.NativePtr) continue; + var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font); + var lookup = font.IndexLookupWrapped(); var glyphs = font.GlyphsWrapped(); foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan) @@ -590,9 +622,48 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal glyphIndex = lookup[sourceGlyph.Codepoint]; if (glyphIndex == ushort.MaxValue) - glyphs.Add(sourceGlyph); + { + glyphIndex = (ushort)glyphs.Length; + glyphs.Add(default); + } + + ref var g = ref glyphs[glyphIndex]; + g = sourceGlyph; + if (noGlobalScale) + { + g.XY *= scale; + g.AdvanceX *= scale; + } else - glyphs[glyphIndex] = sourceGlyph; + { + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + } + + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!rangeBits[leftInt] || !rangeBits[rightInt]) + continue; + if (noGlobalScale) + { + font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale); + } + else + { + font.AddKerningPair( + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); + } } font.NativePtr->FallbackGlyph = null; diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index ed6ad1dfe..8ba103593 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -202,7 +202,7 @@ public static class ImGuiHelpers /// If a positive number is given, numbers will be rounded to this. public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) { - Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; + Func rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x; var font = fontPtr.NativePtr; font->FontSize = rounder(font->FontSize * scale); From 015c313c5ebad2cfe70cdac42afa1f833ccb6f1c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 10 Dec 2023 22:02:33 +0900 Subject: [PATCH 412/585] Move UseAxis/Override to FAF --- Dalamud/Interface/Internal/DalamudIme.cs | 7 ++-- .../Interface/Internal/InterfaceManager.cs | 10 ------ .../Windows/Settings/SettingsWindow.cs | 6 ++-- .../Windows/Settings/Tabs/SettingsTabLook.cs | 6 ++-- .../FontAtlasFactory.BuildToolkit.cs | 36 +++++++++++++++++-- .../Internals/FontAtlasFactory.cs | 11 ++++++ Dalamud/Interface/Utility/ImGuiHelpers.cs | 18 +++++----- 7 files changed, 64 insertions(+), 30 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index e030b4e50..28a9075bd 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -11,6 +11,7 @@ using System.Text.Unicode; using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using ImGuiNET; @@ -196,9 +197,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { - if (Service.Get() - .GetFdtReader(GameFontFamilyAndSize.Axis12) - ?.FindGlyph(chr) is null) + if (Service.Get() + ?.GetFdtReader(GameFontFamilyAndSize.Axis12) + .FindGlyph(chr) is null) { if (!this.EncounteredHan) { diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d252321db..3e004727a 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -189,16 +189,6 @@ internal class InterfaceManager : IDisposable, IServiceType /// public bool IsDispatchingEvents { get; set; } = true; - /// - /// Gets or sets a value indicating whether to override configuration for UseAxis. - /// - public bool? UseAxisOverride { get; set; } = null; - - /// - /// Gets a value indicating whether to use AXIS fonts. - /// - public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; - /// /// Gets a value indicating the native handle of the game main window. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 20ffc781c..027e1a571 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -5,6 +5,7 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.Settings.Tabs; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -65,11 +66,12 @@ internal class SettingsWindow : Window { var configuration = Service.Get(); var interfaceManager = Service.Get(); + var fontAtlasFactory = Service.Get(); - var rebuildFont = interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - interfaceManager.UseAxisOverride = null; + fontAtlasFactory.UseAxisOverride = null; if (rebuildFont) interfaceManager.RebuildFonts(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 35f307655..5293e13c4 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -8,6 +8,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -41,9 +42,8 @@ public class SettingsTabLook : SettingsTab (v, c) => c.UseAxisFontsFromGame = v, v => { - var im = Service.Get(); - im.UseAxisOverride = v; - im.RebuildFonts(); + Service.Get().UseAxisOverride = v; + Service.Get().RebuildFonts(); }), new GapSettingsEntry(5, true), diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index fdef499dd..e73ea7548 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -114,7 +114,7 @@ internal sealed partial class FontAtlasFactory return fontPtr; } - /// + /// public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GlobalScaleExclusions.Contains(fontPtr); @@ -275,7 +275,7 @@ internal sealed partial class FontAtlasFactory { ImFontPtr font; glyphRanges ??= this.factory.DefaultGlyphRanges; - if (Service.Get().UseAxis) + if (this.factory.UseAxis) { font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); } @@ -360,7 +360,8 @@ internal sealed partial class FontAtlasFactory public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) { var dalamudConfiguration = Service.Get(); - if (dalamudConfiguration.EffectiveLanguage == "ko") + if (dalamudConfiguration.EffectiveLanguage == "ko" + || Service.GetNullable()?.EncounteredHangul is true) { this.AddDalamudAssetFont( DalamudAsset.NotoSansKrRegular, @@ -374,6 +375,35 @@ internal sealed partial class FontAtlasFactory UnicodeRanges.HangulJamoExtendedB), }); } + + var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + + var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + this.AddFontFromFile(fontPathCht, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || Service.GetNullable()?.EncounteredHan is true)) + { + this.AddFontFromFile(fontPathChs, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } } public void PreBuildSubstances() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index fc199ef5a..358ccd845 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; using Dalamud.Interface.GameFonts; @@ -106,6 +107,16 @@ internal sealed partial class FontAtlasFactory }); } + /// + /// Gets or sets a value indicating whether to override configuration for UseAxis. + /// + public bool? UseAxisOverride { get; set; } = null; + + /// + /// Gets a value indicating whether to use AXIS fonts. + /// + public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; + /// /// Gets the service instance of . /// diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 8ba103593..e3b0ff8d1 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -548,6 +548,15 @@ public static class ImGuiHelpers /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; + + /// + /// If is default, then returns . + /// + /// The self. + /// The other. + /// if it is not default; otherwise, . + public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => + self.NativePtr is null ? other : self; /// /// Finds the corresponding ImGui viewport ID for the given window handle. @@ -569,15 +578,6 @@ public static class ImGuiHelpers return -1; } - /// - /// If is default, then returns . - /// - /// The self. - /// The other. - /// if it is not default; otherwise, . - public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => - self.NativePtr is null ? other : self; - /// /// Attempts to validate that is valid. /// From de53150bd373275047c49fbd31b804f9a8c5ac3d Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 4 Jan 2024 02:28:41 +0900 Subject: [PATCH 413/585] Optional recursive dependency pulls and fallback dependency load (#1595) * Optional recursive dependency pulls and fallback dependency load * add api10 todo --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Internal/Loader/AssemblyLoadContextBuilder.cs | 9 ++++++++- Dalamud/Plugin/Internal/Loader/LoaderConfig.cs | 2 +- .../Plugin/Internal/Loader/ManagedLoadContext.cs | 13 ++++++++++++- Dalamud/Plugin/Internal/Loader/PluginLoader.cs | 14 +++++--------- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 14 ++++++++++++-- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs index b7a2ffe2e..1a6830a3a 100644 --- a/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs +++ b/Dalamud/Plugin/Internal/Loader/AssemblyLoadContextBuilder.cs @@ -131,9 +131,16 @@ internal class AssemblyLoadContextBuilder /// or the default app context. /// /// The name of the assembly. + /// Pull assmeblies recursively. /// The builder. - public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName) + public AssemblyLoadContextBuilder PreferDefaultLoadContextAssembly(AssemblyName assemblyName, bool recursive) { + if (!recursive) + { + this.defaultAssemblies.Add(assemblyName.Name); + return this; + } + var names = new Queue(new[] { assemblyName }); while (names.TryDequeue(out var name)) diff --git a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs index d3fcdc99e..0b2150069 100644 --- a/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs +++ b/Dalamud/Plugin/Internal/Loader/LoaderConfig.cs @@ -46,7 +46,7 @@ internal class LoaderConfig /// Gets a list of assemblies which should be unified between the host and the plugin. ///
/// what-are-shared-types - public ICollection SharedAssemblies { get; } = new List(); + public ICollection<(AssemblyName Name, bool Recursive)> SharedAssemblies { get; } = new List<(AssemblyName Name, bool Recursive)>(); /// /// Gets or sets a value indicating whether attempt to unify all types from a plugin with the host. diff --git a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs index 4bb326ce4..e0629217a 100644 --- a/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs +++ b/Dalamud/Plugin/Internal/Loader/ManagedLoadContext.cs @@ -194,7 +194,18 @@ internal class ManagedLoadContext : AssemblyLoadContext } } - return null; + // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/loading-managed#algorithm + // > These assemblies are loaded (load-by-name) as needed by the runtime. + // For load-by-name assembiles, the following will happen in order: + // (1) this.Load will be called. + // (2) AssemblyLoadContext.Default's cache will be referred for lookup. + // (3) Default probing will be done from PLATFORM_RESOURCE_ROOTS and APP_PATHS. + // https://learn.microsoft.com/en-us/dotnet/core/dependency-loading/default-probing#managed-assembly-default-probing + // > TRUSTED_PLATFORM_ASSEMBLIES: List of platform and application assembly file paths. + // > APP_PATHS: is not populated by default and is omitted for most applications. + // If we return null here, if the assembly has not been already loaded, the resolution will fail. + // Therefore as the final attempt, we try loading from the default load context. + return this.defaultLoadContext.LoadFromAssemblyName(assemblyName); } /// diff --git a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs index 53aec60ef..63b47cf17 100644 --- a/Dalamud/Plugin/Internal/Loader/PluginLoader.cs +++ b/Dalamud/Plugin/Internal/Loader/PluginLoader.cs @@ -146,18 +146,14 @@ internal class PluginLoader : IDisposable builder.ShadowCopyNativeLibraries(); } - foreach (var assemblyName in config.SharedAssemblies) + foreach (var (assemblyName, recursive) in config.SharedAssemblies) { - builder.PreferDefaultLoadContextAssembly(assemblyName); + builder.PreferDefaultLoadContextAssembly(assemblyName, recursive); } - // This allows plugins to search for dependencies in the Dalamud directory when their assembly - // load would otherwise fail, allowing them to resolve assemblies not already loaded by Dalamud - // itself yet. - builder.AddProbingPath(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); - - // Also make sure that plugins do not load their own Dalamud assembly. - builder.PreferDefaultLoadContextAssembly(Assembly.GetExecutingAssembly().GetName()); + // Note: not adding Dalamud path here as a probing path. + // It will be dealt as the last resort from ManagedLoadContext.Load. + // See there for more details. return builder; } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index aff9a8b43..0ddd4b23e 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -627,8 +627,18 @@ internal class LocalPlugin : IDisposable config.IsUnloadable = true; config.LoadInMemory = true; config.PreferSharedTypes = false; - config.SharedAssemblies.Add(typeof(Lumina.GameData).Assembly.GetName()); - config.SharedAssemblies.Add(typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName()); + + // Pin Lumina and its dependencies recursively (compatibility behavior). + // It currently only pulls in System.* anyway. + // TODO(api10): Remove this. We don't want to pin Lumina anymore, plugins should be able to provide their own. + config.SharedAssemblies.Add((typeof(Lumina.GameData).Assembly.GetName(), true)); + config.SharedAssemblies.Add((typeof(Lumina.Excel.ExcelSheetImpl).Assembly.GetName(), true)); + + // Make sure that plugins do not load their own Dalamud assembly. + // We do not pin this recursively; if a plugin loads its own assembly of Dalamud, it is always wrong, + // but plugins may load other versions of assemblies that Dalamud depends on. + config.SharedAssemblies.Add((typeof(EntryPoint).Assembly.GetName(), false)); + config.SharedAssemblies.Add((typeof(Common.DalamudStartInfo).Assembly.GetName(), false)); } private void EnsureLoader() From 653dca2feb72ccdb60481aa30d8e6ac360be2ea1 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 5 Jan 2024 20:32:14 +0100 Subject: [PATCH 414/585] Fix verbose log in TextureManager.Dispose (#1596) --- Dalamud/Interface/Internal/TextureManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 40aa72913..9f90ea1ad 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Numerics; @@ -273,7 +273,10 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe this.fallbackTextureWrap?.Dispose(); this.framework.Update -= this.FrameworkOnUpdate; - Log.Verbose("Disposing {Num} left behind textures."); + if (this.activeTextures.Count == 0) + return; + + Log.Verbose("Disposing {Num} left behind textures.", this.activeTextures.Count); foreach (var activeTexture in this.activeTextures) { From 84637f6dfa167f70714e781a82840eb976438f00 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 5 Jan 2024 20:54:12 +0100 Subject: [PATCH 415/585] Update ClientStructs (#1590) 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 97b814ca1..07add8584 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 97b814ca15d147911cdac3059623185a57984e0a +Subproject commit 07add8584e6eabb6a8c398b2a7c669cdd607382e From c7c2b2dce1db804e2f7fdd83cc964cdbd6c05951 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Fri, 5 Jan 2024 16:41:26 -0800 Subject: [PATCH 416/585] Revert "Update ClientStructs (#1590)" (#1600) This reverts commit 84637f6dfa167f70714e781a82840eb976438f00. --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 07add8584..97b814ca1 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 07add8584e6eabb6a8c398b2a7c669cdd607382e +Subproject commit 97b814ca15d147911cdac3059623185a57984e0a From 78c0281b9066b36310182d47317be036ea12c199 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 6 Jan 2024 18:38:03 +0100 Subject: [PATCH 417/585] Update ClientStructs (#1599) 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 97b814ca1..837c6fafc 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 97b814ca15d147911cdac3059623185a57984e0a +Subproject commit 837c6fafc13fbdda3e13a833b6085e4ce93d19e1 From 767cc49ecb80e29dbdda2fa8329d3c3341c964fe Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 6 Jan 2024 23:16:12 +0100 Subject: [PATCH 418/585] build: 9.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 a870bee17..5844527ee 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.14 + 9.0.0.15 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 6ccc982d2b1be10283f5c37a7b2e62f05f0f5c78 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 12 Jan 2024 12:21:36 +0900 Subject: [PATCH 419/585] Add docs for RebuildRecommend --- Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs | 13 +++++++++++-- .../Internals/IFontHandleManager.cs | 4 +--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index d32adc1eb..ec3e66e9a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Utility; using ImGuiNET; @@ -18,9 +19,17 @@ public interface IFontAtlas : IDisposable event FontAtlasBuildStepDelegate? BuildStepChange; /// - /// Event fired when a font rebuild operation is suggested.
- /// This will be invoked from the main thread. + /// Event fired when a font rebuild operation is recommended.
+ /// This event will be invoked from the main thread.
+ ///
+ /// Reasons for the event include changes in and + /// initialization of new associated font handles. ///
+ /// + /// You should call or + /// if is not set to true.
+ /// Avoid calling here; it will block the main thread. + ///
event Action? RebuildRecommend; /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs index 795ca61fc..93c688608 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -5,9 +5,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// internal interface IFontHandleManager : IDisposable { - /// - /// Event fired when a font rebuild operation is suggested. - /// + /// event Action? RebuildRecommend; /// From 912cf991dc4741d4ad9ce68a4344c2b6237469a3 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 12 Jan 2024 12:27:28 +0900 Subject: [PATCH 420/585] remove/internalize unused --- Dalamud/Interface/Utility/ImGuiHelpers.cs | 44 ++++++++--------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index e3b0ff8d1..444463d41 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -314,6 +314,7 @@ public static class ImGuiHelpers glyph->U1, glyph->V1, glyph->AdvanceX * scale); + target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint); changed = true; } else if (!missingOnly) @@ -415,6 +416,8 @@ public static class ImGuiHelpers /// If returns null. public static unsafe void* AllocateMemory(int length) { + // TODO: igMemAlloc takes size_t, which is nint; ImGui.NET apparently interpreted that as uint. + // fix that in ImGui.NET. switch (length) { case 0: @@ -436,35 +439,6 @@ public static class ImGuiHelpers } } - /// - /// Mark 4K page as used, after adding a codepoint to a font. - /// - /// The font. - /// The codepoint. - public static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) - { - // Mark 4K page as used - var pageIndex = unchecked((ushort)(codepoint / 4096)); - font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); - } - - /// - /// Creates a new instance of with a natively backed memory. - /// - /// The created instance. - /// Disposable you can call. - public static unsafe IDisposable NewFontAtlasPtrScoped(out ImFontAtlasPtr font) - { - font = new(ImGuiNative.ImFontAtlas_ImFontAtlas()); - var ptr = font.NativePtr; - return Disposable.Create(() => - { - if (ptr != null) - ImGuiNative.ImFontAtlas_destroy(ptr); - ptr = null; - }); - } - /// /// Creates a new instance of with a natively backed memory. /// @@ -557,6 +531,18 @@ public static class ImGuiHelpers /// if it is not default; otherwise, . public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => self.NativePtr is null ? other : self; + + /// + /// Mark 4K page as used, after adding a codepoint to a font. + /// + /// The font. + /// The codepoint. + internal static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) + { + // Mark 4K page as used + var pageIndex = unchecked((ushort)(codepoint / 4096)); + font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); + } /// /// Finds the corresponding ImGui viewport ID for the given window handle. From 3c7900ea13964e7fd45ccb92b384c2a2e467cbd3 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 13 Jan 2024 10:17:34 -0800 Subject: [PATCH 421/585] Add API Compatibility Checks (#1603) * Add GitHub Action job to check for API compatibility Runs critical path DLLs through `apicompat` tool against currently-live stg build to see what is broken. * Revert CS changes for GH Action positive test-case --- .github/workflows/main.yml | 43 +++++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7ada48e50..8a4fdf2e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,48 @@ jobs: with: name: dalamud-artifact path: bin\Release - + + check_api_compat: + name: "Check API Compatibility" + if: ${{ github.event_name == 'pull_request' }} + needs: build + runs-on: windows-latest + steps: + - name: "Install .NET SDK" + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7 + - name: "Install ApiCompat" + run: | + dotnet tool install -g Microsoft.DotNet.ApiCompat.Tool + - name: "Download Proposed Artifacts" + uses: actions/download-artifact@v2 + with: + name: dalamud-artifact + path: .\right + - name: "Download Live (Stg) Artifacts" + run: | + Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip + Expand-Archive -Force latest.zip "left" + - name: "Verify Compatibility" + run: | + $FILES_TO_VALIDATE = "Dalamud.dll","FFXIVClientStructs.dll","Lumina.dll","Lumina.Excel.dll" + + $retcode = 0 + + foreach ($file in $FILES_TO_VALIDATE) { + $testout = "" + Write-Output "::group::=== API COMPATIBILITY CHECK: ${file} ===" + apicompat -l "left\${file}" -r "right\${file}" | Tee-Object -Variable testout + Write-Output "::endgroup::" + if ($testout -ne "APICompat ran successfully without finding any breaking changes.") { + Write-Output "::error::${file} did not pass. Please review it for problems." + $retcode = 1 + } + } + + exit $retcode + deploy_stg: name: Deploy dalamud-distrib staging if: ${{ github.repository_owner == 'goatcorp' && github.event_name == 'push' }} From 86b7c29e9445b2bb41abf1ccc2ddc2f3003a2f7a Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 13 Jan 2024 11:17:26 -0800 Subject: [PATCH 422/585] fix: Make auto-update work again, the lazy way (#1592) * fix: Make auto-update work again, the lazy way. - Move auto-update to run on the first `Notice` message for parity with the welcome message. - Add some logging in a few critical places to make things nicer. * fix overzealous IDE complaints * code-review comments - Remove stray imports that the IDE included - Remove fixme to move auto-updates (for now) * Lazy retry auto-update --- Dalamud/Game/ChatHandlers.cs | 45 ++++++++++++++++++----- Dalamud/Interface/Utility/ImGuiHelpers.cs | 3 +- Dalamud/Plugin/Internal/PluginManager.cs | 8 +++- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 90a399d4c..836fb5ec8 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using CheapLoc; @@ -14,9 +15,9 @@ using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.Internal.Windows.PluginInstaller; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; -using Serilog; namespace Dalamud.Game; @@ -60,6 +61,8 @@ internal class ChatHandlers : IServiceType // { XivChatType.Echo, Color.Gray }, // }; + private static readonly ModuleLog Log = new("CHATHANDLER"); + private readonly Regex rmtRegex = new( @"4KGOLD|We have sufficient stock|VPK\.OM|[Gg]il for free|[Gg]il [Cc]heap|5GOLD|www\.so9\.com|Fast & Convenient|Cheap & Safety Guarantee|【Code|A O A U E|igfans|4KGOLD\.COM|Cheapest Gil with|pvp and bank on google|Selling Cheap GIL|ff14mogstation\.com|Cheap Gil 1000k|gilsforyou|server 1000K =|gils_selling|E A S Y\.C O M|bonus code|mins delivery guarantee|Sell cheap|Salegm\.com|cheap Mog|Off Code:|FF14Mog.com|使用する5%オ|[Oo][Ff][Ff] [Cc]ode( *)[:;]|offers Fantasia", RegexOptions.Compiled | RegexOptions.IgnoreCase); @@ -110,6 +113,7 @@ internal class ChatHandlers : IServiceType private bool hasSeenLoadingMsg; private bool startedAutoUpdatingPlugins; + private CancellationTokenSource deferredAutoUpdateCts = new(); [ServiceManager.ServiceConstructor] private ChatHandlers(ChatGui chatGui) @@ -165,16 +169,19 @@ internal class ChatHandlers : IServiceType if (clientState == null) return; - if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) - this.PrintWelcomeMessage(); + if (type == XivChatType.Notice) + { + if (!this.hasSeenLoadingMsg) + this.PrintWelcomeMessage(); + + if (!this.startedAutoUpdatingPlugins) + this.AutoUpdatePluginsWithRetry(); + } // For injections while logged in if (clientState.LocalPlayer != null && clientState.TerritoryType == 0 && !this.hasSeenLoadingMsg) this.PrintWelcomeMessage(); - if (!this.startedAutoUpdatingPlugins) - this.AutoUpdatePlugins(); - #if !DEBUG && false if (!this.hasSeenLoadingMsg) return; @@ -264,24 +271,42 @@ internal class ChatHandlers : IServiceType this.hasSeenLoadingMsg = true; } - private void AutoUpdatePlugins() + private void AutoUpdatePluginsWithRetry() + { + var firstAttempt = this.AutoUpdatePlugins(); + if (!firstAttempt) + { + Task.Run(() => + { + Task.Delay(30_000, this.deferredAutoUpdateCts.Token); + this.AutoUpdatePlugins(); + }); + } + } + + private bool AutoUpdatePlugins() { var chatGui = Service.GetNullable(); var pluginManager = Service.GetNullable(); var notifications = Service.GetNullable(); if (chatGui == null || pluginManager == null || notifications == null) - return; + { + Log.Warning("Aborting auto-update because a required service was not loaded."); + return false; + } if (!pluginManager.ReposReady || !pluginManager.InstalledPlugins.Any() || !pluginManager.AvailablePlugins.Any()) { // Plugins aren't ready yet. // TODO: We should retry. This sucks, because it means we won't ever get here again until another notice. - return; + Log.Warning("Aborting auto-update because plugins weren't loaded or ready."); + return false; } this.startedAutoUpdatingPlugins = true; + Log.Debug("Beginning plugin auto-update process..."); Task.Run(() => pluginManager.UpdatePluginsAsync(true, !this.configuration.AutoUpdatePlugins, true)).ContinueWith(task => { this.IsAutoUpdateComplete = true; @@ -320,5 +345,7 @@ internal class ChatHandlers : IServiceType } } }); + + return true; } } diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 85f81b203..ad151ec4e 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -31,7 +31,8 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary + && ImGui.GetIO().NativePtr is not null; /// /// Gets the global Dalamud scale; even available before drawing is ready.
diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 0ef3d49f8..020abf437 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -958,7 +958,7 @@ internal partial class PluginManager : IDisposable, IServiceType autoUpdate ? PluginListInvalidationKind.AutoUpdate : PluginListInvalidationKind.Update, updatedList.Select(x => x.InternalName)); - Log.Information("Plugin update OK."); + Log.Information("Plugin update OK. {updateCount} plugins updated.", updatedList.Length); return updatedList; } @@ -1581,6 +1581,8 @@ internal partial class PluginManager : IDisposable, IServiceType private void DetectAvailablePluginUpdates() { + Log.Debug("Starting plugin update check..."); + lock (this.pluginListLock) { this.updatablePluginsList.Clear(); @@ -1615,10 +1617,12 @@ internal partial class PluginManager : IDisposable, IServiceType } } } + + Log.Debug("Update check found {updateCount} available updates.", this.updatablePluginsList.Count); } private void NotifyAvailablePluginsChanged() - { + { this.DetectAvailablePluginUpdates(); this.OnAvailablePluginsChanged?.InvokeSafely(); From 96ef5e18cd7044d1661c74040eb7708f8874d245 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 13 Jan 2024 20:17:57 +0100 Subject: [PATCH 423/585] build: 9.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 5844527ee..6b2f1300a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.15 + 9.0.0.16 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From a71b81c82e88f093cbde282f35629472d0567a7e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Jan 2024 18:36:46 +0000 Subject: [PATCH 424/585] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 837c6fafc..89e713c07 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 837c6fafc13fbdda3e13a833b6085e4ce93d19e1 +Subproject commit 89e713c071dae13112550d3e754193704e230b03 From 7d7ab4bc8b488ecb629256d0f7a8804aa7821f90 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 15 Jan 2024 12:11:05 +0000 Subject: [PATCH 425/585] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 89e713c07..0ca14a0a0 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 89e713c071dae13112550d3e754193704e230b03 +Subproject commit 0ca14a0a047d3df403fd9ed1fee7a43de55d1c66 From d71da3f2c0f6eb6caa969fcf13aff2101157cff7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Jan 2024 15:12:45 +0000 Subject: [PATCH 426/585] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 0ca14a0a0..bbc4b9942 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 0ca14a0a047d3df403fd9ed1fee7a43de55d1c66 +Subproject commit bbc4b994254d6913f51da3a20fad9bf4b8c986e5 From 59278224f71a8bf18f7e34c7a4e2521aadf8a12d Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 16 Jan 2024 20:00:37 +0100 Subject: [PATCH 427/585] build: 9.0.0.17 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 6b2f1300a..ba044a555 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.16 + 9.0.0.17 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From a1a96d762acf741d248203198b62e043536d461d Mon Sep 17 00:00:00 2001 From: Infi Date: Wed, 17 Jan 2024 19:25:54 +0100 Subject: [PATCH 428/585] Fix swapped U and V check --- Dalamud/Interface/UldWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs index 127ea85ec..dd8986bed 100644 --- a/Dalamud/Interface/UldWrapper.cs +++ b/Dalamud/Interface/UldWrapper.cs @@ -107,7 +107,7 @@ public class UldWrapper : IDisposable private IDalamudTextureWrap? CopyRect(int width, int height, byte[] rgbaData, UldRoot.PartData part) { - if (part.V + part.W > width || part.U + part.H > height) + if (part.U + part.W > width || part.V + part.H > height) { return null; } From 14c5ad1605ef35e138d000c128f379630841a6f4 Mon Sep 17 00:00:00 2001 From: Kurochi51 Date: Thu, 18 Jan 2024 21:56:12 +0200 Subject: [PATCH 429/585] Expose `CharacterData.ShieldValue` to Dalamud's Character wrapper. (#1608) --- Dalamud/Game/ClientState/Objects/Types/Character.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index a1eb52edc..ac11bcdd0 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -61,6 +61,11 @@ public unsafe class Character : GameObject ///
public uint MaxCp => this.Struct->CharacterData.MaxCraftingPoints; + /// + /// Gets the shield percentage of this Chara. + /// + public byte ShieldPercentage => this.Struct->CharacterData.ShieldValue; + /// /// Gets the ClassJob of this Chara. /// From b5696afe94b9ace8c58323a751b5bb88cae9cece Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Thu, 18 Jan 2024 21:37:05 +0100 Subject: [PATCH 430/585] Revert "IFontAtlas: font atlas per plugin" --- .../Internal/DalamudConfiguration.cs | 7 +- Dalamud/Interface/GameFonts/FdtFileView.cs | 159 -- .../GameFonts/GameFontFamilyAndSize.cs | 25 +- .../GameFontFamilyAndSizeAttribute.cs | 37 - Dalamud/Interface/GameFonts/GameFontHandle.cs | 85 +- .../Interface/GameFonts/GameFontManager.cs | 507 ++++++ Dalamud/Interface/GameFonts/GameFontStyle.cs | 37 +- Dalamud/Interface/Internal/DalamudIme.cs | 7 +- .../Interface/Internal/DalamudInterface.cs | 13 +- .../Interface/Internal/InterfaceManager.cs | 960 +++++++++--- .../Internal/Windows/ChangelogWindow.cs | 62 +- .../Internal/Windows/Data/DataWindow.cs | 8 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 213 --- .../Windows/Settings/SettingsWindow.cs | 29 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 30 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 50 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 63 +- .../FontAtlasAutoRebuildMode.cs | 22 - .../ManagedFontAtlas/FontAtlasBuildStep.cs | 38 - .../FontAtlasBuildStepDelegate.cs | 15 - .../FontAtlasBuildToolkitUtilities.cs | 133 -- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 141 -- .../IFontAtlasBuildToolkit.cs | 67 - .../IFontAtlasBuildToolkitPostBuild.cs | 26 - .../IFontAtlasBuildToolkitPostPromotion.cs | 33 - .../IFontAtlasBuildToolkitPreBuild.cs | 186 --- .../Interface/ManagedFontAtlas/IFontHandle.cs | 42 - .../Internals/DelegateFontHandle.cs | 334 ---- .../FontAtlasFactory.BuildToolkit.cs | 682 -------- .../FontAtlasFactory.Implementation.cs | 726 --------- .../Internals/FontAtlasFactory.cs | 368 ----- .../Internals/GamePrebakedFontHandle.cs | 857 ---------- .../Internals/IFontHandleManager.cs | 32 - .../Internals/IFontHandleSubstance.cs | 54 - .../Internals/TrueType.Common.cs | 203 --- .../Internals/TrueType.Enums.cs | 84 - .../Internals/TrueType.Files.cs | 148 -- .../Internals/TrueType.GposGsub.cs | 259 --- .../Internals/TrueType.PointerSpan.cs | 443 ------ .../Internals/TrueType.Tables.cs | 1391 ----------------- .../ManagedFontAtlas/Internals/TrueType.cs | 135 -- .../ManagedFontAtlas/SafeFontConfig.cs | 306 ---- Dalamud/Interface/UiBuilder.cs | 182 +-- Dalamud/Interface/Utility/ImGuiHelpers.cs | 243 +-- 44 files changed, 1499 insertions(+), 7943 deletions(-) delete mode 100644 Dalamud/Interface/GameFonts/FdtFileView.cs delete mode 100644 Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs create mode 100644 Dalamud/Interface/GameFonts/GameFontManager.cs delete mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 66c2745c5..76c8f3603 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -148,9 +148,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable public bool UseAxisFontsFromGame { get; set; } = false; /// - /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. + /// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness. + /// + /// Before gamma is applied... + /// * ...TTF fonts loaded with stb or FreeType are in linear space. + /// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4. /// - [Obsolete("It happens that nobody touched this setting", true)] public float FontGammaLevel { get; set; } = 1.4f; /// diff --git a/Dalamud/Interface/GameFonts/FdtFileView.cs b/Dalamud/Interface/GameFonts/FdtFileView.cs deleted file mode 100644 index 896a6dbb4..000000000 --- a/Dalamud/Interface/GameFonts/FdtFileView.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System.Collections.Generic; -using System.IO; - -namespace Dalamud.Interface.GameFonts; - -/// -/// Reference member view of a .fdt file data. -/// -internal readonly unsafe struct FdtFileView -{ - private readonly byte* ptr; - - /// - /// Initializes a new instance of the struct. - /// - /// Pointer to the data. - /// Length of the data. - public FdtFileView(void* ptr, int length) - { - this.ptr = (byte*)ptr; - if (length < sizeof(FdtReader.FdtHeader)) - throw new InvalidDataException("Not enough space for a FdtHeader"); - - if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader)) - throw new InvalidDataException("Not enough space for a FontTableHeader"); - if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader) + - (sizeof(FdtReader.FontTableEntry) * this.FontHeader.FontTableEntryCount)) - throw new InvalidDataException("Not enough space for all the FontTableEntry"); - - if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader)) - throw new InvalidDataException("Not enough space for a KerningTableHeader"); - if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader) + - (sizeof(FdtReader.KerningTableEntry) * this.KerningEntryCount)) - throw new InvalidDataException("Not enough space for all the KerningTableEntry"); - } - - /// - /// Gets the file header. - /// - public ref FdtReader.FdtHeader FileHeader => ref *(FdtReader.FdtHeader*)this.ptr; - - /// - /// Gets the font header. - /// - public ref FdtReader.FontTableHeader FontHeader => - ref *(FdtReader.FontTableHeader*)((nint)this.ptr + this.FileHeader.FontTableHeaderOffset); - - /// - /// Gets the glyphs. - /// - public Span Glyphs => new(this.GlyphsUnsafe, this.FontHeader.FontTableEntryCount); - - /// - /// Gets the kerning header. - /// - public ref FdtReader.KerningTableHeader KerningHeader => - ref *(FdtReader.KerningTableHeader*)((nint)this.ptr + this.FileHeader.KerningTableHeaderOffset); - - /// - /// Gets the number of kerning entries. - /// - public int KerningEntryCount => Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); - - /// - /// Gets the kerning entries. - /// - public Span PairAdjustments => new( - this.ptr + this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader), - this.KerningEntryCount); - - /// - /// Gets the maximum texture index. - /// - public int MaxTextureIndex - { - get - { - var i = 0; - foreach (ref var g in this.Glyphs) - { - if (g.TextureIndex > i) - i = g.TextureIndex; - } - - return i; - } - } - - private FdtReader.FontTableEntry* GlyphsUnsafe => - (FdtReader.FontTableEntry*)(this.ptr + this.FileHeader.FontTableHeaderOffset + - sizeof(FdtReader.FontTableHeader)); - - /// - /// Finds the glyph index for the corresponding codepoint. - /// - /// Unicode codepoint (UTF-32 value). - /// Corresponding index, or a negative number according to . - public int FindGlyphIndex(int codepoint) - { - var comp = FdtReader.CodePointToUtf8Int32(codepoint); - - var glyphs = this.GlyphsUnsafe; - var lo = 0; - var hi = this.FontHeader.FontTableEntryCount - 1; - while (lo <= hi) - { - var i = (int)(((uint)hi + (uint)lo) >> 1); - switch (comp.CompareTo(glyphs[i].CharUtf8)) - { - case 0: - return i; - case > 0: - lo = i + 1; - break; - default: - hi = i - 1; - break; - } - } - - return ~lo; - } - - /// - /// Create a glyph range for use with . - /// - /// Merge two ranges into one if distance is below the value specified in this parameter. - /// Glyph ranges. - public ushort[] ToGlyphRanges(int mergeDistance = 8) - { - var glyphs = this.Glyphs; - var ranges = new List(glyphs.Length) - { - checked((ushort)glyphs[0].CharInt), - checked((ushort)glyphs[0].CharInt), - }; - - foreach (ref var glyph in glyphs[1..]) - { - var c32 = glyph.CharInt; - if (c32 >= 0x10000) - break; - - var c16 = unchecked((ushort)c32); - if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) - { - ranges[^1] = c16; - } - else if (ranges[^1] + 1 < c16) - { - ranges.Add(c16); - ranges.Add(c16); - } - } - - ranges.Add(0); - return ranges.ToArray(); - } -} diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs index 6e66cf19b..dd78baf87 100644 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Enum of available game fonts in specific sizes. /// -public enum GameFontFamilyAndSize +public enum GameFontFamilyAndSize : int { /// /// Placeholder meaning unused. @@ -15,7 +15,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_96.fdt", "common/font/font{0}.tex", -1)] Axis96, /// @@ -23,7 +22,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_12.fdt", "common/font/font{0}.tex", -1)] Axis12, /// @@ -31,7 +29,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_14.fdt", "common/font/font{0}.tex", -1)] Axis14, /// @@ -39,7 +36,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_18.fdt", "common/font/font{0}.tex", -1)] Axis18, /// @@ -47,7 +43,6 @@ public enum GameFontFamilyAndSize /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// - [GameFontFamilyAndSize("common/font/AXIS_36.fdt", "common/font/font{0}.tex", -4)] Axis36, /// @@ -55,7 +50,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// - [GameFontFamilyAndSize("common/font/Jupiter_16.fdt", "common/font/font{0}.tex", -1)] Jupiter16, /// @@ -63,7 +57,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// - [GameFontFamilyAndSize("common/font/Jupiter_20.fdt", "common/font/font{0}.tex", -1)] Jupiter20, /// @@ -71,7 +64,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// - [GameFontFamilyAndSize("common/font/Jupiter_23.fdt", "common/font/font{0}.tex", -1)] Jupiter23, /// @@ -79,7 +71,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// - [GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)] Jupiter45, /// @@ -87,7 +78,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// - [GameFontFamilyAndSize("common/font/Jupiter_46.fdt", "common/font/font{0}.tex", -2)] Jupiter46, /// @@ -95,7 +85,6 @@ public enum GameFontFamilyAndSize /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// - [GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)] Jupiter90, /// @@ -103,7 +92,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// - [GameFontFamilyAndSize("common/font/Meidinger_16.fdt", "common/font/font{0}.tex", -1)] Meidinger16, /// @@ -111,7 +99,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// - [GameFontFamilyAndSize("common/font/Meidinger_20.fdt", "common/font/font{0}.tex", -1)] Meidinger20, /// @@ -119,7 +106,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// - [GameFontFamilyAndSize("common/font/Meidinger_40.fdt", "common/font/font{0}.tex", -4)] Meidinger40, /// @@ -127,7 +113,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)] MiedingerMid10, /// @@ -135,7 +120,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)] MiedingerMid12, /// @@ -143,7 +127,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)] MiedingerMid14, /// @@ -151,7 +134,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)] MiedingerMid18, /// @@ -159,7 +141,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally wide. Contains mostly ASCII range. /// - [GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)] MiedingerMid36, /// @@ -167,7 +148,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)] TrumpGothic184, /// @@ -175,7 +155,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)] TrumpGothic23, /// @@ -183,7 +162,6 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)] TrumpGothic34, /// @@ -191,6 +169,5 @@ public enum GameFontFamilyAndSize /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// - [GameFontFamilyAndSize("common/font/TrumpGothic_68.fdt", "common/font/font{0}.tex", -3)] TrumpGothic68, } diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs deleted file mode 100644 index f5260e4bc..000000000 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Dalamud.Interface.GameFonts; - -/// -/// Marks the path for an enum value. -/// -[AttributeUsage(AttributeTargets.Field)] -internal class GameFontFamilyAndSizeAttribute : Attribute -{ - /// - /// Initializes a new instance of the class. - /// - /// Inner path of the file. - /// the file path format for the relevant .tex files. - /// Horizontal offset of the corresponding font. - public GameFontFamilyAndSizeAttribute(string path, string texPathFormat, int horizontalOffset) - { - this.Path = path; - this.TexPathFormat = texPathFormat; - this.HorizontalOffset = horizontalOffset; - } - - /// - /// Gets the path. - /// - public string Path { get; } - - /// - /// Gets the file path format for the relevant .tex files.
- /// Used for (, ). - ///
- public string TexPathFormat { get; } - - /// - /// Gets the horizontal offset of the corresponding font. - /// - public int HorizontalOffset { get; } -} diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 77461aa0a..d71e725c5 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,76 +1,75 @@ +using System; using System.Numerics; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; - using ImGuiNET; namespace Dalamud.Interface.GameFonts; /// -/// ABI-compatible wrapper for . +/// Prepare and keep game font loaded for use in OnDraw. /// -public sealed class GameFontHandle : IFontHandle +public class GameFontHandle : IDisposable { - private readonly IFontHandle.IInternal fontHandle; - private readonly FontAtlasFactory fontAtlasFactory; + private readonly GameFontManager manager; + private readonly GameFontStyle fontStyle; /// /// Initializes a new instance of the class. /// - /// The wrapped . - /// An instance of . - internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) + /// GameFontManager instance. + /// Font to use. + internal GameFontHandle(GameFontManager manager, GameFontStyle font) { - this.fontHandle = fontHandle; - this.fontAtlasFactory = fontAtlasFactory; + this.manager = manager; + this.fontStyle = font; } - /// - public Exception? LoadException => this.fontHandle.LoadException; - - /// - public bool Available => this.fontHandle.Available; - - /// - [Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)] - public ImFontPtr ImFont => this.fontHandle.ImFont; - /// - /// Gets the font style. Only applicable for . + /// Gets the font style. /// - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] - public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle; + public GameFontStyle Style => this.fontStyle; /// - /// Gets the relevant .
- ///
- /// Only applicable for game fonts. Otherwise it will throw. + /// Gets a value indicating whether this font is ready for use. ///
- [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] - public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!; - - /// - public void Dispose() => this.fontHandle.Dispose(); - - /// - public IDisposable Push() => this.fontHandle.Push(); + public bool Available + { + get + { + unsafe + { + return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null; + } + } + } /// - /// Creates a new .
- ///
- /// Only applicable for game fonts. Otherwise it will throw. + /// Gets the font. + ///
+ public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + + /// + /// Gets the FdtReader. + /// + public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); + + /// + /// Creates a new GameFontLayoutPlan.Builder. /// /// Text. /// A new builder for GameFontLayoutPlan. - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] - public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text); + public GameFontLayoutPlan.Builder LayoutBuilder(string text) + { + return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); + } + + /// + public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); /// /// Draws text. /// /// Text to draw. - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void Text(string text) { if (!this.Available) @@ -94,7 +93,6 @@ public sealed class GameFontHandle : IFontHandle ///
/// Color. /// Text to draw. - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextColored(Vector4 col, string text) { ImGui.PushStyleColor(ImGuiCol.Text, col); @@ -106,7 +104,6 @@ public sealed class GameFontHandle : IFontHandle /// Draws disabled text. ///
/// Text to draw. - [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextDisabled(string text) { unsafe diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs new file mode 100644 index 000000000..b3454e085 --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Utility.Timing; +using ImGuiNET; +using Lumina.Data.Files; +using Serilog; + +using static Dalamud.Interface.Utility.ImGuiHelpers; + +namespace Dalamud.Interface.GameFonts; + +/// +/// Loads game font for use in ImGui. +/// +[ServiceManager.BlockingEarlyLoadedService] +internal class GameFontManager : IServiceType +{ + private static readonly string?[] FontNames = + { + null, + "AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36", + "Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90", + "Meidinger_16", "Meidinger_20", "Meidinger_40", + "MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36", + "TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68", + }; + + private readonly object syncRoot = new(); + + private readonly FdtReader?[] fdts; + private readonly List texturePixels; + private readonly Dictionary fonts = new(); + private readonly Dictionary fontUseCounter = new(); + private readonly Dictionary>> glyphRectIds = new(); + +#pragma warning disable CS0414 + private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; +#pragma warning restore CS0414 + + [ServiceManager.ServiceConstructor] + private GameFontManager(DataManager dataManager) + { + using (Timings.Start("Getting fdt data")) + { + this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray(); + } + + using (Timings.Start("Getting texture data")) + { + var texTasks = Enumerable + .Range(1, 1 + this.fdts + .Where(x => x != null) + .Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max()) + .Max()) + .Select(x => dataManager.GetFile($"common/font/font{x}.tex")!) + .Select(x => new Task(Timings.AttachTimingHandle(() => x.ImageData!))) + .ToArray(); + foreach (var task in texTasks) + task.Start(); + this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList(); + } + } + + /// + /// Describe font into a string. + /// + /// Font to describe. + /// A string in a form of "FontName (NNNpt)". + public static string DescribeFont(GameFontFamilyAndSize font) + { + return font switch + { + GameFontFamilyAndSize.Undefined => "-", + GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)", + GameFontFamilyAndSize.Axis12 => "AXIS (12pt)", + GameFontFamilyAndSize.Axis14 => "AXIS (14pt)", + GameFontFamilyAndSize.Axis18 => "AXIS (18pt)", + GameFontFamilyAndSize.Axis36 => "AXIS (36pt)", + GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)", + GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)", + GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)", + GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)", + GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)", + GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)", + GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)", + GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)", + GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)", + GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)", + GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)", + GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)", + GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)", + GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)", + GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)", + GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)", + GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)", + GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)", + _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), + }; + } + + /// + /// Determines whether a font should be able to display most of stuff. + /// + /// Font to check. + /// True if it can. + public static bool IsGenericPurposeFont(GameFontFamilyAndSize font) + { + return font switch + { + GameFontFamilyAndSize.Axis96 => true, + GameFontFamilyAndSize.Axis12 => true, + GameFontFamilyAndSize.Axis14 => true, + GameFontFamilyAndSize.Axis18 => true, + GameFontFamilyAndSize.Axis36 => true, + _ => false, + }; + } + + /// + /// Unscales fonts after they have been rendered onto atlas. + /// + /// Font to unscale. + /// Scale factor. + /// Whether to call target.BuildLookupTable(). + public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true) + { + if (fontScale == 1) + return; + + unsafe + { + var font = fontPtr.NativePtr; + for (int i = 0, i_ = font->IndexedHotData.Size; i < i_; ++i) + { + font->IndexedHotData.Ref(i).AdvanceX /= fontScale; + font->IndexedHotData.Ref(i).OccupiedWidth /= fontScale; + } + + font->FontSize /= fontScale; + font->Ascent /= fontScale; + font->Descent /= fontScale; + if (font->ConfigData != null) + font->ConfigData->SizePixels /= fontScale; + var glyphs = (ImFontGlyphReal*)font->Glyphs.Data; + for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) + { + var glyph = &glyphs[i]; + glyph->X0 /= fontScale; + glyph->X1 /= fontScale; + glyph->Y0 /= fontScale; + glyph->Y1 /= fontScale; + glyph->AdvanceX /= fontScale; + } + + for (int i = 0, i_ = font->KerningPairs.Size; i < i_; i++) + font->KerningPairs.Ref(i).AdvanceXAdjustment /= fontScale; + for (int i = 0, i_ = font->FrequentKerningPairs.Size; i < i_; i++) + font->FrequentKerningPairs.Ref(i) /= fontScale; + } + + if (rebuildLookupTable && fontPtr.Glyphs.Size > 0) + fontPtr.BuildLookupTableNonstandard(); + } + + /// + /// Create a glyph range for use with ImGui AddFont. + /// + /// Font family and size. + /// Merge two ranges into one if distance is below the value specified in this parameter. + /// Glyph ranges. + public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8) + { + var fdt = this.fdts[(int)family]!; + var ranges = new List(fdt.Glyphs.Count) + { + checked((ushort)fdt.Glyphs[0].CharInt), + checked((ushort)fdt.Glyphs[0].CharInt), + }; + + foreach (var glyph in fdt.Glyphs.Skip(1)) + { + var c32 = glyph.CharInt; + if (c32 >= 0x10000) + break; + + var c16 = unchecked((ushort)c32); + if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) + { + ranges[^1] = c16; + } + else if (ranges[^1] + 1 < c16) + { + ranges.Add(c16); + ranges.Add(c16); + } + } + + return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned); + } + + /// + /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. + /// + /// Font to use. + /// Handle to game font that may or may not be ready yet. + public GameFontHandle NewFontRef(GameFontStyle style) + { + var interfaceManager = Service.Get(); + var needRebuild = false; + + lock (this.syncRoot) + { + this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1; + } + + needRebuild = !this.fonts.ContainsKey(style); + if (needRebuild) + { + Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString()); + Service.GetAsync() + .ContinueWith(task => task.Result.RunOnTick(() => interfaceManager.RebuildFonts())); + } + + return new(this, style); + } + + /// + /// Gets the font. + /// + /// Font to get. + /// Corresponding font or null. + public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null); + + /// + /// Gets the corresponding FdtReader. + /// + /// Font to get. + /// Corresponding FdtReader or null. + public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family]; + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) + { + ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) + { + ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); + } + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) + { + ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); + } + + /// + /// Build fonts before plugins do something more. To be called from InterfaceManager. + /// + public void BuildFonts() + { + this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true; + + this.glyphRectIds.Clear(); + this.fonts.Clear(); + + lock (this.syncRoot) + { + foreach (var style in this.fontUseCounter.Keys) + this.EnsureFont(style); + } + } + + /// + /// Record that ImGui.GetIO().Fonts.Build() has been called. + /// + public void AfterIoFontsBuild() + { + this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; + } + + /// + /// Checks whether GameFontMamager owns an ImFont. + /// + /// ImFontPtr to check. + /// Whether it owns. + public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr); + + /// + /// Post-build fonts before plugins do something more. To be called from InterfaceManager. + /// + public unsafe void AfterBuildFonts() + { + var interfaceManager = Service.Get(); + var ioFonts = ImGui.GetIO().Fonts; + var fontGamma = interfaceManager.FontGamma; + + var pixels8s = new byte*[ioFonts.Textures.Size]; + var pixels32s = new uint*[ioFonts.Textures.Size]; + var widths = new int[ioFonts.Textures.Size]; + var heights = new int[ioFonts.Textures.Size]; + for (var i = 0; i < pixels8s.Length; i++) + { + ioFonts.GetTexDataAsRGBA32(i, out pixels8s[i], out widths[i], out heights[i]); + pixels32s[i] = (uint*)pixels8s[i]; + } + + foreach (var (style, font) in this.fonts) + { + var fdt = this.fdts[(int)style.FamilyAndSize]; + var scale = style.SizePt / fdt.FontHeader.Size; + var fontPtr = font.NativePtr; + + Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale); + + fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; + if (fontPtr->ConfigData != null) + fontPtr->ConfigData->SizePixels = fontPtr->FontSize; + fontPtr->Ascent = fdt.FontHeader.Ascent; + fontPtr->Descent = fdt.FontHeader.Descent; + fontPtr->EllipsisChar = '…'; + foreach (var fallbackCharCandidate in "〓?!") + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); + if ((IntPtr)glyph.NativePtr != IntPtr.Zero) + { + var ptr = font.NativePtr; + ptr->FallbackChar = fallbackCharCandidate; + ptr->FallbackGlyph = glyph.NativePtr; + ptr->FallbackHotData = (ImFontGlyphHotData*)ptr->IndexedHotData.Address(fallbackCharCandidate); + break; + } + } + + // I have no idea what's causing NPE, so just to be safe + try + { + if (font.NativePtr != null && font.NativePtr->ConfigData != null) + { + var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0"); + Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); + } + } + catch (NullReferenceException) + { + // do nothing + } + + foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) + { + var rc = (ImFontAtlasCustomRectReal*)ioFonts.GetCustomRectByIndex(rectId).NativePtr; + var pixels8 = pixels8s[rc->TextureIndex]; + var pixels32 = pixels32s[rc->TextureIndex]; + var width = widths[rc->TextureIndex]; + var height = heights[rc->TextureIndex]; + var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; + var sourceBufferDelta = glyph.TextureChannelByteIndex; + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); + if (widthAdjustment == 0) + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; + pixels32[((rc->Y + y) * width) + rc->X + x] = (uint)(a << 24) | 0xFFFFFFu; + } + } + } + else + { + for (var y = 0; y < glyph.BoundingHeight; y++) + { + for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) + pixels32[((rc->Y + y) * width) + rc->X + x] = 0xFFFFFFu; + } + + for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++) + { + var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); + for (var y = 0; y < glyph.BoundingHeight; y++) + { + float xDelta = xbold; + if (style.BaseSkewStrength > 0) + xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; + else if (style.BaseSkewStrength < 0) + xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; + var xDeltaInt = (int)Math.Floor(xDelta); + var xness = xDelta - xDeltaInt; + for (var x = 0; x < glyph.BoundingWidth; x++) + { + var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x; + var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; + var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))]; + var n = (a1 * xness) + (a2 * (1 - xness)); + var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; + pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n)); + } + } + } + } + + if (Math.Abs(fontGamma - 1.4f) >= 0.001) + { + // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) + for (int y = rc->Y, y_ = rc->Y + rc->Height; y < y_; y++) + { + for (int x = rc->X, x_ = rc->X + rc->Width; x < x_; x++) + { + var i = (((y * width) + x) * 4) + 3; + pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); + } + } + } + } + + UnscaleFont(font, 1 / scale, false); + } + } + + /// + /// Decrease font reference counter. + /// + /// Font to release. + internal void DecreaseFontRef(GameFontStyle style) + { + lock (this.syncRoot) + { + if (!this.fontUseCounter.ContainsKey(style)) + return; + + if ((this.fontUseCounter[style] -= 1) == 0) + this.fontUseCounter.Remove(style); + } + } + + private unsafe void EnsureFont(GameFontStyle style) + { + var rectIds = this.glyphRectIds[style] = new(); + + var fdt = this.fdts[(int)style.FamilyAndSize]; + if (fdt == null) + return; + + ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); + fontConfig.OversampleH = 1; + fontConfig.OversampleV = 1; + fontConfig.PixelSnapH = false; + + var io = ImGui.GetIO(); + var font = io.Fonts.AddFontDefault(fontConfig); + + fontConfig.Destroy(); + + this.fonts[style] = font; + foreach (var glyph in fdt.Glyphs) + { + var c = glyph.Char; + if (c < 32 || c >= 0xFFFF) + continue; + + var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); + rectIds[c] = Tuple.Create( + io.Fonts.AddCustomRectFontGlyph( + font, + c, + glyph.BoundingWidth + widthAdjustment, + glyph.BoundingHeight, + glyph.AdvanceWidth, + new Vector2(0, glyph.CurrentOffsetY)), + glyph); + } + + foreach (var kernPair in fdt.Distances) + font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index fbaf9de07..946473df4 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -64,7 +64,7 @@ public struct GameFontStyle ///
public float SizePt { - readonly get => this.SizePx * 3 / 4; + get => this.SizePx * 3 / 4; set => this.SizePx = value * 4 / 3; } @@ -73,14 +73,14 @@ public struct GameFontStyle ///
public float BaseSkewStrength { - readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx; + get => this.SkewStrength * this.BaseSizePx / this.SizePx; set => this.SkewStrength = value * this.SizePx / this.BaseSizePx; } /// /// Gets the font family. /// - public readonly GameFontFamily Family => this.FamilyAndSize switch + public GameFontFamily Family => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined, GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis, @@ -112,7 +112,7 @@ public struct GameFontStyle /// /// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes. /// - public readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch + public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch { GameFontFamily.Axis => GameFontFamilyAndSize.Axis96, GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16, @@ -126,7 +126,7 @@ public struct GameFontStyle /// /// Gets the base font size in point unit. /// - public readonly float BaseSizePt => this.FamilyAndSize switch + public float BaseSizePt => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => 0, GameFontFamilyAndSize.Axis96 => 9.6f, @@ -158,14 +158,14 @@ public struct GameFontStyle /// /// Gets the base font size in pixel unit. /// - public readonly float BaseSizePx => this.BaseSizePt * 4 / 3; + public float BaseSizePx => this.BaseSizePt * 4 / 3; /// /// Gets or sets a value indicating whether this font is bold. /// public bool Bold { - readonly get => this.Weight > 0f; + get => this.Weight > 0f; set => this.Weight = value ? 1f : 0f; } @@ -174,8 +174,8 @@ public struct GameFontStyle ///
public bool Italic { - readonly get => this.SkewStrength != 0; - set => this.SkewStrength = value ? this.SizePx / 6 : 0; + get => this.SkewStrength != 0; + set => this.SkewStrength = value ? this.SizePx / 7 : 0; } /// @@ -233,26 +233,13 @@ public struct GameFontStyle _ => GameFontFamilyAndSize.Undefined, }; - /// - /// Creates a new scaled instance of struct. - /// - /// The scale. - /// The scaled instance. - public readonly GameFontStyle Scale(float scale) => new() - { - FamilyAndSize = GetRecommendedFamilyAndSize(this.Family, this.SizePt * scale), - SizePx = this.SizePx * scale, - Weight = this.Weight, - SkewStrength = this.SkewStrength * scale, - }; - /// /// Calculates the adjustment to width resulting fron Weight and SkewStrength. /// /// Font header. /// Glyph. /// Width adjustment in pixel unit. - public readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) { var widthDelta = this.Weight; switch (this.BaseSkewStrength) @@ -276,11 +263,11 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// - public override readonly string ToString() + public override string ToString() { return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})"; } diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 28a9075bd..e030b4e50 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -11,7 +11,6 @@ using System.Text.Unicode; using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using ImGuiNET; @@ -197,9 +196,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { - if (Service.Get() - ?.GetFdtReader(GameFontFamilyAndSize.Axis12) - .FindGlyph(chr) is null) + if (Service.Get() + .GetFdtReader(GameFontFamilyAndSize.Axis12) + ?.FindGlyph(chr) is null) { if (!this.EncounteredHan) { diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 60c1f9957..95415659b 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -21,7 +21,6 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -94,8 +93,7 @@ internal class DalamudInterface : IDisposable, IServiceType private DalamudInterface( Dalamud dalamud, DalamudConfiguration configuration, - FontAtlasFactory fontAtlasFactory, - InterfaceManager interfaceManager, + InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, PluginImageCache pluginImageCache, DalamudAssetManager dalamudAssetManager, Game.Framework framework, @@ -105,7 +103,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.dalamud = dalamud; this.configuration = configuration; - this.interfaceManager = interfaceManager; + this.interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -124,14 +122,10 @@ internal class DalamudInterface : IDisposable, IServiceType clientState, configuration, dalamudAssetManager, - fontAtlasFactory, framework, gameGui, titleScreenMenu) { IsOpen = false }; - this.changelogWindow = new ChangelogWindow( - this.titleScreenMenuWindow, - fontAtlasFactory, - dalamudAssetManager) { IsOpen = false }; + this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false }; @@ -213,7 +207,6 @@ internal class DalamudInterface : IDisposable, IServiceType { this.interfaceManager.Draw -= this.OnDraw; - this.WindowSystem.Windows.OfType().AggregateToDisposable().Dispose(); this.WindowSystem.RemoveAllWindows(); this.changelogWindow.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 3e004727a..48157fa86 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,10 +1,13 @@ +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Threading.Tasks; +using System.Text; +using System.Text.Unicode; +using System.Threading; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -16,13 +19,10 @@ using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Internal; -using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -64,9 +64,11 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const int NonMainThreadFontAccessWarningCheckInterval = 10000; - private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); - private static long nextNonMainThreadFontAccessWarningCheck; + private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. + private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. + + private readonly HashSet glyphRequests = new(); + private readonly Dictionary loadedFontInfo = new(); private readonly List deferredDisposeTextures = new(); @@ -79,28 +81,28 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly SwapChainVtableResolver address = new(); + private readonly ManualResetEvent fontBuildSignal; + private readonly SwapChainVtableResolver address; private readonly Hook setCursorHook; private RawDX11Scene? scene; private Hook? presentHook; private Hook? resizeBuffersHook; - private IFontAtlas? dalamudAtlas; - private IFontHandle.IInternal? defaultFontHandle; - private IFontHandle.IInternal? iconFontHandle; - private IFontHandle.IInternal? monoFontHandle; - // can't access imgui IO before first present call private bool lastWantCapture = false; + private bool isRebuildingFonts = false; private bool isOverrideGameCursor = true; - private IntPtr gameWindowHandle; [ServiceManager.ServiceConstructor] private InterfaceManager() { this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); + + this.fontBuildSignal = new ManualResetEvent(false); + + this.address = new SwapChainVtableResolver(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -115,46 +117,43 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// This event gets called each frame to facilitate ImGui drawing. /// - public event RawDX11Scene.BuildUIDelegate? Draw; + public event RawDX11Scene.BuildUIDelegate Draw; /// /// This event gets called when ResizeBuffers is called. /// - public event Action? ResizeBuffers; + public event Action ResizeBuffers; + + /// + /// Gets or sets an action that is executed right before fonts are rebuilt. + /// + public event Action BuildFonts; /// /// Gets or sets an action that is executed right after fonts are rebuilt. /// - public event Action? AfterBuildFonts; + public event Action AfterBuildFonts; /// - /// Gets the default ImGui font.
- /// Accessing this static property outside of the main thread is dangerous and not supported. + /// Gets the default ImGui font. ///
- public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr DefaultFont { get; private set; } /// - /// Gets an included FontAwesome icon font.
- /// Accessing this static property outside of the main thread is dangerous and not supported. + /// Gets an included FontAwesome icon font. ///
- public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr IconFont { get; private set; } /// - /// Gets an included monospaced font.
- /// Accessing this static property outside of the main thread is dangerous and not supported. + /// Gets an included monospaced font. ///
- public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr MonoFont { get; private set; } /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. /// public ImGuiIOPtr LastImGuiIoPtr { get; set; } - /// - /// Gets the DX11 scene. - /// - public RawDX11Scene? Scene => this.scene; - /// /// Gets the D3D11 device instance. /// @@ -179,6 +178,11 @@ internal class InterfaceManager : IDisposable, IServiceType } } + /// + /// Gets or sets a value indicating whether the fonts are built and ready to use. + /// + public bool FontsReady { get; set; } = false; + /// /// Gets a value indicating whether the Dalamud interface ready to use. /// @@ -190,56 +194,49 @@ internal class InterfaceManager : IDisposable, IServiceType public bool IsDispatchingEvents { get; set; } = true; /// - /// Gets a value indicating the native handle of the game main window. + /// Gets or sets a value indicating whether to override configuration for UseAxis. /// - public IntPtr GameWindowHandle - { - get - { - if (this.gameWindowHandle == 0) - { - nint gwh = 0; - while ((gwh = NativeFunctions.FindWindowEx(0, gwh, "FFXIVGAME", 0)) != 0) - { - _ = User32.GetWindowThreadProcessId(gwh, out var pid); - if (pid == Environment.ProcessId && User32.IsWindowVisible(gwh)) - { - this.gameWindowHandle = gwh; - break; - } - } - } - - return this.gameWindowHandle; - } - } + public bool? UseAxisOverride { get; set; } = null; /// - /// Gets the font build task. + /// Gets a value indicating whether to use AXIS fonts. /// - public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; + public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; + + /// + /// Gets or sets the overrided font gamma value, instead of using the value from configuration. + /// + public float? FontGammaOverride { get; set; } = null; + + /// + /// Gets the font gamma value to use. + /// + public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGammaLevel)); + + /// + /// Gets a value indicating whether we're building fonts but haven't generated atlas yet. + /// + public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0); + + /// + /// Gets a value indicating the native handle of the game main window. + /// + public IntPtr GameWindowHandle { get; private set; } /// /// Dispose of managed and unmanaged resources. /// public void Dispose() { - if (Service.GetNullable() is { } framework) - framework.RunOnFrameworkThread(Disposer).Wait(); - else - Disposer(); - - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.dalamudAtlas?.Dispose(); - this.scene?.Dispose(); - return; - - void Disposer() + this.framework.RunOnFrameworkThread(() => { this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - } + }).Wait(); + + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.scene?.Dispose(); } #nullable enable @@ -379,8 +376,93 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public void RebuildFonts() { + if (this.scene == null) + { + Log.Verbose("[FONT] RebuildFonts(): scene not ready, doing nothing"); + return; + } + Log.Verbose("[FONT] RebuildFonts() called"); - this.dalamudAtlas?.BuildFontsAsync(); + + // don't invoke this multiple times per frame, in case multiple plugins call it + if (!this.isRebuildingFonts) + { + Log.Verbose("[FONT] RebuildFonts() trigger"); + this.isRebuildingFonts = true; + this.scene.OnNewRenderFrame += this.RebuildFontsInternal; + } + } + + /// + /// Wait for the rebuilding fonts to complete. + /// + public void WaitForFontRebuild() + { + this.fontBuildSignal.WaitOne(); + } + + /// + /// Requests a default font of specified size to exist. + /// + /// Font size in pixels. + /// Ranges of glyphs. + /// Requets handle. + public SpecialGlyphRequest NewFontSizeRef(float size, List> ranges) + { + var allContained = false; + var fonts = ImGui.GetIO().Fonts.Fonts; + ImFontPtr foundFont = null; + unsafe + { + for (int i = 0, i_ = fonts.Size; i < i_; i++) + { + if (!this.glyphRequests.Any(x => x.FontInternal.NativePtr == fonts[i].NativePtr)) + continue; + + allContained = true; + foreach (var range in ranges) + { + if (!allContained) + break; + + for (var j = range.Item1; j <= range.Item2 && allContained; j++) + allContained &= fonts[i].FindGlyphNoFallback(j).NativePtr != null; + } + + if (allContained) + foundFont = fonts[i]; + + break; + } + } + + var req = new SpecialGlyphRequest(this, size, ranges); + req.FontInternal = foundFont; + + if (!allContained) + this.RebuildFonts(); + + return req; + } + + /// + /// Requests a default font of specified size to exist. + /// + /// Font size in pixels. + /// Text to calculate glyph ranges from. + /// Requets handle. + public SpecialGlyphRequest NewFontSizeRef(float size, string text) + { + List> ranges = new(); + foreach (var c in new SortedSet(text.ToHashSet())) + { + if (ranges.Any() && ranges[^1].Item2 + 1 == c) + ranges[^1] = Tuple.Create(ranges[^1].Item1, c); + else + ranges.Add(Tuple.Create(c, c)); + } + + return this.NewFontSizeRef(size, ranges); } /// @@ -404,11 +486,11 @@ internal class InterfaceManager : IDisposable, IServiceType try { var dxgiDev = this.Device.QueryInterfaceOrNull(); - var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); + var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); if (dxgiAdapter == null) return null; - var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local); + var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local); return (memInfo.CurrentUsage, memInfo.CurrentReservation); } catch @@ -434,65 +516,20 @@ internal class InterfaceManager : IDisposable, IServiceType /// Value. internal void SetImmersiveMode(bool enabled) { - if (this.GameWindowHandle == 0) - throw new InvalidOperationException("Game window is not yet ready."); - var value = enabled ? 1 : 0; - ((Result)NativeFunctions.DwmSetWindowAttribute( - this.GameWindowHandle, - NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - ref value, - sizeof(int))).CheckError(); + if (this.GameWindowHandle == nint.Zero) + return; + + int value = enabled ? 1 : 0; + var hr = NativeFunctions.DwmSetWindowAttribute( + this.GameWindowHandle, + NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + ref value, + sizeof(int)); } - private static InterfaceManager WhenFontsReady() + private static void ShowFontError(string path) { - var im = Service.GetNullable(); - if (im?.dalamudAtlas is not { } atlas) - throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call."); - - if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) - { - nextNonMainThreadFontAccessWarningCheck = - Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; - var stack = new StackTrace(); - if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) - { - if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) - { - NonMainThreadFontAccessWarning.Add(plugin, new()); - Log.Warning( - "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", - plugin.Name, - stack); - } - } - else - { - // Dalamud internal should be made safe right now - throw new InvalidOperationException("Attempted to access fonts outside the main thread."); - } - } - - if (!atlas.HasBuiltAtlas) - atlas.BuildTask.GetAwaiter().GetResult(); - return im; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void RenderImGui(RawDX11Scene scene) - { - var conf = Service.Get(); - - // Process information needed by ImGuiHelpers each frame. - ImGuiHelpers.NewFrame(); - - // Enable viewports if there are no issues. - if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; - else - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; - - scene.Render(); + Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); } private void InitScene(IntPtr swapChain) @@ -509,7 +546,7 @@ internal class InterfaceManager : IDisposable, IServiceType Service.ProvideException(ex); Log.Error(ex, "Could not load ImGui dependencies."); - var res = User32.MessageBox( + var res = PInvoke.User32.MessageBox( IntPtr.Zero, "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", "Dalamud Error", @@ -541,7 +578,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (iniFileInfo.Length > 1200000) { Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName!, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); iniFileInfo.Delete(); } } @@ -586,6 +623,8 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; + this.SetupFonts(); + if (!configuration.IsDocking) { ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; @@ -636,34 +675,26 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { - Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); - Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); - if (this.scene != null && swapChain != this.scene.SwapChain.NativePointer) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); if (this.scene == null) this.InitScene(swapChain); - Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); - - if (!this.dalamudAtlas!.HasBuiltAtlas) - return this.presentHook!.Original(swapChain, syncInterval, presentFlags); - if (this.address.IsReshade) { - var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); + var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags); - RenderImGui(this.scene!); + this.RenderImGui(); this.DisposeTextures(); return pRes; } - RenderImGui(this.scene!); + this.RenderImGui(); this.DisposeTextures(); - return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + return this.presentHook.Original(swapChain, syncInterval, presentFlags); } private void DisposeTextures() @@ -680,73 +711,471 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [ServiceManager.CallWhenServicesReady( - "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] - private void ContinueConstruction( - TargetSigScanner sigScanner, - Framework framework, - FontAtlasFactory fontAtlasFactory) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RenderImGui() { - this.dalamudAtlas = fontAtlasFactory - .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); - using (this.dalamudAtlas.SuppressAutoRebuild()) + // Process information needed by ImGuiHelpers each frame. + ImGuiHelpers.NewFrame(); + + // Check if we can still enable viewports without any issues. + this.CheckViewportState(); + + this.scene.Render(); + } + + private void CheckViewportState() + { + var configuration = Service.Get(); + + if (configuration.IsDisableViewport || this.scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) { - this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); - this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - tk => tk.AddFontAwesomeIconFont( - new() - { - SizePx = DefaultFontSizePx, - GlyphMinAdvanceX = DefaultFontSizePx, - GlyphMaxAdvanceX = DefaultFontSizePx, - }))); - this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - tk => tk.AddDalamudAssetFont( - DalamudAsset.InconsolataRegular, - new() { SizePx = DefaultFontSizePx }))); - this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( - tk => - { - // Note: the first call of this function is done outside the main thread; this is expected. - // Do not use DefaultFont, IconFont, and MonoFont. - // Use font handles directly. - - // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); - - // Broadcast to auto-rebuilding instances - this.AfterBuildFonts?.Invoke(); - }); + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; + return; } - // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. - _ = this.dalamudAtlas.BuildFontsAsync(false); + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + } - this.address.Setup(sigScanner); + /// + /// Loads font for use in ImGui text functions. + /// + private unsafe void SetupFonts() + { + using var setupFontsTimings = Timings.Start("IM SetupFonts"); + + var gameFontManager = Service.Get(); + var dalamud = Service.Get(); + var io = ImGui.GetIO(); + var ioFonts = io.Fonts; + + var fontGamma = this.FontGamma; + + this.fontBuildSignal.Reset(); + ioFonts.Clear(); + ioFonts.TexDesiredWidth = 4096; + + Log.Verbose("[FONT] SetupFonts - 1"); + + foreach (var v in this.loadedFontInfo) + v.Value.Dispose(); + + this.loadedFontInfo.Clear(); + + Log.Verbose("[FONT] SetupFonts - 2"); + + ImFontConfigPtr fontConfig = null; + List garbageList = new(); try { - if (Service.Get().WindowIsImmersive) - this.SetImmersiveMode(true); + var dummyRangeHandle = GCHandle.Alloc(new ushort[] { '0', '0', 0 }, GCHandleType.Pinned); + garbageList.Add(dummyRangeHandle); + + fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); + fontConfig.OversampleH = 1; + fontConfig.OversampleV = 1; + + var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Regular.otf"); + if (!File.Exists(fontPathJp)) + fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); + if (!File.Exists(fontPathJp)) + ShowFontError(fontPathJp); + Log.Verbose("[FONT] fontPathJp = {0}", fontPathJp); + + var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); + if (!File.Exists(fontPathKr)) + fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); + if (!File.Exists(fontPathKr)) + fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); + if (!File.Exists(fontPathKr)) + fontPathKr = null; + Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); + + var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); + + var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); + + // Default font + Log.Verbose("[FONT] SetupFonts - Default font"); + var fontInfo = new TargetFontModification( + "Default", + this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, + this.UseAxis ? DefaultFontSizePx : DefaultFontSizePx + 1, + io.FontGlobalScale); + Log.Verbose("[FONT] SetupFonts - Default corresponding AXIS size: {0}pt ({1}px)", fontInfo.SourceAxis.Style.BaseSizePt, fontInfo.SourceAxis.Style.BaseSizePx); + fontConfig.SizePixels = fontInfo.TargetSizePx * io.FontGlobalScale; + if (this.UseAxis) + { + fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = false; + DefaultFont = ioFonts.AddFontDefault(fontConfig); + this.loadedFontInfo[DefaultFont] = fontInfo; + } + else + { + var rangeHandle = gameFontManager.ToGlyphRanges(GameFontFamilyAndSize.Axis12); + garbageList.Add(rangeHandle); + + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); + this.loadedFontInfo[DefaultFont] = fontInfo; + } + + if (fontPathKr != null + && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) + { + fontConfig.MergeMode = true; + fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathKr, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || this.dalamudIme.EncounteredHan)) + { + fontConfig.MergeMode = true; + var rangeHandle = GCHandle.Alloc(new ushort[] + { + (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), + (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, + (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + + (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), + 0, + }, GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); + fontConfig.MergeMode = false; + } + + // FontAwesome icon font + Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); + { + var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesomeFreeSolid.otf"); + if (!File.Exists(fontPathIcon)) + ShowFontError(fontPathIcon); + + var iconRangeHandle = GCHandle.Alloc(new ushort[] { 0xE000, 0xF8FF, 0, }, GCHandleType.Pinned); + garbageList.Add(iconRangeHandle); + + fontConfig.GlyphRanges = iconRangeHandle.AddrOfPinnedObject(); + fontConfig.PixelSnapH = true; + IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * io.FontGlobalScale, fontConfig); + this.loadedFontInfo[IconFont] = new("Icon", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); + } + + // Monospace font + Log.Verbose("[FONT] SetupFonts - Monospace font"); + { + var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf"); + if (!File.Exists(fontPathMono)) + ShowFontError(fontPathMono); + + fontConfig.GlyphRanges = IntPtr.Zero; + fontConfig.PixelSnapH = true; + MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * io.FontGlobalScale, fontConfig); + this.loadedFontInfo[MonoFont] = new("Mono", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); + } + + // Default font but in requested size for requested glyphs + Log.Verbose("[FONT] SetupFonts - Default font but in requested size for requested glyphs"); + { + Dictionary> extraFontRequests = new(); + foreach (var extraFontRequest in this.glyphRequests) + { + if (!extraFontRequests.ContainsKey(extraFontRequest.Size)) + extraFontRequests[extraFontRequest.Size] = new(); + extraFontRequests[extraFontRequest.Size].Add(extraFontRequest); + } + + foreach (var (fontSize, requests) in extraFontRequests) + { + List<(ushort, ushort)> codepointRanges = new(4 + requests.Sum(x => x.CodepointRanges.Count)) + { + new(Fallback1Codepoint, Fallback1Codepoint), + new(Fallback2Codepoint, Fallback2Codepoint), + // ImGui default ellipsis characters + new(0x2026, 0x2026), + new(0x0085, 0x0085), + }; + + foreach (var request in requests) + codepointRanges.AddRange(request.CodepointRanges.Select(x => (From: x.Item1, To: x.Item2))); + + codepointRanges.Sort(); + List flattenedRanges = new(); + foreach (var range in codepointRanges) + { + if (flattenedRanges.Any() && flattenedRanges[^1] >= range.Item1 - 1) + { + flattenedRanges[^1] = Math.Max(flattenedRanges[^1], range.Item2); + } + else + { + flattenedRanges.Add(range.Item1); + flattenedRanges.Add(range.Item2); + } + } + + flattenedRanges.Add(0); + + fontInfo = new( + $"Requested({fontSize}px)", + this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, + fontSize, + io.FontGlobalScale); + if (this.UseAxis) + { + fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); + fontConfig.SizePixels = fontInfo.SourceAxis.Style.BaseSizePx; + fontConfig.PixelSnapH = false; + + var sizedFont = ioFonts.AddFontDefault(fontConfig); + this.loadedFontInfo[sizedFont] = fontInfo; + foreach (var request in requests) + request.FontInternal = sizedFont; + } + else + { + var rangeHandle = GCHandle.Alloc(flattenedRanges.ToArray(), GCHandleType.Pinned); + garbageList.Add(rangeHandle); + fontConfig.PixelSnapH = true; + + var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontSize * io.FontGlobalScale, fontConfig, rangeHandle.AddrOfPinnedObject()); + this.loadedFontInfo[sizedFont] = fontInfo; + foreach (var request in requests) + request.FontInternal = sizedFont; + } + } + } + + gameFontManager.BuildFonts(); + + var customFontFirstConfigIndex = ioFonts.ConfigData.Size; + + Log.Verbose("[FONT] Invoke OnBuildFonts"); + this.BuildFonts?.InvokeSafely(); + Log.Verbose("[FONT] OnBuildFonts OK!"); + + for (int i = customFontFirstConfigIndex, i_ = ioFonts.ConfigData.Size; i < i_; i++) + { + var config = ioFonts.ConfigData[i]; + if (gameFontManager.OwnsFont(config.DstFont)) + continue; + + config.OversampleH = 1; + config.OversampleV = 1; + + var name = Encoding.UTF8.GetString((byte*)config.Name.Data, config.Name.Count).TrimEnd('\0'); + if (name.IsNullOrEmpty()) + name = $"{config.SizePixels}px"; + + // ImFont information is reflected only if corresponding ImFontConfig has MergeMode not set. + if (config.MergeMode) + { + if (!this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) + { + Log.Warning("MergeMode specified for {0} but not found in loadedFontInfo. Skipping.", name); + continue; + } + } + else + { + if (this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) + { + Log.Warning("MergeMode not specified for {0} but found in loadedFontInfo. Skipping.", name); + continue; + } + + // While the font will be loaded in the scaled size after FontScale is applied, the font will be treated as having the requested size when used from plugins. + this.loadedFontInfo[config.DstFont.NativePtr] = new($"PlReq({name})", config.SizePixels); + } + + config.SizePixels = config.SizePixels * io.FontGlobalScale; + } + + for (int i = 0, i_ = ioFonts.ConfigData.Size; i < i_; i++) + { + var config = ioFonts.ConfigData[i]; + config.RasterizerGamma *= fontGamma; + } + + Log.Verbose("[FONT] ImGui.IO.Build will be called."); + ioFonts.Build(); + gameFontManager.AfterIoFontsBuild(); + this.ClearStacks(); + Log.Verbose("[FONT] ImGui.IO.Build OK!"); + + gameFontManager.AfterBuildFonts(); + + foreach (var (font, mod) in this.loadedFontInfo) + { + // I have no idea what's causing NPE, so just to be safe + try + { + if (font.NativePtr != null && font.NativePtr->ConfigData != null) + { + var nameBytes = Encoding.UTF8.GetBytes($"{mod.Name}\0"); + Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); + } + } + catch (NullReferenceException) + { + // do nothing + } + + Log.Verbose("[FONT] {0}: Unscale with scale value of {1}", mod.Name, mod.Scale); + GameFontManager.UnscaleFont(font, mod.Scale, false); + + if (mod.Axis == TargetFontModification.AxisMode.Overwrite) + { + Log.Verbose("[FONT] {0}: Overwrite from AXIS of size {1}px (was {2}px)", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); + GameFontManager.UnscaleFont(font, font.FontSize / mod.SourceAxis.ImFont.FontSize, false); + var ascentDiff = mod.SourceAxis.ImFont.Ascent - font.Ascent; + font.Ascent += ascentDiff; + font.Descent = ascentDiff; + font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar; + font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar; + ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); + } + else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly) + { + Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); + if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) + mod.SourceAxis.ImFont.FontSize -= 1; + ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); + if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) + mod.SourceAxis.ImFont.FontSize += 1; + } + + Log.Verbose("[FONT] {0}: Resize from {1}px to {2}px", mod.Name, font.FontSize, mod.TargetSizePx); + GameFontManager.UnscaleFont(font, font.FontSize / mod.TargetSizePx, false); + } + + // Fill missing glyphs in MonoFont from DefaultFont + ImGuiHelpers.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); + + for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++) + { + var font = ioFonts.Fonts[i]; + if (font.Glyphs.Size == 0) + { + Log.Warning("[FONT] Font has no glyph: {0}", font.GetDebugName()); + continue; + } + + if (font.FindGlyphNoFallback(Fallback1Codepoint).NativePtr != null) + font.FallbackChar = Fallback1Codepoint; + + font.BuildLookupTableNonstandard(); + } + + Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); + this.AfterBuildFonts?.InvokeSafely(); + Log.Verbose("[FONT] OnAfterBuildFonts OK!"); + + if (ioFonts.Fonts[0].NativePtr != DefaultFont.NativePtr) + Log.Warning("[FONT] First font is not DefaultFont"); + + Log.Verbose("[FONT] Fonts built!"); + + this.fontBuildSignal.Set(); + + this.FontsReady = true; } - catch (Exception ex) + finally { - Log.Error(ex, "Could not enable immersive mode"); + if (fontConfig.NativePtr != null) + fontConfig.Destroy(); + + foreach (var garbage in garbageList) + garbage.Free(); } + } - this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); + [ServiceManager.CallWhenServicesReady( + "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] + private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration) + { + this.address.Setup(sigScanner); + this.framework.RunOnFrameworkThread(() => + { + while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) + { + _ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid); - Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); - Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); + if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle)) + break; + } - this.setCursorHook.Enable(); - this.presentHook.Enable(); - this.resizeBuffersHook.Enable(); + try + { + if (configuration.WindowIsImmersive) + this.SetImmersiveMode(true); + } + catch (Exception ex) + { + Log.Error(ex, "Could not enable immersive mode"); + } + + this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); + this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); + + Log.Verbose("===== S W A P C H A I N ====="); + Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); + Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); + + this.setCursorHook.Enable(); + this.presentHook.Enable(); + this.resizeBuffersHook.Enable(); + }); + } + + // This is intended to only be called as a handler attached to scene.OnNewRenderFrame + private void RebuildFontsInternal() + { + Log.Verbose("[FONT] RebuildFontsInternal() called"); + this.SetupFonts(); + + Log.Verbose("[FONT] RebuildFontsInternal() detaching"); + this.scene!.OnNewRenderFrame -= this.RebuildFontsInternal; + + Log.Verbose("[FONT] Calling InvalidateFonts"); + this.scene.InvalidateFonts(); + + Log.Verbose("[FONT] Font Rebuild OK!"); + + this.isRebuildingFonts = false; } private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) @@ -777,17 +1206,14 @@ internal class InterfaceManager : IDisposable, IServiceType private IntPtr SetCursorDetour(IntPtr hCursor) { - if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) + if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed - ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() - : this.setCursorHook.Original(hCursor); + return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); } private void OnNewInputFrame() { - var io = ImGui.GetIO(); var dalamudInterface = Service.GetNullable(); var gamepadState = Service.GetNullable(); var keyState = Service.GetNullable(); @@ -795,21 +1221,18 @@ internal class InterfaceManager : IDisposable, IServiceType if (dalamudInterface == null || gamepadState == null || keyState == null) return; - // Prevent setting the footgun from ImGui Demo; the Space key isn't removing the flag at the moment. - io.ConfigFlags &= ~ImGuiConfigFlags.NoMouse; - // fix for keys in game getting stuck, if you were holding a game key (like run) // and then clicked on an imgui textbox - imgui would swallow the keyup event, // so the game would think the key remained pressed continuously until you left // imgui and pressed and released the key again - if (io.WantTextInput) + if (ImGui.GetIO().WantTextInput) { keyState.ClearAll(); } // TODO: mouse state? - var gamepadEnabled = (io.BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; + var gamepadEnabled = (ImGui.GetIO().BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; // NOTE (Chiv) Activate ImGui navigation via L1+L3 press // (mimicking how mouse navigation is activated via L1+R3 press in game). @@ -817,12 +1240,12 @@ internal class InterfaceManager : IDisposable, IServiceType && gamepadState.Raw(GamepadButtons.L1) > 0 && gamepadState.Pressed(GamepadButtons.L3) > 0) { - io.ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; + ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; gamepadState.NavEnableGamepad ^= true; dalamudInterface.ToggleGamepadModeNotifierWindow(); } - if (gamepadEnabled && (io.ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) + if (gamepadEnabled && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) { var northButton = gamepadState.Raw(GamepadButtons.North) != 0; var eastButton = gamepadState.Raw(GamepadButtons.East) != 0; @@ -841,6 +1264,7 @@ internal class InterfaceManager : IDisposable, IServiceType var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0; var r2Button = gamepadState.Raw(GamepadButtons.R2) != 0; + var io = ImGui.GetIO(); io.AddKeyEvent(ImGuiKey.GamepadFaceUp, northButton); io.AddKeyEvent(ImGuiKey.GamepadFaceRight, eastButton); io.AddKeyEvent(ImGuiKey.GamepadFaceDown, southButton); @@ -888,10 +1312,7 @@ internal class InterfaceManager : IDisposable, IServiceType var snap = ImGuiManagedAsserts.GetSnapshot(); if (this.IsDispatchingEvents) - { - using (this.defaultFontHandle?.Push()) - this.Draw?.Invoke(); - } + this.Draw?.Invoke(); ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); @@ -918,4 +1339,123 @@ internal class InterfaceManager : IDisposable, IServiceType /// public InterfaceManager Manager { get; init; } } + + /// + /// Represents a glyph request. + /// + public class SpecialGlyphRequest : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// InterfaceManager to associate. + /// Font size in pixels. + /// Codepoint ranges. + internal SpecialGlyphRequest(InterfaceManager manager, float size, List> ranges) + { + this.Manager = manager; + this.Size = size; + this.CodepointRanges = ranges; + this.Manager.glyphRequests.Add(this); + } + + /// + /// Gets the font of specified size, or DefaultFont if it's not ready yet. + /// + public ImFontPtr Font + { + get + { + unsafe + { + return this.FontInternal.NativePtr == null ? DefaultFont : this.FontInternal; + } + } + } + + /// + /// Gets or sets the associated ImFont. + /// + internal ImFontPtr FontInternal { get; set; } + + /// + /// Gets associated InterfaceManager. + /// + internal InterfaceManager Manager { get; init; } + + /// + /// Gets font size. + /// + internal float Size { get; init; } + + /// + /// Gets codepoint ranges. + /// + internal List> CodepointRanges { get; init; } + + /// + public void Dispose() + { + this.Manager.glyphRequests.Remove(this); + } + } + + private unsafe class TargetFontModification : IDisposable + { + /// + /// Initializes a new instance of the class. + /// Constructs new target font modification information, assuming that AXIS fonts will not be applied. + /// + /// Name of the font to write to ImGui font information. + /// Target font size in pixels, which will not be considered for further scaling. + internal TargetFontModification(string name, float sizePx) + { + this.Name = name; + this.Axis = AxisMode.Suppress; + this.TargetSizePx = sizePx; + this.Scale = 1; + this.SourceAxis = null; + } + + /// + /// Initializes a new instance of the class. + /// Constructs new target font modification information. + /// + /// Name of the font to write to ImGui font information. + /// Whether and how to use AXIS fonts. + /// Target font size in pixels, which will not be considered for further scaling. + /// Font scale to be referred for loading AXIS font of appropriate size. + internal TargetFontModification(string name, AxisMode axis, float sizePx, float globalFontScale) + { + this.Name = name; + this.Axis = axis; + this.TargetSizePx = sizePx; + this.Scale = globalFontScale; + this.SourceAxis = Service.Get().NewFontRef(new(GameFontFamily.Axis, this.TargetSizePx * this.Scale)); + } + + internal enum AxisMode + { + Suppress, + GameGlyphsOnly, + Overwrite, + } + + internal string Name { get; private init; } + + internal AxisMode Axis { get; private init; } + + internal float TargetSizePx { get; private init; } + + internal float Scale { get; private init; } + + internal GameFontHandle? SourceAxis { get; private init; } + + internal bool SourceAxisAvailable => this.SourceAxis != null && this.SourceAxis.ImFont.NativePtr != null; + + public void Dispose() + { + this.SourceAxis?.Dispose(); + } + } } diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index ae59db36a..b9e7ab686 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Linq; using System.Numerics; @@ -6,8 +7,6 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -32,14 +31,8 @@ internal sealed class ChangelogWindow : Window, IDisposable • Plugins can now add tooltips and interaction to the server info bar • The Dalamud/plugin installer UI has been refreshed "; - + private readonly TitleScreenMenuWindow tsmWindow; - - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private readonly IFontAtlas privateAtlas; - private readonly Lazy bannerFont; - private readonly Lazy apiBumpExplainerTexture; - private readonly Lazy logoTexture; private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) { @@ -53,36 +46,27 @@ internal sealed class ChangelogWindow : Window, IDisposable Point2 = Vector2.One, }; + private IDalamudTextureWrap? apiBumpExplainerTexture; + private IDalamudTextureWrap? logoTexture; + private GameFontHandle? bannerFont; + private State state = State.WindowFadeIn; private bool needFadeRestart = false; - + /// /// Initializes a new instance of the class. /// /// TSM window. - /// An instance of . - /// An instance of . - public ChangelogWindow( - TitleScreenMenuWindow tsmWindow, - FontAtlasFactory fontAtlasFactory, - DalamudAssetManager assets) + public ChangelogWindow(TitleScreenMenuWindow tsmWindow) : base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true) { this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; - this.privateAtlas = this.scopedFinalizer.Add( - fontAtlasFactory.CreateFontAtlas(this.Namespace, FontAtlasAutoRebuildMode.Async)); - this.bannerFont = new( - () => this.scopedFinalizer.Add( - this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.MiedingerMid18)))); - - this.apiBumpExplainerTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.ChangelogApiBumpIcon)); - this.logoTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.Logo)); // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - _ = this.bannerFont.Value; + Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); } private enum State @@ -113,12 +97,20 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - _ = this.bannerFont; + this.MakeFont(Service.Get()); this.state = State.WindowFadeIn; this.windowFade.Reset(); this.bodyFade.Reset(); this.needFadeRestart = true; + + if (this.apiBumpExplainerTexture == null) + { + var dalamud = Service.Get(); + var tm = Service.Get(); + this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png"))) + ?? throw new Exception("Could not load api bump explainer."); + } base.OnOpen(); } @@ -194,7 +186,10 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2)); using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) - ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize); + { + this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); + ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); + } } ImGui.SameLine(); @@ -210,7 +205,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) { - using var font = this.bannerFont.Value.Push(); + using var font = ImRaii.PushFont(this.bannerFont!.ImFont); switch (this.state) { @@ -280,11 +275,9 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.TextWrapped("If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."); ImGuiHelpers.ScaledDummy(15); - - ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture.Value.Width); - ImGui.Image( - this.apiBumpExplainerTexture.Value.ImGuiHandle, - this.apiBumpExplainerTexture.Value.Size); + + ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width); + ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size); DrawNextButton(State.Links); break; @@ -384,4 +377,7 @@ internal sealed class ChangelogWindow : Window, IDisposable public void Dispose() { } + + private void MakeFont(GameFontManager gfm) => + this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 951d3d91c..20c3d6d01 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -6,8 +6,6 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Utility; - using ImGuiNET; using Serilog; @@ -16,7 +14,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class responsible for drawing the data/debug window. /// -internal class DataWindow : Window, IDisposable +internal class DataWindow : Window { private readonly IDataWindowWidget[] modules = { @@ -36,7 +34,6 @@ internal class DataWindow : Window, IDisposable new FlyTextWidget(), new FontAwesomeTestWidget(), new GameInventoryTestWidget(), - new GamePrebakedFontsTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), @@ -79,9 +76,6 @@ internal class DataWindow : Window, IDisposable this.Load(); } - /// - public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose(); - /// public override void OnOpen() { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs deleted file mode 100644 index dba293e8b..000000000 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Text; - -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; -using Dalamud.Interface.Utility; -using Dalamud.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.Internal.Windows.Data.Widgets; - -/// -/// Widget for testing game prebaked fonts. -/// -internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable -{ - private ImVectorWrapper testStringBuffer; - private IFontAtlas? privateAtlas; - private IReadOnlyDictionary Handle)[]>? fontHandles; - private bool useGlobalScale; - private bool useWordWrap; - private bool useItalic; - private bool useBold; - private bool useMinimumBuild; - - /// - public string[]? CommandShortcuts { get; init; } - - /// - public string DisplayName { get; init; } = "Game Prebaked Fonts"; - - /// - public bool Ready { get; set; } - - /// - public void Load() => this.Ready = true; - - /// - public unsafe void Draw() - { - ImGui.AlignTextToFramePadding(); - fixed (byte* labelPtr = "Global Scale"u8) - { - var v = (byte)(this.useGlobalScale ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - { - this.useGlobalScale = v != 0; - this.ClearAtlas(); - } - } - - ImGui.SameLine(); - fixed (byte* labelPtr = "Word Wrap"u8) - { - var v = (byte)(this.useWordWrap ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - this.useWordWrap = v != 0; - } - - ImGui.SameLine(); - fixed (byte* labelPtr = "Italic"u8) - { - var v = (byte)(this.useItalic ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - { - this.useItalic = v != 0; - this.ClearAtlas(); - } - } - - ImGui.SameLine(); - fixed (byte* labelPtr = "Bold"u8) - { - var v = (byte)(this.useBold ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - { - this.useBold = v != 0; - this.ClearAtlas(); - } - } - - ImGui.SameLine(); - fixed (byte* labelPtr = "Minimum Range"u8) - { - var v = (byte)(this.useMinimumBuild ? 1 : 0); - if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) - { - this.useMinimumBuild = v != 0; - this.ClearAtlas(); - } - } - - ImGui.SameLine(); - if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed) - { - this.testStringBuffer.Dispose(); - this.testStringBuffer = ImVectorWrapper.CreateFromSpan( - "(Game)-[Font] {Test}. 0123456789!! <氣気气きキ기>。"u8, - minCapacity: 1024); - } - - fixed (byte* labelPtr = "Test Input"u8) - { - if (ImGuiNative.igInputTextMultiline( - labelPtr, - this.testStringBuffer.Data, - (uint)this.testStringBuffer.Capacity, - new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale), - 0, - null, - null) != 0) - { - var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); - if (len + 4 >= this.testStringBuffer.Capacity) - this.testStringBuffer.EnsureCapacityExponential(len + 4); - if (len < this.testStringBuffer.Capacity) - { - this.testStringBuffer.LengthUnsafe = len; - this.testStringBuffer.StorageSpan[len] = default; - } - - if (this.useMinimumBuild) - _ = this.privateAtlas?.BuildFontsAsync(); - } - } - - this.privateAtlas ??= - Service.Get().CreateFontAtlas( - nameof(GamePrebakedFontsTestWidget), - FontAtlasAutoRebuildMode.Async, - this.useGlobalScale); - this.fontHandles ??= - Enum.GetValues() - .Where(x => x.GetAttribute() is not null) - .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) - .GroupBy(x => x.Family) - .ToImmutableDictionary( - x => x.Key, - x => x.Select( - y => (y, new Lazy( - () => this.useMinimumBuild - ? this.privateAtlas.NewDelegateFontHandle( - e => - e.OnPreBuild( - tk => tk.AddGameGlyphs( - y, - Encoding.UTF8.GetString( - this.testStringBuffer.DataSpan).ToGlyphRange(), - default))) - : this.privateAtlas.NewGameFontHandle(y)))) - .ToArray()); - - var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); - foreach (var (family, items) in this.fontHandles) - { - if (!ImGui.CollapsingHeader($"{family} Family")) - continue; - - foreach (var (gfs, handle) in items) - { - ImGui.TextUnformatted($"{gfs.SizePt}pt"); - ImGui.SameLine(offsetX); - ImGuiNative.igPushTextWrapPos(this.useWordWrap ? 0f : -1f); - try - { - if (handle.Value.LoadException is { } exc) - { - ImGui.TextUnformatted(exc.ToString()); - } - else if (!handle.Value.Available) - { - fixed (byte* labelPtr = "Loading..."u8) - ImGuiNative.igTextUnformatted(labelPtr, labelPtr + 8 + ((Environment.TickCount / 200) % 3)); - } - else - { - if (!this.useGlobalScale) - ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); - using var pushPop = handle.Value.Push(); - ImGuiNative.igTextUnformatted( - this.testStringBuffer.Data, - this.testStringBuffer.Data + this.testStringBuffer.Length); - } - } - finally - { - ImGuiNative.igPopTextWrapPos(); - ImGuiNative.igSetWindowFontScale(1); - } - } - } - } - - /// - public void Dispose() - { - this.ClearAtlas(); - this.testStringBuffer.Dispose(); - } - - private void ClearAtlas() - { - this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value)) - .AggregateToDisposable().Dispose(); - this.fontHandles = null; - this.privateAtlas?.Dispose(); - this.privateAtlas = null; - } -} diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 027e1a571..7d4489f8d 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -5,10 +5,10 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.Settings.Tabs; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Internal; using Dalamud.Utility; using ImGuiNET; @@ -19,7 +19,14 @@ namespace Dalamud.Interface.Internal.Windows.Settings; /// internal class SettingsWindow : Window { - private SettingsTab[]? tabs; + private readonly SettingsTab[] tabs = + { + new SettingsTabGeneral(), + new SettingsTabLook(), + new SettingsTabDtr(), + new SettingsTabExperimental(), + new SettingsTabAbout(), + }; private string searchInput = string.Empty; @@ -42,15 +49,6 @@ internal class SettingsWindow : Window /// public override void OnOpen() { - this.tabs ??= new SettingsTab[] - { - new SettingsTabGeneral(), - new SettingsTabLook(), - new SettingsTabDtr(), - new SettingsTabExperimental(), - new SettingsTabAbout(), - }; - foreach (var settingsTab in this.tabs) { settingsTab.Load(); @@ -66,12 +64,15 @@ internal class SettingsWindow : Window { var configuration = Service.Get(); var interfaceManager = Service.Get(); - var fontAtlasFactory = Service.Get(); - var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = + ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale || + interfaceManager.FontGamma != configuration.FontGammaLevel || + interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - fontAtlasFactory.UseAxisOverride = null; + interfaceManager.FontGammaOverride = null; + interfaceManager.UseAxisOverride = null; if (rebuildFont) interfaceManager.RebuildFonts(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 8714fd666..5b6f6b02f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -1,13 +1,13 @@ -using System.Diagnostics; +using System; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Linq; using System.Numerics; using CheapLoc; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; @@ -15,6 +15,7 @@ using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; +using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -172,21 +173,16 @@ Contribute at: https://github.com/goatcorp/Dalamud "; private readonly Stopwatch creditsThrottler; - private readonly IFontAtlas privateAtlas; private string creditsText; private bool resetNow = false; private IDalamudTextureWrap? logoTexture; - private IFontHandle? thankYouFont; + private GameFontHandle? thankYouFont; public SettingsTabAbout() { this.creditsThrottler = new(); - - this.privateAtlas = Service - .Get() - .CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async); } public override SettingsEntry[] Entries { get; } = { }; @@ -211,7 +207,11 @@ Contribute at: https://github.com/goatcorp/Dalamud this.creditsThrottler.Restart(); - this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34)); + if (this.thankYouFont == null) + { + var gfm = Service.Get(); + this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34)); + } this.resetNow = true; @@ -269,12 +269,14 @@ Contribute at: https://github.com/goatcorp/Dalamud if (this.thankYouFont != null) { - using var fontPush = this.thankYouFont.Push(); + ImGui.PushFont(this.thankYouFont.ImFont); var thankYouLenX = ImGui.CalcTextSize(ThankYouText).X; ImGui.Dummy(new Vector2((windowX / 2) - (thankYouLenX / 2), 0f)); ImGui.SameLine(); ImGui.TextUnformatted(ThankYouText); + + ImGui.PopFont(); } ImGuiHelpers.ScaledDummy(0, windowSize.Y + 50f); @@ -303,5 +305,9 @@ Contribute at: https://github.com/goatcorp/Dalamud /// /// Disposes of managed and unmanaged resources. /// - public override void Dispose() => this.privateAtlas.Dispose(); + public override void Dispose() + { + this.logoTexture?.Dispose(); + this.thankYouFont?.Dispose(); + } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 5293e13c4..02e8ce789 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,14 +1,12 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; -using System.Text; using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -30,6 +28,7 @@ public class SettingsTabLook : SettingsTab }; private float globalUiScale; + private float fontGamma; public override SettingsEntry[] Entries { get; } = { @@ -42,8 +41,9 @@ public class SettingsTabLook : SettingsTab (v, c) => c.UseAxisFontsFromGame = v, v => { - Service.Get().UseAxisOverride = v; - Service.Get().RebuildFonts(); + var im = Service.Get(); + im.UseAxisOverride = v; + im.RebuildFonts(); }), new GapSettingsEntry(5, true), @@ -145,7 +145,6 @@ public class SettingsTabLook : SettingsTab public override void Draw() { var interfaceManager = Service.Get(); - var fontBuildTask = interfaceManager.FontBuildTask; ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); @@ -165,19 +164,6 @@ public class SettingsTabLook : SettingsTab } } - if (!fontBuildTask.IsCompleted) - { - ImGui.SameLine(); - var buildingFonts = Loc.Localize("DalamudSettingsFontBuildInProgressWithEndingThreeDots", "Building fonts..."); - unsafe - { - var len = Encoding.UTF8.GetByteCount(buildingFonts); - var p = stackalloc byte[len]; - Encoding.UTF8.GetBytes(buildingFonts, new(p, len)); - ImGuiNative.igTextUnformatted(p, (p + len + ((Environment.TickCount / 200) % 3)) - 2); - } - } - var globalUiScaleInPt = 12f * this.globalUiScale; if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp)) { @@ -188,25 +174,33 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays.")); - if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled) + ImGuiHelpers.ScaledDummy(5); + + ImGui.AlignTextToFramePadding(); + ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); + ImGui.SameLine(); + if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) { - ImGui.TextColored( - ImGuiColors.DalamudRed, - Loc.Localize("DalamudSettingsFontBuildFaulted", "Failed to load fonts as requested.")); - if (fontBuildTask.Exception is not null - && ImGui.CollapsingHeader("##DalamudSetingsFontBuildFaultReason")) - { - foreach (var e in fontBuildTask.Exception.InnerExceptions) - ImGui.TextUnformatted(e.ToString()); - } + this.fontGamma = 1.4f; + interfaceManager.FontGammaOverride = this.fontGamma; + interfaceManager.RebuildFonts(); } + if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp)) + { + interfaceManager.FontGammaOverride = this.fontGamma; + interfaceManager.RebuildFonts(); + } + + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text.")); + base.Draw(); } public override void Load() { this.globalUiScale = Service.Get().GlobalUiScale; + this.fontGamma = Service.Get().FontGammaLevel; base.Load(); } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 9c385a99c..42bca89ff 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -7,14 +7,11 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; -using Dalamud.Utility; using ImGuiNET; @@ -30,17 +27,16 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly ClientState clientState; private readonly DalamudConfiguration configuration; + private readonly Framework framework; private readonly GameGui gameGui; private readonly TitleScreenMenu titleScreenMenu; - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private readonly IFontAtlas privateAtlas; - private readonly Lazy myFontHandle; private readonly Lazy shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); private readonly Dictionary logoEasings = new(); + private readonly Dictionary specialGlyphRequests = new(); private InOutCubic? fadeOutEasing; @@ -52,7 +48,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// An instance of . /// An instance of . /// An instance of . - /// An instance of . /// An instance of . /// An instance of . /// An instance of . @@ -60,7 +55,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable ClientState clientState, DalamudConfiguration configuration, DalamudAssetManager dalamudAssetManager, - FontAtlasFactory fontAtlasFactory, Framework framework, GameGui gameGui, TitleScreenMenu titleScreenMenu) @@ -71,6 +65,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable { this.clientState = clientState; this.configuration = configuration; + this.framework = framework; this.gameGui = gameGui; this.titleScreenMenu = titleScreenMenu; @@ -82,25 +77,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; - this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); - this.privateAtlas = fontAtlasFactory.CreateFontAtlas(this.WindowName, FontAtlasAutoRebuildMode.Async); - this.scopedFinalizer.Add(this.privateAtlas); - - this.myFontHandle = new( - () => this.scopedFinalizer.Add( - this.privateAtlas.NewDelegateFontHandle( - e => e.OnPreBuild( - toolkit => toolkit.AddDalamudDefaultFont( - TargetFontSizePx, - titleScreenMenu.Entries.SelectMany(x => x.Name).ToGlyphRange()))))); - - titleScreenMenu.EntryListChange += this.TitleScreenMenuEntryListChange; - this.scopedFinalizer.Add(() => titleScreenMenu.EntryListChange -= this.TitleScreenMenuEntryListChange); - this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); framework.Update += this.FrameworkOnUpdate; - this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate); } private enum State @@ -115,9 +94,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public bool AllowDrawing { get; set; } = true; - /// - public void Dispose() => this.scopedFinalizer.Dispose(); - /// public override void PreDraw() { @@ -133,6 +109,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable base.PostDraw(); } + /// + public void Dispose() + { + this.framework.Update -= this.FrameworkOnUpdate; + } + /// public override void Draw() { @@ -264,12 +246,33 @@ internal class TitleScreenMenuWindow : Window, IDisposable break; } } + + var srcText = entries.Select(e => e.Name).ToHashSet(); + var keys = this.specialGlyphRequests.Keys.ToHashSet(); + keys.RemoveWhere(x => srcText.Contains(x)); + foreach (var key in keys) + { + this.specialGlyphRequests[key].Dispose(); + this.specialGlyphRequests.Remove(key); + } } private bool DrawEntry( TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) { - using var fontScopeDispose = this.myFontHandle.Value.Push(); + InterfaceManager.SpecialGlyphRequest fontHandle; + if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) + { + fontHandle.Dispose(); + this.specialGlyphRequests.Remove(entry.Name); + fontHandle = null; + } + + if (fontHandle == null) + this.specialGlyphRequests[entry.Name] = fontHandle = Service.Get().NewFontSizeRef(TargetFontSizePx, entry.Name); + + ImGui.PushFont(fontHandle.Font); + ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size); var scale = ImGui.GetIO().FontGlobalScale; @@ -380,6 +383,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable initialCursor.Y += entry.Texture.Height * scale; ImGui.SetCursorPos(initialCursor); + ImGui.PopFont(); + return isHover; } @@ -396,6 +401,4 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } - - private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs deleted file mode 100644 index 50e591390..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// How to rebuild . -/// -public enum FontAtlasAutoRebuildMode -{ - /// - /// Do not rebuild. - /// - Disable, - - /// - /// Rebuild on new frame. - /// - OnNewFrame, - - /// - /// Rebuild asynchronously. - /// - Async, -} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs deleted file mode 100644 index 345ab729d..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs +++ /dev/null @@ -1,38 +0,0 @@ -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Build step for . -/// -public enum FontAtlasBuildStep -{ - /// - /// An invalid value. This should never be passed through event callbacks. - /// - Invalid, - - /// - /// Called before calling .
- /// Expect to be passed. - ///
- PreBuild, - - /// - /// Called after calling .
- /// Expect to be passed.
- ///
- /// This callback is not guaranteed to happen after , - /// but it will never happen on its own. - ///
- PostBuild, - - /// - /// Called after promoting staging font atlas to the actual atlas for .
- /// Expect to be passed.
- ///
- /// This callback is not guaranteed to happen after , - /// but it will never happen on its own. - ///
- PostPromotion, -} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs deleted file mode 100644 index 4f5b34061..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Delegate to be called when a font needs to be built. -/// -/// A toolkit that may help you for font building steps. -/// -/// An implementation of may implement all of -/// , , and -/// .
-/// Either use to identify the build step, or use -/// , , -/// and for routing. -///
-public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs deleted file mode 100644 index 586887a3b..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs +++ /dev/null @@ -1,133 +0,0 @@ -using System.Collections.Generic; -using System.Runtime.CompilerServices; - -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Convenience function for building fonts through . -/// -public static class FontAtlasBuildToolkitUtilities -{ - /// - /// Compiles given s into an array of containing ImGui glyph ranges. - /// - /// The chars. - /// Add fallback codepoints to the range. - /// Add ellipsis codepoints to the range. - /// The compiled range. - public static ushort[] ToGlyphRange( - this IEnumerable enumerable, - bool addFallbackCodepoints = true, - bool addEllipsisCodepoints = true) - { - using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); - foreach (var c in enumerable) - builder.AddChar(c); - return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); - } - - /// - /// Compiles given s into an array of containing ImGui glyph ranges. - /// - /// The chars. - /// Add fallback codepoints to the range. - /// Add ellipsis codepoints to the range. - /// The compiled range. - public static ushort[] ToGlyphRange( - this ReadOnlySpan span, - bool addFallbackCodepoints = true, - bool addEllipsisCodepoints = true) - { - using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); - foreach (var c in span) - builder.AddChar(c); - return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); - } - - /// - /// Compiles given string into an array of containing ImGui glyph ranges. - /// - /// The string. - /// Add fallback codepoints to the range. - /// Add ellipsis codepoints to the range. - /// The compiled range. - public static ushort[] ToGlyphRange( - this string @string, - bool addFallbackCodepoints = true, - bool addEllipsisCodepoints = true) => - @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); - - /// - /// Finds the corresponding in - /// . that corresponds to the - /// specified font . - /// - /// The toolkit. - /// The font. - /// The relevant config pointer, or empty config pointer if not found. - public static unsafe ImFontConfigPtr FindConfigPtr(this IFontAtlasBuildToolkit toolkit, ImFontPtr fontPtr) - { - foreach (ref var c in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan) - { - if (c.DstFont == fontPtr.NativePtr) - return new((nint)Unsafe.AsPointer(ref c)); - } - - return default; - } - - /// - /// Invokes - /// if of - /// is . - /// - /// The toolkit. - /// The action. - /// This, for method chaining. - public static IFontAtlasBuildToolkit OnPreBuild( - this IFontAtlasBuildToolkit toolkit, - Action action) - { - if (toolkit.BuildStep is FontAtlasBuildStep.PreBuild) - action.Invoke((IFontAtlasBuildToolkitPreBuild)toolkit); - return toolkit; - } - - /// - /// Invokes - /// if of - /// is . - /// - /// The toolkit. - /// The action. - /// toolkit, for method chaining. - public static IFontAtlasBuildToolkit OnPostBuild( - this IFontAtlasBuildToolkit toolkit, - Action action) - { - if (toolkit.BuildStep is FontAtlasBuildStep.PostBuild) - action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit); - return toolkit; - } - - /// - /// Invokes - /// if of - /// is . - /// - /// The toolkit. - /// The action. - /// toolkit, for method chaining. - public static IFontAtlasBuildToolkit OnPostPromotion( - this IFontAtlasBuildToolkit toolkit, - Action action) - { - if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion) - action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit); - return toolkit; - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs deleted file mode 100644 index ec3e66e9a..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Threading.Tasks; - -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Wrapper for . -/// -public interface IFontAtlas : IDisposable -{ - /// - /// Event to be called on build step changes.
- /// is meaningless for this event. - ///
- event FontAtlasBuildStepDelegate? BuildStepChange; - - /// - /// Event fired when a font rebuild operation is recommended.
- /// This event will be invoked from the main thread.
- ///
- /// Reasons for the event include changes in and - /// initialization of new associated font handles. - ///
- /// - /// You should call or - /// if is not set to true.
- /// Avoid calling here; it will block the main thread. - ///
- event Action? RebuildRecommend; - - /// - /// Gets the name of the atlas. For logging and debugging purposes. - /// - string Name { get; } - - /// - /// Gets a value how the atlas should be rebuilt when the relevant Dalamud Configuration changes. - /// - FontAtlasAutoRebuildMode AutoRebuildMode { get; } - - /// - /// Gets the font atlas. Might be empty. - /// - ImFontAtlasPtr ImAtlas { get; } - - /// - /// Gets the task that represents the current font rebuild state. - /// - Task BuildTask { get; } - - /// - /// Gets a value indicating whether there exists any built atlas, regardless of . - /// - bool HasBuiltAtlas { get; } - - /// - /// Gets a value indicating whether this font atlas is under the effect of global scale. - /// - bool IsGlobalScaled { get; } - - /// - /// Suppresses automatically rebuilding fonts for the scope. - /// - /// An instance of that will release the suppression. - /// - /// Use when you will be creating multiple new handles, and want rebuild to trigger only when you're done doing so. - /// This function will effectively do nothing, if is set to - /// . - /// - /// - /// - /// using (atlas.SuppressBuild()) { - /// this.font1 = atlas.NewGameFontHandle(...); - /// this.font2 = atlas.NewDelegateFontHandle(...); - /// } - /// - /// - public IDisposable SuppressAutoRebuild(); - - /// - /// Creates a new from game's built-in fonts. - /// - /// Font to use. - /// Handle to a font that may or may not be ready yet. - public IFontHandle NewGameFontHandle(GameFontStyle style); - - /// - /// Creates a new IFontHandle using your own callbacks. - /// - /// Callback for . - /// Handle to a font that may or may not be ready yet. - /// - /// On initialization: - /// - /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => { - /// var config = new SafeFontConfig { SizePx = 16 }; - /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); - /// tk.AddGameSymbol(config); - /// tk.AddExtraGlyphsForDalamudLanguage(config); - /// // optionally do the following if you have to add more than one font here, - /// // to specify which font added during this delegate is the final font to use. - /// tk.Font = config.MergeFont; - /// })); - /// // or - /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); - /// - ///
- /// On use: - /// - /// using (this.fontHandle.Push()) - /// ImGui.TextUnformatted("Example"); - /// - ///
- public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate); - - /// - /// Queues rebuilding fonts, on the main thread.
- /// Note that would not necessarily get changed from calling this function. - ///
- /// If is . - void BuildFontsOnNextFrame(); - - /// - /// Rebuilds fonts immediately, on the current thread.
- /// Even the callback for will be called on the same thread. - ///
- /// If is . - void BuildFontsImmediately(); - - /// - /// Rebuilds fonts asynchronously, on any thread. - /// - /// Call on the main thread. - /// The task. - /// If is . - Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs deleted file mode 100644 index 4b016bbb2..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Runtime.InteropServices; - -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Common stuff for and . -/// -public interface IFontAtlasBuildToolkit -{ - /// - /// Gets or sets the font relevant to the call. - /// - ImFontPtr Font { get; set; } - - /// - /// Gets the current scale this font atlas is being built with. - /// - float Scale { get; } - - /// - /// Gets a value indicating whether the current build operation is asynchronous. - /// - bool IsAsyncBuildOperation { get; } - - /// - /// Gets the current build step. - /// - FontAtlasBuildStep BuildStep { get; } - - /// - /// Gets the font atlas being built. - /// - ImFontAtlasPtr NewImAtlas { get; } - - /// - /// Gets the wrapper for of .
- /// This does not need to be disposed. Calling does nothing.- - ///
- /// Modification of this vector may result in undefined behaviors. - ///
- ImVectorWrapper Fonts { get; } - - /// - /// Queues an item to be disposed after the native atlas gets disposed, successful or not. - /// - /// Disposable type. - /// The disposable. - /// The same . - T DisposeWithAtlas(T disposable) where T : IDisposable; - - /// - /// Queues an item to be disposed after the native atlas gets disposed, successful or not. - /// - /// The gc handle. - /// The same . - GCHandle DisposeWithAtlas(GCHandle gcHandle); - - /// - /// Queues an item to be disposed after the native atlas gets disposed, successful or not. - /// - /// The action to run on dispose. - void DisposeWithAtlas(Action action); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs deleted file mode 100644 index 3c14197e0..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Dalamud.Interface.Internal; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Toolkit for use when the build state is . -/// -public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit -{ - /// - /// Gets whether global scaling is ignored for the given font. - /// - /// The font. - /// True if ignored. - bool IsGlobalScaleIgnored(ImFontPtr fontPtr); - - /// - /// Stores a texture to be managed with the atlas. - /// - /// The texture wrap. - /// Dispose the wrap on error. - /// The texture index. - int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs deleted file mode 100644 index 8c3c91624..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs +++ /dev/null @@ -1,33 +0,0 @@ -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Toolkit for use when the build state is . -/// -public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit -{ - /// - /// Copies glyphs across fonts, in a safer way.
- /// If the font does not belong to the current atlas, this function is a no-op. - ///
- /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - /// Low codepoint range to copy. - /// High codepoing range to copy. - void CopyGlyphsAcrossFonts( - ImFontPtr source, - ImFontPtr target, - bool missingOnly, - bool rebuildLookupTable = true, - char rangeLow = ' ', - char rangeHigh = '\uFFFE'); - - /// - /// Calls , with some fixups. - /// - /// The font. - void BuildLookupTable(ImFontPtr font); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs deleted file mode 100644 index cb8a27a54..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ /dev/null @@ -1,186 +0,0 @@ -using System.IO; -using System.Runtime.InteropServices; - -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Toolkit for use when the build state is .
-///
-/// After returns, -/// either must be set, -/// or at least one font must have been added to the atlas using one of AddFont... functions. -///
-public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit -{ - /// - /// Queues an item to be disposed after the whole build process gets complete, successful or not. - /// - /// Disposable type. - /// The disposable. - /// The same . - T DisposeAfterBuild(T disposable) where T : IDisposable; - - /// - /// Queues an item to be disposed after the whole build process gets complete, successful or not. - /// - /// The gc handle. - /// The same . - GCHandle DisposeAfterBuild(GCHandle gcHandle); - - /// - /// Queues an item to be disposed after the whole build process gets complete, successful or not. - /// - /// The action to run on dispose. - void DisposeAfterBuild(Action action); - - /// - /// Excludes given font from global scaling. - /// - /// The font. - /// Same with . - ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); - - /// - /// Gets whether global scaling is ignored for the given font. - /// - /// The font. - /// True if ignored. - bool IsGlobalScaleIgnored(ImFontPtr fontPtr); - - /// - /// Adds a font from memory region allocated using .
- /// It WILL crash if you try to use a memory pointer allocated in some other way.
- /// - /// Do NOT call on the once this function has - /// been called, unless is set and the function has thrown an error. - /// - ///
- /// Memory address for the data allocated using . - /// The size of the font file.. - /// The font config. - /// Free if an exception happens. - /// A debug tag. - /// The newly added font. - unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( - nint dataPointer, - int dataSize, - in SafeFontConfig fontConfig, - bool freeOnException, - string debugTag) - => this.AddFontFromImGuiHeapAllocatedMemory( - (void*)dataPointer, - dataSize, - fontConfig, - freeOnException, - debugTag); - - /// - /// Adds a font from memory region allocated using .
- /// It WILL crash if you try to use a memory pointer allocated in some other way.
- /// Do NOT call on the once this - /// function has been called. - ///
- /// Memory address for the data allocated using . - /// The size of the font file.. - /// The font config. - /// Free if an exception happens. - /// A debug tag. - /// The newly added font. - unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( - void* dataPointer, - int dataSize, - in SafeFontConfig fontConfig, - bool freeOnException, - string debugTag); - - /// - /// Adds a font from a file. - /// - /// The file path to create a new font from. - /// The font config. - /// The newly added font. - ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig); - - /// - /// Adds a font from a stream. - /// - /// The stream to create a new font from. - /// The font config. - /// Dispose when this function returns or throws. - /// A debug tag. - /// The newly added font. - ImFontPtr AddFontFromStream(Stream stream, in SafeFontConfig fontConfig, bool leaveOpen, string debugTag); - - /// - /// Adds a font from memory. - /// - /// The span to create from. - /// The font config. - /// A debug tag. - /// The newly added font. - ImFontPtr AddFontFromMemory(ReadOnlySpan span, in SafeFontConfig fontConfig, string debugTag); - - /// - /// Adds the default font known to the current font atlas.
- ///
- /// Includes and .
- /// As this involves adding multiple fonts, calling this function will set - /// as the return value of this function, if it was empty before. - ///
- /// Font size in pixels. - /// The glyph ranges. Use .ToGlyphRange to build. - /// A font returned from . - ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null); - - /// - /// Adds a font that is shipped with Dalamud.
- ///
- /// Note: if game symbols font file is requested but is unavailable, - /// then it will take the glyphs from game's built-in fonts, and everything in - /// will be ignored but , , - /// and . - ///
- /// The font type. - /// The font config. - /// The added font. - ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig); - - /// - /// Same with (, ...), - /// but using only FontAwesome icon ranges.
- /// will be ignored. - ///
- /// The font config. - /// The added font. - ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig); - - /// - /// Adds the game's symbols into the provided font.
- /// will be ignored.
- /// If the game symbol font file is unavailable, only will be honored. - ///
- /// The font config. - /// The added font. - ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig); - - /// - /// Adds the game glyphs to the font. - /// - /// The font style. - /// The glyph ranges. - /// The font to merge to. If empty, then a new font will be created. - /// The added font. - ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont); - - /// - /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
- /// will be ignored. - ///
- /// The font config. - void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs deleted file mode 100644 index 854594663..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ /dev/null @@ -1,42 +0,0 @@ -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Represents a reference counting handle for fonts. -/// -public interface IFontHandle : IDisposable -{ - /// - /// Represents a reference counting handle for fonts. Dalamud internal use only. - /// - internal interface IInternal : IFontHandle - { - /// - /// Gets the font.
- /// Use of this properly is safe only from the UI thread.
- /// Use if the intended purpose of this property is .
- /// Futures changes may make simple not enough. - ///
- ImFontPtr ImFont { get; } - } - - /// - /// Gets the load exception, if it failed to load. Otherwise, it is null. - /// - Exception? LoadException { get; } - - /// - /// Gets a value indicating whether this font is ready for use.
- /// Use directly if you want to keep the current ImGui font if the font is not ready. - ///
- bool Available { get; } - - /// - /// Pushes the current font into ImGui font stack using , if available.
- /// Use to access the current font.
- /// You may not access the font once you dispose this object. - ///
- /// A disposable object that will call (1) on dispose. - IDisposable Push(); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs deleted file mode 100644 index f0ed09155..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ /dev/null @@ -1,334 +0,0 @@ -using System.Collections.Generic; -using System.Linq; - -using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Logging.Internal; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// A font handle representing a user-callback generated font. -/// -internal class DelegateFontHandle : IFontHandle.IInternal -{ - private IFontHandleManager? manager; - - /// - /// Initializes a new instance of the class. - /// - /// An instance of . - /// Callback for . - public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) - { - this.manager = manager; - this.CallOnBuildStepChange = callOnBuildStepChange; - } - - /// - /// Gets the function to be called on build step changes. - /// - public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } - - /// - public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); - - /// - public bool Available => this.ImFont.IsNotNullAndLoaded(); - - /// - public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; - - private IFontHandleManager ManagerNotDisposed => - this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); - - /// - public void Dispose() - { - this.manager?.FreeFontHandle(this); - this.manager = null; - } - - /// - public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); - - /// - /// Manager for s. - /// - internal sealed class HandleManager : IFontHandleManager - { - private readonly HashSet handles = new(); - private readonly object syncRoot = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The name of the owner atlas. - public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager"; - - /// - public event Action? RebuildRecommend; - - /// - public string Name { get; } - - /// - public IFontHandleSubstance? Substance { get; set; } - - /// - public void Dispose() - { - lock (this.syncRoot) - { - this.handles.Clear(); - this.Substance?.Dispose(); - this.Substance = null; - } - } - - /// - public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) - { - var key = new DelegateFontHandle(this, buildStepDelegate); - lock (this.syncRoot) - this.handles.Add(key); - this.RebuildRecommend?.Invoke(); - return key; - } - - /// - public void FreeFontHandle(IFontHandle handle) - { - if (handle is not DelegateFontHandle cgfh) - return; - - lock (this.syncRoot) - this.handles.Remove(cgfh); - } - - /// - public IFontHandleSubstance NewSubstance() - { - lock (this.syncRoot) - return new HandleSubstance(this, this.handles.ToArray()); - } - } - - /// - /// Substance from . - /// - internal sealed class HandleSubstance : IFontHandleSubstance - { - private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); - - // Not owned by this class. Do not dispose. - private readonly DelegateFontHandle[] relevantHandles; - - // Owned by this class, but ImFontPtr values still do not belong to this. - private readonly Dictionary fonts = new(); - private readonly Dictionary buildExceptions = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The manager. - /// The relevant handles. - public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) - { - this.Manager = manager; - this.relevantHandles = relevantHandles; - } - - /// - public IFontHandleManager Manager { get; } - - /// - public void Dispose() - { - this.fonts.Clear(); - this.buildExceptions.Clear(); - } - - /// - public ImFontPtr GetFontPtr(IFontHandle handle) => - handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default; - - /// - public Exception? GetBuildException(IFontHandle handle) => - handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default; - - /// - public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - var fontsVector = toolkitPreBuild.Fonts; - foreach (var k in this.relevantHandles) - { - var fontCountPrevious = fontsVector.Length; - - try - { - toolkitPreBuild.Font = default; - k.CallOnBuildStepChange(toolkitPreBuild); - if (toolkitPreBuild.Font.IsNull()) - { - if (fontCountPrevious == fontsVector.Length) - { - throw new InvalidOperationException( - $"{nameof(FontAtlasBuildStepDelegate)} must either set the " + - $"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font."); - } - - toolkitPreBuild.Font = fontsVector[^1]; - } - else - { - var found = false; - unsafe - { - for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++) - { - if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr) - found = true; - } - } - - if (!found) - { - throw new InvalidOperationException( - "The font does not exist in the atlas' font array. If you need an empty font, try" + - "adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" + - "glyph range."); - } - } - - if (fontsVector.Length - fontCountPrevious != 1) - { - Log.Warning( - "[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " + - "Using the most recently added font. " + - "Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", - this.Manager.Name, - fontsVector.Length - fontCountPrevious, - nameof(FontAtlasBuildStepDelegate), - nameof(SafeFontConfig), - nameof(SafeFontConfig.MergeFont), - nameof(ImFontConfigPtr), - nameof(ImFontConfigPtr.MergeMode)); - } - - for (var i = fontCountPrevious; i < fontsVector.Length; i++) - { - if (fontsVector[i].ValidateUnsafe() is { } ex) - { - throw new InvalidOperationException( - "One of the newly added fonts seem to be pointing to an invalid memory address.", - ex); - } - } - - // Check for duplicate entries; duplicates will result in free-after-free - for (var i = 0; i < fontCountPrevious; i++) - { - for (var j = fontCountPrevious; j < fontsVector.Length; j++) - { - unsafe - { - if (fontsVector[i].NativePtr == fontsVector[j].NativePtr) - throw new InvalidOperationException("An already added font has been added again."); - } - } - } - - this.fonts[k] = toolkitPreBuild.Font; - } - catch (Exception e) - { - this.fonts[k] = default; - this.buildExceptions[k] = e; - - Log.Error( - e, - "[{name}:Substance] An error has occurred while during {delegate} PreBuild call.", - this.Manager.Name, - nameof(FontAtlasBuildStepDelegate)); - - // Sanitization, in a futile attempt to prevent crashes on invalid parameters - unsafe - { - var distinct = - fontsVector - .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates - .Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them - .ToArray(); - - // We're adding the contents back; do not destroy the contents - fontsVector.Clear(true); - fontsVector.AddRange(distinct.AsSpan()); - } - } - } - } - - /// - public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - // irrelevant - } - - /// - public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) - { - foreach (var k in this.relevantHandles) - { - if (!this.fonts[k].IsNotNullAndLoaded()) - continue; - - try - { - toolkitPostBuild.Font = this.fonts[k]; - k.CallOnBuildStepChange.Invoke(toolkitPostBuild); - } - catch (Exception e) - { - this.fonts[k] = default; - this.buildExceptions[k] = e; - - Log.Error( - e, - "[{name}] An error has occurred while during {delegate} PostBuild call.", - this.Manager.Name, - nameof(FontAtlasBuildStepDelegate)); - } - } - } - - /// - public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) - { - foreach (var k in this.relevantHandles) - { - if (!this.fonts[k].IsNotNullAndLoaded()) - continue; - - try - { - toolkitPostPromotion.Font = this.fonts[k]; - k.CallOnBuildStepChange.Invoke(toolkitPostPromotion); - } - catch (Exception e) - { - this.fonts[k] = default; - this.buildExceptions[k] = e; - - Log.Error( - e, - "[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.", - this.Manager.Name, - nameof(FontAtlasBuildStepDelegate)); - } - } - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs deleted file mode 100644 index e73ea7548..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ /dev/null @@ -1,682 +0,0 @@ -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text.Unicode; - -using Dalamud.Configuration.Internal; -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Storage.Assets; -using Dalamud.Utility; - -using ImGuiNET; - -using SharpDX.DXGI; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Standalone font atlas. -/// -internal sealed partial class FontAtlasFactory -{ - private static readonly Dictionary> PairAdjustmentsCache = - new(); - - /// - /// Implementations for and - /// . - /// - private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable - { - private static readonly ushort FontAwesomeIconMin = - (ushort)Enum.GetValues().Where(x => x > 0).Min(); - - private static readonly ushort FontAwesomeIconMax = - (ushort)Enum.GetValues().Where(x => x > 0).Max(); - - private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new(); - private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance; - private readonly FontAtlasFactory factory; - private readonly FontAtlasBuiltData data; - - /// - /// Initializes a new instance of the class. - /// - /// An instance of . - /// New atlas. - /// An instance of . - /// Specify whether the current build operation is an asynchronous one. - public BuildToolkit( - FontAtlasFactory factory, - FontAtlasBuiltData data, - GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance, - bool isAsync) - { - this.data = data; - this.gameFontHandleSubstance = gameFontHandleSubstance; - this.IsAsyncBuildOperation = isAsync; - this.factory = factory; - } - - /// - public ImFontPtr Font { get; set; } - - /// - public float Scale => this.data.Scale; - - /// - public bool IsAsyncBuildOperation { get; } - - /// - public FontAtlasBuildStep BuildStep { get; set; } - - /// - public ImFontAtlasPtr NewImAtlas => this.data.Atlas; - - /// - public ImVectorWrapper Fonts => this.data.Fonts; - - /// - /// Gets the list of fonts to ignore global scale. - /// - public List GlobalScaleExclusions { get; } = new(); - - /// - public void Dispose() => this.disposeAfterBuild.Dispose(); - - /// - public T2 DisposeAfterBuild(T2 disposable) where T2 : IDisposable => - this.disposeAfterBuild.Add(disposable); - - /// - public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle); - - /// - public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action); - - /// - public T DisposeWithAtlas(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable); - - /// - public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle); - - /// - public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action); - - /// - public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) - { - this.GlobalScaleExclusions.Add(fontPtr); - return fontPtr; - } - - /// - public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => - this.GlobalScaleExclusions.Contains(fontPtr); - - /// - public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => - this.data.AddNewTexture(textureWrap, disposeOnError); - - /// - public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( - void* dataPointer, - int dataSize, - in SafeFontConfig fontConfig, - bool freeOnException, - string debugTag) - { - Log.Verbose( - "[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}", - this.data.Owner?.Name ?? "(error)", - (nint)this.NewImAtlas.NativePtr, - nameof(this.AddFontFromImGuiHeapAllocatedMemory), - (nint)dataPointer, - dataSize, - debugTag); - - try - { - fontConfig.ThrowOnInvalidValues(); - - var raw = fontConfig.Raw with - { - FontData = dataPointer, - FontDataSize = dataSize, - }; - - if (fontConfig.GlyphRanges is not { Length: > 0 } ranges) - ranges = new ushort[] { 1, 0xFFFE, 0 }; - - raw.GlyphRanges = (ushort*)this.DisposeAfterBuild( - GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject(); - - TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw); - - var font = this.NewImAtlas.AddFont(&raw); - - var dataHash = default(HashCode); - dataHash.AddBytes(new(dataPointer, dataSize)); - var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32); - - List<(char Left, char Right, float Distance)> pairAdjustments; - lock (PairAdjustmentsCache) - { - if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments)) - { - PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new()); - try - { - pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray()); - } - catch - { - // don't care - } - } - } - - foreach (var pair in pairAdjustments) - { - if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges)) - continue; - if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges)) - continue; - - font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels); - } - - return font; - } - catch - { - if (freeOnException) - ImGuiNative.igMemFree(dataPointer); - throw; - } - } - - /// - public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig) - { - return this.AddFontFromStream( - File.OpenRead(path), - fontConfig, - false, - $"{nameof(this.AddFontFromFile)}({path})"); - } - - /// - public unsafe ImFontPtr AddFontFromStream( - Stream stream, - in SafeFontConfig fontConfig, - bool leaveOpen, - string debugTag) - { - using var streamCloser = leaveOpen ? null : stream; - if (!stream.CanSeek) - { - // There is no need to dispose a MemoryStream. - var ms = new MemoryStream(); - stream.CopyTo(ms); - stream = ms; - } - - var length = checked((int)(uint)stream.Length); - var memory = ImGuiHelpers.AllocateMemory(length); - try - { - stream.ReadExactly(new(memory, length)); - return this.AddFontFromImGuiHeapAllocatedMemory( - memory, - length, - fontConfig, - false, - $"{nameof(this.AddFontFromStream)}({debugTag})"); - } - catch - { - ImGuiNative.igMemFree(memory); - throw; - } - } - - /// - public unsafe ImFontPtr AddFontFromMemory( - ReadOnlySpan span, - in SafeFontConfig fontConfig, - string debugTag) - { - var length = span.Length; - var memory = ImGuiHelpers.AllocateMemory(length); - try - { - span.CopyTo(new(memory, length)); - return this.AddFontFromImGuiHeapAllocatedMemory( - memory, - length, - fontConfig, - false, - $"{nameof(this.AddFontFromMemory)}({debugTag})"); - } - catch - { - ImGuiNative.igMemFree(memory); - throw; - } - } - - /// - public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) - { - ImFontPtr font; - glyphRanges ??= this.factory.DefaultGlyphRanges; - if (this.factory.UseAxis) - { - font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); - } - else - { - font = this.AddDalamudAssetFont( - DalamudAsset.NotoSansJpMedium, - new() { SizePx = sizePx, GlyphRanges = glyphRanges }); - this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font }); - } - - this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font }); - if (this.Font.IsNull()) - this.Font = font; - return font; - } - - /// - public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig) - { - if (asset.GetPurpose() != DalamudAssetPurpose.Font) - throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font."); - - switch (asset) - { - case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile: - return this.factory.AddFont( - this, - asset, - fontConfig with - { - FontNo = 0, - SizePx = (fontConfig.SizePx * 3) / 2, - }); - - case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: - { - return this.AddGameGlyphs( - new(GameFontFamily.Axis, fontConfig.SizePx), - fontConfig.GlyphRanges, - fontConfig.MergeFont); - } - - default: - return this.factory.AddFont( - this, - asset, - fontConfig with - { - FontNo = 0, - }); - } - } - - /// - public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( - DalamudAsset.FontAwesomeFreeSolid, - fontConfig with - { - GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 }, - }); - - /// - public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) => - this.AddDalamudAssetFont( - DalamudAsset.LodestoneGameSymbol, - fontConfig with - { - GlyphRanges = new ushort[] - { - GamePrebakedFontHandle.SeIconCharMin, - GamePrebakedFontHandle.SeIconCharMax, - 0, - }, - }); - - /// - public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) => - this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges); - - /// - public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) - { - var dalamudConfiguration = Service.Get(); - if (dalamudConfiguration.EffectiveLanguage == "ko" - || Service.GetNullable()?.EncounteredHangul is true) - { - this.AddDalamudAssetFont( - DalamudAsset.NotoSansKrRegular, - fontConfig with - { - GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( - UnicodeRanges.HangulJamo, - UnicodeRanges.HangulCompatibilityJamo, - UnicodeRanges.HangulSyllables, - UnicodeRanges.HangulJamoExtendedA, - UnicodeRanges.HangulJamoExtendedB), - }); - } - - var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); - var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc"); - if (!File.Exists(fontPathChs)) - fontPathChs = null; - - var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc"); - if (!File.Exists(fontPathCht)) - fontPathCht = null; - - if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") - { - this.AddFontFromFile(fontPathCht, fontConfig with - { - GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( - UnicodeRanges.CjkUnifiedIdeographs, - UnicodeRanges.CjkUnifiedIdeographsExtensionA), - }); - } - else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" - || Service.GetNullable()?.EncounteredHan is true)) - { - this.AddFontFromFile(fontPathChs, fontConfig with - { - GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( - UnicodeRanges.CjkUnifiedIdeographs, - UnicodeRanges.CjkUnifiedIdeographsExtensionA), - }); - } - } - - public void PreBuildSubstances() - { - foreach (var substance in this.data.Substances) - substance.OnPreBuild(this); - foreach (var substance in this.data.Substances) - substance.OnPreBuildCleanup(this); - } - - public unsafe void PreBuild() - { - var configData = this.data.ConfigData; - foreach (ref var config in configData.DataSpan) - { - if (this.GlobalScaleExclusions.Contains(new(config.DstFont))) - continue; - - config.SizePixels *= this.Scale; - - config.GlyphMaxAdvanceX *= this.Scale; - if (float.IsInfinity(config.GlyphMaxAdvanceX)) - config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue; - - config.GlyphMinAdvanceX *= this.Scale; - if (float.IsInfinity(config.GlyphMinAdvanceX)) - config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; - - config.GlyphOffset *= this.Scale; - } - } - - public void DoBuild() - { - // ImGui will call AddFontDefault() on Build() call. - // AddFontDefault() will reliably crash, when invoked multithreaded. - // We add a dummy font to prevent that. - if (this.data.ConfigData.Length == 0) - { - this.AddDalamudAssetFont( - DalamudAsset.NotoSansJpMedium, - new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 }); - } - - if (!this.NewImAtlas.Build()) - throw new InvalidOperationException("ImFontAtlas.Build failed"); - - this.BuildStep = FontAtlasBuildStep.PostBuild; - } - - public unsafe void PostBuild() - { - var scale = this.Scale; - foreach (ref var font in this.Fonts.DataSpan) - { - if (!this.GlobalScaleExclusions.Contains(font)) - font.AdjustGlyphMetrics(1 / scale, 1 / scale); - - foreach (var c in FallbackCodepoints) - { - var g = font.FindGlyphNoFallback(c); - if (g.NativePtr == null) - continue; - - font.UpdateFallbackChar(c); - break; - } - - foreach (var c in EllipsisCodepoints) - { - var g = font.FindGlyphNoFallback(c); - if (g.NativePtr == null) - continue; - - font.EllipsisChar = c; - break; - } - } - } - - public void PostBuildSubstances() - { - foreach (var substance in this.data.Substances) - substance.OnPostBuild(this); - } - - public unsafe void UploadTextures() - { - var buf = Array.Empty(); - try - { - var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); - var bpp = use4 ? 2 : 4; - var width = this.NewImAtlas.TexWidth; - var height = this.NewImAtlas.TexHeight; - foreach (ref var texture in this.data.ImTextures.DataSpan) - { - if (texture.TexID != 0) - { - // Nothing to do - } - else if (texture.TexPixelsRGBA32 is not null) - { - var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( - new(texture.TexPixelsRGBA32, width * height * 4), - width * 4, - width, - height, - use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm); - this.data.AddExistingTexture(wrap); - texture.TexID = wrap.ImGuiHandle; - } - else if (texture.TexPixelsAlpha8 is not null) - { - var numPixels = width * height; - if (buf.Length < numPixels * bpp) - { - ArrayPool.Shared.Return(buf); - buf = ArrayPool.Shared.Rent(numPixels * bpp); - } - - fixed (void* pBuf = buf) - { - var sourcePtr = texture.TexPixelsAlpha8; - if (use4) - { - var target = (ushort*)pBuf; - while (numPixels-- > 0) - { - *target = (ushort)((*sourcePtr << 8) | 0x0FFF); - target++; - sourcePtr++; - } - } - else - { - var target = (uint*)pBuf; - while (numPixels-- > 0) - { - *target = (uint)((*sourcePtr << 24) | 0x00FFFFFF); - target++; - sourcePtr++; - } - } - } - - var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( - buf, - width * bpp, - width, - height, - use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm); - this.data.AddExistingTexture(wrap); - texture.TexID = wrap.ImGuiHandle; - continue; - } - else - { - Log.Warning( - "[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null", - this.data.Owner?.Name ?? "(error)"); - } - - if (texture.TexPixelsRGBA32 is not null) - ImGuiNative.igMemFree(texture.TexPixelsRGBA32); - if (texture.TexPixelsAlpha8 is not null) - ImGuiNative.igMemFree(texture.TexPixelsAlpha8); - texture.TexPixelsRGBA32 = null; - texture.TexPixelsAlpha8 = null; - } - } - finally - { - ArrayPool.Shared.Return(buf); - } - } - } - - /// - /// Implementations for . - /// - private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion - { - private readonly FontAtlasBuiltData builtData; - - /// - /// Initializes a new instance of the class. - /// - /// The built data. - public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; - - /// - public ImFontPtr Font { get; set; } - - /// - public float Scale => this.builtData.Scale; - - /// - public bool IsAsyncBuildOperation => true; - - /// - public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; - - /// - public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; - - /// - public unsafe ImVectorWrapper Fonts => new( - &this.NewImAtlas.NativePtr->Fonts, - x => ImGuiNative.ImFont_destroy(x->NativePtr)); - - /// - public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); - - /// - public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); - - /// - public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); - - /// - public unsafe void CopyGlyphsAcrossFonts( - ImFontPtr source, - ImFontPtr target, - bool missingOnly, - bool rebuildLookupTable = true, - char rangeLow = ' ', - char rangeHigh = '\uFFFE') - { - var sourceFound = false; - var targetFound = false; - foreach (var f in this.Fonts) - { - sourceFound |= f.NativePtr == source.NativePtr; - targetFound |= f.NativePtr == target.NativePtr; - } - - if (sourceFound && targetFound) - { - ImGuiHelpers.CopyGlyphsAcrossFonts( - source, - target, - missingOnly, - false, - rangeLow, - rangeHigh); - if (rebuildLookupTable) - this.BuildLookupTable(target); - } - } - - /// - public unsafe void BuildLookupTable(ImFontPtr font) - { - // Need to clear previous Fallback pointers before BuildLookupTable, or it may crash - font.NativePtr->FallbackGlyph = null; - font.NativePtr->FallbackHotData = null; - font.BuildLookupTable(); - - // Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking - // Codepoint < FallbackHotData.size always means that it's not fallback char. - // Otherwise, having a fallback character in ImGui.InputText gets strange. - var indexedHotData = font.IndexedHotDataWrapped(); - var indexLookup = font.IndexLookupWrapped(); - ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.NativePtr->FallbackHotData; - for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++) - { - if (indexLookup[codepoint] == ushort.MaxValue) - { - indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX; - indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth; - } - } - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs deleted file mode 100644 index 5656fc673..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ /dev/null @@ -1,726 +0,0 @@ -// #define VeryVerboseLog - -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reactive.Disposables; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; -using Dalamud.Utility; - -using ImGuiNET; - -using JetBrains.Annotations; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Standalone font atlas. -/// -internal sealed partial class FontAtlasFactory -{ - /// - /// Fallback codepoints for ImFont. - /// - public const string FallbackCodepoints = "\u3013\uFFFD?-"; - - /// - /// Ellipsis codepoints for ImFont. - /// - public const string EllipsisCodepoints = "\u2026\u0085"; - - /// - /// If set, disables concurrent font build operation. - /// - private static readonly object? NoConcurrentBuildOperationLock = null; // new(); - - private static readonly ModuleLog Log = new(nameof(FontAtlasFactory)); - - private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); - - private struct FontAtlasBuiltData : IDisposable - { - public readonly DalamudFontAtlas? Owner; - public readonly ImFontAtlasPtr Atlas; - public readonly float Scale; - - public bool IsBuildInProgress; - - private readonly List? wraps; - private readonly List? substances; - private readonly DisposeSafety.ScopedFinalizer? garbage; - - public unsafe FontAtlasBuiltData( - DalamudFontAtlas owner, - IEnumerable substances, - float scale) - { - this.Owner = owner; - this.Scale = scale; - this.garbage = new(); - - try - { - var substancesList = this.substances = new(); - foreach (var s in substances) - substancesList.Add(this.garbage.Add(s)); - this.garbage.Add(() => substancesList.Clear()); - - var wrapsCopy = this.wraps = new(); - this.garbage.Add(() => wrapsCopy.Clear()); - - var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); - this.Atlas = atlasPtr; - if (this.Atlas.NativePtr is null) - throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); - - this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); - this.IsBuildInProgress = true; - } - catch - { - this.garbage.Dispose(); - throw; - } - } - - public readonly DisposeSafety.ScopedFinalizer Garbage => - this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); - - public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); - - public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); - - public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); - - public readonly IReadOnlyList Wraps => - (IReadOnlyList?)this.wraps ?? Array.Empty(); - - public readonly IReadOnlyList Substances => - (IReadOnlyList?)this.substances ?? Array.Empty(); - - public readonly void AddExistingTexture(IDalamudTextureWrap wrap) - { - if (this.wraps is null) - throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); - - this.wraps.Add(this.Garbage.Add(wrap)); - } - - public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) - { - if (this.wraps is null) - throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); - - var handle = wrap.ImGuiHandle; - var index = this.ImTextures.IndexOf(x => x.TexID == handle); - if (index == -1) - { - try - { - this.wraps.EnsureCapacity(this.wraps.Count + 1); - this.ImTextures.EnsureCapacityExponential(this.ImTextures.Length + 1); - - index = this.ImTextures.Length; - this.wraps.Add(this.Garbage.Add(wrap)); - this.ImTextures.Add(new() { TexID = handle }); - } - catch (Exception e) - { - if (disposeOnError) - wrap.Dispose(); - - if (this.wraps.Count != this.ImTextures.Length) - { - Log.Error( - e, - "{name} failed, and {wraps} and {imtextures} have different number of items", - nameof(this.AddNewTexture), - nameof(this.Wraps), - nameof(this.ImTextures)); - - if (this.wraps.Count > 0 && this.wraps[^1] == wrap) - this.wraps.RemoveAt(this.wraps.Count - 1); - if (this.ImTextures.Length > 0 && this.ImTextures[^1].TexID == handle) - this.ImTextures.RemoveAt(this.ImTextures.Length - 1); - - if (this.wraps.Count != this.ImTextures.Length) - Log.Fatal("^ Failed to undo due to an internal inconsistency; embrace for a crash"); - } - - throw; - } - } - - return index; - } - - public unsafe void Dispose() - { - if (this.garbage is null) - return; - - if (this.IsBuildInProgress) - { - Log.Error( - "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + - "Stack:\n{trace}", - this.Owner?.Name ?? "", - (nint)this.Atlas.NativePtr, - new StackTrace()); - while (this.IsBuildInProgress) - Thread.Sleep(100); - } - -#if VeryVerboseLog - Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); -#endif - this.garbage.Dispose(); - } - - public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) - { - var axisSubstance = this.Substances.OfType().Single(); - return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; - } - } - - private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback - { - private readonly DisposeSafety.ScopedFinalizer disposables = new(); - private readonly FontAtlasFactory factory; - private readonly DelegateFontHandle.HandleManager delegateFontHandleManager; - private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; - private readonly IFontHandleManager[] fontHandleManagers; - - private readonly object syncRootPostPromotion = new(); - private readonly object syncRoot = new(); - - private Task buildTask = EmptyTask; - private FontAtlasBuiltData builtData; - - private int buildSuppressionCounter; - private bool buildSuppressionSuppressed; - - private int buildIndex; - private bool buildQueued; - private bool disposed = false; - - /// - /// Initializes a new instance of the class. - /// - /// The factory. - /// Name of atlas, for debugging and logging purposes. - /// Specify how to auto rebuild. - /// Whether the fonts in the atlas are under the effect of global scale. - public DalamudFontAtlas( - FontAtlasFactory factory, - string atlasName, - FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled) - { - this.IsGlobalScaled = isGlobalScaled; - try - { - this.factory = factory; - this.AutoRebuildMode = autoRebuildMode; - this.Name = atlasName; - - this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend; - this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend); - - this.fontHandleManagers = new IFontHandleManager[] - { - this.delegateFontHandleManager = this.disposables.Add( - new DelegateFontHandle.HandleManager(atlasName)), - this.gameFontHandleManager = this.disposables.Add( - new GamePrebakedFontHandle.HandleManager(atlasName, factory)), - }; - foreach (var fhm in this.fontHandleManagers) - fhm.RebuildRecommend += this.OnRebuildRecommend; - } - catch - { - this.disposables.Dispose(); - throw; - } - - this.factory.SceneTask.ContinueWith( - r => - { - lock (this.syncRoot) - { - if (this.disposed) - return; - - r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame; - this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame); - } - - if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) - this.BuildFontsOnNextFrame(); - }); - } - - /// - /// Finalizes an instance of the class. - /// - ~DalamudFontAtlas() - { - lock (this.syncRoot) - { - this.buildTask.ToDisposableIgnoreExceptions().Dispose(); - this.builtData.Dispose(); - } - } - - /// - public event FontAtlasBuildStepDelegate? BuildStepChange; - - /// - public event Action? RebuildRecommend; - - /// - public event Action? BeforeDispose; - - /// - public event Action? AfterDispose; - - /// - public string Name { get; } - - /// - public FontAtlasAutoRebuildMode AutoRebuildMode { get; } - - /// - public ImFontAtlasPtr ImAtlas - { - get - { - lock (this.syncRoot) - return this.builtData.Atlas; - } - } - - /// - public Task BuildTask => this.buildTask; - - /// - public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); - - /// - public bool IsGlobalScaled { get; } - - /// - public void Dispose() - { - if (this.disposed) - return; - - this.BeforeDispose?.InvokeSafely(this); - - try - { - lock (this.syncRoot) - { - this.disposed = true; - this.buildTask.ToDisposableIgnoreExceptions().Dispose(); - this.buildTask = EmptyTask; - this.disposables.Add(this.builtData); - this.builtData = default; - this.disposables.Dispose(); - } - - try - { - this.AfterDispose?.Invoke(this, null); - } - catch - { - // ignore - } - } - catch (Exception e) - { - try - { - this.AfterDispose?.Invoke(this, e); - } - catch - { - // ignore - } - } - - GC.SuppressFinalize(this); - } - - /// - public IDisposable SuppressAutoRebuild() - { - this.buildSuppressionCounter++; - return Disposable.Create( - () => - { - this.buildSuppressionCounter--; - if (this.buildSuppressionSuppressed) - this.OnRebuildRecommend(); - }); - } - - /// - public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); - - /// - public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => - this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); - - /// - public void BuildFontsOnNextFrame() - { - if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) - { - throw new InvalidOperationException( - $"{nameof(this.BuildFontsOnNextFrame)} cannot be used when " + - $"{nameof(this.AutoRebuildMode)} is set to " + - $"{nameof(FontAtlasAutoRebuildMode.Async)}."); - } - - if (!this.buildTask.IsCompleted || this.buildQueued) - return; - -#if VeryVerboseLog - Log.Verbose("[{name}] Queueing from {source}.", this.Name, nameof(this.BuildFontsOnNextFrame)); -#endif - - this.buildQueued = true; - } - - /// - public void BuildFontsImmediately() - { -#if VeryVerboseLog - Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsImmediately)); -#endif - - if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) - { - throw new InvalidOperationException( - $"{nameof(this.BuildFontsImmediately)} cannot be used when " + - $"{nameof(this.AutoRebuildMode)} is set to " + - $"{nameof(FontAtlasAutoRebuildMode.Async)}."); - } - - var tcs = new TaskCompletionSource(); - int rebuildIndex; - try - { - rebuildIndex = ++this.buildIndex; - lock (this.syncRoot) - { - if (!this.buildTask.IsCompleted) - throw new InvalidOperationException("Font rebuild is already in progress."); - - this.buildTask = tcs.Task; - } - -#if VeryVerboseLog - Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsImmediately)); -#endif - - var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; - var r = this.RebuildFontsPrivate(false, scale); - r.Wait(); - if (r.IsCompletedSuccessfully) - tcs.SetResult(r.Result); - else if (r.Exception is not null) - tcs.SetException(r.Exception); - else - tcs.SetCanceled(); - } - catch (Exception e) - { - tcs.SetException(e); - Log.Error(e, "[{name}] Failed to build fonts.", this.Name); - throw; - } - - this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately)); - } - - /// - public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true) - { -#if VeryVerboseLog - Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync)); -#endif - - if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) - { - throw new InvalidOperationException( - $"{nameof(this.BuildFontsAsync)} cannot be used when " + - $"{nameof(this.AutoRebuildMode)} is set to " + - $"{nameof(FontAtlasAutoRebuildMode.OnNewFrame)}."); - } - - lock (this.syncRoot) - { - var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; - var rebuildIndex = ++this.buildIndex; - return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); - - async Task BuildInner(Task unused) - { - Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); - lock (this.syncRoot) - { - if (this.buildIndex != rebuildIndex) - return default; - } - - var res = await this.RebuildFontsPrivate(true, scale); - if (res.Atlas.IsNull()) - return res; - - if (callPostPromotionOnMainThread) - { - await this.factory.Framework.RunOnFrameworkThread( - () => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync))); - } - else - { - this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); - } - - return res; - } - } - } - - private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) - { - lock (this.syncRoot) - { - if (this.buildIndex != rebuildIndex) - { - data.ExplicitDisposeIgnoreExceptions(); - return; - } - - this.builtData.ExplicitDisposeIgnoreExceptions(); - this.builtData = data; - this.buildTask = EmptyTask; - foreach (var substance in data.Substances) - substance.Manager.Substance = substance; - } - - lock (this.syncRootPostPromotion) - { - if (this.buildIndex != rebuildIndex) - { - data.ExplicitDisposeIgnoreExceptions(); - return; - } - - var toolkit = new BuildToolkitPostPromotion(data); - - try - { - this.BuildStepChange?.Invoke(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {delegateName} PostPromotion error", - this.Name, - nameof(FontAtlasBuildStepDelegate)); - } - - foreach (var substance in data.Substances) - { - try - { - substance.OnPostPromotion(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {substance} PostPromotion error", - this.Name, - substance.GetType().FullName ?? substance.GetType().Name); - } - } - - foreach (var font in toolkit.Fonts) - { - try - { - toolkit.BuildLookupTable(font); - } - catch (Exception e) - { - Log.Error(e, "[{name}] BuildLookupTable error", this.Name); - } - } - -#if VeryVerboseLog - Log.Verbose("[{name}] Built from {source}.", this.Name, source); -#endif - } - } - - private void ImGuiSceneOnNewRenderFrame() - { - if (!this.buildQueued) - return; - - try - { - if (this.AutoRebuildMode != FontAtlasAutoRebuildMode.Async) - this.BuildFontsImmediately(); - } - finally - { - this.buildQueued = false; - } - } - - private Task RebuildFontsPrivate(bool isAsync, float scale) - { - if (NoConcurrentBuildOperationLock is null) - return this.RebuildFontsPrivateReal(isAsync, scale); - lock (NoConcurrentBuildOperationLock) - return this.RebuildFontsPrivateReal(isAsync, scale); - } - - private async Task RebuildFontsPrivateReal(bool isAsync, float scale) - { - lock (this.syncRoot) - { - // this lock ensures that this.buildTask is properly set. - } - - var sw = new Stopwatch(); - sw.Start(); - - var res = default(FontAtlasBuiltData); - nint atlasPtr = 0; - try - { - res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); - unsafe - { - atlasPtr = (nint)res.Atlas.NativePtr; - } - - Log.Verbose( - "[{name}:{functionname}] 0x{ptr:X}: PreBuild (at {sw}ms)", - this.Name, - nameof(this.RebuildFontsPrivateReal), - atlasPtr, - sw.ElapsedMilliseconds); - - using var toolkit = res.CreateToolkit(this.factory, isAsync); - this.BuildStepChange?.Invoke(toolkit); - toolkit.PreBuildSubstances(); - toolkit.PreBuild(); - -#if VeryVerboseLog - Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); -#endif - - toolkit.DoBuild(); - -#if VeryVerboseLog - Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: PostBuild (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); -#endif - - toolkit.PostBuild(); - toolkit.PostBuildSubstances(); - this.BuildStepChange?.Invoke(toolkit); - - if (this.factory.SceneTask is { IsCompleted: false } sceneTask) - { - Log.Verbose( - "[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)", - this.Name, - nameof(this.RebuildFontsPrivateReal), - atlasPtr, - sw.ElapsedMilliseconds); - await sceneTask.ConfigureAwait(!isAsync); - } - -#if VeryVerboseLog - Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: UploadTextures (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); -#endif - toolkit.UploadTextures(); - - Log.Verbose( - "[{name}:{functionname}] 0x{ptr:X}: Complete (at {sw}ms)", - this.Name, - nameof(this.RebuildFontsPrivateReal), - atlasPtr, - sw.ElapsedMilliseconds); - - res.IsBuildInProgress = false; - return res; - } - catch (Exception e) - { - Log.Error( - e, - "[{name}:{functionname}] 0x{ptr:X}: Failed (at {sw}ms)", - this.Name, - nameof(this.RebuildFontsPrivateReal), - atlasPtr, - sw.ElapsedMilliseconds); - res.IsBuildInProgress = false; - res.Dispose(); - throw; - } - finally - { - this.buildQueued = false; - } - } - - private void OnRebuildRecommend() - { - if (this.disposed) - return; - - if (this.buildSuppressionCounter > 0) - { - this.buildSuppressionSuppressed = true; - return; - } - - this.buildSuppressionSuppressed = false; - this.factory.Framework.RunOnFrameworkThread( - () => - { - this.RebuildRecommend?.InvokeSafely(); - - switch (this.AutoRebuildMode) - { - case FontAtlasAutoRebuildMode.Async: - _ = this.BuildFontsAsync(); - break; - case FontAtlasAutoRebuildMode.OnNewFrame: - this.BuildFontsOnNextFrame(); - break; - case FontAtlasAutoRebuildMode.Disable: - default: - break; - } - }); - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs deleted file mode 100644 index 358ccd845..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ /dev/null @@ -1,368 +0,0 @@ -using System.Buffers; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -using Dalamud.Configuration.Internal; -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; -using Dalamud.Storage.Assets; -using Dalamud.Utility; - -using ImGuiNET; - -using ImGuiScene; - -using Lumina.Data.Files; - -using SharpDX; -using SharpDX.Direct3D11; -using SharpDX.DXGI; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Factory for the implementation of . -/// -[ServiceManager.BlockingEarlyLoadedService] -internal sealed partial class FontAtlasFactory - : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable -{ - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private readonly CancellationTokenSource cancellationTokenSource = new(); - private readonly IReadOnlyDictionary> fdtFiles; - private readonly IReadOnlyDictionary[]>> texFiles; - private readonly IReadOnlyDictionary> prebakedTextureWraps; - private readonly Task defaultGlyphRanges; - private readonly DalamudAssetManager dalamudAssetManager; - - [ServiceManager.ServiceConstructor] - private FontAtlasFactory( - DataManager dataManager, - Framework framework, - InterfaceManager interfaceManager, - DalamudAssetManager dalamudAssetManager) - { - this.Framework = framework; - this.InterfaceManager = interfaceManager; - this.dalamudAssetManager = dalamudAssetManager; - this.SceneTask = Service - .GetAsync() - .ContinueWith(r => r.Result.Manager.Scene); - - var gffasInfo = Enum.GetValues() - .Select( - x => - ( - Font: x, - Attr: x.GetAttribute())) - .Where(x => x.Attr is not null) - .ToArray(); - var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray(); - - this.fdtFiles = gffasInfo.ToImmutableDictionary( - x => x.Font, - x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); - var channelCountsTask = texPaths.ToImmutableDictionary( - x => x, - x => Task.WhenAll( - gffasInfo.Where(y => y.Attr.TexPathFormat == x) - .Select(y => this.fdtFiles[y.Font])) - .ContinueWith( - files => 1 + files.Result.Max( - file => - { - unsafe - { - using var pin = file.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Length); - return fdt.MaxTextureIndex; - } - }))); - this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary( - x => x.Key, - x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result])); - this.texFiles = channelCountsTask.ToImmutableDictionary( - x => x.Key, - x => x.Value.ContinueWith( - y => Enumerable - .Range(1, 1 + ((y.Result - 1) / 4)) - .Select(z => Task.Run(() => dataManager.GetFile(string.Format(x.Key, z))!)) - .ToArray())); - this.defaultGlyphRanges = - this.fdtFiles[GameFontFamilyAndSize.Axis12] - .ContinueWith( - file => - { - unsafe - { - using var pin = file.Result.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Result.Length); - return fdt.ToGlyphRanges(); - } - }); - } - - /// - /// Gets or sets a value indicating whether to override configuration for UseAxis. - /// - public bool? UseAxisOverride { get; set; } = null; - - /// - /// Gets a value indicating whether to use AXIS fonts. - /// - public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; - - /// - /// Gets the service instance of . - /// - public Framework Framework { get; } - - /// - /// Gets the service instance of .
- /// may not yet be available. - ///
- public InterfaceManager InterfaceManager { get; } - - /// - /// Gets the async task for inside . - /// - public Task SceneTask { get; } - - /// - /// Gets the default glyph ranges (glyph ranges of ). - /// - public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges); - - /// - /// Gets a value indicating whether game symbol font file is available. - /// - public bool HasGameSymbolsFontFile => - this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); - - /// - public void Dispose() - { - this.cancellationTokenSource.Cancel(); - this.scopedFinalizer.Dispose(); - this.cancellationTokenSource.Dispose(); - } - - /// - /// Creates a new instance of a class that implements the interface. - /// - /// Name of atlas, for debugging and logging purposes. - /// Specify how to auto rebuild. - /// Whether the fonts in the atlas is global scaled. - /// The new font atlas. - public IFontAtlas CreateFontAtlas( - string atlasName, - FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled = true) => - new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled); - - /// - /// Adds the font from Dalamud Assets. - /// - /// The toolkitPostBuild. - /// The font. - /// The font config. - /// The address and size. - public ImFontPtr AddFont( - IFontAtlasBuildToolkitPreBuild toolkitPreBuild, - DalamudAsset asset, - in SafeFontConfig fontConfig) => - toolkitPreBuild.AddFontFromStream( - this.dalamudAssetManager.CreateStream(asset), - fontConfig, - false, - $"Asset({asset})"); - - /// - /// Gets the for the . - /// - /// The font family and size. - /// The . - public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); - - /// - public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) - { - var arr = ExtractResult(this.fdtFiles[gffas]); - var handle = arr.AsMemory().Pin(); - try - { - fdtFileView = new(handle.Pointer, arr.Length); - return handle; - } - catch - { - handle.Dispose(); - throw; - } - } - - /// - public int GetFontTextureCount(string texPathFormat) => - ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length; - - /// - public TexFile GetTexFile(string texPathFormat, int index) => - ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]); - - /// - public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex) - { - lock (this.prebakedTextureWraps[texPathFormat]) - { - var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]); - var fileIndex = textureIndex / 4; - var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; - wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex); - return CloneTextureWrap(wraps[textureIndex]); - } - } - - private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); - - private static unsafe void ExtractChannelFromB8G8R8A8( - Span target, - ReadOnlySpan source, - int channelIndex, - bool targetIsB4G4R4A4) - { - var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); - - fixed (byte* sourcePtrImmutable = source) - { - var rptr = sourcePtrImmutable + channelIndex; - fixed (void* targetPtr = target) - { - if (targetIsB4G4R4A4) - { - var wptr = (ushort*)targetPtr; - while (numPixels-- > 0) - { - *wptr = (ushort)((*rptr << 8) | 0x0FFF); - wptr++; - rptr += 4; - } - } - else - { - var wptr = (uint*)targetPtr; - while (numPixels-- > 0) - { - *wptr = (uint)((*rptr << 24) | 0x00FFFFFF); - wptr++; - rptr += 4; - } - } - } - } - } - - /// - /// Clones a texture wrap, by getting a new reference to the underlying and the - /// texture behind. - /// - /// The to clone from. - /// The cloned . - private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) - { - var srv = CppObject.FromPointer(wrap.ImGuiHandle); - using var res = srv.Resource; - using var tex2D = res.QueryInterface(); - var description = tex2D.Description; - return new DalamudTextureWrap( - new D3DTextureWrap( - srv.QueryInterface(), - description.Width, - description.Height)); - } - - private static unsafe void ExtractChannelFromB4G4R4A4( - Span target, - ReadOnlySpan source, - int channelIndex, - bool targetIsB4G4R4A4) - { - var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); - fixed (byte* sourcePtrImmutable = source) - { - var rptr = sourcePtrImmutable + (channelIndex / 2); - var rshift = (channelIndex & 1) == 0 ? 0 : 4; - fixed (void* targetPtr = target) - { - if (targetIsB4G4R4A4) - { - var wptr = (ushort*)targetPtr; - while (numPixels-- > 0) - { - *wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF); - wptr++; - rptr += 2; - } - } - else - { - var wptr = (uint*)targetPtr; - while (numPixels-- > 0) - { - var v = (*rptr >> rshift) & 0xF; - v |= v << 4; - *wptr = (uint)((v << 24) | 0x00FFFFFF); - wptr++; - rptr += 4; - } - } - } - } - } - - private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) - { - var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); - var numPixels = texFile.Header.Width * texFile.Header.Height; - - _ = Service.Get(); - var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); - var bpp = targetIsB4G4R4A4 ? 2 : 4; - var buffer = ArrayPool.Shared.Rent(numPixels * bpp); - try - { - var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _); - switch (texFile.Header.Format) - { - case TexFile.TextureFormat.B4G4R4A4: - // Game ships with this format. - ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); - break; - case TexFile.TextureFormat.B8G8R8A8: - // In case of modded font textures. - ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); - break; - default: - // Unlikely. - ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4); - break; - } - - return this.scopedFinalizer.Add( - this.InterfaceManager.LoadImageFromDxgiFormat( - buffer, - texFile.Header.Width * bpp, - texFile.Header.Width, - texFile.Header.Height, - targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)); - } - finally - { - ArrayPool.Shared.Return(buffer); - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs deleted file mode 100644 index 99c817a91..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ /dev/null @@ -1,857 +0,0 @@ -using System.Buffers; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reactive.Disposables; - -using Dalamud.Game.Text; -using Dalamud.Interface.GameFonts; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; -using Dalamud.Utility; - -using ImGuiNET; - -using Lumina.Data.Files; - -using Vector4 = System.Numerics.Vector4; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// A font handle that uses the game's built-in fonts, optionally with some styling. -/// -internal class GamePrebakedFontHandle : IFontHandle.IInternal -{ - /// - /// The smallest value of . - /// - public static readonly char SeIconCharMin = (char)Enum.GetValues().Min(); - - /// - /// The largest value of . - /// - public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); - - private IFontHandleManager? manager; - - /// - /// Initializes a new instance of the class. - /// - /// An instance of . - /// Font to use. - public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style) - { - if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined) - throw new ArgumentOutOfRangeException(nameof(style), style, null); - - if (style.SizePt <= 0) - throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style)); - - this.manager = manager; - this.FontStyle = style; - } - - /// - /// Provider for for `common/font/fontNN.tex`. - /// - public interface IGameFontTextureProvider - { - /// - /// Creates the for the .
- /// Dispose after use. - ///
- /// The font family and size. - /// The view. - /// Dispose this after use.. - public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView); - - /// - /// Gets the number of font textures. - /// - /// Format of .tex path. - /// The number of textures. - public int GetFontTextureCount(string texPathFormat); - - /// - /// Gets the for the given index of a font. - /// - /// Format of .tex path. - /// The index of .tex file. - /// The . - public TexFile GetTexFile(string texPathFormat, int index); - - /// - /// Gets a new reference of the font texture. - /// - /// Format of .tex path. - /// Texture index. - /// The texture. - public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex); - } - - /// - /// Gets the font style. - /// - public GameFontStyle FontStyle { get; } - - /// - public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); - - /// - public bool Available => this.ImFont.IsNotNullAndLoaded(); - - /// - public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; - - private IFontHandleManager ManagerNotDisposed => - this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); - - /// - public void Dispose() - { - this.manager?.FreeFontHandle(this); - this.manager = null; - } - - /// - public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); - - /// - /// Manager for s. - /// - internal sealed class HandleManager : IFontHandleManager - { - private readonly Dictionary gameFontsRc = new(); - private readonly object syncRoot = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The name of the owner atlas. - /// An instance of . - public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider) - { - this.GameFontTextureProvider = gameFontTextureProvider; - this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager"; - } - - /// - public event Action? RebuildRecommend; - - /// - public string Name { get; } - - /// - public IFontHandleSubstance? Substance { get; set; } - - /// - /// Gets an instance of . - /// - public IGameFontTextureProvider GameFontTextureProvider { get; } - - /// - public void Dispose() - { - this.Substance?.Dispose(); - this.Substance = null; - } - - /// - public IFontHandle NewFontHandle(GameFontStyle style) - { - var handle = new GamePrebakedFontHandle(this, style); - bool suggestRebuild; - lock (this.syncRoot) - { - this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; - suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; - } - - if (suggestRebuild) - this.RebuildRecommend?.Invoke(); - - return handle; - } - - /// - public void FreeFontHandle(IFontHandle handle) - { - if (handle is not GamePrebakedFontHandle ggfh) - return; - - lock (this.syncRoot) - { - if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) - return; - - if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0) - this.gameFontsRc.Remove(ggfh.FontStyle); - } - } - - /// - public IFontHandleSubstance NewSubstance() - { - lock (this.syncRoot) - return new HandleSubstance(this, this.gameFontsRc.Keys); - } - } - - /// - /// Substance from . - /// - internal sealed class HandleSubstance : IFontHandleSubstance - { - private readonly HandleManager handleManager; - private readonly HashSet gameFontStyles; - - // Owned by this class, but ImFontPtr values still do not belong to this. - private readonly Dictionary fonts = new(); - private readonly Dictionary buildExceptions = new(); - private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new(); - - private readonly HashSet templatedFonts = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The manager. - /// The game font styles. - public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) - { - this.handleManager = manager; - Service.Get(); - this.gameFontStyles = new(gameFontStyles); - } - - /// - public IFontHandleManager Manager => this.handleManager; - - /// - public void Dispose() - { - } - - /// - /// Attaches game symbols to the given font. If font is null, it will be created. - /// - /// The toolkitPostBuild. - /// The font to attach to. - /// The game font style. - /// The intended glyph ranges. - /// if it is not empty; otherwise a new font. - public ImFontPtr AttachGameGlyphs( - IFontAtlasBuildToolkitPreBuild toolkitPreBuild, - ImFontPtr font, - GameFontStyle style, - ushort[]? glyphRanges = null) - { - if (font.IsNull()) - font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx); - this.attachments.Add((font, style, glyphRanges)); - return font; - } - - /// - /// Creates or gets a relevant for the given . - /// - /// The game font style. - /// The toolkitPostBuild. - /// The font. - public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - try - { - if (!this.fonts.TryGetValue(style, out var plan)) - { - plan = new( - style, - toolkitPreBuild.Scale, - this.handleManager.GameFontTextureProvider, - this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); - this.fonts[style] = plan; - } - - plan.AttachFont(plan.FullRangeFont); - return plan.FullRangeFont; - } - catch (Exception e) - { - this.buildExceptions[style] = e; - throw; - } - } - - /// - public ImFontPtr GetFontPtr(IFontHandle handle) => - handle is GamePrebakedFontHandle ggfh - ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default - : default; - - /// - public Exception? GetBuildException(IFontHandle handle) => - handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default; - - /// - public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - foreach (var style in this.gameFontStyles) - { - if (this.fonts.ContainsKey(style)) - continue; - - try - { - _ = this.GetOrCreateFont(style, toolkitPreBuild); - } - catch - { - // ignore; it should have been recorded from the call - } - } - } - - /// - public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) - { - foreach (var (font, style, ranges) in this.attachments) - { - var effectiveStyle = - toolkitPreBuild.IsGlobalScaleIgnored(font) - ? style.Scale(1 / toolkitPreBuild.Scale) - : style; - if (!this.fonts.TryGetValue(style, out var plan)) - { - plan = new( - effectiveStyle, - toolkitPreBuild.Scale, - this.handleManager.GameFontTextureProvider, - this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); - this.fonts[style] = plan; - } - - plan.AttachFont(font, ranges); - } - - foreach (var plan in this.fonts.Values) - { - plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas); - } - } - - /// - public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) - { - var allTextureIndices = new Dictionary(); - var allTexFiles = new Dictionary(); - using var rentReturn = Disposable.Create( - () => - { - foreach (var x in allTextureIndices.Values) - ArrayPool.Shared.Return(x); - foreach (var x in allTexFiles.Values) - ArrayPool.Shared.Return(x); - }); - - var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; - var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; - for (var i = 0; i < pixels8Array.Length; i++) - toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _); - - foreach (var (style, plan) in this.fonts) - { - try - { - foreach (var font in plan.Ranges.Keys) - this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale); - - plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths); - plan.CopyGlyphsToRanges(toolkitPostBuild); - plan.PostProcessFullRangeFont(toolkitPostBuild.Scale); - } - catch (Exception e) - { - this.buildExceptions[style] = e; - this.fonts[style] = default; - } - } - } - - /// - public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) - { - // Irrelevant - } - - /// - /// Creates a new template font. - /// - /// The toolkitPostBuild. - /// The size of the font. - /// The font. - private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx) - { - var font = toolkitPreBuild.AddDalamudAssetFont( - DalamudAsset.NotoSansJpMedium, - new() - { - GlyphRanges = new ushort[] { ' ', ' ', '\0' }, - SizePx = sizePx, - }); - this.templatedFonts.Add(font); - return font; - } - - private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale) - { - if (!this.templatedFonts.Contains(font)) - return; - - var fas = style.Scale(atlasScale).FamilyAndSize; - using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); - ref var fdtFontHeader = ref fdt.FontHeader; - var fontPtr = font.NativePtr; - - var scale = style.SizePt / fdtFontHeader.Size; - fontPtr->Ascent = fdtFontHeader.Ascent * scale; - fontPtr->Descent = fdtFontHeader.Descent * scale; - fontPtr->EllipsisChar = '…'; - } - } - - [SuppressMessage( - "StyleCop.CSharp.MaintainabilityRules", - "SA1401:Fields should be private", - Justification = "Internal")] - private sealed class FontDrawPlan : IDisposable - { - public readonly GameFontStyle Style; - public readonly GameFontStyle BaseStyle; - public readonly GameFontFamilyAndSizeAttribute BaseAttr; - public readonly int TexCount; - public readonly Dictionary Ranges = new(); - public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new(); - public readonly ushort[] RectLookup = new ushort[0x10000]; - public readonly FdtFileView Fdt; - public readonly ImFontPtr FullRangeFont; - - private readonly IDisposable fdtHandle; - private readonly IGameFontTextureProvider gftp; - - public FontDrawPlan( - GameFontStyle style, - float scale, - IGameFontTextureProvider gameFontTextureProvider, - ImFontPtr fullRangeFont) - { - this.Style = style; - this.BaseStyle = style.Scale(scale); - this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute()!; - this.gftp = gameFontTextureProvider; - this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat); - this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt); - this.RectLookup.AsSpan().Fill(ushort.MaxValue); - this.FullRangeFont = fullRangeFont; - this.Ranges[fullRangeFont] = new(0x10000); - } - - public void Dispose() - { - this.fdtHandle.Dispose(); - } - - public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null) - { - if (!this.Ranges.TryGetValue(font, out var rangeBitArray)) - rangeBitArray = this.Ranges[font] = new(0x10000); - - if (glyphRanges is null) - { - foreach (ref var g in this.Fdt.Glyphs) - { - var c = g.CharInt; - if (c is >= 0x20 and <= 0xFFFE) - rangeBitArray[c] = true; - } - - return; - } - - for (var i = 0; i < glyphRanges.Length - 1; i += 2) - { - if (glyphRanges[i] == 0) - break; - var from = (int)glyphRanges[i]; - var to = (int)glyphRanges[i + 1]; - for (var j = from; j <= to; j++) - rangeBitArray[j] = true; - } - } - - public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas) - { - var glyphs = this.Fdt.Glyphs; - var ranges = this.Ranges[this.FullRangeFont]; - foreach (var (font, extraRange) in this.Ranges) - { - if (font.NativePtr != this.FullRangeFont.NativePtr) - ranges.Or(extraRange); - } - - if (this.Style is not { Weight: 0, SkewStrength: 0 }) - { - for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) - { - ref var glyph = ref glyphs[fdtGlyphIndex]; - var cint = glyph.CharInt; - if (cint > char.MaxValue) - continue; - if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) - continue; - - var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph); - this.RectLookup[cint] = (ushort)this.Rects.Count; - this.Rects.Add( - ( - atlas.AddCustomRectFontGlyph( - this.FullRangeFont, - (char)cint, - glyph.BoundingWidth + widthAdjustment, - glyph.BoundingHeight, - glyph.AdvanceWidth, - new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)), - fdtGlyphIndex)); - } - } - else - { - for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) - { - ref var glyph = ref glyphs[fdtGlyphIndex]; - var cint = glyph.CharInt; - if (cint > char.MaxValue) - continue; - if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) - continue; - - this.RectLookup[cint] = (ushort)this.Rects.Count; - this.Rects.Add((-1, fdtGlyphIndex)); - } - } - } - - public unsafe void PostProcessFullRangeFont(float atlasScale) - { - var round = 1 / atlasScale; - var pfrf = this.FullRangeFont.NativePtr; - ref var frf = ref *pfrf; - - frf.FontSize = MathF.Round(frf.FontSize / round) * round; - frf.Ascent = MathF.Round(frf.Ascent / round) * round; - frf.Descent = MathF.Round(frf.Descent / round) * round; - - var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; - foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan) - { - var w = (g.X1 - g.X0) * scale; - var h = (g.Y1 - g.Y0) * scale; - g.X0 = MathF.Round((g.X0 * scale) / round) * round; - g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; - g.X1 = g.X0 + w; - g.Y1 = g.Y0 + h; - g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; - } - - var fullRange = this.Ranges[this.FullRangeFont]; - foreach (ref var k in this.Fdt.PairAdjustments) - { - var (leftInt, rightInt) = (k.LeftInt, k.RightInt); - if (leftInt > char.MaxValue || rightInt > char.MaxValue) - continue; - if (!fullRange[leftInt] || !fullRange[rightInt]) - continue; - ImGuiNative.ImFont_AddKerningPair( - pfrf, - (ushort)leftInt, - (ushort)rightInt, - MathF.Round((k.RightOffset * scale) / round) * round); - } - - pfrf->FallbackGlyph = null; - ImGuiNative.ImFont_BuildLookupTable(pfrf); - - foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) - { - var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate); - if ((nint)glyph == IntPtr.Zero) - continue; - frf.FallbackChar = fallbackCharCandidate; - frf.FallbackGlyph = glyph; - frf.FallbackHotData = - (ImFontGlyphHotData*)frf.IndexedHotData.Address( - fallbackCharCandidate); - break; - } - } - - public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) - { - var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; - var atlasScale = toolkitPostBuild.Scale; - var round = 1 / atlasScale; - - foreach (var (font, rangeBits) in this.Ranges) - { - if (font.NativePtr == this.FullRangeFont.NativePtr) - continue; - - var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font); - - var lookup = font.IndexLookupWrapped(); - var glyphs = font.GlyphsWrapped(); - foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan) - { - if (!rangeBits[sourceGlyph.Codepoint]) - continue; - - var glyphIndex = ushort.MaxValue; - if (sourceGlyph.Codepoint < lookup.Length) - glyphIndex = lookup[sourceGlyph.Codepoint]; - - if (glyphIndex == ushort.MaxValue) - { - glyphIndex = (ushort)glyphs.Length; - glyphs.Add(default); - } - - ref var g = ref glyphs[glyphIndex]; - g = sourceGlyph; - if (noGlobalScale) - { - g.XY *= scale; - g.AdvanceX *= scale; - } - else - { - var w = (g.X1 - g.X0) * scale; - var h = (g.Y1 - g.Y0) * scale; - g.X0 = MathF.Round((g.X0 * scale) / round) * round; - g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; - g.X1 = g.X0 + w; - g.Y1 = g.Y0 + h; - g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; - } - } - - foreach (ref var k in this.Fdt.PairAdjustments) - { - var (leftInt, rightInt) = (k.LeftInt, k.RightInt); - if (leftInt > char.MaxValue || rightInt > char.MaxValue) - continue; - if (!rangeBits[leftInt] || !rangeBits[rightInt]) - continue; - if (noGlobalScale) - { - font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale); - } - else - { - font.AddKerningPair( - (ushort)leftInt, - (ushort)rightInt, - MathF.Round((k.RightOffset * scale) / round) * round); - } - } - - font.NativePtr->FallbackGlyph = null; - font.BuildLookupTable(); - - foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) - { - var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr; - if ((nint)glyph == IntPtr.Zero) - continue; - - ref var frf = ref *font.NativePtr; - frf.FallbackChar = fallbackCharCandidate; - frf.FallbackGlyph = glyph; - frf.FallbackHotData = - (ImFontGlyphHotData*)frf.IndexedHotData.Address( - fallbackCharCandidate); - break; - } - } - } - - public unsafe void SetFullRangeFontGlyphs( - IFontAtlasBuildToolkitPostBuild toolkitPostBuild, - Dictionary allTexFiles, - Dictionary allTextureIndices, - byte*[] pixels8Array, - int[] widths) - { - var glyphs = this.FullRangeFont.GlyphsWrapped(); - var lookups = this.FullRangeFont.IndexLookupWrapped(); - - ref var fdtFontHeader = ref this.Fdt.FontHeader; - var fdtGlyphs = this.Fdt.Glyphs; - var fdtTexSize = new Vector4( - this.Fdt.FontHeader.TextureWidth, - this.Fdt.FontHeader.TextureHeight, - this.Fdt.FontHeader.TextureWidth, - this.Fdt.FontHeader.TextureHeight); - - if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles)) - { - allTexFiles.Add( - this.BaseAttr.TexPathFormat, - texFiles = ArrayPool.Shared.Rent(this.TexCount)); - } - - if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices)) - { - allTextureIndices.Add( - this.BaseAttr.TexPathFormat, - textureIndices = ArrayPool.Shared.Rent(this.TexCount)); - textureIndices.AsSpan(0, this.TexCount).Fill(-1); - } - - var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1)); - var pixelStrength = stackalloc byte[pixelWidth]; - for (var i = 0; i < pixelWidth; i++) - pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i)); - - var minGlyphY = 0; - var maxGlyphY = 0; - foreach (ref var g in fdtGlyphs) - { - minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY); - maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY); - } - - var horzShift = stackalloc int[maxGlyphY - minGlyphY]; - var horzBlend = stackalloc byte[maxGlyphY - minGlyphY]; - horzShift -= minGlyphY; - horzBlend -= minGlyphY; - if (this.BaseStyle.BaseSkewStrength != 0) - { - for (var i = minGlyphY; i < maxGlyphY; i++) - { - float blend = this.BaseStyle.BaseSkewStrength switch - { - > 0 => fdtFontHeader.LineHeight - i, - < 0 => -i, - _ => throw new InvalidOperationException(), - }; - blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight; - horzShift[i] = (int)MathF.Floor(blend); - horzBlend[i] = (byte)(255 * (blend - horzShift[i])); - } - } - - foreach (var (rectId, fdtGlyphIndex) in this.Rects) - { - ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex]; - if (rectId == -1) - { - ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex]; - if (textureIndex == -1) - { - textureIndex = toolkitPostBuild.StoreTexture( - this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex), - true); - } - - var glyph = new ImGuiHelpers.ImFontGlyphReal - { - AdvanceX = fdtGlyph.AdvanceWidth, - Codepoint = fdtGlyph.Char, - Colored = false, - TextureIndex = textureIndex, - Visible = true, - X0 = this.BaseAttr.HorizontalOffset, - Y0 = fdtGlyph.CurrentOffsetY, - U0 = fdtGlyph.TextureOffsetX, - V0 = fdtGlyph.TextureOffsetY, - U1 = fdtGlyph.BoundingWidth, - V1 = fdtGlyph.BoundingHeight, - }; - - glyph.XY1 = glyph.XY0 + glyph.UV1; - glyph.UV1 += glyph.UV0; - glyph.UV /= fdtTexSize; - - glyphs.Add(glyph); - } - else - { - ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas - .GetCustomRectByIndex(rectId) - .NativePtr; - var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph); - - // Glyph is scaled at this point; undo that. - ref var glyph = ref glyphs[lookups[rc.GlyphId]]; - glyph.X0 = this.BaseAttr.HorizontalOffset; - glyph.Y0 = fdtGlyph.CurrentOffsetY; - glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment; - glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight; - glyph.AdvanceX = fdtGlyph.AdvanceWidth; - - var pixels8 = pixels8Array[rc.TextureIndex]; - var width = widths[rc.TextureIndex]; - texFiles[fdtGlyph.TextureFileIndex] ??= - this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex); - var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData; - var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex; - - for (var y = 0; y < fdtGlyph.BoundingHeight; y++) - { - var sourcePixelIndex = - ((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX; - sourcePixelIndex *= 4; - sourcePixelIndex += sourceBufferDelta; - var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y]; - - var targetOffset = ((rc.Y + y) * width) + rc.X; - for (var x = 0; x < rc.Width; x++) - pixels8[targetOffset + x] = 0; - - targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y]; - if (blend1 == 0) - { - for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) - { - var n = sourceBuffer[sourcePixelIndex + 4]; - for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) - { - ref var p = ref pixels8[targetOffset + boldOffset]; - p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255)); - } - } - } - else - { - var blend2 = 255 - blend1; - for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) - { - var a1 = sourceBuffer[sourcePixelIndex]; - var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4]; - var n = (a1 * blend1) + (a2 * blend2); - - for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) - { - ref var p = ref pixels8[targetOffset + boldOffset]; - p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255)); - } - } - } - } - } - } - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs deleted file mode 100644 index 93c688608..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Manager for . -/// -internal interface IFontHandleManager : IDisposable -{ - /// - event Action? RebuildRecommend; - - /// - /// Gets the name of the font handle manager. For logging and debugging purposes. - /// - string Name { get; } - - /// - /// Gets or sets the active font handle substance. - /// - IFontHandleSubstance? Substance { get; set; } - - /// - /// Decrease font reference counter. - /// - /// Handle being released. - void FreeFontHandle(IFontHandle handle); - - /// - /// Creates a new substance of the font atlas. - /// - /// The new substance. - IFontHandleSubstance NewSubstance(); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs deleted file mode 100644 index f6c5c6591..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ /dev/null @@ -1,54 +0,0 @@ -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Substance of a font. -/// -internal interface IFontHandleSubstance : IDisposable -{ - /// - /// Gets the manager relevant to this instance of . - /// - IFontHandleManager Manager { get; } - - /// - /// Gets the font. - /// - /// The handle to get from. - /// Corresponding font or null. - ImFontPtr GetFontPtr(IFontHandle handle); - - /// - /// Gets the exception happened while loading for the font. - /// - /// The handle to get from. - /// Corresponding font or null. - Exception? GetBuildException(IFontHandle handle); - - /// - /// Called before call. - /// - /// The toolkit. - void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); - - /// - /// Called between and calls.
- /// Any further modification to will result in undefined behavior. - ///
- /// The toolkit. - void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); - - /// - /// Called after call. - /// - /// The toolkit. - void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild); - - /// - /// Called on the specific thread depending on after - /// promoting the staging atlas to direct use with . - /// - /// The toolkit. - void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion); -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs deleted file mode 100644 index 8e7149853..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System.Buffers.Binary; -using System.Runtime.InteropServices; -using System.Text; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - private struct Fixed : IComparable - { - public ushort Major; - public ushort Minor; - - public Fixed(ushort major, ushort minor) - { - this.Major = major; - this.Minor = minor; - } - - public Fixed(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.Major); - span.ReadBig(ref offset, out this.Minor); - } - - public int CompareTo(Fixed other) - { - var majorComparison = this.Major.CompareTo(other.Major); - return majorComparison != 0 ? majorComparison : this.Minor.CompareTo(other.Minor); - } - } - - private struct KerningPair : IEquatable - { - public ushort Left; - public ushort Right; - public short Value; - - public KerningPair(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.Left); - span.ReadBig(ref offset, out this.Right); - span.ReadBig(ref offset, out this.Value); - } - - public KerningPair(ushort left, ushort right, short value) - { - this.Left = left; - this.Right = right; - this.Value = value; - } - - public static bool operator ==(KerningPair left, KerningPair right) => left.Equals(right); - - public static bool operator !=(KerningPair left, KerningPair right) => !left.Equals(right); - - public static KerningPair ReverseEndianness(KerningPair pair) => new() - { - Left = BinaryPrimitives.ReverseEndianness(pair.Left), - Right = BinaryPrimitives.ReverseEndianness(pair.Right), - Value = BinaryPrimitives.ReverseEndianness(pair.Value), - }; - - public bool Equals(KerningPair other) => - this.Left == other.Left && this.Right == other.Right && this.Value == other.Value; - - public override bool Equals(object? obj) => obj is KerningPair other && this.Equals(other); - - public override int GetHashCode() => HashCode.Combine(this.Left, this.Right, this.Value); - - public override string ToString() => $"KerningPair[{this.Left}, {this.Right}] = {this.Value}"; - } - - [StructLayout(LayoutKind.Explicit, Size = 4)] - private struct PlatformAndEncoding - { - [FieldOffset(0)] - public PlatformId Platform; - - [FieldOffset(2)] - public UnicodeEncodingId UnicodeEncoding; - - [FieldOffset(2)] - public MacintoshEncodingId MacintoshEncoding; - - [FieldOffset(2)] - public IsoEncodingId IsoEncoding; - - [FieldOffset(2)] - public WindowsEncodingId WindowsEncoding; - - public PlatformAndEncoding(PointerSpan source) - { - var offset = 0; - source.ReadBig(ref offset, out this.Platform); - source.ReadBig(ref offset, out this.UnicodeEncoding); - } - - public static PlatformAndEncoding ReverseEndianness(PlatformAndEncoding value) => new() - { - Platform = (PlatformId)BinaryPrimitives.ReverseEndianness((ushort)value.Platform), - UnicodeEncoding = (UnicodeEncodingId)BinaryPrimitives.ReverseEndianness((ushort)value.UnicodeEncoding), - }; - - public readonly string Decode(Span data) - { - switch (this.Platform) - { - case PlatformId.Unicode: - switch (this.UnicodeEncoding) - { - case UnicodeEncodingId.Unicode_2_0_Bmp: - case UnicodeEncodingId.Unicode_2_0_Full: - return Encoding.BigEndianUnicode.GetString(data); - } - - break; - - case PlatformId.Macintosh: - switch (this.MacintoshEncoding) - { - case MacintoshEncodingId.Roman: - return Encoding.ASCII.GetString(data); - } - - break; - - case PlatformId.Windows: - switch (this.WindowsEncoding) - { - case WindowsEncodingId.Symbol: - case WindowsEncodingId.UnicodeBmp: - case WindowsEncodingId.UnicodeFullRepertoire: - return Encoding.BigEndianUnicode.GetString(data); - } - - break; - } - - throw new NotSupportedException(); - } - } - - [StructLayout(LayoutKind.Explicit)] - private struct TagStruct : IEquatable, IComparable - { - [FieldOffset(0)] - public unsafe fixed byte Tag[4]; - - [FieldOffset(0)] - public uint NativeValue; - - public unsafe TagStruct(char c1, char c2, char c3, char c4) - { - this.Tag[0] = checked((byte)c1); - this.Tag[1] = checked((byte)c2); - this.Tag[2] = checked((byte)c3); - this.Tag[3] = checked((byte)c4); - } - - public unsafe TagStruct(PointerSpan span) - { - this.Tag[0] = span[0]; - this.Tag[1] = span[1]; - this.Tag[2] = span[2]; - this.Tag[3] = span[3]; - } - - public unsafe TagStruct(ReadOnlySpan span) - { - this.Tag[0] = span[0]; - this.Tag[1] = span[1]; - this.Tag[2] = span[2]; - this.Tag[3] = span[3]; - } - - public unsafe byte this[int index] - { - get => this.Tag[index]; - set => this.Tag[index] = value; - } - - public static bool operator ==(TagStruct left, TagStruct right) => left.Equals(right); - - public static bool operator !=(TagStruct left, TagStruct right) => !left.Equals(right); - - public bool Equals(TagStruct other) => this.NativeValue == other.NativeValue; - - public override bool Equals(object? obj) => obj is TagStruct other && this.Equals(other); - - public override int GetHashCode() => (int)this.NativeValue; - - public int CompareTo(TagStruct other) => this.NativeValue.CompareTo(other.NativeValue); - - public override unsafe string ToString() => - $"0x{this.NativeValue:08X} \"{(char)this.Tag[0]}{(char)this.Tag[1]}{(char)this.Tag[2]}{(char)this.Tag[3]}\""; - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs deleted file mode 100644 index f6a653a51..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] - private enum IsoEncodingId : ushort - { - Ascii = 0, - Iso_10646 = 1, - Iso_8859_1 = 2, - } - - private enum MacintoshEncodingId : ushort - { - Roman = 0, - } - - private enum NameId : ushort - { - CopyrightNotice = 0, - FamilyName = 1, - SubfamilyName = 2, - UniqueId = 3, - FullFontName = 4, - VersionString = 5, - PostScriptName = 6, - Trademark = 7, - Manufacturer = 8, - Designer = 9, - Description = 10, - UrlVendor = 11, - UrlDesigner = 12, - LicenseDescription = 13, - LicenseInfoUrl = 14, - TypographicFamilyName = 16, - TypographicSubfamilyName = 17, - CompatibleFullMac = 18, - SampleText = 19, - PoscSriptCidFindFontName = 20, - WwsFamilyName = 21, - WwsSubfamilyName = 22, - LightBackgroundPalette = 23, - DarkBackgroundPalette = 24, - VariationPostScriptNamePrefix = 25, - } - - private enum PlatformId : ushort - { - Unicode = 0, - Macintosh = 1, // discouraged - Iso = 2, // deprecated - Windows = 3, - Custom = 4, // OTF Windows NT compatibility mapping - } - - [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] - private enum UnicodeEncodingId : ushort - { - Unicode_1_0 = 0, // deprecated - Unicode_1_1 = 1, // deprecated - IsoIec_10646 = 2, // deprecated - Unicode_2_0_Bmp = 3, - Unicode_2_0_Full = 4, - UnicodeVariationSequences = 5, - UnicodeFullRepertoire = 6, - } - - private enum WindowsEncodingId : ushort - { - Symbol = 0, - UnicodeBmp = 1, - ShiftJis = 2, - Prc = 3, - Big5 = 4, - Wansung = 5, - Johab = 6, - UnicodeFullRepertoire = 10, - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs deleted file mode 100644 index 3d89dd806..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] -[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] -[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] -[SuppressMessage( - "StyleCop.CSharp.NamingRules", - "SA1310:Field names should not contain underscore", - Justification = "Version name")] -[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name")] -internal static partial class TrueTypeUtils -{ - private readonly struct SfntFile : IReadOnlyDictionary> - { - // http://formats.kaitai.io/ttf/ttf.svg - - public static readonly TagStruct FileTagTrueType1 = new('1', '\0', '\0', '\0'); - public static readonly TagStruct FileTagType1 = new('t', 'y', 'p', '1'); - public static readonly TagStruct FileTagOpenTypeWithCff = new('O', 'T', 'T', 'O'); - public static readonly TagStruct FileTagOpenType1_0 = new('\0', '\x01', '\0', '\0'); - public static readonly TagStruct FileTagTrueTypeApple = new('t', 'r', 'u', 'e'); - - public readonly PointerSpan Memory; - public readonly int OffsetInCollection; - public readonly ushort TableCount; - - public SfntFile(PointerSpan memory, int offsetInCollection = 0) - { - var span = memory.Span; - this.Memory = memory; - this.OffsetInCollection = offsetInCollection; - this.TableCount = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); - } - - public int Count => this.TableCount; - - public IEnumerable Keys => this.Select(x => x.Key); - - public IEnumerable> Values => this.Select(x => x.Value); - - public PointerSpan this[TagStruct key] => this.First(x => x.Key == key).Value; - - public IEnumerator>> GetEnumerator() - { - var offset = 12; - for (var i = 0; i < this.TableCount; i++) - { - var dte = new DirectoryTableEntry(this.Memory[offset..]); - yield return new(dte.Tag, this.Memory.Slice(dte.Offset - this.OffsetInCollection, dte.Length)); - - offset += Unsafe.SizeOf(); - } - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - public bool ContainsKey(TagStruct key) => this.Any(x => x.Key == key); - - public bool TryGetValue(TagStruct key, out PointerSpan value) - { - foreach (var (k, v) in this) - { - if (k == key) - { - value = v; - return true; - } - } - - value = default; - return false; - } - - public readonly struct DirectoryTableEntry - { - public readonly PointerSpan Memory; - - public DirectoryTableEntry(PointerSpan span) => this.Memory = span; - - public TagStruct Tag => new(this.Memory); - - public uint Checksum => this.Memory.ReadU32Big(4); - - public int Offset => this.Memory.ReadI32Big(8); - - public int Length => this.Memory.ReadI32Big(12); - } - } - - private readonly struct TtcFile : IReadOnlyList - { - public static readonly TagStruct FileTag = new('t', 't', 'c', 'f'); - - public readonly PointerSpan Memory; - public readonly TagStruct Tag; - public readonly ushort MajorVersion; - public readonly ushort MinorVersion; - public readonly int FontCount; - - public TtcFile(PointerSpan memory) - { - var span = memory.Span; - this.Memory = memory; - this.Tag = new(span); - if (this.Tag != FileTag) - throw new InvalidOperationException(); - - this.MajorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); - this.MinorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[6..]); - this.FontCount = BinaryPrimitives.ReadInt32BigEndian(span[8..]); - } - - public int Count => this.FontCount; - - public SfntFile this[int index] - { - get - { - if (index < 0 || index >= this.FontCount) - { - throw new IndexOutOfRangeException( - $"The requested font #{index} does not exist in this .ttc file."); - } - - var offset = BinaryPrimitives.ReadInt32BigEndian(this.Memory.Span[(12 + 4 * index)..]); - return new(this.Memory[offset..], offset); - } - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < this.FontCount; i++) - yield return this[i]; - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs deleted file mode 100644 index d200de47b..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.Contracts; -using System.Linq; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - [Flags] - private enum LookupFlags : byte - { - RightToLeft = 1 << 0, - IgnoreBaseGlyphs = 1 << 1, - IgnoreLigatures = 1 << 2, - IgnoreMarks = 1 << 3, - UseMarkFilteringSet = 1 << 4, - } - - private enum LookupType : ushort - { - SingleAdjustment = 1, - PairAdjustment = 2, - CursiveAttachment = 3, - MarkToBaseAttachment = 4, - MarkToLigatureAttachment = 5, - MarkToMarkAttachment = 6, - ContextPositioning = 7, - ChainedContextPositioning = 8, - ExtensionPositioning = 9, - } - - private readonly struct ClassDefTable - { - public readonly PointerSpan Memory; - - public ClassDefTable(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public Format1ClassArray Format1 => new(this.Memory); - - public Format2ClassRanges Format2 => new(this.Memory); - - public IEnumerable<(ushort Class, ushort GlyphId)> Enumerate() - { - switch (this.Format) - { - case 1: - { - var format1 = this.Format1; - var startId = format1.StartGlyphId; - var count = format1.GlyphCount; - var classes = format1.ClassValueArray; - for (var i = 0; i < count; i++) - yield return (classes[i], (ushort)(i + startId)); - - break; - } - - case 2: - { - foreach (var range in this.Format2.ClassValueArray) - { - var @class = range.Class; - var startId = range.StartGlyphId; - var count = range.EndGlyphId - startId + 1; - for (var i = 0; i < count; i++) - yield return (@class, (ushort)(startId + i)); - } - - break; - } - } - } - - [Pure] - public ushort GetClass(ushort glyphId) - { - switch (this.Format) - { - case 1: - { - var format1 = this.Format1; - var startId = format1.StartGlyphId; - if (startId <= glyphId && glyphId < startId + format1.GlyphCount) - return this.Format1.ClassValueArray[glyphId - startId]; - - break; - } - - case 2: - { - var rangeSpan = this.Format2.ClassValueArray; - var i = rangeSpan.BinarySearch(new Format2ClassRanges.ClassRangeRecord { EndGlyphId = glyphId }); - if (i >= 0 && rangeSpan[i].ContainsGlyph(glyphId)) - return rangeSpan[i].Class; - - break; - } - } - - return 0; - } - - public readonly struct Format1ClassArray - { - public readonly PointerSpan Memory; - - public Format1ClassArray(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort StartGlyphId => this.Memory.ReadU16Big(2); - - public ushort GlyphCount => this.Memory.ReadU16Big(4); - - public BigEndianPointerSpan ClassValueArray => new( - this.Memory[6..].As(this.GlyphCount), - BinaryPrimitives.ReverseEndianness); - } - - public readonly struct Format2ClassRanges - { - public readonly PointerSpan Memory; - - public Format2ClassRanges(PointerSpan memory) => this.Memory = memory; - - public ushort ClassRangeCount => this.Memory.ReadU16Big(2); - - public BigEndianPointerSpan ClassValueArray => new( - this.Memory[4..].As(this.ClassRangeCount), - ClassRangeRecord.ReverseEndianness); - - public struct ClassRangeRecord : IComparable - { - public ushort StartGlyphId; - public ushort EndGlyphId; - public ushort Class; - - public static ClassRangeRecord ReverseEndianness(ClassRangeRecord value) => new() - { - StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), - EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), - Class = BinaryPrimitives.ReverseEndianness(value.Class), - }; - - public int CompareTo(ClassRangeRecord other) => this.EndGlyphId.CompareTo(other.EndGlyphId); - - public bool ContainsGlyph(ushort glyphId) => - this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; - } - } - } - - private readonly struct CoverageTable - { - public readonly PointerSpan Memory; - - public CoverageTable(PointerSpan memory) => this.Memory = memory; - - public enum CoverageFormat : ushort - { - Glyphs = 1, - RangeRecords = 2, - } - - public CoverageFormat Format => this.Memory.ReadEnumBig(0); - - public ushort Count => this.Memory.ReadU16Big(2); - - public BigEndianPointerSpan Glyphs => - this.Format == CoverageFormat.Glyphs - ? new(this.Memory[4..].As(this.Count), BinaryPrimitives.ReverseEndianness) - : default(BigEndianPointerSpan); - - public BigEndianPointerSpan RangeRecords => - this.Format == CoverageFormat.RangeRecords - ? new(this.Memory[4..].As(this.Count), RangeRecord.ReverseEndianness) - : default(BigEndianPointerSpan); - - public int GetCoverageIndex(ushort glyphId) - { - switch (this.Format) - { - case CoverageFormat.Glyphs: - return this.Glyphs.BinarySearch(glyphId); - - case CoverageFormat.RangeRecords: - { - var index = this.RangeRecords.BinarySearch( - (in RangeRecord record) => glyphId.CompareTo(record.EndGlyphId)); - - if (index >= 0 && this.RangeRecords[index].ContainsGlyph(glyphId)) - return index; - - return -1; - } - - default: - return -1; - } - } - - public struct RangeRecord - { - public ushort StartGlyphId; - public ushort EndGlyphId; - public ushort StartCoverageIndex; - - public static RangeRecord ReverseEndianness(RangeRecord value) => new() - { - StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), - EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), - StartCoverageIndex = BinaryPrimitives.ReverseEndianness(value.StartCoverageIndex), - }; - - public bool ContainsGlyph(ushort glyphId) => - this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; - } - } - - private readonly struct LookupTable : IEnumerable> - { - public readonly PointerSpan Memory; - - public LookupTable(PointerSpan memory) => this.Memory = memory; - - public LookupType Type => this.Memory.ReadEnumBig(0); - - public byte MarkAttachmentType => this.Memory[2]; - - public LookupFlags Flags => (LookupFlags)this.Memory[3]; - - public ushort SubtableCount => this.Memory.ReadU16Big(4); - - public BigEndianPointerSpan SubtableOffsets => new( - this.Memory[6..].As(this.SubtableCount), - BinaryPrimitives.ReverseEndianness); - - public PointerSpan this[int index] => this.Memory[this.SubtableOffsets[this.EnsureIndex(index)] ..]; - - public IEnumerator> GetEnumerator() - { - foreach (var i in Enumerable.Range(0, this.SubtableCount)) - yield return this.Memory[this.SubtableOffsets[i] ..]; - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - private int EnsureIndex(int index) => index >= 0 && index < this.SubtableCount - ? index - : throw new IndexOutOfRangeException(); - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs deleted file mode 100644 index c91df4ff2..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs +++ /dev/null @@ -1,443 +0,0 @@ -using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; -using System.Reactive.Disposables; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - private delegate int BinarySearchComparer(in T value); - - private static IDisposable CreatePointerSpan(this T[] data, out PointerSpan pointerSpan) - where T : unmanaged - { - var gchandle = GCHandle.Alloc(data, GCHandleType.Pinned); - pointerSpan = new(gchandle.AddrOfPinnedObject(), data.Length); - return Disposable.Create(() => gchandle.Free()); - } - - private static int BinarySearch(this IReadOnlyList span, in T value) - where T : unmanaged, IComparable - { - var l = 0; - var r = span.Count - 1; - while (l <= r) - { - var i = (int)(((uint)r + (uint)l) >> 1); - var c = value.CompareTo(span[i]); - switch (c) - { - case 0: - return i; - case > 0: - l = i + 1; - break; - default: - r = i - 1; - break; - } - } - - return ~l; - } - - private static int BinarySearch(this IReadOnlyList span, BinarySearchComparer comparer) - where T : unmanaged - { - var l = 0; - var r = span.Count - 1; - while (l <= r) - { - var i = (int)(((uint)r + (uint)l) >> 1); - var c = comparer(span[i]); - switch (c) - { - case 0: - return i; - case > 0: - l = i + 1; - break; - default: - r = i - 1; - break; - } - } - - return ~l; - } - - private static short ReadI16Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); - - private static int ReadI32Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); - - private static long ReadI64Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); - - private static ushort ReadU16Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); - - private static uint ReadU32Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); - - private static ulong ReadU64Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); - - private static Half ReadF16Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); - - private static float ReadF32Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); - - private static double ReadF64Big(this PointerSpan ps, int offset) => - BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out short value) => - value = BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out int value) => - value = BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out long value) => - value = BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out ushort value) => - value = BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out uint value) => - value = BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out ulong value) => - value = BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out Half value) => - value = BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out float value) => - value = BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, int offset, out double value) => - value = BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); - - private static void ReadBig(this PointerSpan ps, ref int offset, out short value) - { - ps.ReadBig(offset, out value); - offset += 2; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out int value) - { - ps.ReadBig(offset, out value); - offset += 4; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out long value) - { - ps.ReadBig(offset, out value); - offset += 8; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out ushort value) - { - ps.ReadBig(offset, out value); - offset += 2; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out uint value) - { - ps.ReadBig(offset, out value); - offset += 4; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out ulong value) - { - ps.ReadBig(offset, out value); - offset += 8; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out Half value) - { - ps.ReadBig(offset, out value); - offset += 2; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out float value) - { - ps.ReadBig(offset, out value); - offset += 4; - } - - private static void ReadBig(this PointerSpan ps, ref int offset, out double value) - { - ps.ReadBig(offset, out value); - offset += 8; - } - - private static unsafe T ReadEnumBig(this PointerSpan ps, int offset) where T : unmanaged, Enum - { - switch (Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T)))) - { - case 1: - var b1 = ps.Span[offset]; - return *(T*)&b1; - case 2: - var b2 = ps.ReadU16Big(offset); - return *(T*)&b2; - case 4: - var b4 = ps.ReadU32Big(offset); - return *(T*)&b4; - case 8: - var b8 = ps.ReadU64Big(offset); - return *(T*)&b8; - default: - throw new ArgumentException("Enum is not of size 1, 2, 4, or 8.", nameof(T), null); - } - } - - private static void ReadBig(this PointerSpan ps, int offset, out T value) where T : unmanaged, Enum => - value = ps.ReadEnumBig(offset); - - private static void ReadBig(this PointerSpan ps, ref int offset, out T value) where T : unmanaged, Enum - { - value = ps.ReadEnumBig(offset); - offset += Unsafe.SizeOf(); - } - - private readonly unsafe struct PointerSpan : IList, IReadOnlyList, ICollection - where T : unmanaged - { - public readonly T* Pointer; - - public PointerSpan(T* pointer, int count) - { - this.Pointer = pointer; - this.Count = count; - } - - public PointerSpan(nint pointer, int count) - : this((T*)pointer, count) - { - } - - public Span Span => new(this.Pointer, this.Count); - - public bool IsEmpty => this.Count == 0; - - public int Count { get; } - - public int Length => this.Count; - - public int ByteCount => sizeof(T) * this.Count; - - bool ICollection.IsSynchronized => false; - - object ICollection.SyncRoot => this; - - bool ICollection.IsReadOnly => false; - - public ref T this[int index] => ref this.Pointer[this.EnsureIndex(index)]; - - public PointerSpan this[Range range] => this.Slice(range.GetOffsetAndLength(this.Count)); - - T IList.this[int index] - { - get => this.Pointer[this.EnsureIndex(index)]; - set => this.Pointer[this.EnsureIndex(index)] = value; - } - - T IReadOnlyList.this[int index] => this.Pointer[this.EnsureIndex(index)]; - - public bool ContainsPointer(T2* obj) where T2 : unmanaged => - (T*)obj >= this.Pointer && (T*)(obj + 1) <= this.Pointer + this.Count; - - public PointerSpan Slice(int offset, int count) => new(this.Pointer + offset, count); - - public PointerSpan Slice((int Offset, int Count) offsetAndCount) - => this.Slice(offsetAndCount.Offset, offsetAndCount.Count); - - public PointerSpan As(int count) - where T2 : unmanaged => - count > this.Count / sizeof(T2) - ? throw new ArgumentOutOfRangeException( - nameof(count), - count, - $"Wanted {count} items; had {this.Count / sizeof(T2)} items") - : new((T2*)this.Pointer, count); - - public PointerSpan As() - where T2 : unmanaged => - new((T2*)this.Pointer, this.Count / sizeof(T2)); - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < this.Count; i++) - yield return this[i]; - } - - void ICollection.Add(T item) => throw new NotSupportedException(); - - void ICollection.Clear() => throw new NotSupportedException(); - - bool ICollection.Contains(T item) - { - for (var i = 0; i < this.Count; i++) - { - if (Equals(this.Pointer[i], item)) - return true; - } - - return false; - } - - void ICollection.CopyTo(T[] array, int arrayIndex) - { - if (array.Length < this.Count) - throw new ArgumentException(null, nameof(array)); - - if (array.Length < arrayIndex + this.Count) - throw new ArgumentException(null, nameof(arrayIndex)); - - for (var i = 0; i < this.Count; i++) - array[arrayIndex + i] = this.Pointer[i]; - } - - bool ICollection.Remove(T item) => throw new NotSupportedException(); - - int IList.IndexOf(T item) - { - for (var i = 0; i < this.Count; i++) - { - if (Equals(this.Pointer[i], item)) - return i; - } - - return -1; - } - - void IList.Insert(int index, T item) => throw new NotSupportedException(); - - void IList.RemoveAt(int index) => throw new NotSupportedException(); - - void ICollection.CopyTo(Array array, int arrayIndex) - { - if (array.Length < this.Count) - throw new ArgumentException(null, nameof(array)); - - if (array.Length < arrayIndex + this.Count) - throw new ArgumentException(null, nameof(arrayIndex)); - - for (var i = 0; i < this.Count; i++) - array.SetValue(this.Pointer[i], arrayIndex + i); - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - private int EnsureIndex(int index) => - index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); - } - - private readonly unsafe struct BigEndianPointerSpan - : IList, IReadOnlyList, ICollection - where T : unmanaged - { - public readonly T* Pointer; - - private readonly Func reverseEndianness; - - public BigEndianPointerSpan(PointerSpan pointerSpan, Func reverseEndianness) - { - this.reverseEndianness = reverseEndianness; - this.Pointer = pointerSpan.Pointer; - this.Count = pointerSpan.Count; - } - - public int Count { get; } - - public int Length => this.Count; - - public int ByteCount => sizeof(T) * this.Count; - - public bool IsSynchronized => true; - - public object SyncRoot => this; - - public bool IsReadOnly => true; - - public T this[int index] - { - get => - BitConverter.IsLittleEndian - ? this.reverseEndianness(this.Pointer[this.EnsureIndex(index)]) - : this.Pointer[this.EnsureIndex(index)]; - set => this.Pointer[this.EnsureIndex(index)] = - BitConverter.IsLittleEndian - ? this.reverseEndianness(value) - : value; - } - - public IEnumerator GetEnumerator() - { - for (var i = 0; i < this.Count; i++) - yield return this[i]; - } - - void ICollection.Add(T item) => throw new NotSupportedException(); - - void ICollection.Clear() => throw new NotSupportedException(); - - bool ICollection.Contains(T item) => throw new NotSupportedException(); - - void ICollection.CopyTo(T[] array, int arrayIndex) - { - if (array.Length < this.Count) - throw new ArgumentException(null, nameof(array)); - - if (array.Length < arrayIndex + this.Count) - throw new ArgumentException(null, nameof(arrayIndex)); - - for (var i = 0; i < this.Count; i++) - array[arrayIndex + i] = this[i]; - } - - bool ICollection.Remove(T item) => throw new NotSupportedException(); - - int IList.IndexOf(T item) - { - for (var i = 0; i < this.Count; i++) - { - if (Equals(this[i], item)) - return i; - } - - return -1; - } - - void IList.Insert(int index, T item) => throw new NotSupportedException(); - - void IList.RemoveAt(int index) => throw new NotSupportedException(); - - void ICollection.CopyTo(Array array, int arrayIndex) - { - if (array.Length < this.Count) - throw new ArgumentException(null, nameof(array)); - - if (array.Length < arrayIndex + this.Count) - throw new ArgumentException(null, nameof(arrayIndex)); - - for (var i = 0; i < this.Count; i++) - array.SetValue(this[i], arrayIndex + i); - } - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - private int EnsureIndex(int index) => - index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs deleted file mode 100644 index 80cf4b7da..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs +++ /dev/null @@ -1,1391 +0,0 @@ -using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] -[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] -[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] -internal static partial class TrueTypeUtils -{ - [Flags] - private enum ValueFormat : ushort - { - PlacementX = 1 << 0, - PlacementY = 1 << 1, - AdvanceX = 1 << 2, - AdvanceY = 1 << 3, - PlacementDeviceOffsetX = 1 << 4, - PlacementDeviceOffsetY = 1 << 5, - AdvanceDeviceOffsetX = 1 << 6, - AdvanceDeviceOffsetY = 1 << 7, - - ValidBits = 0 - | PlacementX | PlacementY - | AdvanceX | AdvanceY - | PlacementDeviceOffsetX | PlacementDeviceOffsetY - | AdvanceDeviceOffsetX | AdvanceDeviceOffsetY, - } - - private static int NumBytes(this ValueFormat value) => - ushort.PopCount((ushort)(value & ValueFormat.ValidBits)) * 2; - - private readonly struct Cmap - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/cmap - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html - - public static readonly TagStruct DirectoryTableTag = new('c', 'm', 'a', 'p'); - - public readonly PointerSpan Memory; - - public Cmap(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Cmap(PointerSpan memory) => this.Memory = memory; - - public ushort Version => this.Memory.ReadU16Big(0); - - public ushort RecordCount => this.Memory.ReadU16Big(2); - - public BigEndianPointerSpan Records => new( - this.Memory[4..].As(this.RecordCount), - EncodingRecord.ReverseEndianness); - - public EncodingRecord? UnicodeEncodingRecord => - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Bmp }) - ?? - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Full }) - ?? - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.UnicodeFullRepertoire }) - ?? - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeBmp }) - ?? - this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( - x => x!.Value.PlatformAndEncoding is - { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeFullRepertoire }); - - public CmapFormat? UnicodeTable => this.GetTable(this.UnicodeEncodingRecord); - - public CmapFormat? GetTable(EncodingRecord? encodingRecord) => - encodingRecord is { } record - ? this.Memory.ReadU16Big(record.SubtableOffset) switch - { - 0 => new CmapFormat0(this.Memory[record.SubtableOffset..]), - 2 => new CmapFormat2(this.Memory[record.SubtableOffset..]), - 4 => new CmapFormat4(this.Memory[record.SubtableOffset..]), - 6 => new CmapFormat6(this.Memory[record.SubtableOffset..]), - 8 => new CmapFormat8(this.Memory[record.SubtableOffset..]), - 10 => new CmapFormat10(this.Memory[record.SubtableOffset..]), - 12 or 13 => new CmapFormat12And13(this.Memory[record.SubtableOffset..]), - _ => null, - } - : null; - - public struct EncodingRecord - { - public PlatformAndEncoding PlatformAndEncoding; - public int SubtableOffset; - - public EncodingRecord(PointerSpan span) - { - this.PlatformAndEncoding = new(span); - var offset = Unsafe.SizeOf(); - span.ReadBig(ref offset, out this.SubtableOffset); - } - - public static EncodingRecord ReverseEndianness(EncodingRecord value) => new() - { - PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), - SubtableOffset = BinaryPrimitives.ReverseEndianness(value.SubtableOffset), - }; - } - - public struct MapGroup : IComparable - { - public int StartCharCode; - public int EndCharCode; - public int GlyphId; - - public MapGroup(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.StartCharCode); - span.ReadBig(ref offset, out this.EndCharCode); - span.ReadBig(ref offset, out this.GlyphId); - } - - public static MapGroup ReverseEndianness(MapGroup obj) => new() - { - StartCharCode = BinaryPrimitives.ReverseEndianness(obj.StartCharCode), - EndCharCode = BinaryPrimitives.ReverseEndianness(obj.EndCharCode), - GlyphId = BinaryPrimitives.ReverseEndianness(obj.GlyphId), - }; - - public int CompareTo(MapGroup other) - { - var endCharCodeComparison = this.EndCharCode.CompareTo(other.EndCharCode); - if (endCharCodeComparison != 0) return endCharCodeComparison; - - var startCharCodeComparison = this.StartCharCode.CompareTo(other.StartCharCode); - if (startCharCodeComparison != 0) return startCharCodeComparison; - - return this.GlyphId.CompareTo(other.GlyphId); - } - } - - public abstract class CmapFormat : IReadOnlyDictionary - { - public int Count => this.Count(x => x.Value != 0); - - public IEnumerable Keys => this.Select(x => x.Key); - - public IEnumerable Values => this.Select(x => x.Value); - - public ushort this[int key] => throw new NotImplementedException(); - - public abstract ushort CharToGlyph(int c); - - public abstract IEnumerator> GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); - - public bool ContainsKey(int key) => this.CharToGlyph(key) != 0; - - public bool TryGetValue(int key, out ushort value) - { - value = this.CharToGlyph(key); - return value != 0; - } - } - - public class CmapFormat0 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat0(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public ushort Language => this.Memory.ReadU16Big(4); - - public PointerSpan GlyphIdArray => this.Memory.Slice(6, 256); - - public override ushort CharToGlyph(int c) => c is >= 0 and < 256 ? this.GlyphIdArray[c] : (byte)0; - - public override IEnumerator> GetEnumerator() - { - for (var codepoint = 0; codepoint < 256; codepoint++) - { - if (this.GlyphIdArray[codepoint] is var glyphId and not 0) - yield return new(codepoint, glyphId); - } - } - } - - public class CmapFormat2 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat2(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public ushort Language => this.Memory.ReadU16Big(4); - - public BigEndianPointerSpan SubHeaderKeys => new( - this.Memory[6..].As(256), - BinaryPrimitives.ReverseEndianness); - - public PointerSpan Data => this.Memory[518..]; - - public bool TryGetSubHeader( - int keyIndex, out SubHeader subheader, out BigEndianPointerSpan glyphSpan) - { - if (keyIndex < 0 || keyIndex >= this.SubHeaderKeys.Count) - { - subheader = default; - glyphSpan = default; - return false; - } - - var offset = this.SubHeaderKeys[keyIndex]; - if (offset + Unsafe.SizeOf() > this.Data.Length) - { - subheader = default; - glyphSpan = default; - return false; - } - - subheader = new(this.Data[offset..]); - glyphSpan = new( - this.Data[(offset + Unsafe.SizeOf() + subheader.IdRangeOffset)..] - .As(subheader.EntryCount), - BinaryPrimitives.ReverseEndianness); - - return true; - } - - public override ushort CharToGlyph(int c) - { - if (!this.TryGetSubHeader(c >> 8, out var sh, out var glyphSpan)) - return 0; - - c = (c & 0xFF) - sh.FirstCode; - if (c > 0 || c >= glyphSpan.Count) - return 0; - - var res = glyphSpan[c]; - return res == 0 ? (ushort)0 : unchecked((ushort)(res + sh.IdDelta)); - } - - public override IEnumerator> GetEnumerator() - { - for (var i = 0; i < this.SubHeaderKeys.Count; i++) - { - if (!this.TryGetSubHeader(i, out var sh, out var glyphSpan)) - continue; - - for (var j = 0; j < glyphSpan.Count; j++) - { - var res = glyphSpan[j]; - if (res == 0) - continue; - - var glyphId = unchecked((ushort)(res + sh.IdDelta)); - if (glyphId == 0) - continue; - - var codepoint = (i << 8) | (sh.FirstCode + j); - yield return new(codepoint, glyphId); - } - } - } - - public struct SubHeader - { - public ushort FirstCode; - public ushort EntryCount; - public ushort IdDelta; - public ushort IdRangeOffset; - - public SubHeader(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.FirstCode); - span.ReadBig(ref offset, out this.EntryCount); - span.ReadBig(ref offset, out this.IdDelta); - span.ReadBig(ref offset, out this.IdRangeOffset); - } - } - } - - public class CmapFormat4 : CmapFormat - { - public const int EndCodesOffset = 14; - - public readonly PointerSpan Memory; - - public CmapFormat4(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public ushort Language => this.Memory.ReadU16Big(4); - - public ushort SegCountX2 => this.Memory.ReadU16Big(6); - - public ushort SearchRange => this.Memory.ReadU16Big(8); - - public ushort EntrySelector => this.Memory.ReadU16Big(10); - - public ushort RangeShift => this.Memory.ReadU16Big(12); - - public BigEndianPointerSpan EndCodes => new( - this.Memory.Slice(EndCodesOffset, this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public BigEndianPointerSpan StartCodes => new( - this.Memory.Slice(EndCodesOffset + 2 + (1 * this.SegCountX2), this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public BigEndianPointerSpan IdDeltas => new( - this.Memory.Slice(EndCodesOffset + 2 + (2 * this.SegCountX2), this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public BigEndianPointerSpan IdRangeOffsets => new( - this.Memory.Slice(EndCodesOffset + 2 + (3 * this.SegCountX2), this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public BigEndianPointerSpan GlyphIds => new( - this.Memory.Slice(EndCodesOffset + 2 + (4 * this.SegCountX2), this.SegCountX2).As(), - BinaryPrimitives.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - if (c is < 0 or >= 0x10000) - return 0; - - var i = this.EndCodes.BinarySearch((ushort)c); - if (i < 0) - return 0; - - var startCode = this.StartCodes[i]; - var endCode = this.EndCodes[i]; - if (c < startCode || c > endCode) - return 0; - - var idRangeOffset = this.IdRangeOffsets[i]; - var idDelta = this.IdDeltas[i]; - if (idRangeOffset == 0) - return unchecked((ushort)(c + idDelta)); - - var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; - if (ptr > this.Memory.Length) - return 0; - - var glyphs = new BigEndianPointerSpan( - this.Memory[ptr..].As(endCode - startCode + 1), - BinaryPrimitives.ReverseEndianness); - - var glyph = glyphs[c - startCode]; - return unchecked(glyph == 0 ? (ushort)0 : (ushort)(idDelta + glyph)); - } - - public override IEnumerator> GetEnumerator() - { - var startCodes = this.StartCodes; - var endCodes = this.EndCodes; - var idDeltas = this.IdDeltas; - var idRangeOffsets = this.IdRangeOffsets; - - for (var i = 0; i < this.SegCountX2 / 2; i++) - { - var startCode = startCodes[i]; - var endCode = endCodes[i]; - var idRangeOffset = idRangeOffsets[i]; - var idDelta = idDeltas[i]; - - if (idRangeOffset == 0) - { - for (var c = (int)startCode; c <= endCode; c++) - yield return new(c, (ushort)(c + idDelta)); - } - else - { - var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; - if (ptr >= this.Memory.Length) - continue; - - var glyphs = new BigEndianPointerSpan( - this.Memory[ptr..].As(endCode - startCode + 1), - BinaryPrimitives.ReverseEndianness); - - for (var j = 0; j < glyphs.Count; j++) - { - var glyphId = glyphs[j]; - if (glyphId == 0) - continue; - - glyphId += idDelta; - if (glyphId == 0) - continue; - - yield return new(startCode + j, glyphId); - } - } - } - } - } - - public class CmapFormat6 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat6(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public ushort Language => this.Memory.ReadU16Big(4); - - public ushort FirstCode => this.Memory.ReadU16Big(6); - - public ushort EntryCount => this.Memory.ReadU16Big(8); - - public BigEndianPointerSpan GlyphIds => new( - this.Memory[10..].As(this.EntryCount), - BinaryPrimitives.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - var glyphIds = this.GlyphIds; - if (c < this.FirstCode || c >= this.FirstCode + this.GlyphIds.Count) - return 0; - - return glyphIds[c - this.FirstCode]; - } - - public override IEnumerator> GetEnumerator() - { - var glyphIds = this.GlyphIds; - for (var i = 0; i < this.GlyphIds.Length; i++) - { - var g = glyphIds[i]; - if (g != 0) - yield return new(this.FirstCode + i, g); - } - } - } - - public class CmapFormat8 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat8(PointerSpan memory) => this.Memory = memory; - - public int Format => this.Memory.ReadI32Big(0); - - public int Length => this.Memory.ReadI32Big(4); - - public int Language => this.Memory.ReadI32Big(8); - - public PointerSpan Is32 => this.Memory.Slice(12, 8192); - - public int NumGroups => this.Memory.ReadI32Big(8204); - - public BigEndianPointerSpan Groups => - new(this.Memory[8208..].As(), MapGroup.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - var groups = this.Groups; - - var i = groups.BinarySearch((in MapGroup value) => c.CompareTo(value.EndCharCode)); - if (i < 0) - return 0; - - var group = groups[i]; - if (c < group.StartCharCode || c > group.EndCharCode) - return 0; - - return unchecked((ushort)(group.GlyphId + c - group.StartCharCode)); - } - - public override IEnumerator> GetEnumerator() - { - foreach (var group in this.Groups) - { - for (var j = group.StartCharCode; j <= group.EndCharCode; j++) - { - var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); - if (glyphId == 0) - continue; - - yield return new(j, glyphId); - } - } - } - } - - public class CmapFormat10 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat10(PointerSpan memory) => this.Memory = memory; - - public int Format => this.Memory.ReadI32Big(0); - - public int Length => this.Memory.ReadI32Big(4); - - public int Language => this.Memory.ReadI32Big(8); - - public int StartCharCode => this.Memory.ReadI32Big(12); - - public int NumChars => this.Memory.ReadI32Big(16); - - public BigEndianPointerSpan GlyphIdArray => new( - this.Memory.Slice(20, this.NumChars * 2).As(), - BinaryPrimitives.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - if (c < this.StartCharCode || c >= this.StartCharCode + this.GlyphIdArray.Count) - return 0; - - return this.GlyphIdArray[c]; - } - - public override IEnumerator> GetEnumerator() - { - for (var i = 0; i < this.GlyphIdArray.Count; i++) - { - var glyph = this.GlyphIdArray[i]; - if (glyph != 0) - yield return new(this.StartCharCode + i, glyph); - } - } - } - - public class CmapFormat12And13 : CmapFormat - { - public readonly PointerSpan Memory; - - public CmapFormat12And13(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public int Length => this.Memory.ReadI32Big(4); - - public int Language => this.Memory.ReadI32Big(8); - - public int NumGroups => this.Memory.ReadI32Big(12); - - public BigEndianPointerSpan Groups => new( - this.Memory[16..].As(this.NumGroups), - MapGroup.ReverseEndianness); - - public override ushort CharToGlyph(int c) - { - var groups = this.Groups; - - var i = groups.BinarySearch(new MapGroup() { EndCharCode = c }); - if (i < 0) - return 0; - - var group = groups[i]; - if (c < group.StartCharCode || c > group.EndCharCode) - return 0; - - if (this.Format == 12) - return (ushort)(group.GlyphId + c - group.StartCharCode); - else - return (ushort)group.GlyphId; - } - - public override IEnumerator> GetEnumerator() - { - var groups = this.Groups; - if (this.Format == 12) - { - foreach (var group in groups) - { - for (var j = group.StartCharCode; j <= group.EndCharCode; j++) - { - var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); - if (glyphId == 0) - continue; - - yield return new(j, glyphId); - } - } - } - else - { - foreach (var group in groups) - { - if (group.GlyphId == 0) - continue; - - for (var j = group.StartCharCode; j <= group.EndCharCode; j++) - yield return new(j, (ushort)group.GlyphId); - } - } - } - } - } - - private readonly struct Gpos - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos - - public static readonly TagStruct DirectoryTableTag = new('G', 'P', 'O', 'S'); - - public readonly PointerSpan Memory; - - public Gpos(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Gpos(PointerSpan memory) => this.Memory = memory; - - public Fixed Version => new(this.Memory); - - public ushort ScriptListOffset => this.Memory.ReadU16Big(4); - - public ushort FeatureListOffset => this.Memory.ReadU16Big(6); - - public ushort LookupListOffset => this.Memory.ReadU16Big(8); - - public uint FeatureVariationsOffset => this.Version.CompareTo(new(1, 1)) >= 0 - ? this.Memory.ReadU32Big(10) - : 0; - - public BigEndianPointerSpan LookupOffsetList => new( - this.Memory[(this.LookupListOffset + 2)..].As( - this.Memory.ReadU16Big(this.LookupListOffset)), - BinaryPrimitives.ReverseEndianness); - - public IEnumerable EnumerateLookupTables() - { - foreach (var offset in this.LookupOffsetList) - yield return new(this.Memory[(this.LookupListOffset + offset)..]); - } - - public IEnumerable ExtractAdvanceX() => - this.EnumerateLookupTables() - .SelectMany( - lookupTable => lookupTable.Type switch - { - LookupType.PairAdjustment => - lookupTable.SelectMany(y => new PairAdjustmentPositioning(y).ExtractAdvanceX()), - LookupType.ExtensionPositioning => - lookupTable - .Where(y => y.ReadU16Big(0) == 1) - .Select(y => new ExtensionPositioningSubtableFormat1(y)) - .Where(y => y.ExtensionLookupType == LookupType.PairAdjustment) - .SelectMany(y => new PairAdjustmentPositioning(y.ExtensionData).ExtractAdvanceX()), - _ => Array.Empty(), - }); - - public struct ValueRecord - { - public short PlacementX; - public short PlacementY; - public short AdvanceX; - public short AdvanceY; - public short PlacementDeviceOffsetX; - public short PlacementDeviceOffsetY; - public short AdvanceDeviceOffsetX; - public short AdvanceDeviceOffsetY; - - public ValueRecord(PointerSpan pointerSpan, ValueFormat valueFormat) - { - var offset = 0; - if ((valueFormat & ValueFormat.PlacementX) != 0) - pointerSpan.ReadBig(ref offset, out this.PlacementX); - - if ((valueFormat & ValueFormat.PlacementY) != 0) - pointerSpan.ReadBig(ref offset, out this.PlacementY); - - if ((valueFormat & ValueFormat.AdvanceX) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceX); - if ((valueFormat & ValueFormat.AdvanceY) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceY); - if ((valueFormat & ValueFormat.PlacementDeviceOffsetX) != 0) - pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetX); - - if ((valueFormat & ValueFormat.PlacementDeviceOffsetY) != 0) - pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetY); - - if ((valueFormat & ValueFormat.AdvanceDeviceOffsetX) != 0) - pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetX); - - if ((valueFormat & ValueFormat.AdvanceDeviceOffsetY) != 0) - pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetY); - } - } - - public readonly struct PairAdjustmentPositioning - { - public readonly PointerSpan Memory; - - public PairAdjustmentPositioning(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public IEnumerable ExtractAdvanceX() => this.Format switch - { - 1 => new Format1(this.Memory).ExtractAdvanceX(), - 2 => new Format2(this.Memory).ExtractAdvanceX(), - _ => Array.Empty(), - }; - - public readonly struct Format1 - { - public readonly PointerSpan Memory; - - public Format1(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort CoverageOffset => this.Memory.ReadU16Big(2); - - public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); - - public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); - - public ushort PairSetCount => this.Memory.ReadU16Big(8); - - public BigEndianPointerSpan PairSetOffsets => new( - this.Memory[10..].As(this.PairSetCount), - BinaryPrimitives.ReverseEndianness); - - public CoverageTable CoverageTable => new(this.Memory[this.CoverageOffset..]); - - public PairSet this[int index] => new( - this.Memory[this.PairSetOffsets[index] ..], - this.ValueFormat1, - this.ValueFormat2); - - public IEnumerable ExtractAdvanceX() - { - if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && - (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) - { - yield break; - } - - var coverageTable = this.CoverageTable; - switch (coverageTable.Format) - { - case CoverageTable.CoverageFormat.Glyphs: - { - var glyphSpan = coverageTable.Glyphs; - foreach (var coverageIndex in Enumerable.Range(0, glyphSpan.Count)) - { - var glyph1Id = glyphSpan[coverageIndex]; - PairSet pairSetView; - try - { - pairSetView = this[coverageIndex]; - } - catch (ArgumentOutOfRangeException) - { - yield break; - } - catch (IndexOutOfRangeException) - { - yield break; - } - - foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) - { - var pair = pairSetView[pairIndex]; - var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); - if (adj >= 10000) - System.Diagnostics.Debugger.Break(); - - if (adj != 0) - yield return new(glyph1Id, pair.SecondGlyph, adj); - } - } - - break; - } - - case CoverageTable.CoverageFormat.RangeRecords: - { - foreach (var rangeRecord in coverageTable.RangeRecords) - { - var startGlyphId = rangeRecord.StartGlyphId; - var endGlyphId = rangeRecord.EndGlyphId; - var startCoverageIndex = rangeRecord.StartCoverageIndex; - var glyphCount = endGlyphId - startGlyphId + 1; - foreach (var glyph1Id in Enumerable.Range(startGlyphId, glyphCount)) - { - PairSet pairSetView; - try - { - pairSetView = this[startCoverageIndex + glyph1Id - startGlyphId]; - } - catch (ArgumentOutOfRangeException) - { - yield break; - } - catch (IndexOutOfRangeException) - { - yield break; - } - - foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) - { - var pair = pairSetView[pairIndex]; - var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); - if (adj != 0) - yield return new((ushort)glyph1Id, pair.SecondGlyph, adj); - } - } - } - - break; - } - } - } - - public readonly struct PairSet - { - public readonly PointerSpan Memory; - public readonly ValueFormat ValueFormat1; - public readonly ValueFormat ValueFormat2; - public readonly int PairValue1Size; - public readonly int PairValue2Size; - public readonly int PairSize; - - public PairSet( - PointerSpan memory, - ValueFormat valueFormat1, - ValueFormat valueFormat2) - { - this.Memory = memory; - this.ValueFormat1 = valueFormat1; - this.ValueFormat2 = valueFormat2; - this.PairValue1Size = this.ValueFormat1.NumBytes(); - this.PairValue2Size = this.ValueFormat2.NumBytes(); - this.PairSize = 2 + this.PairValue1Size + this.PairValue2Size; - } - - public ushort Count => this.Memory.ReadU16Big(0); - - public PairValueRecord this[int index] - { - get - { - var pvr = this.Memory.Slice(2 + (this.PairSize * index), this.PairSize); - return new() - { - SecondGlyph = pvr.ReadU16Big(0), - Record1 = new(pvr.Slice(2, this.PairValue1Size), this.ValueFormat1), - Record2 = new( - pvr.Slice(2 + this.PairValue1Size, this.PairValue2Size), - this.ValueFormat2), - }; - } - } - - public struct PairValueRecord - { - public ushort SecondGlyph; - public ValueRecord Record1; - public ValueRecord Record2; - } - } - } - - public readonly struct Format2 - { - public readonly PointerSpan Memory; - public readonly int PairValue1Size; - public readonly int PairValue2Size; - public readonly int PairSize; - - public Format2(PointerSpan memory) - { - this.Memory = memory; - this.PairValue1Size = this.ValueFormat1.NumBytes(); - this.PairValue2Size = this.ValueFormat2.NumBytes(); - this.PairSize = this.PairValue1Size + this.PairValue2Size; - } - - public ushort Format => this.Memory.ReadU16Big(0); - - public ushort CoverageOffset => this.Memory.ReadU16Big(2); - - public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); - - public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); - - public ushort ClassDef1Offset => this.Memory.ReadU16Big(8); - - public ushort ClassDef2Offset => this.Memory.ReadU16Big(10); - - public ushort Class1Count => this.Memory.ReadU16Big(12); - - public ushort Class2Count => this.Memory.ReadU16Big(14); - - public ClassDefTable ClassDefTable1 => new(this.Memory[this.ClassDef1Offset..]); - - public ClassDefTable ClassDefTable2 => new(this.Memory[this.ClassDef2Offset..]); - - public (ValueRecord Record1, ValueRecord Record2) this[(int Class1Index, int Class2Index) v] => - this[v.Class1Index, v.Class2Index]; - - public (ValueRecord Record1, ValueRecord Record2) this[int class1Index, int class2Index] - { - get - { - if (class1Index < 0 || class1Index >= this.Class1Count) - throw new IndexOutOfRangeException(); - - if (class2Index < 0 || class2Index >= this.Class2Count) - throw new IndexOutOfRangeException(); - - var offset = 16 + (this.PairSize * ((class1Index * this.Class2Count) + class2Index)); - return ( - new(this.Memory.Slice(offset, this.PairValue1Size), this.ValueFormat1), - new( - this.Memory.Slice(offset + this.PairValue1Size, this.PairValue2Size), - this.ValueFormat2)); - } - } - - public IEnumerable ExtractAdvanceX() - { - if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && - (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) - { - yield break; - } - - var classes1 = this.ClassDefTable1.Enumerate() - .GroupBy(x => x.Class, x => x.GlyphId) - .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); - - var classes2 = this.ClassDefTable2.Enumerate() - .GroupBy(x => x.Class, x => x.GlyphId) - .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); - - foreach (var class1 in Enumerable.Range(0, this.Class1Count)) - { - if (!classes1.TryGetValue((ushort)class1, out var glyphs1)) - continue; - - foreach (var class2 in Enumerable.Range(0, this.Class2Count)) - { - if (!classes2.TryGetValue((ushort)class2, out var glyphs2)) - continue; - - (ValueRecord, ValueRecord) record; - try - { - record = this[class1, class2]; - } - catch (ArgumentOutOfRangeException) - { - yield break; - } - catch (IndexOutOfRangeException) - { - yield break; - } - - var val = record.Item1.AdvanceX + record.Item2.PlacementX; - if (val == 0) - continue; - - foreach (var glyph1 in glyphs1) - { - foreach (var glyph2 in glyphs2) - { - yield return new(glyph1, glyph2, (short)val); - } - } - } - } - } - } - } - - public readonly struct ExtensionPositioningSubtableFormat1 - { - public readonly PointerSpan Memory; - - public ExtensionPositioningSubtableFormat1(PointerSpan memory) => this.Memory = memory; - - public ushort Format => this.Memory.ReadU16Big(0); - - public LookupType ExtensionLookupType => this.Memory.ReadEnumBig(2); - - public int ExtensionOffset => this.Memory.ReadI32Big(4); - - public PointerSpan ExtensionData => this.Memory[this.ExtensionOffset..]; - } - } - - private readonly struct Head - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/head - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html - - public const uint MagicNumberValue = 0x5F0F3CF5; - public static readonly TagStruct DirectoryTableTag = new('h', 'e', 'a', 'd'); - - public readonly PointerSpan Memory; - - public Head(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Head(PointerSpan memory) => this.Memory = memory; - - [Flags] - public enum HeadFlags : ushort - { - BaselineForFontAtZeroY = 1 << 0, - LeftSideBearingAtZeroX = 1 << 1, - InstructionsDependOnPointSize = 1 << 2, - ForcePpemsInteger = 1 << 3, - InstructionsAlterAdvanceWidth = 1 << 4, - VerticalLayout = 1 << 5, - Reserved6 = 1 << 6, - RequiresLayoutForCorrectLinguisticRendering = 1 << 7, - IsAatFont = 1 << 8, - ContainsRtlGlyph = 1 << 9, - ContainsIndicStyleRearrangementEffects = 1 << 10, - Lossless = 1 << 11, - ProduceCompatibleMetrics = 1 << 12, - OptimizedForClearType = 1 << 13, - IsLastResortFont = 1 << 14, - Reserved15 = 1 << 15, - } - - [Flags] - public enum MacStyleFlags : ushort - { - Bold = 1 << 0, - Italic = 1 << 1, - Underline = 1 << 2, - Outline = 1 << 3, - Shadow = 1 << 4, - Condensed = 1 << 5, - Extended = 1 << 6, - } - - public Fixed Version => new(this.Memory); - - public Fixed FontRevision => new(this.Memory[4..]); - - public uint ChecksumAdjustment => this.Memory.ReadU32Big(8); - - public uint MagicNumber => this.Memory.ReadU32Big(12); - - public HeadFlags Flags => this.Memory.ReadEnumBig(16); - - public ushort UnitsPerEm => this.Memory.ReadU16Big(18); - - public ulong CreatedTimestamp => this.Memory.ReadU64Big(20); - - public ulong ModifiedTimestamp => this.Memory.ReadU64Big(28); - - public ushort MinX => this.Memory.ReadU16Big(36); - - public ushort MinY => this.Memory.ReadU16Big(38); - - public ushort MaxX => this.Memory.ReadU16Big(40); - - public ushort MaxY => this.Memory.ReadU16Big(42); - - public MacStyleFlags MacStyle => this.Memory.ReadEnumBig(44); - - public ushort LowestRecommendedPpem => this.Memory.ReadU16Big(46); - - public ushort FontDirectionHint => this.Memory.ReadU16Big(48); - - public ushort IndexToLocFormat => this.Memory.ReadU16Big(50); - - public ushort GlyphDataFormat => this.Memory.ReadU16Big(52); - } - - private readonly struct Kern - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/kern - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html - - public static readonly TagStruct DirectoryTableTag = new('k', 'e', 'r', 'n'); - - public readonly PointerSpan Memory; - - public Kern(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Kern(PointerSpan memory) => this.Memory = memory; - - public ushort Version => this.Memory.ReadU16Big(0); - - public IEnumerable EnumerateHorizontalPairs() => this.Version switch - { - 0 => new Version0(this.Memory).EnumerateHorizontalPairs(), - 1 => new Version1(this.Memory).EnumerateHorizontalPairs(), - _ => Array.Empty(), - }; - - public readonly struct Format0 - { - public readonly PointerSpan Memory; - - public Format0(PointerSpan memory) => this.Memory = memory; - - public ushort PairCount => this.Memory.ReadU16Big(0); - - public ushort SearchRange => this.Memory.ReadU16Big(2); - - public ushort EntrySelector => this.Memory.ReadU16Big(4); - - public ushort RangeShift => this.Memory.ReadU16Big(6); - - public BigEndianPointerSpan Pairs => new( - this.Memory[8..].As(this.PairCount), - KerningPair.ReverseEndianness); - } - - public readonly struct Version0 - { - public readonly PointerSpan Memory; - - public Version0(PointerSpan memory) => this.Memory = memory; - - [Flags] - public enum CoverageFlags : byte - { - Horizontal = 1 << 0, - Minimum = 1 << 1, - CrossStream = 1 << 2, - Override = 1 << 3, - } - - public ushort Version => this.Memory.ReadU16Big(0); - - public ushort NumSubtables => this.Memory.ReadU16Big(2); - - public PointerSpan Data => this.Memory[4..]; - - public IEnumerable EnumerateSubtables() - { - var data = this.Data; - for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) - { - var st = new Subtable(data); - data = data[st.Length..]; - yield return st; - } - } - - public IEnumerable EnumerateHorizontalPairs() - { - var accumulator = new Dictionary<(ushort Left, ushort Right), short>(); - foreach (var subtable in this.EnumerateSubtables()) - { - var isOverride = (subtable.Flags & CoverageFlags.Override) != 0; - var isMinimum = (subtable.Flags & CoverageFlags.Minimum) != 0; - foreach (var t in subtable.EnumeratePairs()) - { - if (isOverride) - { - accumulator[(t.Left, t.Right)] = t.Value; - } - else if (isMinimum) - { - accumulator[(t.Left, t.Right)] = Math.Max( - accumulator.GetValueOrDefault((t.Left, t.Right), t.Value), - t.Value); - } - else - { - accumulator[(t.Left, t.Right)] = (short)( - accumulator.GetValueOrDefault( - (t.Left, t.Right)) + t.Value); - } - } - } - - return accumulator.Select( - x => new KerningPair { Left = x.Key.Left, Right = x.Key.Right, Value = x.Value }); - } - - public readonly struct Subtable - { - public readonly PointerSpan Memory; - - public Subtable(PointerSpan memory) => this.Memory = memory; - - public ushort Version => this.Memory.ReadU16Big(0); - - public ushort Length => this.Memory.ReadU16Big(2); - - public byte Format => this.Memory[4]; - - public CoverageFlags Flags => this.Memory.ReadEnumBig(5); - - public PointerSpan Data => this.Memory[6..]; - - public IEnumerable EnumeratePairs() => this.Format switch - { - 0 => new Format0(this.Data).Pairs, - _ => Array.Empty(), - }; - } - } - - public readonly struct Version1 - { - public readonly PointerSpan Memory; - - public Version1(PointerSpan memory) => this.Memory = memory; - - [Flags] - public enum CoverageFlags : byte - { - Vertical = 1 << 0, - CrossStream = 1 << 1, - Variation = 1 << 2, - } - - public Fixed Version => new(this.Memory); - - public int NumSubtables => this.Memory.ReadI16Big(4); - - public PointerSpan Data => this.Memory[8..]; - - public IEnumerable EnumerateSubtables() - { - var data = this.Data; - for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) - { - var st = new Subtable(data); - data = data[st.Length..]; - yield return st; - } - } - - public IEnumerable EnumerateHorizontalPairs() => this - .EnumerateSubtables() - .Where(x => x.Flags == 0) - .SelectMany(x => x.EnumeratePairs()); - - public readonly struct Subtable - { - public readonly PointerSpan Memory; - - public Subtable(PointerSpan memory) => this.Memory = memory; - - public int Length => this.Memory.ReadI32Big(0); - - public byte Format => this.Memory[4]; - - public CoverageFlags Flags => this.Memory.ReadEnumBig(5); - - public ushort TupleIndex => this.Memory.ReadU16Big(6); - - public PointerSpan Data => this.Memory[8..]; - - public IEnumerable EnumeratePairs() => this.Format switch - { - 0 => new Format0(this.Data).Pairs, - _ => Array.Empty(), - }; - } - } - } - - private readonly struct Name - { - // https://docs.microsoft.com/en-us/typography/opentype/spec/name - // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html - - public static readonly TagStruct DirectoryTableTag = new('n', 'a', 'm', 'e'); - - public readonly PointerSpan Memory; - - public Name(SfntFile file) - : this(file[DirectoryTableTag]) - { - } - - public Name(PointerSpan memory) => this.Memory = memory; - - public ushort Version => this.Memory.ReadU16Big(0); - - public ushort Count => this.Memory.ReadU16Big(2); - - public ushort StorageOffset => this.Memory.ReadU16Big(4); - - public BigEndianPointerSpan NameRecords => new( - this.Memory[6..].As(this.Count), - NameRecord.ReverseEndianness); - - public ushort LanguageCount => - this.Version == 0 ? (ushort)0 : this.Memory.ReadU16Big(6 + this.NameRecords.ByteCount); - - public BigEndianPointerSpan LanguageRecords => this.Version == 0 - ? default - : new( - this.Memory[ - (8 + this.NameRecords - .ByteCount)..] - .As( - this.LanguageCount), - LanguageRecord.ReverseEndianness); - - public PointerSpan Storage => this.Memory[this.StorageOffset..]; - - public string this[in NameRecord record] => - record.PlatformAndEncoding.Decode(this.Storage.Span.Slice(record.StringOffset, record.Length)); - - public string this[in LanguageRecord record] => - Encoding.ASCII.GetString(this.Storage.Span.Slice(record.LanguageTagOffset, record.Length)); - - public struct NameRecord - { - public PlatformAndEncoding PlatformAndEncoding; - public ushort LanguageId; - public NameId NameId; - public ushort Length; - public ushort StringOffset; - - public NameRecord(PointerSpan span) - { - this.PlatformAndEncoding = new(span); - var offset = Unsafe.SizeOf(); - span.ReadBig(ref offset, out this.LanguageId); - span.ReadBig(ref offset, out this.NameId); - span.ReadBig(ref offset, out this.Length); - span.ReadBig(ref offset, out this.StringOffset); - } - - public static NameRecord ReverseEndianness(NameRecord value) => new() - { - PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), - LanguageId = BinaryPrimitives.ReverseEndianness(value.LanguageId), - NameId = (NameId)BinaryPrimitives.ReverseEndianness((ushort)value.NameId), - Length = BinaryPrimitives.ReverseEndianness(value.Length), - StringOffset = BinaryPrimitives.ReverseEndianness(value.StringOffset), - }; - } - - public struct LanguageRecord - { - public ushort Length; - public ushort LanguageTagOffset; - - public LanguageRecord(PointerSpan span) - { - var offset = 0; - span.ReadBig(ref offset, out this.Length); - span.ReadBig(ref offset, out this.LanguageTagOffset); - } - - public static LanguageRecord ReverseEndianness(LanguageRecord value) => new() - { - Length = BinaryPrimitives.ReverseEndianness(value.Length), - LanguageTagOffset = BinaryPrimitives.ReverseEndianness(value.LanguageTagOffset), - }; - } - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs deleted file mode 100644 index 1d437d56d..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Buffers.Binary; -using System.Collections.Generic; -using System.Linq; - -using Dalamud.Interface.Utility; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Deals with TrueType. -/// -internal static partial class TrueTypeUtils -{ - /// - /// Checks whether the given will fail in , - /// and throws an appropriate exception if it is the case. - /// - /// The font config. - public static unsafe void CheckImGuiCompatibleOrThrow(in ImFontConfig fontConfig) - { - var ranges = fontConfig.GlyphRanges; - var sfnt = AsSfntFile(fontConfig); - var cmap = new Cmap(sfnt); - if (cmap.UnicodeTable is not { } unicodeTable) - throw new NotSupportedException("The font does not have a compatible Unicode character mapping table."); - if (unicodeTable.All(x => !ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(x.Key, ranges))) - throw new NotSupportedException("The font does not have any glyph that falls under the requested range."); - } - - /// - /// Enumerates through horizontal pair adjustments of a kern and gpos tables. - /// - /// The font config. - /// The enumerable of pair adjustments. Distance values need to be multiplied by font size in pixels. - public static IEnumerable<(char Left, char Right, float Distance)> ExtractHorizontalPairAdjustments( - ImFontConfig fontConfig) - { - float multiplier; - Dictionary glyphToCodepoints; - Gpos gpos = default; - Kern kern = default; - - try - { - var sfnt = AsSfntFile(fontConfig); - var head = new Head(sfnt); - multiplier = 3f / 4 / head.UnitsPerEm; - - if (new Cmap(sfnt).UnicodeTable is not { } table) - yield break; - - if (sfnt.ContainsKey(Kern.DirectoryTableTag)) - kern = new(sfnt); - else if (sfnt.ContainsKey(Gpos.DirectoryTableTag)) - gpos = new(sfnt); - else - yield break; - - glyphToCodepoints = table - .GroupBy(x => x.Value, x => x.Key) - .OrderBy(x => x.Key) - .ToDictionary( - x => x.Key, - x => x.Where(y => y <= ushort.MaxValue) - .Select(y => (char)y) - .ToArray()); - } - catch - { - // don't care; give up - yield break; - } - - if (kern.Memory.Count != 0) - { - foreach (var pair in kern.EnumerateHorizontalPairs()) - { - if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) - continue; - if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) - continue; - - foreach (var l in leftChars) - { - foreach (var r in rightChars) - yield return (l, r, pair.Value * multiplier); - } - } - } - else if (gpos.Memory.Count != 0) - { - foreach (var pair in gpos.ExtractAdvanceX()) - { - if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) - continue; - if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) - continue; - - foreach (var l in leftChars) - { - foreach (var r in rightChars) - yield return (l, r, pair.Value * multiplier); - } - } - } - } - - private static unsafe SfntFile AsSfntFile(in ImFontConfig fontConfig) - { - var memory = new PointerSpan((byte*)fontConfig.FontData, fontConfig.FontDataSize); - if (memory.Length < 4) - throw new NotSupportedException("File is too short to even have a magic."); - - var magic = memory.ReadU32Big(0); - if (BitConverter.IsLittleEndian) - magic = BinaryPrimitives.ReverseEndianness(magic); - - if (magic == SfntFile.FileTagTrueType1.NativeValue) - return new(memory); - if (magic == SfntFile.FileTagType1.NativeValue) - return new(memory); - if (magic == SfntFile.FileTagOpenTypeWithCff.NativeValue) - return new(memory); - if (magic == SfntFile.FileTagOpenType1_0.NativeValue) - return new(memory); - if (magic == SfntFile.FileTagTrueTypeApple.NativeValue) - return new(memory); - if (magic == TtcFile.FileTag.NativeValue) - return new TtcFile(memory)[fontConfig.FontNo]; - - throw new NotSupportedException($"The given file with the magic 0x{magic:X08} is not supported."); - } -} diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs deleted file mode 100644 index cb7f7c65a..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs +++ /dev/null @@ -1,306 +0,0 @@ -using System.Numerics; -using System.Runtime.CompilerServices; -using System.Text; - -using ImGuiNET; - -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Managed version of , to avoid unnecessary heap allocation and use of unsafe blocks. -/// -public struct SafeFontConfig -{ - /// - /// The raw config. - /// - public ImFontConfig Raw; - - /// - /// Initializes a new instance of the struct. - /// - public SafeFontConfig() - { - this.OversampleH = 1; - this.OversampleV = 1; - this.PixelSnapH = true; - this.GlyphMaxAdvanceX = float.MaxValue; - this.RasterizerMultiply = 1f; - this.RasterizerGamma = 1.4f; - this.EllipsisChar = unchecked((char)-1); - this.Raw.FontDataOwnedByAtlas = 1; - } - - /// - /// Initializes a new instance of the struct, - /// copying applicable values from an existing instance of . - /// - /// Config to copy from. - public unsafe SafeFontConfig(ImFontConfigPtr config) - : this() - { - if (config.NativePtr is not null) - { - this.Raw = *config.NativePtr; - this.Raw.GlyphRanges = null; - } - } - - /// - /// Gets or sets the index of font within a TTF/OTF file. - /// - public int FontNo - { - get => this.Raw.FontNo; - set => this.Raw.FontNo = EnsureRange(value, 0, int.MaxValue); - } - - /// - /// Gets or sets the desired size of the new font, in pixels.
- /// Effectively, this is the line height.
- /// Value is tied with . - ///
- public float SizePx - { - get => this.Raw.SizePixels; - set => this.Raw.SizePixels = EnsureRange(value, float.Epsilon, float.MaxValue); - } - - /// - /// Gets or sets the desired size of the new font, in points.
- /// Effectively, this is the line height.
- /// Value is tied with . - ///
- public float SizePt - { - get => (this.Raw.SizePixels * 3) / 4; - set => this.Raw.SizePixels = EnsureRange((value * 4) / 3, float.Epsilon, float.MaxValue); - } - - /// - /// Gets or sets the horizontal oversampling pixel count.
- /// Rasterize at higher quality for sub-pixel positioning.
- /// Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.
- /// Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. - ///
- public int OversampleH - { - get => this.Raw.OversampleH; - set => this.Raw.OversampleH = EnsureRange(value, 1, int.MaxValue); - } - - /// - /// Gets or sets the vertical oversampling pixel count.
- /// Rasterize at higher quality for sub-pixel positioning.
- /// This is not really useful as we don't use sub-pixel positions on the Y axis. - ///
- public int OversampleV - { - get => this.Raw.OversampleV; - set => this.Raw.OversampleV = EnsureRange(value, 1, int.MaxValue); - } - - /// - /// Gets or sets a value indicating whether to align every glyph to pixel boundary.
- /// Useful e.g. if you are merging a non-pixel aligned font with the default font.
- /// If enabled, you can set and to 1. - ///
- public bool PixelSnapH - { - get => this.Raw.PixelSnapH != 0; - set => this.Raw.PixelSnapH = value ? (byte)1 : (byte)0; - } - - /// - /// Gets or sets the extra spacing (in pixels) between glyphs.
- /// Only X axis is supported for now.
- /// Effectively, it is the letter spacing. - ///
- public Vector2 GlyphExtraSpacing - { - get => this.Raw.GlyphExtraSpacing; - set => this.Raw.GlyphExtraSpacing = new( - EnsureRange(value.X, float.MinValue, float.MaxValue), - EnsureRange(value.Y, float.MinValue, float.MaxValue)); - } - - /// - /// Gets or sets the offset all glyphs from this font input.
- /// Use this to offset fonts vertically when merging multiple fonts. - ///
- public Vector2 GlyphOffset - { - get => this.Raw.GlyphOffset; - set => this.Raw.GlyphOffset = new( - EnsureRange(value.X, float.MinValue, float.MaxValue), - EnsureRange(value.Y, float.MinValue, float.MaxValue)); - } - - /// - /// Gets or sets the glyph ranges, which is a user-provided list of Unicode range. - /// Each range has 2 values, and values are inclusive.
- /// The list must be zero-terminated.
- /// If empty or null, then all the glyphs from the font that is in the range of UCS-2 will be added. - ///
- public ushort[]? GlyphRanges { get; set; } - - /// - /// Gets or sets the minimum AdvanceX for glyphs.
- /// Set only to align font icons.
- /// Set both / to enforce mono-space font. - ///
- public float GlyphMinAdvanceX - { - get => this.Raw.GlyphMinAdvanceX; - set => this.Raw.GlyphMinAdvanceX = - float.IsFinite(value) - ? value - : throw new ArgumentOutOfRangeException( - nameof(value), - value, - $"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); - } - - /// - /// Gets or sets the maximum AdvanceX for glyphs. - /// - public float GlyphMaxAdvanceX - { - get => this.Raw.GlyphMaxAdvanceX; - set => this.Raw.GlyphMaxAdvanceX = - float.IsFinite(value) - ? value - : throw new ArgumentOutOfRangeException( - nameof(value), - value, - $"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); - } - - /// - /// Gets or sets a value that either brightens (>1.0f) or darkens (<1.0f) the font output.
- /// Brightening small fonts may be a good workaround to make them more readable. - ///
- public float RasterizerMultiply - { - get => this.Raw.RasterizerMultiply; - set => this.Raw.RasterizerMultiply = EnsureRange(value, float.Epsilon, float.MaxValue); - } - - /// - /// Gets or sets the gamma value for fonts. - /// - public float RasterizerGamma - { - get => this.Raw.RasterizerGamma; - set => this.Raw.RasterizerGamma = EnsureRange(value, float.Epsilon, float.MaxValue); - } - - /// - /// Gets or sets a value explicitly specifying unicode codepoint of the ellipsis character.
- /// When fonts are being merged first specified ellipsis will be used. - ///
- public char EllipsisChar - { - get => (char)this.Raw.EllipsisChar; - set => this.Raw.EllipsisChar = value; - } - - /// - /// Gets or sets the desired name of the new font. Names longer than 40 bytes will be partially lost. - /// - public unsafe string Name - { - get - { - fixed (void* pName = this.Raw.Name) - { - var span = new ReadOnlySpan(pName, 40); - var firstNull = span.IndexOf((byte)0); - if (firstNull != -1) - span = span[..firstNull]; - return Encoding.UTF8.GetString(span); - } - } - - set - { - fixed (void* pName = this.Raw.Name) - { - var span = new Span(pName, 40); - Encoding.UTF8.GetBytes(value, span); - } - } - } - - /// - /// Gets or sets the desired font to merge with, if set. - /// - public unsafe ImFontPtr MergeFont - { - get => this.Raw.DstFont is not null ? this.Raw.DstFont : default; - set - { - this.Raw.MergeMode = value.NativePtr is null ? (byte)0 : (byte)1; - this.Raw.DstFont = value.NativePtr is null ? default : value.NativePtr; - } - } - - /// - /// Throws with appropriate messages, - /// if this has invalid values. - /// - public readonly void ThrowOnInvalidValues() - { - if (!(this.Raw.FontNo >= 0)) - throw new ArgumentException($"{nameof(this.FontNo)} must not be a negative number."); - - if (!(this.Raw.SizePixels > 0)) - throw new ArgumentException($"{nameof(this.SizePx)} must be a positive number."); - - if (!(this.Raw.OversampleH >= 1)) - throw new ArgumentException($"{nameof(this.OversampleH)} must be a negative number."); - - if (!(this.Raw.OversampleV >= 1)) - throw new ArgumentException($"{nameof(this.OversampleV)} must be a negative number."); - - if (!float.IsFinite(this.Raw.GlyphMinAdvanceX)) - throw new ArgumentException($"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); - - if (!float.IsFinite(this.Raw.GlyphMaxAdvanceX)) - throw new ArgumentException($"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); - - if (!(this.Raw.RasterizerMultiply > 0)) - throw new ArgumentException($"{nameof(this.RasterizerMultiply)} must be a positive number."); - - if (!(this.Raw.RasterizerGamma > 0)) - throw new ArgumentException($"{nameof(this.RasterizerGamma)} must be a positive number."); - - if (this.GlyphRanges is { Length: > 0 } ranges) - { - if (ranges[0] == 0) - { - throw new ArgumentException( - "Font ranges cannot start with 0.", - nameof(this.GlyphRanges)); - } - - if (ranges[(ranges.Length - 1) & ~1] != 0) - { - throw new ArgumentException( - "Font ranges must terminate with a zero at even indices.", - nameof(this.GlyphRanges)); - } - } - } - - private static T EnsureRange(T value, T min, T max, [CallerMemberName] string callerName = "") - where T : INumber - { - if (value < min) - throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be less than {min}."); - if (value > max) - throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be more than {max}."); - - return value; - } -} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index a477ec09e..dd2e5bad3 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -11,8 +12,6 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -31,13 +30,11 @@ public sealed class UiBuilder : IDisposable private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); - private readonly Framework framework = Service.Get(); + private readonly GameFontManager gameFontManager = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); - private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -48,32 +45,14 @@ public sealed class UiBuilder : IDisposable /// The plugin namespace. internal UiBuilder(string namespaceName) { - try - { - this.stopwatch = new Stopwatch(); - this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); - this.namespaceName = namespaceName; + this.stopwatch = new Stopwatch(); + this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); + this.namespaceName = namespaceName; - this.interfaceManager.Draw += this.OnDraw; - this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw); - - this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; - this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); - - this.FontAtlas = - this.scopedFinalizer - .Add( - Service - .Get() - .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); - this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; - this.FontAtlas.RebuildRecommend += this.RebuildFonts; - } - catch - { - this.scopedFinalizer.Dispose(); - throw; - } + this.interfaceManager.Draw += this.OnDraw; + this.interfaceManager.BuildFonts += this.OnBuildFonts; + this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; + this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; } /// @@ -101,19 +80,19 @@ public sealed class UiBuilder : IDisposable /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler. + /// pointers inside this handler.
+ /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! ///
- [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] - public event Action? BuildFonts; + public event Action BuildFonts; /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler. + /// pointers inside this handler.
+ /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! ///
- [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] - public event Action? AfterBuildFonts; + public event Action AfterBuildFonts; /// /// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown. @@ -128,57 +107,18 @@ public sealed class UiBuilder : IDisposable public event Action HideUi; /// - /// Gets the default Dalamud font size in points. + /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. /// - public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt; - - /// - /// Gets the default Dalamud font size in pixels. - /// - public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx; - - /// - /// Gets the default Dalamud font - supporting all game languages and icons.
- /// Accessing this static property outside of is dangerous and not supported. - ///
- /// - /// A font handle corresponding to this font can be obtained with: - /// - /// fontAtlas.NewDelegateFontHandle( - /// e => e.OnPreBuild( - /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); - /// - /// public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt. ///
- /// - /// A font handle corresponding to this font can be obtained with: - /// - /// fontAtlas.NewDelegateFontHandle( - /// e => e.OnPreBuild( - /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); - /// - /// public static ImFontPtr IconFont => InterfaceManager.IconFont; /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. ///
- /// - /// A font handle corresponding to this font can be obtained with: - /// - /// fontAtlas.NewDelegateFontHandle( - /// e => e.OnPreBuild( - /// tk => tk.AddDalamudAssetFont( - /// DalamudAsset.InconsolataRegular, - /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); - /// - /// public static ImFontPtr MonoFont => InterfaceManager.MonoFont; /// @@ -250,11 +190,6 @@ public sealed class UiBuilder : IDisposable /// public bool UiPrepared => Service.GetNullable() != null; - /// - /// Gets the plugin-private font atlas. - /// - public IFontAtlas FontAtlas { get; } - /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// @@ -384,7 +319,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) + .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) .Unwrap(); } else @@ -406,7 +341,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) + .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) .Unwrap(); } else @@ -422,49 +357,19 @@ public sealed class UiBuilder : IDisposable ///
/// Font to get. /// Handle to the game font which may or may not be available for use yet. - [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] - public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), - Service.Get()); + public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style); /// /// Call this to queue a rebuild of the font atlas.
- /// This will invoke any and handlers and ensure that any - /// loaded fonts are ready to be used on the next UI frame. + /// This will invoke any handlers and ensure that any loaded fonts are + /// ready to be used on the next UI frame. ///
public void RebuildFonts() { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); - if (this.AfterBuildFonts is null && this.BuildFonts is null) - this.FontAtlas.BuildFontsAsync(); - else - this.FontAtlas.BuildFontsOnNextFrame(); + this.interfaceManager.RebuildFonts(); } - /// - /// Creates an isolated . - /// - /// Specify when and how to rebuild this atlas. - /// Whether the fonts in the atlas is global scaled. - /// Name for debugging purposes. - /// A new instance of . - /// - /// Use this to create extra font atlases, if you want to create and dispose fonts without having to rebuild all - /// other fonts together.
- /// If is not , - /// the font rebuilding functions must be called manually. - ///
- public IFontAtlas CreateFontAtlas( - FontAtlasAutoRebuildMode autoRebuildMode, - bool isGlobalScaled = true, - string? debugName = null) => - this.scopedFinalizer.Add(Service - .Get() - .CreateFontAtlas( - this.namespaceName + ":" + (debugName ?? "custom"), - autoRebuildMode, - isGlobalScaled)); - /// /// Add a notification to the notification queue. /// @@ -487,7 +392,12 @@ public sealed class UiBuilder : IDisposable /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); + void IDisposable.Dispose() + { + this.interfaceManager.Draw -= this.OnDraw; + this.interfaceManager.BuildFonts -= this.OnBuildFonts; + this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; + } /// /// Open the registered configuration UI, if it exists. @@ -553,12 +463,8 @@ public sealed class UiBuilder : IDisposable this.ShowUi?.InvokeSafely(); } - // just in case, if something goes wrong, prevent drawing; otherwise it probably will crash. - if (!this.FontAtlas.BuildTask.IsCompletedSuccessfully - && (this.BuildFonts is not null || this.AfterBuildFonts is not null)) - { + if (!this.interfaceManager.FontsReady) return; - } ImGui.PushID(this.namespaceName); if (DoStats) @@ -620,28 +526,14 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } - private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) + private void OnBuildFonts() { - if (e.IsAsyncBuildOperation) - return; + this.BuildFonts?.InvokeSafely(); + } - e.OnPreBuild( - _ => - { - var prev = ImGui.GetIO().NativePtr->Fonts; - ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; - this.BuildFonts?.InvokeSafely(); - ImGui.GetIO().NativePtr->Fonts = prev; - }); - - e.OnPostBuild( - _ => - { - var prev = ImGui.GetIO().NativePtr->Fonts; - ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; - this.AfterBuildFonts?.InvokeSafely(); - ImGui.GetIO().NativePtr->Fonts = prev; - }); + private void OnAfterBuildFonts() + { + this.AfterBuildFonts?.InvokeSafely(); } private void OnResizeBuffers() diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 444463d41..ad151ec4e 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,15 +1,10 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Numerics; -using System.Reactive.Disposables; using System.Runtime.InteropServices; -using System.Text.Unicode; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; -using Dalamud.Interface.ManagedFontAtlas; -using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; @@ -36,7 +31,8 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary + && ImGui.GetIO().NativePtr is not null; /// /// Gets the global Dalamud scale; even available before drawing is ready.
@@ -202,7 +198,7 @@ public static class ImGuiHelpers /// If a positive number is given, numbers will be rounded to this. public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) { - Func rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x; + Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; var font = fontPtr.NativePtr; font->FontSize = rounder(font->FontSize * scale); @@ -314,7 +310,6 @@ public static class ImGuiHelpers glyph->U1, glyph->V1, glyph->AdvanceX * scale); - target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint); changed = true; } else if (!missingOnly) @@ -348,18 +343,25 @@ public static class ImGuiHelpers } if (changed && rebuildLookupTable) - { - // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. - // FallbackGlyph is resolved after resolving ' '. - // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, - // making FindGlyph return nullptr. - // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, - // making ImGui attempt to treat whatever was there as a ' '. - // This may cause random glyphs to be sized randomly, if not an access violation exception. - target.NativePtr->FallbackGlyph = null; + target.BuildLookupTableNonstandard(); + } - target.BuildLookupTable(); - } + /// + /// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions. + /// + /// The font. + public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font) + { + // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. + // FallbackGlyph is resolved after resolving ' '. + // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, + // making FindGlyph return nullptr. + // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, + // making ImGui attempt to treat whatever was there as a ' '. + // This may cause random glyphs to be sized randomly, if not an access violation exception. + font.NativePtr->FallbackGlyph = null; + + font.BuildLookupTable(); } /// @@ -405,103 +407,6 @@ public static class ImGuiHelpers public static void CenterCursorFor(float itemWidth) => ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); - /// - /// Allocates memory on the heap using
- /// Memory must be freed using . - ///
- /// Note that null is a valid return value when is 0. - ///
- /// The length of allocated memory. - /// The allocated memory. - /// If returns null. - public static unsafe void* AllocateMemory(int length) - { - // TODO: igMemAlloc takes size_t, which is nint; ImGui.NET apparently interpreted that as uint. - // fix that in ImGui.NET. - switch (length) - { - case 0: - return null; - case < 0: - throw new ArgumentOutOfRangeException( - nameof(length), - length, - $"{nameof(length)} cannot be a negative number."); - default: - var memory = ImGuiNative.igMemAlloc((uint)length); - if (memory is null) - { - throw new OutOfMemoryException( - $"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}"); - } - - return memory; - } - } - - /// - /// Creates a new instance of with a natively backed memory. - /// - /// The created instance. - /// Disposable you can call. - public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder) - { - builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()); - var ptr = builder.NativePtr; - return Disposable.Create(() => - { - if (ptr != null) - ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr); - ptr = null; - }); - } - - /// - /// Builds ImGui Glyph Ranges for use with . - /// - /// The builder. - /// Add fallback codepoints to the range. - /// Add ellipsis codepoints to the range. - /// When disposed, the resource allocated for the range will be freed. - public static unsafe ushort[] BuildRangesToArray( - this ImFontGlyphRangesBuilderPtr builder, - bool addFallbackCodepoints = true, - bool addEllipsisCodepoints = true) - { - if (addFallbackCodepoints) - builder.AddText(FontAtlasFactory.FallbackCodepoints); - if (addEllipsisCodepoints) - { - builder.AddText(FontAtlasFactory.EllipsisCodepoints); - builder.AddChar('.'); - } - - builder.BuildRanges(out var vec); - return new ReadOnlySpan((void*)vec.Data, vec.Size).ToArray(); - } - - /// - public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges) - => CreateImGuiRangesFrom((IEnumerable)ranges); - - /// - /// Creates glyph ranges from .
- /// Use values from . - ///
- /// The unicode ranges. - /// The range array that can be used for . - public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => - ranges - .Where(x => x.FirstCodePoint <= ushort.MaxValue) - .SelectMany( - x => new[] - { - (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), - (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), - }) - .Append((ushort)0) - .ToArray(); - /// /// Determines whether is empty. /// @@ -510,7 +415,7 @@ public static class ImGuiHelpers public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; /// - /// Determines whether is empty. + /// Determines whether is not null and loaded. /// /// The pointer. /// Whether it is empty. @@ -522,27 +427,6 @@ public static class ImGuiHelpers /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; - - /// - /// If is default, then returns . - /// - /// The self. - /// The other. - /// if it is not default; otherwise, . - public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => - self.NativePtr is null ? other : self; - - /// - /// Mark 4K page as used, after adding a codepoint to a font. - /// - /// The font. - /// The codepoint. - internal static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) - { - // Mark 4K page as used - var pageIndex = unchecked((ushort)(codepoint / 4096)); - font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); - } /// /// Finds the corresponding ImGui viewport ID for the given window handle. @@ -564,89 +448,6 @@ public static class ImGuiHelpers return -1; } - /// - /// Attempts to validate that is valid. - /// - /// The font pointer. - /// The exception, if any occurred during validation. - internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr) - { - try - { - var font = fontPtr.NativePtr; - if (font is null) - throw new NullReferenceException("The font is null."); - - _ = Marshal.ReadIntPtr((nint)font); - if (font->IndexedHotData.Data != 0) - _ = Marshal.ReadIntPtr(font->IndexedHotData.Data); - if (font->FrequentKerningPairs.Data != 0) - _ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data); - if (font->IndexLookup.Data != 0) - _ = Marshal.ReadIntPtr(font->IndexLookup.Data); - if (font->Glyphs.Data != 0) - _ = Marshal.ReadIntPtr(font->Glyphs.Data); - if (font->KerningPairs.Data != 0) - _ = Marshal.ReadIntPtr(font->KerningPairs.Data); - if (font->ConfigDataCount == 0 && font->ConfigData is not null) - throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?"); - if (font->ConfigDataCount != 0 && font->ConfigData is null) - throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?"); - if (font->ConfigData is not null) - _ = Marshal.ReadIntPtr((nint)font->ConfigData); - if (font->FallbackGlyph is not null - && ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data)) - throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); - if (font->FallbackHotData is not null - && ((nint)font->FallbackHotData < font->IndexedHotData.Data - || (nint)font->FallbackHotData >= font->IndexedHotData.Data)) - throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); - if (font->ContainerAtlas is not null) - _ = Marshal.ReadIntPtr((nint)font->ContainerAtlas); - } - catch (Exception e) - { - return e; - } - - return null; - } - - /// - /// Updates the fallback char of . - /// - /// The font. - /// The fallback character. - internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c) - { - font.FallbackChar = c; - font.NativePtr->FallbackHotData = - (ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar); - } - - /// - /// Determines if the supplied codepoint is inside the given range, - /// in format of . - /// - /// The codepoint. - /// The ranges. - /// Whether it is the case. - internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr) - { - if (codepoint is <= 0 or >= ushort.MaxValue) - return false; - - while (*rangePtr != 0) - { - var from = *rangePtr++; - var to = *rangePtr++; - if (from <= codepoint && codepoint <= to) - return true; - } - - return false; - } - /// /// Get data needed for each new frame. /// From b3740d0539e5a03d4a2a5c64c479be7c3ee10dd5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:03:14 +0100 Subject: [PATCH 431/585] add Profile.RemoveByInternalNameAsync() --- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- Dalamud/Plugin/Internal/Profiles/Profile.cs | 36 ++++++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 20e2ea7af..5d250a533 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1297,7 +1297,7 @@ internal partial class PluginManager : IDisposable, IServiceType try { // We don't need to apply, it doesn't matter - await this.profileManager.DefaultProfile.RemoveAsync(repoManifest.InternalName, false); + await this.profileManager.DefaultProfile.RemoveByInternalNameAsync(repoManifest.InternalName, false); } catch (ProfileOperationException) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 3e7a2ed55..36cafa29b 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -208,7 +208,7 @@ internal class Profile { entry = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); if (entry == null) - throw new PluginNotFoundException(workingPluginId.ToString()); + throw new PluginNotFoundException(workingPluginId); if (!this.modelV1.Plugins.Remove(entry)) throw new Exception("Couldn't remove plugin from model collection"); @@ -233,6 +233,31 @@ internal class Profile await this.manager.ApplyAllWantStatesAsync(); } + /// + /// Remove a plugin from this profile. + /// This will block until all states have been applied. + /// + /// The internal name of the plugin. + /// Whether or not the current state should immediately be applied. + /// A representing the asynchronous operation. + public async Task RemoveByInternalNameAsync(string internalName, bool apply = true) + { + Guid? pluginToRemove = null; + lock (this) + { + foreach (var plugin in this.Plugins) + { + if (plugin.InternalName.Equals(internalName, StringComparison.Ordinal)) + { + pluginToRemove = plugin.WorkingPluginId; + break; + } + } + } + + await this.RemoveAsync(pluginToRemove ?? throw new PluginNotFoundException(internalName), apply); + } + /// /// This function tries to migrate all plugins with this internalName which do not have /// a GUID to the specified GUID. @@ -308,4 +333,13 @@ internal sealed class PluginNotFoundException : ProfileOperationException : base($"The plugin '{internalName}' was not found in the profile") { } + + /// + /// Initializes a new instance of the class. + /// + /// The ID of the plugin causing the error. + public PluginNotFoundException(Guid workingPluginId) + : base($"The plugin '{workingPluginId}' was not found in the profile") + { + } } From d827151ee550cb0500690dd1ee56ca7c6f501f98 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:21:37 +0100 Subject: [PATCH 432/585] add icon for dev plugins --- Dalamud/DalamudAsset.cs | 19 ++++++---- .../Internal/Windows/PluginImageCache.cs | 6 ++++ .../PluginInstaller/PluginInstallerWindow.cs | 8 +++++ .../PluginInstaller/ProfileManagerWidget.cs | 35 ++++++++++++------- Dalamud/Logging/Internal/ModuleLog.cs | 10 +++--- 5 files changed, 55 insertions(+), 23 deletions(-) diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index 184193796..a7b35b196 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -63,41 +63,48 @@ public enum DalamudAsset [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "troubleIcon.png")] TroubleIcon = 1006, + + /// + /// : The plugin trouble icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "devPluginIcon.png")] + DevPluginIcon = 1007, /// /// : The plugin update icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "updateIcon.png")] - UpdateIcon = 1007, + UpdateIcon = 1008, /// /// : The plugin installed icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "installedIcon.png")] - InstalledIcon = 1008, + InstalledIcon = 1009, /// /// : The third party plugin icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "thirdIcon.png")] - ThirdIcon = 1009, + ThirdIcon = 1010, /// /// : The installed third party plugin icon overlay. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "thirdInstalledIcon.png")] - ThirdInstalledIcon = 1010, + ThirdInstalledIcon = 1011, /// /// : The API bump explainer icon. /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "changelogApiBump.png")] - ChangelogApiBumpIcon = 1011, + ChangelogApiBumpIcon = 1012, /// /// : The background shade for @@ -105,7 +112,7 @@ public enum DalamudAsset /// [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] [DalamudAssetPath("UIRes", "tsmShade.png")] - TitleScreenMenuShade = 1012, + TitleScreenMenuShade = 1013, /// /// : Noto Sans CJK JP Medium. diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 528507229..29adbb3e5 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -98,6 +98,12 @@ internal class PluginImageCache : IDisposable, IServiceType /// public IDalamudTextureWrap TroubleIcon => this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TroubleIcon, this.EmptyTexture); + + /// + /// Gets the devPlugin icon overlay. + /// + public IDalamudTextureWrap DevPluginIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon, this.EmptyTexture); /// /// Gets the plugin update icon overlay. diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 6db48405d..240383695 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1800,6 +1800,14 @@ internal class PluginInstallerWindow : Window, IDisposable var isLoaded = plugin is { IsLoaded: true }; + if (plugin is LocalDevPlugin) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); + ImGui.Image(this.imageCache.DevPluginIcon.ImGuiHandle, iconSize); + ImGui.PopStyleVar(); + ImGui.SetCursorPos(cursorBeforeImage); + } + if (updateAvailable) ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); else if ((trouble && !pluginDisabled) || isOrphan) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index def5f8ce8..26006c84a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -12,6 +12,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Profiles; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; using Serilog; @@ -315,13 +316,13 @@ internal class ProfileManagerWidget if (ImGui.BeginListBox("###pluginPicker", new Vector2(width, width - 80))) { // TODO: Plugin searching should be abstracted... installer and this should use the same search - foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && !x.IsDev && + foreach (var plugin in pm.InstalledPlugins.Where(x => x.Manifest.SupportsProfiles && (this.pickerSearch.IsNullOrWhitespace() || x.Manifest.Name.ToLowerInvariant().Contains(this.pickerSearch.ToLowerInvariant())))) { using var disabled2 = ImRaii.Disabled(profile.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName)); - if (ImGui.Selectable($"{plugin.Manifest.Name}###selector{plugin.Manifest.InternalName}")) + if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}")) { Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); @@ -426,18 +427,28 @@ internal class ProfileManagerWidget Guid? wantRemovePluginGuid = null; using var syncScope = profile.GetSyncScope(); - foreach (var plugin in profile.Plugins.ToArray()) + foreach (var profileEntry in profile.Plugins.ToArray()) { didAny = true; - var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == plugin.WorkingPluginId); + var pmPlugin = pm.InstalledPlugins.FirstOrDefault(x => x.Manifest.WorkingPluginId == profileEntry.WorkingPluginId); var btnOffset = 2; if (pmPlugin != null) { + var cursorBeforeIcon = ImGui.GetCursorPos(); pic.TryGetIcon(pmPlugin, pmPlugin.Manifest, pmPlugin.IsThirdParty, out var icon); icon ??= pic.DefaultIcon; ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight)); + + if (pmPlugin is LocalDevPlugin) + { + ImGui.SetCursorPos(cursorBeforeIcon); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); + ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); + ImGui.PopStyleVar(); + } + ImGui.SameLine(); var text = $"{pmPlugin.Name}"; @@ -454,17 +465,17 @@ internal class ProfileManagerWidget ImGui.Image(pic.DefaultIcon.ImGuiHandle, new Vector2(pluginLineHeight)); ImGui.SameLine(); - var text = Locs.NotInstalled(plugin.InternalName); + var text = Locs.NotInstalled(profileEntry.InternalName); var textHeight = ImGui.CalcTextSize(text); var before = ImGui.GetCursorPos(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); ImGui.TextUnformatted(text); - var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == plugin.InternalName); + var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == profileEntry.InternalName); var installable = pm.AvailablePlugins.FirstOrDefault( - x => x.InternalName == plugin.InternalName && !x.SourceRepo.IsThirdParty); + x => x.InternalName == profileEntry.InternalName && !x.SourceRepo.IsThirdParty); if (firstAvailableInstalled != null) { @@ -494,10 +505,10 @@ internal class ProfileManagerWidget ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30)); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); - var enabled = plugin.IsEnabled; - if (ImGui.Checkbox($"###{this.editingProfileGuid}-{plugin.InternalName}", ref enabled)) + var enabled = profileEntry.IsEnabled; + if (ImGui.Checkbox($"###{this.editingProfileGuid}-{profileEntry.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.WorkingPluginId, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } @@ -505,9 +516,9 @@ internal class ProfileManagerWidget ImGui.SetCursorPosX(windowSize.X - (ImGuiHelpers.GlobalScale * 30 * btnOffset) - 5); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (ImGui.GetFrameHeight() / 2)); - if (ImGuiComponents.IconButton($"###removePlugin{plugin.InternalName}", FontAwesomeIcon.Trash)) + if (ImGuiComponents.IconButton($"###removePlugin{profileEntry.InternalName}", FontAwesomeIcon.Trash)) { - wantRemovePluginGuid = plugin.WorkingPluginId; + wantRemovePluginGuid = profileEntry.WorkingPluginId; } if (ImGui.IsItemHovered()) diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index e59db09d3..1fe955294 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -43,7 +43,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Verbose(Exception exception, string messageTemplate, params object?[] values) + public void Verbose(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values); /// @@ -62,7 +62,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Debug(Exception exception, string messageTemplate, params object?[] values) + public void Debug(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values); /// @@ -81,7 +81,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Information(Exception exception, string messageTemplate, params object?[] values) + public void Information(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values); /// @@ -100,7 +100,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Warning(Exception exception, string messageTemplate, params object?[] values) + public void Warning(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values); /// @@ -138,7 +138,7 @@ public class ModuleLog /// The message template. /// Values to log. [MessageTemplateFormatMethod("messageTemplate")] - public void Fatal(Exception exception, string messageTemplate, params object?[] values) + public void Fatal(Exception? exception, string messageTemplate, params object?[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); [MessageTemplateFormatMethod("messageTemplate")] From 256f4989f7795a15f47badcb3f4b8e2bb0e628db Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:39:18 +0100 Subject: [PATCH 433/585] add some validation code to catch issues --- Dalamud/Plugin/Internal/PluginManager.cs | 33 +++++++++++++++++++ .../Internal/Profiles/ProfileManager.cs | 31 +++++++++++++---- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 5d250a533..7af530ee9 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -664,6 +664,15 @@ internal partial class PluginManager : IDisposable, IServiceType this.PluginsReady = true; this.NotifyinstalledPluginsListChanged(); sigScanner.Save(); + + try + { + this.ParanoiaValidatePluginsAndProfiles(); + } + catch (Exception ex) + { + Log.Error(ex, "Plugin and profile validation failed!"); + } }, tokenSource.Token); } @@ -1256,6 +1265,30 @@ internal partial class PluginManager : IDisposable, IServiceType } } + /// + /// Check if there are any inconsistencies with our plugins, their IDs, and our profiles. + /// + private void ParanoiaValidatePluginsAndProfiles() + { + var seenIds = new List(); + + foreach (var installedPlugin in this.InstalledPlugins) + { + if (installedPlugin.Manifest.WorkingPluginId == Guid.Empty) + throw new Exception($"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has an empty WorkingPluginId."); + + if (seenIds.Contains(installedPlugin.Manifest.WorkingPluginId)) + { + throw new Exception( + $"{(installedPlugin is LocalDevPlugin ? "DevPlugin" : "Plugin")} '{installedPlugin.Manifest.InternalName}' has a duplicate WorkingPluginId '{installedPlugin.Manifest.WorkingPluginId}'"); + } + + seenIds.Add(installedPlugin.Manifest.WorkingPluginId); + } + + this.profileManager.ParanoiaValidateProfiles(); + } + private async Task DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting) { var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 6b51f7535..768583bea 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -216,19 +216,18 @@ internal class ProfileManager : IServiceType this.isBusy = true; Log.Information("Getting want states..."); - List wantActive; + List wantActive; lock (this.profiles) { wantActive = this.profiles .Where(x => x.IsEnabled) - .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled) - .Select(plugin => plugin.WorkingPluginId)) + .SelectMany(profile => profile.Plugins.Where(plugin => plugin.IsEnabled)) .Distinct().ToList(); } - foreach (var internalName in wantActive) + foreach (var profilePluginEntry in wantActive) { - Log.Information("\t=> Want {Name}", internalName); + Log.Information("\t=> Want {Name}({WorkingPluginId})", profilePluginEntry.InternalName, profilePluginEntry.WorkingPluginId); } Log.Information("Applying want states..."); @@ -238,7 +237,7 @@ internal class ProfileManager : IServiceType var pm = Service.Get(); foreach (var installedPlugin in pm.InstalledPlugins) { - var wantThis = wantActive.Contains(installedPlugin.Manifest.WorkingPluginId); + var wantThis = wantActive.Any(x => x.WorkingPluginId == installedPlugin.Manifest.WorkingPluginId); switch (wantThis) { case true when !installedPlugin.IsLoaded: @@ -314,6 +313,26 @@ internal class ProfileManager : IServiceType profile.MigrateProfilesToGuidsForPlugin(internalName, newGuid); } } + + /// + /// Validate profiles for errors. + /// + /// Thrown when a profile is not sane. + public void ParanoiaValidateProfiles() + { + foreach (var profile in this.profiles) + { + var seenIds = new List(); + + foreach (var pluginEntry in profile.Plugins) + { + if (seenIds.Contains(pluginEntry.WorkingPluginId)) + throw new Exception($"Plugin '{pluginEntry.WorkingPluginId}'('{pluginEntry.InternalName}') is twice in profile '{profile.Guid}'('{profile.Name}')"); + + seenIds.Add(pluginEntry.WorkingPluginId); + } + } + } private string GenerateUniqueProfileName(string startingWith) { From 9024c9b00c8ec826c47468f01a8485647af3fb84 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:47:56 +0100 Subject: [PATCH 434/585] track internal name nonetheless --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 10 +++++----- .../Windows/PluginInstaller/ProfileManagerWidget.cs | 4 ++-- Dalamud/Plugin/Internal/PluginManager.cs | 10 +++++----- Dalamud/Plugin/Internal/Profiles/Profile.cs | 6 ++++-- Dalamud/Plugin/Internal/Profiles/ProfileManager.cs | 7 ++++--- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- 6 files changed, 21 insertions(+), 18 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 240383695..1545efb65 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2579,7 +2579,7 @@ internal class PluginInstallerWindow : Window, IDisposable { if (inProfile) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true)) .ContinueWith(this.DisplayErrorContinuation, Locs.Profiles_CouldNotAdd); } else @@ -2604,7 +2604,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.IsLoaded, false)) + Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, plugin.IsLoaded, false)) .GetAwaiter().GetResult(); foreach (var profile in profileManager.Profiles.Where(x => !x.IsDefaultProfile && x.Plugins.Any(y => y.InternalName == plugin.Manifest.InternalName))) { @@ -2682,7 +2682,7 @@ internal class PluginInstallerWindow : Window, IDisposable { await plugin.UnloadAsync(); await applicableProfile.AddOrUpdateAsync( - plugin.Manifest.WorkingPluginId, false, false); + plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); }).ContinueWith(t => @@ -2699,7 +2699,7 @@ internal class PluginInstallerWindow : Window, IDisposable this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; this.enableDisableWorkingPluginId = plugin.Manifest.WorkingPluginId; - await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false); await plugin.LoadAsync(PluginLoadReason.Installer); notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success); @@ -2720,7 +2720,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (shouldUpdate) { // We need to update the profile right here, because PM will not enable the plugin otherwise - await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false); await this.UpdateSinglePlugin(availableUpdate); } else diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 26006c84a..62806404a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -324,7 +324,7 @@ internal class ProfileManagerWidget if (ImGui.Selectable($"{plugin.Manifest.Name}{(plugin is LocalDevPlugin ? "(dev plugin)" : string.Empty)}###selector{plugin.Manifest.InternalName}")) { - Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false)) + Task.Run(() => profile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } } @@ -508,7 +508,7 @@ internal class ProfileManagerWidget var enabled = profileEntry.IsEnabled; if (ImGui.Checkbox($"###{this.editingProfileGuid}-{profileEntry.InternalName}", ref enabled)) { - Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, enabled)) + Task.Run(() => profile.AddOrUpdateAsync(profileEntry.WorkingPluginId, profileEntry.InternalName, enabled)) .ContinueWith(this.installer.DisplayErrorContinuation, Locs.ErrorCouldNotChangeState); } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 7af530ee9..c57487d1d 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1511,28 +1511,28 @@ internal partial class PluginManager : IDisposable, IServiceType { // We didn't want this plugin, and StartOnBoot is on. That means we don't want it and it should stay off until manually enabled. Log.Verbose("DevPlugin {Name} disabled and StartOnBoot => disable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == true && devPlugin.StartOnBoot) { // We wanted this plugin, and StartOnBoot is on. That means we actually do want it. Log.Verbose("DevPlugin {Name} enabled and StartOnBoot => enable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, true, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, true, false); loadPlugin = !doNotLoad; } else if (wantsInDefaultProfile == true && !devPlugin.StartOnBoot) { // We wanted this plugin, but StartOnBoot is off. This means we don't want it anymore. Log.Verbose("DevPlugin {Name} enabled and !StartOnBoot => disable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); loadPlugin = false; } else if (wantsInDefaultProfile == false && !devPlugin.StartOnBoot) { // We didn't want this plugin, and StartOnBoot is off. We don't want it. Log.Verbose("DevPlugin {Name} disabled and !StartOnBoot => disable", plugin.Manifest.InternalName); - await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, false, false); + await this.profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); loadPlugin = false; } @@ -1544,7 +1544,7 @@ internal partial class PluginManager : IDisposable, IServiceType #pragma warning restore CS0618 // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, defaultState); + var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); if (loadPlugin) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 36cafa29b..df5b045e2 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -158,10 +158,11 @@ internal class Profile /// This will block until all states have been applied. /// /// The ID of the plugin. + /// The internal name of the plugin, if available. /// Whether or not the plugin should be enabled. /// Whether or not the current state should immediately be applied. /// A representing the asynchronous operation. - public async Task AddOrUpdateAsync(Guid workingPluginId, bool state, bool apply = true) + public async Task AddOrUpdateAsync(Guid workingPluginId, string? internalName, bool state, bool apply = true) { Debug.Assert(workingPluginId != Guid.Empty, "Trying to add plugin with empty guid"); @@ -176,6 +177,7 @@ internal class Profile { this.modelV1.Plugins.Add(new ProfileModelV1.ProfileModelV1Plugin { + InternalName = internalName, WorkingPluginId = workingPluginId, IsEnabled = state, }); @@ -219,7 +221,7 @@ internal class Profile { if (!this.IsDefaultProfile) { - await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, this.IsEnabled && entry.IsEnabled, false); + await this.manager.DefaultProfile.AddOrUpdateAsync(workingPluginId, entry.InternalName, this.IsEnabled && entry.IsEnabled, false); } else { diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 768583bea..10d94de73 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -70,10 +70,11 @@ internal class ProfileManager : IServiceType /// Check if any enabled profile wants a specific plugin enabled. /// /// The ID of the plugin. + /// The internal name of the plugin, if available. /// The state the plugin shall be in, if it needs to be added. /// Whether or not the plugin should be added to the default preset, if it's not present in any preset. /// Whether or not the plugin shall be enabled. - public async Task GetWantStateAsync(Guid workingPluginId, bool defaultState, bool addIfNotDeclared = true) + public async Task GetWantStateAsync(Guid workingPluginId, string? internalName, bool defaultState, bool addIfNotDeclared = true) { var want = false; var wasInAnyProfile = false; @@ -93,8 +94,8 @@ internal class ProfileManager : IServiceType if (!wasInAnyProfile && addIfNotDeclared) { - Log.Warning("{Guid} was not in any profile, adding to default with {Default}", workingPluginId, defaultState); - await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, defaultState, false); + Log.Warning("'{Guid}'('{InternalName}') was not in any profile, adding to default with {Default}", workingPluginId, internalName, defaultState); + await this.DefaultProfile.AddOrUpdateAsync(workingPluginId, internalName, defaultState, false); return defaultState; } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 348563781..0f65bafb2 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -164,7 +164,7 @@ internal class LocalPlugin : IDisposable /// INCLUDES the default profile. /// public bool IsWantedByAnyProfile => - Service.Get().GetWantStateAsync(this.manifest.WorkingPluginId, false, false).GetAwaiter().GetResult(); + Service.Get().GetWantStateAsync(this.manifest.WorkingPluginId, this.Manifest.InternalName, false, false).GetAwaiter().GetResult(); /// /// Gets a value indicating whether this plugin's API level is out of date. From 23ddc7824126efccc226e36d6a8cf328a992757a Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 18 Jan 2024 22:53:17 +0100 Subject: [PATCH 435/585] add bodge "match to plugin" UI for installed plugins --- .../PluginInstaller/ProfileManagerWidget.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 62806404a..2d45869e0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -479,8 +479,22 @@ internal class ProfileManagerWidget if (firstAvailableInstalled != null) { - // TODO - ImGui.Text("GOAT WAS TOO LAZY TO IMPLEMENT THIS"); + ImGui.Text($"Match to plugin '{firstAvailableInstalled.Name}'?"); + ImGui.SameLine(); + if (ImGuiComponents.IconButtonWithText( + FontAwesomeIcon.Check, + "Yes, use this one")) + { + profileEntry.WorkingPluginId = firstAvailableInstalled.Manifest.WorkingPluginId; + Task.Run(async () => + { + await profman.ApplyAllWantStatesAsync(); + }) + .ContinueWith(t => + { + this.installer.DisplayErrorContinuation(t, Locs.ErrorCouldNotChangeState); + }); + } } else if (installable != null) { From 63b16bcc7cd5fba67fbe9943caae92f851e05441 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 19 Jan 2024 07:26:56 +0900 Subject: [PATCH 436/585] Reapply "IFontAtlas: font atlas per plugin" This reverts commit b5696afe94b9ace8c58323a751b5bb88cae9cece. --- .../Internal/DalamudConfiguration.cs | 7 +- Dalamud/Interface/GameFonts/FdtFileView.cs | 159 ++ .../GameFonts/GameFontFamilyAndSize.cs | 25 +- .../GameFontFamilyAndSizeAttribute.cs | 37 + Dalamud/Interface/GameFonts/GameFontHandle.cs | 83 +- .../Interface/GameFonts/GameFontManager.cs | 507 ------ Dalamud/Interface/GameFonts/GameFontStyle.cs | 37 +- Dalamud/Interface/Internal/DalamudIme.cs | 7 +- .../Interface/Internal/DalamudInterface.cs | 13 +- .../Interface/Internal/InterfaceManager.cs | 964 +++--------- .../Internal/Windows/ChangelogWindow.cs | 62 +- .../Internal/Windows/Data/DataWindow.cs | 8 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 213 +++ .../Windows/Settings/SettingsWindow.cs | 29 +- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 30 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 50 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 63 +- .../FontAtlasAutoRebuildMode.cs | 22 + .../ManagedFontAtlas/FontAtlasBuildStep.cs | 38 + .../FontAtlasBuildStepDelegate.cs | 15 + .../FontAtlasBuildToolkitUtilities.cs | 133 ++ .../Interface/ManagedFontAtlas/IFontAtlas.cs | 141 ++ .../IFontAtlasBuildToolkit.cs | 67 + .../IFontAtlasBuildToolkitPostBuild.cs | 26 + .../IFontAtlasBuildToolkitPostPromotion.cs | 33 + .../IFontAtlasBuildToolkitPreBuild.cs | 186 +++ .../Interface/ManagedFontAtlas/IFontHandle.cs | 42 + .../Internals/DelegateFontHandle.cs | 334 ++++ .../FontAtlasFactory.BuildToolkit.cs | 682 ++++++++ .../FontAtlasFactory.Implementation.cs | 726 +++++++++ .../Internals/FontAtlasFactory.cs | 368 +++++ .../Internals/GamePrebakedFontHandle.cs | 857 ++++++++++ .../Internals/IFontHandleManager.cs | 32 + .../Internals/IFontHandleSubstance.cs | 54 + .../Internals/TrueType.Common.cs | 203 +++ .../Internals/TrueType.Enums.cs | 84 + .../Internals/TrueType.Files.cs | 148 ++ .../Internals/TrueType.GposGsub.cs | 259 +++ .../Internals/TrueType.PointerSpan.cs | 443 ++++++ .../Internals/TrueType.Tables.cs | 1391 +++++++++++++++++ .../ManagedFontAtlas/Internals/TrueType.cs | 135 ++ .../ManagedFontAtlas/SafeFontConfig.cs | 306 ++++ Dalamud/Interface/UiBuilder.cs | 182 ++- Dalamud/Interface/Utility/ImGuiHelpers.cs | 243 ++- 44 files changed, 7944 insertions(+), 1500 deletions(-) create mode 100644 Dalamud/Interface/GameFonts/FdtFileView.cs create mode 100644 Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs delete mode 100644 Dalamud/Interface/GameFonts/GameFontManager.cs create mode 100644 Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 76c8f3603..66c2745c5 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -148,12 +148,9 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable public bool UseAxisFontsFromGame { get; set; } = false; /// - /// Gets or sets the gamma value to apply for Dalamud fonts. Effects text thickness. - /// - /// Before gamma is applied... - /// * ...TTF fonts loaded with stb or FreeType are in linear space. - /// * ...the game's prebaked AXIS fonts are in gamma space with gamma value of 1.4. + /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. /// + [Obsolete("It happens that nobody touched this setting", true)] public float FontGammaLevel { get; set; } = 1.4f; /// diff --git a/Dalamud/Interface/GameFonts/FdtFileView.cs b/Dalamud/Interface/GameFonts/FdtFileView.cs new file mode 100644 index 000000000..896a6dbb4 --- /dev/null +++ b/Dalamud/Interface/GameFonts/FdtFileView.cs @@ -0,0 +1,159 @@ +using System.Collections.Generic; +using System.IO; + +namespace Dalamud.Interface.GameFonts; + +/// +/// Reference member view of a .fdt file data. +/// +internal readonly unsafe struct FdtFileView +{ + private readonly byte* ptr; + + /// + /// Initializes a new instance of the struct. + /// + /// Pointer to the data. + /// Length of the data. + public FdtFileView(void* ptr, int length) + { + this.ptr = (byte*)ptr; + if (length < sizeof(FdtReader.FdtHeader)) + throw new InvalidDataException("Not enough space for a FdtHeader"); + + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader)) + throw new InvalidDataException("Not enough space for a FontTableHeader"); + if (length < this.FileHeader.FontTableHeaderOffset + sizeof(FdtReader.FontTableHeader) + + (sizeof(FdtReader.FontTableEntry) * this.FontHeader.FontTableEntryCount)) + throw new InvalidDataException("Not enough space for all the FontTableEntry"); + + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader)) + throw new InvalidDataException("Not enough space for a KerningTableHeader"); + if (length < this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader) + + (sizeof(FdtReader.KerningTableEntry) * this.KerningEntryCount)) + throw new InvalidDataException("Not enough space for all the KerningTableEntry"); + } + + /// + /// Gets the file header. + /// + public ref FdtReader.FdtHeader FileHeader => ref *(FdtReader.FdtHeader*)this.ptr; + + /// + /// Gets the font header. + /// + public ref FdtReader.FontTableHeader FontHeader => + ref *(FdtReader.FontTableHeader*)((nint)this.ptr + this.FileHeader.FontTableHeaderOffset); + + /// + /// Gets the glyphs. + /// + public Span Glyphs => new(this.GlyphsUnsafe, this.FontHeader.FontTableEntryCount); + + /// + /// Gets the kerning header. + /// + public ref FdtReader.KerningTableHeader KerningHeader => + ref *(FdtReader.KerningTableHeader*)((nint)this.ptr + this.FileHeader.KerningTableHeaderOffset); + + /// + /// Gets the number of kerning entries. + /// + public int KerningEntryCount => Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); + + /// + /// Gets the kerning entries. + /// + public Span PairAdjustments => new( + this.ptr + this.FileHeader.KerningTableHeaderOffset + sizeof(FdtReader.KerningTableHeader), + this.KerningEntryCount); + + /// + /// Gets the maximum texture index. + /// + public int MaxTextureIndex + { + get + { + var i = 0; + foreach (ref var g in this.Glyphs) + { + if (g.TextureIndex > i) + i = g.TextureIndex; + } + + return i; + } + } + + private FdtReader.FontTableEntry* GlyphsUnsafe => + (FdtReader.FontTableEntry*)(this.ptr + this.FileHeader.FontTableHeaderOffset + + sizeof(FdtReader.FontTableHeader)); + + /// + /// Finds the glyph index for the corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding index, or a negative number according to . + public int FindGlyphIndex(int codepoint) + { + var comp = FdtReader.CodePointToUtf8Int32(codepoint); + + var glyphs = this.GlyphsUnsafe; + var lo = 0; + var hi = this.FontHeader.FontTableEntryCount - 1; + while (lo <= hi) + { + var i = (int)(((uint)hi + (uint)lo) >> 1); + switch (comp.CompareTo(glyphs[i].CharUtf8)) + { + case 0: + return i; + case > 0: + lo = i + 1; + break; + default: + hi = i - 1; + break; + } + } + + return ~lo; + } + + /// + /// Create a glyph range for use with . + /// + /// Merge two ranges into one if distance is below the value specified in this parameter. + /// Glyph ranges. + public ushort[] ToGlyphRanges(int mergeDistance = 8) + { + var glyphs = this.Glyphs; + var ranges = new List(glyphs.Length) + { + checked((ushort)glyphs[0].CharInt), + checked((ushort)glyphs[0].CharInt), + }; + + foreach (ref var glyph in glyphs[1..]) + { + var c32 = glyph.CharInt; + if (c32 >= 0x10000) + break; + + var c16 = unchecked((ushort)c32); + if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) + { + ranges[^1] = c16; + } + else if (ranges[^1] + 1 < c16) + { + ranges.Add(c16); + ranges.Add(c16); + } + } + + ranges.Add(0); + return ranges.ToArray(); + } +} diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs index dd78baf87..6e66cf19b 100644 --- a/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSize.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Enum of available game fonts in specific sizes. /// -public enum GameFontFamilyAndSize : int +public enum GameFontFamilyAndSize { /// /// Placeholder meaning unused. @@ -15,6 +15,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_96.fdt", "common/font/font{0}.tex", -1)] Axis96, /// @@ -22,6 +23,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_12.fdt", "common/font/font{0}.tex", -1)] Axis12, /// @@ -29,6 +31,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_14.fdt", "common/font/font{0}.tex", -1)] Axis14, /// @@ -36,6 +39,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_18.fdt", "common/font/font{0}.tex", -1)] Axis18, /// @@ -43,6 +47,7 @@ public enum GameFontFamilyAndSize : int /// /// Contains Japanese characters in addition to Latin characters. Used in game for the whole UI. /// + [GameFontFamilyAndSize("common/font/AXIS_36.fdt", "common/font/font{0}.tex", -4)] Axis36, /// @@ -50,6 +55,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_16.fdt", "common/font/font{0}.tex", -1)] Jupiter16, /// @@ -57,6 +63,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_20.fdt", "common/font/font{0}.tex", -1)] Jupiter20, /// @@ -64,6 +71,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_23.fdt", "common/font/font{0}.tex", -1)] Jupiter23, /// @@ -71,6 +79,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_45.fdt", "common/font/font{0}.tex", -2)] Jupiter45, /// @@ -78,6 +87,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly ASCII range. Used in game for job names. /// + [GameFontFamilyAndSize("common/font/Jupiter_46.fdt", "common/font/font{0}.tex", -2)] Jupiter46, /// @@ -85,6 +95,7 @@ public enum GameFontFamilyAndSize : int /// /// Serif font. Contains mostly numbers. Used in game for flying texts. /// + [GameFontFamilyAndSize("common/font/Jupiter_90.fdt", "common/font/font{0}.tex", -4)] Jupiter90, /// @@ -92,6 +103,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_16.fdt", "common/font/font{0}.tex", -1)] Meidinger16, /// @@ -99,6 +111,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_20.fdt", "common/font/font{0}.tex", -1)] Meidinger20, /// @@ -106,6 +119,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly numbers. Used in game for HP/MP/IL stuff. /// + [GameFontFamilyAndSize("common/font/Meidinger_40.fdt", "common/font/font{0}.tex", -4)] Meidinger40, /// @@ -113,6 +127,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_10.fdt", "common/font/font{0}.tex", -1)] MiedingerMid10, /// @@ -120,6 +135,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_12.fdt", "common/font/font{0}.tex", -1)] MiedingerMid12, /// @@ -127,6 +143,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_14.fdt", "common/font/font{0}.tex", -1)] MiedingerMid14, /// @@ -134,6 +151,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_18.fdt", "common/font/font{0}.tex", -1)] MiedingerMid18, /// @@ -141,6 +159,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally wide. Contains mostly ASCII range. /// + [GameFontFamilyAndSize("common/font/MiedingerMid_36.fdt", "common/font/font{0}.tex", -2)] MiedingerMid36, /// @@ -148,6 +167,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_184.fdt", "common/font/font{0}.tex", -1)] TrumpGothic184, /// @@ -155,6 +175,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_23.fdt", "common/font/font{0}.tex", -1)] TrumpGothic23, /// @@ -162,6 +183,7 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_34.fdt", "common/font/font{0}.tex", -1)] TrumpGothic34, /// @@ -169,5 +191,6 @@ public enum GameFontFamilyAndSize : int /// /// Horizontally narrow. Contains mostly ASCII range. Used for addon titles. /// + [GameFontFamilyAndSize("common/font/TrumpGothic_68.fdt", "common/font/font{0}.tex", -3)] TrumpGothic68, } diff --git a/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs new file mode 100644 index 000000000..f5260e4bc --- /dev/null +++ b/Dalamud/Interface/GameFonts/GameFontFamilyAndSizeAttribute.cs @@ -0,0 +1,37 @@ +namespace Dalamud.Interface.GameFonts; + +/// +/// Marks the path for an enum value. +/// +[AttributeUsage(AttributeTargets.Field)] +internal class GameFontFamilyAndSizeAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// Inner path of the file. + /// the file path format for the relevant .tex files. + /// Horizontal offset of the corresponding font. + public GameFontFamilyAndSizeAttribute(string path, string texPathFormat, int horizontalOffset) + { + this.Path = path; + this.TexPathFormat = texPathFormat; + this.HorizontalOffset = horizontalOffset; + } + + /// + /// Gets the path. + /// + public string Path { get; } + + /// + /// Gets the file path format for the relevant .tex files.
+ /// Used for (, ). + ///
+ public string TexPathFormat { get; } + + /// + /// Gets the horizontal offset of the corresponding font. + /// + public int HorizontalOffset { get; } +} diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index d71e725c5..77461aa0a 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,75 +1,76 @@ -using System; using System.Numerics; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; + using ImGuiNET; namespace Dalamud.Interface.GameFonts; /// -/// Prepare and keep game font loaded for use in OnDraw. +/// ABI-compatible wrapper for . /// -public class GameFontHandle : IDisposable +public sealed class GameFontHandle : IFontHandle { - private readonly GameFontManager manager; - private readonly GameFontStyle fontStyle; + private readonly IFontHandle.IInternal fontHandle; + private readonly FontAtlasFactory fontAtlasFactory; /// /// Initializes a new instance of the class. /// - /// GameFontManager instance. - /// Font to use. - internal GameFontHandle(GameFontManager manager, GameFontStyle font) + /// The wrapped . + /// An instance of . + internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) { - this.manager = manager; - this.fontStyle = font; + this.fontHandle = fontHandle; + this.fontAtlasFactory = fontAtlasFactory; } - /// - /// Gets the font style. - /// - public GameFontStyle Style => this.fontStyle; + /// + public Exception? LoadException => this.fontHandle.LoadException; + + /// + public bool Available => this.fontHandle.Available; + + /// + [Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)] + public ImFontPtr ImFont => this.fontHandle.ImFont; /// - /// Gets a value indicating whether this font is ready for use. + /// Gets the font style. Only applicable for . /// - public bool Available - { - get - { - unsafe - { - return this.manager.GetFont(this.fontStyle).GetValueOrDefault(null).NativePtr != null; - } - } - } + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontStyle Style => ((GamePrebakedFontHandle)this.fontHandle).FontStyle; /// - /// Gets the font. + /// Gets the relevant .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
- public ImFontPtr ImFont => this.manager.GetFont(this.fontStyle).Value; + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public FdtReader FdtReader => this.fontAtlasFactory.GetFdtReader(this.Style.FamilyAndSize)!; + + /// + public void Dispose() => this.fontHandle.Dispose(); + + /// + public IDisposable Push() => this.fontHandle.Push(); /// - /// Gets the FdtReader. - /// - public FdtReader FdtReader => this.manager.GetFdtReader(this.fontStyle.FamilyAndSize); - - /// - /// Creates a new GameFontLayoutPlan.Builder. + /// Creates a new .
+ ///
+ /// Only applicable for game fonts. Otherwise it will throw. ///
/// Text. /// A new builder for GameFontLayoutPlan. - public GameFontLayoutPlan.Builder LayoutBuilder(string text) - { - return new GameFontLayoutPlan.Builder(this.ImFont, this.FdtReader, text); - } - - /// - public void Dispose() => this.manager.DecreaseFontRef(this.fontStyle); + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] + public GameFontLayoutPlan.Builder LayoutBuilder(string text) => new(this.ImFont, this.FdtReader, text); /// /// Draws text. /// /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void Text(string text) { if (!this.Available) @@ -93,6 +94,7 @@ public class GameFontHandle : IDisposable ///
/// Color. /// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextColored(Vector4 col, string text) { ImGui.PushStyleColor(ImGuiCol.Text, col); @@ -104,6 +106,7 @@ public class GameFontHandle : IDisposable /// Draws disabled text. ///
/// Text to draw. + [Obsolete("If you use this, let the fact that you use this be known at Dalamud Discord.", false)] public void TextDisabled(string text) { unsafe diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs deleted file mode 100644 index b3454e085..000000000 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ /dev/null @@ -1,507 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -using Dalamud.Data; -using Dalamud.Game; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; -using Dalamud.Utility.Timing; -using ImGuiNET; -using Lumina.Data.Files; -using Serilog; - -using static Dalamud.Interface.Utility.ImGuiHelpers; - -namespace Dalamud.Interface.GameFonts; - -/// -/// Loads game font for use in ImGui. -/// -[ServiceManager.BlockingEarlyLoadedService] -internal class GameFontManager : IServiceType -{ - private static readonly string?[] FontNames = - { - null, - "AXIS_96", "AXIS_12", "AXIS_14", "AXIS_18", "AXIS_36", - "Jupiter_16", "Jupiter_20", "Jupiter_23", "Jupiter_45", "Jupiter_46", "Jupiter_90", - "Meidinger_16", "Meidinger_20", "Meidinger_40", - "MiedingerMid_10", "MiedingerMid_12", "MiedingerMid_14", "MiedingerMid_18", "MiedingerMid_36", - "TrumpGothic_184", "TrumpGothic_23", "TrumpGothic_34", "TrumpGothic_68", - }; - - private readonly object syncRoot = new(); - - private readonly FdtReader?[] fdts; - private readonly List texturePixels; - private readonly Dictionary fonts = new(); - private readonly Dictionary fontUseCounter = new(); - private readonly Dictionary>> glyphRectIds = new(); - -#pragma warning disable CS0414 - private bool isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; -#pragma warning restore CS0414 - - [ServiceManager.ServiceConstructor] - private GameFontManager(DataManager dataManager) - { - using (Timings.Start("Getting fdt data")) - { - this.fdts = FontNames.Select(fontName => fontName == null ? null : new FdtReader(dataManager.GetFile($"common/font/{fontName}.fdt")!.Data)).ToArray(); - } - - using (Timings.Start("Getting texture data")) - { - var texTasks = Enumerable - .Range(1, 1 + this.fdts - .Where(x => x != null) - .Select(x => x.Glyphs.Select(y => y.TextureFileIndex).Max()) - .Max()) - .Select(x => dataManager.GetFile($"common/font/font{x}.tex")!) - .Select(x => new Task(Timings.AttachTimingHandle(() => x.ImageData!))) - .ToArray(); - foreach (var task in texTasks) - task.Start(); - this.texturePixels = texTasks.Select(x => x.GetAwaiter().GetResult()).ToList(); - } - } - - /// - /// Describe font into a string. - /// - /// Font to describe. - /// A string in a form of "FontName (NNNpt)". - public static string DescribeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Undefined => "-", - GameFontFamilyAndSize.Axis96 => "AXIS (9.6pt)", - GameFontFamilyAndSize.Axis12 => "AXIS (12pt)", - GameFontFamilyAndSize.Axis14 => "AXIS (14pt)", - GameFontFamilyAndSize.Axis18 => "AXIS (18pt)", - GameFontFamilyAndSize.Axis36 => "AXIS (36pt)", - GameFontFamilyAndSize.Jupiter16 => "Jupiter (16pt)", - GameFontFamilyAndSize.Jupiter20 => "Jupiter (20pt)", - GameFontFamilyAndSize.Jupiter23 => "Jupiter (23pt)", - GameFontFamilyAndSize.Jupiter45 => "Jupiter Numeric (45pt)", - GameFontFamilyAndSize.Jupiter46 => "Jupiter (46pt)", - GameFontFamilyAndSize.Jupiter90 => "Jupiter Numeric (90pt)", - GameFontFamilyAndSize.Meidinger16 => "Meidinger Numeric (16pt)", - GameFontFamilyAndSize.Meidinger20 => "Meidinger Numeric (20pt)", - GameFontFamilyAndSize.Meidinger40 => "Meidinger Numeric (40pt)", - GameFontFamilyAndSize.MiedingerMid10 => "MiedingerMid (10pt)", - GameFontFamilyAndSize.MiedingerMid12 => "MiedingerMid (12pt)", - GameFontFamilyAndSize.MiedingerMid14 => "MiedingerMid (14pt)", - GameFontFamilyAndSize.MiedingerMid18 => "MiedingerMid (18pt)", - GameFontFamilyAndSize.MiedingerMid36 => "MiedingerMid (36pt)", - GameFontFamilyAndSize.TrumpGothic184 => "Trump Gothic (18.4pt)", - GameFontFamilyAndSize.TrumpGothic23 => "Trump Gothic (23pt)", - GameFontFamilyAndSize.TrumpGothic34 => "Trump Gothic (34pt)", - GameFontFamilyAndSize.TrumpGothic68 => "Trump Gothic (68pt)", - _ => throw new ArgumentOutOfRangeException(nameof(font), font, "Invalid argument"), - }; - } - - /// - /// Determines whether a font should be able to display most of stuff. - /// - /// Font to check. - /// True if it can. - public static bool IsGenericPurposeFont(GameFontFamilyAndSize font) - { - return font switch - { - GameFontFamilyAndSize.Axis96 => true, - GameFontFamilyAndSize.Axis12 => true, - GameFontFamilyAndSize.Axis14 => true, - GameFontFamilyAndSize.Axis18 => true, - GameFontFamilyAndSize.Axis36 => true, - _ => false, - }; - } - - /// - /// Unscales fonts after they have been rendered onto atlas. - /// - /// Font to unscale. - /// Scale factor. - /// Whether to call target.BuildLookupTable(). - public static void UnscaleFont(ImFontPtr fontPtr, float fontScale, bool rebuildLookupTable = true) - { - if (fontScale == 1) - return; - - unsafe - { - var font = fontPtr.NativePtr; - for (int i = 0, i_ = font->IndexedHotData.Size; i < i_; ++i) - { - font->IndexedHotData.Ref(i).AdvanceX /= fontScale; - font->IndexedHotData.Ref(i).OccupiedWidth /= fontScale; - } - - font->FontSize /= fontScale; - font->Ascent /= fontScale; - font->Descent /= fontScale; - if (font->ConfigData != null) - font->ConfigData->SizePixels /= fontScale; - var glyphs = (ImFontGlyphReal*)font->Glyphs.Data; - for (int i = 0, i_ = font->Glyphs.Size; i < i_; i++) - { - var glyph = &glyphs[i]; - glyph->X0 /= fontScale; - glyph->X1 /= fontScale; - glyph->Y0 /= fontScale; - glyph->Y1 /= fontScale; - glyph->AdvanceX /= fontScale; - } - - for (int i = 0, i_ = font->KerningPairs.Size; i < i_; i++) - font->KerningPairs.Ref(i).AdvanceXAdjustment /= fontScale; - for (int i = 0, i_ = font->FrequentKerningPairs.Size; i < i_; i++) - font->FrequentKerningPairs.Ref(i) /= fontScale; - } - - if (rebuildLookupTable && fontPtr.Glyphs.Size > 0) - fontPtr.BuildLookupTableNonstandard(); - } - - /// - /// Create a glyph range for use with ImGui AddFont. - /// - /// Font family and size. - /// Merge two ranges into one if distance is below the value specified in this parameter. - /// Glyph ranges. - public GCHandle ToGlyphRanges(GameFontFamilyAndSize family, int mergeDistance = 8) - { - var fdt = this.fdts[(int)family]!; - var ranges = new List(fdt.Glyphs.Count) - { - checked((ushort)fdt.Glyphs[0].CharInt), - checked((ushort)fdt.Glyphs[0].CharInt), - }; - - foreach (var glyph in fdt.Glyphs.Skip(1)) - { - var c32 = glyph.CharInt; - if (c32 >= 0x10000) - break; - - var c16 = unchecked((ushort)c32); - if (ranges[^1] + mergeDistance >= c16 && c16 > ranges[^1]) - { - ranges[^1] = c16; - } - else if (ranges[^1] + 1 < c16) - { - ranges.Add(c16); - ranges.Add(c16); - } - } - - return GCHandle.Alloc(ranges.ToArray(), GCHandleType.Pinned); - } - - /// - /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. - /// - /// Font to use. - /// Handle to game font that may or may not be ready yet. - public GameFontHandle NewFontRef(GameFontStyle style) - { - var interfaceManager = Service.Get(); - var needRebuild = false; - - lock (this.syncRoot) - { - this.fontUseCounter[style] = this.fontUseCounter.GetValueOrDefault(style, 0) + 1; - } - - needRebuild = !this.fonts.ContainsKey(style); - if (needRebuild) - { - Log.Information("[GameFontManager] NewFontRef: Queueing RebuildFonts because {0} has been requested.", style.ToString()); - Service.GetAsync() - .ContinueWith(task => task.Result.RunOnTick(() => interfaceManager.RebuildFonts())); - } - - return new(this, style); - } - - /// - /// Gets the font. - /// - /// Font to get. - /// Corresponding font or null. - public ImFontPtr? GetFont(GameFontStyle style) => this.fonts.GetValueOrDefault(style, null); - - /// - /// Gets the corresponding FdtReader. - /// - /// Font to get. - /// Corresponding FdtReader or null. - public FdtReader? GetFdtReader(GameFontFamilyAndSize family) => this.fdts[(int)family]; - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); - } - - /// - /// Fills missing glyphs in target font from source font, if both are not null. - /// - /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - public void CopyGlyphsAcrossFonts(GameFontStyle source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) - { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], this.fonts[target], missingOnly, rebuildLookupTable); - } - - /// - /// Build fonts before plugins do something more. To be called from InterfaceManager. - /// - public void BuildFonts() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = true; - - this.glyphRectIds.Clear(); - this.fonts.Clear(); - - lock (this.syncRoot) - { - foreach (var style in this.fontUseCounter.Keys) - this.EnsureFont(style); - } - } - - /// - /// Record that ImGui.GetIO().Fonts.Build() has been called. - /// - public void AfterIoFontsBuild() - { - this.isBetweenBuildFontsAndRightAfterImGuiIoFontsBuild = false; - } - - /// - /// Checks whether GameFontMamager owns an ImFont. - /// - /// ImFontPtr to check. - /// Whether it owns. - public bool OwnsFont(ImFontPtr fontPtr) => this.fonts.ContainsValue(fontPtr); - - /// - /// Post-build fonts before plugins do something more. To be called from InterfaceManager. - /// - public unsafe void AfterBuildFonts() - { - var interfaceManager = Service.Get(); - var ioFonts = ImGui.GetIO().Fonts; - var fontGamma = interfaceManager.FontGamma; - - var pixels8s = new byte*[ioFonts.Textures.Size]; - var pixels32s = new uint*[ioFonts.Textures.Size]; - var widths = new int[ioFonts.Textures.Size]; - var heights = new int[ioFonts.Textures.Size]; - for (var i = 0; i < pixels8s.Length; i++) - { - ioFonts.GetTexDataAsRGBA32(i, out pixels8s[i], out widths[i], out heights[i]); - pixels32s[i] = (uint*)pixels8s[i]; - } - - foreach (var (style, font) in this.fonts) - { - var fdt = this.fdts[(int)style.FamilyAndSize]; - var scale = style.SizePt / fdt.FontHeader.Size; - var fontPtr = font.NativePtr; - - Log.Verbose("[GameFontManager] AfterBuildFonts: Scaling {0} from {1}pt to {2}pt (scale: {3})", style.ToString(), fdt.FontHeader.Size, style.SizePt, scale); - - fontPtr->FontSize = fdt.FontHeader.Size * 4 / 3; - if (fontPtr->ConfigData != null) - fontPtr->ConfigData->SizePixels = fontPtr->FontSize; - fontPtr->Ascent = fdt.FontHeader.Ascent; - fontPtr->Descent = fdt.FontHeader.Descent; - fontPtr->EllipsisChar = '…'; - foreach (var fallbackCharCandidate in "〓?!") - { - var glyph = font.FindGlyphNoFallback(fallbackCharCandidate); - if ((IntPtr)glyph.NativePtr != IntPtr.Zero) - { - var ptr = font.NativePtr; - ptr->FallbackChar = fallbackCharCandidate; - ptr->FallbackGlyph = glyph.NativePtr; - ptr->FallbackHotData = (ImFontGlyphHotData*)ptr->IndexedHotData.Address(fallbackCharCandidate); - break; - } - } - - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes(style.ToString() + "\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - foreach (var (c, (rectId, glyph)) in this.glyphRectIds[style]) - { - var rc = (ImFontAtlasCustomRectReal*)ioFonts.GetCustomRectByIndex(rectId).NativePtr; - var pixels8 = pixels8s[rc->TextureIndex]; - var pixels32 = pixels32s[rc->TextureIndex]; - var width = widths[rc->TextureIndex]; - var height = heights[rc->TextureIndex]; - var sourceBuffer = this.texturePixels[glyph.TextureFileIndex]; - var sourceBufferDelta = glyph.TextureChannelByteIndex; - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - if (widthAdjustment == 0) - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var a = sourceBuffer[sourceBufferDelta + (4 * (((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x))]; - pixels32[((rc->Y + y) * width) + rc->X + x] = (uint)(a << 24) | 0xFFFFFFu; - } - } - } - else - { - for (var y = 0; y < glyph.BoundingHeight; y++) - { - for (var x = 0; x < glyph.BoundingWidth + widthAdjustment; x++) - pixels32[((rc->Y + y) * width) + rc->X + x] = 0xFFFFFFu; - } - - for (int xbold = 0, xbold_ = Math.Max(1, (int)Math.Ceiling(style.Weight + 1)); xbold < xbold_; xbold++) - { - var boldStrength = Math.Min(1f, style.Weight + 1 - xbold); - for (var y = 0; y < glyph.BoundingHeight; y++) - { - float xDelta = xbold; - if (style.BaseSkewStrength > 0) - xDelta += style.BaseSkewStrength * (fdt.FontHeader.LineHeight - glyph.CurrentOffsetY - y) / fdt.FontHeader.LineHeight; - else if (style.BaseSkewStrength < 0) - xDelta -= style.BaseSkewStrength * (glyph.CurrentOffsetY + y) / fdt.FontHeader.LineHeight; - var xDeltaInt = (int)Math.Floor(xDelta); - var xness = xDelta - xDeltaInt; - for (var x = 0; x < glyph.BoundingWidth; x++) - { - var sourcePixelIndex = ((glyph.TextureOffsetY + y) * fdt.FontHeader.TextureWidth) + glyph.TextureOffsetX + x; - var a1 = sourceBuffer[sourceBufferDelta + (4 * sourcePixelIndex)]; - var a2 = x == glyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourceBufferDelta + (4 * (sourcePixelIndex + 1))]; - var n = (a1 * xness) + (a2 * (1 - xness)); - var targetOffset = ((rc->Y + y) * width) + rc->X + x + xDeltaInt; - pixels8[(targetOffset * 4) + 3] = Math.Max(pixels8[(targetOffset * 4) + 3], (byte)(boldStrength * n)); - } - } - } - } - - if (Math.Abs(fontGamma - 1.4f) >= 0.001) - { - // Gamma correction (stbtt/FreeType would output in linear space whereas most real world usages will apply 1.4 or 1.8 gamma; Windows/XIV prebaked uses 1.4) - for (int y = rc->Y, y_ = rc->Y + rc->Height; y < y_; y++) - { - for (int x = rc->X, x_ = rc->X + rc->Width; x < x_; x++) - { - var i = (((y * width) + x) * 4) + 3; - pixels8[i] = (byte)(Math.Pow(pixels8[i] / 255.0f, 1.4f / fontGamma) * 255.0f); - } - } - } - } - - UnscaleFont(font, 1 / scale, false); - } - } - - /// - /// Decrease font reference counter. - /// - /// Font to release. - internal void DecreaseFontRef(GameFontStyle style) - { - lock (this.syncRoot) - { - if (!this.fontUseCounter.ContainsKey(style)) - return; - - if ((this.fontUseCounter[style] -= 1) == 0) - this.fontUseCounter.Remove(style); - } - } - - private unsafe void EnsureFont(GameFontStyle style) - { - var rectIds = this.glyphRectIds[style] = new(); - - var fdt = this.fdts[(int)style.FamilyAndSize]; - if (fdt == null) - return; - - ImFontConfigPtr fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - fontConfig.PixelSnapH = false; - - var io = ImGui.GetIO(); - var font = io.Fonts.AddFontDefault(fontConfig); - - fontConfig.Destroy(); - - this.fonts[style] = font; - foreach (var glyph in fdt.Glyphs) - { - var c = glyph.Char; - if (c < 32 || c >= 0xFFFF) - continue; - - var widthAdjustment = style.CalculateBaseWidthAdjustment(fdt, glyph); - rectIds[c] = Tuple.Create( - io.Fonts.AddCustomRectFontGlyph( - font, - c, - glyph.BoundingWidth + widthAdjustment, - glyph.BoundingHeight, - glyph.AdvanceWidth, - new Vector2(0, glyph.CurrentOffsetY)), - glyph); - } - - foreach (var kernPair in fdt.Distances) - font.AddKerningPair(kernPair.Left, kernPair.Right, kernPair.RightOffset); - } -} diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index 946473df4..fbaf9de07 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -64,7 +64,7 @@ public struct GameFontStyle ///
public float SizePt { - get => this.SizePx * 3 / 4; + readonly get => this.SizePx * 3 / 4; set => this.SizePx = value * 4 / 3; } @@ -73,14 +73,14 @@ public struct GameFontStyle ///
public float BaseSkewStrength { - get => this.SkewStrength * this.BaseSizePx / this.SizePx; + readonly get => this.SkewStrength * this.BaseSizePx / this.SizePx; set => this.SkewStrength = value * this.SizePx / this.BaseSizePx; } /// /// Gets the font family. /// - public GameFontFamily Family => this.FamilyAndSize switch + public readonly GameFontFamily Family => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => GameFontFamily.Undefined, GameFontFamilyAndSize.Axis96 => GameFontFamily.Axis, @@ -112,7 +112,7 @@ public struct GameFontStyle /// /// Gets the corresponding GameFontFamilyAndSize but with minimum possible font sizes. /// - public GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch + public readonly GameFontFamilyAndSize FamilyWithMinimumSize => this.Family switch { GameFontFamily.Axis => GameFontFamilyAndSize.Axis96, GameFontFamily.Jupiter => GameFontFamilyAndSize.Jupiter16, @@ -126,7 +126,7 @@ public struct GameFontStyle /// /// Gets the base font size in point unit. /// - public float BaseSizePt => this.FamilyAndSize switch + public readonly float BaseSizePt => this.FamilyAndSize switch { GameFontFamilyAndSize.Undefined => 0, GameFontFamilyAndSize.Axis96 => 9.6f, @@ -158,14 +158,14 @@ public struct GameFontStyle /// /// Gets the base font size in pixel unit. /// - public float BaseSizePx => this.BaseSizePt * 4 / 3; + public readonly float BaseSizePx => this.BaseSizePt * 4 / 3; /// /// Gets or sets a value indicating whether this font is bold. /// public bool Bold { - get => this.Weight > 0f; + readonly get => this.Weight > 0f; set => this.Weight = value ? 1f : 0f; } @@ -174,8 +174,8 @@ public struct GameFontStyle ///
public bool Italic { - get => this.SkewStrength != 0; - set => this.SkewStrength = value ? this.SizePx / 7 : 0; + readonly get => this.SkewStrength != 0; + set => this.SkewStrength = value ? this.SizePx / 6 : 0; } /// @@ -233,13 +233,26 @@ public struct GameFontStyle _ => GameFontFamilyAndSize.Undefined, }; + /// + /// Creates a new scaled instance of struct. + /// + /// The scale. + /// The scaled instance. + public readonly GameFontStyle Scale(float scale) => new() + { + FamilyAndSize = GetRecommendedFamilyAndSize(this.Family, this.SizePt * scale), + SizePx = this.SizePx * scale, + Weight = this.Weight, + SkewStrength = this.SkewStrength * scale, + }; + /// /// Calculates the adjustment to width resulting fron Weight and SkewStrength. /// /// Font header. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + public readonly int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) { var widthDelta = this.Weight; switch (this.BaseSkewStrength) @@ -263,11 +276,11 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + public readonly int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// - public override string ToString() + public override readonly string ToString() { return $"GameFontStyle({this.FamilyAndSize}, {this.SizePt}pt, skew={this.SkewStrength}, weight={this.Weight})"; } diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index e030b4e50..28a9075bd 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -11,6 +11,7 @@ using System.Text.Unicode; using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using ImGuiNET; @@ -196,9 +197,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { - if (Service.Get() - .GetFdtReader(GameFontFamilyAndSize.Axis12) - ?.FindGlyph(chr) is null) + if (Service.Get() + ?.GetFdtReader(GameFontFamilyAndSize.Axis12) + .FindGlyph(chr) is null) { if (!this.EncounteredHan) { diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 95415659b..60c1f9957 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.SelfTest; using Dalamud.Interface.Internal.Windows.Settings; using Dalamud.Interface.Internal.Windows.StyleEditor; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -93,7 +94,8 @@ internal class DalamudInterface : IDisposable, IServiceType private DalamudInterface( Dalamud dalamud, DalamudConfiguration configuration, - InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, + FontAtlasFactory fontAtlasFactory, + InterfaceManager interfaceManager, PluginImageCache pluginImageCache, DalamudAssetManager dalamudAssetManager, Game.Framework framework, @@ -103,7 +105,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.dalamud = dalamud; this.configuration = configuration; - this.interfaceManager = interfaceManagerWithScene.Manager; + this.interfaceManager = interfaceManager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -122,10 +124,14 @@ internal class DalamudInterface : IDisposable, IServiceType clientState, configuration, dalamudAssetManager, + fontAtlasFactory, framework, gameGui, titleScreenMenu) { IsOpen = false }; - this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; + this.changelogWindow = new ChangelogWindow( + this.titleScreenMenuWindow, + fontAtlasFactory, + dalamudAssetManager) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; this.hitchSettingsWindow = new HitchSettingsWindow() { IsOpen = false }; @@ -207,6 +213,7 @@ internal class DalamudInterface : IDisposable, IServiceType { this.interfaceManager.Draw -= this.OnDraw; + this.WindowSystem.Windows.OfType().AggregateToDisposable().Dispose(); this.WindowSystem.RemoveAllWindows(); this.changelogWindow.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 48157fa86..3e004727a 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,13 +1,10 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Text; -using System.Text.Unicode; -using System.Threading; +using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; @@ -19,10 +16,13 @@ using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Storage.Assets; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -64,11 +64,9 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. - private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. - - private readonly HashSet glyphRequests = new(); - private readonly Dictionary loadedFontInfo = new(); + private const int NonMainThreadFontAccessWarningCheckInterval = 10000; + private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); + private static long nextNonMainThreadFontAccessWarningCheck; private readonly List deferredDisposeTextures = new(); @@ -81,28 +79,28 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly DalamudIme dalamudIme = Service.Get(); - private readonly ManualResetEvent fontBuildSignal; - private readonly SwapChainVtableResolver address; + private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; private RawDX11Scene? scene; private Hook? presentHook; private Hook? resizeBuffersHook; + private IFontAtlas? dalamudAtlas; + private IFontHandle.IInternal? defaultFontHandle; + private IFontHandle.IInternal? iconFontHandle; + private IFontHandle.IInternal? monoFontHandle; + // can't access imgui IO before first present call private bool lastWantCapture = false; - private bool isRebuildingFonts = false; private bool isOverrideGameCursor = true; + private IntPtr gameWindowHandle; [ServiceManager.ServiceConstructor] private InterfaceManager() { this.setCursorHook = Hook.FromImport( null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); - - this.fontBuildSignal = new ManualResetEvent(false); - - this.address = new SwapChainVtableResolver(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -117,43 +115,46 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// This event gets called each frame to facilitate ImGui drawing. /// - public event RawDX11Scene.BuildUIDelegate Draw; + public event RawDX11Scene.BuildUIDelegate? Draw; /// /// This event gets called when ResizeBuffers is called. /// - public event Action ResizeBuffers; - - /// - /// Gets or sets an action that is executed right before fonts are rebuilt. - /// - public event Action BuildFonts; + public event Action? ResizeBuffers; /// /// Gets or sets an action that is executed right after fonts are rebuilt. /// - public event Action AfterBuildFonts; + public event Action? AfterBuildFonts; /// - /// Gets the default ImGui font. + /// Gets the default ImGui font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont { get; private set; } + public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included FontAwesome icon font. + /// Gets an included FontAwesome icon font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont { get; private set; } + public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// - /// Gets an included monospaced font. + /// Gets an included monospaced font.
+ /// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont { get; private set; } + public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. /// public ImGuiIOPtr LastImGuiIoPtr { get; set; } + /// + /// Gets the DX11 scene. + /// + public RawDX11Scene? Scene => this.scene; + /// /// Gets the D3D11 device instance. /// @@ -178,11 +179,6 @@ internal class InterfaceManager : IDisposable, IServiceType } } - /// - /// Gets or sets a value indicating whether the fonts are built and ready to use. - /// - public bool FontsReady { get; set; } = false; - /// /// Gets a value indicating whether the Dalamud interface ready to use. /// @@ -193,50 +189,57 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public bool IsDispatchingEvents { get; set; } = true; - /// - /// Gets or sets a value indicating whether to override configuration for UseAxis. - /// - public bool? UseAxisOverride { get; set; } = null; - - /// - /// Gets a value indicating whether to use AXIS fonts. - /// - public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; - - /// - /// Gets or sets the overrided font gamma value, instead of using the value from configuration. - /// - public float? FontGammaOverride { get; set; } = null; - - /// - /// Gets the font gamma value to use. - /// - public float FontGamma => Math.Max(0.1f, this.FontGammaOverride.GetValueOrDefault(Service.Get().FontGammaLevel)); - - /// - /// Gets a value indicating whether we're building fonts but haven't generated atlas yet. - /// - public bool IsBuildingFontsBeforeAtlasBuild => this.isRebuildingFonts && !this.fontBuildSignal.WaitOne(0); - /// /// Gets a value indicating the native handle of the game main window. /// - public IntPtr GameWindowHandle { get; private set; } + public IntPtr GameWindowHandle + { + get + { + if (this.gameWindowHandle == 0) + { + nint gwh = 0; + while ((gwh = NativeFunctions.FindWindowEx(0, gwh, "FFXIVGAME", 0)) != 0) + { + _ = User32.GetWindowThreadProcessId(gwh, out var pid); + if (pid == Environment.ProcessId && User32.IsWindowVisible(gwh)) + { + this.gameWindowHandle = gwh; + break; + } + } + } + + return this.gameWindowHandle; + } + } + + /// + /// Gets the font build task. + /// + public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; /// /// Dispose of managed and unmanaged resources. /// public void Dispose() { - this.framework.RunOnFrameworkThread(() => + if (Service.GetNullable() is { } framework) + framework.RunOnFrameworkThread(Disposer).Wait(); + else + Disposer(); + + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.dalamudAtlas?.Dispose(); + this.scene?.Dispose(); + return; + + void Disposer() { this.setCursorHook.Dispose(); this.presentHook?.Dispose(); this.resizeBuffersHook?.Dispose(); - }).Wait(); - - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.scene?.Dispose(); + } } #nullable enable @@ -376,93 +379,8 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public void RebuildFonts() { - if (this.scene == null) - { - Log.Verbose("[FONT] RebuildFonts(): scene not ready, doing nothing"); - return; - } - Log.Verbose("[FONT] RebuildFonts() called"); - - // don't invoke this multiple times per frame, in case multiple plugins call it - if (!this.isRebuildingFonts) - { - Log.Verbose("[FONT] RebuildFonts() trigger"); - this.isRebuildingFonts = true; - this.scene.OnNewRenderFrame += this.RebuildFontsInternal; - } - } - - /// - /// Wait for the rebuilding fonts to complete. - /// - public void WaitForFontRebuild() - { - this.fontBuildSignal.WaitOne(); - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Ranges of glyphs. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, List> ranges) - { - var allContained = false; - var fonts = ImGui.GetIO().Fonts.Fonts; - ImFontPtr foundFont = null; - unsafe - { - for (int i = 0, i_ = fonts.Size; i < i_; i++) - { - if (!this.glyphRequests.Any(x => x.FontInternal.NativePtr == fonts[i].NativePtr)) - continue; - - allContained = true; - foreach (var range in ranges) - { - if (!allContained) - break; - - for (var j = range.Item1; j <= range.Item2 && allContained; j++) - allContained &= fonts[i].FindGlyphNoFallback(j).NativePtr != null; - } - - if (allContained) - foundFont = fonts[i]; - - break; - } - } - - var req = new SpecialGlyphRequest(this, size, ranges); - req.FontInternal = foundFont; - - if (!allContained) - this.RebuildFonts(); - - return req; - } - - /// - /// Requests a default font of specified size to exist. - /// - /// Font size in pixels. - /// Text to calculate glyph ranges from. - /// Requets handle. - public SpecialGlyphRequest NewFontSizeRef(float size, string text) - { - List> ranges = new(); - foreach (var c in new SortedSet(text.ToHashSet())) - { - if (ranges.Any() && ranges[^1].Item2 + 1 == c) - ranges[^1] = Tuple.Create(ranges[^1].Item1, c); - else - ranges.Add(Tuple.Create(c, c)); - } - - return this.NewFontSizeRef(size, ranges); + this.dalamudAtlas?.BuildFontsAsync(); } /// @@ -486,11 +404,11 @@ internal class InterfaceManager : IDisposable, IServiceType try { var dxgiDev = this.Device.QueryInterfaceOrNull(); - var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); + var dxgiAdapter = dxgiDev?.Adapter.QueryInterfaceOrNull(); if (dxgiAdapter == null) return null; - var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, SharpDX.DXGI.MemorySegmentGroup.Local); + var memInfo = dxgiAdapter.QueryVideoMemoryInfo(0, MemorySegmentGroup.Local); return (memInfo.CurrentUsage, memInfo.CurrentReservation); } catch @@ -516,20 +434,65 @@ internal class InterfaceManager : IDisposable, IServiceType /// Value. internal void SetImmersiveMode(bool enabled) { - if (this.GameWindowHandle == nint.Zero) - return; - - int value = enabled ? 1 : 0; - var hr = NativeFunctions.DwmSetWindowAttribute( - this.GameWindowHandle, - NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - ref value, - sizeof(int)); + if (this.GameWindowHandle == 0) + throw new InvalidOperationException("Game window is not yet ready."); + var value = enabled ? 1 : 0; + ((Result)NativeFunctions.DwmSetWindowAttribute( + this.GameWindowHandle, + NativeFunctions.DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + ref value, + sizeof(int))).CheckError(); } - private static void ShowFontError(string path) + private static InterfaceManager WhenFontsReady() { - Util.Fatal($"One or more files required by XIVLauncher were not found.\nPlease restart and report this error if it occurs again.\n\n{path}", "Error"); + var im = Service.GetNullable(); + if (im?.dalamudAtlas is not { } atlas) + throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call."); + + if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) + { + nextNonMainThreadFontAccessWarningCheck = + Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; + var stack = new StackTrace(); + if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) + { + if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) + { + NonMainThreadFontAccessWarning.Add(plugin, new()); + Log.Warning( + "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", + plugin.Name, + stack); + } + } + else + { + // Dalamud internal should be made safe right now + throw new InvalidOperationException("Attempted to access fonts outside the main thread."); + } + } + + if (!atlas.HasBuiltAtlas) + atlas.BuildTask.GetAwaiter().GetResult(); + return im; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void RenderImGui(RawDX11Scene scene) + { + var conf = Service.Get(); + + // Process information needed by ImGuiHelpers each frame. + ImGuiHelpers.NewFrame(); + + // Enable viewports if there are no issues. + if (conf.IsDisableViewport || scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; + else + ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; + + scene.Render(); } private void InitScene(IntPtr swapChain) @@ -546,7 +509,7 @@ internal class InterfaceManager : IDisposable, IServiceType Service.ProvideException(ex); Log.Error(ex, "Could not load ImGui dependencies."); - var res = PInvoke.User32.MessageBox( + var res = User32.MessageBox( IntPtr.Zero, "Dalamud plugins require the Microsoft Visual C++ Redistributable to be installed.\nPlease install the runtime from the official Microsoft website or disable Dalamud.\n\nDo you want to download the redistributable now?", "Dalamud Error", @@ -578,7 +541,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (iniFileInfo.Length > 1200000) { Log.Warning("dalamudUI.ini was over 1mb, deleting"); - iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); + iniFileInfo.CopyTo(Path.Combine(iniFileInfo.DirectoryName!, $"dalamudUI-{DateTimeOffset.Now.ToUnixTimeSeconds()}.ini")); iniFileInfo.Delete(); } } @@ -623,8 +586,6 @@ internal class InterfaceManager : IDisposable, IServiceType ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - this.SetupFonts(); - if (!configuration.IsDocking) { ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.DockingEnable; @@ -675,26 +636,34 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { + Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); + Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); + if (this.scene != null && swapChain != this.scene.SwapChain.NativePointer) return this.presentHook!.Original(swapChain, syncInterval, presentFlags); if (this.scene == null) this.InitScene(swapChain); + Debug.Assert(this.scene is not null, "InitScene did not set the scene field, but did not throw an exception."); + + if (!this.dalamudAtlas!.HasBuiltAtlas) + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); + if (this.address.IsReshade) { - var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags); + var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); return pRes; } - this.RenderImGui(); + RenderImGui(this.scene!); this.DisposeTextures(); - return this.presentHook.Original(swapChain, syncInterval, presentFlags); + return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } private void DisposeTextures() @@ -711,471 +680,73 @@ internal class InterfaceManager : IDisposable, IServiceType } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void RenderImGui() + [ServiceManager.CallWhenServicesReady( + "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] + private void ContinueConstruction( + TargetSigScanner sigScanner, + Framework framework, + FontAtlasFactory fontAtlasFactory) { - // Process information needed by ImGuiHelpers each frame. - ImGuiHelpers.NewFrame(); - - // Check if we can still enable viewports without any issues. - this.CheckViewportState(); - - this.scene.Render(); - } - - private void CheckViewportState() - { - var configuration = Service.Get(); - - if (configuration.IsDisableViewport || this.scene.SwapChain.IsFullScreen || ImGui.GetPlatformIO().Monitors.Size == 1) + this.dalamudAtlas = fontAtlasFactory + .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); + using (this.dalamudAtlas.SuppressAutoRebuild()) { - ImGui.GetIO().ConfigFlags &= ~ImGuiConfigFlags.ViewportsEnable; - return; + this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); + this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont( + new() + { + SizePx = DefaultFontSizePx, + GlyphMinAdvanceX = DefaultFontSizePx, + GlyphMaxAdvanceX = DefaultFontSizePx, + }))); + this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddDalamudAssetFont( + DalamudAsset.InconsolataRegular, + new() { SizePx = DefaultFontSizePx }))); + this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( + tk => + { + // Note: the first call of this function is done outside the main thread; this is expected. + // Do not use DefaultFont, IconFont, and MonoFont. + // Use font handles directly. + + // Fill missing glyphs in MonoFont from DefaultFont + tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + + // Broadcast to auto-rebuilding instances + this.AfterBuildFonts?.Invoke(); + }); } - ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.ViewportsEnable; - } + // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. + _ = this.dalamudAtlas.BuildFontsAsync(false); - /// - /// Loads font for use in ImGui text functions. - /// - private unsafe void SetupFonts() - { - using var setupFontsTimings = Timings.Start("IM SetupFonts"); - - var gameFontManager = Service.Get(); - var dalamud = Service.Get(); - var io = ImGui.GetIO(); - var ioFonts = io.Fonts; - - var fontGamma = this.FontGamma; - - this.fontBuildSignal.Reset(); - ioFonts.Clear(); - ioFonts.TexDesiredWidth = 4096; - - Log.Verbose("[FONT] SetupFonts - 1"); - - foreach (var v in this.loadedFontInfo) - v.Value.Dispose(); - - this.loadedFontInfo.Clear(); - - Log.Verbose("[FONT] SetupFonts - 2"); - - ImFontConfigPtr fontConfig = null; - List garbageList = new(); + this.address.Setup(sigScanner); try { - var dummyRangeHandle = GCHandle.Alloc(new ushort[] { '0', '0', 0 }, GCHandleType.Pinned); - garbageList.Add(dummyRangeHandle); - - fontConfig = ImGuiNative.ImFontConfig_ImFontConfig(); - fontConfig.OversampleH = 1; - fontConfig.OversampleV = 1; - - var fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Regular.otf"); - if (!File.Exists(fontPathJp)) - fontPathJp = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKjp-Medium.otf"); - if (!File.Exists(fontPathJp)) - ShowFontError(fontPathJp); - Log.Verbose("[FONT] fontPathJp = {0}", fontPathJp); - - var fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansCJKkr-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "NotoSansKR-Regular.otf"); - if (!File.Exists(fontPathKr)) - fontPathKr = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "malgun.ttf"); - if (!File.Exists(fontPathKr)) - fontPathKr = null; - Log.Verbose("[FONT] fontPathKr = {0}", fontPathKr); - - var fontPathChs = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msyh.ttc"); - if (!File.Exists(fontPathChs)) - fontPathChs = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathChs); - - var fontPathCht = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts", "msjh.ttc"); - if (!File.Exists(fontPathCht)) - fontPathCht = null; - Log.Verbose("[FONT] fontPathChs = {0}", fontPathCht); - - // Default font - Log.Verbose("[FONT] SetupFonts - Default font"); - var fontInfo = new TargetFontModification( - "Default", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - this.UseAxis ? DefaultFontSizePx : DefaultFontSizePx + 1, - io.FontGlobalScale); - Log.Verbose("[FONT] SetupFonts - Default corresponding AXIS size: {0}pt ({1}px)", fontInfo.SourceAxis.Style.BaseSizePt, fontInfo.SourceAxis.Style.BaseSizePx); - fontConfig.SizePixels = fontInfo.TargetSizePx * io.FontGlobalScale; - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = false; - DefaultFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - else - { - var rangeHandle = gameFontManager.ToGlyphRanges(GameFontFamilyAndSize.Axis12); - garbageList.Add(rangeHandle); - - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - DefaultFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontConfig.SizePixels, fontConfig); - this.loadedFontInfo[DefaultFont] = fontInfo; - } - - if (fontPathKr != null - && (Service.Get().EffectiveLanguage == "ko" || this.dalamudIme.EncounteredHangul)) - { - fontConfig.MergeMode = true; - fontConfig.GlyphRanges = ioFonts.GetGlyphRangesKorean(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathKr, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathCht, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" - || this.dalamudIme.EncounteredHan)) - { - fontConfig.MergeMode = true; - var rangeHandle = GCHandle.Alloc(new ushort[] - { - (ushort)UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographs.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographs.Length - 1)), - (ushort)UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint, - (ushort)(UnicodeRanges.CjkUnifiedIdeographsExtensionA.FirstCodePoint + - (UnicodeRanges.CjkUnifiedIdeographsExtensionA.Length - 1)), - 0, - }, GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.GlyphRanges = rangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - ioFonts.AddFontFromFileTTF(fontPathChs, fontConfig.SizePixels, fontConfig); - fontConfig.MergeMode = false; - } - - // FontAwesome icon font - Log.Verbose("[FONT] SetupFonts - FontAwesome icon font"); - { - var fontPathIcon = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "FontAwesomeFreeSolid.otf"); - if (!File.Exists(fontPathIcon)) - ShowFontError(fontPathIcon); - - var iconRangeHandle = GCHandle.Alloc(new ushort[] { 0xE000, 0xF8FF, 0, }, GCHandleType.Pinned); - garbageList.Add(iconRangeHandle); - - fontConfig.GlyphRanges = iconRangeHandle.AddrOfPinnedObject(); - fontConfig.PixelSnapH = true; - IconFont = ioFonts.AddFontFromFileTTF(fontPathIcon, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[IconFont] = new("Icon", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Monospace font - Log.Verbose("[FONT] SetupFonts - Monospace font"); - { - var fontPathMono = Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "Inconsolata-Regular.ttf"); - if (!File.Exists(fontPathMono)) - ShowFontError(fontPathMono); - - fontConfig.GlyphRanges = IntPtr.Zero; - fontConfig.PixelSnapH = true; - MonoFont = ioFonts.AddFontFromFileTTF(fontPathMono, DefaultFontSizePx * io.FontGlobalScale, fontConfig); - this.loadedFontInfo[MonoFont] = new("Mono", TargetFontModification.AxisMode.GameGlyphsOnly, DefaultFontSizePx, io.FontGlobalScale); - } - - // Default font but in requested size for requested glyphs - Log.Verbose("[FONT] SetupFonts - Default font but in requested size for requested glyphs"); - { - Dictionary> extraFontRequests = new(); - foreach (var extraFontRequest in this.glyphRequests) - { - if (!extraFontRequests.ContainsKey(extraFontRequest.Size)) - extraFontRequests[extraFontRequest.Size] = new(); - extraFontRequests[extraFontRequest.Size].Add(extraFontRequest); - } - - foreach (var (fontSize, requests) in extraFontRequests) - { - List<(ushort, ushort)> codepointRanges = new(4 + requests.Sum(x => x.CodepointRanges.Count)) - { - new(Fallback1Codepoint, Fallback1Codepoint), - new(Fallback2Codepoint, Fallback2Codepoint), - // ImGui default ellipsis characters - new(0x2026, 0x2026), - new(0x0085, 0x0085), - }; - - foreach (var request in requests) - codepointRanges.AddRange(request.CodepointRanges.Select(x => (From: x.Item1, To: x.Item2))); - - codepointRanges.Sort(); - List flattenedRanges = new(); - foreach (var range in codepointRanges) - { - if (flattenedRanges.Any() && flattenedRanges[^1] >= range.Item1 - 1) - { - flattenedRanges[^1] = Math.Max(flattenedRanges[^1], range.Item2); - } - else - { - flattenedRanges.Add(range.Item1); - flattenedRanges.Add(range.Item2); - } - } - - flattenedRanges.Add(0); - - fontInfo = new( - $"Requested({fontSize}px)", - this.UseAxis ? TargetFontModification.AxisMode.Overwrite : TargetFontModification.AxisMode.GameGlyphsOnly, - fontSize, - io.FontGlobalScale); - if (this.UseAxis) - { - fontConfig.GlyphRanges = dummyRangeHandle.AddrOfPinnedObject(); - fontConfig.SizePixels = fontInfo.SourceAxis.Style.BaseSizePx; - fontConfig.PixelSnapH = false; - - var sizedFont = ioFonts.AddFontDefault(fontConfig); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - else - { - var rangeHandle = GCHandle.Alloc(flattenedRanges.ToArray(), GCHandleType.Pinned); - garbageList.Add(rangeHandle); - fontConfig.PixelSnapH = true; - - var sizedFont = ioFonts.AddFontFromFileTTF(fontPathJp, fontSize * io.FontGlobalScale, fontConfig, rangeHandle.AddrOfPinnedObject()); - this.loadedFontInfo[sizedFont] = fontInfo; - foreach (var request in requests) - request.FontInternal = sizedFont; - } - } - } - - gameFontManager.BuildFonts(); - - var customFontFirstConfigIndex = ioFonts.ConfigData.Size; - - Log.Verbose("[FONT] Invoke OnBuildFonts"); - this.BuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnBuildFonts OK!"); - - for (int i = customFontFirstConfigIndex, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - if (gameFontManager.OwnsFont(config.DstFont)) - continue; - - config.OversampleH = 1; - config.OversampleV = 1; - - var name = Encoding.UTF8.GetString((byte*)config.Name.Data, config.Name.Count).TrimEnd('\0'); - if (name.IsNullOrEmpty()) - name = $"{config.SizePixels}px"; - - // ImFont information is reflected only if corresponding ImFontConfig has MergeMode not set. - if (config.MergeMode) - { - if (!this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode specified for {0} but not found in loadedFontInfo. Skipping.", name); - continue; - } - } - else - { - if (this.loadedFontInfo.ContainsKey(config.DstFont.NativePtr)) - { - Log.Warning("MergeMode not specified for {0} but found in loadedFontInfo. Skipping.", name); - continue; - } - - // While the font will be loaded in the scaled size after FontScale is applied, the font will be treated as having the requested size when used from plugins. - this.loadedFontInfo[config.DstFont.NativePtr] = new($"PlReq({name})", config.SizePixels); - } - - config.SizePixels = config.SizePixels * io.FontGlobalScale; - } - - for (int i = 0, i_ = ioFonts.ConfigData.Size; i < i_; i++) - { - var config = ioFonts.ConfigData[i]; - config.RasterizerGamma *= fontGamma; - } - - Log.Verbose("[FONT] ImGui.IO.Build will be called."); - ioFonts.Build(); - gameFontManager.AfterIoFontsBuild(); - this.ClearStacks(); - Log.Verbose("[FONT] ImGui.IO.Build OK!"); - - gameFontManager.AfterBuildFonts(); - - foreach (var (font, mod) in this.loadedFontInfo) - { - // I have no idea what's causing NPE, so just to be safe - try - { - if (font.NativePtr != null && font.NativePtr->ConfigData != null) - { - var nameBytes = Encoding.UTF8.GetBytes($"{mod.Name}\0"); - Marshal.Copy(nameBytes, 0, (IntPtr)font.ConfigData.Name.Data, Math.Min(nameBytes.Length, font.ConfigData.Name.Count)); - } - } - catch (NullReferenceException) - { - // do nothing - } - - Log.Verbose("[FONT] {0}: Unscale with scale value of {1}", mod.Name, mod.Scale); - GameFontManager.UnscaleFont(font, mod.Scale, false); - - if (mod.Axis == TargetFontModification.AxisMode.Overwrite) - { - Log.Verbose("[FONT] {0}: Overwrite from AXIS of size {1}px (was {2}px)", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - GameFontManager.UnscaleFont(font, font.FontSize / mod.SourceAxis.ImFont.FontSize, false); - var ascentDiff = mod.SourceAxis.ImFont.Ascent - font.Ascent; - font.Ascent += ascentDiff; - font.Descent = ascentDiff; - font.FallbackChar = mod.SourceAxis.ImFont.FallbackChar; - font.EllipsisChar = mod.SourceAxis.ImFont.EllipsisChar; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, false, false); - } - else if (mod.Axis == TargetFontModification.AxisMode.GameGlyphsOnly) - { - Log.Verbose("[FONT] {0}: Overwrite game specific glyphs from AXIS of size {1}px", mod.Name, mod.SourceAxis.ImFont.FontSize, font.FontSize); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize -= 1; - ImGuiHelpers.CopyGlyphsAcrossFonts(mod.SourceAxis.ImFont, font, true, false, 0xE020, 0xE0DB); - if (!this.UseAxis && font.NativePtr == DefaultFont.NativePtr) - mod.SourceAxis.ImFont.FontSize += 1; - } - - Log.Verbose("[FONT] {0}: Resize from {1}px to {2}px", mod.Name, font.FontSize, mod.TargetSizePx); - GameFontManager.UnscaleFont(font, font.FontSize / mod.TargetSizePx, false); - } - - // Fill missing glyphs in MonoFont from DefaultFont - ImGuiHelpers.CopyGlyphsAcrossFonts(DefaultFont, MonoFont, true, false); - - for (int i = 0, i_ = ioFonts.Fonts.Size; i < i_; i++) - { - var font = ioFonts.Fonts[i]; - if (font.Glyphs.Size == 0) - { - Log.Warning("[FONT] Font has no glyph: {0}", font.GetDebugName()); - continue; - } - - if (font.FindGlyphNoFallback(Fallback1Codepoint).NativePtr != null) - font.FallbackChar = Fallback1Codepoint; - - font.BuildLookupTableNonstandard(); - } - - Log.Verbose("[FONT] Invoke OnAfterBuildFonts"); - this.AfterBuildFonts?.InvokeSafely(); - Log.Verbose("[FONT] OnAfterBuildFonts OK!"); - - if (ioFonts.Fonts[0].NativePtr != DefaultFont.NativePtr) - Log.Warning("[FONT] First font is not DefaultFont"); - - Log.Verbose("[FONT] Fonts built!"); - - this.fontBuildSignal.Set(); - - this.FontsReady = true; + if (Service.Get().WindowIsImmersive) + this.SetImmersiveMode(true); } - finally + catch (Exception ex) { - if (fontConfig.NativePtr != null) - fontConfig.Destroy(); - - foreach (var garbage in garbageList) - garbage.Free(); + Log.Error(ex, "Could not enable immersive mode"); } - } - [ServiceManager.CallWhenServicesReady( - "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] - private void ContinueConstruction(TargetSigScanner sigScanner, DalamudConfiguration configuration) - { - this.address.Setup(sigScanner); - this.framework.RunOnFrameworkThread(() => - { - while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) - { - _ = User32.GetWindowThreadProcessId(this.GameWindowHandle, out var pid); + this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); + this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - if (pid == Environment.ProcessId && User32.IsWindowVisible(this.GameWindowHandle)) - break; - } + Log.Verbose("===== S W A P C H A I N ====="); + Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); + Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - try - { - if (configuration.WindowIsImmersive) - this.SetImmersiveMode(true); - } - catch (Exception ex) - { - Log.Error(ex, "Could not enable immersive mode"); - } - - this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); - - Log.Verbose("===== S W A P C H A I N ====="); - Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); - Log.Verbose($"ResizeBuffers address 0x{this.resizeBuffersHook!.Address.ToInt64():X}"); - - this.setCursorHook.Enable(); - this.presentHook.Enable(); - this.resizeBuffersHook.Enable(); - }); - } - - // This is intended to only be called as a handler attached to scene.OnNewRenderFrame - private void RebuildFontsInternal() - { - Log.Verbose("[FONT] RebuildFontsInternal() called"); - this.SetupFonts(); - - Log.Verbose("[FONT] RebuildFontsInternal() detaching"); - this.scene!.OnNewRenderFrame -= this.RebuildFontsInternal; - - Log.Verbose("[FONT] Calling InvalidateFonts"); - this.scene.InvalidateFonts(); - - Log.Verbose("[FONT] Font Rebuild OK!"); - - this.isRebuildingFonts = false; + this.setCursorHook.Enable(); + this.presentHook.Enable(); + this.resizeBuffersHook.Enable(); } private IntPtr ResizeBuffersDetour(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags) @@ -1206,14 +777,17 @@ internal class InterfaceManager : IDisposable, IServiceType private IntPtr SetCursorDetour(IntPtr hCursor) { - if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) + if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); + return this.setCursorHook.IsDisposed + ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() + : this.setCursorHook.Original(hCursor); } private void OnNewInputFrame() { + var io = ImGui.GetIO(); var dalamudInterface = Service.GetNullable(); var gamepadState = Service.GetNullable(); var keyState = Service.GetNullable(); @@ -1221,18 +795,21 @@ internal class InterfaceManager : IDisposable, IServiceType if (dalamudInterface == null || gamepadState == null || keyState == null) return; + // Prevent setting the footgun from ImGui Demo; the Space key isn't removing the flag at the moment. + io.ConfigFlags &= ~ImGuiConfigFlags.NoMouse; + // fix for keys in game getting stuck, if you were holding a game key (like run) // and then clicked on an imgui textbox - imgui would swallow the keyup event, // so the game would think the key remained pressed continuously until you left // imgui and pressed and released the key again - if (ImGui.GetIO().WantTextInput) + if (io.WantTextInput) { keyState.ClearAll(); } // TODO: mouse state? - var gamepadEnabled = (ImGui.GetIO().BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; + var gamepadEnabled = (io.BackendFlags & ImGuiBackendFlags.HasGamepad) > 0; // NOTE (Chiv) Activate ImGui navigation via L1+L3 press // (mimicking how mouse navigation is activated via L1+R3 press in game). @@ -1240,12 +817,12 @@ internal class InterfaceManager : IDisposable, IServiceType && gamepadState.Raw(GamepadButtons.L1) > 0 && gamepadState.Pressed(GamepadButtons.L3) > 0) { - ImGui.GetIO().ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; + io.ConfigFlags ^= ImGuiConfigFlags.NavEnableGamepad; gamepadState.NavEnableGamepad ^= true; dalamudInterface.ToggleGamepadModeNotifierWindow(); } - if (gamepadEnabled && (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) + if (gamepadEnabled && (io.ConfigFlags & ImGuiConfigFlags.NavEnableGamepad) > 0) { var northButton = gamepadState.Raw(GamepadButtons.North) != 0; var eastButton = gamepadState.Raw(GamepadButtons.East) != 0; @@ -1264,7 +841,6 @@ internal class InterfaceManager : IDisposable, IServiceType var r1Button = gamepadState.Raw(GamepadButtons.R1) != 0; var r2Button = gamepadState.Raw(GamepadButtons.R2) != 0; - var io = ImGui.GetIO(); io.AddKeyEvent(ImGuiKey.GamepadFaceUp, northButton); io.AddKeyEvent(ImGuiKey.GamepadFaceRight, eastButton); io.AddKeyEvent(ImGuiKey.GamepadFaceDown, southButton); @@ -1312,7 +888,10 @@ internal class InterfaceManager : IDisposable, IServiceType var snap = ImGuiManagedAsserts.GetSnapshot(); if (this.IsDispatchingEvents) - this.Draw?.Invoke(); + { + using (this.defaultFontHandle?.Push()) + this.Draw?.Invoke(); + } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); @@ -1339,123 +918,4 @@ internal class InterfaceManager : IDisposable, IServiceType /// public InterfaceManager Manager { get; init; } } - - /// - /// Represents a glyph request. - /// - public class SpecialGlyphRequest : IDisposable - { - /// - /// Initializes a new instance of the class. - /// - /// InterfaceManager to associate. - /// Font size in pixels. - /// Codepoint ranges. - internal SpecialGlyphRequest(InterfaceManager manager, float size, List> ranges) - { - this.Manager = manager; - this.Size = size; - this.CodepointRanges = ranges; - this.Manager.glyphRequests.Add(this); - } - - /// - /// Gets the font of specified size, or DefaultFont if it's not ready yet. - /// - public ImFontPtr Font - { - get - { - unsafe - { - return this.FontInternal.NativePtr == null ? DefaultFont : this.FontInternal; - } - } - } - - /// - /// Gets or sets the associated ImFont. - /// - internal ImFontPtr FontInternal { get; set; } - - /// - /// Gets associated InterfaceManager. - /// - internal InterfaceManager Manager { get; init; } - - /// - /// Gets font size. - /// - internal float Size { get; init; } - - /// - /// Gets codepoint ranges. - /// - internal List> CodepointRanges { get; init; } - - /// - public void Dispose() - { - this.Manager.glyphRequests.Remove(this); - } - } - - private unsafe class TargetFontModification : IDisposable - { - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information, assuming that AXIS fonts will not be applied. - /// - /// Name of the font to write to ImGui font information. - /// Target font size in pixels, which will not be considered for further scaling. - internal TargetFontModification(string name, float sizePx) - { - this.Name = name; - this.Axis = AxisMode.Suppress; - this.TargetSizePx = sizePx; - this.Scale = 1; - this.SourceAxis = null; - } - - /// - /// Initializes a new instance of the class. - /// Constructs new target font modification information. - /// - /// Name of the font to write to ImGui font information. - /// Whether and how to use AXIS fonts. - /// Target font size in pixels, which will not be considered for further scaling. - /// Font scale to be referred for loading AXIS font of appropriate size. - internal TargetFontModification(string name, AxisMode axis, float sizePx, float globalFontScale) - { - this.Name = name; - this.Axis = axis; - this.TargetSizePx = sizePx; - this.Scale = globalFontScale; - this.SourceAxis = Service.Get().NewFontRef(new(GameFontFamily.Axis, this.TargetSizePx * this.Scale)); - } - - internal enum AxisMode - { - Suppress, - GameGlyphsOnly, - Overwrite, - } - - internal string Name { get; private init; } - - internal AxisMode Axis { get; private init; } - - internal float TargetSizePx { get; private init; } - - internal float Scale { get; private init; } - - internal GameFontHandle? SourceAxis { get; private init; } - - internal bool SourceAxisAvailable => this.SourceAxis != null && this.SourceAxis.ImFont.NativePtr != null; - - public void Dispose() - { - this.SourceAxis?.Dispose(); - } - } } diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index b9e7ab686..ae59db36a 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -1,4 +1,3 @@ -using System.IO; using System.Linq; using System.Numerics; @@ -7,6 +6,8 @@ using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; @@ -31,8 +32,14 @@ internal sealed class ChangelogWindow : Window, IDisposable • Plugins can now add tooltips and interaction to the server info bar • The Dalamud/plugin installer UI has been refreshed "; - + private readonly TitleScreenMenuWindow tsmWindow; + + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy bannerFont; + private readonly Lazy apiBumpExplainerTexture; + private readonly Lazy logoTexture; private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) { @@ -46,27 +53,36 @@ internal sealed class ChangelogWindow : Window, IDisposable Point2 = Vector2.One, }; - private IDalamudTextureWrap? apiBumpExplainerTexture; - private IDalamudTextureWrap? logoTexture; - private GameFontHandle? bannerFont; - private State state = State.WindowFadeIn; private bool needFadeRestart = false; - + /// /// Initializes a new instance of the class. /// /// TSM window. - public ChangelogWindow(TitleScreenMenuWindow tsmWindow) + /// An instance of . + /// An instance of . + public ChangelogWindow( + TitleScreenMenuWindow tsmWindow, + FontAtlasFactory fontAtlasFactory, + DalamudAssetManager assets) : base("What's new in Dalamud?##ChangelogWindow", ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse, true) { this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; + this.privateAtlas = this.scopedFinalizer.Add( + fontAtlasFactory.CreateFontAtlas(this.Namespace, FontAtlasAutoRebuildMode.Async)); + this.bannerFont = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.MiedingerMid18)))); + + this.apiBumpExplainerTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.ChangelogApiBumpIcon)); + this.logoTexture = new(() => assets.GetDalamudTextureWrap(DalamudAsset.Logo)); // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); + _ = this.bannerFont.Value; } private enum State @@ -97,20 +113,12 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - this.MakeFont(Service.Get()); + _ = this.bannerFont; this.state = State.WindowFadeIn; this.windowFade.Reset(); this.bodyFade.Reset(); this.needFadeRestart = true; - - if (this.apiBumpExplainerTexture == null) - { - var dalamud = Service.Get(); - var tm = Service.Get(); - this.apiBumpExplainerTexture = tm.GetTextureFromFile(new FileInfo(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "changelogApiBump.png"))) - ?? throw new Exception("Could not load api bump explainer."); - } base.OnOpen(); } @@ -186,10 +194,7 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.SetCursorPos(new Vector2(logoContainerSize.X / 2 - logoSize.X / 2, logoContainerSize.Y / 2 - logoSize.Y / 2)); using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) - { - this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); - ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); - } + ImGui.Image(this.logoTexture.Value.ImGuiHandle, logoSize); } ImGui.SameLine(); @@ -205,7 +210,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 1f, 0f, 1f))) { - using var font = ImRaii.PushFont(this.bannerFont!.ImFont); + using var font = this.bannerFont.Value.Push(); switch (this.state) { @@ -275,9 +280,11 @@ internal sealed class ChangelogWindow : Window, IDisposable ImGui.TextWrapped("If some plugins are displayed with a red cross in the 'Installed Plugins' tab, they may not yet be available."); ImGuiHelpers.ScaledDummy(15); - - ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture!.Width); - ImGui.Image(this.apiBumpExplainerTexture.ImGuiHandle, this.apiBumpExplainerTexture.Size); + + ImGuiHelpers.CenterCursorFor(this.apiBumpExplainerTexture.Value.Width); + ImGui.Image( + this.apiBumpExplainerTexture.Value.ImGuiHandle, + this.apiBumpExplainerTexture.Value.Size); DrawNextButton(State.Links); break; @@ -377,7 +384,4 @@ internal sealed class ChangelogWindow : Window, IDisposable public void Dispose() { } - - private void MakeFont(GameFontManager gfm) => - this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index 20c3d6d01..951d3d91c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -6,6 +6,8 @@ using Dalamud.Interface.Components; using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; +using Dalamud.Utility; + using ImGuiNET; using Serilog; @@ -14,7 +16,7 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class responsible for drawing the data/debug window. /// -internal class DataWindow : Window +internal class DataWindow : Window, IDisposable { private readonly IDataWindowWidget[] modules = { @@ -34,6 +36,7 @@ internal class DataWindow : Window new FlyTextWidget(), new FontAwesomeTestWidget(), new GameInventoryTestWidget(), + new GamePrebakedFontsTestWidget(), new GamepadWidget(), new GaugeWidget(), new HookWidget(), @@ -76,6 +79,9 @@ internal class DataWindow : Window this.Load(); } + /// + public void Dispose() => this.modules.OfType().AggregateToDisposable().Dispose(); + /// public override void OnOpen() { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs new file mode 100644 index 000000000..dba293e8b --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -0,0 +1,213 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.Data.Widgets; + +/// +/// Widget for testing game prebaked fonts. +/// +internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable +{ + private ImVectorWrapper testStringBuffer; + private IFontAtlas? privateAtlas; + private IReadOnlyDictionary Handle)[]>? fontHandles; + private bool useGlobalScale; + private bool useWordWrap; + private bool useItalic; + private bool useBold; + private bool useMinimumBuild; + + /// + public string[]? CommandShortcuts { get; init; } + + /// + public string DisplayName { get; init; } = "Game Prebaked Fonts"; + + /// + public bool Ready { get; set; } + + /// + public void Load() => this.Ready = true; + + /// + public unsafe void Draw() + { + ImGui.AlignTextToFramePadding(); + fixed (byte* labelPtr = "Global Scale"u8) + { + var v = (byte)(this.useGlobalScale ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useGlobalScale = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Word Wrap"u8) + { + var v = (byte)(this.useWordWrap ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + this.useWordWrap = v != 0; + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Italic"u8) + { + var v = (byte)(this.useItalic ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useItalic = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Bold"u8) + { + var v = (byte)(this.useBold ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useBold = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + fixed (byte* labelPtr = "Minimum Range"u8) + { + var v = (byte)(this.useMinimumBuild ? 1 : 0); + if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) + { + this.useMinimumBuild = v != 0; + this.ClearAtlas(); + } + } + + ImGui.SameLine(); + if (ImGui.Button("Reset Text") || this.testStringBuffer.IsDisposed) + { + this.testStringBuffer.Dispose(); + this.testStringBuffer = ImVectorWrapper.CreateFromSpan( + "(Game)-[Font] {Test}. 0123456789!! <氣気气きキ기>。"u8, + minCapacity: 1024); + } + + fixed (byte* labelPtr = "Test Input"u8) + { + if (ImGuiNative.igInputTextMultiline( + labelPtr, + this.testStringBuffer.Data, + (uint)this.testStringBuffer.Capacity, + new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale), + 0, + null, + null) != 0) + { + var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); + if (len + 4 >= this.testStringBuffer.Capacity) + this.testStringBuffer.EnsureCapacityExponential(len + 4); + if (len < this.testStringBuffer.Capacity) + { + this.testStringBuffer.LengthUnsafe = len; + this.testStringBuffer.StorageSpan[len] = default; + } + + if (this.useMinimumBuild) + _ = this.privateAtlas?.BuildFontsAsync(); + } + } + + this.privateAtlas ??= + Service.Get().CreateFontAtlas( + nameof(GamePrebakedFontsTestWidget), + FontAtlasAutoRebuildMode.Async, + this.useGlobalScale); + this.fontHandles ??= + Enum.GetValues() + .Where(x => x.GetAttribute() is not null) + .Select(x => new GameFontStyle(x) { Italic = this.useItalic, Bold = this.useBold }) + .GroupBy(x => x.Family) + .ToImmutableDictionary( + x => x.Key, + x => x.Select( + y => (y, new Lazy( + () => this.useMinimumBuild + ? this.privateAtlas.NewDelegateFontHandle( + e => + e.OnPreBuild( + tk => tk.AddGameGlyphs( + y, + Encoding.UTF8.GetString( + this.testStringBuffer.DataSpan).ToGlyphRange(), + default))) + : this.privateAtlas.NewGameFontHandle(y)))) + .ToArray()); + + var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); + foreach (var (family, items) in this.fontHandles) + { + if (!ImGui.CollapsingHeader($"{family} Family")) + continue; + + foreach (var (gfs, handle) in items) + { + ImGui.TextUnformatted($"{gfs.SizePt}pt"); + ImGui.SameLine(offsetX); + ImGuiNative.igPushTextWrapPos(this.useWordWrap ? 0f : -1f); + try + { + if (handle.Value.LoadException is { } exc) + { + ImGui.TextUnformatted(exc.ToString()); + } + else if (!handle.Value.Available) + { + fixed (byte* labelPtr = "Loading..."u8) + ImGuiNative.igTextUnformatted(labelPtr, labelPtr + 8 + ((Environment.TickCount / 200) % 3)); + } + else + { + if (!this.useGlobalScale) + ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); + using var pushPop = handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + } + } + finally + { + ImGuiNative.igPopTextWrapPos(); + ImGuiNative.igSetWindowFontScale(1); + } + } + } + } + + /// + public void Dispose() + { + this.ClearAtlas(); + this.testStringBuffer.Dispose(); + } + + private void ClearAtlas() + { + this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value)) + .AggregateToDisposable().Dispose(); + this.fontHandles = null; + this.privateAtlas?.Dispose(); + this.privateAtlas = null; + } +} diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 7d4489f8d..027e1a571 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -5,10 +5,10 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.Settings.Tabs; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Internal; using Dalamud.Utility; using ImGuiNET; @@ -19,14 +19,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings; ///
internal class SettingsWindow : Window { - private readonly SettingsTab[] tabs = - { - new SettingsTabGeneral(), - new SettingsTabLook(), - new SettingsTabDtr(), - new SettingsTabExperimental(), - new SettingsTabAbout(), - }; + private SettingsTab[]? tabs; private string searchInput = string.Empty; @@ -49,6 +42,15 @@ internal class SettingsWindow : Window /// public override void OnOpen() { + this.tabs ??= new SettingsTab[] + { + new SettingsTabGeneral(), + new SettingsTabLook(), + new SettingsTabDtr(), + new SettingsTabExperimental(), + new SettingsTabAbout(), + }; + foreach (var settingsTab in this.tabs) { settingsTab.Load(); @@ -64,15 +66,12 @@ internal class SettingsWindow : Window { var configuration = Service.Get(); var interfaceManager = Service.Get(); + var fontAtlasFactory = Service.Get(); - var rebuildFont = - ImGui.GetIO().FontGlobalScale != configuration.GlobalUiScale || - interfaceManager.FontGamma != configuration.FontGammaLevel || - interfaceManager.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - interfaceManager.FontGammaOverride = null; - interfaceManager.UseAxisOverride = null; + fontAtlasFactory.UseAxisOverride = null; if (rebuildFont) interfaceManager.RebuildFonts(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 5b6f6b02f..8714fd666 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -1,13 +1,13 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Linq; using System.Numerics; using CheapLoc; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; @@ -15,7 +15,6 @@ using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; @@ -173,16 +172,21 @@ Contribute at: https://github.com/goatcorp/Dalamud "; private readonly Stopwatch creditsThrottler; + private readonly IFontAtlas privateAtlas; private string creditsText; private bool resetNow = false; private IDalamudTextureWrap? logoTexture; - private GameFontHandle? thankYouFont; + private IFontHandle? thankYouFont; public SettingsTabAbout() { this.creditsThrottler = new(); + + this.privateAtlas = Service + .Get() + .CreateFontAtlas(nameof(SettingsTabAbout), FontAtlasAutoRebuildMode.Async); } public override SettingsEntry[] Entries { get; } = { }; @@ -207,11 +211,7 @@ Contribute at: https://github.com/goatcorp/Dalamud this.creditsThrottler.Restart(); - if (this.thankYouFont == null) - { - var gfm = Service.Get(); - this.thankYouFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.TrumpGothic34)); - } + this.thankYouFont ??= this.privateAtlas.NewGameFontHandle(new(GameFontFamilyAndSize.TrumpGothic34)); this.resetNow = true; @@ -269,14 +269,12 @@ Contribute at: https://github.com/goatcorp/Dalamud if (this.thankYouFont != null) { - ImGui.PushFont(this.thankYouFont.ImFont); + using var fontPush = this.thankYouFont.Push(); var thankYouLenX = ImGui.CalcTextSize(ThankYouText).X; ImGui.Dummy(new Vector2((windowX / 2) - (thankYouLenX / 2), 0f)); ImGui.SameLine(); ImGui.TextUnformatted(ThankYouText); - - ImGui.PopFont(); } ImGuiHelpers.ScaledDummy(0, windowSize.Y + 50f); @@ -305,9 +303,5 @@ Contribute at: https://github.com/goatcorp/Dalamud /// /// Disposes of managed and unmanaged resources. /// - public override void Dispose() - { - this.logoTexture?.Dispose(); - this.thankYouFont?.Dispose(); - } + public override void Dispose() => this.privateAtlas.Dispose(); } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 02e8ce789..5293e13c4 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,12 +1,14 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using System.Text; using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -28,7 +30,6 @@ public class SettingsTabLook : SettingsTab }; private float globalUiScale; - private float fontGamma; public override SettingsEntry[] Entries { get; } = { @@ -41,9 +42,8 @@ public class SettingsTabLook : SettingsTab (v, c) => c.UseAxisFontsFromGame = v, v => { - var im = Service.Get(); - im.UseAxisOverride = v; - im.RebuildFonts(); + Service.Get().UseAxisOverride = v; + Service.Get().RebuildFonts(); }), new GapSettingsEntry(5, true), @@ -145,6 +145,7 @@ public class SettingsTabLook : SettingsTab public override void Draw() { var interfaceManager = Service.Get(); + var fontBuildTask = interfaceManager.FontBuildTask; ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); @@ -164,6 +165,19 @@ public class SettingsTabLook : SettingsTab } } + if (!fontBuildTask.IsCompleted) + { + ImGui.SameLine(); + var buildingFonts = Loc.Localize("DalamudSettingsFontBuildInProgressWithEndingThreeDots", "Building fonts..."); + unsafe + { + var len = Encoding.UTF8.GetByteCount(buildingFonts); + var p = stackalloc byte[len]; + Encoding.UTF8.GetBytes(buildingFonts, new(p, len)); + ImGuiNative.igTextUnformatted(p, (p + len + ((Environment.TickCount / 200) % 3)) - 2); + } + } + var globalUiScaleInPt = 12f * this.globalUiScale; if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp)) { @@ -174,33 +188,25 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsGlobalUiScaleHint", "Scale text in all XIVLauncher UI elements - this is useful for 4K displays.")); - ImGuiHelpers.ScaledDummy(5); - - ImGui.AlignTextToFramePadding(); - ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); - ImGui.SameLine(); - if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) + if (fontBuildTask.IsFaulted || fontBuildTask.IsCanceled) { - this.fontGamma = 1.4f; - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); + ImGui.TextColored( + ImGuiColors.DalamudRed, + Loc.Localize("DalamudSettingsFontBuildFaulted", "Failed to load fonts as requested.")); + if (fontBuildTask.Exception is not null + && ImGui.CollapsingHeader("##DalamudSetingsFontBuildFaultReason")) + { + foreach (var e in fontBuildTask.Exception.InnerExceptions) + ImGui.TextUnformatted(e.ToString()); + } } - if (ImGui.DragFloat("##DalamudSettingsFontGammaDrag", ref this.fontGamma, 0.005f, 0.3f, 3f, "%.2f", ImGuiSliderFlags.AlwaysClamp)) - { - interfaceManager.FontGammaOverride = this.fontGamma; - interfaceManager.RebuildFonts(); - } - - ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsFontGammaHint", "Changes the thickness of text.")); - base.Draw(); } public override void Load() { this.globalUiScale = Service.Get().GlobalUiScale; - this.fontGamma = Service.Get().FontGammaLevel; base.Load(); } diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 42bca89ff..9c385a99c 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -7,11 +7,14 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.Gui; using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; +using Dalamud.Utility; using ImGuiNET; @@ -27,16 +30,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly ClientState clientState; private readonly DalamudConfiguration configuration; - private readonly Framework framework; private readonly GameGui gameGui; private readonly TitleScreenMenu titleScreenMenu; + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly IFontAtlas privateAtlas; + private readonly Lazy myFontHandle; private readonly Lazy shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); private readonly Dictionary logoEasings = new(); - private readonly Dictionary specialGlyphRequests = new(); private InOutCubic? fadeOutEasing; @@ -48,6 +52,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// An instance of . /// An instance of . /// An instance of . + /// An instance of . /// An instance of . /// An instance of . /// An instance of . @@ -55,6 +60,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable ClientState clientState, DalamudConfiguration configuration, DalamudAssetManager dalamudAssetManager, + FontAtlasFactory fontAtlasFactory, Framework framework, GameGui gameGui, TitleScreenMenu titleScreenMenu) @@ -65,7 +71,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable { this.clientState = clientState; this.configuration = configuration; - this.framework = framework; this.gameGui = gameGui; this.titleScreenMenu = titleScreenMenu; @@ -77,9 +82,25 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); + this.privateAtlas = fontAtlasFactory.CreateFontAtlas(this.WindowName, FontAtlasAutoRebuildMode.Async); + this.scopedFinalizer.Add(this.privateAtlas); + + this.myFontHandle = new( + () => this.scopedFinalizer.Add( + this.privateAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + toolkit => toolkit.AddDalamudDefaultFont( + TargetFontSizePx, + titleScreenMenu.Entries.SelectMany(x => x.Name).ToGlyphRange()))))); + + titleScreenMenu.EntryListChange += this.TitleScreenMenuEntryListChange; + this.scopedFinalizer.Add(() => titleScreenMenu.EntryListChange -= this.TitleScreenMenuEntryListChange); + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); framework.Update += this.FrameworkOnUpdate; + this.scopedFinalizer.Add(() => framework.Update -= this.FrameworkOnUpdate); } private enum State @@ -94,6 +115,9 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public bool AllowDrawing { get; set; } = true; + /// + public void Dispose() => this.scopedFinalizer.Dispose(); + /// public override void PreDraw() { @@ -109,12 +133,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable base.PostDraw(); } - /// - public void Dispose() - { - this.framework.Update -= this.FrameworkOnUpdate; - } - /// public override void Draw() { @@ -246,33 +264,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable break; } } - - var srcText = entries.Select(e => e.Name).ToHashSet(); - var keys = this.specialGlyphRequests.Keys.ToHashSet(); - keys.RemoveWhere(x => srcText.Contains(x)); - foreach (var key in keys) - { - this.specialGlyphRequests[key].Dispose(); - this.specialGlyphRequests.Remove(key); - } } private bool DrawEntry( TitleScreenMenuEntry entry, bool inhibitFadeout, bool showText, bool isFirst, bool overrideAlpha, bool interactable) { - InterfaceManager.SpecialGlyphRequest fontHandle; - if (this.specialGlyphRequests.TryGetValue(entry.Name, out fontHandle) && fontHandle.Size != TargetFontSizePx) - { - fontHandle.Dispose(); - this.specialGlyphRequests.Remove(entry.Name); - fontHandle = null; - } - - if (fontHandle == null) - this.specialGlyphRequests[entry.Name] = fontHandle = Service.Get().NewFontSizeRef(TargetFontSizePx, entry.Name); - - ImGui.PushFont(fontHandle.Font); - ImGui.SetWindowFontScale(TargetFontSizePx / fontHandle.Size); + using var fontScopeDispose = this.myFontHandle.Value.Push(); var scale = ImGui.GetIO().FontGlobalScale; @@ -383,8 +380,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable initialCursor.Y += entry.Texture.Height * scale; ImGui.SetCursorPos(initialCursor); - ImGui.PopFont(); - return isHover; } @@ -401,4 +396,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } + + private void TitleScreenMenuEntryListChange() => this.privateAtlas.BuildFontsAsync(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs new file mode 100644 index 000000000..50e591390 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasAutoRebuildMode.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// How to rebuild . +/// +public enum FontAtlasAutoRebuildMode +{ + /// + /// Do not rebuild. + /// + Disable, + + /// + /// Rebuild on new frame. + /// + OnNewFrame, + + /// + /// Rebuild asynchronously. + /// + Async, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs new file mode 100644 index 000000000..345ab729d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs @@ -0,0 +1,38 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Build step for . +/// +public enum FontAtlasBuildStep +{ + /// + /// An invalid value. This should never be passed through event callbacks. + /// + Invalid, + + /// + /// Called before calling .
+ /// Expect to be passed. + ///
+ PreBuild, + + /// + /// Called after calling .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostBuild, + + /// + /// Called after promoting staging font atlas to the actual atlas for .
+ /// Expect to be passed.
+ ///
+ /// This callback is not guaranteed to happen after , + /// but it will never happen on its own. + ///
+ PostPromotion, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs new file mode 100644 index 000000000..4f5b34061 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs @@ -0,0 +1,15 @@ +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Delegate to be called when a font needs to be built. +/// +/// A toolkit that may help you for font building steps. +/// +/// An implementation of may implement all of +/// , , and +/// .
+/// Either use to identify the build step, or use +/// , , +/// and for routing. +///
+public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs new file mode 100644 index 000000000..586887a3b --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -0,0 +1,133 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Convenience function for building fonts through . +/// +public static class FontAtlasBuildToolkitUtilities +{ + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this IEnumerable enumerable, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in enumerable) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given s into an array of containing ImGui glyph ranges. + /// + /// The chars. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this ReadOnlySpan span, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + using var builderScoped = ImGuiHelpers.NewFontGlyphRangeBuilderPtrScoped(out var builder); + foreach (var c in span) + builder.AddChar(c); + return builder.BuildRangesToArray(addFallbackCodepoints, addEllipsisCodepoints); + } + + /// + /// Compiles given string into an array of containing ImGui glyph ranges. + /// + /// The string. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// The compiled range. + public static ushort[] ToGlyphRange( + this string @string, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) => + @string.AsSpan().ToGlyphRange(addFallbackCodepoints, addEllipsisCodepoints); + + /// + /// Finds the corresponding in + /// . that corresponds to the + /// specified font . + /// + /// The toolkit. + /// The font. + /// The relevant config pointer, or empty config pointer if not found. + public static unsafe ImFontConfigPtr FindConfigPtr(this IFontAtlasBuildToolkit toolkit, ImFontPtr fontPtr) + { + foreach (ref var c in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan) + { + if (c.DstFont == fontPtr.NativePtr) + return new((nint)Unsafe.AsPointer(ref c)); + } + + return default; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// This, for method chaining. + public static IFontAtlasBuildToolkit OnPreBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PreBuild) + action.Invoke((IFontAtlasBuildToolkitPreBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostBuild( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostBuild) + action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit); + return toolkit; + } + + /// + /// Invokes + /// if of + /// is . + /// + /// The toolkit. + /// The action. + /// toolkit, for method chaining. + public static IFontAtlasBuildToolkit OnPostPromotion( + this IFontAtlasBuildToolkit toolkit, + Action action) + { + if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion) + action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit); + return toolkit; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs new file mode 100644 index 000000000..ec3e66e9a --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -0,0 +1,141 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Wrapper for . +/// +public interface IFontAtlas : IDisposable +{ + /// + /// Event to be called on build step changes.
+ /// is meaningless for this event. + ///
+ event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + /// Event fired when a font rebuild operation is recommended.
+ /// This event will be invoked from the main thread.
+ ///
+ /// Reasons for the event include changes in and + /// initialization of new associated font handles. + ///
+ /// + /// You should call or + /// if is not set to true.
+ /// Avoid calling here; it will block the main thread. + ///
+ event Action? RebuildRecommend; + + /// + /// Gets the name of the atlas. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets a value how the atlas should be rebuilt when the relevant Dalamud Configuration changes. + /// + FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + /// Gets the font atlas. Might be empty. + /// + ImFontAtlasPtr ImAtlas { get; } + + /// + /// Gets the task that represents the current font rebuild state. + /// + Task BuildTask { get; } + + /// + /// Gets a value indicating whether there exists any built atlas, regardless of . + /// + bool HasBuiltAtlas { get; } + + /// + /// Gets a value indicating whether this font atlas is under the effect of global scale. + /// + bool IsGlobalScaled { get; } + + /// + /// Suppresses automatically rebuilding fonts for the scope. + /// + /// An instance of that will release the suppression. + /// + /// Use when you will be creating multiple new handles, and want rebuild to trigger only when you're done doing so. + /// This function will effectively do nothing, if is set to + /// . + /// + /// + /// + /// using (atlas.SuppressBuild()) { + /// this.font1 = atlas.NewGameFontHandle(...); + /// this.font2 = atlas.NewDelegateFontHandle(...); + /// } + /// + /// + public IDisposable SuppressAutoRebuild(); + + /// + /// Creates a new from game's built-in fonts. + /// + /// Font to use. + /// Handle to a font that may or may not be ready yet. + public IFontHandle NewGameFontHandle(GameFontStyle style); + + /// + /// Creates a new IFontHandle using your own callbacks. + /// + /// Callback for . + /// Handle to a font that may or may not be ready yet. + /// + /// On initialization: + /// + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => { + /// var config = new SafeFontConfig { SizePx = 16 }; + /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); + /// tk.AddGameSymbol(config); + /// tk.AddExtraGlyphsForDalamudLanguage(config); + /// // optionally do the following if you have to add more than one font here, + /// // to specify which font added during this delegate is the final font to use. + /// tk.Font = config.MergeFont; + /// })); + /// // or + /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(36))); + /// + ///
+ /// On use: + /// + /// using (this.fontHandle.Push()) + /// ImGui.TextUnformatted("Example"); + /// + ///
+ public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate); + + /// + /// Queues rebuilding fonts, on the main thread.
+ /// Note that would not necessarily get changed from calling this function. + ///
+ /// If is . + void BuildFontsOnNextFrame(); + + /// + /// Rebuilds fonts immediately, on the current thread.
+ /// Even the callback for will be called on the same thread. + ///
+ /// If is . + void BuildFontsImmediately(); + + /// + /// Rebuilds fonts asynchronously, on any thread. + /// + /// Call on the main thread. + /// The task. + /// If is . + Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs new file mode 100644 index 000000000..4b016bbb2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -0,0 +1,67 @@ +using System.Runtime.InteropServices; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Common stuff for and . +/// +public interface IFontAtlasBuildToolkit +{ + /// + /// Gets or sets the font relevant to the call. + /// + ImFontPtr Font { get; set; } + + /// + /// Gets the current scale this font atlas is being built with. + /// + float Scale { get; } + + /// + /// Gets a value indicating whether the current build operation is asynchronous. + /// + bool IsAsyncBuildOperation { get; } + + /// + /// Gets the current build step. + /// + FontAtlasBuildStep BuildStep { get; } + + /// + /// Gets the font atlas being built. + /// + ImFontAtlasPtr NewImAtlas { get; } + + /// + /// Gets the wrapper for of .
+ /// This does not need to be disposed. Calling does nothing.- + ///
+ /// Modification of this vector may result in undefined behaviors. + ///
+ ImVectorWrapper Fonts { get; } + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeWithAtlas(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeWithAtlas(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the native atlas gets disposed, successful or not. + /// + /// The action to run on dispose. + void DisposeWithAtlas(Action action); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs new file mode 100644 index 000000000..3c14197e0 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -0,0 +1,26 @@ +using Dalamud.Interface.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit +{ + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + + /// + /// Stores a texture to be managed with the atlas. + /// + /// The texture wrap. + /// Dispose the wrap on error. + /// The texture index. + int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs new file mode 100644 index 000000000..8c3c91624 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs @@ -0,0 +1,33 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is . +/// +public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit +{ + /// + /// Copies glyphs across fonts, in a safer way.
+ /// If the font does not belong to the current atlas, this function is a no-op. + ///
+ /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE'); + + /// + /// Calls , with some fixups. + /// + /// The font. + void BuildLookupTable(ImFontPtr font); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs new file mode 100644 index 000000000..cb8a27a54 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -0,0 +1,186 @@ +using System.IO; +using System.Runtime.InteropServices; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Toolkit for use when the build state is .
+///
+/// After returns, +/// either must be set, +/// or at least one font must have been added to the atlas using one of AddFont... functions. +///
+public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit +{ + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// Disposable type. + /// The disposable. + /// The same . + T DisposeAfterBuild(T disposable) where T : IDisposable; + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The gc handle. + /// The same . + GCHandle DisposeAfterBuild(GCHandle gcHandle); + + /// + /// Queues an item to be disposed after the whole build process gets complete, successful or not. + /// + /// The action to run on dispose. + void DisposeAfterBuild(Action action); + + /// + /// Excludes given font from global scaling. + /// + /// The font. + /// Same with . + ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); + + /// + /// Gets whether global scaling is ignored for the given font. + /// + /// The font. + /// True if ignored. + bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// + /// Do NOT call on the once this function has + /// been called, unless is set and the function has thrown an error. + /// + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + nint dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + => this.AddFontFromImGuiHeapAllocatedMemory( + (void*)dataPointer, + dataSize, + fontConfig, + freeOnException, + debugTag); + + /// + /// Adds a font from memory region allocated using .
+ /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// Do NOT call on the once this + /// function has been called. + ///
+ /// Memory address for the data allocated using . + /// The size of the font file.. + /// The font config. + /// Free if an exception happens. + /// A debug tag. + /// The newly added font. + unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag); + + /// + /// Adds a font from a file. + /// + /// The file path to create a new font from. + /// The font config. + /// The newly added font. + ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig); + + /// + /// Adds a font from a stream. + /// + /// The stream to create a new font from. + /// The font config. + /// Dispose when this function returns or throws. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromStream(Stream stream, in SafeFontConfig fontConfig, bool leaveOpen, string debugTag); + + /// + /// Adds a font from memory. + /// + /// The span to create from. + /// The font config. + /// A debug tag. + /// The newly added font. + ImFontPtr AddFontFromMemory(ReadOnlySpan span, in SafeFontConfig fontConfig, string debugTag); + + /// + /// Adds the default font known to the current font atlas.
+ ///
+ /// Includes and .
+ /// As this involves adding multiple fonts, calling this function will set + /// as the return value of this function, if it was empty before. + ///
+ /// Font size in pixels. + /// The glyph ranges. Use .ToGlyphRange to build. + /// A font returned from . + ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null); + + /// + /// Adds a font that is shipped with Dalamud.
+ ///
+ /// Note: if game symbols font file is requested but is unavailable, + /// then it will take the glyphs from game's built-in fonts, and everything in + /// will be ignored but , , + /// and . + ///
+ /// The font type. + /// The font config. + /// The added font. + ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig); + + /// + /// Same with (, ...), + /// but using only FontAwesome icon ranges.
+ /// will be ignored. + ///
+ /// The font config. + /// The added font. + ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig); + + /// + /// Adds the game's symbols into the provided font.
+ /// will be ignored.
+ /// If the game symbol font file is unavailable, only will be honored. + ///
+ /// The font config. + /// The added font. + ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig); + + /// + /// Adds the game glyphs to the font. + /// + /// The font style. + /// The glyph ranges. + /// The font to merge to. If empty, then a new font will be created. + /// The added font. + ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont); + + /// + /// Adds glyphs of extra languages into the provided font, depending on Dalamud Configuration.
+ /// will be ignored. + ///
+ /// The font config. + void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs new file mode 100644 index 000000000..854594663 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -0,0 +1,42 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Represents a reference counting handle for fonts. +/// +public interface IFontHandle : IDisposable +{ + /// + /// Represents a reference counting handle for fonts. Dalamud internal use only. + /// + internal interface IInternal : IFontHandle + { + /// + /// Gets the font.
+ /// Use of this properly is safe only from the UI thread.
+ /// Use if the intended purpose of this property is .
+ /// Futures changes may make simple not enough. + ///
+ ImFontPtr ImFont { get; } + } + + /// + /// Gets the load exception, if it failed to load. Otherwise, it is null. + /// + Exception? LoadException { get; } + + /// + /// Gets a value indicating whether this font is ready for use.
+ /// Use directly if you want to keep the current ImGui font if the font is not ready. + ///
+ bool Available { get; } + + /// + /// Pushes the current font into ImGui font stack using , if available.
+ /// Use to access the current font.
+ /// You may not access the font once you dispose this object. + ///
+ /// A disposable object that will call (1) on dispose. + IDisposable Push(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs new file mode 100644 index 000000000..f0ed09155 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -0,0 +1,334 @@ +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Logging.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle representing a user-callback generated font. +/// +internal class DelegateFontHandle : IFontHandle.IInternal +{ + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Callback for . + public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) + { + this.manager = manager; + this.CallOnBuildStepChange = callOnBuildStepChange; + } + + /// + /// Gets the function to be called on build step changes. + /// + public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly HashSet handles = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + public HandleManager(string atlasName) => this.Name = $"{atlasName}:{nameof(DelegateFontHandle)}:Manager"; + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + public void Dispose() + { + lock (this.syncRoot) + { + this.handles.Clear(); + this.Substance?.Dispose(); + this.Substance = null; + } + } + + /// + public IFontHandle NewFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) + { + var key = new DelegateFontHandle(this, buildStepDelegate); + lock (this.syncRoot) + this.handles.Add(key); + this.RebuildRecommend?.Invoke(); + return key; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not DelegateFontHandle cgfh) + return; + + lock (this.syncRoot) + this.handles.Remove(cgfh); + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.handles.ToArray()); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); + + // Not owned by this class. Do not dispose. + private readonly DelegateFontHandle[] relevantHandles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The relevant handles. + public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) + { + this.Manager = manager; + this.relevantHandles = relevantHandles; + } + + /// + public IFontHandleManager Manager { get; } + + /// + public void Dispose() + { + this.fonts.Clear(); + this.buildExceptions.Clear(); + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.fonts.GetValueOrDefault(cgfh) : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is DelegateFontHandle cgfh ? this.buildExceptions.GetValueOrDefault(cgfh) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + var fontsVector = toolkitPreBuild.Fonts; + foreach (var k in this.relevantHandles) + { + var fontCountPrevious = fontsVector.Length; + + try + { + toolkitPreBuild.Font = default; + k.CallOnBuildStepChange(toolkitPreBuild); + if (toolkitPreBuild.Font.IsNull()) + { + if (fontCountPrevious == fontsVector.Length) + { + throw new InvalidOperationException( + $"{nameof(FontAtlasBuildStepDelegate)} must either set the " + + $"{nameof(IFontAtlasBuildToolkitPreBuild.Font)} property, or add at least one font."); + } + + toolkitPreBuild.Font = fontsVector[^1]; + } + else + { + var found = false; + unsafe + { + for (var i = fontCountPrevious; !found && i < fontsVector.Length; i++) + { + if (fontsVector[i].NativePtr == toolkitPreBuild.Font.NativePtr) + found = true; + } + } + + if (!found) + { + throw new InvalidOperationException( + "The font does not exist in the atlas' font array. If you need an empty font, try" + + "adding Noto Sans from Dalamud Assets, but using new ushort[]{ ' ', ' ', 0 } as the" + + "glyph range."); + } + } + + if (fontsVector.Length - fontCountPrevious != 1) + { + Log.Warning( + "[{name}:Substance] {n} fonts added from {delegate} PreBuild call; " + + "Using the most recently added font. " + + "Did you mean to use {sfd}.{sfdprop} or {ifcp}.{ifcpprop}?", + this.Manager.Name, + fontsVector.Length - fontCountPrevious, + nameof(FontAtlasBuildStepDelegate), + nameof(SafeFontConfig), + nameof(SafeFontConfig.MergeFont), + nameof(ImFontConfigPtr), + nameof(ImFontConfigPtr.MergeMode)); + } + + for (var i = fontCountPrevious; i < fontsVector.Length; i++) + { + if (fontsVector[i].ValidateUnsafe() is { } ex) + { + throw new InvalidOperationException( + "One of the newly added fonts seem to be pointing to an invalid memory address.", + ex); + } + } + + // Check for duplicate entries; duplicates will result in free-after-free + for (var i = 0; i < fontCountPrevious; i++) + { + for (var j = fontCountPrevious; j < fontsVector.Length; j++) + { + unsafe + { + if (fontsVector[i].NativePtr == fontsVector[j].NativePtr) + throw new InvalidOperationException("An already added font has been added again."); + } + } + } + + this.fonts[k] = toolkitPreBuild.Font; + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PreBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + + // Sanitization, in a futile attempt to prevent crashes on invalid parameters + unsafe + { + var distinct = + fontsVector + .DistinctBy(x => (nint)x.NativePtr) // Remove duplicates + .Where(x => x.ValidateUnsafe() is null) // Remove invalid entries without freeing them + .ToArray(); + + // We're adding the contents back; do not destroy the contents + fontsVector.Clear(true); + fontsVector.AddRange(distinct.AsSpan()); + } + } + } + } + + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + // irrelevant + } + + /// + public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostBuild.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostBuild); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}] An error has occurred while during {delegate} PostBuild call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + foreach (var k in this.relevantHandles) + { + if (!this.fonts[k].IsNotNullAndLoaded()) + continue; + + try + { + toolkitPostPromotion.Font = this.fonts[k]; + k.CallOnBuildStepChange.Invoke(toolkitPostPromotion); + } + catch (Exception e) + { + this.fonts[k] = default; + this.buildExceptions[k] = e; + + Log.Error( + e, + "[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.", + this.Manager.Name, + nameof(FontAtlasBuildStepDelegate)); + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs new file mode 100644 index 000000000..e73ea7548 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -0,0 +1,682 @@ +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Unicode; + +using Dalamud.Configuration.Internal; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + private static readonly Dictionary> PairAdjustmentsCache = + new(); + + /// + /// Implementations for and + /// . + /// + private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable + { + private static readonly ushort FontAwesomeIconMin = + (ushort)Enum.GetValues().Where(x => x > 0).Min(); + + private static readonly ushort FontAwesomeIconMax = + (ushort)Enum.GetValues().Where(x => x > 0).Max(); + + private readonly DisposeSafety.ScopedFinalizer disposeAfterBuild = new(); + private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance; + private readonly FontAtlasFactory factory; + private readonly FontAtlasBuiltData data; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// New atlas. + /// An instance of . + /// Specify whether the current build operation is an asynchronous one. + public BuildToolkit( + FontAtlasFactory factory, + FontAtlasBuiltData data, + GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance, + bool isAsync) + { + this.data = data; + this.gameFontHandleSubstance = gameFontHandleSubstance; + this.IsAsyncBuildOperation = isAsync; + this.factory = factory; + } + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.data.Scale; + + /// + public bool IsAsyncBuildOperation { get; } + + /// + public FontAtlasBuildStep BuildStep { get; set; } + + /// + public ImFontAtlasPtr NewImAtlas => this.data.Atlas; + + /// + public ImVectorWrapper Fonts => this.data.Fonts; + + /// + /// Gets the list of fonts to ignore global scale. + /// + public List GlobalScaleExclusions { get; } = new(); + + /// + public void Dispose() => this.disposeAfterBuild.Dispose(); + + /// + public T2 DisposeAfterBuild(T2 disposable) where T2 : IDisposable => + this.disposeAfterBuild.Add(disposable); + + /// + public GCHandle DisposeAfterBuild(GCHandle gcHandle) => this.disposeAfterBuild.Add(gcHandle); + + /// + public void DisposeAfterBuild(Action action) => this.disposeAfterBuild.Add(action); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.data.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.data.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action); + + /// + public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) + { + this.GlobalScaleExclusions.Add(fontPtr); + return fontPtr; + } + + /// + public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => + this.GlobalScaleExclusions.Contains(fontPtr); + + /// + public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => + this.data.AddNewTexture(textureWrap, disposeOnError); + + /// + public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( + void* dataPointer, + int dataSize, + in SafeFontConfig fontConfig, + bool freeOnException, + string debugTag) + { + Log.Verbose( + "[{name}] 0x{atlas:X}: {funcname}(0x{dataPointer:X}, 0x{dataSize:X}, ...) from {tag}", + this.data.Owner?.Name ?? "(error)", + (nint)this.NewImAtlas.NativePtr, + nameof(this.AddFontFromImGuiHeapAllocatedMemory), + (nint)dataPointer, + dataSize, + debugTag); + + try + { + fontConfig.ThrowOnInvalidValues(); + + var raw = fontConfig.Raw with + { + FontData = dataPointer, + FontDataSize = dataSize, + }; + + if (fontConfig.GlyphRanges is not { Length: > 0 } ranges) + ranges = new ushort[] { 1, 0xFFFE, 0 }; + + raw.GlyphRanges = (ushort*)this.DisposeAfterBuild( + GCHandle.Alloc(ranges, GCHandleType.Pinned)).AddrOfPinnedObject(); + + TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw); + + var font = this.NewImAtlas.AddFont(&raw); + + var dataHash = default(HashCode); + dataHash.AddBytes(new(dataPointer, dataSize)); + var hashIdent = (uint)dataHash.ToHashCode() | ((ulong)dataSize << 32); + + List<(char Left, char Right, float Distance)> pairAdjustments; + lock (PairAdjustmentsCache) + { + if (!PairAdjustmentsCache.TryGetValue(hashIdent, out pairAdjustments)) + { + PairAdjustmentsCache.Add(hashIdent, pairAdjustments = new()); + try + { + pairAdjustments.AddRange(TrueTypeUtils.ExtractHorizontalPairAdjustments(raw).ToArray()); + } + catch + { + // don't care + } + } + } + + foreach (var pair in pairAdjustments) + { + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Left, raw.GlyphRanges)) + continue; + if (!ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(pair.Right, raw.GlyphRanges)) + continue; + + font.AddKerningPair(pair.Left, pair.Right, pair.Distance * raw.SizePixels); + } + + return font; + } + catch + { + if (freeOnException) + ImGuiNative.igMemFree(dataPointer); + throw; + } + } + + /// + public ImFontPtr AddFontFromFile(string path, in SafeFontConfig fontConfig) + { + return this.AddFontFromStream( + File.OpenRead(path), + fontConfig, + false, + $"{nameof(this.AddFontFromFile)}({path})"); + } + + /// + public unsafe ImFontPtr AddFontFromStream( + Stream stream, + in SafeFontConfig fontConfig, + bool leaveOpen, + string debugTag) + { + using var streamCloser = leaveOpen ? null : stream; + if (!stream.CanSeek) + { + // There is no need to dispose a MemoryStream. + var ms = new MemoryStream(); + stream.CopyTo(ms); + stream = ms; + } + + var length = checked((int)(uint)stream.Length); + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + stream.ReadExactly(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromStream)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public unsafe ImFontPtr AddFontFromMemory( + ReadOnlySpan span, + in SafeFontConfig fontConfig, + string debugTag) + { + var length = span.Length; + var memory = ImGuiHelpers.AllocateMemory(length); + try + { + span.CopyTo(new(memory, length)); + return this.AddFontFromImGuiHeapAllocatedMemory( + memory, + length, + fontConfig, + false, + $"{nameof(this.AddFontFromMemory)}({debugTag})"); + } + catch + { + ImGuiNative.igMemFree(memory); + throw; + } + } + + /// + public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) + { + ImFontPtr font; + glyphRanges ??= this.factory.DefaultGlyphRanges; + if (this.factory.UseAxis) + { + font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); + } + else + { + font = this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { SizePx = sizePx, GlyphRanges = glyphRanges }); + this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font }); + } + + this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font }); + if (this.Font.IsNull()) + this.Font = font; + return font; + } + + /// + public ImFontPtr AddDalamudAssetFont(DalamudAsset asset, in SafeFontConfig fontConfig) + { + if (asset.GetPurpose() != DalamudAssetPurpose.Font) + throw new ArgumentOutOfRangeException(nameof(asset), asset, "Must have the purpose of Font."); + + switch (asset) + { + case DalamudAsset.LodestoneGameSymbol when this.factory.HasGameSymbolsFontFile: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + SizePx = (fontConfig.SizePx * 3) / 2, + }); + + case DalamudAsset.LodestoneGameSymbol when !this.factory.HasGameSymbolsFontFile: + { + return this.AddGameGlyphs( + new(GameFontFamily.Axis, fontConfig.SizePx), + fontConfig.GlyphRanges, + fontConfig.MergeFont); + } + + default: + return this.factory.AddFont( + this, + asset, + fontConfig with + { + FontNo = 0, + }); + } + } + + /// + public ImFontPtr AddFontAwesomeIconFont(in SafeFontConfig fontConfig) => this.AddDalamudAssetFont( + DalamudAsset.FontAwesomeFreeSolid, + fontConfig with + { + GlyphRanges = new ushort[] { FontAwesomeIconMin, FontAwesomeIconMax, 0 }, + }); + + /// + public ImFontPtr AddGameSymbol(in SafeFontConfig fontConfig) => + this.AddDalamudAssetFont( + DalamudAsset.LodestoneGameSymbol, + fontConfig with + { + GlyphRanges = new ushort[] + { + GamePrebakedFontHandle.SeIconCharMin, + GamePrebakedFontHandle.SeIconCharMax, + 0, + }, + }); + + /// + public ImFontPtr AddGameGlyphs(GameFontStyle gameFontStyle, ushort[]? glyphRanges, ImFontPtr mergeFont) => + this.gameFontHandleSubstance.AttachGameGlyphs(this, mergeFont, gameFontStyle, glyphRanges); + + /// + public void AttachExtraGlyphsForDalamudLanguage(in SafeFontConfig fontConfig) + { + var dalamudConfiguration = Service.Get(); + if (dalamudConfiguration.EffectiveLanguage == "ko" + || Service.GetNullable()?.EncounteredHangul is true) + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansKrRegular, + fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.HangulJamo, + UnicodeRanges.HangulCompatibilityJamo, + UnicodeRanges.HangulSyllables, + UnicodeRanges.HangulJamoExtendedA, + UnicodeRanges.HangulJamoExtendedB), + }); + } + + var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows); + var fontPathChs = Path.Combine(windowsDir, "Fonts", "msyh.ttc"); + if (!File.Exists(fontPathChs)) + fontPathChs = null; + + var fontPathCht = Path.Combine(windowsDir, "Fonts", "msjh.ttc"); + if (!File.Exists(fontPathCht)) + fontPathCht = null; + + if (fontPathCht != null && Service.Get().EffectiveLanguage == "tw") + { + this.AddFontFromFile(fontPathCht, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } + else if (fontPathChs != null && (Service.Get().EffectiveLanguage == "zh" + || Service.GetNullable()?.EncounteredHan is true)) + { + this.AddFontFromFile(fontPathChs, fontConfig with + { + GlyphRanges = ImGuiHelpers.CreateImGuiRangesFrom( + UnicodeRanges.CjkUnifiedIdeographs, + UnicodeRanges.CjkUnifiedIdeographsExtensionA), + }); + } + } + + public void PreBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPreBuild(this); + foreach (var substance in this.data.Substances) + substance.OnPreBuildCleanup(this); + } + + public unsafe void PreBuild() + { + var configData = this.data.ConfigData; + foreach (ref var config in configData.DataSpan) + { + if (this.GlobalScaleExclusions.Contains(new(config.DstFont))) + continue; + + config.SizePixels *= this.Scale; + + config.GlyphMaxAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMaxAdvanceX)) + config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphMinAdvanceX *= this.Scale; + if (float.IsInfinity(config.GlyphMinAdvanceX)) + config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; + + config.GlyphOffset *= this.Scale; + } + } + + public void DoBuild() + { + // ImGui will call AddFontDefault() on Build() call. + // AddFontDefault() will reliably crash, when invoked multithreaded. + // We add a dummy font to prevent that. + if (this.data.ConfigData.Length == 0) + { + this.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() { GlyphRanges = new ushort[] { ' ', ' ', '\0' }, SizePx = 1 }); + } + + if (!this.NewImAtlas.Build()) + throw new InvalidOperationException("ImFontAtlas.Build failed"); + + this.BuildStep = FontAtlasBuildStep.PostBuild; + } + + public unsafe void PostBuild() + { + var scale = this.Scale; + foreach (ref var font in this.Fonts.DataSpan) + { + if (!this.GlobalScaleExclusions.Contains(font)) + font.AdjustGlyphMetrics(1 / scale, 1 / scale); + + foreach (var c in FallbackCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.UpdateFallbackChar(c); + break; + } + + foreach (var c in EllipsisCodepoints) + { + var g = font.FindGlyphNoFallback(c); + if (g.NativePtr == null) + continue; + + font.EllipsisChar = c; + break; + } + } + } + + public void PostBuildSubstances() + { + foreach (var substance in this.data.Substances) + substance.OnPostBuild(this); + } + + public unsafe void UploadTextures() + { + var buf = Array.Empty(); + try + { + var use4 = this.factory.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = use4 ? 2 : 4; + var width = this.NewImAtlas.TexWidth; + var height = this.NewImAtlas.TexHeight; + foreach (ref var texture in this.data.ImTextures.DataSpan) + { + if (texture.TexID != 0) + { + // Nothing to do + } + else if (texture.TexPixelsRGBA32 is not null) + { + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + new(texture.TexPixelsRGBA32, width * height * 4), + width * 4, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.R8G8B8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + } + else if (texture.TexPixelsAlpha8 is not null) + { + var numPixels = width * height; + if (buf.Length < numPixels * bpp) + { + ArrayPool.Shared.Return(buf); + buf = ArrayPool.Shared.Rent(numPixels * bpp); + } + + fixed (void* pBuf = buf) + { + var sourcePtr = texture.TexPixelsAlpha8; + if (use4) + { + var target = (ushort*)pBuf; + while (numPixels-- > 0) + { + *target = (ushort)((*sourcePtr << 8) | 0x0FFF); + target++; + sourcePtr++; + } + } + else + { + var target = (uint*)pBuf; + while (numPixels-- > 0) + { + *target = (uint)((*sourcePtr << 24) | 0x00FFFFFF); + target++; + sourcePtr++; + } + } + } + + var wrap = this.factory.InterfaceManager.LoadImageFromDxgiFormat( + buf, + width * bpp, + width, + height, + use4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm); + this.data.AddExistingTexture(wrap); + texture.TexID = wrap.ImGuiHandle; + continue; + } + else + { + Log.Warning( + "[{name}]: TexID, TexPixelsRGBA32, and TexPixelsAlpha8 are all null", + this.data.Owner?.Name ?? "(error)"); + } + + if (texture.TexPixelsRGBA32 is not null) + ImGuiNative.igMemFree(texture.TexPixelsRGBA32); + if (texture.TexPixelsAlpha8 is not null) + ImGuiNative.igMemFree(texture.TexPixelsAlpha8); + texture.TexPixelsRGBA32 = null; + texture.TexPixelsAlpha8 = null; + } + } + finally + { + ArrayPool.Shared.Return(buf); + } + } + } + + /// + /// Implementations for . + /// + private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion + { + private readonly FontAtlasBuiltData builtData; + + /// + /// Initializes a new instance of the class. + /// + /// The built data. + public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.builtData.Scale; + + /// + public bool IsAsyncBuildOperation => true; + + /// + public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; + + /// + public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; + + /// + public unsafe ImVectorWrapper Fonts => new( + &this.NewImAtlas.NativePtr->Fonts, + x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); + + /// + public unsafe void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE') + { + var sourceFound = false; + var targetFound = false; + foreach (var f in this.Fonts) + { + sourceFound |= f.NativePtr == source.NativePtr; + targetFound |= f.NativePtr == target.NativePtr; + } + + if (sourceFound && targetFound) + { + ImGuiHelpers.CopyGlyphsAcrossFonts( + source, + target, + missingOnly, + false, + rangeLow, + rangeHigh); + if (rebuildLookupTable) + this.BuildLookupTable(target); + } + } + + /// + public unsafe void BuildLookupTable(ImFontPtr font) + { + // Need to clear previous Fallback pointers before BuildLookupTable, or it may crash + font.NativePtr->FallbackGlyph = null; + font.NativePtr->FallbackHotData = null; + font.BuildLookupTable(); + + // Need to fix our custom ImGui, so that imgui_widgets.cpp:3656 stops thinking + // Codepoint < FallbackHotData.size always means that it's not fallback char. + // Otherwise, having a fallback character in ImGui.InputText gets strange. + var indexedHotData = font.IndexedHotDataWrapped(); + var indexLookup = font.IndexLookupWrapped(); + ref var fallbackHotData = ref *(ImGuiHelpers.ImFontGlyphHotDataReal*)font.NativePtr->FallbackHotData; + for (var codepoint = 0; codepoint < indexedHotData.Length; codepoint++) + { + if (indexLookup[codepoint] == ushort.MaxValue) + { + indexedHotData[codepoint].AdvanceX = fallbackHotData.AdvanceX; + indexedHotData[codepoint].OccupiedWidth = fallbackHotData.OccupiedWidth; + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs new file mode 100644 index 000000000..5656fc673 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -0,0 +1,726 @@ +// #define VeryVerboseLog + +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Logging.Internal; +using Dalamud.Utility; + +using ImGuiNET; + +using JetBrains.Annotations; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Standalone font atlas. +/// +internal sealed partial class FontAtlasFactory +{ + /// + /// Fallback codepoints for ImFont. + /// + public const string FallbackCodepoints = "\u3013\uFFFD?-"; + + /// + /// Ellipsis codepoints for ImFont. + /// + public const string EllipsisCodepoints = "\u2026\u0085"; + + /// + /// If set, disables concurrent font build operation. + /// + private static readonly object? NoConcurrentBuildOperationLock = null; // new(); + + private static readonly ModuleLog Log = new(nameof(FontAtlasFactory)); + + private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); + + private struct FontAtlasBuiltData : IDisposable + { + public readonly DalamudFontAtlas? Owner; + public readonly ImFontAtlasPtr Atlas; + public readonly float Scale; + + public bool IsBuildInProgress; + + private readonly List? wraps; + private readonly List? substances; + private readonly DisposeSafety.ScopedFinalizer? garbage; + + public unsafe FontAtlasBuiltData( + DalamudFontAtlas owner, + IEnumerable substances, + float scale) + { + this.Owner = owner; + this.Scale = scale; + this.garbage = new(); + + try + { + var substancesList = this.substances = new(); + foreach (var s in substances) + substancesList.Add(this.garbage.Add(s)); + this.garbage.Add(() => substancesList.Clear()); + + var wrapsCopy = this.wraps = new(); + this.garbage.Add(() => wrapsCopy.Clear()); + + var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); + this.Atlas = atlasPtr; + if (this.Atlas.NativePtr is null) + throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); + + this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); + this.IsBuildInProgress = true; + } + catch + { + this.garbage.Dispose(); + throw; + } + } + + public readonly DisposeSafety.ScopedFinalizer Garbage => + this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); + + public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + + public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + + public readonly IReadOnlyList Wraps => + (IReadOnlyList?)this.wraps ?? Array.Empty(); + + public readonly IReadOnlyList Substances => + (IReadOnlyList?)this.substances ?? Array.Empty(); + + public readonly void AddExistingTexture(IDalamudTextureWrap wrap) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + this.wraps.Add(this.Garbage.Add(wrap)); + } + + public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) + { + if (this.wraps is null) + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + var handle = wrap.ImGuiHandle; + var index = this.ImTextures.IndexOf(x => x.TexID == handle); + if (index == -1) + { + try + { + this.wraps.EnsureCapacity(this.wraps.Count + 1); + this.ImTextures.EnsureCapacityExponential(this.ImTextures.Length + 1); + + index = this.ImTextures.Length; + this.wraps.Add(this.Garbage.Add(wrap)); + this.ImTextures.Add(new() { TexID = handle }); + } + catch (Exception e) + { + if (disposeOnError) + wrap.Dispose(); + + if (this.wraps.Count != this.ImTextures.Length) + { + Log.Error( + e, + "{name} failed, and {wraps} and {imtextures} have different number of items", + nameof(this.AddNewTexture), + nameof(this.Wraps), + nameof(this.ImTextures)); + + if (this.wraps.Count > 0 && this.wraps[^1] == wrap) + this.wraps.RemoveAt(this.wraps.Count - 1); + if (this.ImTextures.Length > 0 && this.ImTextures[^1].TexID == handle) + this.ImTextures.RemoveAt(this.ImTextures.Length - 1); + + if (this.wraps.Count != this.ImTextures.Length) + Log.Fatal("^ Failed to undo due to an internal inconsistency; embrace for a crash"); + } + + throw; + } + } + + return index; + } + + public unsafe void Dispose() + { + if (this.garbage is null) + return; + + if (this.IsBuildInProgress) + { + Log.Error( + "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + + "Stack:\n{trace}", + this.Owner?.Name ?? "", + (nint)this.Atlas.NativePtr, + new StackTrace()); + while (this.IsBuildInProgress) + Thread.Sleep(100); + } + +#if VeryVerboseLog + Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); +#endif + this.garbage.Dispose(); + } + + public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) + { + var axisSubstance = this.Substances.OfType().Single(); + return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; + } + } + + private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback + { + private readonly DisposeSafety.ScopedFinalizer disposables = new(); + private readonly FontAtlasFactory factory; + private readonly DelegateFontHandle.HandleManager delegateFontHandleManager; + private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; + private readonly IFontHandleManager[] fontHandleManagers; + + private readonly object syncRootPostPromotion = new(); + private readonly object syncRoot = new(); + + private Task buildTask = EmptyTask; + private FontAtlasBuiltData builtData; + + private int buildSuppressionCounter; + private bool buildSuppressionSuppressed; + + private int buildIndex; + private bool buildQueued; + private bool disposed = false; + + /// + /// Initializes a new instance of the class. + /// + /// The factory. + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas are under the effect of global scale. + public DalamudFontAtlas( + FontAtlasFactory factory, + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled) + { + this.IsGlobalScaled = isGlobalScaled; + try + { + this.factory = factory; + this.AutoRebuildMode = autoRebuildMode; + this.Name = atlasName; + + this.factory.InterfaceManager.AfterBuildFonts += this.OnRebuildRecommend; + this.disposables.Add(() => this.factory.InterfaceManager.AfterBuildFonts -= this.OnRebuildRecommend); + + this.fontHandleManagers = new IFontHandleManager[] + { + this.delegateFontHandleManager = this.disposables.Add( + new DelegateFontHandle.HandleManager(atlasName)), + this.gameFontHandleManager = this.disposables.Add( + new GamePrebakedFontHandle.HandleManager(atlasName, factory)), + }; + foreach (var fhm in this.fontHandleManagers) + fhm.RebuildRecommend += this.OnRebuildRecommend; + } + catch + { + this.disposables.Dispose(); + throw; + } + + this.factory.SceneTask.ContinueWith( + r => + { + lock (this.syncRoot) + { + if (this.disposed) + return; + + r.Result.OnNewRenderFrame += this.ImGuiSceneOnNewRenderFrame; + this.disposables.Add(() => r.Result.OnNewRenderFrame -= this.ImGuiSceneOnNewRenderFrame); + } + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + this.BuildFontsOnNextFrame(); + }); + } + + /// + /// Finalizes an instance of the class. + /// + ~DalamudFontAtlas() + { + lock (this.syncRoot) + { + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.builtData.Dispose(); + } + } + + /// + public event FontAtlasBuildStepDelegate? BuildStepChange; + + /// + public event Action? RebuildRecommend; + + /// + public event Action? BeforeDispose; + + /// + public event Action? AfterDispose; + + /// + public string Name { get; } + + /// + public FontAtlasAutoRebuildMode AutoRebuildMode { get; } + + /// + public ImFontAtlasPtr ImAtlas + { + get + { + lock (this.syncRoot) + return this.builtData.Atlas; + } + } + + /// + public Task BuildTask => this.buildTask; + + /// + public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); + + /// + public bool IsGlobalScaled { get; } + + /// + public void Dispose() + { + if (this.disposed) + return; + + this.BeforeDispose?.InvokeSafely(this); + + try + { + lock (this.syncRoot) + { + this.disposed = true; + this.buildTask.ToDisposableIgnoreExceptions().Dispose(); + this.buildTask = EmptyTask; + this.disposables.Add(this.builtData); + this.builtData = default; + this.disposables.Dispose(); + } + + try + { + this.AfterDispose?.Invoke(this, null); + } + catch + { + // ignore + } + } + catch (Exception e) + { + try + { + this.AfterDispose?.Invoke(this, e); + } + catch + { + // ignore + } + } + + GC.SuppressFinalize(this); + } + + /// + public IDisposable SuppressAutoRebuild() + { + this.buildSuppressionCounter++; + return Disposable.Create( + () => + { + this.buildSuppressionCounter--; + if (this.buildSuppressionSuppressed) + this.OnRebuildRecommend(); + }); + } + + /// + public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); + + /// + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => + this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); + + /// + public void BuildFontsOnNextFrame() + { + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsOnNextFrame)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + if (!this.buildTask.IsCompleted || this.buildQueued) + return; + +#if VeryVerboseLog + Log.Verbose("[{name}] Queueing from {source}.", this.Name, nameof(this.BuildFontsOnNextFrame)); +#endif + + this.buildQueued = true; + } + + /// + public void BuildFontsImmediately() + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.Async) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsImmediately)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.Async)}."); + } + + var tcs = new TaskCompletionSource(); + int rebuildIndex; + try + { + rebuildIndex = ++this.buildIndex; + lock (this.syncRoot) + { + if (!this.buildTask.IsCompleted) + throw new InvalidOperationException("Font rebuild is already in progress."); + + this.buildTask = tcs.Task; + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsImmediately)); +#endif + + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var r = this.RebuildFontsPrivate(false, scale); + r.Wait(); + if (r.IsCompletedSuccessfully) + tcs.SetResult(r.Result); + else if (r.Exception is not null) + tcs.SetException(r.Exception); + else + tcs.SetCanceled(); + } + catch (Exception e) + { + tcs.SetException(e); + Log.Error(e, "[{name}] Failed to build fonts.", this.Name); + throw; + } + + this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately)); + } + + /// + public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true) + { +#if VeryVerboseLog + Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync)); +#endif + + if (this.AutoRebuildMode == FontAtlasAutoRebuildMode.OnNewFrame) + { + throw new InvalidOperationException( + $"{nameof(this.BuildFontsAsync)} cannot be used when " + + $"{nameof(this.AutoRebuildMode)} is set to " + + $"{nameof(FontAtlasAutoRebuildMode.OnNewFrame)}."); + } + + lock (this.syncRoot) + { + var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; + var rebuildIndex = ++this.buildIndex; + return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); + + async Task BuildInner(Task unused) + { + Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + return default; + } + + var res = await this.RebuildFontsPrivate(true, scale); + if (res.Atlas.IsNull()) + return res; + + if (callPostPromotionOnMainThread) + { + await this.factory.Framework.RunOnFrameworkThread( + () => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync))); + } + else + { + this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); + } + + return res; + } + } + } + + private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) + { + lock (this.syncRoot) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + this.builtData.ExplicitDisposeIgnoreExceptions(); + this.builtData = data; + this.buildTask = EmptyTask; + foreach (var substance in data.Substances) + substance.Manager.Substance = substance; + } + + lock (this.syncRootPostPromotion) + { + if (this.buildIndex != rebuildIndex) + { + data.ExplicitDisposeIgnoreExceptions(); + return; + } + + var toolkit = new BuildToolkitPostPromotion(data); + + try + { + this.BuildStepChange?.Invoke(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {delegateName} PostPromotion error", + this.Name, + nameof(FontAtlasBuildStepDelegate)); + } + + foreach (var substance in data.Substances) + { + try + { + substance.OnPostPromotion(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {substance} PostPromotion error", + this.Name, + substance.GetType().FullName ?? substance.GetType().Name); + } + } + + foreach (var font in toolkit.Fonts) + { + try + { + toolkit.BuildLookupTable(font); + } + catch (Exception e) + { + Log.Error(e, "[{name}] BuildLookupTable error", this.Name); + } + } + +#if VeryVerboseLog + Log.Verbose("[{name}] Built from {source}.", this.Name, source); +#endif + } + } + + private void ImGuiSceneOnNewRenderFrame() + { + if (!this.buildQueued) + return; + + try + { + if (this.AutoRebuildMode != FontAtlasAutoRebuildMode.Async) + this.BuildFontsImmediately(); + } + finally + { + this.buildQueued = false; + } + } + + private Task RebuildFontsPrivate(bool isAsync, float scale) + { + if (NoConcurrentBuildOperationLock is null) + return this.RebuildFontsPrivateReal(isAsync, scale); + lock (NoConcurrentBuildOperationLock) + return this.RebuildFontsPrivateReal(isAsync, scale); + } + + private async Task RebuildFontsPrivateReal(bool isAsync, float scale) + { + lock (this.syncRoot) + { + // this lock ensures that this.buildTask is properly set. + } + + var sw = new Stopwatch(); + sw.Start(); + + var res = default(FontAtlasBuiltData); + nint atlasPtr = 0; + try + { + res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + unsafe + { + atlasPtr = (nint)res.Atlas.NativePtr; + } + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: PreBuild (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + using var toolkit = res.CreateToolkit(this.factory, isAsync); + this.BuildStepChange?.Invoke(toolkit); + toolkit.PreBuildSubstances(); + toolkit.PreBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.DoBuild(); + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: PostBuild (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + + toolkit.PostBuild(); + toolkit.PostBuildSubstances(); + this.BuildStepChange?.Invoke(toolkit); + + if (this.factory.SceneTask is { IsCompleted: false } sceneTask) + { + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: await SceneTask (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + await sceneTask.ConfigureAwait(!isAsync); + } + +#if VeryVerboseLog + Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: UploadTextures (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); +#endif + toolkit.UploadTextures(); + + Log.Verbose( + "[{name}:{functionname}] 0x{ptr:X}: Complete (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + + res.IsBuildInProgress = false; + return res; + } + catch (Exception e) + { + Log.Error( + e, + "[{name}:{functionname}] 0x{ptr:X}: Failed (at {sw}ms)", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr, + sw.ElapsedMilliseconds); + res.IsBuildInProgress = false; + res.Dispose(); + throw; + } + finally + { + this.buildQueued = false; + } + } + + private void OnRebuildRecommend() + { + if (this.disposed) + return; + + if (this.buildSuppressionCounter > 0) + { + this.buildSuppressionSuppressed = true; + return; + } + + this.buildSuppressionSuppressed = false; + this.factory.Framework.RunOnFrameworkThread( + () => + { + this.RebuildRecommend?.InvokeSafely(); + + switch (this.AutoRebuildMode) + { + case FontAtlasAutoRebuildMode.Async: + _ = this.BuildFontsAsync(); + break; + case FontAtlasAutoRebuildMode.OnNewFrame: + this.BuildFontsOnNextFrame(); + break; + case FontAtlasAutoRebuildMode.Disable: + default: + break; + } + }); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs new file mode 100644 index 000000000..358ccd845 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -0,0 +1,368 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Configuration.Internal; +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + +using ImGuiNET; + +using ImGuiScene; + +using Lumina.Data.Files; + +using SharpDX; +using SharpDX.Direct3D11; +using SharpDX.DXGI; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Factory for the implementation of . +/// +[ServiceManager.BlockingEarlyLoadedService] +internal sealed partial class FontAtlasFactory + : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable +{ + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly IReadOnlyDictionary> fdtFiles; + private readonly IReadOnlyDictionary[]>> texFiles; + private readonly IReadOnlyDictionary> prebakedTextureWraps; + private readonly Task defaultGlyphRanges; + private readonly DalamudAssetManager dalamudAssetManager; + + [ServiceManager.ServiceConstructor] + private FontAtlasFactory( + DataManager dataManager, + Framework framework, + InterfaceManager interfaceManager, + DalamudAssetManager dalamudAssetManager) + { + this.Framework = framework; + this.InterfaceManager = interfaceManager; + this.dalamudAssetManager = dalamudAssetManager; + this.SceneTask = Service + .GetAsync() + .ContinueWith(r => r.Result.Manager.Scene); + + var gffasInfo = Enum.GetValues() + .Select( + x => + ( + Font: x, + Attr: x.GetAttribute())) + .Where(x => x.Attr is not null) + .ToArray(); + var texPaths = gffasInfo.Select(x => x.Attr.TexPathFormat).Distinct().ToArray(); + + this.fdtFiles = gffasInfo.ToImmutableDictionary( + x => x.Font, + x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); + var channelCountsTask = texPaths.ToImmutableDictionary( + x => x, + x => Task.WhenAll( + gffasInfo.Where(y => y.Attr.TexPathFormat == x) + .Select(y => this.fdtFiles[y.Font])) + .ContinueWith( + files => 1 + files.Result.Max( + file => + { + unsafe + { + using var pin = file.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Length); + return fdt.MaxTextureIndex; + } + }))); + this.prebakedTextureWraps = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith(y => new IDalamudTextureWrap?[y.Result])); + this.texFiles = channelCountsTask.ToImmutableDictionary( + x => x.Key, + x => x.Value.ContinueWith( + y => Enumerable + .Range(1, 1 + ((y.Result - 1) / 4)) + .Select(z => Task.Run(() => dataManager.GetFile(string.Format(x.Key, z))!)) + .ToArray())); + this.defaultGlyphRanges = + this.fdtFiles[GameFontFamilyAndSize.Axis12] + .ContinueWith( + file => + { + unsafe + { + using var pin = file.Result.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Result.Length); + return fdt.ToGlyphRanges(); + } + }); + } + + /// + /// Gets or sets a value indicating whether to override configuration for UseAxis. + /// + public bool? UseAxisOverride { get; set; } = null; + + /// + /// Gets a value indicating whether to use AXIS fonts. + /// + public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; + + /// + /// Gets the service instance of . + /// + public Framework Framework { get; } + + /// + /// Gets the service instance of .
+ /// may not yet be available. + ///
+ public InterfaceManager InterfaceManager { get; } + + /// + /// Gets the async task for inside . + /// + public Task SceneTask { get; } + + /// + /// Gets the default glyph ranges (glyph ranges of ). + /// + public ushort[] DefaultGlyphRanges => ExtractResult(this.defaultGlyphRanges); + + /// + /// Gets a value indicating whether game symbol font file is available. + /// + public bool HasGameSymbolsFontFile => + this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); + + /// + public void Dispose() + { + this.cancellationTokenSource.Cancel(); + this.scopedFinalizer.Dispose(); + this.cancellationTokenSource.Dispose(); + } + + /// + /// Creates a new instance of a class that implements the interface. + /// + /// Name of atlas, for debugging and logging purposes. + /// Specify how to auto rebuild. + /// Whether the fonts in the atlas is global scaled. + /// The new font atlas. + public IFontAtlas CreateFontAtlas( + string atlasName, + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true) => + new DalamudFontAtlas(this, atlasName, autoRebuildMode, isGlobalScaled); + + /// + /// Adds the font from Dalamud Assets. + /// + /// The toolkitPostBuild. + /// The font. + /// The font config. + /// The address and size. + public ImFontPtr AddFont( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + DalamudAsset asset, + in SafeFontConfig fontConfig) => + toolkitPreBuild.AddFontFromStream( + this.dalamudAssetManager.CreateStream(asset), + fontConfig, + false, + $"Asset({asset})"); + + /// + /// Gets the for the . + /// + /// The font family and size. + /// The . + public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); + + /// + public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) + { + var arr = ExtractResult(this.fdtFiles[gffas]); + var handle = arr.AsMemory().Pin(); + try + { + fdtFileView = new(handle.Pointer, arr.Length); + return handle; + } + catch + { + handle.Dispose(); + throw; + } + } + + /// + public int GetFontTextureCount(string texPathFormat) => + ExtractResult(this.prebakedTextureWraps[texPathFormat]).Length; + + /// + public TexFile GetTexFile(string texPathFormat, int index) => + ExtractResult(ExtractResult(this.texFiles[texPathFormat])[index]); + + /// + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex) + { + lock (this.prebakedTextureWraps[texPathFormat]) + { + var wraps = ExtractResult(this.prebakedTextureWraps[texPathFormat]); + var fileIndex = textureIndex / 4; + var channelIndex = FdtReader.FontTableEntry.TextureChannelOrder[textureIndex % 4]; + wraps[textureIndex] ??= this.GetChannelTexture(texPathFormat, fileIndex, channelIndex); + return CloneTextureWrap(wraps[textureIndex]); + } + } + + private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); + + private static unsafe void ExtractChannelFromB8G8R8A8( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4) + { + var numPixels = Math.Min(source.Length / 4, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + channelIndex; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)((*rptr << 8) | 0x0FFF); + wptr++; + rptr += 4; + } + } + else + { + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (uint)((*rptr << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + /// + /// Clones a texture wrap, by getting a new reference to the underlying and the + /// texture behind. + /// + /// The to clone from. + /// The cloned . + private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) + { + var srv = CppObject.FromPointer(wrap.ImGuiHandle); + using var res = srv.Resource; + using var tex2D = res.QueryInterface(); + var description = tex2D.Description; + return new DalamudTextureWrap( + new D3DTextureWrap( + srv.QueryInterface(), + description.Width, + description.Height)); + } + + private static unsafe void ExtractChannelFromB4G4R4A4( + Span target, + ReadOnlySpan source, + int channelIndex, + bool targetIsB4G4R4A4) + { + var numPixels = Math.Min(source.Length / 2, target.Length / (targetIsB4G4R4A4 ? 2 : 4)); + fixed (byte* sourcePtrImmutable = source) + { + var rptr = sourcePtrImmutable + (channelIndex / 2); + var rshift = (channelIndex & 1) == 0 ? 0 : 4; + fixed (void* targetPtr = target) + { + if (targetIsB4G4R4A4) + { + var wptr = (ushort*)targetPtr; + while (numPixels-- > 0) + { + *wptr = (ushort)(((*rptr >> rshift) << 12) | 0x0FFF); + wptr++; + rptr += 2; + } + } + else + { + var wptr = (uint*)targetPtr; + while (numPixels-- > 0) + { + var v = (*rptr >> rshift) & 0xF; + v |= v << 4; + *wptr = (uint)((v << 24) | 0x00FFFFFF); + wptr++; + rptr += 4; + } + } + } + } + } + + private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) + { + var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); + var numPixels = texFile.Header.Width * texFile.Header.Height; + + _ = Service.Get(); + var targetIsB4G4R4A4 = this.InterfaceManager.SupportsDxgiFormat(Format.B4G4R4A4_UNorm); + var bpp = targetIsB4G4R4A4 ? 2 : 4; + var buffer = ArrayPool.Shared.Rent(numPixels * bpp); + try + { + var sliceSpan = texFile.SliceSpan(0, 0, out _, out _, out _); + switch (texFile.Header.Format) + { + case TexFile.TextureFormat.B4G4R4A4: + // Game ships with this format. + ExtractChannelFromB4G4R4A4(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); + break; + case TexFile.TextureFormat.B8G8R8A8: + // In case of modded font textures. + ExtractChannelFromB8G8R8A8(buffer, sliceSpan, channelIndex, targetIsB4G4R4A4); + break; + default: + // Unlikely. + ExtractChannelFromB8G8R8A8(buffer, texFile.ImageData, channelIndex, targetIsB4G4R4A4); + break; + } + + return this.scopedFinalizer.Add( + this.InterfaceManager.LoadImageFromDxgiFormat( + buffer, + texFile.Header.Width * bpp, + texFile.Header.Width, + texFile.Header.Height, + targetIsB4G4R4A4 ? Format.B4G4R4A4_UNorm : Format.B8G8R8A8_UNorm)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs new file mode 100644 index 000000000..99c817a91 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -0,0 +1,857 @@ +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Disposables; + +using Dalamud.Game.Text; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Utility; + +using ImGuiNET; + +using Lumina.Data.Files; + +using Vector4 = System.Numerics.Vector4; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// A font handle that uses the game's built-in fonts, optionally with some styling. +/// +internal class GamePrebakedFontHandle : IFontHandle.IInternal +{ + /// + /// The smallest value of . + /// + public static readonly char SeIconCharMin = (char)Enum.GetValues().Min(); + + /// + /// The largest value of . + /// + public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); + + private IFontHandleManager? manager; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + /// Font to use. + public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style) + { + if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined) + throw new ArgumentOutOfRangeException(nameof(style), style, null); + + if (style.SizePt <= 0) + throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style)); + + this.manager = manager; + this.FontStyle = style; + } + + /// + /// Provider for for `common/font/fontNN.tex`. + /// + public interface IGameFontTextureProvider + { + /// + /// Creates the for the .
+ /// Dispose after use. + ///
+ /// The font family and size. + /// The view. + /// Dispose this after use.. + public MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView); + + /// + /// Gets the number of font textures. + /// + /// Format of .tex path. + /// The number of textures. + public int GetFontTextureCount(string texPathFormat); + + /// + /// Gets the for the given index of a font. + /// + /// Format of .tex path. + /// The index of .tex file. + /// The . + public TexFile GetTexFile(string texPathFormat, int index); + + /// + /// Gets a new reference of the font texture. + /// + /// Format of .tex path. + /// Texture index. + /// The texture. + public IDalamudTextureWrap NewFontTextureRef(string texPathFormat, int textureIndex); + } + + /// + /// Gets the font style. + /// + public GameFontStyle FontStyle { get; } + + /// + public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); + + /// + public bool Available => this.ImFont.IsNotNullAndLoaded(); + + /// + public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; + + private IFontHandleManager ManagerNotDisposed => + this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); + + /// + public void Dispose() + { + this.manager?.FreeFontHandle(this); + this.manager = null; + } + + /// + public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + + /// + /// Manager for s. + /// + internal sealed class HandleManager : IFontHandleManager + { + private readonly Dictionary gameFontsRc = new(); + private readonly object syncRoot = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The name of the owner atlas. + /// An instance of . + public HandleManager(string atlasName, IGameFontTextureProvider gameFontTextureProvider) + { + this.GameFontTextureProvider = gameFontTextureProvider; + this.Name = $"{atlasName}:{nameof(GamePrebakedFontHandle)}:Manager"; + } + + /// + public event Action? RebuildRecommend; + + /// + public string Name { get; } + + /// + public IFontHandleSubstance? Substance { get; set; } + + /// + /// Gets an instance of . + /// + public IGameFontTextureProvider GameFontTextureProvider { get; } + + /// + public void Dispose() + { + this.Substance?.Dispose(); + this.Substance = null; + } + + /// + public IFontHandle NewFontHandle(GameFontStyle style) + { + var handle = new GamePrebakedFontHandle(this, style); + bool suggestRebuild; + lock (this.syncRoot) + { + this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; + suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; + } + + if (suggestRebuild) + this.RebuildRecommend?.Invoke(); + + return handle; + } + + /// + public void FreeFontHandle(IFontHandle handle) + { + if (handle is not GamePrebakedFontHandle ggfh) + return; + + lock (this.syncRoot) + { + if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) + return; + + if ((this.gameFontsRc[ggfh.FontStyle] -= 1) == 0) + this.gameFontsRc.Remove(ggfh.FontStyle); + } + } + + /// + public IFontHandleSubstance NewSubstance() + { + lock (this.syncRoot) + return new HandleSubstance(this, this.gameFontsRc.Keys); + } + } + + /// + /// Substance from . + /// + internal sealed class HandleSubstance : IFontHandleSubstance + { + private readonly HandleManager handleManager; + private readonly HashSet gameFontStyles; + + // Owned by this class, but ImFontPtr values still do not belong to this. + private readonly Dictionary fonts = new(); + private readonly Dictionary buildExceptions = new(); + private readonly List<(ImFontPtr Font, GameFontStyle Style, ushort[]? Ranges)> attachments = new(); + + private readonly HashSet templatedFonts = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The manager. + /// The game font styles. + public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) + { + this.handleManager = manager; + Service.Get(); + this.gameFontStyles = new(gameFontStyles); + } + + /// + public IFontHandleManager Manager => this.handleManager; + + /// + public void Dispose() + { + } + + /// + /// Attaches game symbols to the given font. If font is null, it will be created. + /// + /// The toolkitPostBuild. + /// The font to attach to. + /// The game font style. + /// The intended glyph ranges. + /// if it is not empty; otherwise a new font. + public ImFontPtr AttachGameGlyphs( + IFontAtlasBuildToolkitPreBuild toolkitPreBuild, + ImFontPtr font, + GameFontStyle style, + ushort[]? glyphRanges = null) + { + if (font.IsNull()) + font = this.CreateTemplateFont(toolkitPreBuild, style.SizePx); + this.attachments.Add((font, style, glyphRanges)); + return font; + } + + /// + /// Creates or gets a relevant for the given . + /// + /// The game font style. + /// The toolkitPostBuild. + /// The font. + public ImFontPtr GetOrCreateFont(GameFontStyle style, IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + try + { + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + style, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(plan.FullRangeFont); + return plan.FullRangeFont; + } + catch (Exception e) + { + this.buildExceptions[style] = e; + throw; + } + } + + /// + public ImFontPtr GetFontPtr(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh + ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default + : default; + + /// + public Exception? GetBuildException(IFontHandle handle) => + handle is GamePrebakedFontHandle ggfh ? this.buildExceptions.GetValueOrDefault(ggfh.FontStyle) : default; + + /// + public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var style in this.gameFontStyles) + { + if (this.fonts.ContainsKey(style)) + continue; + + try + { + _ = this.GetOrCreateFont(style, toolkitPreBuild); + } + catch + { + // ignore; it should have been recorded from the call + } + } + } + + /// + public void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) + { + foreach (var (font, style, ranges) in this.attachments) + { + var effectiveStyle = + toolkitPreBuild.IsGlobalScaleIgnored(font) + ? style.Scale(1 / toolkitPreBuild.Scale) + : style; + if (!this.fonts.TryGetValue(style, out var plan)) + { + plan = new( + effectiveStyle, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + this.fonts[style] = plan; + } + + plan.AttachFont(font, ranges); + } + + foreach (var plan in this.fonts.Values) + { + plan.EnsureGlyphs(toolkitPreBuild.NewImAtlas); + } + } + + /// + public unsafe void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + var allTextureIndices = new Dictionary(); + var allTexFiles = new Dictionary(); + using var rentReturn = Disposable.Create( + () => + { + foreach (var x in allTextureIndices.Values) + ArrayPool.Shared.Return(x); + foreach (var x in allTexFiles.Values) + ArrayPool.Shared.Return(x); + }); + + var pixels8Array = new byte*[toolkitPostBuild.NewImAtlas.Textures.Size]; + var widths = new int[toolkitPostBuild.NewImAtlas.Textures.Size]; + for (var i = 0; i < pixels8Array.Length; i++) + toolkitPostBuild.NewImAtlas.GetTexDataAsAlpha8(i, out pixels8Array[i], out widths[i], out _); + + foreach (var (style, plan) in this.fonts) + { + try + { + foreach (var font in plan.Ranges.Keys) + this.PatchFontMetricsIfNecessary(style, font, toolkitPostBuild.Scale); + + plan.SetFullRangeFontGlyphs(toolkitPostBuild, allTexFiles, allTextureIndices, pixels8Array, widths); + plan.CopyGlyphsToRanges(toolkitPostBuild); + plan.PostProcessFullRangeFont(toolkitPostBuild.Scale); + } + catch (Exception e) + { + this.buildExceptions[style] = e; + this.fonts[style] = default; + } + } + } + + /// + public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) + { + // Irrelevant + } + + /// + /// Creates a new template font. + /// + /// The toolkitPostBuild. + /// The size of the font. + /// The font. + private ImFontPtr CreateTemplateFont(IFontAtlasBuildToolkitPreBuild toolkitPreBuild, float sizePx) + { + var font = toolkitPreBuild.AddDalamudAssetFont( + DalamudAsset.NotoSansJpMedium, + new() + { + GlyphRanges = new ushort[] { ' ', ' ', '\0' }, + SizePx = sizePx, + }); + this.templatedFonts.Add(font); + return font; + } + + private unsafe void PatchFontMetricsIfNecessary(GameFontStyle style, ImFontPtr font, float atlasScale) + { + if (!this.templatedFonts.Contains(font)) + return; + + var fas = style.Scale(atlasScale).FamilyAndSize; + using var handle = this.handleManager.GameFontTextureProvider.CreateFdtFileView(fas, out var fdt); + ref var fdtFontHeader = ref fdt.FontHeader; + var fontPtr = font.NativePtr; + + var scale = style.SizePt / fdtFontHeader.Size; + fontPtr->Ascent = fdtFontHeader.Ascent * scale; + fontPtr->Descent = fdtFontHeader.Descent * scale; + fontPtr->EllipsisChar = '…'; + } + } + + [SuppressMessage( + "StyleCop.CSharp.MaintainabilityRules", + "SA1401:Fields should be private", + Justification = "Internal")] + private sealed class FontDrawPlan : IDisposable + { + public readonly GameFontStyle Style; + public readonly GameFontStyle BaseStyle; + public readonly GameFontFamilyAndSizeAttribute BaseAttr; + public readonly int TexCount; + public readonly Dictionary Ranges = new(); + public readonly List<(int RectId, int FdtGlyphIndex)> Rects = new(); + public readonly ushort[] RectLookup = new ushort[0x10000]; + public readonly FdtFileView Fdt; + public readonly ImFontPtr FullRangeFont; + + private readonly IDisposable fdtHandle; + private readonly IGameFontTextureProvider gftp; + + public FontDrawPlan( + GameFontStyle style, + float scale, + IGameFontTextureProvider gameFontTextureProvider, + ImFontPtr fullRangeFont) + { + this.Style = style; + this.BaseStyle = style.Scale(scale); + this.BaseAttr = this.BaseStyle.FamilyAndSize.GetAttribute()!; + this.gftp = gameFontTextureProvider; + this.TexCount = this.gftp.GetFontTextureCount(this.BaseAttr.TexPathFormat); + this.fdtHandle = this.gftp.CreateFdtFileView(this.BaseStyle.FamilyAndSize, out this.Fdt); + this.RectLookup.AsSpan().Fill(ushort.MaxValue); + this.FullRangeFont = fullRangeFont; + this.Ranges[fullRangeFont] = new(0x10000); + } + + public void Dispose() + { + this.fdtHandle.Dispose(); + } + + public void AttachFont(ImFontPtr font, ushort[]? glyphRanges = null) + { + if (!this.Ranges.TryGetValue(font, out var rangeBitArray)) + rangeBitArray = this.Ranges[font] = new(0x10000); + + if (glyphRanges is null) + { + foreach (ref var g in this.Fdt.Glyphs) + { + var c = g.CharInt; + if (c is >= 0x20 and <= 0xFFFE) + rangeBitArray[c] = true; + } + + return; + } + + for (var i = 0; i < glyphRanges.Length - 1; i += 2) + { + if (glyphRanges[i] == 0) + break; + var from = (int)glyphRanges[i]; + var to = (int)glyphRanges[i + 1]; + for (var j = from; j <= to; j++) + rangeBitArray[j] = true; + } + } + + public unsafe void EnsureGlyphs(ImFontAtlasPtr atlas) + { + var glyphs = this.Fdt.Glyphs; + var ranges = this.Ranges[this.FullRangeFont]; + foreach (var (font, extraRange) in this.Ranges) + { + if (font.NativePtr != this.FullRangeFont.NativePtr) + ranges.Or(extraRange); + } + + if (this.Style is not { Weight: 0, SkewStrength: 0 }) + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(this.Fdt.FontHeader, glyph); + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add( + ( + atlas.AddCustomRectFontGlyph( + this.FullRangeFont, + (char)cint, + glyph.BoundingWidth + widthAdjustment, + glyph.BoundingHeight, + glyph.AdvanceWidth, + new(this.BaseAttr.HorizontalOffset, glyph.CurrentOffsetY)), + fdtGlyphIndex)); + } + } + else + { + for (var fdtGlyphIndex = 0; fdtGlyphIndex < glyphs.Length; fdtGlyphIndex++) + { + ref var glyph = ref glyphs[fdtGlyphIndex]; + var cint = glyph.CharInt; + if (cint > char.MaxValue) + continue; + if (!ranges[cint] || this.RectLookup[cint] != ushort.MaxValue) + continue; + + this.RectLookup[cint] = (ushort)this.Rects.Count; + this.Rects.Add((-1, fdtGlyphIndex)); + } + } + } + + public unsafe void PostProcessFullRangeFont(float atlasScale) + { + var round = 1 / atlasScale; + var pfrf = this.FullRangeFont.NativePtr; + ref var frf = ref *pfrf; + + frf.FontSize = MathF.Round(frf.FontSize / round) * round; + frf.Ascent = MathF.Round(frf.Ascent / round) * round; + frf.Descent = MathF.Round(frf.Descent / round) * round; + + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + foreach (ref var g in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + + var fullRange = this.Ranges[this.FullRangeFont]; + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!fullRange[leftInt] || !fullRange[rightInt]) + continue; + ImGuiNative.ImFont_AddKerningPair( + pfrf, + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); + } + + pfrf->FallbackGlyph = null; + ImGuiNative.ImFont_BuildLookupTable(pfrf); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = ImGuiNative.ImFont_FindGlyphNoFallback(pfrf, fallbackCharCandidate); + if ((nint)glyph == IntPtr.Zero) + continue; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + + public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) + { + var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; + var atlasScale = toolkitPostBuild.Scale; + var round = 1 / atlasScale; + + foreach (var (font, rangeBits) in this.Ranges) + { + if (font.NativePtr == this.FullRangeFont.NativePtr) + continue; + + var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font); + + var lookup = font.IndexLookupWrapped(); + var glyphs = font.GlyphsWrapped(); + foreach (ref var sourceGlyph in this.FullRangeFont.GlyphsWrapped().DataSpan) + { + if (!rangeBits[sourceGlyph.Codepoint]) + continue; + + var glyphIndex = ushort.MaxValue; + if (sourceGlyph.Codepoint < lookup.Length) + glyphIndex = lookup[sourceGlyph.Codepoint]; + + if (glyphIndex == ushort.MaxValue) + { + glyphIndex = (ushort)glyphs.Length; + glyphs.Add(default); + } + + ref var g = ref glyphs[glyphIndex]; + g = sourceGlyph; + if (noGlobalScale) + { + g.XY *= scale; + g.AdvanceX *= scale; + } + else + { + var w = (g.X1 - g.X0) * scale; + var h = (g.Y1 - g.Y0) * scale; + g.X0 = MathF.Round((g.X0 * scale) / round) * round; + g.Y0 = MathF.Round((g.Y0 * scale) / round) * round; + g.X1 = g.X0 + w; + g.Y1 = g.Y0 + h; + g.AdvanceX = MathF.Round((g.AdvanceX * scale) / round) * round; + } + } + + foreach (ref var k in this.Fdt.PairAdjustments) + { + var (leftInt, rightInt) = (k.LeftInt, k.RightInt); + if (leftInt > char.MaxValue || rightInt > char.MaxValue) + continue; + if (!rangeBits[leftInt] || !rangeBits[rightInt]) + continue; + if (noGlobalScale) + { + font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale); + } + else + { + font.AddKerningPair( + (ushort)leftInt, + (ushort)rightInt, + MathF.Round((k.RightOffset * scale) / round) * round); + } + } + + font.NativePtr->FallbackGlyph = null; + font.BuildLookupTable(); + + foreach (var fallbackCharCandidate in FontAtlasFactory.FallbackCodepoints) + { + var glyph = font.FindGlyphNoFallback(fallbackCharCandidate).NativePtr; + if ((nint)glyph == IntPtr.Zero) + continue; + + ref var frf = ref *font.NativePtr; + frf.FallbackChar = fallbackCharCandidate; + frf.FallbackGlyph = glyph; + frf.FallbackHotData = + (ImFontGlyphHotData*)frf.IndexedHotData.Address( + fallbackCharCandidate); + break; + } + } + } + + public unsafe void SetFullRangeFontGlyphs( + IFontAtlasBuildToolkitPostBuild toolkitPostBuild, + Dictionary allTexFiles, + Dictionary allTextureIndices, + byte*[] pixels8Array, + int[] widths) + { + var glyphs = this.FullRangeFont.GlyphsWrapped(); + var lookups = this.FullRangeFont.IndexLookupWrapped(); + + ref var fdtFontHeader = ref this.Fdt.FontHeader; + var fdtGlyphs = this.Fdt.Glyphs; + var fdtTexSize = new Vector4( + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight, + this.Fdt.FontHeader.TextureWidth, + this.Fdt.FontHeader.TextureHeight); + + if (!allTexFiles.TryGetValue(this.BaseAttr.TexPathFormat, out var texFiles)) + { + allTexFiles.Add( + this.BaseAttr.TexPathFormat, + texFiles = ArrayPool.Shared.Rent(this.TexCount)); + } + + if (!allTextureIndices.TryGetValue(this.BaseAttr.TexPathFormat, out var textureIndices)) + { + allTextureIndices.Add( + this.BaseAttr.TexPathFormat, + textureIndices = ArrayPool.Shared.Rent(this.TexCount)); + textureIndices.AsSpan(0, this.TexCount).Fill(-1); + } + + var pixelWidth = Math.Max(1, (int)MathF.Ceiling(this.BaseStyle.Weight + 1)); + var pixelStrength = stackalloc byte[pixelWidth]; + for (var i = 0; i < pixelWidth; i++) + pixelStrength[i] = (byte)(255 * Math.Min(1f, (this.BaseStyle.Weight + 1) - i)); + + var minGlyphY = 0; + var maxGlyphY = 0; + foreach (ref var g in fdtGlyphs) + { + minGlyphY = Math.Min(g.CurrentOffsetY, minGlyphY); + maxGlyphY = Math.Max(g.BoundingHeight + g.CurrentOffsetY, maxGlyphY); + } + + var horzShift = stackalloc int[maxGlyphY - minGlyphY]; + var horzBlend = stackalloc byte[maxGlyphY - minGlyphY]; + horzShift -= minGlyphY; + horzBlend -= minGlyphY; + if (this.BaseStyle.BaseSkewStrength != 0) + { + for (var i = minGlyphY; i < maxGlyphY; i++) + { + float blend = this.BaseStyle.BaseSkewStrength switch + { + > 0 => fdtFontHeader.LineHeight - i, + < 0 => -i, + _ => throw new InvalidOperationException(), + }; + blend *= this.BaseStyle.BaseSkewStrength / fdtFontHeader.LineHeight; + horzShift[i] = (int)MathF.Floor(blend); + horzBlend[i] = (byte)(255 * (blend - horzShift[i])); + } + } + + foreach (var (rectId, fdtGlyphIndex) in this.Rects) + { + ref var fdtGlyph = ref fdtGlyphs[fdtGlyphIndex]; + if (rectId == -1) + { + ref var textureIndex = ref textureIndices[fdtGlyph.TextureIndex]; + if (textureIndex == -1) + { + textureIndex = toolkitPostBuild.StoreTexture( + this.gftp.NewFontTextureRef(this.BaseAttr.TexPathFormat, fdtGlyph.TextureIndex), + true); + } + + var glyph = new ImGuiHelpers.ImFontGlyphReal + { + AdvanceX = fdtGlyph.AdvanceWidth, + Codepoint = fdtGlyph.Char, + Colored = false, + TextureIndex = textureIndex, + Visible = true, + X0 = this.BaseAttr.HorizontalOffset, + Y0 = fdtGlyph.CurrentOffsetY, + U0 = fdtGlyph.TextureOffsetX, + V0 = fdtGlyph.TextureOffsetY, + U1 = fdtGlyph.BoundingWidth, + V1 = fdtGlyph.BoundingHeight, + }; + + glyph.XY1 = glyph.XY0 + glyph.UV1; + glyph.UV1 += glyph.UV0; + glyph.UV /= fdtTexSize; + + glyphs.Add(glyph); + } + else + { + ref var rc = ref *(ImGuiHelpers.ImFontAtlasCustomRectReal*)toolkitPostBuild.NewImAtlas + .GetCustomRectByIndex(rectId) + .NativePtr; + var widthAdjustment = this.BaseStyle.CalculateBaseWidthAdjustment(fdtFontHeader, fdtGlyph); + + // Glyph is scaled at this point; undo that. + ref var glyph = ref glyphs[lookups[rc.GlyphId]]; + glyph.X0 = this.BaseAttr.HorizontalOffset; + glyph.Y0 = fdtGlyph.CurrentOffsetY; + glyph.X1 = glyph.X0 + fdtGlyph.BoundingWidth + widthAdjustment; + glyph.Y1 = glyph.Y0 + fdtGlyph.BoundingHeight; + glyph.AdvanceX = fdtGlyph.AdvanceWidth; + + var pixels8 = pixels8Array[rc.TextureIndex]; + var width = widths[rc.TextureIndex]; + texFiles[fdtGlyph.TextureFileIndex] ??= + this.gftp.GetTexFile(this.BaseAttr.TexPathFormat, fdtGlyph.TextureFileIndex); + var sourceBuffer = texFiles[fdtGlyph.TextureFileIndex].ImageData; + var sourceBufferDelta = fdtGlyph.TextureChannelByteIndex; + + for (var y = 0; y < fdtGlyph.BoundingHeight; y++) + { + var sourcePixelIndex = + ((fdtGlyph.TextureOffsetY + y) * fdtFontHeader.TextureWidth) + fdtGlyph.TextureOffsetX; + sourcePixelIndex *= 4; + sourcePixelIndex += sourceBufferDelta; + var blend1 = horzBlend[fdtGlyph.CurrentOffsetY + y]; + + var targetOffset = ((rc.Y + y) * width) + rc.X; + for (var x = 0; x < rc.Width; x++) + pixels8[targetOffset + x] = 0; + + targetOffset += horzShift[fdtGlyph.CurrentOffsetY + y]; + if (blend1 == 0) + { + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var n = sourceBuffer[sourcePixelIndex + 4]; + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255)); + } + } + } + else + { + var blend2 = 255 - blend1; + for (var x = 0; x < fdtGlyph.BoundingWidth; x++, sourcePixelIndex += 4, targetOffset++) + { + var a1 = sourceBuffer[sourcePixelIndex]; + var a2 = x == fdtGlyph.BoundingWidth - 1 ? 0 : sourceBuffer[sourcePixelIndex + 4]; + var n = (a1 * blend1) + (a2 * blend2); + + for (var boldOffset = 0; boldOffset < pixelWidth; boldOffset++) + { + ref var p = ref pixels8[targetOffset + boldOffset]; + p = Math.Max(p, (byte)((pixelStrength[boldOffset] * n) / 255 / 255)); + } + } + } + } + } + } + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs new file mode 100644 index 000000000..93c688608 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -0,0 +1,32 @@ +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Manager for . +/// +internal interface IFontHandleManager : IDisposable +{ + /// + event Action? RebuildRecommend; + + /// + /// Gets the name of the font handle manager. For logging and debugging purposes. + /// + string Name { get; } + + /// + /// Gets or sets the active font handle substance. + /// + IFontHandleSubstance? Substance { get; set; } + + /// + /// Decrease font reference counter. + /// + /// Handle being released. + void FreeFontHandle(IFontHandle handle); + + /// + /// Creates a new substance of the font atlas. + /// + /// The new substance. + IFontHandleSubstance NewSubstance(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs new file mode 100644 index 000000000..f6c5c6591 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -0,0 +1,54 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Substance of a font. +/// +internal interface IFontHandleSubstance : IDisposable +{ + /// + /// Gets the manager relevant to this instance of . + /// + IFontHandleManager Manager { get; } + + /// + /// Gets the font. + /// + /// The handle to get from. + /// Corresponding font or null. + ImFontPtr GetFontPtr(IFontHandle handle); + + /// + /// Gets the exception happened while loading for the font. + /// + /// The handle to get from. + /// Corresponding font or null. + Exception? GetBuildException(IFontHandle handle); + + /// + /// Called before call. + /// + /// The toolkit. + void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called between and calls.
+ /// Any further modification to will result in undefined behavior. + ///
+ /// The toolkit. + void OnPreBuildCleanup(IFontAtlasBuildToolkitPreBuild toolkitPreBuild); + + /// + /// Called after call. + /// + /// The toolkit. + void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild); + + /// + /// Called on the specific thread depending on after + /// promoting the staging atlas to direct use with . + /// + /// The toolkit. + void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs new file mode 100644 index 000000000..8e7149853 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Common.cs @@ -0,0 +1,203 @@ +using System.Buffers.Binary; +using System.Runtime.InteropServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private struct Fixed : IComparable + { + public ushort Major; + public ushort Minor; + + public Fixed(ushort major, ushort minor) + { + this.Major = major; + this.Minor = minor; + } + + public Fixed(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Major); + span.ReadBig(ref offset, out this.Minor); + } + + public int CompareTo(Fixed other) + { + var majorComparison = this.Major.CompareTo(other.Major); + return majorComparison != 0 ? majorComparison : this.Minor.CompareTo(other.Minor); + } + } + + private struct KerningPair : IEquatable + { + public ushort Left; + public ushort Right; + public short Value; + + public KerningPair(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Left); + span.ReadBig(ref offset, out this.Right); + span.ReadBig(ref offset, out this.Value); + } + + public KerningPair(ushort left, ushort right, short value) + { + this.Left = left; + this.Right = right; + this.Value = value; + } + + public static bool operator ==(KerningPair left, KerningPair right) => left.Equals(right); + + public static bool operator !=(KerningPair left, KerningPair right) => !left.Equals(right); + + public static KerningPair ReverseEndianness(KerningPair pair) => new() + { + Left = BinaryPrimitives.ReverseEndianness(pair.Left), + Right = BinaryPrimitives.ReverseEndianness(pair.Right), + Value = BinaryPrimitives.ReverseEndianness(pair.Value), + }; + + public bool Equals(KerningPair other) => + this.Left == other.Left && this.Right == other.Right && this.Value == other.Value; + + public override bool Equals(object? obj) => obj is KerningPair other && this.Equals(other); + + public override int GetHashCode() => HashCode.Combine(this.Left, this.Right, this.Value); + + public override string ToString() => $"KerningPair[{this.Left}, {this.Right}] = {this.Value}"; + } + + [StructLayout(LayoutKind.Explicit, Size = 4)] + private struct PlatformAndEncoding + { + [FieldOffset(0)] + public PlatformId Platform; + + [FieldOffset(2)] + public UnicodeEncodingId UnicodeEncoding; + + [FieldOffset(2)] + public MacintoshEncodingId MacintoshEncoding; + + [FieldOffset(2)] + public IsoEncodingId IsoEncoding; + + [FieldOffset(2)] + public WindowsEncodingId WindowsEncoding; + + public PlatformAndEncoding(PointerSpan source) + { + var offset = 0; + source.ReadBig(ref offset, out this.Platform); + source.ReadBig(ref offset, out this.UnicodeEncoding); + } + + public static PlatformAndEncoding ReverseEndianness(PlatformAndEncoding value) => new() + { + Platform = (PlatformId)BinaryPrimitives.ReverseEndianness((ushort)value.Platform), + UnicodeEncoding = (UnicodeEncodingId)BinaryPrimitives.ReverseEndianness((ushort)value.UnicodeEncoding), + }; + + public readonly string Decode(Span data) + { + switch (this.Platform) + { + case PlatformId.Unicode: + switch (this.UnicodeEncoding) + { + case UnicodeEncodingId.Unicode_2_0_Bmp: + case UnicodeEncodingId.Unicode_2_0_Full: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + + case PlatformId.Macintosh: + switch (this.MacintoshEncoding) + { + case MacintoshEncodingId.Roman: + return Encoding.ASCII.GetString(data); + } + + break; + + case PlatformId.Windows: + switch (this.WindowsEncoding) + { + case WindowsEncodingId.Symbol: + case WindowsEncodingId.UnicodeBmp: + case WindowsEncodingId.UnicodeFullRepertoire: + return Encoding.BigEndianUnicode.GetString(data); + } + + break; + } + + throw new NotSupportedException(); + } + } + + [StructLayout(LayoutKind.Explicit)] + private struct TagStruct : IEquatable, IComparable + { + [FieldOffset(0)] + public unsafe fixed byte Tag[4]; + + [FieldOffset(0)] + public uint NativeValue; + + public unsafe TagStruct(char c1, char c2, char c3, char c4) + { + this.Tag[0] = checked((byte)c1); + this.Tag[1] = checked((byte)c2); + this.Tag[2] = checked((byte)c3); + this.Tag[3] = checked((byte)c4); + } + + public unsafe TagStruct(PointerSpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe TagStruct(ReadOnlySpan span) + { + this.Tag[0] = span[0]; + this.Tag[1] = span[1]; + this.Tag[2] = span[2]; + this.Tag[3] = span[3]; + } + + public unsafe byte this[int index] + { + get => this.Tag[index]; + set => this.Tag[index] = value; + } + + public static bool operator ==(TagStruct left, TagStruct right) => left.Equals(right); + + public static bool operator !=(TagStruct left, TagStruct right) => !left.Equals(right); + + public bool Equals(TagStruct other) => this.NativeValue == other.NativeValue; + + public override bool Equals(object? obj) => obj is TagStruct other && this.Equals(other); + + public override int GetHashCode() => (int)this.NativeValue; + + public int CompareTo(TagStruct other) => this.NativeValue.CompareTo(other.NativeValue); + + public override unsafe string ToString() => + $"0x{this.NativeValue:08X} \"{(char)this.Tag[0]}{(char)this.Tag[1]}{(char)this.Tag[2]}{(char)this.Tag[3]}\""; + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs new file mode 100644 index 000000000..f6a653a51 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Enums.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum IsoEncodingId : ushort + { + Ascii = 0, + Iso_10646 = 1, + Iso_8859_1 = 2, + } + + private enum MacintoshEncodingId : ushort + { + Roman = 0, + } + + private enum NameId : ushort + { + CopyrightNotice = 0, + FamilyName = 1, + SubfamilyName = 2, + UniqueId = 3, + FullFontName = 4, + VersionString = 5, + PostScriptName = 6, + Trademark = 7, + Manufacturer = 8, + Designer = 9, + Description = 10, + UrlVendor = 11, + UrlDesigner = 12, + LicenseDescription = 13, + LicenseInfoUrl = 14, + TypographicFamilyName = 16, + TypographicSubfamilyName = 17, + CompatibleFullMac = 18, + SampleText = 19, + PoscSriptCidFindFontName = 20, + WwsFamilyName = 21, + WwsSubfamilyName = 22, + LightBackgroundPalette = 23, + DarkBackgroundPalette = 24, + VariationPostScriptNamePrefix = 25, + } + + private enum PlatformId : ushort + { + Unicode = 0, + Macintosh = 1, // discouraged + Iso = 2, // deprecated + Windows = 3, + Custom = 4, // OTF Windows NT compatibility mapping + } + + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name in enum value names")] + private enum UnicodeEncodingId : ushort + { + Unicode_1_0 = 0, // deprecated + Unicode_1_1 = 1, // deprecated + IsoIec_10646 = 2, // deprecated + Unicode_2_0_Bmp = 3, + Unicode_2_0_Full = 4, + UnicodeVariationSequences = 5, + UnicodeFullRepertoire = 6, + } + + private enum WindowsEncodingId : ushort + { + Symbol = 0, + UnicodeBmp = 1, + ShiftJis = 2, + Prc = 3, + Big5 = 4, + Wansung = 5, + Johab = 6, + UnicodeFullRepertoire = 10, + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs new file mode 100644 index 000000000..3d89dd806 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Files.cs @@ -0,0 +1,148 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +[SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1310:Field names should not contain underscore", + Justification = "Version name")] +[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Version name")] +internal static partial class TrueTypeUtils +{ + private readonly struct SfntFile : IReadOnlyDictionary> + { + // http://formats.kaitai.io/ttf/ttf.svg + + public static readonly TagStruct FileTagTrueType1 = new('1', '\0', '\0', '\0'); + public static readonly TagStruct FileTagType1 = new('t', 'y', 'p', '1'); + public static readonly TagStruct FileTagOpenTypeWithCff = new('O', 'T', 'T', 'O'); + public static readonly TagStruct FileTagOpenType1_0 = new('\0', '\x01', '\0', '\0'); + public static readonly TagStruct FileTagTrueTypeApple = new('t', 'r', 'u', 'e'); + + public readonly PointerSpan Memory; + public readonly int OffsetInCollection; + public readonly ushort TableCount; + + public SfntFile(PointerSpan memory, int offsetInCollection = 0) + { + var span = memory.Span; + this.Memory = memory; + this.OffsetInCollection = offsetInCollection; + this.TableCount = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + } + + public int Count => this.TableCount; + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable> Values => this.Select(x => x.Value); + + public PointerSpan this[TagStruct key] => this.First(x => x.Key == key).Value; + + public IEnumerator>> GetEnumerator() + { + var offset = 12; + for (var i = 0; i < this.TableCount; i++) + { + var dte = new DirectoryTableEntry(this.Memory[offset..]); + yield return new(dte.Tag, this.Memory.Slice(dte.Offset - this.OffsetInCollection, dte.Length)); + + offset += Unsafe.SizeOf(); + } + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(TagStruct key) => this.Any(x => x.Key == key); + + public bool TryGetValue(TagStruct key, out PointerSpan value) + { + foreach (var (k, v) in this) + { + if (k == key) + { + value = v; + return true; + } + } + + value = default; + return false; + } + + public readonly struct DirectoryTableEntry + { + public readonly PointerSpan Memory; + + public DirectoryTableEntry(PointerSpan span) => this.Memory = span; + + public TagStruct Tag => new(this.Memory); + + public uint Checksum => this.Memory.ReadU32Big(4); + + public int Offset => this.Memory.ReadI32Big(8); + + public int Length => this.Memory.ReadI32Big(12); + } + } + + private readonly struct TtcFile : IReadOnlyList + { + public static readonly TagStruct FileTag = new('t', 't', 'c', 'f'); + + public readonly PointerSpan Memory; + public readonly TagStruct Tag; + public readonly ushort MajorVersion; + public readonly ushort MinorVersion; + public readonly int FontCount; + + public TtcFile(PointerSpan memory) + { + var span = memory.Span; + this.Memory = memory; + this.Tag = new(span); + if (this.Tag != FileTag) + throw new InvalidOperationException(); + + this.MajorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[4..]); + this.MinorVersion = BinaryPrimitives.ReadUInt16BigEndian(span[6..]); + this.FontCount = BinaryPrimitives.ReadInt32BigEndian(span[8..]); + } + + public int Count => this.FontCount; + + public SfntFile this[int index] + { + get + { + if (index < 0 || index >= this.FontCount) + { + throw new IndexOutOfRangeException( + $"The requested font #{index} does not exist in this .ttc file."); + } + + var offset = BinaryPrimitives.ReadInt32BigEndian(this.Memory.Span[(12 + 4 * index)..]); + return new(this.Memory[offset..], offset); + } + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.FontCount; i++) + yield return this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs new file mode 100644 index 000000000..d200de47b --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.GposGsub.cs @@ -0,0 +1,259 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Linq; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + [Flags] + private enum LookupFlags : byte + { + RightToLeft = 1 << 0, + IgnoreBaseGlyphs = 1 << 1, + IgnoreLigatures = 1 << 2, + IgnoreMarks = 1 << 3, + UseMarkFilteringSet = 1 << 4, + } + + private enum LookupType : ushort + { + SingleAdjustment = 1, + PairAdjustment = 2, + CursiveAttachment = 3, + MarkToBaseAttachment = 4, + MarkToLigatureAttachment = 5, + MarkToMarkAttachment = 6, + ContextPositioning = 7, + ChainedContextPositioning = 8, + ExtensionPositioning = 9, + } + + private readonly struct ClassDefTable + { + public readonly PointerSpan Memory; + + public ClassDefTable(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public Format1ClassArray Format1 => new(this.Memory); + + public Format2ClassRanges Format2 => new(this.Memory); + + public IEnumerable<(ushort Class, ushort GlyphId)> Enumerate() + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + var count = format1.GlyphCount; + var classes = format1.ClassValueArray; + for (var i = 0; i < count; i++) + yield return (classes[i], (ushort)(i + startId)); + + break; + } + + case 2: + { + foreach (var range in this.Format2.ClassValueArray) + { + var @class = range.Class; + var startId = range.StartGlyphId; + var count = range.EndGlyphId - startId + 1; + for (var i = 0; i < count; i++) + yield return (@class, (ushort)(startId + i)); + } + + break; + } + } + } + + [Pure] + public ushort GetClass(ushort glyphId) + { + switch (this.Format) + { + case 1: + { + var format1 = this.Format1; + var startId = format1.StartGlyphId; + if (startId <= glyphId && glyphId < startId + format1.GlyphCount) + return this.Format1.ClassValueArray[glyphId - startId]; + + break; + } + + case 2: + { + var rangeSpan = this.Format2.ClassValueArray; + var i = rangeSpan.BinarySearch(new Format2ClassRanges.ClassRangeRecord { EndGlyphId = glyphId }); + if (i >= 0 && rangeSpan[i].ContainsGlyph(glyphId)) + return rangeSpan[i].Class; + + break; + } + } + + return 0; + } + + public readonly struct Format1ClassArray + { + public readonly PointerSpan Memory; + + public Format1ClassArray(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort StartGlyphId => this.Memory.ReadU16Big(2); + + public ushort GlyphCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[6..].As(this.GlyphCount), + BinaryPrimitives.ReverseEndianness); + } + + public readonly struct Format2ClassRanges + { + public readonly PointerSpan Memory; + + public Format2ClassRanges(PointerSpan memory) => this.Memory = memory; + + public ushort ClassRangeCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan ClassValueArray => new( + this.Memory[4..].As(this.ClassRangeCount), + ClassRangeRecord.ReverseEndianness); + + public struct ClassRangeRecord : IComparable + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort Class; + + public static ClassRangeRecord ReverseEndianness(ClassRangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + Class = BinaryPrimitives.ReverseEndianness(value.Class), + }; + + public int CompareTo(ClassRangeRecord other) => this.EndGlyphId.CompareTo(other.EndGlyphId); + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + } + + private readonly struct CoverageTable + { + public readonly PointerSpan Memory; + + public CoverageTable(PointerSpan memory) => this.Memory = memory; + + public enum CoverageFormat : ushort + { + Glyphs = 1, + RangeRecords = 2, + } + + public CoverageFormat Format => this.Memory.ReadEnumBig(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Glyphs => + this.Format == CoverageFormat.Glyphs + ? new(this.Memory[4..].As(this.Count), BinaryPrimitives.ReverseEndianness) + : default(BigEndianPointerSpan); + + public BigEndianPointerSpan RangeRecords => + this.Format == CoverageFormat.RangeRecords + ? new(this.Memory[4..].As(this.Count), RangeRecord.ReverseEndianness) + : default(BigEndianPointerSpan); + + public int GetCoverageIndex(ushort glyphId) + { + switch (this.Format) + { + case CoverageFormat.Glyphs: + return this.Glyphs.BinarySearch(glyphId); + + case CoverageFormat.RangeRecords: + { + var index = this.RangeRecords.BinarySearch( + (in RangeRecord record) => glyphId.CompareTo(record.EndGlyphId)); + + if (index >= 0 && this.RangeRecords[index].ContainsGlyph(glyphId)) + return index; + + return -1; + } + + default: + return -1; + } + } + + public struct RangeRecord + { + public ushort StartGlyphId; + public ushort EndGlyphId; + public ushort StartCoverageIndex; + + public static RangeRecord ReverseEndianness(RangeRecord value) => new() + { + StartGlyphId = BinaryPrimitives.ReverseEndianness(value.StartGlyphId), + EndGlyphId = BinaryPrimitives.ReverseEndianness(value.EndGlyphId), + StartCoverageIndex = BinaryPrimitives.ReverseEndianness(value.StartCoverageIndex), + }; + + public bool ContainsGlyph(ushort glyphId) => + this.StartGlyphId <= glyphId && glyphId <= this.EndGlyphId; + } + } + + private readonly struct LookupTable : IEnumerable> + { + public readonly PointerSpan Memory; + + public LookupTable(PointerSpan memory) => this.Memory = memory; + + public LookupType Type => this.Memory.ReadEnumBig(0); + + public byte MarkAttachmentType => this.Memory[2]; + + public LookupFlags Flags => (LookupFlags)this.Memory[3]; + + public ushort SubtableCount => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubtableOffsets => new( + this.Memory[6..].As(this.SubtableCount), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan this[int index] => this.Memory[this.SubtableOffsets[this.EnsureIndex(index)] ..]; + + public IEnumerator> GetEnumerator() + { + foreach (var i in Enumerable.Range(0, this.SubtableCount)) + yield return this.Memory[this.SubtableOffsets[i] ..]; + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => index >= 0 && index < this.SubtableCount + ? index + : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs new file mode 100644 index 000000000..c91df4ff2 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.PointerSpan.cs @@ -0,0 +1,443 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + private delegate int BinarySearchComparer(in T value); + + private static IDisposable CreatePointerSpan(this T[] data, out PointerSpan pointerSpan) + where T : unmanaged + { + var gchandle = GCHandle.Alloc(data, GCHandleType.Pinned); + pointerSpan = new(gchandle.AddrOfPinnedObject(), data.Length); + return Disposable.Create(() => gchandle.Free()); + } + + private static int BinarySearch(this IReadOnlyList span, in T value) + where T : unmanaged, IComparable + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = value.CompareTo(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static int BinarySearch(this IReadOnlyList span, BinarySearchComparer comparer) + where T : unmanaged + { + var l = 0; + var r = span.Count - 1; + while (l <= r) + { + var i = (int)(((uint)r + (uint)l) >> 1); + var c = comparer(span[i]); + switch (c) + { + case 0: + return i; + case > 0: + l = i + 1; + break; + default: + r = i - 1; + break; + } + } + + return ~l; + } + + private static short ReadI16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static int ReadI32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static long ReadI64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static ushort ReadU16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static uint ReadU32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static ulong ReadU64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static Half ReadF16Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static float ReadF32Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static double ReadF64Big(this PointerSpan ps, int offset) => + BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out short value) => + value = BinaryPrimitives.ReadInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out int value) => + value = BinaryPrimitives.ReadInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out long value) => + value = BinaryPrimitives.ReadInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ushort value) => + value = BinaryPrimitives.ReadUInt16BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out uint value) => + value = BinaryPrimitives.ReadUInt32BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out ulong value) => + value = BinaryPrimitives.ReadUInt64BigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out Half value) => + value = BinaryPrimitives.ReadHalfBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out float value) => + value = BinaryPrimitives.ReadSingleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, int offset, out double value) => + value = BinaryPrimitives.ReadDoubleBigEndian(ps.Span[offset..]); + + private static void ReadBig(this PointerSpan ps, ref int offset, out short value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out int value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out long value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ushort value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out uint value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out ulong value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out Half value) + { + ps.ReadBig(offset, out value); + offset += 2; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out float value) + { + ps.ReadBig(offset, out value); + offset += 4; + } + + private static void ReadBig(this PointerSpan ps, ref int offset, out double value) + { + ps.ReadBig(offset, out value); + offset += 8; + } + + private static unsafe T ReadEnumBig(this PointerSpan ps, int offset) where T : unmanaged, Enum + { + switch (Marshal.SizeOf(Enum.GetUnderlyingType(typeof(T)))) + { + case 1: + var b1 = ps.Span[offset]; + return *(T*)&b1; + case 2: + var b2 = ps.ReadU16Big(offset); + return *(T*)&b2; + case 4: + var b4 = ps.ReadU32Big(offset); + return *(T*)&b4; + case 8: + var b8 = ps.ReadU64Big(offset); + return *(T*)&b8; + default: + throw new ArgumentException("Enum is not of size 1, 2, 4, or 8.", nameof(T), null); + } + } + + private static void ReadBig(this PointerSpan ps, int offset, out T value) where T : unmanaged, Enum => + value = ps.ReadEnumBig(offset); + + private static void ReadBig(this PointerSpan ps, ref int offset, out T value) where T : unmanaged, Enum + { + value = ps.ReadEnumBig(offset); + offset += Unsafe.SizeOf(); + } + + private readonly unsafe struct PointerSpan : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + public PointerSpan(T* pointer, int count) + { + this.Pointer = pointer; + this.Count = count; + } + + public PointerSpan(nint pointer, int count) + : this((T*)pointer, count) + { + } + + public Span Span => new(this.Pointer, this.Count); + + public bool IsEmpty => this.Count == 0; + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + bool ICollection.IsSynchronized => false; + + object ICollection.SyncRoot => this; + + bool ICollection.IsReadOnly => false; + + public ref T this[int index] => ref this.Pointer[this.EnsureIndex(index)]; + + public PointerSpan this[Range range] => this.Slice(range.GetOffsetAndLength(this.Count)); + + T IList.this[int index] + { + get => this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = value; + } + + T IReadOnlyList.this[int index] => this.Pointer[this.EnsureIndex(index)]; + + public bool ContainsPointer(T2* obj) where T2 : unmanaged => + (T*)obj >= this.Pointer && (T*)(obj + 1) <= this.Pointer + this.Count; + + public PointerSpan Slice(int offset, int count) => new(this.Pointer + offset, count); + + public PointerSpan Slice((int Offset, int Count) offsetAndCount) + => this.Slice(offsetAndCount.Offset, offsetAndCount.Count); + + public PointerSpan As(int count) + where T2 : unmanaged => + count > this.Count / sizeof(T2) + ? throw new ArgumentOutOfRangeException( + nameof(count), + count, + $"Wanted {count} items; had {this.Count / sizeof(T2)} items") + : new((T2*)this.Pointer, count); + + public PointerSpan As() + where T2 : unmanaged => + new((T2*)this.Pointer, this.Count / sizeof(T2)); + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return true; + } + + return false; + } + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this.Pointer[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this.Pointer[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this.Pointer[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } + + private readonly unsafe struct BigEndianPointerSpan + : IList, IReadOnlyList, ICollection + where T : unmanaged + { + public readonly T* Pointer; + + private readonly Func reverseEndianness; + + public BigEndianPointerSpan(PointerSpan pointerSpan, Func reverseEndianness) + { + this.reverseEndianness = reverseEndianness; + this.Pointer = pointerSpan.Pointer; + this.Count = pointerSpan.Count; + } + + public int Count { get; } + + public int Length => this.Count; + + public int ByteCount => sizeof(T) * this.Count; + + public bool IsSynchronized => true; + + public object SyncRoot => this; + + public bool IsReadOnly => true; + + public T this[int index] + { + get => + BitConverter.IsLittleEndian + ? this.reverseEndianness(this.Pointer[this.EnsureIndex(index)]) + : this.Pointer[this.EnsureIndex(index)]; + set => this.Pointer[this.EnsureIndex(index)] = + BitConverter.IsLittleEndian + ? this.reverseEndianness(value) + : value; + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < this.Count; i++) + yield return this[i]; + } + + void ICollection.Add(T item) => throw new NotSupportedException(); + + void ICollection.Clear() => throw new NotSupportedException(); + + bool ICollection.Contains(T item) => throw new NotSupportedException(); + + void ICollection.CopyTo(T[] array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array[arrayIndex + i] = this[i]; + } + + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + int IList.IndexOf(T item) + { + for (var i = 0; i < this.Count; i++) + { + if (Equals(this[i], item)) + return i; + } + + return -1; + } + + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if (array.Length < this.Count) + throw new ArgumentException(null, nameof(array)); + + if (array.Length < arrayIndex + this.Count) + throw new ArgumentException(null, nameof(arrayIndex)); + + for (var i = 0; i < this.Count; i++) + array.SetValue(this[i], arrayIndex + i); + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + private int EnsureIndex(int index) => + index >= 0 && index < this.Count ? index : throw new IndexOutOfRangeException(); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs new file mode 100644 index 000000000..80cf4b7da --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.Tables.cs @@ -0,0 +1,1391 @@ +using System.Buffers.Binary; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +[SuppressMessage("ReSharper", "NotAccessedField.Local", Justification = "TrueType specification defined fields")] +[SuppressMessage("ReSharper", "UnusedType.Local", Justification = "TrueType specification defined types")] +[SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internal")] +internal static partial class TrueTypeUtils +{ + [Flags] + private enum ValueFormat : ushort + { + PlacementX = 1 << 0, + PlacementY = 1 << 1, + AdvanceX = 1 << 2, + AdvanceY = 1 << 3, + PlacementDeviceOffsetX = 1 << 4, + PlacementDeviceOffsetY = 1 << 5, + AdvanceDeviceOffsetX = 1 << 6, + AdvanceDeviceOffsetY = 1 << 7, + + ValidBits = 0 + | PlacementX | PlacementY + | AdvanceX | AdvanceY + | PlacementDeviceOffsetX | PlacementDeviceOffsetY + | AdvanceDeviceOffsetX | AdvanceDeviceOffsetY, + } + + private static int NumBytes(this ValueFormat value) => + ushort.PopCount((ushort)(value & ValueFormat.ValidBits)) * 2; + + private readonly struct Cmap + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/cmap + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6cmap.html + + public static readonly TagStruct DirectoryTableTag = new('c', 'm', 'a', 'p'); + + public readonly PointerSpan Memory; + + public Cmap(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Cmap(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort RecordCount => this.Memory.ReadU16Big(2); + + public BigEndianPointerSpan Records => new( + this.Memory[4..].As(this.RecordCount), + EncodingRecord.ReverseEndianness); + + public EncodingRecord? UnicodeEncodingRecord => + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Bmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.Unicode_2_0_Full }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Unicode, UnicodeEncoding: UnicodeEncodingId.UnicodeFullRepertoire }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeBmp }) + ?? + this.Records.Select(x => (EncodingRecord?)x).FirstOrDefault( + x => x!.Value.PlatformAndEncoding is + { Platform: PlatformId.Windows, WindowsEncoding: WindowsEncodingId.UnicodeFullRepertoire }); + + public CmapFormat? UnicodeTable => this.GetTable(this.UnicodeEncodingRecord); + + public CmapFormat? GetTable(EncodingRecord? encodingRecord) => + encodingRecord is { } record + ? this.Memory.ReadU16Big(record.SubtableOffset) switch + { + 0 => new CmapFormat0(this.Memory[record.SubtableOffset..]), + 2 => new CmapFormat2(this.Memory[record.SubtableOffset..]), + 4 => new CmapFormat4(this.Memory[record.SubtableOffset..]), + 6 => new CmapFormat6(this.Memory[record.SubtableOffset..]), + 8 => new CmapFormat8(this.Memory[record.SubtableOffset..]), + 10 => new CmapFormat10(this.Memory[record.SubtableOffset..]), + 12 or 13 => new CmapFormat12And13(this.Memory[record.SubtableOffset..]), + _ => null, + } + : null; + + public struct EncodingRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public int SubtableOffset; + + public EncodingRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.SubtableOffset); + } + + public static EncodingRecord ReverseEndianness(EncodingRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + SubtableOffset = BinaryPrimitives.ReverseEndianness(value.SubtableOffset), + }; + } + + public struct MapGroup : IComparable + { + public int StartCharCode; + public int EndCharCode; + public int GlyphId; + + public MapGroup(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.StartCharCode); + span.ReadBig(ref offset, out this.EndCharCode); + span.ReadBig(ref offset, out this.GlyphId); + } + + public static MapGroup ReverseEndianness(MapGroup obj) => new() + { + StartCharCode = BinaryPrimitives.ReverseEndianness(obj.StartCharCode), + EndCharCode = BinaryPrimitives.ReverseEndianness(obj.EndCharCode), + GlyphId = BinaryPrimitives.ReverseEndianness(obj.GlyphId), + }; + + public int CompareTo(MapGroup other) + { + var endCharCodeComparison = this.EndCharCode.CompareTo(other.EndCharCode); + if (endCharCodeComparison != 0) return endCharCodeComparison; + + var startCharCodeComparison = this.StartCharCode.CompareTo(other.StartCharCode); + if (startCharCodeComparison != 0) return startCharCodeComparison; + + return this.GlyphId.CompareTo(other.GlyphId); + } + } + + public abstract class CmapFormat : IReadOnlyDictionary + { + public int Count => this.Count(x => x.Value != 0); + + public IEnumerable Keys => this.Select(x => x.Key); + + public IEnumerable Values => this.Select(x => x.Value); + + public ushort this[int key] => throw new NotImplementedException(); + + public abstract ushort CharToGlyph(int c); + + public abstract IEnumerator> GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public bool ContainsKey(int key) => this.CharToGlyph(key) != 0; + + public bool TryGetValue(int key, out ushort value) + { + value = this.CharToGlyph(key); + return value != 0; + } + } + + public class CmapFormat0 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat0(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public PointerSpan GlyphIdArray => this.Memory.Slice(6, 256); + + public override ushort CharToGlyph(int c) => c is >= 0 and < 256 ? this.GlyphIdArray[c] : (byte)0; + + public override IEnumerator> GetEnumerator() + { + for (var codepoint = 0; codepoint < 256; codepoint++) + { + if (this.GlyphIdArray[codepoint] is var glyphId and not 0) + yield return new(codepoint, glyphId); + } + } + } + + public class CmapFormat2 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat2(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan SubHeaderKeys => new( + this.Memory[6..].As(256), + BinaryPrimitives.ReverseEndianness); + + public PointerSpan Data => this.Memory[518..]; + + public bool TryGetSubHeader( + int keyIndex, out SubHeader subheader, out BigEndianPointerSpan glyphSpan) + { + if (keyIndex < 0 || keyIndex >= this.SubHeaderKeys.Count) + { + subheader = default; + glyphSpan = default; + return false; + } + + var offset = this.SubHeaderKeys[keyIndex]; + if (offset + Unsafe.SizeOf() > this.Data.Length) + { + subheader = default; + glyphSpan = default; + return false; + } + + subheader = new(this.Data[offset..]); + glyphSpan = new( + this.Data[(offset + Unsafe.SizeOf() + subheader.IdRangeOffset)..] + .As(subheader.EntryCount), + BinaryPrimitives.ReverseEndianness); + + return true; + } + + public override ushort CharToGlyph(int c) + { + if (!this.TryGetSubHeader(c >> 8, out var sh, out var glyphSpan)) + return 0; + + c = (c & 0xFF) - sh.FirstCode; + if (c > 0 || c >= glyphSpan.Count) + return 0; + + var res = glyphSpan[c]; + return res == 0 ? (ushort)0 : unchecked((ushort)(res + sh.IdDelta)); + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.SubHeaderKeys.Count; i++) + { + if (!this.TryGetSubHeader(i, out var sh, out var glyphSpan)) + continue; + + for (var j = 0; j < glyphSpan.Count; j++) + { + var res = glyphSpan[j]; + if (res == 0) + continue; + + var glyphId = unchecked((ushort)(res + sh.IdDelta)); + if (glyphId == 0) + continue; + + var codepoint = (i << 8) | (sh.FirstCode + j); + yield return new(codepoint, glyphId); + } + } + } + + public struct SubHeader + { + public ushort FirstCode; + public ushort EntryCount; + public ushort IdDelta; + public ushort IdRangeOffset; + + public SubHeader(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.FirstCode); + span.ReadBig(ref offset, out this.EntryCount); + span.ReadBig(ref offset, out this.IdDelta); + span.ReadBig(ref offset, out this.IdRangeOffset); + } + } + } + + public class CmapFormat4 : CmapFormat + { + public const int EndCodesOffset = 14; + + public readonly PointerSpan Memory; + + public CmapFormat4(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort SegCountX2 => this.Memory.ReadU16Big(6); + + public ushort SearchRange => this.Memory.ReadU16Big(8); + + public ushort EntrySelector => this.Memory.ReadU16Big(10); + + public ushort RangeShift => this.Memory.ReadU16Big(12); + + public BigEndianPointerSpan EndCodes => new( + this.Memory.Slice(EndCodesOffset, this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan StartCodes => new( + this.Memory.Slice(EndCodesOffset + 2 + (1 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdDeltas => new( + this.Memory.Slice(EndCodesOffset + 2 + (2 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan IdRangeOffsets => new( + this.Memory.Slice(EndCodesOffset + 2 + (3 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory.Slice(EndCodesOffset + 2 + (4 * this.SegCountX2), this.SegCountX2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c is < 0 or >= 0x10000) + return 0; + + var i = this.EndCodes.BinarySearch((ushort)c); + if (i < 0) + return 0; + + var startCode = this.StartCodes[i]; + var endCode = this.EndCodes[i]; + if (c < startCode || c > endCode) + return 0; + + var idRangeOffset = this.IdRangeOffsets[i]; + var idDelta = this.IdDeltas[i]; + if (idRangeOffset == 0) + return unchecked((ushort)(c + idDelta)); + + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr > this.Memory.Length) + return 0; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + var glyph = glyphs[c - startCode]; + return unchecked(glyph == 0 ? (ushort)0 : (ushort)(idDelta + glyph)); + } + + public override IEnumerator> GetEnumerator() + { + var startCodes = this.StartCodes; + var endCodes = this.EndCodes; + var idDeltas = this.IdDeltas; + var idRangeOffsets = this.IdRangeOffsets; + + for (var i = 0; i < this.SegCountX2 / 2; i++) + { + var startCode = startCodes[i]; + var endCode = endCodes[i]; + var idRangeOffset = idRangeOffsets[i]; + var idDelta = idDeltas[i]; + + if (idRangeOffset == 0) + { + for (var c = (int)startCode; c <= endCode; c++) + yield return new(c, (ushort)(c + idDelta)); + } + else + { + var ptr = EndCodesOffset + 2 + (3 * this.SegCountX2) + i * 2 + idRangeOffset; + if (ptr >= this.Memory.Length) + continue; + + var glyphs = new BigEndianPointerSpan( + this.Memory[ptr..].As(endCode - startCode + 1), + BinaryPrimitives.ReverseEndianness); + + for (var j = 0; j < glyphs.Count; j++) + { + var glyphId = glyphs[j]; + if (glyphId == 0) + continue; + + glyphId += idDelta; + if (glyphId == 0) + continue; + + yield return new(startCode + j, glyphId); + } + } + } + } + } + + public class CmapFormat6 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat6(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public ushort Language => this.Memory.ReadU16Big(4); + + public ushort FirstCode => this.Memory.ReadU16Big(6); + + public ushort EntryCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan GlyphIds => new( + this.Memory[10..].As(this.EntryCount), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var glyphIds = this.GlyphIds; + if (c < this.FirstCode || c >= this.FirstCode + this.GlyphIds.Count) + return 0; + + return glyphIds[c - this.FirstCode]; + } + + public override IEnumerator> GetEnumerator() + { + var glyphIds = this.GlyphIds; + for (var i = 0; i < this.GlyphIds.Length; i++) + { + var g = glyphIds[i]; + if (g != 0) + yield return new(this.FirstCode + i, g); + } + } + } + + public class CmapFormat8 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat8(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public PointerSpan Is32 => this.Memory.Slice(12, 8192); + + public int NumGroups => this.Memory.ReadI32Big(8204); + + public BigEndianPointerSpan Groups => + new(this.Memory[8208..].As(), MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch((in MapGroup value) => c.CompareTo(value.EndCharCode)); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + return unchecked((ushort)(group.GlyphId + c - group.StartCharCode)); + } + + public override IEnumerator> GetEnumerator() + { + foreach (var group in this.Groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + } + + public class CmapFormat10 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat10(PointerSpan memory) => this.Memory = memory; + + public int Format => this.Memory.ReadI32Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int StartCharCode => this.Memory.ReadI32Big(12); + + public int NumChars => this.Memory.ReadI32Big(16); + + public BigEndianPointerSpan GlyphIdArray => new( + this.Memory.Slice(20, this.NumChars * 2).As(), + BinaryPrimitives.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + if (c < this.StartCharCode || c >= this.StartCharCode + this.GlyphIdArray.Count) + return 0; + + return this.GlyphIdArray[c]; + } + + public override IEnumerator> GetEnumerator() + { + for (var i = 0; i < this.GlyphIdArray.Count; i++) + { + var glyph = this.GlyphIdArray[i]; + if (glyph != 0) + yield return new(this.StartCharCode + i, glyph); + } + } + } + + public class CmapFormat12And13 : CmapFormat + { + public readonly PointerSpan Memory; + + public CmapFormat12And13(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public int Length => this.Memory.ReadI32Big(4); + + public int Language => this.Memory.ReadI32Big(8); + + public int NumGroups => this.Memory.ReadI32Big(12); + + public BigEndianPointerSpan Groups => new( + this.Memory[16..].As(this.NumGroups), + MapGroup.ReverseEndianness); + + public override ushort CharToGlyph(int c) + { + var groups = this.Groups; + + var i = groups.BinarySearch(new MapGroup() { EndCharCode = c }); + if (i < 0) + return 0; + + var group = groups[i]; + if (c < group.StartCharCode || c > group.EndCharCode) + return 0; + + if (this.Format == 12) + return (ushort)(group.GlyphId + c - group.StartCharCode); + else + return (ushort)group.GlyphId; + } + + public override IEnumerator> GetEnumerator() + { + var groups = this.Groups; + if (this.Format == 12) + { + foreach (var group in groups) + { + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + { + var glyphId = (ushort)(group.GlyphId + j - group.StartCharCode); + if (glyphId == 0) + continue; + + yield return new(j, glyphId); + } + } + } + else + { + foreach (var group in groups) + { + if (group.GlyphId == 0) + continue; + + for (var j = group.StartCharCode; j <= group.EndCharCode; j++) + yield return new(j, (ushort)group.GlyphId); + } + } + } + } + } + + private readonly struct Gpos + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/gpos + + public static readonly TagStruct DirectoryTableTag = new('G', 'P', 'O', 'S'); + + public readonly PointerSpan Memory; + + public Gpos(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Gpos(PointerSpan memory) => this.Memory = memory; + + public Fixed Version => new(this.Memory); + + public ushort ScriptListOffset => this.Memory.ReadU16Big(4); + + public ushort FeatureListOffset => this.Memory.ReadU16Big(6); + + public ushort LookupListOffset => this.Memory.ReadU16Big(8); + + public uint FeatureVariationsOffset => this.Version.CompareTo(new(1, 1)) >= 0 + ? this.Memory.ReadU32Big(10) + : 0; + + public BigEndianPointerSpan LookupOffsetList => new( + this.Memory[(this.LookupListOffset + 2)..].As( + this.Memory.ReadU16Big(this.LookupListOffset)), + BinaryPrimitives.ReverseEndianness); + + public IEnumerable EnumerateLookupTables() + { + foreach (var offset in this.LookupOffsetList) + yield return new(this.Memory[(this.LookupListOffset + offset)..]); + } + + public IEnumerable ExtractAdvanceX() => + this.EnumerateLookupTables() + .SelectMany( + lookupTable => lookupTable.Type switch + { + LookupType.PairAdjustment => + lookupTable.SelectMany(y => new PairAdjustmentPositioning(y).ExtractAdvanceX()), + LookupType.ExtensionPositioning => + lookupTable + .Where(y => y.ReadU16Big(0) == 1) + .Select(y => new ExtensionPositioningSubtableFormat1(y)) + .Where(y => y.ExtensionLookupType == LookupType.PairAdjustment) + .SelectMany(y => new PairAdjustmentPositioning(y.ExtensionData).ExtractAdvanceX()), + _ => Array.Empty(), + }); + + public struct ValueRecord + { + public short PlacementX; + public short PlacementY; + public short AdvanceX; + public short AdvanceY; + public short PlacementDeviceOffsetX; + public short PlacementDeviceOffsetY; + public short AdvanceDeviceOffsetX; + public short AdvanceDeviceOffsetY; + + public ValueRecord(PointerSpan pointerSpan, ValueFormat valueFormat) + { + var offset = 0; + if ((valueFormat & ValueFormat.PlacementX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementX); + + if ((valueFormat & ValueFormat.PlacementY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementY); + + if ((valueFormat & ValueFormat.AdvanceX) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceX); + if ((valueFormat & ValueFormat.AdvanceY) != 0) pointerSpan.ReadBig(ref offset, out this.AdvanceY); + if ((valueFormat & ValueFormat.PlacementDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetX); + + if ((valueFormat & ValueFormat.PlacementDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.PlacementDeviceOffsetY); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetX) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetX); + + if ((valueFormat & ValueFormat.AdvanceDeviceOffsetY) != 0) + pointerSpan.ReadBig(ref offset, out this.AdvanceDeviceOffsetY); + } + } + + public readonly struct PairAdjustmentPositioning + { + public readonly PointerSpan Memory; + + public PairAdjustmentPositioning(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public IEnumerable ExtractAdvanceX() => this.Format switch + { + 1 => new Format1(this.Memory).ExtractAdvanceX(), + 2 => new Format2(this.Memory).ExtractAdvanceX(), + _ => Array.Empty(), + }; + + public readonly struct Format1 + { + public readonly PointerSpan Memory; + + public Format1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort PairSetCount => this.Memory.ReadU16Big(8); + + public BigEndianPointerSpan PairSetOffsets => new( + this.Memory[10..].As(this.PairSetCount), + BinaryPrimitives.ReverseEndianness); + + public CoverageTable CoverageTable => new(this.Memory[this.CoverageOffset..]); + + public PairSet this[int index] => new( + this.Memory[this.PairSetOffsets[index] ..], + this.ValueFormat1, + this.ValueFormat2); + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var coverageTable = this.CoverageTable; + switch (coverageTable.Format) + { + case CoverageTable.CoverageFormat.Glyphs: + { + var glyphSpan = coverageTable.Glyphs; + foreach (var coverageIndex in Enumerable.Range(0, glyphSpan.Count)) + { + var glyph1Id = glyphSpan[coverageIndex]; + PairSet pairSetView; + try + { + pairSetView = this[coverageIndex]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj >= 10000) + System.Diagnostics.Debugger.Break(); + + if (adj != 0) + yield return new(glyph1Id, pair.SecondGlyph, adj); + } + } + + break; + } + + case CoverageTable.CoverageFormat.RangeRecords: + { + foreach (var rangeRecord in coverageTable.RangeRecords) + { + var startGlyphId = rangeRecord.StartGlyphId; + var endGlyphId = rangeRecord.EndGlyphId; + var startCoverageIndex = rangeRecord.StartCoverageIndex; + var glyphCount = endGlyphId - startGlyphId + 1; + foreach (var glyph1Id in Enumerable.Range(startGlyphId, glyphCount)) + { + PairSet pairSetView; + try + { + pairSetView = this[startCoverageIndex + glyph1Id - startGlyphId]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + foreach (var pairIndex in Enumerable.Range(0, pairSetView.Count)) + { + var pair = pairSetView[pairIndex]; + var adj = (short)(pair.Record1.AdvanceX + pair.Record2.PlacementX); + if (adj != 0) + yield return new((ushort)glyph1Id, pair.SecondGlyph, adj); + } + } + } + + break; + } + } + } + + public readonly struct PairSet + { + public readonly PointerSpan Memory; + public readonly ValueFormat ValueFormat1; + public readonly ValueFormat ValueFormat2; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public PairSet( + PointerSpan memory, + ValueFormat valueFormat1, + ValueFormat valueFormat2) + { + this.Memory = memory; + this.ValueFormat1 = valueFormat1; + this.ValueFormat2 = valueFormat2; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = 2 + this.PairValue1Size + this.PairValue2Size; + } + + public ushort Count => this.Memory.ReadU16Big(0); + + public PairValueRecord this[int index] + { + get + { + var pvr = this.Memory.Slice(2 + (this.PairSize * index), this.PairSize); + return new() + { + SecondGlyph = pvr.ReadU16Big(0), + Record1 = new(pvr.Slice(2, this.PairValue1Size), this.ValueFormat1), + Record2 = new( + pvr.Slice(2 + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2), + }; + } + } + + public struct PairValueRecord + { + public ushort SecondGlyph; + public ValueRecord Record1; + public ValueRecord Record2; + } + } + } + + public readonly struct Format2 + { + public readonly PointerSpan Memory; + public readonly int PairValue1Size; + public readonly int PairValue2Size; + public readonly int PairSize; + + public Format2(PointerSpan memory) + { + this.Memory = memory; + this.PairValue1Size = this.ValueFormat1.NumBytes(); + this.PairValue2Size = this.ValueFormat2.NumBytes(); + this.PairSize = this.PairValue1Size + this.PairValue2Size; + } + + public ushort Format => this.Memory.ReadU16Big(0); + + public ushort CoverageOffset => this.Memory.ReadU16Big(2); + + public ValueFormat ValueFormat1 => this.Memory.ReadEnumBig(4); + + public ValueFormat ValueFormat2 => this.Memory.ReadEnumBig(6); + + public ushort ClassDef1Offset => this.Memory.ReadU16Big(8); + + public ushort ClassDef2Offset => this.Memory.ReadU16Big(10); + + public ushort Class1Count => this.Memory.ReadU16Big(12); + + public ushort Class2Count => this.Memory.ReadU16Big(14); + + public ClassDefTable ClassDefTable1 => new(this.Memory[this.ClassDef1Offset..]); + + public ClassDefTable ClassDefTable2 => new(this.Memory[this.ClassDef2Offset..]); + + public (ValueRecord Record1, ValueRecord Record2) this[(int Class1Index, int Class2Index) v] => + this[v.Class1Index, v.Class2Index]; + + public (ValueRecord Record1, ValueRecord Record2) this[int class1Index, int class2Index] + { + get + { + if (class1Index < 0 || class1Index >= this.Class1Count) + throw new IndexOutOfRangeException(); + + if (class2Index < 0 || class2Index >= this.Class2Count) + throw new IndexOutOfRangeException(); + + var offset = 16 + (this.PairSize * ((class1Index * this.Class2Count) + class2Index)); + return ( + new(this.Memory.Slice(offset, this.PairValue1Size), this.ValueFormat1), + new( + this.Memory.Slice(offset + this.PairValue1Size, this.PairValue2Size), + this.ValueFormat2)); + } + } + + public IEnumerable ExtractAdvanceX() + { + if ((this.ValueFormat1 & ValueFormat.AdvanceX) == 0 && + (this.ValueFormat2 & ValueFormat.AdvanceX) == 0) + { + yield break; + } + + var classes1 = this.ClassDefTable1.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + var classes2 = this.ClassDefTable2.Enumerate() + .GroupBy(x => x.Class, x => x.GlyphId) + .ToImmutableDictionary(x => x.Key, x => x.ToImmutableSortedSet()); + + foreach (var class1 in Enumerable.Range(0, this.Class1Count)) + { + if (!classes1.TryGetValue((ushort)class1, out var glyphs1)) + continue; + + foreach (var class2 in Enumerable.Range(0, this.Class2Count)) + { + if (!classes2.TryGetValue((ushort)class2, out var glyphs2)) + continue; + + (ValueRecord, ValueRecord) record; + try + { + record = this[class1, class2]; + } + catch (ArgumentOutOfRangeException) + { + yield break; + } + catch (IndexOutOfRangeException) + { + yield break; + } + + var val = record.Item1.AdvanceX + record.Item2.PlacementX; + if (val == 0) + continue; + + foreach (var glyph1 in glyphs1) + { + foreach (var glyph2 in glyphs2) + { + yield return new(glyph1, glyph2, (short)val); + } + } + } + } + } + } + } + + public readonly struct ExtensionPositioningSubtableFormat1 + { + public readonly PointerSpan Memory; + + public ExtensionPositioningSubtableFormat1(PointerSpan memory) => this.Memory = memory; + + public ushort Format => this.Memory.ReadU16Big(0); + + public LookupType ExtensionLookupType => this.Memory.ReadEnumBig(2); + + public int ExtensionOffset => this.Memory.ReadI32Big(4); + + public PointerSpan ExtensionData => this.Memory[this.ExtensionOffset..]; + } + } + + private readonly struct Head + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/head + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html + + public const uint MagicNumberValue = 0x5F0F3CF5; + public static readonly TagStruct DirectoryTableTag = new('h', 'e', 'a', 'd'); + + public readonly PointerSpan Memory; + + public Head(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Head(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum HeadFlags : ushort + { + BaselineForFontAtZeroY = 1 << 0, + LeftSideBearingAtZeroX = 1 << 1, + InstructionsDependOnPointSize = 1 << 2, + ForcePpemsInteger = 1 << 3, + InstructionsAlterAdvanceWidth = 1 << 4, + VerticalLayout = 1 << 5, + Reserved6 = 1 << 6, + RequiresLayoutForCorrectLinguisticRendering = 1 << 7, + IsAatFont = 1 << 8, + ContainsRtlGlyph = 1 << 9, + ContainsIndicStyleRearrangementEffects = 1 << 10, + Lossless = 1 << 11, + ProduceCompatibleMetrics = 1 << 12, + OptimizedForClearType = 1 << 13, + IsLastResortFont = 1 << 14, + Reserved15 = 1 << 15, + } + + [Flags] + public enum MacStyleFlags : ushort + { + Bold = 1 << 0, + Italic = 1 << 1, + Underline = 1 << 2, + Outline = 1 << 3, + Shadow = 1 << 4, + Condensed = 1 << 5, + Extended = 1 << 6, + } + + public Fixed Version => new(this.Memory); + + public Fixed FontRevision => new(this.Memory[4..]); + + public uint ChecksumAdjustment => this.Memory.ReadU32Big(8); + + public uint MagicNumber => this.Memory.ReadU32Big(12); + + public HeadFlags Flags => this.Memory.ReadEnumBig(16); + + public ushort UnitsPerEm => this.Memory.ReadU16Big(18); + + public ulong CreatedTimestamp => this.Memory.ReadU64Big(20); + + public ulong ModifiedTimestamp => this.Memory.ReadU64Big(28); + + public ushort MinX => this.Memory.ReadU16Big(36); + + public ushort MinY => this.Memory.ReadU16Big(38); + + public ushort MaxX => this.Memory.ReadU16Big(40); + + public ushort MaxY => this.Memory.ReadU16Big(42); + + public MacStyleFlags MacStyle => this.Memory.ReadEnumBig(44); + + public ushort LowestRecommendedPpem => this.Memory.ReadU16Big(46); + + public ushort FontDirectionHint => this.Memory.ReadU16Big(48); + + public ushort IndexToLocFormat => this.Memory.ReadU16Big(50); + + public ushort GlyphDataFormat => this.Memory.ReadU16Big(52); + } + + private readonly struct Kern + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/kern + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6kern.html + + public static readonly TagStruct DirectoryTableTag = new('k', 'e', 'r', 'n'); + + public readonly PointerSpan Memory; + + public Kern(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Kern(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public IEnumerable EnumerateHorizontalPairs() => this.Version switch + { + 0 => new Version0(this.Memory).EnumerateHorizontalPairs(), + 1 => new Version1(this.Memory).EnumerateHorizontalPairs(), + _ => Array.Empty(), + }; + + public readonly struct Format0 + { + public readonly PointerSpan Memory; + + public Format0(PointerSpan memory) => this.Memory = memory; + + public ushort PairCount => this.Memory.ReadU16Big(0); + + public ushort SearchRange => this.Memory.ReadU16Big(2); + + public ushort EntrySelector => this.Memory.ReadU16Big(4); + + public ushort RangeShift => this.Memory.ReadU16Big(6); + + public BigEndianPointerSpan Pairs => new( + this.Memory[8..].As(this.PairCount), + KerningPair.ReverseEndianness); + } + + public readonly struct Version0 + { + public readonly PointerSpan Memory; + + public Version0(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Horizontal = 1 << 0, + Minimum = 1 << 1, + CrossStream = 1 << 2, + Override = 1 << 3, + } + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort NumSubtables => this.Memory.ReadU16Big(2); + + public PointerSpan Data => this.Memory[4..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() + { + var accumulator = new Dictionary<(ushort Left, ushort Right), short>(); + foreach (var subtable in this.EnumerateSubtables()) + { + var isOverride = (subtable.Flags & CoverageFlags.Override) != 0; + var isMinimum = (subtable.Flags & CoverageFlags.Minimum) != 0; + foreach (var t in subtable.EnumeratePairs()) + { + if (isOverride) + { + accumulator[(t.Left, t.Right)] = t.Value; + } + else if (isMinimum) + { + accumulator[(t.Left, t.Right)] = Math.Max( + accumulator.GetValueOrDefault((t.Left, t.Right), t.Value), + t.Value); + } + else + { + accumulator[(t.Left, t.Right)] = (short)( + accumulator.GetValueOrDefault( + (t.Left, t.Right)) + t.Value); + } + } + } + + return accumulator.Select( + x => new KerningPair { Left = x.Key.Left, Right = x.Key.Right, Value = x.Value }); + } + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Length => this.Memory.ReadU16Big(2); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public PointerSpan Data => this.Memory[6..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + + public readonly struct Version1 + { + public readonly PointerSpan Memory; + + public Version1(PointerSpan memory) => this.Memory = memory; + + [Flags] + public enum CoverageFlags : byte + { + Vertical = 1 << 0, + CrossStream = 1 << 1, + Variation = 1 << 2, + } + + public Fixed Version => new(this.Memory); + + public int NumSubtables => this.Memory.ReadI16Big(4); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumerateSubtables() + { + var data = this.Data; + for (var i = 0; i < this.NumSubtables && !data.IsEmpty; i++) + { + var st = new Subtable(data); + data = data[st.Length..]; + yield return st; + } + } + + public IEnumerable EnumerateHorizontalPairs() => this + .EnumerateSubtables() + .Where(x => x.Flags == 0) + .SelectMany(x => x.EnumeratePairs()); + + public readonly struct Subtable + { + public readonly PointerSpan Memory; + + public Subtable(PointerSpan memory) => this.Memory = memory; + + public int Length => this.Memory.ReadI32Big(0); + + public byte Format => this.Memory[4]; + + public CoverageFlags Flags => this.Memory.ReadEnumBig(5); + + public ushort TupleIndex => this.Memory.ReadU16Big(6); + + public PointerSpan Data => this.Memory[8..]; + + public IEnumerable EnumeratePairs() => this.Format switch + { + 0 => new Format0(this.Data).Pairs, + _ => Array.Empty(), + }; + } + } + } + + private readonly struct Name + { + // https://docs.microsoft.com/en-us/typography/opentype/spec/name + // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html + + public static readonly TagStruct DirectoryTableTag = new('n', 'a', 'm', 'e'); + + public readonly PointerSpan Memory; + + public Name(SfntFile file) + : this(file[DirectoryTableTag]) + { + } + + public Name(PointerSpan memory) => this.Memory = memory; + + public ushort Version => this.Memory.ReadU16Big(0); + + public ushort Count => this.Memory.ReadU16Big(2); + + public ushort StorageOffset => this.Memory.ReadU16Big(4); + + public BigEndianPointerSpan NameRecords => new( + this.Memory[6..].As(this.Count), + NameRecord.ReverseEndianness); + + public ushort LanguageCount => + this.Version == 0 ? (ushort)0 : this.Memory.ReadU16Big(6 + this.NameRecords.ByteCount); + + public BigEndianPointerSpan LanguageRecords => this.Version == 0 + ? default + : new( + this.Memory[ + (8 + this.NameRecords + .ByteCount)..] + .As( + this.LanguageCount), + LanguageRecord.ReverseEndianness); + + public PointerSpan Storage => this.Memory[this.StorageOffset..]; + + public string this[in NameRecord record] => + record.PlatformAndEncoding.Decode(this.Storage.Span.Slice(record.StringOffset, record.Length)); + + public string this[in LanguageRecord record] => + Encoding.ASCII.GetString(this.Storage.Span.Slice(record.LanguageTagOffset, record.Length)); + + public struct NameRecord + { + public PlatformAndEncoding PlatformAndEncoding; + public ushort LanguageId; + public NameId NameId; + public ushort Length; + public ushort StringOffset; + + public NameRecord(PointerSpan span) + { + this.PlatformAndEncoding = new(span); + var offset = Unsafe.SizeOf(); + span.ReadBig(ref offset, out this.LanguageId); + span.ReadBig(ref offset, out this.NameId); + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.StringOffset); + } + + public static NameRecord ReverseEndianness(NameRecord value) => new() + { + PlatformAndEncoding = PlatformAndEncoding.ReverseEndianness(value.PlatformAndEncoding), + LanguageId = BinaryPrimitives.ReverseEndianness(value.LanguageId), + NameId = (NameId)BinaryPrimitives.ReverseEndianness((ushort)value.NameId), + Length = BinaryPrimitives.ReverseEndianness(value.Length), + StringOffset = BinaryPrimitives.ReverseEndianness(value.StringOffset), + }; + } + + public struct LanguageRecord + { + public ushort Length; + public ushort LanguageTagOffset; + + public LanguageRecord(PointerSpan span) + { + var offset = 0; + span.ReadBig(ref offset, out this.Length); + span.ReadBig(ref offset, out this.LanguageTagOffset); + } + + public static LanguageRecord ReverseEndianness(LanguageRecord value) => new() + { + Length = BinaryPrimitives.ReverseEndianness(value.Length), + LanguageTagOffset = BinaryPrimitives.ReverseEndianness(value.LanguageTagOffset), + }; + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs new file mode 100644 index 000000000..1d437d56d --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/TrueType.cs @@ -0,0 +1,135 @@ +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Deals with TrueType. +/// +internal static partial class TrueTypeUtils +{ + /// + /// Checks whether the given will fail in , + /// and throws an appropriate exception if it is the case. + /// + /// The font config. + public static unsafe void CheckImGuiCompatibleOrThrow(in ImFontConfig fontConfig) + { + var ranges = fontConfig.GlyphRanges; + var sfnt = AsSfntFile(fontConfig); + var cmap = new Cmap(sfnt); + if (cmap.UnicodeTable is not { } unicodeTable) + throw new NotSupportedException("The font does not have a compatible Unicode character mapping table."); + if (unicodeTable.All(x => !ImGuiHelpers.IsCodepointInSuppliedGlyphRangesUnsafe(x.Key, ranges))) + throw new NotSupportedException("The font does not have any glyph that falls under the requested range."); + } + + /// + /// Enumerates through horizontal pair adjustments of a kern and gpos tables. + /// + /// The font config. + /// The enumerable of pair adjustments. Distance values need to be multiplied by font size in pixels. + public static IEnumerable<(char Left, char Right, float Distance)> ExtractHorizontalPairAdjustments( + ImFontConfig fontConfig) + { + float multiplier; + Dictionary glyphToCodepoints; + Gpos gpos = default; + Kern kern = default; + + try + { + var sfnt = AsSfntFile(fontConfig); + var head = new Head(sfnt); + multiplier = 3f / 4 / head.UnitsPerEm; + + if (new Cmap(sfnt).UnicodeTable is not { } table) + yield break; + + if (sfnt.ContainsKey(Kern.DirectoryTableTag)) + kern = new(sfnt); + else if (sfnt.ContainsKey(Gpos.DirectoryTableTag)) + gpos = new(sfnt); + else + yield break; + + glyphToCodepoints = table + .GroupBy(x => x.Value, x => x.Key) + .OrderBy(x => x.Key) + .ToDictionary( + x => x.Key, + x => x.Where(y => y <= ushort.MaxValue) + .Select(y => (char)y) + .ToArray()); + } + catch + { + // don't care; give up + yield break; + } + + if (kern.Memory.Count != 0) + { + foreach (var pair in kern.EnumerateHorizontalPairs()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + else if (gpos.Memory.Count != 0) + { + foreach (var pair in gpos.ExtractAdvanceX()) + { + if (!glyphToCodepoints.TryGetValue(pair.Left, out var leftChars)) + continue; + if (!glyphToCodepoints.TryGetValue(pair.Right, out var rightChars)) + continue; + + foreach (var l in leftChars) + { + foreach (var r in rightChars) + yield return (l, r, pair.Value * multiplier); + } + } + } + } + + private static unsafe SfntFile AsSfntFile(in ImFontConfig fontConfig) + { + var memory = new PointerSpan((byte*)fontConfig.FontData, fontConfig.FontDataSize); + if (memory.Length < 4) + throw new NotSupportedException("File is too short to even have a magic."); + + var magic = memory.ReadU32Big(0); + if (BitConverter.IsLittleEndian) + magic = BinaryPrimitives.ReverseEndianness(magic); + + if (magic == SfntFile.FileTagTrueType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagType1.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenTypeWithCff.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagOpenType1_0.NativeValue) + return new(memory); + if (magic == SfntFile.FileTagTrueTypeApple.NativeValue) + return new(memory); + if (magic == TtcFile.FileTag.NativeValue) + return new TtcFile(memory)[fontConfig.FontNo]; + + throw new NotSupportedException($"The given file with the magic 0x{magic:X08} is not supported."); + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs new file mode 100644 index 000000000..cb7f7c65a --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -0,0 +1,306 @@ +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Text; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Managed version of , to avoid unnecessary heap allocation and use of unsafe blocks. +/// +public struct SafeFontConfig +{ + /// + /// The raw config. + /// + public ImFontConfig Raw; + + /// + /// Initializes a new instance of the struct. + /// + public SafeFontConfig() + { + this.OversampleH = 1; + this.OversampleV = 1; + this.PixelSnapH = true; + this.GlyphMaxAdvanceX = float.MaxValue; + this.RasterizerMultiply = 1f; + this.RasterizerGamma = 1.4f; + this.EllipsisChar = unchecked((char)-1); + this.Raw.FontDataOwnedByAtlas = 1; + } + + /// + /// Initializes a new instance of the struct, + /// copying applicable values from an existing instance of . + /// + /// Config to copy from. + public unsafe SafeFontConfig(ImFontConfigPtr config) + : this() + { + if (config.NativePtr is not null) + { + this.Raw = *config.NativePtr; + this.Raw.GlyphRanges = null; + } + } + + /// + /// Gets or sets the index of font within a TTF/OTF file. + /// + public int FontNo + { + get => this.Raw.FontNo; + set => this.Raw.FontNo = EnsureRange(value, 0, int.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in pixels.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePx + { + get => this.Raw.SizePixels; + set => this.Raw.SizePixels = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the desired size of the new font, in points.
+ /// Effectively, this is the line height.
+ /// Value is tied with . + ///
+ public float SizePt + { + get => (this.Raw.SizePixels * 3) / 4; + set => this.Raw.SizePixels = EnsureRange((value * 4) / 3, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the horizontal oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// Note the difference between 2 and 3 is minimal so you can reduce this to 2 to save memory.
+ /// Read https://github.com/nothings/stb/blob/master/tests/oversample/README.md for details. + ///
+ public int OversampleH + { + get => this.Raw.OversampleH; + set => this.Raw.OversampleH = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets the vertical oversampling pixel count.
+ /// Rasterize at higher quality for sub-pixel positioning.
+ /// This is not really useful as we don't use sub-pixel positions on the Y axis. + ///
+ public int OversampleV + { + get => this.Raw.OversampleV; + set => this.Raw.OversampleV = EnsureRange(value, 1, int.MaxValue); + } + + /// + /// Gets or sets a value indicating whether to align every glyph to pixel boundary.
+ /// Useful e.g. if you are merging a non-pixel aligned font with the default font.
+ /// If enabled, you can set and to 1. + ///
+ public bool PixelSnapH + { + get => this.Raw.PixelSnapH != 0; + set => this.Raw.PixelSnapH = value ? (byte)1 : (byte)0; + } + + /// + /// Gets or sets the extra spacing (in pixels) between glyphs.
+ /// Only X axis is supported for now.
+ /// Effectively, it is the letter spacing. + ///
+ public Vector2 GlyphExtraSpacing + { + get => this.Raw.GlyphExtraSpacing; + set => this.Raw.GlyphExtraSpacing = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the offset all glyphs from this font input.
+ /// Use this to offset fonts vertically when merging multiple fonts. + ///
+ public Vector2 GlyphOffset + { + get => this.Raw.GlyphOffset; + set => this.Raw.GlyphOffset = new( + EnsureRange(value.X, float.MinValue, float.MaxValue), + EnsureRange(value.Y, float.MinValue, float.MaxValue)); + } + + /// + /// Gets or sets the glyph ranges, which is a user-provided list of Unicode range. + /// Each range has 2 values, and values are inclusive.
+ /// The list must be zero-terminated.
+ /// If empty or null, then all the glyphs from the font that is in the range of UCS-2 will be added. + ///
+ public ushort[]? GlyphRanges { get; set; } + + /// + /// Gets or sets the minimum AdvanceX for glyphs.
+ /// Set only to align font icons.
+ /// Set both / to enforce mono-space font. + ///
+ public float GlyphMinAdvanceX + { + get => this.Raw.GlyphMinAdvanceX; + set => this.Raw.GlyphMinAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets the maximum AdvanceX for glyphs. + /// + public float GlyphMaxAdvanceX + { + get => this.Raw.GlyphMaxAdvanceX; + set => this.Raw.GlyphMaxAdvanceX = + float.IsFinite(value) + ? value + : throw new ArgumentOutOfRangeException( + nameof(value), + value, + $"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + } + + /// + /// Gets or sets a value that either brightens (>1.0f) or darkens (<1.0f) the font output.
+ /// Brightening small fonts may be a good workaround to make them more readable. + ///
+ public float RasterizerMultiply + { + get => this.Raw.RasterizerMultiply; + set => this.Raw.RasterizerMultiply = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets the gamma value for fonts. + /// + public float RasterizerGamma + { + get => this.Raw.RasterizerGamma; + set => this.Raw.RasterizerGamma = EnsureRange(value, float.Epsilon, float.MaxValue); + } + + /// + /// Gets or sets a value explicitly specifying unicode codepoint of the ellipsis character.
+ /// When fonts are being merged first specified ellipsis will be used. + ///
+ public char EllipsisChar + { + get => (char)this.Raw.EllipsisChar; + set => this.Raw.EllipsisChar = value; + } + + /// + /// Gets or sets the desired name of the new font. Names longer than 40 bytes will be partially lost. + /// + public unsafe string Name + { + get + { + fixed (void* pName = this.Raw.Name) + { + var span = new ReadOnlySpan(pName, 40); + var firstNull = span.IndexOf((byte)0); + if (firstNull != -1) + span = span[..firstNull]; + return Encoding.UTF8.GetString(span); + } + } + + set + { + fixed (void* pName = this.Raw.Name) + { + var span = new Span(pName, 40); + Encoding.UTF8.GetBytes(value, span); + } + } + } + + /// + /// Gets or sets the desired font to merge with, if set. + /// + public unsafe ImFontPtr MergeFont + { + get => this.Raw.DstFont is not null ? this.Raw.DstFont : default; + set + { + this.Raw.MergeMode = value.NativePtr is null ? (byte)0 : (byte)1; + this.Raw.DstFont = value.NativePtr is null ? default : value.NativePtr; + } + } + + /// + /// Throws with appropriate messages, + /// if this has invalid values. + /// + public readonly void ThrowOnInvalidValues() + { + if (!(this.Raw.FontNo >= 0)) + throw new ArgumentException($"{nameof(this.FontNo)} must not be a negative number."); + + if (!(this.Raw.SizePixels > 0)) + throw new ArgumentException($"{nameof(this.SizePx)} must be a positive number."); + + if (!(this.Raw.OversampleH >= 1)) + throw new ArgumentException($"{nameof(this.OversampleH)} must be a negative number."); + + if (!(this.Raw.OversampleV >= 1)) + throw new ArgumentException($"{nameof(this.OversampleV)} must be a negative number."); + + if (!float.IsFinite(this.Raw.GlyphMinAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMinAdvanceX)} must be a finite number."); + + if (!float.IsFinite(this.Raw.GlyphMaxAdvanceX)) + throw new ArgumentException($"{nameof(this.GlyphMaxAdvanceX)} must be a finite number."); + + if (!(this.Raw.RasterizerMultiply > 0)) + throw new ArgumentException($"{nameof(this.RasterizerMultiply)} must be a positive number."); + + if (!(this.Raw.RasterizerGamma > 0)) + throw new ArgumentException($"{nameof(this.RasterizerGamma)} must be a positive number."); + + if (this.GlyphRanges is { Length: > 0 } ranges) + { + if (ranges[0] == 0) + { + throw new ArgumentException( + "Font ranges cannot start with 0.", + nameof(this.GlyphRanges)); + } + + if (ranges[(ranges.Length - 1) & ~1] != 0) + { + throw new ArgumentException( + "Font ranges must terminate with a zero at even indices.", + nameof(this.GlyphRanges)); + } + } + } + + private static T EnsureRange(T value, T min, T max, [CallerMemberName] string callerName = "") + where T : INumber + { + if (value < min) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be less than {min}."); + if (value > max) + throw new ArgumentOutOfRangeException(callerName, value, $"{callerName} cannot be more than {max}."); + + return value; + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index dd2e5bad3..a477ec09e 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -12,6 +11,8 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -30,11 +31,13 @@ public sealed class UiBuilder : IDisposable private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); - private readonly GameFontManager gameFontManager = Service.Get(); + private readonly Framework framework = Service.Get(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -45,14 +48,32 @@ public sealed class UiBuilder : IDisposable /// The plugin namespace. internal UiBuilder(string namespaceName) { - this.stopwatch = new Stopwatch(); - this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); - this.namespaceName = namespaceName; + try + { + this.stopwatch = new Stopwatch(); + this.hitchDetector = new HitchDetector($"UiBuilder({namespaceName})", this.configuration.UiBuilderHitch); + this.namespaceName = namespaceName; - this.interfaceManager.Draw += this.OnDraw; - this.interfaceManager.BuildFonts += this.OnBuildFonts; - this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; - this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.interfaceManager.Draw += this.OnDraw; + this.scopedFinalizer.Add(() => this.interfaceManager.Draw -= this.OnDraw); + + this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.scopedFinalizer.Add(() => this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers); + + this.FontAtlas = + this.scopedFinalizer + .Add( + Service + .Get() + .CreateFontAtlas(namespaceName, FontAtlasAutoRebuildMode.Disable)); + this.FontAtlas.BuildStepChange += this.PrivateAtlasOnBuildStepChange; + this.FontAtlas.RebuildRecommend += this.RebuildFonts; + } + catch + { + this.scopedFinalizer.Dispose(); + throw; + } } /// @@ -80,19 +101,19 @@ public sealed class UiBuilder : IDisposable /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action BuildFonts; + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + public event Action? BuildFonts; /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
/// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those - /// pointers inside this handler.
- /// PLEASE remove this handler inside Dispose, or when you no longer need your fonts! + /// pointers inside this handler. ///
- public event Action AfterBuildFonts; + [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + public event Action? AfterBuildFonts; /// /// Gets or sets an action that is called when plugin UI or interface modifications are supposed to be shown. @@ -107,18 +128,57 @@ public sealed class UiBuilder : IDisposable public event Action HideUi; /// - /// Gets the default Dalamud font based on Noto Sans CJK Medium in 17pt - supporting all game languages and icons. + /// Gets the default Dalamud font size in points. /// + public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt; + + /// + /// Gets the default Dalamud font size in pixels. + /// + public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx; + + /// + /// Gets the default Dalamud font - supporting all game languages and icons.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// fontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); + /// + /// public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid in 17pt. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// fontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr IconFont => InterfaceManager.IconFont; /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular in 16pt. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
+ /// Accessing this static property outside of is dangerous and not supported. ///
+ /// + /// A font handle corresponding to this font can be obtained with: + /// + /// fontAtlas.NewDelegateFontHandle( + /// e => e.OnPreBuild( + /// tk => tk.AddDalamudAssetFont( + /// DalamudAsset.InconsolataRegular, + /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// + /// public static ImFontPtr MonoFont => InterfaceManager.MonoFont; /// @@ -190,6 +250,11 @@ public sealed class UiBuilder : IDisposable /// public bool UiPrepared => Service.GetNullable() != null; + /// + /// Gets the plugin-private font atlas. + /// + public IFontAtlas FontAtlas { get; } + /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. /// @@ -319,7 +384,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -341,7 +406,7 @@ public sealed class UiBuilder : IDisposable if (runInFrameworkThread) { return this.InterfaceManagerWithSceneAsync - .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .ContinueWith(_ => this.framework.RunOnFrameworkThread(func)) .Unwrap(); } else @@ -357,19 +422,49 @@ public sealed class UiBuilder : IDisposable ///
/// Font to get. /// Handle to the game font which may or may not be available for use yet. - public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style); + [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] + public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( + (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), + Service.Get()); /// /// Call this to queue a rebuild of the font atlas.
- /// This will invoke any handlers and ensure that any loaded fonts are - /// ready to be used on the next UI frame. + /// This will invoke any and handlers and ensure that any + /// loaded fonts are ready to be used on the next UI frame. ///
public void RebuildFonts() { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); - this.interfaceManager.RebuildFonts(); + if (this.AfterBuildFonts is null && this.BuildFonts is null) + this.FontAtlas.BuildFontsAsync(); + else + this.FontAtlas.BuildFontsOnNextFrame(); } + /// + /// Creates an isolated . + /// + /// Specify when and how to rebuild this atlas. + /// Whether the fonts in the atlas is global scaled. + /// Name for debugging purposes. + /// A new instance of . + /// + /// Use this to create extra font atlases, if you want to create and dispose fonts without having to rebuild all + /// other fonts together.
+ /// If is not , + /// the font rebuilding functions must be called manually. + ///
+ public IFontAtlas CreateFontAtlas( + FontAtlasAutoRebuildMode autoRebuildMode, + bool isGlobalScaled = true, + string? debugName = null) => + this.scopedFinalizer.Add(Service + .Get() + .CreateFontAtlas( + this.namespaceName + ":" + (debugName ?? "custom"), + autoRebuildMode, + isGlobalScaled)); + /// /// Add a notification to the notification queue. /// @@ -392,12 +487,7 @@ public sealed class UiBuilder : IDisposable /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() - { - this.interfaceManager.Draw -= this.OnDraw; - this.interfaceManager.BuildFonts -= this.OnBuildFonts; - this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; - } + void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); /// /// Open the registered configuration UI, if it exists. @@ -463,8 +553,12 @@ public sealed class UiBuilder : IDisposable this.ShowUi?.InvokeSafely(); } - if (!this.interfaceManager.FontsReady) + // just in case, if something goes wrong, prevent drawing; otherwise it probably will crash. + if (!this.FontAtlas.BuildTask.IsCompletedSuccessfully + && (this.BuildFonts is not null || this.AfterBuildFonts is not null)) + { return; + } ImGui.PushID(this.namespaceName); if (DoStats) @@ -526,14 +620,28 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } - private void OnBuildFonts() + private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) { - this.BuildFonts?.InvokeSafely(); - } + if (e.IsAsyncBuildOperation) + return; - private void OnAfterBuildFonts() - { - this.AfterBuildFonts?.InvokeSafely(); + e.OnPreBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.BuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); + + e.OnPostBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + this.AfterBuildFonts?.InvokeSafely(); + ImGui.GetIO().NativePtr->Fonts = prev; + }); } private void OnResizeBuffers() diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index ad151ec4e..444463d41 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,10 +1,15 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Numerics; +using System.Reactive.Disposables; using System.Runtime.InteropServices; +using System.Text.Unicode; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility.Raii; using ImGuiNET; using ImGuiScene; @@ -31,8 +36,7 @@ public static class ImGuiHelpers /// This does not necessarily mean you can call drawing functions. /// public static unsafe bool IsImGuiInitialized => - ImGui.GetCurrentContext() is not (nint)0 // KW: IDEs get mad without the cast, despite being unnecessary - && ImGui.GetIO().NativePtr is not null; + ImGui.GetCurrentContext() != nint.Zero && ImGui.GetIO().NativePtr is not null; ///
/// Gets the global Dalamud scale; even available before drawing is ready.
@@ -198,7 +202,7 @@ public static class ImGuiHelpers /// If a positive number is given, numbers will be rounded to this. public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) { - Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; + Func rounder = round > 0 ? x => MathF.Round(x / round) * round : x => x; var font = fontPtr.NativePtr; font->FontSize = rounder(font->FontSize * scale); @@ -310,6 +314,7 @@ public static class ImGuiHelpers glyph->U1, glyph->V1, glyph->AdvanceX * scale); + target.Mark4KPageUsedAfterGlyphAdd((ushort)glyph->Codepoint); changed = true; } else if (!missingOnly) @@ -343,25 +348,18 @@ public static class ImGuiHelpers } if (changed && rebuildLookupTable) - target.BuildLookupTableNonstandard(); - } + { + // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. + // FallbackGlyph is resolved after resolving ' '. + // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, + // making FindGlyph return nullptr. + // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, + // making ImGui attempt to treat whatever was there as a ' '. + // This may cause random glyphs to be sized randomly, if not an access violation exception. + target.NativePtr->FallbackGlyph = null; - /// - /// Call ImFont::BuildLookupTable, after attempting to fulfill some preconditions. - /// - /// The font. - public static unsafe void BuildLookupTableNonstandard(this ImFontPtr font) - { - // ImGui resolves ' ' with FindGlyph, which uses FallbackGlyph. - // FallbackGlyph is resolved after resolving ' '. - // On the first call of BuildLookupTable, called from BuildFonts, FallbackGlyph is set to null, - // making FindGlyph return nullptr. - // On our secondary calls of BuildLookupTable, FallbackGlyph is set to some value that is not null, - // making ImGui attempt to treat whatever was there as a ' '. - // This may cause random glyphs to be sized randomly, if not an access violation exception. - font.NativePtr->FallbackGlyph = null; - - font.BuildLookupTable(); + target.BuildLookupTable(); + } } /// @@ -407,6 +405,103 @@ public static class ImGuiHelpers public static void CenterCursorFor(float itemWidth) => ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + /// + /// Allocates memory on the heap using
+ /// Memory must be freed using . + ///
+ /// Note that null is a valid return value when is 0. + ///
+ /// The length of allocated memory. + /// The allocated memory. + /// If returns null. + public static unsafe void* AllocateMemory(int length) + { + // TODO: igMemAlloc takes size_t, which is nint; ImGui.NET apparently interpreted that as uint. + // fix that in ImGui.NET. + switch (length) + { + case 0: + return null; + case < 0: + throw new ArgumentOutOfRangeException( + nameof(length), + length, + $"{nameof(length)} cannot be a negative number."); + default: + var memory = ImGuiNative.igMemAlloc((uint)length); + if (memory is null) + { + throw new OutOfMemoryException( + $"Failed to allocate {length} bytes using {nameof(ImGuiNative.igMemAlloc)}"); + } + + return memory; + } + } + + /// + /// Creates a new instance of with a natively backed memory. + /// + /// The created instance. + /// Disposable you can call. + public static unsafe IDisposable NewFontGlyphRangeBuilderPtrScoped(out ImFontGlyphRangesBuilderPtr builder) + { + builder = new(ImGuiNative.ImFontGlyphRangesBuilder_ImFontGlyphRangesBuilder()); + var ptr = builder.NativePtr; + return Disposable.Create(() => + { + if (ptr != null) + ImGuiNative.ImFontGlyphRangesBuilder_destroy(ptr); + ptr = null; + }); + } + + /// + /// Builds ImGui Glyph Ranges for use with . + /// + /// The builder. + /// Add fallback codepoints to the range. + /// Add ellipsis codepoints to the range. + /// When disposed, the resource allocated for the range will be freed. + public static unsafe ushort[] BuildRangesToArray( + this ImFontGlyphRangesBuilderPtr builder, + bool addFallbackCodepoints = true, + bool addEllipsisCodepoints = true) + { + if (addFallbackCodepoints) + builder.AddText(FontAtlasFactory.FallbackCodepoints); + if (addEllipsisCodepoints) + { + builder.AddText(FontAtlasFactory.EllipsisCodepoints); + builder.AddChar('.'); + } + + builder.BuildRanges(out var vec); + return new ReadOnlySpan((void*)vec.Data, vec.Size).ToArray(); + } + + /// + public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges) + => CreateImGuiRangesFrom((IEnumerable)ranges); + + /// + /// Creates glyph ranges from .
+ /// Use values from . + ///
+ /// The unicode ranges. + /// The range array that can be used for . + public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => + ranges + .Where(x => x.FirstCodePoint <= ushort.MaxValue) + .SelectMany( + x => new[] + { + (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), + (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), + }) + .Append((ushort)0) + .ToArray(); + /// /// Determines whether is empty. /// @@ -415,7 +510,7 @@ public static class ImGuiHelpers public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; /// - /// Determines whether is not null and loaded. + /// Determines whether is empty. /// /// The pointer. /// Whether it is empty. @@ -427,6 +522,27 @@ public static class ImGuiHelpers /// The pointer. /// Whether it is empty. public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; + + /// + /// If is default, then returns . + /// + /// The self. + /// The other. + /// if it is not default; otherwise, . + public static unsafe ImFontPtr OrElse(this ImFontPtr self, ImFontPtr other) => + self.NativePtr is null ? other : self; + + /// + /// Mark 4K page as used, after adding a codepoint to a font. + /// + /// The font. + /// The codepoint. + internal static unsafe void Mark4KPageUsedAfterGlyphAdd(this ImFontPtr font, ushort codepoint) + { + // Mark 4K page as used + var pageIndex = unchecked((ushort)(codepoint / 4096)); + font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); + } /// /// Finds the corresponding ImGui viewport ID for the given window handle. @@ -448,6 +564,89 @@ public static class ImGuiHelpers return -1; } + /// + /// Attempts to validate that is valid. + /// + /// The font pointer. + /// The exception, if any occurred during validation. + internal static unsafe Exception? ValidateUnsafe(this ImFontPtr fontPtr) + { + try + { + var font = fontPtr.NativePtr; + if (font is null) + throw new NullReferenceException("The font is null."); + + _ = Marshal.ReadIntPtr((nint)font); + if (font->IndexedHotData.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexedHotData.Data); + if (font->FrequentKerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->FrequentKerningPairs.Data); + if (font->IndexLookup.Data != 0) + _ = Marshal.ReadIntPtr(font->IndexLookup.Data); + if (font->Glyphs.Data != 0) + _ = Marshal.ReadIntPtr(font->Glyphs.Data); + if (font->KerningPairs.Data != 0) + _ = Marshal.ReadIntPtr(font->KerningPairs.Data); + if (font->ConfigDataCount == 0 && font->ConfigData is not null) + throw new InvalidOperationException("ConfigDataCount == 0 but ConfigData is not null?"); + if (font->ConfigDataCount != 0 && font->ConfigData is null) + throw new InvalidOperationException("ConfigDataCount != 0 but ConfigData is null?"); + if (font->ConfigData is not null) + _ = Marshal.ReadIntPtr((nint)font->ConfigData); + if (font->FallbackGlyph is not null + && ((nint)font->FallbackGlyph < font->Glyphs.Data || (nint)font->FallbackGlyph >= font->Glyphs.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->FallbackHotData is not null + && ((nint)font->FallbackHotData < font->IndexedHotData.Data + || (nint)font->FallbackHotData >= font->IndexedHotData.Data)) + throw new InvalidOperationException("FallbackGlyph is not in range of Glyphs.Data"); + if (font->ContainerAtlas is not null) + _ = Marshal.ReadIntPtr((nint)font->ContainerAtlas); + } + catch (Exception e) + { + return e; + } + + return null; + } + + /// + /// Updates the fallback char of . + /// + /// The font. + /// The fallback character. + internal static unsafe void UpdateFallbackChar(this ImFontPtr font, char c) + { + font.FallbackChar = c; + font.NativePtr->FallbackHotData = + (ImFontGlyphHotData*)((ImFontGlyphHotDataReal*)font.IndexedHotData.Data + font.FallbackChar); + } + + /// + /// Determines if the supplied codepoint is inside the given range, + /// in format of . + /// + /// The codepoint. + /// The ranges. + /// Whether it is the case. + internal static unsafe bool IsCodepointInSuppliedGlyphRangesUnsafe(int codepoint, ushort* rangePtr) + { + if (codepoint is <= 0 or >= ushort.MaxValue) + return false; + + while (*rangePtr != 0) + { + var from = *rangePtr++; + var to = *rangePtr++; + if (from <= codepoint && codepoint <= to) + return true; + } + + return false; + } + /// /// Get data needed for each new frame. /// From 0c2a722f83e77e58af3f5cb2b828b8c7aa7f8858 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 19 Jan 2024 08:11:33 +0900 Subject: [PATCH 437/585] Fallback behavior for bad use of DstFont values --- .../FontAtlasFactory.Implementation.cs | 64 ++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 5656fc673..eddccfa76 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -209,7 +209,7 @@ internal sealed partial class FontAtlasFactory private int buildIndex; private bool buildQueued; - private bool disposed = false; + private bool disposed; /// /// Initializes a new instance of the class. @@ -612,6 +612,7 @@ internal sealed partial class FontAtlasFactory var res = default(FontAtlasBuiltData); nint atlasPtr = 0; + BuildToolkit? toolkit = null; try { res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); @@ -627,11 +628,44 @@ internal sealed partial class FontAtlasFactory atlasPtr, sw.ElapsedMilliseconds); - using var toolkit = res.CreateToolkit(this.factory, isAsync); + toolkit = res.CreateToolkit(this.factory, isAsync); this.BuildStepChange?.Invoke(toolkit); toolkit.PreBuildSubstances(); toolkit.PreBuild(); + // Prevent NewImAtlas.ConfigData[].DstFont pointing to a font not owned by the new atlas, + // by making it add a font with default configuration first instead. + if (!ValidateMergeFontReferences(default)) + { + Log.Warning( + "[{name}:{functionname}] 0x{ptr:X}: refering to fonts outside the new atlas; " + + "adding a default font, and using that as the merge target.", + this.Name, + nameof(this.RebuildFontsPrivateReal), + atlasPtr); + + res.IsBuildInProgress = false; + toolkit.Dispose(); + res.Dispose(); + + res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + unsafe + { + atlasPtr = (nint)res.Atlas.NativePtr; + } + + toolkit = res.CreateToolkit(this.factory, isAsync); + + // PreBuildSubstances deals with toolkit.Add... function family. Do this first. + var defaultFont = toolkit.AddDalamudDefaultFont(InterfaceManager.DefaultFontSizePx, null); + + this.BuildStepChange?.Invoke(toolkit); + toolkit.PreBuildSubstances(); + toolkit.PreBuild(); + + _ = ValidateMergeFontReferences(defaultFont); + } + #if VeryVerboseLog Log.Verbose("[{name}:{functionname}] 0x{ptr:X}: Build (at {sw}ms)", this.Name, nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); #endif @@ -687,8 +721,34 @@ internal sealed partial class FontAtlasFactory } finally { + toolkit?.Dispose(); this.buildQueued = false; } + + unsafe bool ValidateMergeFontReferences(ImFontPtr replacementDstFont) + { + var correct = true; + foreach (ref var configData in toolkit.NewImAtlas.ConfigDataWrapped().DataSpan) + { + var found = false; + foreach (ref var font in toolkit.Fonts.DataSpan) + { + if (configData.DstFont == font) + { + found = true; + break; + } + } + + if (!found) + { + correct = false; + configData.DstFont = replacementDstFont; + } + } + + return correct; + } } private void OnRebuildRecommend() From ad12045c86b53f698dd611b8ff2a16198fbcdb3f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 19 Jan 2024 08:31:08 +0900 Subject: [PATCH 438/585] Fix notifications font --- Dalamud/Interface/Internal/InterfaceManager.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 3e004727a..94597f3da 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -890,12 +890,13 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.IsDispatchingEvents) { using (this.defaultFontHandle?.Push()) + { this.Draw?.Invoke(); + Service.Get().Draw(); + } } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); - - Service.Get().Draw(); } /// From b415f5a8741f6590ccc3ad64e643b17edeb283ae Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:12:32 +0100 Subject: [PATCH 439/585] never offer updates for dev plugins --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 1545efb65..b2fa50a03 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2246,6 +2246,11 @@ internal class PluginInstallerWindow : Window, IDisposable } var availablePluginUpdate = this.pluginListUpdatable.FirstOrDefault(up => up.InstalledPlugin == plugin); + + // Dev plugins can never update + if (plugin.IsDev) + availablePluginUpdate = null; + // Update available if (availablePluginUpdate != default) { From d26db7e05342b61e95419827537036c835b886d2 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:26:59 +0100 Subject: [PATCH 440/585] don't tell people to wait for an update, if one is available --- .../PluginInstaller/PluginInstallerWindow.cs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index b2fa50a03..0c5437724 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -1881,16 +1881,32 @@ internal class PluginInstallerWindow : Window, IDisposable if (plugin is { IsOutdated: true, IsBanned: false } || installableOutdated) { ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGui.TextWrapped(Locs.PluginBody_Outdated); + + var bodyText = Locs.PluginBody_Outdated + " "; + if (updateAvailable) + bodyText += Locs.PluginBody_Outdated_CanNowUpdate; + else + bodyText += Locs.PluginBody_Outdated_WaitForUpdate; + + ImGui.TextWrapped(bodyText); ImGui.PopStyleColor(); } else if (plugin is { IsBanned: true }) { // Banned warning ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); - ImGuiHelpers.SafeTextWrapped(plugin.BanReason.IsNullOrEmpty() - ? Locs.PluginBody_Banned - : Locs.PluginBody_BannedReason(plugin.BanReason)); + + var bodyText = plugin.BanReason.IsNullOrEmpty() + ? Locs.PluginBody_Banned + : Locs.PluginBody_BannedReason(plugin.BanReason); + bodyText += " "; + + if (updateAvailable) + bodyText += Locs.PluginBody_Outdated_CanNowUpdate; + else + bodyText += Locs.PluginBody_Outdated_WaitForUpdate; + + ImGuiHelpers.SafeTextWrapped(bodyText); ImGui.PopStyleColor(); } @@ -3497,7 +3513,11 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginBody_Plugin3rdPartyRepo(string url) => Loc.Localize("InstallerPlugin3rdPartyRepo", "From custom plugin repository {0}").Format(url); - public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible at the moment. Please wait for it to be updated by its author."); + public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible."); + + public static string PluginBody_Outdated_WaitForUpdate => Loc.Localize("InstallerOutdatedWaitForUpdate", "Please wait for it to be updated by its author."); + + public static string PluginBody_Outdated_CanNowUpdate => Loc.Localize("InstallerOutdatedCanNowUpdate", "An update is available for installation."); public static string PluginBody_Orphaned => Loc.Localize("InstallerOrphanedPluginBody ", "This plugin's source repository is no longer available. You may need to reinstall it from its repository, or re-add the repository."); @@ -3507,7 +3527,7 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginBody_LoadFailed => Loc.Localize("InstallerLoadFailedPluginBody ", "This plugin failed to load. Please contact the author for more information."); - public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available at the moment. Please wait for it to be updated by its author."); + public static string PluginBody_Banned => Loc.Localize("InstallerBannedPluginBody ", "This plugin was automatically disabled due to incompatibilities and is not available."); public static string PluginBody_Policy => Loc.Localize("InstallerPolicyPluginBody ", "Plugin loads for this type of plugin were manually disabled."); From 4e95d4fe37e811b031538b2469cdfb060c82e69d Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:32:39 +0100 Subject: [PATCH 441/585] allow load of devPlugins in non-default profile --- Dalamud/Plugin/Internal/PluginManager.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index c57487d1d..b0a421b0d 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1506,6 +1506,16 @@ internal partial class PluginManager : IDisposable, IServiceType { // We don't know about this plugin, so we don't want to do anything here. // The code below will take care of it and add it with the default value. + Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName); + + // If it is wanted by any other plugin, we do want to load it. This means we are looking it up twice, but I don't care right now. + // I am putting a TODO so that goat will clean it up some day soon. + if (await this.profileManager.GetWantStateAsync( + plugin.Manifest.WorkingPluginId, + plugin.Manifest.InternalName, + false, + false)) + loadPlugin = true; } else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) { @@ -1544,19 +1554,20 @@ internal partial class PluginManager : IDisposable, IServiceType #pragma warning restore CS0618 // Need to do this here, so plugins that don't load are still added to the default profile - var wantToLoad = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); - + var wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); + Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin); + if (loadPlugin) { try { - if (wantToLoad && !plugin.IsOrphaned) + if (wantedByAnyProfile && !plugin.IsOrphaned) { await plugin.LoadAsync(reason); } else { - Log.Verbose($"{name} not loaded, wantToLoad:{wantToLoad} orphaned:{plugin.IsOrphaned}"); + Log.Verbose($"{name} not loaded, wantToLoad:{wantedByAnyProfile} orphaned:{plugin.IsOrphaned}"); } } catch (InvalidPluginException) From 57b8a5d932b0449acd4fe840c6d6c601cbfcf56d Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:42:44 +0100 Subject: [PATCH 442/585] prevent double-lookup for dev plugins in non-default profiles --- Dalamud/Plugin/Internal/PluginManager.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index b0a421b0d..8bfb38c34 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1493,6 +1493,8 @@ internal partial class PluginManager : IDisposable, IServiceType if (plugin.Manifest.WorkingPluginId == Guid.Empty) throw new Exception("Plugin should have a WorkingPluginId at this point"); this.profileManager.MigrateProfilesToGuidsForPlugin(plugin.Manifest.InternalName, plugin.Manifest.WorkingPluginId); + + var wantedByAnyProfile = false; // Now, if this is a devPlugin, figure out if we want to load it if (isDev) @@ -1508,13 +1510,12 @@ internal partial class PluginManager : IDisposable, IServiceType // The code below will take care of it and add it with the default value. Log.Verbose("DevPlugin {Name} not wanted in default plugin", plugin.Manifest.InternalName); - // If it is wanted by any other plugin, we do want to load it. This means we are looking it up twice, but I don't care right now. - // I am putting a TODO so that goat will clean it up some day soon. - if (await this.profileManager.GetWantStateAsync( - plugin.Manifest.WorkingPluginId, - plugin.Manifest.InternalName, - false, - false)) + // Check if any profile wants this plugin. We need to do this here, since we want to allow loading a dev plugin if a non-default profile wants it active. + // Note that this will not add the plugin to the default profile. That's done below in any other case. + wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, false, false); + + // If it is wanted by any other profile, we do want to load it. + if (wantedByAnyProfile) loadPlugin = true; } else if (wantsInDefaultProfile == false && devPlugin.StartOnBoot) @@ -1553,8 +1554,9 @@ internal partial class PluginManager : IDisposable, IServiceType var defaultState = manifest?.Disabled != true && loadPlugin; #pragma warning restore CS0618 - // Need to do this here, so plugins that don't load are still added to the default profile - var wantedByAnyProfile = await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); + // Plugins that aren't in any profile will be added to the default profile with this call. + // We are skipping a double-lookup for dev plugins that are wanted by non-default profiles, as noted above. + wantedByAnyProfile = wantedByAnyProfile || await this.profileManager.GetWantStateAsync(plugin.Manifest.WorkingPluginId, plugin.Manifest.InternalName, defaultState); Log.Information("{Name} defaultState: {State} wantedByAnyProfile: {WantedByAny} loadPlugin: {LoadPlugin}", plugin.Manifest.InternalName, defaultState, wantedByAnyProfile, loadPlugin); if (loadPlugin) From af2f0f290f0a80926740d17cde0eaf190e8ca2c0 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 19 Jan 2024 23:43:24 +0100 Subject: [PATCH 443/585] dev plugins are now allowed to be in profiles --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0c5437724..0d1a07769 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2555,7 +2555,7 @@ internal class PluginInstallerWindow : Window, IDisposable var profileManager = Service.Get(); var config = Service.Get(); - var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev; + var applicableForProfiles = plugin.Manifest.SupportsProfiles /*&& !plugin.IsDev*/; var profilesThatWantThisPlugin = profileManager.Profiles .Where(x => x.WantsPlugin(plugin.Manifest.WorkingPluginId) != null) .ToArray(); From 4f4f604ef87bece0c4cb113ac74150fbf09e01b4 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 20 Jan 2024 01:10:07 +0100 Subject: [PATCH 444/585] show all plugins - be it dev, installed, available, orphaned - in the available tab --- .../PluginInstaller/PluginInstallerWindow.cs | 89 +++++++++++++------ .../PluginInstaller/ProfileManagerWidget.cs | 6 +- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 0d1a07769..5007691ab 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -107,6 +107,7 @@ internal class PluginInstallerWindow : Window, IDisposable private int updatePluginCount = 0; private List? updatedPlugins; + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Makes sense like this")] private List pluginListAvailable = new(); private List pluginListInstalled = new(); private List pluginListUpdatable = new(); @@ -1126,45 +1127,79 @@ internal class PluginInstallerWindow : Window, IDisposable this.DrawChangelog(logEntry); } } - + + private record PluginInstallerAvailablePluginProxy(RemotePluginManifest? RemoteManifest, LocalPlugin? LocalPlugin); + +#pragma warning disable SA1201 private void DrawAvailablePluginList() +#pragma warning restore SA1201 { - var pluginList = this.pluginListAvailable; + var availableManifests = this.pluginListAvailable; + var installedPlugins = this.pluginListInstalled.ToList(); // Copy intended - if (pluginList.Count == 0) + if (availableManifests.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey, Locs.TabBody_SearchNoCompatible); return; } - var filteredManifests = pluginList + var filteredAvailableManifests = availableManifests .Where(rm => !this.IsManifestFiltered(rm)) .ToList(); - if (filteredManifests.Count == 0) + if (filteredAvailableManifests.Count == 0) { ImGui.TextColored(ImGuiColors.DalamudGrey2, Locs.TabBody_SearchNoMatching); return; } - // get list to show and reset category dirty flag - var categoryManifestsList = this.categoryManager.GetCurrentCategoryContent(filteredManifests); + var proxies = new List(); + + // Go through all AVAILABLE manifests, associate them with a NON-DEV local plugin, if one is available, and remove it from the pile + foreach (var availableManifest in this.categoryManager.GetCurrentCategoryContent(filteredAvailableManifests).Cast()) + { + var plugin = this.pluginListInstalled.FirstOrDefault(plugin => plugin.Manifest.InternalName == availableManifest.InternalName && plugin.Manifest.RepoUrl == availableManifest.RepoUrl); + + // We "consumed" this plugin from the pile and remove it. + if (plugin != null && !plugin.IsDev) + { + installedPlugins.Remove(plugin); + proxies.Add(new PluginInstallerAvailablePluginProxy(null, plugin)); + + continue; + } + + proxies.Add(new PluginInstallerAvailablePluginProxy(availableManifest, null)); + } + + // Now, add all applicable local plugins that haven't been "used up", in most cases either dev or orphaned plugins. + foreach (var installedPlugin in installedPlugins) + { + if (this.IsManifestFiltered(installedPlugin.Manifest)) + continue; + + // TODO: We should also check categories here, for good measure + + proxies.Add(new PluginInstallerAvailablePluginProxy(null, installedPlugin)); + } var i = 0; - foreach (var manifest in categoryManifestsList) + foreach (var proxy in proxies) { - if (manifest is not RemotePluginManifest remoteManifest) - continue; - var (isInstalled, plugin) = this.IsManifestInstalled(remoteManifest); + IPluginManifest applicableManifest = proxy.LocalPlugin != null ? proxy.LocalPlugin.Manifest : proxy.RemoteManifest; - ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}"); - if (isInstalled) + if (applicableManifest == null) + throw new Exception("Could not determine manifest for available plugin"); + + ImGui.PushID($"{applicableManifest.InternalName}{applicableManifest.AssemblyVersion}"); + + if (proxy.LocalPlugin != null) { - this.DrawInstalledPlugin(plugin, i++, true); + this.DrawInstalledPlugin(proxy.LocalPlugin, i++, true); } - else + else if (proxy.RemoteManifest != null) { - this.DrawAvailablePlugin(remoteManifest, i++); + this.DrawAvailablePlugin(proxy.RemoteManifest, i++); } ImGui.PopID(); @@ -1800,14 +1835,6 @@ internal class PluginInstallerWindow : Window, IDisposable var isLoaded = plugin is { IsLoaded: true }; - if (plugin is LocalDevPlugin) - { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); - ImGui.Image(this.imageCache.DevPluginIcon.ImGuiHandle, iconSize); - ImGui.PopStyleVar(); - ImGui.SetCursorPos(cursorBeforeImage); - } - if (updateAvailable) ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); else if ((trouble && !pluginDisabled) || isOrphan) @@ -1836,8 +1863,7 @@ internal class PluginInstallerWindow : Window, IDisposable // Name ImGui.TextUnformatted(label); - // Verified Checkmark, don't show for dev plugins - if (plugin is null or { IsDev: false }) + // Verified Checkmark or dev plugin wrench { ImGui.SameLine(); ImGui.Text(" "); @@ -1847,8 +1873,15 @@ internal class PluginInstallerWindow : Window, IDisposable var unverifiedOutlineColor = KnownColor.Black.Vector(); var verifiedIconColor = KnownColor.RoyalBlue.Vector() with { W = 0.75f }; var unverifiedIconColor = KnownColor.Orange.Vector(); - - if (!isThirdParty) + var devIconOutlineColor = KnownColor.White.Vector(); + var devIconColor = KnownColor.MediumOrchid.Vector(); + + if (plugin is LocalDevPlugin) + { + this.DrawFontawesomeIconOutlined(FontAwesomeIcon.Wrench, devIconOutlineColor, devIconColor); + this.VerifiedCheckmarkFadeTooltip(label, "This is a dev plugin. You added it."); + } + else if (!isThirdParty) { this.DrawFontawesomeIconOutlined(FontAwesomeIcon.CheckCircle, verifiedOutlineColor, verifiedIconColor); this.VerifiedCheckmarkFadeTooltip(label, Locs.VerifiedCheckmark_VerifiedTooltip); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 2d45869e0..eafea9d16 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -441,17 +441,17 @@ internal class ProfileManagerWidget ImGui.Image(icon.ImGuiHandle, new Vector2(pluginLineHeight)); - if (pmPlugin is LocalDevPlugin) + if (pmPlugin.IsDev) { ImGui.SetCursorPos(cursorBeforeIcon); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.4f); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0.7f); ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); ImGui.PopStyleVar(); } ImGui.SameLine(); - var text = $"{pmPlugin.Name}"; + var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}"; var textHeight = ImGui.CalcTextSize(text); var before = ImGui.GetCursorPos(); From 1a19cbf277a3e8d2a0dc079de4c12641bb9f7da2 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 20 Jan 2024 10:21:50 +0900 Subject: [PATCH 445/585] Set ImGui default font upon default font atlas update --- Dalamud/Interface/Internal/InterfaceManager.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 94597f3da..8915b3e3d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -717,6 +717,12 @@ internal class InterfaceManager : IDisposable, IServiceType // Fill missing glyphs in MonoFont from DefaultFont tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + // Update default font + unsafe + { + ImGui.GetIO().NativePtr->FontDefault = this.defaultFontHandle.ImFont; + } + // Broadcast to auto-rebuilding instances this.AfterBuildFonts?.Invoke(); }); @@ -889,11 +895,8 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.IsDispatchingEvents) { - using (this.defaultFontHandle?.Push()) - { - this.Draw?.Invoke(); - Service.Get().Draw(); - } + this.Draw?.Invoke(); + Service.Get().Draw(); } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); From dd5cbdfd5daacadde9fbbf2b42a72af2b7c9dbcb Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 00:15:12 +0900 Subject: [PATCH 446/585] IFontAtlas API9 compat: support reading GameFontHandle.ImFont during UiBuilder.After/BuildFonts --- .../IFontAtlasBuildToolkit.cs | 16 +++++++ .../Internals/DelegateFontHandle.cs | 9 ++++ .../FontAtlasFactory.BuildToolkit.cs | 30 ++++++++++++- .../Internals/GamePrebakedFontHandle.cs | 32 ++++++++++++-- .../Internals/IFontHandleSubstance.cs | 17 ++++++- Dalamud/Interface/UiBuilder.cs | 44 ++++++++++++------- Dalamud/Logging/PluginLog.cs | 3 ++ Dalamud/Utility/Api10ToDoAttribute.cs | 19 ++++++++ 8 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 Dalamud/Utility/Api10ToDoAttribute.cs diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs index 4b016bbb2..a997c48c1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -1,6 +1,8 @@ using System.Runtime.InteropServices; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; +using Dalamud.Utility; using ImGuiNET; @@ -11,6 +13,20 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public interface IFontAtlasBuildToolkit { + /// + /// Functionalities for compatibility behavior.
+ ///
+ [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + internal interface IApi9Compat : IFontAtlasBuildToolkit + { + /// + /// Invokes , temporarily applying s.
+ ///
+ /// The action to invoke. + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public void FromUiBuilderObsoleteEventHandlers(Action action); + } + /// /// Gets or sets the font relevant to the call. /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index f0ed09155..99067a9de 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Logging.Internal; +using Dalamud.Utility; using ImGuiNET; @@ -144,6 +145,14 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public IFontHandleManager Manager { get; } + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; } + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public bool CreateFontOnAccess { get; set; } + /// public void Dispose() { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index e73ea7548..fde115c9e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -30,7 +30,7 @@ internal sealed partial class FontAtlasFactory /// Implementations for and /// . ///
- private class BuildToolkit : IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable + private class BuildToolkit : IFontAtlasBuildToolkit.IApi9Compat, IFontAtlasBuildToolkitPreBuild, IFontAtlasBuildToolkitPostBuild, IDisposable { private static readonly ushort FontAwesomeIconMin = (ushort)Enum.GetValues().Where(x => x > 0).Min(); @@ -107,6 +107,34 @@ internal sealed partial class FontAtlasFactory /// public void DisposeWithAtlas(Action action) => this.data.Garbage.Add(action); + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public void FromUiBuilderObsoleteEventHandlers(Action action) + { + var previousSubstances = new IFontHandleSubstance[this.data.Substances.Count]; + for (var i = 0; i < previousSubstances.Length; i++) + { + previousSubstances[i] = this.data.Substances[i].Manager.Substance; + this.data.Substances[i].Manager.Substance = this.data.Substances[i]; + this.data.Substances[i].CreateFontOnAccess = true; + this.data.Substances[i].PreBuildToolkitForApi9Compat = this; + } + + try + { + action(); + } + finally + { + for (var i = 0; i < previousSubstances.Length; i++) + { + this.data.Substances[i].Manager.Substance = previousSubstances[i]; + this.data.Substances[i].CreateFontOnAccess = false; + this.data.Substances[i].PreBuildToolkitForApi9Compat = null; + } + } + } + /// public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 99c817a91..2686259bc 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -230,6 +230,14 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public IFontHandleManager Manager => this.handleManager; + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; } + + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public bool CreateFontOnAccess { get; set; } + /// public void Dispose() { @@ -285,11 +293,27 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } } + // Use this on API 10. + // /// + // public ImFontPtr GetFontPtr(IFontHandle handle) => + // handle is GamePrebakedFontHandle ggfh + // ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default + // : default; + /// - public ImFontPtr GetFontPtr(IFontHandle handle) => - handle is GamePrebakedFontHandle ggfh - ? this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont ?? default - : default; + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public ImFontPtr GetFontPtr(IFontHandle handle) + { + if (handle is not GamePrebakedFontHandle ggfh) + return default; + if (this.fonts.GetValueOrDefault(ggfh.FontStyle)?.FullRangeFont is { } font) + return font; + if (!this.CreateFontOnAccess) + return default; + if (this.PreBuildToolkitForApi9Compat is not { } tk) + return default; + return this.GetOrCreateFont(ggfh.FontStyle, tk); + } /// public Exception? GetBuildException(IFontHandle handle) => diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs index f6c5c6591..c800c30ac 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -1,4 +1,6 @@ -using ImGuiNET; +using Dalamud.Utility; + +using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -12,6 +14,19 @@ internal interface IFontHandleSubstance : IDisposable ///
IFontHandleManager Manager { get; } + /// + /// Gets or sets the relevant for this. + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + IFontAtlasBuildToolkitPreBuild? PreBuildToolkitForApi9Compat { get; set; } + + /// + /// Gets or sets a value indicating whether to create a new instance of on first + /// access, for compatibility with API 9. + /// + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + bool CreateFontOnAccess { get; set; } + /// /// Gets the font. /// diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index a477ec09e..87e3b9032 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -104,6 +104,7 @@ public sealed class UiBuilder : IDisposable /// pointers inside this handler. ///
[Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? BuildFonts; /// @@ -113,6 +114,7 @@ public sealed class UiBuilder : IDisposable /// pointers inside this handler. /// [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? AfterBuildFonts; ///
@@ -423,6 +425,7 @@ public sealed class UiBuilder : IDisposable /// Font to get. /// Handle to the game font which may or may not be available for use yet. [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), Service.Get()); @@ -620,28 +623,37 @@ public sealed class UiBuilder : IDisposable this.hitchDetector.Stop(); } + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] private unsafe void PrivateAtlasOnBuildStepChange(IFontAtlasBuildToolkit e) { if (e.IsAsyncBuildOperation) return; - e.OnPreBuild( - _ => - { - var prev = ImGui.GetIO().NativePtr->Fonts; - ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; - this.BuildFonts?.InvokeSafely(); - ImGui.GetIO().NativePtr->Fonts = prev; - }); + if (this.BuildFonts is not null) + { + e.OnPreBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + ((IFontAtlasBuildToolkit.IApi9Compat)e) + .FromUiBuilderObsoleteEventHandlers(() => this.BuildFonts?.InvokeSafely()); + ImGui.GetIO().NativePtr->Fonts = prev; + }); + } - e.OnPostBuild( - _ => - { - var prev = ImGui.GetIO().NativePtr->Fonts; - ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; - this.AfterBuildFonts?.InvokeSafely(); - ImGui.GetIO().NativePtr->Fonts = prev; - }); + if (this.AfterBuildFonts is not null) + { + e.OnPostBuild( + _ => + { + var prev = ImGui.GetIO().NativePtr->Fonts; + ImGui.GetIO().NativePtr->Fonts = e.NewImAtlas.NativePtr; + ((IFontAtlasBuildToolkit.IApi9Compat)e) + .FromUiBuilderObsoleteEventHandlers(() => this.AfterBuildFonts?.InvokeSafely()); + ImGui.GetIO().NativePtr->Fonts = prev; + }); + } } private void OnResizeBuffers() diff --git a/Dalamud/Logging/PluginLog.cs b/Dalamud/Logging/PluginLog.cs index decf10b4c..e3744c617 100644 --- a/Dalamud/Logging/PluginLog.cs +++ b/Dalamud/Logging/PluginLog.cs @@ -1,6 +1,8 @@ using System.Reflection; using Dalamud.Plugin.Services; +using Dalamud.Utility; + using Serilog; using Serilog.Events; @@ -14,6 +16,7 @@ namespace Dalamud.Logging; /// move over as soon as reasonably possible for performance reasons. /// [Obsolete("Static PluginLog will be removed in API 10. Developers should use IPluginLog.")] +[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public static class PluginLog { #region "Log" prefixed Serilog style methods diff --git a/Dalamud/Utility/Api10ToDoAttribute.cs b/Dalamud/Utility/Api10ToDoAttribute.cs new file mode 100644 index 000000000..f397f8f0c --- /dev/null +++ b/Dalamud/Utility/Api10ToDoAttribute.cs @@ -0,0 +1,19 @@ +namespace Dalamud.Utility; + +/// +/// Utility class for marking something to be changed for API 10, for ease of lookup. +/// +[AttributeUsage(AttributeTargets.All, Inherited = false)] +internal sealed class Api10ToDoAttribute : Attribute +{ + /// + /// Marks that this exists purely for making API 9 plugins work. + /// + public const string DeleteCompatBehavior = "Delete. This is for making API 9 plugins work."; + + /// + /// Initializes a new instance of the class. + /// + /// The explanation. + public Api10ToDoAttribute(string what) => _ = what; +} From 8afe277c0218b8f54c5250550009edc91b9a0040 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 00:45:46 +0900 Subject: [PATCH 447/585] Make IFontHandle.Pop return a concrete struct --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 10 ++++- .../Interface/ManagedFontAtlas/IFontHandle.cs | 40 ++++++++++++++++++- .../Internals/DelegateFontHandle.cs | 3 +- .../Internals/GamePrebakedFontHandle.cs | 3 +- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 77461aa0a..d11414517 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -2,6 +2,7 @@ using System.Numerics; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Utility; using ImGuiNET; @@ -10,6 +11,7 @@ namespace Dalamud.Interface.GameFonts; /// /// ABI-compatible wrapper for . /// +[Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public sealed class GameFontHandle : IFontHandle { private readonly IFontHandle.IInternal fontHandle; @@ -53,8 +55,14 @@ public sealed class GameFontHandle : IFontHandle /// public void Dispose() => this.fontHandle.Dispose(); - /// + /// + /// Pushes the font. + /// + /// An that can be used to pop the font on dispose. public IDisposable Push() => this.fontHandle.Push(); + + /// + IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push(); /// /// Creates a new .
diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 854594663..47f384c11 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,4 +1,6 @@ -using ImGuiNET; +using Dalamud.Utility; + +using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; @@ -38,5 +40,39 @@ public interface IFontHandle : IDisposable /// You may not access the font once you dispose this object. ///
/// A disposable object that will call (1) on dispose. - IDisposable Push(); + /// If called outside of the main thread. + FontPopper Push(); + + /// + /// The wrapper for popping fonts. + /// + public struct FontPopper : IDisposable + { + private int count; + + /// + /// Initializes a new instance of the struct. + /// + /// The font to push. + /// Whether to push. + internal FontPopper(ImFontPtr fontPtr, bool push) + { + if (!push) + return; + + ThreadSafety.AssertMainThread(); + + this.count = 1; + ImGui.PushFont(fontPtr); + } + + /// + public void Dispose() + { + ThreadSafety.AssertMainThread(); + + while (this.count-- > 0) + ImGui.PopFont(); + } + } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index 99067a9de..bde349736 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -2,7 +2,6 @@ using System.Linq; using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; using Dalamud.Logging.Internal; using Dalamud.Utility; @@ -53,7 +52,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal } /// - public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); ///
/// Manager for s. diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 2686259bc..feda47a8a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -9,7 +9,6 @@ using Dalamud.Game.Text; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; -using Dalamud.Interface.Utility.Raii; using Dalamud.Utility; using ImGuiNET; @@ -117,7 +116,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - public IDisposable Push() => ImRaii.PushFont(this.ImFont, this.Available); + public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); /// /// Manager for s. From 7c1ca4001d0b4787638cd065deb25ccffe2f7e27 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 00:47:09 +0900 Subject: [PATCH 448/585] Docs --- Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 47f384c11..460fd53a0 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -41,6 +41,10 @@ public interface IFontHandle : IDisposable /// /// A disposable object that will call (1) on dispose. /// If called outside of the main thread. + /// + /// Only intended for use with using keywords, such as using (handle.Push()).
+ /// Should you store or transfer the return value to somewhere else, use as the type. + ///
FontPopper Push(); /// From d70b430e0dd19b934b74c39591cd3c504747b6f0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 03:10:41 +0900 Subject: [PATCH 449/585] Add IFontHandle.Lock and WaitAsync --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 16 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 53 ++++++ .../Interface/ManagedFontAtlas/IFontHandle.cs | 80 ++++++++- .../Internals/DelegateFontHandle.cs | 118 ++++++++++++-- .../FontAtlasFactory.Implementation.cs | 153 +++++++++++------- .../Internals/GamePrebakedFontHandle.cs | 117 +++++++++++++- .../Internals/IFontHandleManager.cs | 10 +- .../Internals/IFontHandleSubstance.cs | 5 + Dalamud/Utility/IRefCountable.cs | 77 +++++++++ 9 files changed, 543 insertions(+), 86 deletions(-) create mode 100644 Dalamud/Utility/IRefCountable.cs diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index d11414517..6591ce0fe 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -1,4 +1,5 @@ using System.Numerics; +using System.Threading.Tasks; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; @@ -28,6 +29,13 @@ public sealed class GameFontHandle : IFontHandle this.fontAtlasFactory = fontAtlasFactory; } + /// + public event Action ImFontChanged + { + add => this.fontHandle.ImFontChanged += value; + remove => this.fontHandle.ImFontChanged -= value; + } + /// public Exception? LoadException => this.fontHandle.LoadException; @@ -55,15 +63,21 @@ public sealed class GameFontHandle : IFontHandle /// public void Dispose() => this.fontHandle.Dispose(); + /// + public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock(); + /// /// Pushes the font. /// /// An that can be used to pop the font on dispose. public IDisposable Push() => this.fontHandle.Push(); - + /// IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push(); + /// + public Task WaitAsync() => this.fontHandle.WaitAsync(); + /// /// Creates a new .
///
diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index dba293e8b..b3b57343c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -1,7 +1,9 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; +using System.Numerics; using System.Text; +using System.Threading.Tasks; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; @@ -11,6 +13,8 @@ using Dalamud.Utility; using ImGuiNET; +using Serilog; + namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// @@ -103,6 +107,10 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable minCapacity: 1024); } + ImGui.SameLine(); + if (ImGui.Button("Test Lock")) + Task.Run(this.TestLock); + fixed (byte* labelPtr = "Test Input"u8) { if (ImGuiNative.igInputTextMultiline( @@ -210,4 +218,49 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable this.privateAtlas?.Dispose(); this.privateAtlas = null; } + + private async void TestLock() + { + if (this.fontHandles is not { } fontHandlesCopy) + return; + + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting for build"); + + await using var garbage = new DisposeSafety.ScopedFinalizer(); + var fonts = new List(); + IFontHandle[] handles; + try + { + handles = fontHandlesCopy.Values.SelectMany(x => x).Select(x => x.Handle.Value).ToArray(); + foreach (var handle in handles) + { + await handle.WaitAsync(); + var locked = handle.Lock(); + garbage.Add(locked); + fonts.Add(locked); + } + } + catch (ObjectDisposedException) + { + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} cancelled"); + return; + } + + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {nameof(this.TestLock)} waiting in lock"); + await Task.Delay(5000); + + foreach (var (font, handle) in fonts.Zip(handles)) + TestSingle(font, handle); + + return; + + unsafe void TestSingle(ImFontPtr fontPtr, IFontHandle handle) + { + var dim = default(Vector2); + var test = "Test string"u8; + fixed (byte* pTest = test) + ImGuiNative.ImFont_CalcTextSizeA(&dim, fontPtr, fontPtr.FontSize, float.MaxValue, 0, pTest, null, null); + Log.Information($"{nameof(GamePrebakedFontsTestWidget)}: {handle} => {dim}"); + } + } } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 460fd53a0..81ce84a63 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,4 +1,6 @@ -using Dalamud.Utility; +using System.Threading.Tasks; + +using Dalamud.Utility; using ImGuiNET; @@ -9,6 +11,11 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public interface IFontHandle : IDisposable { + /// + /// Called when the built instance of has been changed. + /// + event Action ImFontChanged; + /// /// Represents a reference counting handle for fonts. Dalamud internal use only. /// @@ -18,7 +25,8 @@ public interface IFontHandle : IDisposable /// Gets the font.
/// Use of this properly is safe only from the UI thread.
/// Use if the intended purpose of this property is .
- /// Futures changes may make simple not enough. + /// Futures changes may make simple not enough.
+ /// If you need to access a font outside the UI thread, consider using . ///
ImFontPtr ImFont { get; } } @@ -29,11 +37,27 @@ public interface IFontHandle : IDisposable Exception? LoadException { get; } /// - /// Gets a value indicating whether this font is ready for use.
- /// Use directly if you want to keep the current ImGui font if the font is not ready. + /// Gets a value indicating whether this font is ready for use. ///
+ /// + /// Once set to true, it will remain true.
+ /// Use directly if you want to keep the current ImGui font if the font is not ready.
+ /// Alternatively, use to wait for this property to become true. + ///
bool Available { get; } + /// + /// Locks the fully constructed instance of corresponding to the this + /// , for read-only use in any thread. + /// + /// An instance of that must be disposed after use. + /// + /// Calling . will not unlock the + /// locked by this function. + /// + /// If is false. + ImFontLocked Lock(); + /// /// Pushes the current font into ImGui font stack using , if available.
/// Use to access the current font.
@@ -47,6 +71,54 @@ public interface IFontHandle : IDisposable /// FontPopper Push(); + /// + /// Waits for to become true. + /// + /// A task containing this . + Task WaitAsync(); + + /// + /// The wrapper for , guaranteeing that the associated data will be available as long as + /// this struct is not disposed. + /// + public struct ImFontLocked : IDisposable + { + /// + /// The associated . + /// + public ImFontPtr ImFont; + + private IRefCountable? owner; + + /// + /// Initializes a new instance of the struct, + /// and incrase the reference count of . + /// + /// The contained font. + /// The owner. + internal ImFontLocked(ImFontPtr imFont, IRefCountable owner) + { + owner.AddRef(); + this.ImFont = imFont; + this.owner = owner; + } + + public static implicit operator ImFontPtr(ImFontLocked l) => l.ImFont; + + public static unsafe implicit operator ImFont*(ImFontLocked l) => l.ImFont.NativePtr; + + /// + public void Dispose() + { + if (this.owner is null) + return; + + this.owner.Release(); + this.owner = null; + this.ImFont = default; + } + } + /// /// The wrapper for popping fonts. /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index bde349736..f50967fae 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; @@ -27,6 +28,11 @@ internal class DelegateFontHandle : IFontHandle.IInternal this.CallOnBuildStepChange = callOnBuildStepChange; } + /// + public event Action? ImFontChanged; + + private event Action? Disposed; + /// /// Gets the function to be called on build step changes. /// @@ -49,11 +55,76 @@ internal class DelegateFontHandle : IFontHandle.IInternal { this.manager?.FreeFontHandle(this); this.manager = null; + this.Disposed?.InvokeSafely(this); + this.ImFontChanged = null; + } + + /// + public IFontHandle.ImFontLocked Lock() + { + IFontHandleSubstance? prevSubstance = default; + while (true) + { + var substance = this.ManagerNotDisposed.Substance; + if (substance is null) + throw new InvalidOperationException(); + if (substance == prevSubstance) + throw new ObjectDisposedException(nameof(DelegateFontHandle)); + + prevSubstance = substance; + try + { + substance.DataRoot.AddRef(); + } + catch (ObjectDisposedException) + { + continue; + } + + try + { + var fontPtr = substance.GetFontPtr(this); + if (fontPtr.IsNull()) + continue; + return new(fontPtr, substance.DataRoot); + } + finally + { + substance.DataRoot.Release(); + } + } } /// public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + /// + public Task WaitAsync() + { + if (this.Available) + return Task.FromResult(this); + + var tcs = new TaskCompletionSource(); + this.ImFontChanged += OnImFontChanged; + this.Disposed += OnImFontChanged; + if (this.Available) + OnImFontChanged(this); + return tcs.Task; + + void OnImFontChanged(IFontHandle unused) + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnImFontChanged; + if (this.manager is null) + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + else + tcs.SetResult(this); + } + } + /// /// Manager for s. /// @@ -81,11 +152,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal public void Dispose() { lock (this.syncRoot) - { this.handles.Clear(); - this.Substance?.Dispose(); - this.Substance = null; - } } /// @@ -109,10 +176,20 @@ internal class DelegateFontHandle : IFontHandle.IInternal } /// - public IFontHandleSubstance NewSubstance() + public void InvokeFontHandleImFontChanged() + { + if (this.Substance is not HandleSubstance hs) + return; + + foreach (var handle in hs.RelevantHandles) + handle.ImFontChanged?.InvokeSafely(handle); + } + + /// + public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { lock (this.syncRoot) - return new HandleSubstance(this, this.handles.ToArray()); + return new HandleSubstance(this, dataRoot, this.handles.ToArray()); } } @@ -123,9 +200,6 @@ internal class DelegateFontHandle : IFontHandle.IInternal { private static readonly ModuleLog Log = new($"{nameof(DelegateFontHandle)}.{nameof(HandleSubstance)}"); - // Not owned by this class. Do not dispose. - private readonly DelegateFontHandle[] relevantHandles; - // Owned by this class, but ImFontPtr values still do not belong to this. private readonly Dictionary fonts = new(); private readonly Dictionary buildExceptions = new(); @@ -134,13 +208,29 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// Initializes a new instance of the class. ///
/// The manager. + /// The data root. /// The relevant handles. - public HandleSubstance(IFontHandleManager manager, DelegateFontHandle[] relevantHandles) + public HandleSubstance( + IFontHandleManager manager, + IRefCountable dataRoot, + DelegateFontHandle[] relevantHandles) { + // We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot. + this.Manager = manager; - this.relevantHandles = relevantHandles; + this.DataRoot = dataRoot; + this.RelevantHandles = relevantHandles; } + /// + /// Gets the relevant handles. + /// + // Not owned by this class. Do not dispose. + public DelegateFontHandle[] RelevantHandles { get; } + + /// + public IRefCountable DataRoot { get; } + /// public IFontHandleManager Manager { get; } @@ -171,7 +261,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal public void OnPreBuild(IFontAtlasBuildToolkitPreBuild toolkitPreBuild) { var fontsVector = toolkitPreBuild.Fonts; - foreach (var k in this.relevantHandles) + foreach (var k in this.RelevantHandles) { var fontCountPrevious = fontsVector.Length; @@ -288,7 +378,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { - foreach (var k in this.relevantHandles) + foreach (var k in this.RelevantHandles) { if (!this.fonts[k].IsNotNullAndLoaded()) continue; @@ -315,7 +405,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) { - foreach (var k in this.relevantHandles) + foreach (var k in this.RelevantHandles) { if (!this.fonts[k].IsNotNullAndLoaded()) continue; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index eddccfa76..99ce8dab9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -43,68 +43,67 @@ internal sealed partial class FontAtlasFactory private static readonly Task EmptyTask = Task.FromResult(default(FontAtlasBuiltData)); - private struct FontAtlasBuiltData : IDisposable + private class FontAtlasBuiltData : IRefCountable { - public readonly DalamudFontAtlas? Owner; - public readonly ImFontAtlasPtr Atlas; - public readonly float Scale; + private readonly List wraps; + private readonly List substances; - public bool IsBuildInProgress; + private int refCount; - private readonly List? wraps; - private readonly List? substances; - private readonly DisposeSafety.ScopedFinalizer? garbage; - - public unsafe FontAtlasBuiltData( - DalamudFontAtlas owner, - IEnumerable substances, - float scale) + public unsafe FontAtlasBuiltData(DalamudFontAtlas owner, float scale) { this.Owner = owner; this.Scale = scale; - this.garbage = new(); + this.Garbage = new(); + this.refCount = 1; try { var substancesList = this.substances = new(); - foreach (var s in substances) - substancesList.Add(this.garbage.Add(s)); - this.garbage.Add(() => substancesList.Clear()); + this.Garbage.Add(() => substancesList.Clear()); var wrapsCopy = this.wraps = new(); - this.garbage.Add(() => wrapsCopy.Clear()); + this.Garbage.Add(() => wrapsCopy.Clear()); var atlasPtr = ImGuiNative.ImFontAtlas_ImFontAtlas(); this.Atlas = atlasPtr; if (this.Atlas.NativePtr is null) throw new OutOfMemoryException($"Failed to allocate a new {nameof(ImFontAtlas)}."); - this.garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); + this.Garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); this.IsBuildInProgress = true; } catch { - this.garbage.Dispose(); + this.Garbage.Dispose(); throw; } } - public readonly DisposeSafety.ScopedFinalizer Garbage => - this.garbage ?? throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + public DalamudFontAtlas? Owner { get; } - public readonly ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); + public ImFontAtlasPtr Atlas { get; } - public readonly ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + public float Scale { get; } - public readonly ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + public bool IsBuildInProgress { get; set; } - public readonly IReadOnlyList Wraps => - (IReadOnlyList?)this.wraps ?? Array.Empty(); + public DisposeSafety.ScopedFinalizer Garbage { get; } - public readonly IReadOnlyList Substances => - (IReadOnlyList?)this.substances ?? Array.Empty(); + public ImVectorWrapper Fonts => this.Atlas.FontsWrapped(); - public readonly void AddExistingTexture(IDalamudTextureWrap wrap) + public ImVectorWrapper ConfigData => this.Atlas.ConfigDataWrapped(); + + public ImVectorWrapper ImTextures => this.Atlas.TexturesWrapped(); + + public IReadOnlyList Wraps => this.wraps; + + public IReadOnlyList Substances => this.substances; + + public void InitialAddSubstance(IFontHandleSubstance substance) => + this.substances.Add(this.Garbage.Add(substance)); + + public void AddExistingTexture(IDalamudTextureWrap wrap) { if (this.wraps is null) throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); @@ -112,7 +111,7 @@ internal sealed partial class FontAtlasFactory this.wraps.Add(this.Garbage.Add(wrap)); } - public readonly int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) + public int AddNewTexture(IDalamudTextureWrap wrap, bool disposeOnError) { if (this.wraps is null) throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); @@ -160,27 +159,47 @@ internal sealed partial class FontAtlasFactory return index; } - public unsafe void Dispose() + public int AddRef() => IRefCountable.AlterRefCount(1, ref this.refCount, out var newRefCount) switch { - if (this.garbage is null) - return; + IRefCountable.RefCountResult.StillAlive => newRefCount, + IRefCountable.RefCountResult.AlreadyDisposed => + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)), + IRefCountable.RefCountResult.FinalRelease => throw new InvalidOperationException(), + _ => throw new InvalidOperationException(), + }; - if (this.IsBuildInProgress) + public unsafe int Release() + { + switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount)) { - Log.Error( - "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + - "Stack:\n{trace}", - this.Owner?.Name ?? "", - (nint)this.Atlas.NativePtr, - new StackTrace()); - while (this.IsBuildInProgress) - Thread.Sleep(100); - } + case IRefCountable.RefCountResult.StillAlive: + return newRefCount; + + case IRefCountable.RefCountResult.FinalRelease: + if (this.IsBuildInProgress) + { + Log.Error( + "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + + "Stack:\n{trace}", + this.Owner?.Name ?? "", + (nint)this.Atlas.NativePtr, + new StackTrace()); + while (this.IsBuildInProgress) + Thread.Sleep(100); + } #if VeryVerboseLog - Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); + Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); #endif - this.garbage.Dispose(); + this.Garbage.Dispose(); + return newRefCount; + + case IRefCountable.RefCountResult.AlreadyDisposed: + throw new ObjectDisposedException(nameof(FontAtlasBuiltData)); + + default: + throw new InvalidOperationException(); + } } public BuildToolkit CreateToolkit(FontAtlasFactory factory, bool isAsync) @@ -201,8 +220,8 @@ internal sealed partial class FontAtlasFactory private readonly object syncRootPostPromotion = new(); private readonly object syncRoot = new(); - private Task buildTask = EmptyTask; - private FontAtlasBuiltData builtData; + private Task buildTask = EmptyTask; + private FontAtlasBuiltData? builtData; private int buildSuppressionCounter; private bool buildSuppressionSuppressed; @@ -275,7 +294,8 @@ internal sealed partial class FontAtlasFactory lock (this.syncRoot) { this.buildTask.ToDisposableIgnoreExceptions().Dispose(); - this.builtData.Dispose(); + this.builtData?.Release(); + this.builtData = null; } } @@ -303,7 +323,7 @@ internal sealed partial class FontAtlasFactory get { lock (this.syncRoot) - return this.builtData.Atlas; + return this.builtData?.Atlas ?? default; } } @@ -311,7 +331,7 @@ internal sealed partial class FontAtlasFactory public Task BuildTask => this.buildTask; /// - public bool HasBuiltAtlas => !this.builtData.Atlas.IsNull(); + public bool HasBuiltAtlas => !(this.builtData?.Atlas.IsNull() ?? true); /// public bool IsGlobalScaled { get; } @@ -474,13 +494,13 @@ internal sealed partial class FontAtlasFactory var rebuildIndex = ++this.buildIndex; return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); - async Task BuildInner(Task unused) + async Task BuildInner(Task unused) { Log.Verbose("[{name}] Building from {source}.", this.Name, nameof(this.BuildFontsAsync)); lock (this.syncRoot) { if (this.buildIndex != rebuildIndex) - return default; + return null; } var res = await this.RebuildFontsPrivate(true, scale); @@ -512,8 +532,10 @@ internal sealed partial class FontAtlasFactory return; } - this.builtData.ExplicitDisposeIgnoreExceptions(); + var prevBuiltData = this.builtData; this.builtData = data; + prevBuiltData.ExplicitDisposeIgnoreExceptions(); + this.buildTask = EmptyTask; foreach (var substance in data.Substances) substance.Manager.Substance = substance; @@ -570,6 +592,9 @@ internal sealed partial class FontAtlasFactory } } + foreach (var substance in data.Substances) + substance.Manager.InvokeFontHandleImFontChanged(); + #if VeryVerboseLog Log.Verbose("[{name}] Built from {source}.", this.Name, source); #endif @@ -610,12 +635,14 @@ internal sealed partial class FontAtlasFactory var sw = new Stopwatch(); sw.Start(); - var res = default(FontAtlasBuiltData); + FontAtlasBuiltData? res = null; nint atlasPtr = 0; BuildToolkit? toolkit = null; try { - res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + res = new(this, scale); + foreach (var fhm in this.fontHandleManagers) + res.InitialAddSubstance(fhm.NewSubstance(res)); unsafe { atlasPtr = (nint)res.Atlas.NativePtr; @@ -646,9 +673,11 @@ internal sealed partial class FontAtlasFactory res.IsBuildInProgress = false; toolkit.Dispose(); - res.Dispose(); + res.Release(); - res = new(this, this.fontHandleManagers.Select(x => x.NewSubstance()), scale); + res = new(this, scale); + foreach (var fhm in this.fontHandleManagers) + res.InitialAddSubstance(fhm.NewSubstance(res)); unsafe { atlasPtr = (nint)res.Atlas.NativePtr; @@ -715,8 +744,12 @@ internal sealed partial class FontAtlasFactory nameof(this.RebuildFontsPrivateReal), atlasPtr, sw.ElapsedMilliseconds); - res.IsBuildInProgress = false; - res.Dispose(); + if (res is not null) + { + res.IsBuildInProgress = false; + res.Release(); + } + throw; } finally diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index feda47a8a..c05b3a96d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; +using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.GameFonts; @@ -53,6 +54,11 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal this.FontStyle = style; } + /// + public event Action? ImFontChanged; + + private event Action? Disposed; + /// /// Provider for for `common/font/fontNN.tex`. /// @@ -113,17 +119,86 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal { this.manager?.FreeFontHandle(this); this.manager = null; + this.Disposed?.InvokeSafely(this); + this.ImFontChanged = null; + } + + /// + public IFontHandle.ImFontLocked Lock() + { + IFontHandleSubstance? prevSubstance = default; + while (true) + { + var substance = this.ManagerNotDisposed.Substance; + if (substance is null) + throw new InvalidOperationException(); + if (substance == prevSubstance) + throw new ObjectDisposedException(nameof(DelegateFontHandle)); + + prevSubstance = substance; + try + { + substance.DataRoot.AddRef(); + } + catch (ObjectDisposedException) + { + continue; + } + + try + { + var fontPtr = substance.GetFontPtr(this); + if (fontPtr.IsNull()) + continue; + return new(fontPtr, substance.DataRoot); + } + finally + { + substance.DataRoot.Release(); + } + } } /// public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + /// + public Task WaitAsync() + { + if (this.Available) + return Task.FromResult(this); + + var tcs = new TaskCompletionSource(); + this.ImFontChanged += OnImFontChanged; + this.Disposed += OnImFontChanged; + if (this.Available) + OnImFontChanged(this); + return tcs.Task; + + void OnImFontChanged(IFontHandle unused) + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnImFontChanged; + if (this.manager is null) + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + else + tcs.SetResult(this); + } + } + + /// + public override string ToString() => $"{nameof(GamePrebakedFontHandle)}({this.FontStyle})"; + /// /// Manager for s. /// internal sealed class HandleManager : IFontHandleManager { private readonly Dictionary gameFontsRc = new(); + private readonly HashSet handles = new(); private readonly object syncRoot = new(); /// @@ -154,8 +229,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public void Dispose() { - this.Substance?.Dispose(); - this.Substance = null; + // empty } /// @@ -165,6 +239,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal bool suggestRebuild; lock (this.syncRoot) { + this.handles.Add(handle); this.gameFontsRc[style] = this.gameFontsRc.GetValueOrDefault(style, 0) + 1; suggestRebuild = this.Substance?.GetFontPtr(handle).IsNotNullAndLoaded() is not true; } @@ -183,6 +258,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal lock (this.syncRoot) { + this.handles.Remove(ggfh); if (!this.gameFontsRc.ContainsKey(ggfh.FontStyle)) return; @@ -192,10 +268,20 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - public IFontHandleSubstance NewSubstance() + public void InvokeFontHandleImFontChanged() + { + if (this.Substance is not HandleSubstance hs) + return; + + foreach (var handle in hs.RelevantHandles) + handle.ImFontChanged?.InvokeSafely(handle); + } + + /// + public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { lock (this.syncRoot) - return new HandleSubstance(this, this.gameFontsRc.Keys); + return new HandleSubstance(this, dataRoot, this.handles.ToArray(), this.gameFontsRc.Keys); } } @@ -218,14 +304,32 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// Initializes a new instance of the class. /// /// The manager. + /// The data root. + /// The relevant handles. /// The game font styles. - public HandleSubstance(HandleManager manager, IEnumerable gameFontStyles) + public HandleSubstance( + HandleManager manager, + IRefCountable dataRoot, + GamePrebakedFontHandle[] relevantHandles, + IEnumerable gameFontStyles) { + // We do not call dataRoot.AddRef; this object is dependant on lifetime of dataRoot. + this.handleManager = manager; - Service.Get(); + this.DataRoot = dataRoot; + this.RelevantHandles = relevantHandles; this.gameFontStyles = new(gameFontStyles); } + /// + /// Gets the relevant handles. + /// + // Not owned by this class. Do not dispose. + public GamePrebakedFontHandle[] RelevantHandles { get; } + + /// + public IRefCountable DataRoot { get; } + /// public IFontHandleManager Manager => this.handleManager; @@ -240,6 +344,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public void Dispose() { + // empty } /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs index 93c688608..7066817b7 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -1,3 +1,5 @@ +using Dalamud.Utility; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -27,6 +29,12 @@ internal interface IFontHandleManager : IDisposable /// /// Creates a new substance of the font atlas. /// + /// The data root. /// The new substance. - IFontHandleSubstance NewSubstance(); + IFontHandleSubstance NewSubstance(IRefCountable dataRoot); + + /// + /// Invokes . + /// + void InvokeFontHandleImFontChanged(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs index c800c30ac..73c14efc1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -9,6 +9,11 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// internal interface IFontHandleSubstance : IDisposable { + /// + /// Gets the data root relevant to this instance of . + /// + IRefCountable DataRoot { get; } + /// /// Gets the manager relevant to this instance of . /// diff --git a/Dalamud/Utility/IRefCountable.cs b/Dalamud/Utility/IRefCountable.cs new file mode 100644 index 000000000..76d1059d1 --- /dev/null +++ b/Dalamud/Utility/IRefCountable.cs @@ -0,0 +1,77 @@ +using System.Diagnostics; +using System.Threading; + +namespace Dalamud.Utility; + +/// +/// Interface for reference counting. +/// +internal interface IRefCountable : IDisposable +{ + /// + /// Result for . + /// + public enum RefCountResult + { + /// + /// The object still has remaining references. No futher action should be done. + /// + StillAlive = 1, + + /// + /// The last reference to the object has been released. The object should be fully released. + /// + FinalRelease = 2, + + /// + /// The object already has been disposed. may be thrown. + /// + AlreadyDisposed = 3, + } + + /// + /// Adds a reference to this reference counted object. + /// + /// The new number of references. + int AddRef(); + + /// + /// Releases a reference from this reference counted object.
+ /// When all references are released, the object will be fully disposed. + ///
+ /// The new number of references. + int Release(); + + /// + /// Alias for . + /// + void IDisposable.Dispose() => this.Release(); + + /// + /// Alters by . + /// + /// The delta to the reference count. + /// The reference to the reference count. + /// The new reference count. + /// The followup action that should be done. + public static RefCountResult AlterRefCount(int delta, ref int refCount, out int newRefCount) + { + Debug.Assert(delta is 1 or -1, "delta must be 1 or -1"); + + while (true) + { + var refCountCopy = refCount; + if (refCountCopy <= 0) + { + newRefCount = refCountCopy; + return RefCountResult.AlreadyDisposed; + } + + newRefCount = refCountCopy + delta; + if (refCountCopy != Interlocked.CompareExchange(ref refCount, newRefCount, refCountCopy)) + continue; + + return newRefCount == 0 ? RefCountResult.FinalRelease : RefCountResult.StillAlive; + } + } +} From 967ae973084e843d8313df7e7458d2cffa678459 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 03:41:26 +0900 Subject: [PATCH 450/585] Expose wrapped default font handle --- .../Interface/Internal/InterfaceManager.cs | 35 ++++-- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 4 + .../Interface/ManagedFontAtlas/IFontHandle.cs | 7 +- Dalamud/Interface/UiBuilder.cs | 106 ++++++++++++++++-- 4 files changed, 131 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 8915b3e3d..62f9145bf 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -13,7 +13,6 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Hooking.WndProcHook; -using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; @@ -87,9 +86,6 @@ internal class InterfaceManager : IDisposable, IServiceType private Hook? resizeBuffersHook; private IFontAtlas? dalamudAtlas; - private IFontHandle.IInternal? defaultFontHandle; - private IFontHandle.IInternal? iconFontHandle; - private IFontHandle.IInternal? monoFontHandle; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -131,19 +127,34 @@ internal class InterfaceManager : IDisposable, IServiceType /// Gets the default ImGui font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont => WhenFontsReady().defaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr DefaultFont => WhenFontsReady().DefaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included FontAwesome icon font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont => WhenFontsReady().iconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr IconFont => WhenFontsReady().IconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included monospaced font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont => WhenFontsReady().monoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr MonoFont => WhenFontsReady().MonoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + + /// + /// Gets the default font handle. + /// + public IFontHandle.IInternal? DefaultFontHandle { get; private set; } + + /// + /// Gets the icon font handle. + /// + public IFontHandle.IInternal? IconFontHandle { get; private set; } + + /// + /// Gets the mono font handle. + /// + public IFontHandle.IInternal? MonoFontHandle { get; private set; } /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. @@ -691,9 +702,9 @@ internal class InterfaceManager : IDisposable, IServiceType .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); using (this.dalamudAtlas.SuppressAutoRebuild()) { - this.defaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.DefaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); - this.iconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.IconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddFontAwesomeIconFont( new() @@ -702,7 +713,7 @@ internal class InterfaceManager : IDisposable, IServiceType GlyphMinAdvanceX = DefaultFontSizePx, GlyphMaxAdvanceX = DefaultFontSizePx, }))); - this.monoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.MonoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, @@ -715,12 +726,12 @@ internal class InterfaceManager : IDisposable, IServiceType // Use font handles directly. // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.defaultFontHandle.ImFont, this.monoFontHandle.ImFont, true); + tk.CopyGlyphsAcrossFonts(this.DefaultFontHandle.ImFont, this.MonoFontHandle.ImFont, true); // Update default font unsafe { - ImGui.GetIO().NativePtr->FontDefault = this.defaultFontHandle.ImFont; + ImGui.GetIO().NativePtr->FontDefault = this.DefaultFontHandle.ImFont; } // Broadcast to auto-rebuilding instances diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index ec3e66e9a..491292f9d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -122,6 +122,10 @@ public interface IFontAtlas : IDisposable /// Note that would not necessarily get changed from calling this function. /// /// If is . + /// + /// Using this method will block the main thread on rebuilding fonts, effectively calling + /// from the main thread. Consider migrating to . + /// void BuildFontsOnNextFrame(); /// diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 81ce84a63..eb57b815f 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -12,7 +12,12 @@ namespace Dalamud.Interface.ManagedFontAtlas; public interface IFontHandle : IDisposable { /// - /// Called when the built instance of has been changed. + /// Called when the built instance of has been changed.
+ /// This event will be invoked on the same thread with + /// ., + /// when the build step is .
+ /// See , , and + /// . ///
event Action ImFontChanged; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 87e3b9032..43912f224 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -41,6 +41,10 @@ public sealed class UiBuilder : IDisposable private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; + private IFontHandle? defaultFontHandle; + private IFontHandle? iconFontHandle; + private IFontHandle? monoFontHandle; + /// /// Initializes a new instance of the class and registers it. /// You do not have to call this manually. @@ -103,7 +107,14 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. /// - [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + /// + /// To add your custom font, use . or + /// .
+ /// To be notified on font changes after fonts are built, use + /// ..
+ /// For all other purposes, use .. + ///
+ [Obsolete("See remarks.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? BuildFonts; @@ -113,6 +124,13 @@ public sealed class UiBuilder : IDisposable /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
+ /// + /// To add your custom font, use . or + /// .
+ /// To be notified on font changes after fonts are built, use + /// ..
+ /// For all other purposes, use .. + ///
[Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? AfterBuildFonts; @@ -143,6 +161,23 @@ public sealed class UiBuilder : IDisposable /// Gets the default Dalamud font - supporting all game languages and icons.
/// Accessing this static property outside of is dangerous and not supported. ///
+ public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; + + /// + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ public static ImFontPtr IconFont => InterfaceManager.IconFont; + + /// + /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
+ /// Accessing this static property outside of is dangerous and not supported. + ///
+ public static ImFontPtr MonoFont => InterfaceManager.MonoFont; + + /// + /// Gets the handle to the default Dalamud font - supporting all game languages and icons. + /// /// /// A font handle corresponding to this font can be obtained with: /// @@ -151,11 +186,15 @@ public sealed class UiBuilder : IDisposable /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); /// /// - public static ImFontPtr DefaultFont => InterfaceManager.DefaultFont; + public IFontHandle DefaultFontHandle => + this.defaultFontHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.DefaultFontHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); /// - /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud icon font based on FontAwesome 5 Free solid. ///
/// /// A font handle corresponding to this font can be obtained with: @@ -165,11 +204,15 @@ public sealed class UiBuilder : IDisposable /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); ///
/// - public static ImFontPtr IconFont => InterfaceManager.IconFont; + public IFontHandle IconFontHandle => + this.iconFontHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.IconFontHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); /// - /// Gets the default Dalamud monospaced font based on Inconsolata Regular.
- /// Accessing this static property outside of is dangerous and not supported. + /// Gets the default Dalamud monospaced font based on Inconsolata Regular. ///
/// /// A font handle corresponding to this font can be obtained with: @@ -181,7 +224,12 @@ public sealed class UiBuilder : IDisposable /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); ///
/// - public static ImFontPtr MonoFont => InterfaceManager.MonoFont; + public IFontHandle MonoFontHandle => + this.monoFontHandle ??= + this.scopedFinalizer.Add( + new FontHandleWrapper( + this.InterfaceManagerWithScene?.MonoFontHandle + ?? throw new InvalidOperationException("Scene is not yet ready."))); /// /// Gets the game's active Direct3D device. @@ -660,4 +708,46 @@ public sealed class UiBuilder : IDisposable { this.ResizeBuffers?.InvokeSafely(); } + + private class FontHandleWrapper : IFontHandle + { + private IFontHandle? wrapped; + + public FontHandleWrapper(IFontHandle wrapped) + { + this.wrapped = wrapped; + this.wrapped.ImFontChanged += this.WrappedOnImFontChanged; + } + + public event Action? ImFontChanged; + + public Exception? LoadException => + this.wrapped!.LoadException ?? new ObjectDisposedException(nameof(FontHandleWrapper)); + + public bool Available => this.wrapped?.Available ?? false; + + public void Dispose() + { + if (this.wrapped is not { } w) + return; + + this.wrapped = null; + w.ImFontChanged -= this.WrappedOnImFontChanged; + // Note: do not dispose w; we do not own it + } + + public IFontHandle.ImFontLocked Lock() => + this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + + public IFontHandle.FontPopper Push() => + this.wrapped?.Push() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + + public Task WaitAsync() => + this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ?? + throw new ObjectDisposedException(nameof(FontHandleWrapper)); + + public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})"; + + private void WrappedOnImFontChanged(IFontHandle obj) => this.ImFontChanged.InvokeSafely(this); + } } From 0701d7805a94723eb8ec8a948c5e5279325e922f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:07:21 +0900 Subject: [PATCH 451/585] BuildFonts remarks --- Dalamud/Interface/UiBuilder.cs | 45 +++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 43912f224..02decf103 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -13,6 +13,7 @@ using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -103,7 +104,7 @@ public sealed class UiBuilder : IDisposable /// /// Gets or sets an action that is called any time ImGui fonts need to be rebuilt.
- /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt + /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
@@ -112,7 +113,36 @@ public sealed class UiBuilder : IDisposable /// .
/// To be notified on font changes after fonts are built, use /// ..
- /// For all other purposes, use .. + /// For all other purposes, use ..
+ ///
+ /// Note that you will be calling above functions once, instead of every time inside a build step change callback. + /// For example, you can make all font handles from your plugin constructor, and then use the created handles during + /// event, by using in a scope.
+ /// You may dispose your font handle anytime, as long as it's not in use in . + /// Font handles may be constructed anytime, as long as the owner or + /// is not disposed.
+ ///
+ /// If you were storing , consider if the job can be achieved solely by using + /// without directly using an instance of .
+ /// If you do need it, evaluate if you need to access fonts outside the main thread.
+ /// If it is the case, use to obtain a safe-to-access instance of + /// , once resolves.
+ /// Otherwise, use , and obtain the instance of via + /// . Do not let the escape the using scope.
+ ///
+ /// If your plugin sets to a non-default value, then + /// should be accessed using + /// , as the font handle member variables are only available + /// once drawing facilities are available.
+ ///
+ /// Examples:
+ /// * .
+ /// * .
+ /// * ctor.
+ /// * : + /// note how a new instance of is constructed, and + /// is called from another function, without having to manually + /// initialize font rebuild process. /// [Obsolete("See remarks.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] @@ -120,18 +150,11 @@ public sealed class UiBuilder : IDisposable /// /// Gets or sets an action that is called any time right after ImGui fonts are rebuilt.
- /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt + /// Any ImFontPtr objects that you store can be invalidated when fonts are rebuilt /// (at any time), so you should both reload your custom fonts and restore those /// pointers inside this handler. ///
- /// - /// To add your custom font, use . or - /// .
- /// To be notified on font changes after fonts are built, use - /// ..
- /// For all other purposes, use .. - ///
- [Obsolete($"Use {nameof(this.FontAtlas)} instead.", false)] + [Obsolete($"See remarks for {nameof(BuildFonts)}.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public event Action? AfterBuildFonts; From 127b91f4b0d05ca08591f3b8cfbda8a6d9f706ea Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:12:40 +0900 Subject: [PATCH 452/585] Fix doc --- Dalamud/Interface/UiBuilder.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 02decf103..c27c9ab84 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -140,9 +140,9 @@ public sealed class UiBuilder : IDisposable /// * .
/// * ctor.
/// * : - /// note how a new instance of is constructed, and - /// is called from another function, without having to manually - /// initialize font rebuild process. + /// note how the construction of a new instance of and + /// call of are done in different functions, + /// without having to manually initiate font rebuild process. /// [Obsolete("See remarks.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] From af1133f99973af30d01b1946f7513810320a6243 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:21:26 +0900 Subject: [PATCH 453/585] Determine optional assets availability on startup --- Dalamud.CorePlugin/PluginImpl.cs | 4 ++++ Dalamud/Storage/Assets/DalamudAssetManager.cs | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index ef99f6def..96d212dd3 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -69,6 +69,10 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; + this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += fc => + { + Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}"); + }; Service.Get().AddHandler("/coreplug", new(this.OnCommand) { HelpMessage = "Access the plugin." }); diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 70a91c4bf..7edb1c61d 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -69,6 +69,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Select(x => x.ToContentDisposedTask())) .ContinueWith(_ => loadTimings.Dispose()), "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); + + Task.WhenAll( + Enum.GetValues() + .Where(x => x is not DalamudAsset.Empty4X4) + .Where(x => x.GetAttribute()?.Required is false) + .Select(this.CreateStreamAsync) + .Select(x => x.ToContentDisposedTask())) + .ContinueWith(r => Log.Verbose($"Optional assets load state: {r}")); } /// From 3e3297f7a8eeb3edcd83021ac90c25fb1d3f0482 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:49:51 +0900 Subject: [PATCH 454/585] Use Lock instead of .ImFont --- Dalamud/Interface/Internal/InterfaceManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 62f9145bf..159ae15bf 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -725,13 +725,16 @@ internal class InterfaceManager : IDisposable, IServiceType // Do not use DefaultFont, IconFont, and MonoFont. // Use font handles directly. + using var defaultFont = this.DefaultFontHandle.Lock(); + using var monoFont = this.MonoFontHandle.Lock(); + // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(this.DefaultFontHandle.ImFont, this.MonoFontHandle.ImFont, true); + tk.CopyGlyphsAcrossFonts(defaultFont, monoFont, true); // Update default font unsafe { - ImGui.GetIO().NativePtr->FontDefault = this.DefaultFontHandle.ImFont; + ImGui.GetIO().NativePtr->FontDefault = defaultFont; } // Broadcast to auto-rebuilding instances From a409ea60d6442a37c218dee954e27fc4e5e113d9 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 04:54:35 +0900 Subject: [PATCH 455/585] Update docs --- .../IFontAtlasBuildToolkitPreBuild.cs | 14 ++++++++------ Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index cb8a27a54..38d8d2fe8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -54,11 +54,11 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds a font from memory region allocated using .
- /// It WILL crash if you try to use a memory pointer allocated in some other way.
- /// + /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// /// Do NOT call on the once this function has /// been called, unless is set and the function has thrown an error. - ///
+ /// ///
/// Memory address for the data allocated using . /// The size of the font file.. @@ -81,9 +81,11 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// Adds a font from memory region allocated using .
- /// It WILL crash if you try to use a memory pointer allocated in some other way.
- /// Do NOT call on the once this - /// function has been called. + /// It WILL crash if you try to use a memory pointer allocated in some other way.
+ /// + /// Do NOT call on the once this function has + /// been called, unless is set and the function has thrown an error. + /// ///
/// Memory address for the data allocated using . /// The size of the font file.. diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index eb57b815f..877cd60c9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -53,7 +53,8 @@ public interface IFontHandle : IDisposable /// /// Locks the fully constructed instance of corresponding to the this - /// , for read-only use in any thread. + /// , for use in any thread.
+ /// Modification of the font will exhibit undefined behavior if some other thread also uses the font. ///
/// An instance of that must be disposed after use. /// From 500df36cae48bbf0132cec86248dcc4605b26fd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Jan 2024 23:48:44 +0000 Subject: [PATCH 456/585] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index bbc4b9942..e9341bb30 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit bbc4b994254d6913f51da3a20fad9bf4b8c986e5 +Subproject commit e9341bb3038bf4200300f21be4a8629525d15596 From 29b3e0aa97683d1dcb11421fd3aef0b909b05119 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 13:15:36 +0900 Subject: [PATCH 457/585] Make IFontHandle.Push return IDisposable, and add IFontHandle.Pop --- Dalamud/Dalamud.csproj | 1 + Dalamud/Interface/GameFonts/GameFontHandle.cs | 4 +- .../Interface/Internal/InterfaceManager.cs | 7 ++ .../Widgets/GamePrebakedFontsTestWidget.cs | 20 ++++- .../Interface/ManagedFontAtlas/IFontHandle.cs | 49 +++--------- .../Internals/DelegateFontHandle.cs | 36 ++++++++- .../Internals/GamePrebakedFontHandle.cs | 33 +++++++- .../Internals/SimplePushedFont.cs | 78 +++++++++++++++++++ Dalamud/Interface/UiBuilder.cs | 4 +- 9 files changed, 185 insertions(+), 47 deletions(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index ba044a555..f58a0c47a 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -70,6 +70,7 @@ + all diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 6591ce0fe..7bda27eae 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -72,8 +72,8 @@ public sealed class GameFontHandle : IFontHandle /// An that can be used to pop the font on dispose. public IDisposable Push() => this.fontHandle.Push(); - /// - IFontHandle.FontPopper IFontHandle.Push() => this.fontHandle.Push(); + /// + public void Pop() => this.fontHandle.Pop(); /// public Task WaitAsync() => this.fontHandle.WaitAsync(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 159ae15bf..e1b714ee8 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -230,6 +230,11 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public Task FontBuildTask => WhenFontsReady().dalamudAtlas!.BuildTask; + /// + /// Gets the number of calls to so far. + /// + public long CumulativePresentCalls { get; private set; } + /// /// Dispose of managed and unmanaged resources. /// @@ -647,6 +652,8 @@ internal class InterfaceManager : IDisposable, IServiceType */ private IntPtr PresentDetour(IntPtr swapChain, uint syncInterval, uint presentFlags) { + this.CumulativePresentCalls++; + Debug.Assert(this.presentHook is not null, "How did PresentDetour get called when presentHook is null?"); Debug.Assert(this.dalamudAtlas is not null, "dalamudAtlas should have been set already"); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index b3b57343c..7b649a895 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -163,6 +163,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable .ToArray()); var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); + var counter = 0; foreach (var (family, items) in this.fontHandles) { if (!ImGui.CollapsingHeader($"{family} Family")) @@ -188,10 +189,21 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { if (!this.useGlobalScale) ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); - using var pushPop = handle.Value.Push(); - ImGuiNative.igTextUnformatted( - this.testStringBuffer.Data, - this.testStringBuffer.Data + this.testStringBuffer.Length); + if (counter++ % 2 == 0) + { + using var pushPop = handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + } + else + { + handle.Value.Push(); + ImGuiNative.igTextUnformatted( + this.testStringBuffer.Data, + this.testStringBuffer.Data + this.testStringBuffer.Length); + handle.Value.Pop(); + } } } finally diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 877cd60c9..94edc9777 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -65,17 +65,23 @@ public interface IFontHandle : IDisposable ImFontLocked Lock(); /// - /// Pushes the current font into ImGui font stack using , if available.
+ /// Pushes the current font into ImGui font stack, if available.
/// Use to access the current font.
/// You may not access the font once you dispose this object. ///
- /// A disposable object that will call (1) on dispose. + /// A disposable object that will pop the font on dispose. /// If called outside of the main thread. /// - /// Only intended for use with using keywords, such as using (handle.Push()).
- /// Should you store or transfer the return value to somewhere else, use as the type. + /// This function uses , and may do extra things. + /// Use or to undo this operation. + /// Do not use . ///
- FontPopper Push(); + IDisposable Push(); + + /// + /// Pops the font pushed to ImGui using , cleaning up any extra information as needed. + /// + void Pop(); /// /// Waits for to become true. @@ -124,37 +130,4 @@ public interface IFontHandle : IDisposable this.ImFont = default; } } - - /// - /// The wrapper for popping fonts. - /// - public struct FontPopper : IDisposable - { - private int count; - - /// - /// Initializes a new instance of the struct. - /// - /// The font to push. - /// Whether to push. - internal FontPopper(ImFontPtr fontPtr, bool push) - { - if (!push) - return; - - ThreadSafety.AssertMainThread(); - - this.count = 1; - ImGui.PushFont(fontPtr); - } - - /// - public void Dispose() - { - ThreadSafety.AssertMainThread(); - - while (this.count-- > 0) - ImGui.PopFont(); - } - } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index f50967fae..e1c18e923 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -2,12 +2,15 @@ using System.Linq; using System.Threading.Tasks; +using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using Dalamud.Utility; using ImGuiNET; +using Serilog; + namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// @@ -15,7 +18,10 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// internal class DelegateFontHandle : IFontHandle.IInternal { + private readonly List pushedFonts = new(8); + private IFontHandleManager? manager; + private long lastCumulativePresentCalls; /// /// Initializes a new instance of the class. @@ -53,6 +59,8 @@ internal class DelegateFontHandle : IFontHandle.IInternal /// public void Dispose() { + if (this.pushedFonts.Count > 0) + Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); this.manager?.FreeFontHandle(this); this.manager = null; this.Disposed?.InvokeSafely(this); @@ -96,7 +104,33 @@ internal class DelegateFontHandle : IFontHandle.IInternal } /// - public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + public IDisposable Push() + { + ThreadSafety.AssertMainThread(); + var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; + if (this.lastCumulativePresentCalls != cumulativePresentCalls) + { + this.lastCumulativePresentCalls = cumulativePresentCalls; + if (this.pushedFonts.Count > 0) + { + Log.Warning( + $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + + $"You might be missing a call to {nameof(this.Pop)}."); + this.pushedFonts.Clear(); + } + } + + var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); + this.pushedFonts.Add(rented); + return rented; + } + + /// + public void Pop() + { + ThreadSafety.AssertMainThread(); + this.pushedFonts[^1].Dispose(); + } /// public Task WaitAsync() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index c05b3a96d..0e8301785 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -16,6 +16,8 @@ using ImGuiNET; using Lumina.Data.Files; +using Serilog; + using Vector4 = System.Numerics.Vector4; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -35,7 +37,10 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); + private readonly List pushedFonts = new(8); + private IFontHandleManager? manager; + private long lastCumulativePresentCalls; /// /// Initializes a new instance of the class. @@ -160,7 +165,33 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal } /// - public IFontHandle.FontPopper Push() => new(this.ImFont, this.Available); + public IDisposable Push() + { + ThreadSafety.AssertMainThread(); + var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; + if (this.lastCumulativePresentCalls != cumulativePresentCalls) + { + this.lastCumulativePresentCalls = cumulativePresentCalls; + if (this.pushedFonts.Count > 0) + { + Log.Warning( + $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + + $"You might be missing a call to {nameof(this.Pop)}."); + this.pushedFonts.Clear(); + } + } + + var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); + this.pushedFonts.Add(rented); + return rented; + } + + /// + public void Pop() + { + ThreadSafety.AssertMainThread(); + this.pushedFonts[^1].Dispose(); + } /// public Task WaitAsync() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs new file mode 100644 index 000000000..3f7255386 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Diagnostics; + +using Dalamud.Interface.Utility; + +using ImGuiNET; + +using Microsoft.Extensions.ObjectPool; + +using Serilog; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Reusable font push/popper. +/// +internal sealed class SimplePushedFont : IDisposable +{ + // Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose. + private static readonly ObjectPool Pool = + new DefaultObjectPool(new DefaultPooledObjectPolicy()); + + private List? stack; + private ImFontPtr font; + + /// + /// Pushes the font, and return an instance of . + /// + /// The -private stack. + /// The font pointer being pushed. + /// Whether to push. + /// this. + public static SimplePushedFont Rent(List stack, ImFontPtr fontPtr, bool push) + { + push &= !fontPtr.IsNull(); + + var rented = Pool.Get(); + Debug.Assert(rented.font.IsNull(), "Rented object must not have its font set"); + rented.stack = stack; + + if (push) + { + rented.font = fontPtr; + ImGui.PushFont(fontPtr); + } + + return rented; + } + + /// + public unsafe void Dispose() + { + if (this.stack is null || !ReferenceEquals(this.stack[^1], this)) + { + throw new InvalidOperationException("Tried to pop a non-pushed font."); + } + + this.stack.RemoveAt(this.stack.Count - 1); + + if (!this.font.IsNull()) + { + if (ImGui.GetFont().NativePtr == this.font.NativePtr) + { + ImGui.PopFont(); + } + else + { + Log.Warning( + $"{nameof(IFontHandle.Pop)}: The font currently being popped does not match the pushed font. " + + $"Doing nothing."); + } + } + + this.font = default; + this.stack = null; + Pool.Return(this); + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index c27c9ab84..1134704ee 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -762,9 +762,11 @@ public sealed class UiBuilder : IDisposable public IFontHandle.ImFontLocked Lock() => this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); - public IFontHandle.FontPopper Push() => + public IDisposable Push() => this.wrapped?.Push() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + public void Pop() => this.wrapped?.Pop(); + public Task WaitAsync() => this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); From fc4d08927b82f332b81f318091226b06cd0a993c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 21 Jan 2024 15:11:31 +0900 Subject: [PATCH 458/585] Fix Dalamud Configuration revert not rebuilding fonts --- Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 027e1a571..c325028e1 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -69,6 +69,7 @@ internal class SettingsWindow : Window var fontAtlasFactory = Service.Get(); var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; + rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale); ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; fontAtlasFactory.UseAxisOverride = null; From d1291364e01ba7e031f611397eae84c1d32b64d5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 19:30:09 +0900 Subject: [PATCH 459/585] Fix FontHandleWrapper and some docs --- Dalamud/Interface/UiBuilder.cs | 22 +++++++++---------- .../Storage/Assets/IDalamudAssetManager.cs | 6 +++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 1134704ee..d01d307c3 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -744,10 +744,12 @@ public sealed class UiBuilder : IDisposable public event Action? ImFontChanged; - public Exception? LoadException => - this.wrapped!.LoadException ?? new ObjectDisposedException(nameof(FontHandleWrapper)); + public Exception? LoadException => this.WrappedNotDisposed.LoadException; - public bool Available => this.wrapped?.Available ?? false; + public bool Available => this.WrappedNotDisposed.Available; + + private IFontHandle WrappedNotDisposed => + this.wrapped ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); public void Dispose() { @@ -759,19 +761,17 @@ public sealed class UiBuilder : IDisposable // Note: do not dispose w; we do not own it } - public IFontHandle.ImFontLocked Lock() => - this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + public IFontHandle.ImFontLocked Lock() => this.WrappedNotDisposed.Lock(); - public IDisposable Push() => - this.wrapped?.Push() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); + public IDisposable Push() => this.WrappedNotDisposed.Push(); - public void Pop() => this.wrapped?.Pop(); + public void Pop() => this.WrappedNotDisposed.Pop(); public Task WaitAsync() => - this.wrapped?.WaitAsync().ContinueWith(_ => (IFontHandle)this) ?? - throw new ObjectDisposedException(nameof(FontHandleWrapper)); + this.WrappedNotDisposed.WaitAsync().ContinueWith(_ => (IFontHandle)this); - public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})"; + public override string ToString() => + $"{nameof(FontHandleWrapper)}({this.wrapped?.ToString() ?? "disposed"})"; private void WrappedOnImFontChanged(IFontHandle obj) => this.ImFontChanged.InvokeSafely(this); } diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs index 4fb83df80..1202891b8 100644 --- a/Dalamud/Storage/Assets/IDalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.Contracts; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Contracts; using System.IO; using System.Threading.Tasks; @@ -64,8 +65,9 @@ internal interface IDalamudAssetManager /// /// The texture asset. /// The default return value, if the asset is not ready for whatever reason. - /// The texture wrap. + /// The texture wrap. Can be null only if is null. [Pure] + [return: NotNullIfNotNull(nameof(defaultWrap))] IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap); /// From 5479149e79a9a91aff74ebbb1c4adf250ca93137 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 20:51:29 +0900 Subject: [PATCH 460/585] Lock font resources on Push and miscellaneous direct accesses These changes ensure that using a font under some other thread's ownership from the UI thread for rendering into ImGui purposes always work. * `FontHandle`: * Moved common code from `DelegateFontHandle` and `GamePrebakedFontHandle`. * Added `LockUntilPostFrame` so that the obtained `ImFontPtr` and its accompanying resources are kept valid until everything is rendered. * Added more code comments to `Try/Lock`. * Moved font access thread checking logic from `InterfaceManager` to `LockUntilPostFrame`. * `Push`ing a font will now also perform `LockUntilPostFrame`. * `GameFontHandle`: Make the property `ImFont` a forwarder to `FontHandle.LockUntilPostFrame`. * `InterfaceManager`: * Added companion logic to `FontHandle.LockUntilPostFrame`. * Accessing default/icon/mono fonts will forward to `FontHandle.LockUntilPostFrame`. * Changed `List` to `ConcurrentBag` as texture disposal can be done outside the main thread, and a race condition is possible. --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 26 +- .../Interface/Internal/InterfaceManager.cs | 92 +++--- .../Interface/ManagedFontAtlas/IFontHandle.cs | 21 +- .../Internals/DelegateFontHandle.cs | 135 +-------- .../ManagedFontAtlas/Internals/FontHandle.cs | 263 ++++++++++++++++++ .../Internals/GamePrebakedFontHandle.cs | 132 +-------- .../Internals/SimplePushedFont.cs | 7 +- Dalamud/Interface/UiBuilder.cs | 2 +- 8 files changed, 331 insertions(+), 347 deletions(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 7bda27eae..4c472c032 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -15,15 +15,16 @@ namespace Dalamud.Interface.GameFonts; [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public sealed class GameFontHandle : IFontHandle { - private readonly IFontHandle.IInternal fontHandle; + private readonly GamePrebakedFontHandle fontHandle; private readonly FontAtlasFactory fontAtlasFactory; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class.
+ /// Ownership of is transferred. ///
- /// The wrapped . + /// The wrapped . /// An instance of . - internal GameFontHandle(IFontHandle.IInternal fontHandle, FontAtlasFactory fontAtlasFactory) + internal GameFontHandle(GamePrebakedFontHandle fontHandle, FontAtlasFactory fontAtlasFactory) { this.fontHandle = fontHandle; this.fontAtlasFactory = fontAtlasFactory; @@ -42,9 +43,15 @@ public sealed class GameFontHandle : IFontHandle /// public bool Available => this.fontHandle.Available; - /// - [Obsolete($"Use {nameof(Push)}, and then use {nameof(ImGui.GetFont)} instead.", false)] - public ImFontPtr ImFont => this.fontHandle.ImFont; + /// + /// Gets the font.
+ /// Use of this properly is safe only from the UI thread.
+ /// Use if the intended purpose of this property is .
+ /// Futures changes may make simple not enough.
+ /// If you need to access a font outside the UI thread, use . + ///
+ [Obsolete($"Use {nameof(Push)}-{nameof(ImGui.GetFont)} or {nameof(Lock)} instead.", false)] + public ImFontPtr ImFont => this.fontHandle.LockUntilPostFrame(); /// /// Gets the font style. Only applicable for . @@ -66,10 +73,7 @@ public sealed class GameFontHandle : IFontHandle /// public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock(); - /// - /// Pushes the font. - /// - /// An that can be used to pop the font on dispose. + /// public IDisposable Push() => this.fontHandle.Push(); /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index e1b714ee8..25baa5e29 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -20,8 +21,6 @@ using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; -using Dalamud.Plugin.Internal; -using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -63,15 +62,9 @@ internal class InterfaceManager : IDisposable, IServiceType /// public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private const int NonMainThreadFontAccessWarningCheckInterval = 10000; - private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); - private static long nextNonMainThreadFontAccessWarningCheck; + private readonly ConcurrentBag deferredDisposeTextures = new(); + private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); - private readonly List deferredDisposeTextures = new(); - - [ServiceManager.ServiceDependency] - private readonly Framework framework = Service.Get(); - [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); @@ -127,34 +120,37 @@ internal class InterfaceManager : IDisposable, IServiceType /// Gets the default ImGui font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr DefaultFont => WhenFontsReady().DefaultFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr DefaultFont => + WhenFontsReady().DefaultFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included FontAwesome icon font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr IconFont => WhenFontsReady().IconFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr IconFont => + WhenFontsReady().IconFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault); /// /// Gets an included monospaced font.
/// Accessing this static property outside of the main thread is dangerous and not supported. ///
- public static ImFontPtr MonoFont => WhenFontsReady().MonoFontHandle!.ImFont.OrElse(ImGui.GetIO().FontDefault); + public static ImFontPtr MonoFont => + WhenFontsReady().MonoFontHandle!.LockUntilPostFrame().OrElse(ImGui.GetIO().FontDefault); /// /// Gets the default font handle. /// - public IFontHandle.IInternal? DefaultFontHandle { get; private set; } + public FontHandle? DefaultFontHandle { get; private set; } /// /// Gets the icon font handle. /// - public IFontHandle.IInternal? IconFontHandle { get; private set; } + public FontHandle? IconFontHandle { get; private set; } /// /// Gets the mono font handle. /// - public IFontHandle.IInternal? MonoFontHandle { get; private set; } + public FontHandle? MonoFontHandle { get; private set; } /// /// Gets or sets the pointer to ImGui.IO(), when it was last used. @@ -408,6 +404,15 @@ internal class InterfaceManager : IDisposable, IServiceType this.deferredDisposeTextures.Add(wrap); } + /// + /// Enqueue an to be disposed at the end of the frame. + /// + /// The disposable. + public void EnqueueDeferredDispose(in IFontHandle.ImFontLocked locked) + { + this.deferredDisposeImFontLockeds.Add(locked); + } + /// /// Get video memory information. /// @@ -466,29 +471,6 @@ internal class InterfaceManager : IDisposable, IServiceType if (im?.dalamudAtlas is not { } atlas) throw new InvalidOperationException($"Tried to access fonts before {nameof(ContinueConstruction)} call."); - if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) - { - nextNonMainThreadFontAccessWarningCheck = - Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; - var stack = new StackTrace(); - if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) - { - if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) - { - NonMainThreadFontAccessWarning.Add(plugin, new()); - Log.Warning( - "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", - plugin.Name, - stack); - } - } - else - { - // Dalamud internal should be made safe right now - throw new InvalidOperationException("Attempted to access fonts outside the main thread."); - } - } - if (!atlas.HasBuiltAtlas) atlas.BuildTask.GetAwaiter().GetResult(); return im; @@ -673,28 +655,38 @@ internal class InterfaceManager : IDisposable, IServiceType var pRes = this.presentHook!.Original(swapChain, syncInterval, presentFlags); RenderImGui(this.scene!); - this.DisposeTextures(); + this.CleanupPostImGuiRender(); return pRes; } RenderImGui(this.scene!); - this.DisposeTextures(); + this.CleanupPostImGuiRender(); return this.presentHook!.Original(swapChain, syncInterval, presentFlags); } - private void DisposeTextures() + private void CleanupPostImGuiRender() { - if (this.deferredDisposeTextures.Count > 0) + if (!this.deferredDisposeTextures.IsEmpty) { - Log.Verbose("[IM] Disposing {Count} textures", this.deferredDisposeTextures.Count); - foreach (var texture in this.deferredDisposeTextures) + var count = 0; + while (this.deferredDisposeTextures.TryTake(out var d)) { - texture.RealDispose(); + count++; + d.RealDispose(); } - this.deferredDisposeTextures.Clear(); + Log.Verbose("[IM] Disposing {Count} textures", count); + } + + if (!this.deferredDisposeImFontLockeds.IsEmpty) + { + // Not logging; the main purpose of this is to keep resources used for rendering the frame to be kept + // referenced until the resources are actually done being used, and it is expected that this will be + // frequent. + while (this.deferredDisposeImFontLockeds.TryTake(out var d)) + d.Dispose(); } } @@ -709,9 +701,9 @@ internal class InterfaceManager : IDisposable, IServiceType .CreateFontAtlas(nameof(InterfaceManager), FontAtlasAutoRebuildMode.Disable); using (this.dalamudAtlas.SuppressAutoRebuild()) { - this.DefaultFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); - this.IconFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddFontAwesomeIconFont( new() @@ -720,7 +712,7 @@ internal class InterfaceManager : IDisposable, IServiceType GlyphMinAdvanceX = DefaultFontSizePx, GlyphMaxAdvanceX = DefaultFontSizePx, }))); - this.MonoFontHandle = (IFontHandle.IInternal)this.dalamudAtlas.NewDelegateFontHandle( + this.MonoFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 94edc9777..97d345925 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -21,21 +21,6 @@ public interface IFontHandle : IDisposable /// event Action ImFontChanged; - /// - /// Represents a reference counting handle for fonts. Dalamud internal use only. - /// - internal interface IInternal : IFontHandle - { - /// - /// Gets the font.
- /// Use of this properly is safe only from the UI thread.
- /// Use if the intended purpose of this property is .
- /// Futures changes may make simple not enough.
- /// If you need to access a font outside the UI thread, consider using . - ///
- ImFontPtr ImFont { get; } - } - /// /// Gets the load exception, if it failed to load. Otherwise, it is null. /// @@ -45,7 +30,6 @@ public interface IFontHandle : IDisposable /// Gets a value indicating whether this font is ready for use. ///
/// - /// Once set to true, it will remain true.
/// Use directly if you want to keep the current ImGui font if the font is not ready.
/// Alternatively, use to wait for this property to become true. ///
@@ -103,14 +87,13 @@ public interface IFontHandle : IDisposable private IRefCountable? owner; /// - /// Initializes a new instance of the struct, - /// and incrase the reference count of . + /// Initializes a new instance of the struct. + /// Ownership of reference of is transferred. /// /// The contained font. /// The owner. internal ImFontLocked(ImFontPtr imFont, IRefCountable owner) { - owner.AddRef(); this.ImFont = imFont; this.owner = owner; } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index e1c18e923..53a836511 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -1,164 +1,35 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; using Dalamud.Logging.Internal; using Dalamud.Utility; using ImGuiNET; -using Serilog; - namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// /// A font handle representing a user-callback generated font. /// -internal class DelegateFontHandle : IFontHandle.IInternal +internal sealed class DelegateFontHandle : FontHandle { - private readonly List pushedFonts = new(8); - - private IFontHandleManager? manager; - private long lastCumulativePresentCalls; - /// /// Initializes a new instance of the class. /// /// An instance of . /// Callback for . public DelegateFontHandle(IFontHandleManager manager, FontAtlasBuildStepDelegate callOnBuildStepChange) + : base(manager) { - this.manager = manager; this.CallOnBuildStepChange = callOnBuildStepChange; } - /// - public event Action? ImFontChanged; - - private event Action? Disposed; - /// /// Gets the function to be called on build step changes. /// public FontAtlasBuildStepDelegate CallOnBuildStepChange { get; } - /// - public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); - - /// - public bool Available => this.ImFont.IsNotNullAndLoaded(); - - /// - public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; - - private IFontHandleManager ManagerNotDisposed => - this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); - - /// - public void Dispose() - { - if (this.pushedFonts.Count > 0) - Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); - this.manager?.FreeFontHandle(this); - this.manager = null; - this.Disposed?.InvokeSafely(this); - this.ImFontChanged = null; - } - - /// - public IFontHandle.ImFontLocked Lock() - { - IFontHandleSubstance? prevSubstance = default; - while (true) - { - var substance = this.ManagerNotDisposed.Substance; - if (substance is null) - throw new InvalidOperationException(); - if (substance == prevSubstance) - throw new ObjectDisposedException(nameof(DelegateFontHandle)); - - prevSubstance = substance; - try - { - substance.DataRoot.AddRef(); - } - catch (ObjectDisposedException) - { - continue; - } - - try - { - var fontPtr = substance.GetFontPtr(this); - if (fontPtr.IsNull()) - continue; - return new(fontPtr, substance.DataRoot); - } - finally - { - substance.DataRoot.Release(); - } - } - } - - /// - public IDisposable Push() - { - ThreadSafety.AssertMainThread(); - var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; - if (this.lastCumulativePresentCalls != cumulativePresentCalls) - { - this.lastCumulativePresentCalls = cumulativePresentCalls; - if (this.pushedFonts.Count > 0) - { - Log.Warning( - $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + - $"You might be missing a call to {nameof(this.Pop)}."); - this.pushedFonts.Clear(); - } - } - - var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); - this.pushedFonts.Add(rented); - return rented; - } - - /// - public void Pop() - { - ThreadSafety.AssertMainThread(); - this.pushedFonts[^1].Dispose(); - } - - /// - public Task WaitAsync() - { - if (this.Available) - return Task.FromResult(this); - - var tcs = new TaskCompletionSource(); - this.ImFontChanged += OnImFontChanged; - this.Disposed += OnImFontChanged; - if (this.Available) - OnImFontChanged(this); - return tcs.Task; - - void OnImFontChanged(IFontHandle unused) - { - if (tcs.Task.IsCompletedSuccessfully) - return; - - this.ImFontChanged -= OnImFontChanged; - this.Disposed -= OnImFontChanged; - if (this.manager is null) - tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); - else - tcs.SetResult(this); - } - } - /// /// Manager for s. /// @@ -216,7 +87,7 @@ internal class DelegateFontHandle : IFontHandle.IInternal return; foreach (var handle in hs.RelevantHandles) - handle.ImFontChanged?.InvokeSafely(handle); + handle.InvokeImFontChanged(); } /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs new file mode 100644 index 000000000..93b17f86e --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -0,0 +1,263 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using ImGuiNET; + +using Serilog; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Default implementation for . +/// +internal abstract class FontHandle : IFontHandle +{ + private const int NonMainThreadFontAccessWarningCheckInterval = 10000; + private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); + private static long nextNonMainThreadFontAccessWarningCheck; + + private readonly InterfaceManager interfaceManager; + private readonly List pushedFonts = new(8); + + private IFontHandleManager? manager; + private long lastCumulativePresentCalls; + + /// + /// Initializes a new instance of the class. + /// + /// An instance of . + protected FontHandle(IFontHandleManager manager) + { + this.interfaceManager = Service.Get(); + this.manager = manager; + } + + /// + public event Action? ImFontChanged; + + /// + /// Event to be called on the first call. + /// + protected event Action? Disposed; + + /// + public Exception? LoadException => this.Manager.Substance?.GetBuildException(this); + + /// + public bool Available => (this.Manager.Substance?.GetFontPtr(this) ?? default).IsNotNullAndLoaded(); + + /// + /// Gets the associated . + /// + /// When the object has already been disposed. + protected IFontHandleManager Manager => this.manager ?? throw new ObjectDisposedException(this.GetType().Name); + + /// + public void Dispose() + { + if (this.manager is null) + return; + + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Obtains an instance of corresponding to this font handle, + /// to be released after rendering the current frame. + /// + /// The font pointer, or default if unavailble. + /// + /// Behavior is undefined on access outside the main thread. + /// + public ImFontPtr LockUntilPostFrame() + { + if (this.TryLock(out _) is not { } locked) + return default; + + if (!ThreadSafety.IsMainThread && nextNonMainThreadFontAccessWarningCheck < Environment.TickCount64) + { + nextNonMainThreadFontAccessWarningCheck = + Environment.TickCount64 + NonMainThreadFontAccessWarningCheckInterval; + var stack = new StackTrace(); + if (Service.GetNullable()?.FindCallingPlugin(stack) is { } plugin) + { + if (!NonMainThreadFontAccessWarning.TryGetValue(plugin, out _)) + { + NonMainThreadFontAccessWarning.Add(plugin, new()); + Log.Warning( + "[IM] {pluginName}: Accessing fonts outside the main thread is deprecated.\n{stack}", + plugin.Name, + stack); + } + } + else + { + // Dalamud internal should be made safe right now + throw new InvalidOperationException("Attempted to access fonts outside the main thread."); + } + } + + this.interfaceManager.EnqueueDeferredDispose(locked); + return locked.ImFont; + } + + /// + /// Attempts to lock the fully constructed instance of corresponding to the this + /// , for use in any thread.
+ /// Modification of the font will exhibit undefined behavior if some other thread also uses the font. + ///
+ /// The error message, if any. + /// + /// An instance of that must be disposed after use on success; + /// null with populated on failure. + /// + /// Still may be thrown. + public IFontHandle.ImFontLocked? TryLock(out string? errorMessage) + { + IFontHandleSubstance? prevSubstance = default; + while (true) + { + var substance = this.Manager.Substance; + + // Does the associated IFontAtlas have a built substance? + if (substance is null) + { + errorMessage = "The font atlas has not been built yet."; + return null; + } + + // Did we loop (because it did not have the requested font), + // and are the fetched substance same between loops? + if (substance == prevSubstance) + { + errorMessage = "The font atlas did not built the requested handle yet."; + return null; + } + + prevSubstance = substance; + + // Try to lock the substance. + try + { + substance.DataRoot.AddRef(); + } + catch (ObjectDisposedException) + { + // If it got invalidated, it's probably because a new substance is incoming. Try again. + continue; + } + + var fontPtr = substance.GetFontPtr(this); + if (fontPtr.IsNull()) + { + // The font for the requested handle is unavailable. Release the reference and try again. + substance.DataRoot.Release(); + continue; + } + + // Transfer the ownership of reference. + errorMessage = null; + return new(fontPtr, substance.DataRoot); + } + } + + /// + public IFontHandle.ImFontLocked Lock() => + this.TryLock(out var errorMessage) ?? throw new InvalidOperationException(errorMessage); + + /// + public IDisposable Push() + { + ThreadSafety.AssertMainThread(); + + // Warn if the client is not properly managing the pushed font stack. + var cumulativePresentCalls = this.interfaceManager.CumulativePresentCalls; + if (this.lastCumulativePresentCalls != cumulativePresentCalls) + { + this.lastCumulativePresentCalls = cumulativePresentCalls; + if (this.pushedFonts.Count > 0) + { + Log.Warning( + $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + + $"You might be missing a call to {nameof(this.Pop)}."); + this.pushedFonts.Clear(); + } + } + + var font = default(ImFontPtr); + if (this.TryLock(out _) is { } locked) + { + font = locked.ImFont; + this.interfaceManager.EnqueueDeferredDispose(locked); + } + + var rented = SimplePushedFont.Rent(this.pushedFonts, font); + this.pushedFonts.Add(rented); + return rented; + } + + /// + public void Pop() + { + ThreadSafety.AssertMainThread(); + this.pushedFonts[^1].Dispose(); + } + + /// + public Task WaitAsync() + { + if (this.Available) + return Task.FromResult(this); + + var tcs = new TaskCompletionSource(); + this.ImFontChanged += OnImFontChanged; + this.Disposed += OnImFontChanged; + if (this.Available) + OnImFontChanged(this); + return tcs.Task; + + void OnImFontChanged(IFontHandle unused) + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnImFontChanged; + if (this.manager is null) + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + else + tcs.SetResult(this); + } + } + + /// + /// Invokes . + /// + protected void InvokeImFontChanged() => this.ImFontChanged.InvokeSafely(this); + + /// + /// Overrideable implementation for . + /// + /// If true, then the function is being called from . + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (this.pushedFonts.Count > 0) + Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); + this.Manager.FreeFontHandle(this); + this.manager = null; + this.Disposed?.InvokeSafely(this); + this.ImFontChanged = null; + } + } +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index 0e8301785..e062405b8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reactive.Disposables; -using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.GameFonts; @@ -16,8 +15,6 @@ using ImGuiNET; using Lumina.Data.Files; -using Serilog; - using Vector4 = System.Numerics.Vector4; namespace Dalamud.Interface.ManagedFontAtlas.Internals; @@ -25,7 +22,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// /// A font handle that uses the game's built-in fonts, optionally with some styling. /// -internal class GamePrebakedFontHandle : IFontHandle.IInternal +internal class GamePrebakedFontHandle : FontHandle { /// /// The smallest value of . @@ -37,17 +34,13 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal /// public static readonly char SeIconCharMax = (char)Enum.GetValues().Max(); - private readonly List pushedFonts = new(8); - - private IFontHandleManager? manager; - private long lastCumulativePresentCalls; - /// /// Initializes a new instance of the class. /// /// An instance of . /// Font to use. public GamePrebakedFontHandle(IFontHandleManager manager, GameFontStyle style) + : base(manager) { if (!Enum.IsDefined(style.FamilyAndSize) || style.FamilyAndSize == GameFontFamilyAndSize.Undefined) throw new ArgumentOutOfRangeException(nameof(style), style, null); @@ -55,15 +48,9 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal if (style.SizePt <= 0) throw new ArgumentException($"{nameof(style.SizePt)} must be a positive number.", nameof(style)); - this.manager = manager; this.FontStyle = style; } - /// - public event Action? ImFontChanged; - - private event Action? Disposed; - /// /// Provider for for `common/font/fontNN.tex`. /// @@ -107,119 +94,6 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal ///
public GameFontStyle FontStyle { get; } - /// - public Exception? LoadException => this.ManagerNotDisposed.Substance?.GetBuildException(this); - - /// - public bool Available => this.ImFont.IsNotNullAndLoaded(); - - /// - public ImFontPtr ImFont => this.ManagerNotDisposed.Substance?.GetFontPtr(this) ?? default; - - private IFontHandleManager ManagerNotDisposed => - this.manager ?? throw new ObjectDisposedException(nameof(GamePrebakedFontHandle)); - - /// - public void Dispose() - { - this.manager?.FreeFontHandle(this); - this.manager = null; - this.Disposed?.InvokeSafely(this); - this.ImFontChanged = null; - } - - /// - public IFontHandle.ImFontLocked Lock() - { - IFontHandleSubstance? prevSubstance = default; - while (true) - { - var substance = this.ManagerNotDisposed.Substance; - if (substance is null) - throw new InvalidOperationException(); - if (substance == prevSubstance) - throw new ObjectDisposedException(nameof(DelegateFontHandle)); - - prevSubstance = substance; - try - { - substance.DataRoot.AddRef(); - } - catch (ObjectDisposedException) - { - continue; - } - - try - { - var fontPtr = substance.GetFontPtr(this); - if (fontPtr.IsNull()) - continue; - return new(fontPtr, substance.DataRoot); - } - finally - { - substance.DataRoot.Release(); - } - } - } - - /// - public IDisposable Push() - { - ThreadSafety.AssertMainThread(); - var cumulativePresentCalls = Service.GetNullable()?.CumulativePresentCalls ?? 0L; - if (this.lastCumulativePresentCalls != cumulativePresentCalls) - { - this.lastCumulativePresentCalls = cumulativePresentCalls; - if (this.pushedFonts.Count > 0) - { - Log.Warning( - $"{nameof(this.Push)} has been called, but the handle-private stack was not empty. " + - $"You might be missing a call to {nameof(this.Pop)}."); - this.pushedFonts.Clear(); - } - } - - var rented = SimplePushedFont.Rent(this.pushedFonts, this.ImFont, this.Available); - this.pushedFonts.Add(rented); - return rented; - } - - /// - public void Pop() - { - ThreadSafety.AssertMainThread(); - this.pushedFonts[^1].Dispose(); - } - - /// - public Task WaitAsync() - { - if (this.Available) - return Task.FromResult(this); - - var tcs = new TaskCompletionSource(); - this.ImFontChanged += OnImFontChanged; - this.Disposed += OnImFontChanged; - if (this.Available) - OnImFontChanged(this); - return tcs.Task; - - void OnImFontChanged(IFontHandle unused) - { - if (tcs.Task.IsCompletedSuccessfully) - return; - - this.ImFontChanged -= OnImFontChanged; - this.Disposed -= OnImFontChanged; - if (this.manager is null) - tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); - else - tcs.SetResult(this); - } - } - /// public override string ToString() => $"{nameof(GamePrebakedFontHandle)}({this.FontStyle})"; @@ -305,7 +179,7 @@ internal class GamePrebakedFontHandle : IFontHandle.IInternal return; foreach (var handle in hs.RelevantHandles) - handle.ImFontChanged?.InvokeSafely(handle); + handle.InvokeImFontChanged(); } /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs index 3f7255386..0642e7be1 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs @@ -28,17 +28,14 @@ internal sealed class SimplePushedFont : IDisposable ///
/// The -private stack. /// The font pointer being pushed. - /// Whether to push. /// this. - public static SimplePushedFont Rent(List stack, ImFontPtr fontPtr, bool push) + public static SimplePushedFont Rent(List stack, ImFontPtr fontPtr) { - push &= !fontPtr.IsNull(); - var rented = Pool.Get(); Debug.Assert(rented.font.IsNull(), "Rented object must not have its font set"); rented.stack = stack; - if (push) + if (fontPtr.IsNotNullAndLoaded()) { rented.font = fontPtr; ImGui.PushFont(fontPtr); diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 1134704ee..ea3803f35 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -498,7 +498,7 @@ public sealed class UiBuilder : IDisposable [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (IFontHandle.IInternal)this.FontAtlas.NewGameFontHandle(style), + (GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style), Service.Get()); /// From fb8beb9370865652cf96fb57975d85885a954dbe Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 22:09:47 +0900 Subject: [PATCH 461/585] Move PostPromotion modification functions to PostBuild These changes are done to ensure that `IFontHandle.Lock` will be guaranteed to obtain a fully built font that will not be modified any further (unless `PostPromotion` is being used for modifying fonts, which should not be done by clients.) * Moved `CopyGlyphsAcrossFonts` and `BuildLookupTable` from `PostPromotion` to `PostBuild` build toolkit. * `IFontAtlasBuildToolkit`: Added `GetFont` to enable retrieving font corresponding to a handle being built. * `InterfaceManager`: Use `OnPostBuild` for copying glyphs from Mono to Default. * `FontAtlasBuildStep`: * Removed `Invalid` to prevent an unnecessary switch-case warnings. * Added contracts on when `IFontAtlas.BuildStepChanged` will be called. --- .../Interface/Internal/InterfaceManager.cs | 40 +++---- .../ManagedFontAtlas/FontAtlasBuildStep.cs | 19 +-- .../IFontAtlasBuildToolkit.cs | 8 ++ .../IFontAtlasBuildToolkitPostBuild.cs | 24 ++++ .../IFontAtlasBuildToolkitPostPromotion.cs | 26 +--- .../FontAtlasFactory.BuildToolkit.cs | 112 +++++++++++------- .../FontAtlasFactory.Implementation.cs | 85 ++++++------- Dalamud/Interface/UiBuilder.cs | 2 + 8 files changed, 180 insertions(+), 136 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 25baa5e29..93050d67a 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -717,28 +717,28 @@ internal class InterfaceManager : IDisposable, IServiceType tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, new() { SizePx = DefaultFontSizePx }))); - this.dalamudAtlas.BuildStepChange += e => e.OnPostPromotion( - tk => - { - // Note: the first call of this function is done outside the main thread; this is expected. - // Do not use DefaultFont, IconFont, and MonoFont. - // Use font handles directly. - - using var defaultFont = this.DefaultFontHandle.Lock(); - using var monoFont = this.MonoFontHandle.Lock(); - - // Fill missing glyphs in MonoFont from DefaultFont - tk.CopyGlyphsAcrossFonts(defaultFont, monoFont, true); - - // Update default font - unsafe + this.dalamudAtlas.BuildStepChange += e => e + .OnPostBuild( + tk => { - ImGui.GetIO().NativePtr->FontDefault = defaultFont; - } + // Fill missing glyphs in MonoFont from DefaultFont. + tk.CopyGlyphsAcrossFonts( + tk.GetFont(this.DefaultFontHandle), + tk.GetFont(this.MonoFontHandle), + missingOnly: true); + }) + .OnPostPromotion( + tk => + { + // Update the ImGui default font. + unsafe + { + ImGui.GetIO().NativePtr->FontDefault = tk.GetFont(this.DefaultFontHandle); + } - // Broadcast to auto-rebuilding instances - this.AfterBuildFonts?.Invoke(); - }); + // Broadcast to auto-rebuilding instances. + this.AfterBuildFonts?.Invoke(); + }); } // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs index 345ab729d..ba94db435 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs @@ -7,32 +7,35 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public enum FontAtlasBuildStep { - /// - /// An invalid value. This should never be passed through event callbacks. - /// - Invalid, + // Note: leave 0 alone; make default(FontAtlasBuildStep) not have a valid value /// /// Called before calling .
- /// Expect to be passed. + /// Expect to be passed.
+ /// When called from , this will be called before the delegates + /// passed to . ///
- PreBuild, + PreBuild = 1, /// /// Called after calling .
/// Expect to be passed.
+ /// When called from , this will be called after the delegates + /// passed to ; you can do cross-font operations here.
///
/// This callback is not guaranteed to happen after , /// but it will never happen on its own. ///
- PostBuild, + PostBuild = 2, /// /// Called after promoting staging font atlas to the actual atlas for .
/// Expect to be passed.
+ /// When called from , this will be called after the delegates + /// passed to ; you should not make modifications to fonts.
///
/// This callback is not guaranteed to happen after , /// but it will never happen on its own. ///
- PostPromotion, + PostPromotion = 3, } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs index a997c48c1..f75ed4686 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -80,4 +80,12 @@ public interface IFontAtlasBuildToolkit ///
/// The action to run on dispose. void DisposeWithAtlas(Action action); + + /// + /// Gets the instance of corresponding to + /// from . + /// + /// The font handle. + /// The corresonding , or default if not found. + ImFontPtr GetFont(IFontHandle fontHandle); } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs index 3c14197e0..eb7c7e08c 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -23,4 +23,28 @@ public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit /// Dispose the wrap on error. /// The texture index. int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError); + + /// + /// Copies glyphs across fonts, in a safer way.
+ /// If the font does not belong to the current atlas, this function is a no-op. + ///
+ /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + char rangeLow = ' ', + char rangeHigh = '\uFFFE'); + + /// + /// Calls , with some fixups. + /// + /// The font. + void BuildLookupTable(ImFontPtr font); } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs index 8c3c91624..930851fc7 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs @@ -1,5 +1,3 @@ -using ImGuiNET; - namespace Dalamud.Interface.ManagedFontAtlas; /// @@ -7,27 +5,5 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit { - /// - /// Copies glyphs across fonts, in a safer way.
- /// If the font does not belong to the current atlas, this function is a no-op. - ///
- /// Source font. - /// Target font. - /// Whether to copy missing glyphs only. - /// Whether to call target.BuildLookupTable(). - /// Low codepoint range to copy. - /// High codepoing range to copy. - void CopyGlyphsAcrossFonts( - ImFontPtr source, - ImFontPtr target, - bool missingOnly, - bool rebuildLookupTable = true, - char rangeLow = ' ', - char rangeHigh = '\uFFFE'); - - /// - /// Calls , with some fixups. - /// - /// The font. - void BuildLookupTable(ImFontPtr font); + // empty } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index fde115c9e..3addfabe8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -135,6 +135,19 @@ internal sealed partial class FontAtlasFactory } } + /// + public ImFontPtr GetFont(IFontHandle fontHandle) + { + foreach (var s in this.data.Substances) + { + var f = s.GetFontPtr(fontHandle); + if (!f.IsNull()) + return f; + } + + return default; + } + /// public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) { @@ -608,49 +621,6 @@ internal sealed partial class FontAtlasFactory ArrayPool.Shared.Return(buf); } } - } - - /// - /// Implementations for . - /// - private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion - { - private readonly FontAtlasBuiltData builtData; - - /// - /// Initializes a new instance of the class. - /// - /// The built data. - public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; - - /// - public ImFontPtr Font { get; set; } - - /// - public float Scale => this.builtData.Scale; - - /// - public bool IsAsyncBuildOperation => true; - - /// - public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; - - /// - public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; - - /// - public unsafe ImVectorWrapper Fonts => new( - &this.NewImAtlas.NativePtr->Fonts, - x => ImGuiNative.ImFont_destroy(x->NativePtr)); - - /// - public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); - - /// - public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); - - /// - public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); /// public unsafe void CopyGlyphsAcrossFonts( @@ -707,4 +677,60 @@ internal sealed partial class FontAtlasFactory } } } + + /// + /// Implementations for . + /// + private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion + { + private readonly FontAtlasBuiltData builtData; + + /// + /// Initializes a new instance of the class. + /// + /// The built data. + public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; + + /// + public ImFontPtr Font { get; set; } + + /// + public float Scale => this.builtData.Scale; + + /// + public bool IsAsyncBuildOperation => true; + + /// + public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; + + /// + public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; + + /// + public unsafe ImVectorWrapper Fonts => new( + &this.NewImAtlas.NativePtr->Fonts, + x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); + + /// + public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); + + /// + public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); + + /// + public ImFontPtr GetFont(IFontHandle fontHandle) + { + foreach (var s in this.builtData.Substances) + { + var f = s.GetFontPtr(fontHandle); + if (!f.IsNull()) + return f; + } + + return default; + } + } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 99ce8dab9..85f7219b2 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reactive.Disposables; -using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.GameFonts; @@ -168,7 +167,7 @@ internal sealed partial class FontAtlasFactory _ => throw new InvalidOperationException(), }; - public unsafe int Release() + public int Release() { switch (IRefCountable.AlterRefCount(-1, ref this.refCount, out var newRefCount)) { @@ -176,22 +175,35 @@ internal sealed partial class FontAtlasFactory return newRefCount; case IRefCountable.RefCountResult.FinalRelease: - if (this.IsBuildInProgress) - { - Log.Error( - "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; waiting for build.\n" + - "Stack:\n{trace}", - this.Owner?.Name ?? "", - (nint)this.Atlas.NativePtr, - new StackTrace()); - while (this.IsBuildInProgress) - Thread.Sleep(100); - } - #if VeryVerboseLog Log.Verbose("[{name}] 0x{ptr:X}: Disposing", this.Owner?.Name ?? "", (nint)this.Atlas.NativePtr); #endif - this.Garbage.Dispose(); + + if (this.IsBuildInProgress) + { + unsafe + { + Log.Error( + "[{name}] 0x{ptr:X}: Trying to dispose while build is in progress; disposing later.\n" + + "Stack:\n{trace}", + this.Owner?.Name ?? "", + (nint)this.Atlas.NativePtr, + new StackTrace()); + } + + Task.Run( + async () => + { + while (this.IsBuildInProgress) + await Task.Delay(100); + this.Garbage.Dispose(); + }); + } + else + { + this.Garbage.Dispose(); + } + return newRefCount; case IRefCountable.RefCountResult.AlreadyDisposed: @@ -549,20 +561,10 @@ internal sealed partial class FontAtlasFactory return; } - var toolkit = new BuildToolkitPostPromotion(data); + foreach (var substance in data.Substances) + substance.Manager.InvokeFontHandleImFontChanged(); - try - { - this.BuildStepChange?.Invoke(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {delegateName} PostPromotion error", - this.Name, - nameof(FontAtlasBuildStepDelegate)); - } + var toolkit = new BuildToolkitPostPromotion(data); foreach (var substance in data.Substances) { @@ -580,20 +582,18 @@ internal sealed partial class FontAtlasFactory } } - foreach (var font in toolkit.Fonts) + try { - try - { - toolkit.BuildLookupTable(font); - } - catch (Exception e) - { - Log.Error(e, "[{name}] BuildLookupTable error", this.Name); - } + this.BuildStepChange?.Invoke(toolkit); + } + catch (Exception e) + { + Log.Error( + e, + "[{name}] {delegateName} PostPromotion error", + this.Name, + nameof(FontAtlasBuildStepDelegate)); } - - foreach (var substance in data.Substances) - substance.Manager.InvokeFontHandleImFontChanged(); #if VeryVerboseLog Log.Verbose("[{name}] Built from {source}.", this.Name, source); @@ -709,6 +709,9 @@ internal sealed partial class FontAtlasFactory toolkit.PostBuildSubstances(); this.BuildStepChange?.Invoke(toolkit); + foreach (var font in toolkit.Fonts) + toolkit.BuildLookupTable(font); + if (this.factory.SceneTask is { IsCompleted: false } sceneTask) { Log.Verbose( @@ -754,6 +757,8 @@ internal sealed partial class FontAtlasFactory } finally { + // RS is being dumb + // ReSharper disable once ConstantConditionalAccessQualifier toolkit?.Dispose(); this.buildQueued = false; } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index ea3803f35..af4cc39c2 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -700,6 +700,8 @@ public sealed class UiBuilder : IDisposable if (e.IsAsyncBuildOperation) return; + ThreadSafety.AssertMainThread(); + if (this.BuildFonts is not null) { e.OnPreBuild( From 871deca6e936e0528bb8e4cbd6031935a3099dd3 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 23:00:47 +0900 Subject: [PATCH 462/585] Remove PostPromotion event `PostPromotion` is removed, as `IFontHandle.ImFontChanged` now does the job. It also removes the possibility that resources may get disposed while post promotion callback is in progress. * `IFontHandle.ImFontChanged` is now called with a locked instance of the font. * `IFontHandle.ImFontLocked`: Added `NewRef` to increase reference count. --- Dalamud.CorePlugin/PluginImpl.cs | 2 +- Dalamud/Interface/GameFonts/GameFontHandle.cs | 2 +- .../Interface/Internal/InterfaceManager.cs | 46 ++++++------ .../ManagedFontAtlas/FontAtlasBuildStep.cs | 11 --- .../FontAtlasBuildStepDelegate.cs | 7 +- .../FontAtlasBuildToolkitUtilities.cs | 17 ----- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 6 +- .../IFontAtlasBuildToolkitPostPromotion.cs | 9 --- .../Interface/ManagedFontAtlas/IFontHandle.cs | 29 ++++++-- .../Internals/DelegateFontHandle.cs | 40 +---------- .../FontAtlasFactory.BuildToolkit.cs | 56 --------------- .../FontAtlasFactory.Implementation.cs | 71 +++++-------------- .../ManagedFontAtlas/Internals/FontHandle.cs | 66 ++++++++++++----- .../Internals/GamePrebakedFontHandle.cs | 19 +---- .../Internals/IFontHandleManager.cs | 5 -- .../Internals/IFontHandleSubstance.cs | 16 ++--- Dalamud/Interface/UiBuilder.cs | 5 +- 17 files changed, 138 insertions(+), 269 deletions(-) delete mode 100644 Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 96d212dd3..cb9b4368a 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -69,7 +69,7 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; this.Interface.UiBuilder.OpenMainUi += this.OnOpenMainUi; - this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += fc => + this.Interface.UiBuilder.DefaultFontHandle.ImFontChanged += (fc, _) => { Log.Information($"CorePlugin : DefaultFontHandle.ImFontChanged called {fc}"); }; diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 4c472c032..679452ba4 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -31,7 +31,7 @@ public sealed class GameFontHandle : IFontHandle } /// - public event Action ImFontChanged + public event IFontHandle.ImFontChangedDelegate ImFontChanged { add => this.fontHandle.ImFontChanged += value; remove => this.fontHandle.ImFontChanged -= value; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 93050d67a..18bb95799 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -79,6 +79,7 @@ internal class InterfaceManager : IDisposable, IServiceType private Hook? resizeBuffersHook; private IFontAtlas? dalamudAtlas; + private IFontHandle.ImFontLocked defaultFontResourceLock; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -717,32 +718,35 @@ internal class InterfaceManager : IDisposable, IServiceType tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, new() { SizePx = DefaultFontSizePx }))); - this.dalamudAtlas.BuildStepChange += e => e - .OnPostBuild( - tk => + this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild( + tk => + { + // Fill missing glyphs in MonoFont from DefaultFont. + tk.CopyGlyphsAcrossFonts( + tk.GetFont(this.DefaultFontHandle), + tk.GetFont(this.MonoFontHandle), + missingOnly: true); + }); + this.DefaultFontHandle.ImFontChanged += (_, font) => Service.Get().RunOnFrameworkThread( + () => + { + // Update the ImGui default font. + unsafe { - // Fill missing glyphs in MonoFont from DefaultFont. - tk.CopyGlyphsAcrossFonts( - tk.GetFont(this.DefaultFontHandle), - tk.GetFont(this.MonoFontHandle), - missingOnly: true); - }) - .OnPostPromotion( - tk => - { - // Update the ImGui default font. - unsafe - { - ImGui.GetIO().NativePtr->FontDefault = tk.GetFont(this.DefaultFontHandle); - } + ImGui.GetIO().NativePtr->FontDefault = font; + } - // Broadcast to auto-rebuilding instances. - this.AfterBuildFonts?.Invoke(); - }); + // Update the reference to the resources of the default font. + this.defaultFontResourceLock.Dispose(); + this.defaultFontResourceLock = font.NewRef(); + + // Broadcast to auto-rebuilding instances. + this.AfterBuildFonts?.Invoke(); + }); } // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. - _ = this.dalamudAtlas.BuildFontsAsync(false); + _ = this.dalamudAtlas.BuildFontsAsync(); this.address.Setup(sigScanner); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs index ba94db435..dcfcc32e3 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStep.cs @@ -27,15 +27,4 @@ public enum FontAtlasBuildStep /// but it will never happen on its own. /// PostBuild = 2, - - /// - /// Called after promoting staging font atlas to the actual atlas for .
- /// Expect to be passed.
- /// When called from , this will be called after the delegates - /// passed to ; you should not make modifications to fonts.
- ///
- /// This callback is not guaranteed to happen after , - /// but it will never happen on its own. - ///
- PostPromotion = 3, } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs index 4f5b34061..2ed88102f 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildStepDelegate.cs @@ -6,10 +6,9 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// A toolkit that may help you for font building steps. /// /// An implementation of may implement all of -/// , , and -/// .
+/// and .
/// Either use to identify the build step, or use -/// , , -/// and for routing. +/// and +/// for routing. ///
public delegate void FontAtlasBuildStepDelegate(IFontAtlasBuildToolkit toolkit); diff --git a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs index 586887a3b..4c3e9023a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs +++ b/Dalamud/Interface/ManagedFontAtlas/FontAtlasBuildToolkitUtilities.cs @@ -113,21 +113,4 @@ public static class FontAtlasBuildToolkitUtilities action.Invoke((IFontAtlasBuildToolkitPostBuild)toolkit); return toolkit; } - - /// - /// Invokes - /// if of - /// is . - /// - /// The toolkit. - /// The action. - /// toolkit, for method chaining. - public static IFontAtlasBuildToolkit OnPostPromotion( - this IFontAtlasBuildToolkit toolkit, - Action action) - { - if (toolkit.BuildStep is FontAtlasBuildStep.PostPromotion) - action.Invoke((IFontAtlasBuildToolkitPostPromotion)toolkit); - return toolkit; - } } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index 491292f9d..a9c21f94e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -129,8 +129,7 @@ public interface IFontAtlas : IDisposable void BuildFontsOnNextFrame(); /// - /// Rebuilds fonts immediately, on the current thread.
- /// Even the callback for will be called on the same thread. + /// Rebuilds fonts immediately, on the current thread. ///
/// If is . void BuildFontsImmediately(); @@ -138,8 +137,7 @@ public interface IFontAtlas : IDisposable /// /// Rebuilds fonts asynchronously, on any thread. /// - /// Call on the main thread. /// The task. /// If is . - Task BuildFontsAsync(bool callPostPromotionOnMainThread = true); + Task BuildFontsAsync(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs deleted file mode 100644 index 930851fc7..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostPromotion.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Dalamud.Interface.ManagedFontAtlas; - -/// -/// Toolkit for use when the build state is . -/// -public interface IFontAtlasBuildToolkitPostPromotion : IFontAtlasBuildToolkit -{ - // empty -} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 97d345925..d58a89e56 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -12,14 +12,17 @@ namespace Dalamud.Interface.ManagedFontAtlas; public interface IFontHandle : IDisposable { /// - /// Called when the built instance of has been changed.
- /// This event will be invoked on the same thread with - /// ., - /// when the build step is .
- /// See , , and - /// . + /// Delegate for . ///
- event Action ImFontChanged; + /// The relevant font handle. + /// The locked font for this font handle, locked during the call of this delegate. + public delegate void ImFontChangedDelegate(IFontHandle fontHandle, ImFontLocked lockedFont); + + /// + /// Called when the built instance of has been changed.
+ /// This event can be invoked outside the main thread. + ///
+ event ImFontChangedDelegate ImFontChanged; /// /// Gets the load exception, if it failed to load. Otherwise, it is null. @@ -102,6 +105,18 @@ public interface IFontHandle : IDisposable public static unsafe implicit operator ImFont*(ImFontLocked l) => l.ImFont.NativePtr; + /// + /// Creates a new instance of with an additional reference to the owner. + /// + /// The new locked instance. + public readonly ImFontLocked NewRef() + { + if (this.owner is null) + throw new ObjectDisposedException(nameof(ImFontLocked)); + this.owner.AddRef(); + return new(this.ImFont, this.owner); + } + /// public void Dispose() { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs index 53a836511..b13c60a53 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/DelegateFontHandle.cs @@ -80,16 +80,6 @@ internal sealed class DelegateFontHandle : FontHandle this.handles.Remove(cgfh); } - /// - public void InvokeFontHandleImFontChanged() - { - if (this.Substance is not HandleSubstance hs) - return; - - foreach (var handle in hs.RelevantHandles) - handle.InvokeImFontChanged(); - } - /// public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { @@ -133,6 +123,9 @@ internal sealed class DelegateFontHandle : FontHandle // Not owned by this class. Do not dispose. public DelegateFontHandle[] RelevantHandles { get; } + /// + ICollection IFontHandleSubstance.RelevantHandles => this.RelevantHandles; + /// public IRefCountable DataRoot { get; } @@ -306,32 +299,5 @@ internal sealed class DelegateFontHandle : FontHandle } } } - - /// - public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) - { - foreach (var k in this.RelevantHandles) - { - if (!this.fonts[k].IsNotNullAndLoaded()) - continue; - - try - { - toolkitPostPromotion.Font = this.fonts[k]; - k.CallOnBuildStepChange.Invoke(toolkitPostPromotion); - } - catch (Exception e) - { - this.fonts[k] = default; - this.buildExceptions[k] = e; - - Log.Error( - e, - "[{name}:Substance] An error has occurred while during {delegate} PostPromotion call.", - this.Manager.Name, - nameof(FontAtlasBuildStepDelegate)); - } - } - } } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 3addfabe8..e2b096701 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -677,60 +677,4 @@ internal sealed partial class FontAtlasFactory } } } - - /// - /// Implementations for . - /// - private class BuildToolkitPostPromotion : IFontAtlasBuildToolkitPostPromotion - { - private readonly FontAtlasBuiltData builtData; - - /// - /// Initializes a new instance of the class. - /// - /// The built data. - public BuildToolkitPostPromotion(FontAtlasBuiltData builtData) => this.builtData = builtData; - - /// - public ImFontPtr Font { get; set; } - - /// - public float Scale => this.builtData.Scale; - - /// - public bool IsAsyncBuildOperation => true; - - /// - public FontAtlasBuildStep BuildStep => FontAtlasBuildStep.PostPromotion; - - /// - public ImFontAtlasPtr NewImAtlas => this.builtData.Atlas; - - /// - public unsafe ImVectorWrapper Fonts => new( - &this.NewImAtlas.NativePtr->Fonts, - x => ImGuiNative.ImFont_destroy(x->NativePtr)); - - /// - public T DisposeWithAtlas(T disposable) where T : IDisposable => this.builtData.Garbage.Add(disposable); - - /// - public GCHandle DisposeWithAtlas(GCHandle gcHandle) => this.builtData.Garbage.Add(gcHandle); - - /// - public void DisposeWithAtlas(Action action) => this.builtData.Garbage.Add(action); - - /// - public ImFontPtr GetFont(IFontHandle fontHandle) - { - foreach (var s in this.builtData.Substances) - { - var f = s.GetFontPtr(fontHandle); - if (!f.IsNull()) - return f; - } - - return default; - } - } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 85f7219b2..4e98bf226 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -486,7 +486,7 @@ internal sealed partial class FontAtlasFactory } /// - public Task BuildFontsAsync(bool callPostPromotionOnMainThread = true) + public Task BuildFontsAsync() { #if VeryVerboseLog Log.Verbose("[{name}] Called: {source}.", this.Name, nameof(this.BuildFontsAsync)); @@ -519,15 +519,7 @@ internal sealed partial class FontAtlasFactory if (res.Atlas.IsNull()) return res; - if (callPostPromotionOnMainThread) - { - await this.factory.Framework.RunOnFrameworkThread( - () => this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync))); - } - else - { - this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); - } + this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); return res; } @@ -536,6 +528,10 @@ internal sealed partial class FontAtlasFactory private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) { + // Capture the locks inside the lock block, so that the fonts are guaranteed to be the ones just built. + var fontsAndLocks = new List<(FontHandle FontHandle, IFontHandle.ImFontLocked Lock)>(); + using var garbage = new DisposeSafety.ScopedFinalizer(); + lock (this.syncRoot) { if (this.buildIndex != rebuildIndex) @@ -549,56 +545,25 @@ internal sealed partial class FontAtlasFactory prevBuiltData.ExplicitDisposeIgnoreExceptions(); this.buildTask = EmptyTask; + fontsAndLocks.EnsureCapacity(data.Substances.Sum(x => x.RelevantHandles.Count)); foreach (var substance in data.Substances) + { substance.Manager.Substance = substance; + foreach (var fontHandle in substance.RelevantHandles) + { + substance.DataRoot.AddRef(); + var locked = new IFontHandle.ImFontLocked(substance.GetFontPtr(fontHandle), substance.DataRoot); + fontsAndLocks.Add((fontHandle, garbage.Add(locked))); + } + } } - lock (this.syncRootPostPromotion) - { - if (this.buildIndex != rebuildIndex) - { - data.ExplicitDisposeIgnoreExceptions(); - return; - } - - foreach (var substance in data.Substances) - substance.Manager.InvokeFontHandleImFontChanged(); - - var toolkit = new BuildToolkitPostPromotion(data); - - foreach (var substance in data.Substances) - { - try - { - substance.OnPostPromotion(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {substance} PostPromotion error", - this.Name, - substance.GetType().FullName ?? substance.GetType().Name); - } - } - - try - { - this.BuildStepChange?.Invoke(toolkit); - } - catch (Exception e) - { - Log.Error( - e, - "[{name}] {delegateName} PostPromotion error", - this.Name, - nameof(FontAtlasBuildStepDelegate)); - } + foreach (var (fontHandle, lockedFont) in fontsAndLocks) + fontHandle.InvokeImFontChanged(lockedFont); #if VeryVerboseLog - Log.Verbose("[{name}] Built from {source}.", this.Name, source); + Log.Verbose("[{name}] Built from {source}.", this.Name, source); #endif - } } private void ImGuiSceneOnNewRenderFrame() diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 93b17f86e..d01b0df87 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -41,12 +41,12 @@ internal abstract class FontHandle : IFontHandle } /// - public event Action? ImFontChanged; + public event IFontHandle.ImFontChangedDelegate? ImFontChanged; /// /// Event to be called on the first call. /// - protected event Action? Disposed; + protected event Action? Disposed; /// public Exception? LoadException => this.Manager.Substance?.GetBuildException(this); @@ -70,6 +70,22 @@ internal abstract class FontHandle : IFontHandle GC.SuppressFinalize(this); } + /// + /// Invokes . + /// + /// The font, locked during the call of . + public void InvokeImFontChanged(IFontHandle.ImFontLocked font) + { + try + { + this.ImFontChanged?.Invoke(this, font); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(this.InvokeImFontChanged)}: error"); + } + } + /// /// Obtains an instance of corresponding to this font handle, /// to be released after rendering the current frame. @@ -220,35 +236,51 @@ internal abstract class FontHandle : IFontHandle var tcs = new TaskCompletionSource(); this.ImFontChanged += OnImFontChanged; - this.Disposed += OnImFontChanged; + this.Disposed += OnDisposed; if (this.Available) - OnImFontChanged(this); + OnImFontChanged(this, default); return tcs.Task; - void OnImFontChanged(IFontHandle unused) + void OnImFontChanged(IFontHandle unused, IFontHandle.ImFontLocked unused2) { if (tcs.Task.IsCompletedSuccessfully) return; this.ImFontChanged -= OnImFontChanged; - this.Disposed -= OnImFontChanged; - if (this.manager is null) - tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); - else + this.Disposed -= OnDisposed; + try + { tcs.SetResult(this); + } + catch + { + // ignore + } + } + + void OnDisposed() + { + if (tcs.Task.IsCompletedSuccessfully) + return; + + this.ImFontChanged -= OnImFontChanged; + this.Disposed -= OnDisposed; + try + { + tcs.SetException(new ObjectDisposedException(nameof(GamePrebakedFontHandle))); + } + catch + { + // ignore + } } } /// - /// Invokes . - /// - protected void InvokeImFontChanged() => this.ImFontChanged.InvokeSafely(this); - - /// - /// Overrideable implementation for . + /// Implementation for . /// /// If true, then the function is being called from . - protected virtual void Dispose(bool disposing) + protected void Dispose(bool disposing) { if (disposing) { @@ -256,7 +288,7 @@ internal abstract class FontHandle : IFontHandle Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); this.Manager.FreeFontHandle(this); this.manager = null; - this.Disposed?.InvokeSafely(this); + this.Disposed?.InvokeSafely(); this.ImFontChanged = null; } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index e062405b8..b6c9817aa 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -172,16 +172,6 @@ internal class GamePrebakedFontHandle : FontHandle } } - /// - public void InvokeFontHandleImFontChanged() - { - if (this.Substance is not HandleSubstance hs) - return; - - foreach (var handle in hs.RelevantHandles) - handle.InvokeImFontChanged(); - } - /// public IFontHandleSubstance NewSubstance(IRefCountable dataRoot) { @@ -232,6 +222,9 @@ internal class GamePrebakedFontHandle : FontHandle // Not owned by this class. Do not dispose. public GamePrebakedFontHandle[] RelevantHandles { get; } + /// + ICollection IFontHandleSubstance.RelevantHandles => this.RelevantHandles; + /// public IRefCountable DataRoot { get; } @@ -413,12 +406,6 @@ internal class GamePrebakedFontHandle : FontHandle } } - /// - public void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion) - { - // Irrelevant - } - /// /// Creates a new template font. /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs index 7066817b7..94976598a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleManager.cs @@ -32,9 +32,4 @@ internal interface IFontHandleManager : IDisposable /// The data root. /// The new substance. IFontHandleSubstance NewSubstance(IRefCountable dataRoot); - - /// - /// Invokes . - /// - void InvokeFontHandleImFontChanged(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs index 73c14efc1..62c893a48 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/IFontHandleSubstance.cs @@ -1,4 +1,6 @@ -using Dalamud.Utility; +using System.Collections.Generic; + +using Dalamud.Utility; using ImGuiNET; @@ -32,6 +34,11 @@ internal interface IFontHandleSubstance : IDisposable [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] bool CreateFontOnAccess { get; set; } + /// + /// Gets the relevant handles. + /// + public ICollection RelevantHandles { get; } + /// /// Gets the font. /// @@ -64,11 +71,4 @@ internal interface IFontHandleSubstance : IDisposable /// /// The toolkit. void OnPostBuild(IFontAtlasBuildToolkitPostBuild toolkitPostBuild); - - /// - /// Called on the specific thread depending on after - /// promoting the staging atlas to direct use with . - /// - /// The toolkit. - void OnPostPromotion(IFontAtlasBuildToolkitPostPromotion toolkitPostPromotion); } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index af4cc39c2..b038d44ba 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -744,7 +744,7 @@ public sealed class UiBuilder : IDisposable this.wrapped.ImFontChanged += this.WrappedOnImFontChanged; } - public event Action? ImFontChanged; + public event IFontHandle.ImFontChangedDelegate? ImFontChanged; public Exception? LoadException => this.wrapped!.LoadException ?? new ObjectDisposedException(nameof(FontHandleWrapper)); @@ -775,6 +775,7 @@ public sealed class UiBuilder : IDisposable public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})"; - private void WrappedOnImFontChanged(IFontHandle obj) => this.ImFontChanged.InvokeSafely(this); + private void WrappedOnImFontChanged(IFontHandle obj, IFontHandle.ImFontLocked lockedFont) => + this.ImFontChanged?.Invoke(obj, lockedFont); } } From df89472d4ccf5cc903fd5009e7caad1251dc04dd Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 23:18:18 +0900 Subject: [PATCH 463/585] Consistent BuildTask resolution timing `BuildFontsImmediately` and `BuildFontsAsync` set `BuildTask` to completion at different point of build process, and changed the code to make it consistent that `BuildTask` is set to completion after `PromoteBuiltData` returns. --- .../FontAtlasFactory.Implementation.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 4e98bf226..7fadf669d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -4,6 +4,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reactive.Disposables; +using System.Runtime.ExceptionServices; +using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.GameFonts; @@ -229,7 +231,6 @@ internal sealed partial class FontAtlasFactory private readonly GamePrebakedFontHandle.HandleManager gameFontHandleManager; private readonly IFontHandleManager[] fontHandleManagers; - private readonly object syncRootPostPromotion = new(); private readonly object syncRoot = new(); private Task buildTask = EmptyTask; @@ -449,10 +450,9 @@ internal sealed partial class FontAtlasFactory } var tcs = new TaskCompletionSource(); - int rebuildIndex; try { - rebuildIndex = ++this.buildIndex; + var rebuildIndex = Interlocked.Increment(ref this.buildIndex); lock (this.syncRoot) { if (!this.buildTask.IsCompleted) @@ -469,11 +469,18 @@ internal sealed partial class FontAtlasFactory var r = this.RebuildFontsPrivate(false, scale); r.Wait(); if (r.IsCompletedSuccessfully) + { + this.PromoteBuiltData(rebuildIndex, r.Result, nameof(this.BuildFontsImmediately)); tcs.SetResult(r.Result); - else if (r.Exception is not null) - tcs.SetException(r.Exception); + } + else if ((r.Exception?.InnerException ?? r.Exception) is { } taskException) + { + ExceptionDispatchInfo.Capture(taskException).Throw(); + } else - tcs.SetCanceled(); + { + throw new OperationCanceledException(); + } } catch (Exception e) { @@ -481,8 +488,6 @@ internal sealed partial class FontAtlasFactory Log.Error(e, "[{name}] Failed to build fonts.", this.Name); throw; } - - this.InvokePostPromotion(rebuildIndex, tcs.Task.Result, nameof(this.BuildFontsImmediately)); } /// @@ -503,7 +508,7 @@ internal sealed partial class FontAtlasFactory lock (this.syncRoot) { var scale = this.IsGlobalScaled ? ImGuiHelpers.GlobalScaleSafe : 1f; - var rebuildIndex = ++this.buildIndex; + var rebuildIndex = Interlocked.Increment(ref this.buildIndex); return this.buildTask = this.buildTask.ContinueWith(BuildInner).Unwrap(); async Task BuildInner(Task unused) @@ -519,14 +524,14 @@ internal sealed partial class FontAtlasFactory if (res.Atlas.IsNull()) return res; - this.InvokePostPromotion(rebuildIndex, res, nameof(this.BuildFontsAsync)); + this.PromoteBuiltData(rebuildIndex, res, nameof(this.BuildFontsAsync)); return res; } } } - private void InvokePostPromotion(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) + private void PromoteBuiltData(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) { // Capture the locks inside the lock block, so that the fonts are guaranteed to be the ones just built. var fontsAndLocks = new List<(FontHandle FontHandle, IFontHandle.ImFontLocked Lock)>(); From 68dc16803c0a23993aa89193dd99cbdc73e14940 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 23 Jan 2024 23:39:25 +0900 Subject: [PATCH 464/585] Turn ImFontLocked into a class As `ImFontLocked` utilizes a reference counter, changed it to a class so that at worst case we still got the destructor to decrease the reference count. --- .../Interface/Internal/InterfaceManager.cs | 34 ++++++---- .../Interface/ManagedFontAtlas/IFontHandle.cs | 67 ++++++++++++++----- .../FontAtlasFactory.Implementation.cs | 4 +- .../ManagedFontAtlas/Internals/FontHandle.cs | 2 +- .../Internals/SimplePushedFont.cs | 2 +- 5 files changed, 75 insertions(+), 34 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 18bb95799..82299a136 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -79,7 +79,7 @@ internal class InterfaceManager : IDisposable, IServiceType private Hook? resizeBuffersHook; private IFontAtlas? dalamudAtlas; - private IFontHandle.ImFontLocked defaultFontResourceLock; + private IFontHandle.ImFontLocked? defaultFontResourceLock; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -243,6 +243,8 @@ internal class InterfaceManager : IDisposable, IServiceType Disposer(); this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + this.defaultFontResourceLock?.Dispose(); // lock outlives handle and atlas + this.defaultFontResourceLock = null; this.dalamudAtlas?.Dispose(); this.scene?.Dispose(); return; @@ -727,22 +729,26 @@ internal class InterfaceManager : IDisposable, IServiceType tk.GetFont(this.MonoFontHandle), missingOnly: true); }); - this.DefaultFontHandle.ImFontChanged += (_, font) => Service.Get().RunOnFrameworkThread( - () => - { - // Update the ImGui default font. - unsafe + this.DefaultFontHandle.ImFontChanged += (_, font) => + { + var fontLocked = font.NewRef(); + Service.Get().RunOnFrameworkThread( + () => { - ImGui.GetIO().NativePtr->FontDefault = font; - } + // Update the ImGui default font. + unsafe + { + ImGui.GetIO().NativePtr->FontDefault = fontLocked; + } - // Update the reference to the resources of the default font. - this.defaultFontResourceLock.Dispose(); - this.defaultFontResourceLock = font.NewRef(); + // Update the reference to the resources of the default font. + this.defaultFontResourceLock?.Dispose(); + this.defaultFontResourceLock = fontLocked; - // Broadcast to auto-rebuilding instances. - this.AfterBuildFonts?.Invoke(); - }); + // Broadcast to auto-rebuilding instances. + this.AfterBuildFonts?.Invoke(); + }); + }; } // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index d58a89e56..dd3775236 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,9 +1,14 @@ -using System.Threading.Tasks; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; +using Microsoft.Extensions.ObjectPool; + namespace Dalamud.Interface.ManagedFontAtlas; /// @@ -80,26 +85,23 @@ public interface IFontHandle : IDisposable /// The wrapper for , guaranteeing that the associated data will be available as long as /// this struct is not disposed. /// - public struct ImFontLocked : IDisposable + public class ImFontLocked : IDisposable { - /// - /// The associated . - /// - public ImFontPtr ImFont; + // Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose. + private static readonly ObjectPool Pool = + new DefaultObjectPool(new DefaultPooledObjectPolicy()); private IRefCountable? owner; /// - /// Initializes a new instance of the struct. - /// Ownership of reference of is transferred. + /// Finalizes an instance of the class. /// - /// The contained font. - /// The owner. - internal ImFontLocked(ImFontPtr imFont, IRefCountable owner) - { - this.ImFont = imFont; - this.owner = owner; - } + ~ImFontLocked() => this.FreeOwner(); + + /// + /// Gets the associated . + /// + public ImFontPtr ImFont { get; private set; } public static implicit operator ImFontPtr(ImFontLocked l) => l.ImFont; @@ -109,16 +111,47 @@ public interface IFontHandle : IDisposable /// Creates a new instance of with an additional reference to the owner. /// /// The new locked instance. - public readonly ImFontLocked NewRef() + public ImFontLocked NewRef() { if (this.owner is null) throw new ObjectDisposedException(nameof(ImFontLocked)); + + var rented = Pool.Get(); + rented.owner = this.owner; + rented.ImFont = this.ImFont; + this.owner.AddRef(); - return new(this.ImFont, this.owner); + return rented; } /// + [SuppressMessage( + "Usage", + "CA1816:Dispose methods should call SuppressFinalize", + Justification = "Dispose returns this object to the pool.")] public void Dispose() + { + this.FreeOwner(); + Pool.Return(this); + } + + /// + /// Initializes a new instance of the class. + /// Ownership of reference of is transferred. + /// + /// The contained font. + /// The owner. + /// The rented instance of . + internal static ImFontLocked Rent(ImFontPtr font, IRefCountable owner) + { + var rented = Pool.Get(); + Debug.Assert(rented.ImFont.IsNull(), "Rented object must not have its font set"); + rented.ImFont = font; + rented.owner = owner; + return rented; + } + + private void FreeOwner() { if (this.owner is null) return; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 7fadf669d..06bc5b7ab 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -557,7 +557,9 @@ internal sealed partial class FontAtlasFactory foreach (var fontHandle in substance.RelevantHandles) { substance.DataRoot.AddRef(); - var locked = new IFontHandle.ImFontLocked(substance.GetFontPtr(fontHandle), substance.DataRoot); + var locked = IFontHandle.ImFontLocked.Rent( + substance.GetFontPtr(fontHandle), + substance.DataRoot); fontsAndLocks.Add((fontHandle, garbage.Add(locked))); } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index d01b0df87..f8291cc51 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -182,7 +182,7 @@ internal abstract class FontHandle : IFontHandle // Transfer the ownership of reference. errorMessage = null; - return new(fontPtr, substance.DataRoot); + return IFontHandle.ImFontLocked.Rent(fontPtr, substance.DataRoot); } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs index 0642e7be1..0c96025ac 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/SimplePushedFont.cs @@ -28,7 +28,7 @@ internal sealed class SimplePushedFont : IDisposable /// /// The -private stack. /// The font pointer being pushed. - /// this. + /// The rented instance of . public static SimplePushedFont Rent(List stack, ImFontPtr fontPtr) { var rented = Pool.Get(); From 5161053cb34d9a8299beb982c703fe1782f7a55f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 24 Jan 2024 00:19:27 +0900 Subject: [PATCH 465/585] Move `IFontHandle.ImFontLocked` to `ILockedImFont`+impl --- Dalamud/Interface/GameFonts/GameFontHandle.cs | 2 +- .../Interface/Internal/InterfaceManager.cs | 10 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 2 +- .../Interface/ManagedFontAtlas/IFontHandle.cs | 96 +------------------ .../ManagedFontAtlas/ILockedImFont.cs | 21 ++++ .../FontAtlasFactory.Implementation.cs | 4 +- .../ManagedFontAtlas/Internals/FontHandle.cs | 14 +-- .../Internals/LockedImFont.cs | 62 ++++++++++++ Dalamud/Interface/UiBuilder.cs | 4 +- 9 files changed, 105 insertions(+), 110 deletions(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs diff --git a/Dalamud/Interface/GameFonts/GameFontHandle.cs b/Dalamud/Interface/GameFonts/GameFontHandle.cs index 679452ba4..2594eea0e 100644 --- a/Dalamud/Interface/GameFonts/GameFontHandle.cs +++ b/Dalamud/Interface/GameFonts/GameFontHandle.cs @@ -71,7 +71,7 @@ public sealed class GameFontHandle : IFontHandle public void Dispose() => this.fontHandle.Dispose(); /// - public IFontHandle.ImFontLocked Lock() => this.fontHandle.Lock(); + public ILockedImFont Lock() => this.fontHandle.Lock(); /// public IDisposable Push() => this.fontHandle.Push(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 82299a136..6cf4a8b90 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -63,7 +63,7 @@ internal class InterfaceManager : IDisposable, IServiceType public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; private readonly ConcurrentBag deferredDisposeTextures = new(); - private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); + private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); @@ -79,7 +79,7 @@ internal class InterfaceManager : IDisposable, IServiceType private Hook? resizeBuffersHook; private IFontAtlas? dalamudAtlas; - private IFontHandle.ImFontLocked? defaultFontResourceLock; + private ILockedImFont? defaultFontResourceLock; // can't access imgui IO before first present call private bool lastWantCapture = false; @@ -408,10 +408,10 @@ internal class InterfaceManager : IDisposable, IServiceType } /// - /// Enqueue an to be disposed at the end of the frame. + /// Enqueue an to be disposed at the end of the frame. /// /// The disposable. - public void EnqueueDeferredDispose(in IFontHandle.ImFontLocked locked) + public void EnqueueDeferredDispose(in ILockedImFont locked) { this.deferredDisposeImFontLockeds.Add(locked); } @@ -738,7 +738,7 @@ internal class InterfaceManager : IDisposable, IServiceType // Update the ImGui default font. unsafe { - ImGui.GetIO().NativePtr->FontDefault = fontLocked; + ImGui.GetIO().NativePtr->FontDefault = fontLocked.ImFont; } // Update the reference to the resources of the default font. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index 7b649a895..b486cc7d9 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -249,7 +249,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable await handle.WaitAsync(); var locked = handle.Lock(); garbage.Add(locked); - fonts.Add(locked); + fonts.Add(locked.ImFont); } } catch (ObjectDisposedException) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index dd3775236..11c26616b 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -1,14 +1,7 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; - -using Dalamud.Interface.Utility; -using Dalamud.Utility; +using System.Threading.Tasks; using ImGuiNET; -using Microsoft.Extensions.ObjectPool; - namespace Dalamud.Interface.ManagedFontAtlas; /// @@ -21,7 +14,7 @@ public interface IFontHandle : IDisposable /// /// The relevant font handle. /// The locked font for this font handle, locked during the call of this delegate. - public delegate void ImFontChangedDelegate(IFontHandle fontHandle, ImFontLocked lockedFont); + public delegate void ImFontChangedDelegate(IFontHandle fontHandle, ILockedImFont lockedFont); /// /// Called when the built instance of has been changed.
@@ -48,13 +41,13 @@ public interface IFontHandle : IDisposable /// , for use in any thread.
/// Modification of the font will exhibit undefined behavior if some other thread also uses the font. ///
- /// An instance of that must be disposed after use. + /// An instance of that must be disposed after use. /// /// Calling . will not unlock the /// locked by this function. /// /// If is false. - ImFontLocked Lock(); + ILockedImFont Lock(); /// /// Pushes the current font into ImGui font stack, if available.
@@ -80,85 +73,4 @@ public interface IFontHandle : IDisposable ///
/// A task containing this . Task WaitAsync(); - - /// - /// The wrapper for , guaranteeing that the associated data will be available as long as - /// this struct is not disposed. - /// - public class ImFontLocked : IDisposable - { - // Using constructor instead of DefaultObjectPoolProvider, since we do not want the pool to call Dispose. - private static readonly ObjectPool Pool = - new DefaultObjectPool(new DefaultPooledObjectPolicy()); - - private IRefCountable? owner; - - /// - /// Finalizes an instance of the class. - /// - ~ImFontLocked() => this.FreeOwner(); - - /// - /// Gets the associated . - /// - public ImFontPtr ImFont { get; private set; } - - public static implicit operator ImFontPtr(ImFontLocked l) => l.ImFont; - - public static unsafe implicit operator ImFont*(ImFontLocked l) => l.ImFont.NativePtr; - - /// - /// Creates a new instance of with an additional reference to the owner. - /// - /// The new locked instance. - public ImFontLocked NewRef() - { - if (this.owner is null) - throw new ObjectDisposedException(nameof(ImFontLocked)); - - var rented = Pool.Get(); - rented.owner = this.owner; - rented.ImFont = this.ImFont; - - this.owner.AddRef(); - return rented; - } - - /// - [SuppressMessage( - "Usage", - "CA1816:Dispose methods should call SuppressFinalize", - Justification = "Dispose returns this object to the pool.")] - public void Dispose() - { - this.FreeOwner(); - Pool.Return(this); - } - - /// - /// Initializes a new instance of the class. - /// Ownership of reference of is transferred. - /// - /// The contained font. - /// The owner. - /// The rented instance of . - internal static ImFontLocked Rent(ImFontPtr font, IRefCountable owner) - { - var rented = Pool.Get(); - Debug.Assert(rented.ImFont.IsNull(), "Rented object must not have its font set"); - rented.ImFont = font; - rented.owner = owner; - return rented; - } - - private void FreeOwner() - { - if (this.owner is null) - return; - - this.owner.Release(); - this.owner = null; - this.ImFont = default; - } - } } diff --git a/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs new file mode 100644 index 000000000..9136d2723 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs @@ -0,0 +1,21 @@ +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// The wrapper for , guaranteeing that the associated data will be available as long as +/// this struct is not disposed. +/// +public interface ILockedImFont : IDisposable +{ + /// + /// Gets the associated . + /// + ImFontPtr ImFont { get; } + + /// + /// Creates a new instance of with an additional reference to the owner. + /// + /// The new locked instance. + ILockedImFont NewRef(); +} diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 06bc5b7ab..4d636b8cf 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -534,7 +534,7 @@ internal sealed partial class FontAtlasFactory private void PromoteBuiltData(int rebuildIndex, FontAtlasBuiltData data, [UsedImplicitly] string source) { // Capture the locks inside the lock block, so that the fonts are guaranteed to be the ones just built. - var fontsAndLocks = new List<(FontHandle FontHandle, IFontHandle.ImFontLocked Lock)>(); + var fontsAndLocks = new List<(FontHandle FontHandle, ILockedImFont Lock)>(); using var garbage = new DisposeSafety.ScopedFinalizer(); lock (this.syncRoot) @@ -557,7 +557,7 @@ internal sealed partial class FontAtlasFactory foreach (var fontHandle in substance.RelevantHandles) { substance.DataRoot.AddRef(); - var locked = IFontHandle.ImFontLocked.Rent( + var locked = new LockedImFont( substance.GetFontPtr(fontHandle), substance.DataRoot); fontsAndLocks.Add((fontHandle, garbage.Add(locked))); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index f8291cc51..47254a5c9 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -74,7 +74,7 @@ internal abstract class FontHandle : IFontHandle /// Invokes . /// /// The font, locked during the call of . - public void InvokeImFontChanged(IFontHandle.ImFontLocked font) + public void InvokeImFontChanged(ILockedImFont font) { try { @@ -133,11 +133,11 @@ internal abstract class FontHandle : IFontHandle /// /// The error message, if any. /// - /// An instance of that must be disposed after use on success; + /// An instance of that must be disposed after use on success; /// null with populated on failure. /// /// Still may be thrown. - public IFontHandle.ImFontLocked? TryLock(out string? errorMessage) + public ILockedImFont? TryLock(out string? errorMessage) { IFontHandleSubstance? prevSubstance = default; while (true) @@ -182,12 +182,12 @@ internal abstract class FontHandle : IFontHandle // Transfer the ownership of reference. errorMessage = null; - return IFontHandle.ImFontLocked.Rent(fontPtr, substance.DataRoot); + return new LockedImFont(fontPtr, substance.DataRoot); } } /// - public IFontHandle.ImFontLocked Lock() => + public ILockedImFont Lock() => this.TryLock(out var errorMessage) ?? throw new InvalidOperationException(errorMessage); /// @@ -238,10 +238,10 @@ internal abstract class FontHandle : IFontHandle this.ImFontChanged += OnImFontChanged; this.Disposed += OnDisposed; if (this.Available) - OnImFontChanged(this, default); + OnImFontChanged(this, null); return tcs.Task; - void OnImFontChanged(IFontHandle unused, IFontHandle.ImFontLocked unused2) + void OnImFontChanged(IFontHandle unused, ILockedImFont? unused2) { if (tcs.Task.IsCompletedSuccessfully) return; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs new file mode 100644 index 000000000..bd50502c8 --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/LockedImFont.cs @@ -0,0 +1,62 @@ +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// The implementation for . +/// +internal class LockedImFont : ILockedImFont +{ + private IRefCountable? owner; + + /// + /// Initializes a new instance of the class. + /// Ownership of reference of is transferred. + /// + /// The contained font. + /// The owner. + /// The rented instance of . + internal LockedImFont(ImFontPtr font, IRefCountable owner) + { + this.ImFont = font; + this.owner = owner; + } + + /// + /// Finalizes an instance of the class. + /// + ~LockedImFont() => this.FreeOwner(); + + /// + public ImFontPtr ImFont { get; private set; } + + /// + public ILockedImFont NewRef() + { + if (this.owner is null) + throw new ObjectDisposedException(nameof(LockedImFont)); + + var newRef = new LockedImFont(this.ImFont, this.owner); + this.owner.AddRef(); + return newRef; + } + + /// + public void Dispose() + { + this.FreeOwner(); + GC.SuppressFinalize(this); + } + + private void FreeOwner() + { + if (this.owner is null) + return; + + this.owner.Release(); + this.owner = null; + this.ImFont = default; + } +} diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index b038d44ba..ce5a09b22 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -761,7 +761,7 @@ public sealed class UiBuilder : IDisposable // Note: do not dispose w; we do not own it } - public IFontHandle.ImFontLocked Lock() => + public ILockedImFont Lock() => this.wrapped?.Lock() ?? throw new ObjectDisposedException(nameof(FontHandleWrapper)); public IDisposable Push() => @@ -775,7 +775,7 @@ public sealed class UiBuilder : IDisposable public override string ToString() => $"{nameof(FontHandleWrapper)}({this.wrapped})"; - private void WrappedOnImFontChanged(IFontHandle obj, IFontHandle.ImFontLocked lockedFont) => + private void WrappedOnImFontChanged(IFontHandle obj, ILockedImFont lockedFont) => this.ImFontChanged?.Invoke(obj, lockedFont); } } From 9d6756fbca58f6069a26ea54065aea7af48ef181 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Jan 2024 12:11:07 +0000 Subject: [PATCH 466/585] Update ClientStructs --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index e9341bb30..d0108d2e6 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e9341bb3038bf4200300f21be4a8629525d15596 +Subproject commit d0108d2e6e30a6b51b1124fc27ec3523c8ca3acb From 31c3c1ecc0c5306c481312d79312835c37460e66 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 28 Jan 2024 17:55:49 -0800 Subject: [PATCH 467/585] Fix reset and reload not working --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 4 ++-- Dalamud/Plugin/Internal/PluginManager.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 5007691ab..18cac5cf2 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2556,7 +2556,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.MenuItem(Locs.PluginContext_DeletePluginConfigReload)) { - this.ShowDeletePluginConfigWarningModal(plugin.Name).ContinueWith(t => + this.ShowDeletePluginConfigWarningModal(plugin.Manifest.InternalName).ContinueWith(t => { var shouldDelete = t.Result; @@ -2571,7 +2571,7 @@ internal class PluginInstallerWindow : Window, IDisposable { this.installStatus = OperationStatus.Idle; - this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Name)); + this.DisplayErrorContinuation(task, Locs.ErrorModal_DeleteConfigFail(plugin.Manifest.InternalName)); }); } }); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 8bfb38c34..6bdf73036 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1094,7 +1094,7 @@ internal partial class PluginManager : IDisposable, IServiceType { try { - this.PluginConfigs.Delete(plugin.Name); + this.PluginConfigs.Delete(plugin.Manifest.InternalName); break; } catch (IOException) From e065f3e988315ebc29c5c82a73130f9e979d4126 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:26:22 -0800 Subject: [PATCH 468/585] Show Name in text prompt --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 18cac5cf2..40fc66221 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2556,7 +2556,7 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.MenuItem(Locs.PluginContext_DeletePluginConfigReload)) { - this.ShowDeletePluginConfigWarningModal(plugin.Manifest.InternalName).ContinueWith(t => + this.ShowDeletePluginConfigWarningModal(plugin.Manifest.Name).ContinueWith(t => { var shouldDelete = t.Result; From 0e724be4f8b3e778761e0c65d7d27560c43a1f73 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 28 Jan 2024 18:27:36 -0800 Subject: [PATCH 469/585] Fix loc typo --- .../Internal/Windows/PluginInstaller/PluginInstallerWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 40fc66221..83d819634 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3773,7 +3773,7 @@ internal class PluginInstallerWindow : Window, IDisposable public static string DeletePluginConfigWarningModal_Title => Loc.Localize("InstallerDeletePluginConfigWarning", "Warning###InstallerDeletePluginConfigWarning"); - public static string DeletePluginConfigWarningModal_Body(string pluginName) => Loc.Localize("InstallerDeletePluginConfigWarningBody", "Are you sure you want to delete all data and configuration for v{0}?").Format(pluginName); + public static string DeletePluginConfigWarningModal_Body(string pluginName) => Loc.Localize("InstallerDeletePluginConfigWarningBody", "Are you sure you want to delete all data and configuration for {0}?").Format(pluginName); public static string DeletePluginConfirmWarningModal_Yes => Loc.Localize("InstallerDeletePluginConfigWarningYes", "Yes"); From 866c41c2d8282258ef9d56796113009053142898 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 30 Jan 2024 03:03:31 +0100 Subject: [PATCH 470/585] Update ClientStructs (#1623) 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 d0108d2e6..0549ab9a9 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit d0108d2e6e30a6b51b1124fc27ec3523c8ca3acb +Subproject commit 0549ab9a993b4c4c8c0b4dcd4e31ed5413f75387 From 65265b678e1e56c28e4c23dc2f89f18b76c95a32 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 4 Feb 2024 19:57:00 +0100 Subject: [PATCH 471/585] Update ClientStructs (#1626) 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 0549ab9a9..b5f5f68e1 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 0549ab9a993b4c4c8c0b4dcd4e31ed5413f75387 +Subproject commit b5f5f68e147e1a21a0f0c88345f8d8c359678317 From 1d32e8fe45821de870f8730f58d9e3e60a12885d Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 7 Feb 2024 18:33:35 +0000 Subject: [PATCH 472/585] Fix language selector throwing a exception, use native name for Taiwan (#1634) The language selector has only been showing language codes and not the actual language names since dd0159ae5a2174819c1541644e5cdbd4ddd98a1d because "tw" (Taiwan Mandarin) was added and it's not supported by CultureInfo. This adds a specific check like the language code to work around this and stop throwing exceptions. Also converts to a switch so it looks a bit nicer. --- .../Widgets/LanguageChooserSettingsEntry.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs index 85f8a826f..c8cc1f42c 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/LanguageChooserSettingsEntry.cs @@ -31,17 +31,20 @@ public sealed class LanguageChooserSettingsEntry : SettingsEntry try { var locLanguagesList = new List(); - string locLanguage; foreach (var language in this.languages) { - if (language != "ko") + switch (language) { - locLanguage = CultureInfo.GetCultureInfo(language).NativeName; - locLanguagesList.Add(char.ToUpper(locLanguage[0]) + locLanguage[1..]); - } - else - { - locLanguagesList.Add("Korean"); + case "ko": + locLanguagesList.Add("Korean"); + break; + case "tw": + locLanguagesList.Add("中華民國國語"); + break; + default: + string locLanguage = CultureInfo.GetCultureInfo(language).NativeName; + locLanguagesList.Add(char.ToUpper(locLanguage[0]) + locLanguage[1..]); + break; } } From 7112651b7762ac1f5554811e7a820e97a7fbb779 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 7 Feb 2024 18:37:55 +0000 Subject: [PATCH 473/585] Remove EnableWindowsTargeting from build.sh's run step (#1633) This removes the property that shouldn't be there, because it was considered a target. --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 build.sh diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index a4c346c80..5aa50b1c1 --- a/build.sh +++ b/build.sh @@ -59,4 +59,4 @@ fi echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false /p:EnableWindowsTargeting=true -nologo -clp:NoSummary --verbosity quiet -"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- /p:EnableWindowsTargeting=true "$@" +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" From 8b30781b4c2b4f5169d254bd4a17f4c040a5e0dd Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Wed, 7 Feb 2024 13:06:03 -0500 Subject: [PATCH 474/585] Change "Enable AntiDebug" label to make it clearer You need to enable this to allow debugging, but the label has the negative which doesn't make sense. Now it's called "Disable Debugging Protections" which is what it actually does. --- Dalamud/Interface/Internal/DalamudInterface.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 60c1f9957..6035ca0ec 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -667,7 +667,7 @@ internal class DalamudInterface : IDisposable, IServiceType } var antiDebug = Service.Get(); - if (ImGui.MenuItem("Enable AntiDebug", null, antiDebug.IsEnabled)) + if (ImGui.MenuItem("Disable Debugging Protections", null, antiDebug.IsEnabled)) { var newEnabled = !antiDebug.IsEnabled; if (newEnabled) From 0d10a179664af93c428dd5fabdf38a0be8dbc306 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Thu, 8 Feb 2024 09:53:51 -0800 Subject: [PATCH 475/585] Make CommandWidget look better (and expose more info) (#1631) - Expose the plugin that owns the command. --- .../Windows/Data/Widgets/CommandWidget.cs | 53 +++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs index 8ec704888..c4c74274a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/CommandWidget.cs @@ -1,4 +1,8 @@ -using Dalamud.Game.Command; +using System.Linq; + +using Dalamud.Game.Command; +using Dalamud.Interface.Utility.Raii; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -28,9 +32,52 @@ internal class CommandWidget : IDataWindowWidget { var commandManager = Service.Get(); - foreach (var command in commandManager.Commands) + var tableFlags = ImGuiTableFlags.ScrollY | ImGuiTableFlags.Borders | ImGuiTableFlags.SizingStretchProp | + ImGuiTableFlags.Sortable | ImGuiTableFlags.SortTristate; + using var table = ImRaii.Table("CommandList", 4, tableFlags); + if (table) { - ImGui.Text($"{command.Key}\n -> {command.Value.HelpMessage}\n -> In help: {command.Value.ShowInHelp}\n\n"); + ImGui.TableSetupScrollFreeze(0, 1); + + ImGui.TableSetupColumn("Command"); + ImGui.TableSetupColumn("Plugin"); + ImGui.TableSetupColumn("HelpMessage", ImGuiTableColumnFlags.NoSort); + ImGui.TableSetupColumn("In Help?", ImGuiTableColumnFlags.NoSort); + ImGui.TableHeadersRow(); + + var sortSpecs = ImGui.TableGetSortSpecs(); + var commands = commandManager.Commands.ToArray(); + + if (sortSpecs.SpecsCount != 0) + { + commands = sortSpecs.Specs.ColumnIndex switch + { + 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? commands.OrderBy(kv => kv.Key).ToArray() + : commands.OrderByDescending(kv => kv.Key).ToArray(), + 1 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? commands.OrderBy(kv => kv.Value.LoaderAssemblyName).ToArray() + : commands.OrderByDescending(kv => kv.Value.LoaderAssemblyName).ToArray(), + _ => commands, + }; + } + + foreach (var command in commands) + { + ImGui.TableNextRow(); + + ImGui.TableSetColumnIndex(0); + ImGui.Text(command.Key); + + ImGui.TableNextColumn(); + ImGui.Text(command.Value.LoaderAssemblyName); + + ImGui.TableNextColumn(); + ImGui.TextWrapped(command.Value.HelpMessage); + + ImGui.TableNextColumn(); + ImGui.Text(command.Value.ShowInHelp ? "Yes" : "No"); + } } } } From df65d59f8b376631337948cea2c4bd1746b2c904 Mon Sep 17 00:00:00 2001 From: marzent Date: Sat, 10 Feb 2024 13:03:11 +0100 Subject: [PATCH 476/585] add more exception handler options to dev menu --- Dalamud/Dalamud.cs | 52 +++++++++++++++---- .../Interface/Internal/DalamudInterface.cs | 14 ++++- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 4ab617d0a..8c858ce7c 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -117,6 +117,14 @@ internal sealed class Dalamud : IServiceType } }); } + + this.DefaultExceptionFilter = NativeFunctions.SetUnhandledExceptionFilter(nint.Zero); + NativeFunctions.SetUnhandledExceptionFilter(this.DefaultExceptionFilter); + Log.Debug($"SE default exception filter at {this.DefaultExceptionFilter.ToInt64():X}"); + + var debugSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??"; + this.DebugExceptionFilter = Service.Get().ScanText(debugSig); + Log.Debug($"SE debug exception filter at {this.DebugExceptionFilter.ToInt64():X}"); } /// @@ -128,7 +136,17 @@ internal sealed class Dalamud : IServiceType /// Gets location of stored assets. /// internal DirectoryInfo AssetDirectory => new(this.StartInfo.AssetDirectory!); - + + /// + /// Gets the in-game default exception filter. + /// + private nint DefaultExceptionFilter { get; } + + /// + /// Gets the in-game debug exception filter. + /// + private nint DebugExceptionFilter { get; } + /// /// Signal to the crash handler process that we should restart the game. /// @@ -191,18 +209,32 @@ internal sealed class Dalamud : IServiceType } /// - /// Replace the built-in exception handler with a debug one. + /// Replace the current exception handler with the default one. /// - internal void ReplaceExceptionHandler() - { - var releaseSig = "40 55 53 56 48 8D AC 24 ?? ?? ?? ?? B8 ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 2B E0 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 85 ?? ?? ?? ?? 48 83 3D ?? ?? ?? ?? ??"; - var releaseFilter = Service.Get().ScanText(releaseSig); - Log.Debug($"SE debug filter at {releaseFilter.ToInt64():X}"); + internal void UseDefaultExceptionHandler() => + this.SetExceptionHandler(this.DefaultExceptionFilter); - var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter); - Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter); + /// + /// Replace the current exception handler with a debug one. + /// + internal void UseDebugExceptionHandler() => + this.SetExceptionHandler(this.DebugExceptionFilter); + + /// + /// Disable the current exception handler. + /// + internal void UseNoExceptionHandler() => + this.SetExceptionHandler(nint.Zero); + + /// + /// Helper function to set the exception handler. + /// + private void SetExceptionHandler(nint newFilter) + { + var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(newFilter); + Log.Debug("Set ExceptionFilter to {0}, old: {1}", newFilter, oldFilter); } - + private void SetupClientStructsResolver(DirectoryInfo cacheDir) { using (Timings.Start("CS Resolver Init")) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 6035ca0ec..b8ca98584 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -863,9 +863,19 @@ internal class DalamudInterface : IDisposable, IServiceType if (ImGui.BeginMenu("Game")) { - if (ImGui.MenuItem("Replace ExceptionHandler")) + if (ImGui.MenuItem("Use in-game default ExceptionHandler")) { - this.dalamud.ReplaceExceptionHandler(); + this.dalamud.UseDefaultExceptionHandler(); + } + + if (ImGui.MenuItem("Use in-game debug ExceptionHandler")) + { + this.dalamud.UseDebugExceptionHandler(); + } + + if (ImGui.MenuItem("Disable in-game ExceptionHandler")) + { + this.dalamud.UseNoExceptionHandler(); } ImGui.EndMenu(); From 16bc6b86e5fed795539d3929b732c3f292057254 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 11 Feb 2024 19:20:26 +0100 Subject: [PATCH 477/585] Update ClientStructs (#1630) 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 b5f5f68e1..e3bd59106 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b5f5f68e147e1a21a0f0c88345f8d8c359678317 +Subproject commit e3bd5910678683a718e68f0f940c88b08c24eba5 From 386e5f245c9661ebbd27f270952648852e4dabde Mon Sep 17 00:00:00 2001 From: marzent Date: Sun, 11 Feb 2024 22:01:34 +0100 Subject: [PATCH 478/585] Make the crash handler work on wine properly (#1636) * add LoadMethod to DalamudStartInfo * add to_wstring utility function * append full injector launch args for VEH * remove usage of std::chrono::zoned_time * fix injector arguments in crash handler restart * enable VEH on wine * remove dead wine detection code * write out tspack with std::fstream * fix off-by-one error in get_window_string() * remove usage of std::chrono when writing tspack * do not deadlock on crashing DalamudCrashHandler --- Dalamud.Boot/DalamudStartInfo.cpp | 15 +++ Dalamud.Boot/DalamudStartInfo.h | 7 ++ Dalamud.Boot/crashhandler_shared.h | 1 + Dalamud.Boot/dllmain.cpp | 2 - Dalamud.Boot/utils.cpp | 23 ++--- Dalamud.Boot/utils.h | 2 +- Dalamud.Boot/veh.cpp | 40 +++++++- Dalamud.Common/DalamudStartInfo.cs | 5 + Dalamud.Common/LoadMethod.cs | 17 ++++ Dalamud.Injector/EntryPoint.cs | 8 +- DalamudCrashHandler/DalamudCrashHandler.cpp | 105 ++++++++++---------- 11 files changed, 148 insertions(+), 77 deletions(-) create mode 100644 Dalamud.Common/LoadMethod.cs diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index e2fed1beb..d20265bf8 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -68,10 +68,25 @@ void from_json(const nlohmann::json& json, DalamudStartInfo::ClientLanguage& val } } +void from_json(const nlohmann::json& json, DalamudStartInfo::LoadMethod& value) { + if (json.is_number_integer()) { + value = static_cast(json.get()); + + } + else if (json.is_string()) { + const auto langstr = unicode::convert(json.get(), &unicode::lower); + if (langstr == "entrypoint") + value = DalamudStartInfo::LoadMethod::Entrypoint; + else if (langstr == "inject") + value = DalamudStartInfo::LoadMethod::DllInject; + } +} + void from_json(const nlohmann::json& json, DalamudStartInfo& config) { if (!json.is_object()) return; + config.DalamudLoadMethod = json.value("LoadMethod", config.DalamudLoadMethod); config.WorkingDirectory = json.value("WorkingDirectory", config.WorkingDirectory); config.ConfigurationPath = json.value("ConfigurationPath", config.ConfigurationPath); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 73a1a0d34..5cee8f16b 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -26,6 +26,13 @@ struct DalamudStartInfo { }; friend void from_json(const nlohmann::json&, ClientLanguage&); + enum class LoadMethod : int { + Entrypoint, + DllInject, + }; + friend void from_json(const nlohmann::json&, LoadMethod&); + + LoadMethod DalamudLoadMethod = LoadMethod::Entrypoint; std::string WorkingDirectory; std::string ConfigurationPath; std::string PluginDirectory; diff --git a/Dalamud.Boot/crashhandler_shared.h b/Dalamud.Boot/crashhandler_shared.h index 4e8cbb520..8d93e4460 100644 --- a/Dalamud.Boot/crashhandler_shared.h +++ b/Dalamud.Boot/crashhandler_shared.h @@ -14,6 +14,7 @@ struct exception_info CONTEXT ContextRecord; uint64_t nLifetime; HANDLE hThreadHandle; + HANDLE hEventHandle; DWORD dwStackTraceLength; DWORD dwTroubleshootingPackDataLength; }; diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 8ffef40b0..2566016e8 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -135,8 +135,6 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { logging::I("Initializing VEH..."); if (g_startInfo.NoExceptionHandlers) { logging::W("=> Exception handlers are disabled from DalamudStartInfo."); - } else if (utils::is_running_on_wine()) { - logging::I("=> VEH was disabled, running on wine"); } else if (g_startInfo.BootVehEnabled) { if (veh::add_handler(g_startInfo.BootVehFull, g_startInfo.WorkingDirectory)) logging::I("=> Done!"); diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index b45795045..62a9d7055 100644 --- a/Dalamud.Boot/utils.cpp +++ b/Dalamud.Boot/utils.cpp @@ -578,21 +578,14 @@ std::vector utils::get_env_list(const wchar_t* pcszName) { return res; } -bool utils::is_running_on_wine() { - if (get_env(L"XL_WINEONLINUX")) - return true; - HMODULE hntdll = GetModuleHandleW(L"ntdll.dll"); - if (!hntdll) - return true; - if (GetProcAddress(hntdll, "wine_get_version")) - return true; - if (GetProcAddress(hntdll, "wine_get_host_version")) - return true; - if (GetProcAddress(hntdll, "wine_server_call")) - return true; - if (GetProcAddress(hntdll, "wine_unix_to_nt_file_name")) - return true; - return false; +std::wstring utils::to_wstring(const std::string& str) { + if (str.empty()) return std::wstring(); + size_t convertedChars = 0; + size_t newStrSize = str.size() + 1; + std::wstring wstr(newStrSize, L'\0'); + mbstowcs_s(&convertedChars, &wstr[0], newStrSize, str.c_str(), _TRUNCATE); + wstr.resize(convertedChars - 1); + return wstr; } std::filesystem::path utils::get_module_path(HMODULE hModule) { diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h index 5e3caa4d6..ebf48a294 100644 --- a/Dalamud.Boot/utils.h +++ b/Dalamud.Boot/utils.h @@ -264,7 +264,7 @@ namespace utils { return get_env_list(unicode::convert(pcszName).c_str()); } - bool is_running_on_wine(); + std::wstring to_wstring(const std::string& str); std::filesystem::path get_module_path(HMODULE hModule); diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 0898441ff..eb27acce7 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -26,6 +26,7 @@ PVOID g_veh_handle = nullptr; bool g_veh_do_full_dump = false; HANDLE g_crashhandler_process = nullptr; +HANDLE g_crashhandler_event = nullptr; HANDLE g_crashhandler_pipe_write = nullptr; std::recursive_mutex g_exception_handler_mutex; @@ -101,8 +102,21 @@ bool is_ffxiv_address(const wchar_t* module_name, const DWORD64 address) static void append_injector_launch_args(std::vector& args) { - args.emplace_back(L"-g"); - args.emplace_back(utils::loaded_module::current_process().path().wstring()); + args.emplace_back(L"--game=\"" + utils::loaded_module::current_process().path().wstring() + L"\""); + switch (g_startInfo.DalamudLoadMethod) { + case DalamudStartInfo::LoadMethod::Entrypoint: + args.emplace_back(L"--mode=entrypoint"); + break; + case DalamudStartInfo::LoadMethod::DllInject: + args.emplace_back(L"--mode=inject"); + } + args.emplace_back(L"--logpath=\"" + utils::to_wstring(g_startInfo.BootLogPath) + L"\""); + args.emplace_back(L"--dalamud-working-directory=\"" + utils::to_wstring(g_startInfo.WorkingDirectory) + L"\""); + args.emplace_back(L"--dalamud-configuration-path=\"" + utils::to_wstring(g_startInfo.ConfigurationPath) + L"\""); + args.emplace_back(L"--dalamud-plugin-directory=\"" + utils::to_wstring(g_startInfo.PluginDirectory) + L"\""); + args.emplace_back(L"--dalamud-asset-directory=\"" + utils::to_wstring(g_startInfo.AssetDirectory) + L"\""); + args.emplace_back(L"--dalamud-client-language=" + std::to_wstring(static_cast(g_startInfo.Language))); + args.emplace_back(L"--dalamud-delay-initialize=" + std::to_wstring(g_startInfo.DelayInitializeMs)); if (g_startInfo.BootShowConsole) args.emplace_back(L"--console"); if (g_startInfo.BootEnableEtw) @@ -158,6 +172,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) g_time_start.time_since_epoch()).count(); exinfo.nLifetime = lifetime; DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), g_crashhandler_process, &exinfo.hThreadHandle, 0, TRUE, DUPLICATE_SAME_ACCESS); + DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS); std::wstring stackTrace; if (void* fn; const auto err = static_cast(g_clr->get_function_pointer( @@ -185,7 +200,20 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &g_startInfo.TroubleshootingPackData[0], static_cast(std::span(g_startInfo.TroubleshootingPackData).size_bytes()), &written, nullptr) || std::span(g_startInfo.TroubleshootingPackData).size_bytes() != written) return EXCEPTION_CONTINUE_SEARCH; - SuspendThread(GetCurrentThread()); + HANDLE waitHandles[] = { g_crashhandler_process, g_crashhandler_event }; + DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE); + + switch (waitResult) { + case WAIT_OBJECT_0: + logging::E("DalamudCrashHandler.exe exited unexpectedly"); + break; + case WAIT_OBJECT_0 + 1: + logging::I("Crashing thread was resumed"); + break; + default: + logging::E("Unexpected WaitForMultipleObjects return code 0x{:x}", waitResult); + } + return EXCEPTION_CONTINUE_SEARCH; } @@ -308,6 +336,12 @@ bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) return false; } + if (!(g_crashhandler_event = CreateEventW(NULL, FALSE, FALSE, NULL))) + { + logging::W("Failed to create crash synchronization event: CreateEventW error 0x{:x}", GetLastError()); + return false; + } + CloseHandle(pi.hThread); g_crashhandler_process = pi.hProcess; diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index 5126fe3a4..edf21d174 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -17,6 +17,11 @@ public record DalamudStartInfo // ignored } + /// + /// Gets or sets the Dalamud load method. + /// + public LoadMethod LoadMethod { get; set; } + /// /// Gets or sets the working directory of the XIVLauncher installations. /// diff --git a/Dalamud.Common/LoadMethod.cs b/Dalamud.Common/LoadMethod.cs new file mode 100644 index 000000000..ca50098e2 --- /dev/null +++ b/Dalamud.Common/LoadMethod.cs @@ -0,0 +1,17 @@ +namespace Dalamud.Common; + +/// +/// Enum describing the method Dalamud has been loaded. +/// +public enum LoadMethod +{ + /// + /// Load Dalamud by rewriting the games entrypoint. + /// + Entrypoint, + + /// + /// Load Dalamud via DLL-injection. + /// + DllInject, +} diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 3ffb7ba18..f839d9656 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -680,11 +680,11 @@ namespace Dalamud.Injector mode = mode == null ? "entrypoint" : mode.ToLowerInvariant(); if (mode.Length > 0 && mode.Length <= 10 && "entrypoint"[0..mode.Length] == mode) { - mode = "entrypoint"; + dalamudStartInfo.LoadMethod = LoadMethod.Entrypoint; } else if (mode.Length > 0 && mode.Length <= 6 && "inject"[0..mode.Length] == mode) { - mode = "inject"; + dalamudStartInfo.LoadMethod = LoadMethod.DllInject; } else { @@ -796,7 +796,7 @@ namespace Dalamud.Injector noFixAcl, p => { - if (!withoutDalamud && mode == "entrypoint") + if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.Entrypoint) { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); @@ -813,7 +813,7 @@ namespace Dalamud.Injector Log.Verbose("Game process started with PID {0}", process.Id); - if (!withoutDalamud && mode == "inject") + if (!withoutDalamud && dalamudStartInfo.LoadMethod == LoadMethod.DllInject) { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 18f7f0791..1930b6fb4 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -58,7 +58,7 @@ std::wstring u8_to_ws(const std::string& s) { } std::wstring get_window_string(HWND hWnd) { - std::wstring buf(GetWindowTextLengthW(hWnd), L'\0'); + std::wstring buf(GetWindowTextLengthW(hWnd) + 1, L'\0'); GetWindowTextW(hWnd, &buf[0], static_cast(buf.size())); return buf; } @@ -456,8 +456,10 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s { L"All files (*.*)", L"*" }, }}; - IShellItemPtr pItem; + std::optional filePath; + std::fstream fileStream; try { + IShellItemPtr pItem; SYSTEMTIME st; GetLocalTime(&st); IFileSaveDialogPtr pDialog; @@ -474,33 +476,39 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s } throw_if_failed(pDialog->GetResult(&pItem), {}, "pDialog->GetResult"); - - IBindCtxPtr pBindCtx; - throw_if_failed(CreateBindCtx(0, &pBindCtx), {}, "CreateBindCtx"); - auto options = BIND_OPTS{.cbStruct = sizeof(BIND_OPTS), .grfMode = STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE}; - throw_if_failed(pBindCtx->SetBindOptions(&options), {}, "pBindCtx->SetBindOptions"); + PWSTR pFilePath = nullptr; + throw_if_failed(pItem->GetDisplayName(SIGDN_FILESYSPATH, &pFilePath), {}, "pItem->GetDisplayName"); + pItem.Release(); + filePath.emplace(pFilePath); - IStreamPtr pStream; - throw_if_failed(pItem->BindToHandler(pBindCtx, BHID_Stream, IID_PPV_ARGS(&pStream)), {}, "pItem->BindToHandler"); - - throw_if_failed(pStream->SetSize({}), {}, "pStream->SetSize"); + fileStream.open(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc); mz_zip_archive zipa{}; - zipa.m_pIO_opaque = &*pStream; + zipa.m_pIO_opaque = &fileStream; zipa.m_pRead = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t { - const auto pStream = static_cast(pOpaque); - throw_if_failed(pStream->Seek({ .QuadPart = static_cast(file_ofs) }, STREAM_SEEK_SET, nullptr), {}, "pStream->Seek"); - ULONG read; - throw_if_failed(pStream->Read(pBuf, static_cast(n), &read), {}, "pStream->Read"); - return read; + const auto pStream = static_cast(pOpaque); + if (!pStream || !pStream->is_open()) + throw std::runtime_error("Read operation failed: Stream is not open"); + pStream->seekg(file_ofs, std::ios::beg); + if (pStream->fail()) + throw std::runtime_error("Read operation failed: Error seeking in stream"); + pStream->read(static_cast(pBuf), n); + if (pStream->fail()) + throw std::runtime_error("Read operation failed: Error reading from stream"); + return pStream->gcount(); }; zipa.m_pWrite = [](void* pOpaque, mz_uint64 file_ofs, const void* pBuf, size_t n) -> size_t { - const auto pStream = static_cast(pOpaque); - throw_if_failed(pStream->Seek({ .QuadPart = static_cast(file_ofs) }, STREAM_SEEK_SET, nullptr), {}, "pStream->Seek"); - ULONG written; - throw_if_failed(pStream->Write(pBuf, static_cast(n), &written), {}, "pStream->Write"); - return written; + const auto pStream = static_cast(pOpaque); + if (!pStream || !pStream->is_open()) + throw std::runtime_error("Write operation failed: Stream is not open"); + pStream->seekp(file_ofs, std::ios::beg); + if (pStream->fail()) + throw std::runtime_error("Write operation failed: Error seeking in stream"); + pStream->write(static_cast(pBuf), n); + if (pStream->fail()) + throw std::runtime_error("Write operation failed: Error writing to stream"); + return n; }; const auto mz_throw_if_failed = [&zipa](mz_bool res, const std::string& clue) { if (!res) @@ -545,7 +553,14 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s } auto handleInfo = HandleAndBaseOffset{.h = hLogFile, .off = baseOffset.QuadPart}; - const auto modt = std::chrono::system_clock::to_time_t(std::chrono::clock_cast(last_write_time(logFilePath))); + WIN32_FILE_ATTRIBUTE_DATA fileInfo = { 0 }; + time_t modt = time(nullptr); + if (GetFileAttributesExW(logFilePath.c_str(), GetFileExInfoStandard, &fileInfo)) { + ULARGE_INTEGER ull = { 0 }; + ull.LowPart = fileInfo.ftLastWriteTime.dwLowDateTime; + ull.HighPart = fileInfo.ftLastWriteTime.dwHighDateTime; + modt = ull.QuadPart / 10000000ULL - 11644473600ULL; + } mz_throw_if_failed(mz_zip_writer_add_read_buf_callback( &zipa, pcszLogFileName, @@ -564,34 +579,20 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s } catch (const std::exception& e) { MessageBoxW(hWndParent, std::format(L"Failed to save file: {}", u8_to_ws(e.what())).c_str(), get_window_string(hWndParent).c_str(), MB_OK | MB_ICONERROR); - - if (pItem) { + fileStream.close(); + if (filePath) { try { - IFileOperationPtr pFileOps; - throw_if_failed(pFileOps.CreateInstance(__uuidof(FileOperation), nullptr, CLSCTX_ALL)); - throw_if_failed(pFileOps->SetOperationFlags(FOF_NO_UI)); - throw_if_failed(pFileOps->DeleteItem(pItem, nullptr)); - throw_if_failed(pFileOps->PerformOperations()); - } catch (const std::exception& e2) { + std::filesystem::remove(*filePath); + } catch (const std::filesystem::filesystem_error& e2) { std::wcerr << std::format(L"Failed to remove temporary file: {}", u8_to_ws(e2.what())) << std::endl; } - pItem.Release(); } + return; } - if (pItem) { - PWSTR pwszFileName; - if (FAILED(pItem->GetDisplayName(SIGDN_FILESYSPATH, &pwszFileName))) { - if (FAILED(pItem->GetDisplayName(SIGDN_DESKTOPABSOLUTEEDITING, &pwszFileName))) { - MessageBoxW(hWndParent, L"The file has been saved to the specified path.", get_window_string(hWndParent).c_str(), MB_OK | MB_ICONINFORMATION); - } else { - std::unique_ptr::type, decltype(CoTaskMemFree)*> pszFileNamePtr(pwszFileName, CoTaskMemFree); - MessageBoxW(hWndParent, std::format(L"The file has been saved to: {}", pwszFileName).c_str(), get_window_string(hWndParent).c_str(), MB_OK | MB_ICONINFORMATION); - } - } else { - std::unique_ptr::type, decltype(CoTaskMemFree)*> pszFileNamePtr(pwszFileName, CoTaskMemFree); - ShellExecuteW(hWndParent, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", pwszFileName)).c_str(), nullptr, SW_SHOW); - } + fileStream.close(); + if (filePath) { + ShellExecuteW(hWndParent, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", *filePath)).c_str(), nullptr, SW_SHOW); } } @@ -612,7 +613,8 @@ void restart_game_using_injector(int nRadioButton, const std::vector args; - args.emplace_back((std::filesystem::path(pathStr).parent_path() / L"Dalamud.Injector.exe").wstring()); + std::wstring injectorPath = (std::filesystem::path(pathStr).parent_path() / L"Dalamud.Injector.exe").wstring(); + args.emplace_back(L'\"' + injectorPath + L'\"'); args.emplace_back(L"launch"); switch (nRadioButton) { case IdRadioRestartWithout3pPlugins: @@ -625,12 +627,11 @@ void restart_game_using_injector(int nRadioButton, const std::vector Date: Tue, 13 Feb 2024 05:56:38 +0900 Subject: [PATCH 479/585] Changes to Dalamud Boot DLL so that it works in WINE (#1111) * Changes to Dalamud Boot DLL so that it works in WINE * Make asm clearer --- Dalamud.Boot/Dalamud.Boot.vcxproj | 14 + Dalamud.Boot/Dalamud.Boot.vcxproj.filters | 10 + Dalamud.Boot/dllmain.cpp | 2 +- Dalamud.Boot/module.def | 5 + Dalamud.Boot/pch.h | 3 - Dalamud.Boot/rewrite_entrypoint.cpp | 301 +++++++++------------ Dalamud.Boot/rewrite_entrypoint_thunks.asm | 82 ++++++ 7 files changed, 235 insertions(+), 182 deletions(-) create mode 100644 Dalamud.Boot/module.def create mode 100644 Dalamud.Boot/rewrite_entrypoint_thunks.asm diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index ea263d7f9..dd3f57632 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -32,6 +32,9 @@ obj\$(Configuration)\ + + + true $(SolutionDir)bin\lib\$(Configuration)\libMinHook\;$(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64) @@ -70,6 +73,7 @@ false false + module.def @@ -83,9 +87,13 @@ true true + module.def + + + nethost.dll @@ -181,6 +189,12 @@ + + + + + + diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters index 8b4483684..a1b1650e2 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters @@ -147,4 +147,14 @@ + + + Dalamud.Boot DLL + + + + + Dalamud.Boot DLL + + \ No newline at end of file diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index 2566016e8..cf31b7016 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -159,7 +159,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { return 0; } -DllExport DWORD WINAPI Initialize(LPVOID lpParam) { +extern "C" DWORD WINAPI Initialize(LPVOID lpParam) { return InitializeImpl(lpParam, CreateEvent(nullptr, TRUE, FALSE, nullptr)); } diff --git a/Dalamud.Boot/module.def b/Dalamud.Boot/module.def new file mode 100644 index 000000000..047d825e5 --- /dev/null +++ b/Dalamud.Boot/module.def @@ -0,0 +1,5 @@ +LIBRARY Dalamud.Boot +EXPORTS + Initialize @1 + RewriteRemoteEntryPointW @2 + RewrittenEntryPoint @3 diff --git a/Dalamud.Boot/pch.h b/Dalamud.Boot/pch.h index 3302a44fb..6dda9d03b 100644 --- a/Dalamud.Boot/pch.h +++ b/Dalamud.Boot/pch.h @@ -61,9 +61,6 @@ #include "unicode.h" -// Commonly used macros -#define DllExport extern "C" __declspec(dllexport) - // Global variables extern HMODULE g_hModule; extern HINSTANCE g_hGameInstance; diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index 85a3a950b..a47254701 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -5,111 +5,87 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue); struct RewrittenEntryPointParameters { - void* pAllocation; char* pEntrypoint; - char* pEntrypointBytes; size_t entrypointLength; - char* pLoadInfo; - HANDLE hMainThread; - HANDLE hMainThreadContinue; }; -#pragma pack(push, 1) -struct EntryPointThunkTemplate { - struct DUMMYSTRUCTNAME { - struct { - const uint8_t op_mov_rdi[2]{ 0x48, 0xbf }; - void* ptr = nullptr; - } fn; +namespace thunks { + constexpr uint64_t Terminator = 0xCCCCCCCCCCCCCCCCu; + constexpr uint64_t Placeholder = 0x0606060606060606u; + + extern "C" void EntryPointReplacement(); + extern "C" void RewrittenEntryPoint_Standalone(); - const uint8_t op_call_rdi[2]{ 0xff, 0xd7 }; - } CallTrampoline; -}; + void* resolve_thunk_address(void (*pfn)()) { + const auto ptr = reinterpret_cast(pfn); + if (*ptr == 0xe9) + return ptr + 5 + *reinterpret_cast(ptr + 1); + return ptr; + } -struct TrampolineTemplate { - const struct { - const uint8_t op_sub_rsp_imm[3]{ 0x48, 0x81, 0xec }; - const uint32_t length = 0x80; - } stack_alloc; + size_t get_thunk_length(void (*pfn)()) { + size_t length = 0; + for (auto ptr = reinterpret_cast(resolve_thunk_address(pfn)); *reinterpret_cast(ptr) != Terminator; ptr++) + length++; + return length; + } - struct DUMMYSTRUCTNAME { - struct { - const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 }; - void* val = nullptr; - } lpLibFileName; + template + void* fill_placeholders(void* pfn, const T& value) { + auto ptr = static_cast(pfn); - struct { - const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf }; - decltype(&LoadLibraryW) ptr = nullptr; - } fn; + while (*reinterpret_cast(ptr) != Placeholder) + ptr++; - const uint8_t op_call_rdi[2]{ 0xff, 0xd7 }; - } CallLoadLibrary_nethost; + *reinterpret_cast(ptr) = 0; + *reinterpret_cast(ptr) = value; + return ptr + sizeof(value); + } - struct DUMMYSTRUCTNAME { - struct { - const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 }; - void* val = nullptr; - } lpLibFileName; + template + void* fill_placeholders(void* ptr, const T& value, TArgs&&...more_values) { + return fill_placeholders(fill_placeholders(ptr, value), std::forward(more_values)...); + } - struct { - const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf }; - decltype(&LoadLibraryW) ptr = nullptr; - } fn; + std::vector create_entrypointreplacement() { + std::vector buf(get_thunk_length(&EntryPointReplacement)); + memcpy(buf.data(), resolve_thunk_address(&EntryPointReplacement), buf.size()); + return buf; + } - const uint8_t op_call_rdi[2]{ 0xff, 0xd7 }; - } CallLoadLibrary_DalamudBoot; + std::vector create_standalone_rewrittenentrypoint(const std::filesystem::path& dalamud_path) { + const auto nethost_path = std::filesystem::path(dalamud_path).replace_filename(L"nethost.dll"); - struct { - const uint8_t hModule_op_mov_rcx_rax[3]{ 0x48, 0x89, 0xc1 }; + // These are null terminated, since pointers are returned from .c_str() + const auto dalamud_path_wview = std::wstring_view(dalamud_path.c_str()); + const auto nethost_path_wview = std::wstring_view(nethost_path.c_str()); - struct { - const uint8_t op_mov_rdx_imm[2]{ 0x48, 0xba }; - void* val = nullptr; - } lpProcName; + // +2 is for null terminator + const auto dalamud_path_view = std::span(reinterpret_cast(dalamud_path_wview.data()), dalamud_path_wview.size() * 2 + 2); + const auto nethost_path_view = std::span(reinterpret_cast(nethost_path_wview.data()), nethost_path_wview.size() * 2 + 2); - struct { - const uint8_t op_mov_rdi_imm[2]{ 0x48, 0xbf }; - decltype(&GetProcAddress) ptr = nullptr; - } fn; + std::vector buffer; + const auto thunk_template_length = thunks::get_thunk_length(&thunks::RewrittenEntryPoint_Standalone); + buffer.reserve(thunk_template_length + dalamud_path_view.size() + nethost_path_view.size()); + buffer.resize(thunk_template_length); + memcpy(buffer.data(), resolve_thunk_address(&thunks::RewrittenEntryPoint_Standalone), thunk_template_length); - const uint8_t op_call_rdi[2]{ 0xff, 0xd7 }; - } CallGetProcAddress; + // &::GetProcAddress will return Dalamud.dll's import table entry. + // GetProcAddress(..., "GetProcAddress") returns the address inside kernel32.dll. + const auto kernel32 = GetModuleHandleA("kernel32.dll"); - struct { - const uint8_t op_add_rsp_imm[3]{ 0x48, 0x81, 0xc4 }; - const uint32_t length = 0x80; - } stack_release; - - struct DUMMYSTRUCTNAME2 { - // rdi := returned value from GetProcAddress - const uint8_t op_mov_rdi_rax[3]{ 0x48, 0x89, 0xc7 }; - // rax := return address - const uint8_t op_pop_rax[1]{ 0x58 }; - - // rax := rax - sizeof thunk (last instruction must be call) - struct { - const uint8_t op_sub_rax_imm4[2]{ 0x48, 0x2d }; - const uint32_t displacement = static_cast(sizeof EntryPointThunkTemplate); - } op_sub_rax_to_entry_point; - - struct { - const uint8_t op_mov_rcx_imm[2]{ 0x48, 0xb9 }; - void* val = nullptr; - } param; - - const uint8_t op_push_rax[1]{ 0x50 }; - const uint8_t op_jmp_rdi[2]{ 0xff, 0xe7 }; - } CallInjectEntryPoint; - - const char buf_CallGetProcAddress_lpProcName[20] = "RewrittenEntryPoint"; - uint8_t buf_EntryPointBackup[sizeof EntryPointThunkTemplate]{}; - -#pragma pack(push, 8) - RewrittenEntryPointParameters parameters{}; -#pragma pack(pop) -}; -#pragma pack(pop) + thunks::fill_placeholders(buffer.data(), + /* pfnLoadLibraryW = */ GetProcAddress(kernel32, "LoadLibraryW"), + /* pfnGetProcAddress = */ GetProcAddress(kernel32, "GetProcAddress"), + /* pRewrittenEntryPointParameters = */ Placeholder, + /* nNethostOffset = */ 0, + /* nDalamudOffset = */ nethost_path_view.size_bytes() + ); + buffer.insert(buffer.end(), nethost_path_view.begin(), nethost_path_view.end()); + buffer.insert(buffer.end(), dalamud_path_view.begin(), dalamud_path_view.end()); + return buffer; + } +} void read_process_memory_or_throw(HANDLE hProcess, void* pAddress, void* data, size_t len) { SIZE_T read = 0; @@ -170,10 +146,17 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path exe.read(reinterpret_cast(&exe_section_headers[0]), sizeof IMAGE_SECTION_HEADER * exe_section_headers.size()); if (!exe) throw std::runtime_error("Game executable is corrupt (Truncated section header)."); + + SYSTEM_INFO sysinfo; + GetSystemInfo(&sysinfo); for (MEMORY_BASIC_INFORMATION mbi{}; VirtualQueryEx(hProcess, mbi.BaseAddress, &mbi, sizeof mbi); mbi.BaseAddress = static_cast(mbi.BaseAddress) + mbi.RegionSize) { + + // wine: apparently there exists a RegionSize of 0xFFF + mbi.RegionSize = (mbi.RegionSize + sysinfo.dwPageSize - 1) / sysinfo.dwPageSize * sysinfo.dwPageSize; + if (!(mbi.State & MEM_COMMIT) || mbi.Type != MEM_IMAGE) continue; @@ -241,18 +224,6 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path throw std::runtime_error("corresponding base address not found"); } -std::string from_utf16(const std::wstring& wstr, UINT codePage = CP_UTF8) { - std::string str(WideCharToMultiByte(codePage, 0, &wstr[0], static_cast(wstr.size()), nullptr, 0, nullptr, nullptr), 0); - WideCharToMultiByte(codePage, 0, &wstr[0], static_cast(wstr.size()), &str[0], static_cast(str.size()), nullptr, nullptr); - return str; -} - -std::wstring to_utf16(const std::string& str, UINT codePage = CP_UTF8, bool errorOnInvalidChars = false) { - std::wstring wstr(MultiByteToWideChar(codePage, 0, &str[0], static_cast(str.size()), nullptr, 0), 0); - MultiByteToWideChar(codePage, errorOnInvalidChars ? MB_ERR_INVALID_CHARS : 0, &str[0], static_cast(str.size()), &wstr[0], static_cast(wstr.size())); - return wstr; -} - /// @brief Rewrite target process' entry point so that this DLL can be loaded and executed first. /// @param hProcess Process handle. /// @param pcwzPath Path to target process. @@ -263,9 +234,9 @@ std::wstring to_utf16(const std::string& str, UINT codePage = CP_UTF8, bool erro /// Instead, we have to enumerate through all the files mapped into target process' virtual address space and find the base address /// of memory region corresponding to the path given. /// -DllExport DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) { +extern "C" DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) { try { - const auto base_address = reinterpret_cast(get_mapped_image_base_address(hProcess, pcwzPath)); + const auto base_address = static_cast(get_mapped_image_base_address(hProcess, pcwzPath)); IMAGE_DOS_HEADER dos_header{}; union { @@ -279,60 +250,35 @@ DllExport DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* ? nt_header32.OptionalHeader.AddressOfEntryPoint : nt_header64.OptionalHeader.AddressOfEntryPoint); - auto path = get_path_from_local_module(g_hModule).wstring(); - path.resize(path.size() + 1); // ensure null termination - auto path_bytes = std::span(reinterpret_cast(&path[0]), std::span(path).size_bytes()); + auto standalone_rewrittenentrypoint = thunks::create_standalone_rewrittenentrypoint(get_path_from_local_module(g_hModule)); + auto entrypoint_replacement = thunks::create_entrypointreplacement(); - auto nethost_path = (get_path_from_local_module(g_hModule).parent_path() / L"nethost.dll").wstring(); - nethost_path.resize(nethost_path.size() + 1); // ensure null termination - auto nethost_path_bytes = std::span(reinterpret_cast(&nethost_path[0]), std::span(nethost_path).size_bytes()); - - auto load_info = from_utf16(pcwzLoadInfo); + auto load_info = unicode::convert(pcwzLoadInfo); load_info.resize(load_info.size() + 1); //ensure null termination - // Allocate full buffer in advance to keep reference to trampoline valid. - std::vector buffer(sizeof TrampolineTemplate + load_info.size() + nethost_path_bytes.size() + path_bytes.size()); - auto& trampoline = *reinterpret_cast(&buffer[0]); - const auto load_info_buffer = std::span(buffer).subspan(sizeof trampoline, load_info.size()); - const auto nethost_path_buffer = std::span(buffer).subspan(sizeof trampoline + load_info.size(), nethost_path_bytes.size()); - const auto dalamud_path_buffer = std::span(buffer).subspan(sizeof trampoline + load_info.size() + nethost_path_bytes.size(), path_bytes.size()); - - new(&trampoline)TrampolineTemplate(); // this line initializes given buffer instead of allocating memory - memcpy(&load_info_buffer[0], &load_info[0], load_info_buffer.size()); - memcpy(&nethost_path_buffer[0], &nethost_path_bytes[0], nethost_path_buffer.size()); - memcpy(&dalamud_path_buffer[0], &path_bytes[0], dalamud_path_buffer.size()); - - // Backup remote process' original entry point. - read_process_memory_or_throw(hProcess, entrypoint, trampoline.buf_EntryPointBackup); + std::vector buffer(sizeof(RewrittenEntryPointParameters) + entrypoint_replacement.size() + load_info.size() + standalone_rewrittenentrypoint.size()); // Allocate buffer in remote process, which will be used to fill addresses in the local buffer. - const auto remote_buffer = reinterpret_cast(VirtualAllocEx(hProcess, nullptr, buffer.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); - - // Fill the values to be used in RewrittenEntryPoint - trampoline.parameters = { - .pAllocation = remote_buffer, - .pEntrypoint = entrypoint, - .pEntrypointBytes = remote_buffer + offsetof(TrampolineTemplate, buf_EntryPointBackup), - .entrypointLength = sizeof trampoline.buf_EntryPointBackup, - .pLoadInfo = remote_buffer + (&load_info_buffer[0] - &buffer[0]), - }; + const auto remote_buffer = static_cast(VirtualAllocEx(hProcess, nullptr, buffer.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); + + auto& params = *reinterpret_cast(buffer.data()); + params.entrypointLength = entrypoint_replacement.size(); + params.pEntrypoint = entrypoint; - // Fill the addresses referred in machine code. - trampoline.CallLoadLibrary_nethost.lpLibFileName.val = remote_buffer + (&nethost_path_buffer[0] - &buffer[0]); - trampoline.CallLoadLibrary_nethost.fn.ptr = LoadLibraryW; - trampoline.CallLoadLibrary_DalamudBoot.lpLibFileName.val = remote_buffer + (&dalamud_path_buffer[0] - &buffer[0]); - trampoline.CallLoadLibrary_DalamudBoot.fn.ptr = LoadLibraryW; - trampoline.CallGetProcAddress.lpProcName.val = remote_buffer + offsetof(TrampolineTemplate, buf_CallGetProcAddress_lpProcName); - trampoline.CallGetProcAddress.fn.ptr = GetProcAddress; - trampoline.CallInjectEntryPoint.param.val = remote_buffer + offsetof(TrampolineTemplate, parameters); + // Backup original entry point. + read_process_memory_or_throw(hProcess, entrypoint, &buffer[sizeof params], entrypoint_replacement.size()); + + memcpy(&buffer[sizeof params + entrypoint_replacement.size()], load_info.data(), load_info.size()); + + thunks::fill_placeholders(standalone_rewrittenentrypoint.data(), remote_buffer); + memcpy(&buffer[sizeof params + entrypoint_replacement.size() + load_info.size()], standalone_rewrittenentrypoint.data(), standalone_rewrittenentrypoint.size()); // Write the local buffer into the buffer in remote process. write_process_memory_or_throw(hProcess, remote_buffer, buffer.data(), buffer.size()); - // Overwrite remote process' entry point with a thunk that immediately calls our trampoline function. - EntryPointThunkTemplate thunk{}; - thunk.CallTrampoline.fn.ptr = remote_buffer; - write_process_memory_or_throw(hProcess, entrypoint, thunk); + thunks::fill_placeholders(entrypoint_replacement.data(), remote_buffer + sizeof params + entrypoint_replacement.size() + load_info.size()); + // Overwrite remote process' entry point with a thunk that will load our DLLs and call our trampoline function. + write_process_memory_or_throw(hProcess, entrypoint, entrypoint_replacement.data(), entrypoint_replacement.size()); return 0; } catch (const std::exception& e) { @@ -341,44 +287,43 @@ DllExport DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* } } -/// @deprecated -DllExport DWORD WINAPI RewriteRemoteEntryPoint(HANDLE hProcess, const wchar_t* pcwzPath, const char* pcszLoadInfo) { - return RewriteRemoteEntryPointW(hProcess, pcwzPath, to_utf16(pcszLoadInfo).c_str()); +static void AbortRewrittenEntryPoint(DWORD err, const std::wstring& clue) { + wchar_t* pwszMsg = nullptr; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + err, + MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), + reinterpret_cast(&pwszMsg), + 0, + nullptr); + + if (MessageBoxW(nullptr, std::format( + L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\nError: 0x{:08X} {}\n\n{}", + err, pwszMsg ? pwszMsg : L"", clue).c_str(), + L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO) + ExitProcess(-1); } /// @brief Entry point function "called" instead of game's original main entry point. /// @param params Parameters set up from RewriteRemoteEntryPoint. -DllExport void WINAPI RewrittenEntryPoint(RewrittenEntryPointParameters& params) { - params.hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); - if (!params.hMainThreadContinue) - ExitProcess(-1); +extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) { + const auto pOriginalEntryPointBytes = reinterpret_cast(¶ms) + sizeof(params); + const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength; - // Do whatever the work in a separate thread to minimize the stack usage at this context, - // as this function really should have been a naked procedure but __declspec(naked) isn't supported in x64 version of msvc. - params.hMainThread = CreateThread(nullptr, 0, [](void* p) -> DWORD { - try { - std::string loadInfo; - auto& params = *reinterpret_cast(p); - { - // Restore original entry point. - // Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect. - write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, params.pEntrypointBytes, params.entrypointLength); + // Restore original entry point. + // Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect. + if (SIZE_T written; !WriteProcessMemory(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength, &written)) + return AbortRewrittenEntryPoint(GetLastError(), L"WriteProcessMemory(entrypoint restoration)"); - // Make a copy of load info, as the whole params will be freed after this code block. - loadInfo = params.pLoadInfo; - } + const auto hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); + if (!hMainThreadContinue) + return AbortRewrittenEntryPoint(GetLastError(), L"CreateEventW"); - InitializeImpl(&loadInfo[0], params.hMainThreadContinue); - return 0; - } catch (const std::exception& e) { - MessageBoxA(nullptr, std::format("Failed to load Dalamud.\n\nError: {}", e.what()).c_str(), "Dalamud.Boot", MB_OK | MB_ICONERROR); - ExitProcess(-1); - } - }, ¶ms, 0, nullptr); - if (!params.hMainThread) - ExitProcess(-1); + if (const auto result = InitializeImpl(pLoadInfo, hMainThreadContinue)) + return AbortRewrittenEntryPoint(result, L"InitializeImpl"); - CloseHandle(params.hMainThread); - WaitForSingleObject(params.hMainThreadContinue, INFINITE); - VirtualFree(params.pAllocation, 0, MEM_RELEASE); + WaitForSingleObject(hMainThreadContinue, INFINITE); + VirtualFree(¶ms, 0, MEM_RELEASE); } diff --git a/Dalamud.Boot/rewrite_entrypoint_thunks.asm b/Dalamud.Boot/rewrite_entrypoint_thunks.asm new file mode 100644 index 000000000..af7be8287 --- /dev/null +++ b/Dalamud.Boot/rewrite_entrypoint_thunks.asm @@ -0,0 +1,82 @@ +PUBLIC EntryPointReplacement +PUBLIC RewrittenEntryPoint_Standalone +PUBLIC RewrittenEntryPoint + +; 06 and 07 are invalid opcodes +; CC is int3 = bp +; using 0CCCCCCCCCCCCCCCCh as function terminator +; using 00606060606060606h as placeholders + +TERMINATOR = 0CCCCCCCCCCCCCCCCh +PLACEHOLDER = 00606060606060606h + +.code + +EntryPointReplacement PROC + start: + ; rsp % 0x10 = 0x08 + lea rax, [start] + push rax + + ; rsp % 0x10 = 0x00 + mov rax, PLACEHOLDER + + ; this calls RewrittenEntryPoint_Standalone + jmp rax + + dq TERMINATOR +EntryPointReplacement ENDP + +RewrittenEntryPoint_Standalone PROC + start: + ; stack is aligned to 0x10; see above + sub rsp, 20h + lea rcx, [embeddedData] + add rcx, qword ptr [nNethostOffset] + call qword ptr [pfnLoadLibraryW] + + lea rcx, [embeddedData] + add rcx, qword ptr [nDalamudOffset] + call qword ptr [pfnLoadLibraryW] + + mov rcx, rax + lea rdx, [pcszEntryPointName] + call qword ptr [pfnGetProcAddress] + + mov rcx, qword ptr [pRewrittenEntryPointParameters] + ; this calls RewrittenEntryPoint + jmp rax + + pfnLoadLibraryW: + dq PLACEHOLDER + + pfnGetProcAddress: + dq PLACEHOLDER + + pRewrittenEntryPointParameters: + dq PLACEHOLDER + + nNethostOffset: + dq PLACEHOLDER + + nDalamudOffset: + dq PLACEHOLDER + + pcszEntryPointName: + db "RewrittenEntryPoint", 0 + + embeddedData: + + dq TERMINATOR +RewrittenEntryPoint_Standalone ENDP + +EXTERN RewrittenEntryPoint_AdjustedStack :PROC + +RewrittenEntryPoint PROC + ; stack is aligned to 0x10; see above + call RewrittenEntryPoint_AdjustedStack + add rsp, 20h + ret +RewrittenEntryPoint ENDP + +END From 7e78b6293b0de07083d8c216dd76843e3aafa159 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 13 Feb 2024 07:31:49 +0900 Subject: [PATCH 480/585] Make RewriteRemoteEntryPointW report IErrorInfo, VirtualProtectEx before WriteProcessMemory --- Dalamud.Boot/Dalamud.Boot.vcxproj | 2 +- Dalamud.Boot/dllmain.cpp | 8 +- Dalamud.Boot/pch.h | 3 + Dalamud.Boot/rewrite_entrypoint.cpp | 133 +++++++++++++++++++---- Dalamud.Boot/utils.cpp | 44 ++++++-- Dalamud.Boot/utils.h | 5 + Dalamud.Injector.Boot/main.cpp | 10 +- Dalamud.Injector/EntryPoint.cs | 160 +++++++++++++++------------- lib/CoreCLR/boot.cpp | 10 +- lib/CoreCLR/boot.h | 2 +- 10 files changed, 254 insertions(+), 123 deletions(-) diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index dd3f57632..ab68c1ec0 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -199,4 +199,4 @@ - \ No newline at end of file + diff --git a/Dalamud.Boot/dllmain.cpp b/Dalamud.Boot/dllmain.cpp index cf31b7016..e6aa9c4ac 100644 --- a/Dalamud.Boot/dllmain.cpp +++ b/Dalamud.Boot/dllmain.cpp @@ -9,7 +9,7 @@ HMODULE g_hModule; HINSTANCE g_hGameInstance = GetModuleHandleW(nullptr); -DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { +HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { g_startInfo.from_envvars(); std::string jsonParseError; @@ -114,7 +114,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { logging::I("Calling InitializeClrAndGetEntryPoint"); void* entrypoint_vfn; - int result = InitializeClrAndGetEntryPoint( + const auto result = InitializeClrAndGetEntryPoint( g_hModule, g_startInfo.BootEnableEtw, runtimeconfig_path, @@ -124,7 +124,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { L"Dalamud.EntryPoint+InitDelegate, Dalamud", &entrypoint_vfn); - if (result != 0) + if (FAILED(result)) return result; using custom_component_entry_point_fn = void (CORECLR_DELEGATE_CALLTYPE*)(LPVOID, HANDLE); @@ -156,7 +156,7 @@ DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue) { entrypoint_fn(lpParam, hMainThreadContinue); logging::I("Done!"); - return 0; + return S_OK; } extern "C" DWORD WINAPI Initialize(LPVOID lpParam) { diff --git a/Dalamud.Boot/pch.h b/Dalamud.Boot/pch.h index 6dda9d03b..a09882c74 100644 --- a/Dalamud.Boot/pch.h +++ b/Dalamud.Boot/pch.h @@ -26,6 +26,9 @@ // MSVC Compiler Intrinsic #include +// COM +#include + // C++ Standard Libraries #include #include diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index a47254701..6ece3665c 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -1,8 +1,9 @@ #include "pch.h" #include "logging.h" +#include "utils.h" -DWORD WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue); +HRESULT WINAPI InitializeImpl(LPVOID lpParam, HANDLE hMainThreadContinue); struct RewrittenEntryPointParameters { char* pEntrypoint; @@ -102,6 +103,7 @@ void read_process_memory_or_throw(HANDLE hProcess, void* pAddress, T& data) { void write_process_memory_or_throw(HANDLE hProcess, void* pAddress, const void* data, size_t len) { SIZE_T written = 0; + const utils::memory_tenderizer tenderizer(hProcess, pAddress, len, PAGE_EXECUTE_READWRITE); if (!WriteProcessMemory(hProcess, pAddress, data, len, &written)) throw std::runtime_error("WriteProcessMemory failure"); if (written != len) @@ -227,15 +229,18 @@ void* get_mapped_image_base_address(HANDLE hProcess, const std::filesystem::path /// @brief Rewrite target process' entry point so that this DLL can be loaded and executed first. /// @param hProcess Process handle. /// @param pcwzPath Path to target process. -/// @param pcszLoadInfo JSON string to be passed to Initialize. -/// @return 0 if successful; nonzero if unsuccessful +/// @param pcwzLoadInfo JSON string to be passed to Initialize. +/// @return null if successful; memory containing wide string allocated via GlobalAlloc if unsuccessful /// /// When the process has just been started up via CreateProcess (CREATE_SUSPENDED), GetModuleFileName and alikes result in an error. /// Instead, we have to enumerate through all the files mapped into target process' virtual address space and find the base address /// of memory region corresponding to the path given. /// -extern "C" DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) { +extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* pcwzPath, const wchar_t* pcwzLoadInfo) { + std::wstring last_operation; + SetLastError(ERROR_SUCCESS); try { + last_operation = L"get_mapped_image_base_address"; const auto base_address = static_cast(get_mapped_image_base_address(hProcess, pcwzPath)); IMAGE_DOS_HEADER dos_header{}; @@ -244,21 +249,34 @@ extern "C" DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* IMAGE_NT_HEADERS64 nt_header64{}; }; + last_operation = L"read_process_memory_or_throw(base_address)"; read_process_memory_or_throw(hProcess, base_address, dos_header); + + last_operation = L"read_process_memory_or_throw(base_address + dos_header.e_lfanew)"; read_process_memory_or_throw(hProcess, base_address + dos_header.e_lfanew, nt_header64); const auto entrypoint = base_address + (nt_header32.OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC ? nt_header32.OptionalHeader.AddressOfEntryPoint : nt_header64.OptionalHeader.AddressOfEntryPoint); - auto standalone_rewrittenentrypoint = thunks::create_standalone_rewrittenentrypoint(get_path_from_local_module(g_hModule)); + last_operation = L"get_path_from_local_module(g_hModule)"; + auto local_module_path = get_path_from_local_module(g_hModule); + + last_operation = L"thunks::create_standalone_rewrittenentrypoint(local_module_path)"; + auto standalone_rewrittenentrypoint = thunks::create_standalone_rewrittenentrypoint(local_module_path); + + last_operation = L"thunks::create_entrypointreplacement()"; auto entrypoint_replacement = thunks::create_entrypointreplacement(); + last_operation = L"unicode::convert(pcwzLoadInfo)"; auto load_info = unicode::convert(pcwzLoadInfo); load_info.resize(load_info.size() + 1); //ensure null termination - std::vector buffer(sizeof(RewrittenEntryPointParameters) + entrypoint_replacement.size() + load_info.size() + standalone_rewrittenentrypoint.size()); + const auto bufferSize = sizeof(RewrittenEntryPointParameters) + entrypoint_replacement.size() + load_info.size() + standalone_rewrittenentrypoint.size(); + last_operation = std::format(L"std::vector alloc({}b)", bufferSize); + std::vector buffer(bufferSize); // Allocate buffer in remote process, which will be used to fill addresses in the local buffer. + last_operation = std::format(L"VirtualAllocEx({}b)", bufferSize); const auto remote_buffer = static_cast(VirtualAllocEx(hProcess, nullptr, buffer.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)); auto& params = *reinterpret_cast(buffer.data()); @@ -266,24 +284,51 @@ extern "C" DWORD WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_t* params.pEntrypoint = entrypoint; // Backup original entry point. + last_operation = std::format(L"read_process_memory_or_throw(entrypoint, {}b)", entrypoint_replacement.size()); read_process_memory_or_throw(hProcess, entrypoint, &buffer[sizeof params], entrypoint_replacement.size()); memcpy(&buffer[sizeof params + entrypoint_replacement.size()], load_info.data(), load_info.size()); + last_operation = L"thunks::fill_placeholders(EntryPointReplacement)"; thunks::fill_placeholders(standalone_rewrittenentrypoint.data(), remote_buffer); memcpy(&buffer[sizeof params + entrypoint_replacement.size() + load_info.size()], standalone_rewrittenentrypoint.data(), standalone_rewrittenentrypoint.size()); // Write the local buffer into the buffer in remote process. + last_operation = std::format(L"write_process_memory_or_throw(remote_buffer, {}b)", buffer.size()); write_process_memory_or_throw(hProcess, remote_buffer, buffer.data(), buffer.size()); + last_operation = L"thunks::fill_placeholders(RewrittenEntryPoint_Standalone::pRewrittenEntryPointParameters)"; thunks::fill_placeholders(entrypoint_replacement.data(), remote_buffer + sizeof params + entrypoint_replacement.size() + load_info.size()); + // Overwrite remote process' entry point with a thunk that will load our DLLs and call our trampoline function. + last_operation = std::format(L"write_process_memory_or_throw(entrypoint={:X}, {}b)", reinterpret_cast(entrypoint), buffer.size()); write_process_memory_or_throw(hProcess, entrypoint, entrypoint_replacement.data(), entrypoint_replacement.size()); - return 0; + return S_OK; } catch (const std::exception& e) { - OutputDebugStringA(std::format("RewriteRemoteEntryPoint failure: {} (GetLastError: {})\n", e.what(), GetLastError()).c_str()); - return 1; + const auto err = GetLastError(); + const auto hr = err == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(err); + auto formatted = std::format( + L"{}: {} ({})", + last_operation, + unicode::convert(e.what()), + utils::format_win32_error(err)); + OutputDebugStringW((formatted + L"\r\n").c_str()); + + ICreateErrorInfoPtr cei; + if (FAILED(CreateErrorInfo(&cei))) + return hr; + if (FAILED(cei->SetSource(const_cast(L"Dalamud.Boot")))) + return hr; + if (FAILED(cei->SetDescription(const_cast(formatted.c_str())))) + return hr; + + IErrorInfoPtr ei; + if (FAILED(cei.QueryInterface(IID_PPV_ARGS(&ei)))) + return hr; + + (void)SetErrorInfo(0, ei); + return hr; } } @@ -300,8 +345,9 @@ static void AbortRewrittenEntryPoint(DWORD err, const std::wstring& clue) { nullptr); if (MessageBoxW(nullptr, std::format( - L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\nError: 0x{:08X} {}\n\n{}", - err, pwszMsg ? pwszMsg : L"", clue).c_str(), + L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n\n{}", + utils::format_win32_error(err), + clue).c_str(), L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO) ExitProcess(-1); } @@ -309,21 +355,62 @@ static void AbortRewrittenEntryPoint(DWORD err, const std::wstring& clue) { /// @brief Entry point function "called" instead of game's original main entry point. /// @param params Parameters set up from RewriteRemoteEntryPoint. extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) { - const auto pOriginalEntryPointBytes = reinterpret_cast(¶ms) + sizeof(params); - const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength; + HANDLE hMainThreadContinue = nullptr; + auto hr = S_OK; + std::wstring last_operation; + std::wstring exc_msg; + SetLastError(ERROR_SUCCESS); - // Restore original entry point. - // Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect. - if (SIZE_T written; !WriteProcessMemory(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength, &written)) - return AbortRewrittenEntryPoint(GetLastError(), L"WriteProcessMemory(entrypoint restoration)"); + try { + const auto pOriginalEntryPointBytes = reinterpret_cast(¶ms) + sizeof(params); + const auto pLoadInfo = pOriginalEntryPointBytes + params.entrypointLength; - const auto hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); - if (!hMainThreadContinue) - return AbortRewrittenEntryPoint(GetLastError(), L"CreateEventW"); + // Restore original entry point. + // Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect. + last_operation = L"restore original entry point"; + write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength); - if (const auto result = InitializeImpl(pLoadInfo, hMainThreadContinue)) - return AbortRewrittenEntryPoint(result, L"InitializeImpl"); + hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); + last_operation = L"hMainThreadContinue = CreateEventW"; + if (!hMainThreadContinue) + throw std::runtime_error("CreateEventW"); + + last_operation = L"InitializeImpl"; + hr = InitializeImpl(pLoadInfo, hMainThreadContinue); + } catch (const std::exception& e) { + if (hr == S_OK) { + const auto err = GetLastError(); + hr = err == ERROR_SUCCESS ? E_FAIL : HRESULT_FROM_WIN32(err); + } + + ICreateErrorInfoPtr cei; + IErrorInfoPtr ei; + if (SUCCEEDED(CreateErrorInfo(&cei)) + && SUCCEEDED(cei->SetDescription(const_cast(unicode::convert(e.what()).c_str()))) + && SUCCEEDED(cei.QueryInterface(IID_PPV_ARGS(&ei)))) { + (void)SetErrorInfo(0, ei); + } + } + + if (FAILED(hr)) { + const _com_error err(hr); + auto desc = err.Description(); + if (desc.length() == 0) + desc = err.ErrorMessage(); + if (MessageBoxW(nullptr, std::format( + L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n{}", + last_operation, + desc.GetBSTR()).c_str(), + L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO) + ExitProcess(-1); + if (hMainThreadContinue) { + CloseHandle(hMainThreadContinue); + hMainThreadContinue = nullptr; + } + } + + if (hMainThreadContinue) + WaitForSingleObject(hMainThreadContinue, INFINITE); - WaitForSingleObject(hMainThreadContinue, INFINITE); VirtualFree(¶ms, 0, MEM_RELEASE); } diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index 62a9d7055..419ee6397 100644 --- a/Dalamud.Boot/utils.cpp +++ b/Dalamud.Boot/utils.cpp @@ -408,14 +408,20 @@ utils::signature_finder::result utils::signature_finder::find_one() const { return find(1, 1, false).front(); } -utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect) : m_data(reinterpret_cast(const_cast(pAddress)), length) { +utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect) + : memory_tenderizer(GetCurrentProcess(), pAddress, length, dwNewProtect) { +} + +utils::memory_tenderizer::memory_tenderizer(HANDLE hProcess, const void* pAddress, size_t length, DWORD dwNewProtect) +: m_process(hProcess) +, m_data(static_cast(const_cast(pAddress)), length) { try { - for (auto pCoveredAddress = &m_data[0]; - pCoveredAddress < &m_data[0] + m_data.size(); - pCoveredAddress = reinterpret_cast(m_regions.back().BaseAddress) + m_regions.back().RegionSize) { + for (auto pCoveredAddress = m_data.data(); + pCoveredAddress < m_data.data() + m_data.size(); + pCoveredAddress = static_cast(m_regions.back().BaseAddress) + m_regions.back().RegionSize) { MEMORY_BASIC_INFORMATION region{}; - if (!VirtualQuery(pCoveredAddress, ®ion, sizeof region)) { + if (!VirtualQueryEx(hProcess, pCoveredAddress, ®ion, sizeof region)) { throw std::runtime_error(std::format( "VirtualQuery(addr=0x{:X}, ..., cb={}) failed with Win32 code 0x{:X}", reinterpret_cast(pCoveredAddress), @@ -423,7 +429,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, GetLastError())); } - if (!VirtualProtect(region.BaseAddress, region.RegionSize, dwNewProtect, ®ion.Protect)) { + if (!VirtualProtectEx(hProcess, region.BaseAddress, region.RegionSize, dwNewProtect, ®ion.Protect)) { throw std::runtime_error(std::format( "(Change)VirtualProtect(addr=0x{:X}, size=0x{:X}, ..., ...) failed with Win32 code 0x{:X}", reinterpret_cast(region.BaseAddress), @@ -436,7 +442,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, } catch (...) { for (auto& region : std::ranges::reverse_view(m_regions)) { - if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, ®ion.Protect)) { + if (!VirtualProtectEx(hProcess, region.BaseAddress, region.RegionSize, region.Protect, ®ion.Protect)) { // Could not restore; fast fail __fastfail(GetLastError()); } @@ -448,7 +454,7 @@ utils::memory_tenderizer::memory_tenderizer(const void* pAddress, size_t length, utils::memory_tenderizer::~memory_tenderizer() { for (auto& region : std::ranges::reverse_view(m_regions)) { - if (!VirtualProtect(region.BaseAddress, region.RegionSize, region.Protect, ®ion.Protect)) { + if (!VirtualProtectEx(m_process, region.BaseAddress, region.RegionSize, region.Protect, ®ion.Protect)) { // Could not restore; fast fail __fastfail(GetLastError()); } @@ -654,3 +660,25 @@ std::wstring utils::escape_shell_arg(const std::wstring& arg) { } return res; } + +std::wstring utils::format_win32_error(DWORD err) { + wchar_t* pwszMsg = nullptr; + FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | + FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + err, + MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), + reinterpret_cast(&pwszMsg), + 0, + nullptr); + if (pwszMsg) { + std::wstring result = std::format(L"Win32 error ({}=0x{:X}): {}", err, err, pwszMsg); + while (!result.empty() && std::isspace(result.back())) + result.pop_back(); + LocalFree(pwszMsg); + return result; + } + + return std::format(L"Win32 error ({}=0x{:X})", err, err); +} diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h index ebf48a294..fef920f60 100644 --- a/Dalamud.Boot/utils.h +++ b/Dalamud.Boot/utils.h @@ -111,10 +111,13 @@ namespace utils { }; class memory_tenderizer { + HANDLE m_process; std::span m_data; std::vector m_regions; public: + memory_tenderizer(HANDLE hProcess, const void* pAddress, size_t length, DWORD dwNewProtect); + memory_tenderizer(const void* pAddress, size_t length, DWORD dwNewProtect); template&& std::is_standard_layout_v>> @@ -275,4 +278,6 @@ namespace utils { void wait_for_game_window(); std::wstring escape_shell_arg(const std::wstring& arg); + + std::wstring format_win32_error(DWORD err); } diff --git a/Dalamud.Injector.Boot/main.cpp b/Dalamud.Injector.Boot/main.cpp index 741505d08..7fc44f5e1 100644 --- a/Dalamud.Injector.Boot/main.cpp +++ b/Dalamud.Injector.Boot/main.cpp @@ -23,7 +23,7 @@ int wmain(int argc, wchar_t** argv) // =========================================================================== // void* entrypoint_vfn; - int result = InitializeClrAndGetEntryPoint( + const auto result = InitializeClrAndGetEntryPoint( GetModuleHandleW(nullptr), false, runtimeconfig_path, @@ -33,15 +33,15 @@ int wmain(int argc, wchar_t** argv) L"Dalamud.Injector.EntryPoint+MainDelegate, Dalamud.Injector", &entrypoint_vfn); - if (result != 0) + if (FAILED(result)) return result; - typedef void (CORECLR_DELEGATE_CALLTYPE* custom_component_entry_point_fn)(int, wchar_t**); + 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..."); - entrypoint_fn(argc, argv); + const auto ret = entrypoint_fn(argc, argv); logging::I("Done!"); - return 0; + return ret; } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index f839d9656..9e2b95657 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -31,89 +31,100 @@ namespace Dalamud.Injector /// /// Count of arguments. /// char** string arguments. - public delegate void MainDelegate(int argc, IntPtr argvPtr); + /// Return value (HRESULT). + public delegate int MainDelegate(int argc, IntPtr argvPtr); /// /// Start the Dalamud injector. /// /// Count of arguments. /// byte** string arguments. - public static void Main(int argc, IntPtr argvPtr) + /// Return value (HRESULT). + public static int Main(int argc, IntPtr argvPtr) { - List args = new(argc); - - unsafe + try { - var argv = (IntPtr*)argvPtr; - for (var i = 0; i < argc; i++) - args.Add(Marshal.PtrToStringUni(argv[i])); - } + List args = new(argc); - Init(args); - args.Remove("-v"); // Remove "verbose" flag - - if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test") - { - Environment.Exit(ProcessLaunchTestCommand(args)); - return; - } - - DalamudStartInfo startInfo = null; - if (args.Count == 1) - { - // No command defaults to inject - args.Add("inject"); - args.Add("--all"); - -#if !DEBUG - args.Add("--warn"); -#endif - - } - else if (int.TryParse(args[1], out var _)) - { - // Assume that PID has been passed. - args.Insert(1, "inject"); - - // If originally second parameter exists, then assume that it's a base64 encoded start info. - // Dalamud.Injector.exe inject [pid] [base64] - if (args.Count == 4) + unsafe { - startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[3]))); - args.RemoveAt(3); + var argv = (IntPtr*)argvPtr; + for (var i = 0; i < argc; i++) + args.Add(Marshal.PtrToStringUni(argv[i])); + } + + Init(args); + args.Remove("-v"); // Remove "verbose" flag + + if (args.Count >= 2 && args[1].ToLowerInvariant() == "launch-test") + { + return ProcessLaunchTestCommand(args); + } + + DalamudStartInfo startInfo = null; + if (args.Count == 1) + { + // No command defaults to inject + args.Add("inject"); + args.Add("--all"); + + #if !DEBUG + args.Add("--warn"); + #endif + + } + else if (int.TryParse(args[1], out var _)) + { + // Assume that PID has been passed. + args.Insert(1, "inject"); + + // If originally second parameter exists, then assume that it's a base64 encoded start info. + // Dalamud.Injector.exe inject [pid] [base64] + if (args.Count == 4) + { + startInfo = JsonConvert.DeserializeObject(Encoding.UTF8.GetString(Convert.FromBase64String(args[3]))); + args.RemoveAt(3); + } + } + + startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args); + // Remove already handled arguments + args.Remove("--console"); + args.Remove("--msgbox1"); + args.Remove("--msgbox2"); + args.Remove("--msgbox3"); + args.Remove("--etw"); + args.Remove("--veh"); + args.Remove("--veh-full"); + args.Remove("--no-plugin"); + args.Remove("--no-3rd-plugin"); + args.Remove("--crash-handler-console"); + args.Remove("--no-exception-handlers"); + + var mainCommand = args[1].ToLowerInvariant(); + if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand) + { + return ProcessInjectCommand(args, startInfo); + } + else if (mainCommand.Length > 0 && mainCommand.Length <= 6 && + "launch"[..mainCommand.Length] == mainCommand) + { + return ProcessLaunchCommand(args, startInfo); + } + else if (mainCommand.Length > 0 && mainCommand.Length <= 4 && + "help"[..mainCommand.Length] == mainCommand) + { + return ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null); + } + else + { + throw new CommandLineException($"\"{mainCommand}\" is not a valid command."); } } - - startInfo = ExtractAndInitializeStartInfoFromArguments(startInfo, args); - // Remove already handled arguments - args.Remove("--console"); - args.Remove("--msgbox1"); - args.Remove("--msgbox2"); - args.Remove("--msgbox3"); - args.Remove("--etw"); - args.Remove("--veh"); - args.Remove("--veh-full"); - args.Remove("--no-plugin"); - args.Remove("--no-3rd-plugin"); - args.Remove("--crash-handler-console"); - args.Remove("--no-exception-handlers"); - - var mainCommand = args[1].ToLowerInvariant(); - if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "inject"[..mainCommand.Length] == mainCommand) + catch (Exception e) { - Environment.Exit(ProcessInjectCommand(args, startInfo)); - } - else if (mainCommand.Length > 0 && mainCommand.Length <= 6 && "launch"[..mainCommand.Length] == mainCommand) - { - Environment.Exit(ProcessLaunchCommand(args, startInfo)); - } - else if (mainCommand.Length > 0 && mainCommand.Length <= 4 && "help"[..mainCommand.Length] == mainCommand) - { - Environment.Exit(ProcessHelpCommand(args, args.Count >= 3 ? args[2] : null)); - } - else - { - throw new CommandLineException($"\"{mainCommand}\" is not a valid command."); + Log.Error(e, "Operation failed."); + return e.HResult; } } @@ -189,6 +200,7 @@ namespace Dalamud.Injector CullLogFile(logPath, 1 * 1024 * 1024); Log.Logger = new LoggerConfiguration() + .WriteTo.Console(standardErrorFromLevel: LogEventLevel.Debug) .WriteTo.File(logPath, fileSizeLimitBytes: null) .MinimumLevel.ControlledBy(levelSwitch) .CreateLogger(); @@ -800,12 +812,8 @@ namespace Dalamud.Injector { var startInfo = AdjustStartInfo(dalamudStartInfo, gamePath); Log.Information("Using start info: {0}", JsonConvert.SerializeObject(startInfo)); - if (RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo)) != 0) - { - Log.Error("[HOOKS] RewriteRemoteEntryPointW failed"); - throw new Exception("RewriteRemoteEntryPointW failed"); - } - + Marshal.ThrowExceptionForHR( + RewriteRemoteEntryPointW(p.Handle, gamePath, JsonConvert.SerializeObject(startInfo))); Log.Verbose("RewriteRemoteEntryPointW called!"); } }, diff --git a/lib/CoreCLR/boot.cpp b/lib/CoreCLR/boot.cpp index e3db99c4f..54276aad1 100644 --- a/lib/CoreCLR/boot.cpp +++ b/lib/CoreCLR/boot.cpp @@ -27,7 +27,7 @@ void ConsoleTeardown() std::optional g_clr; -int InitializeClrAndGetEntryPoint( +HRESULT InitializeClrAndGetEntryPoint( void* calling_module, bool enable_etw, std::wstring runtimeconfig_path, @@ -76,7 +76,7 @@ int InitializeClrAndGetEntryPoint( if (result != 0) { logging::E("Unable to get RoamingAppData path (err={})", result); - return result; + return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); } std::filesystem::path fs_app_data(_appdata); @@ -92,7 +92,7 @@ int InitializeClrAndGetEntryPoint( if (!std::filesystem::exists(dotnet_path)) { logging::E("Error: Unable to find .NET runtime path"); - return 1; + return HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND); } get_hostfxr_parameters init_parameters @@ -137,12 +137,12 @@ int InitializeClrAndGetEntryPoint( entrypoint_delegate_type_name.c_str(), nullptr, entrypoint_fn)) != 0) { - logging::E("Failed to load module (err={})", result); + logging::E("Failed to load module (err=0x{:X})", static_cast(result)); return result; } logging::I("Done!"); // =========================================================================== // - return 0; + return S_OK; } diff --git a/lib/CoreCLR/boot.h b/lib/CoreCLR/boot.h index f75077edd..33bc58bbf 100644 --- a/lib/CoreCLR/boot.h +++ b/lib/CoreCLR/boot.h @@ -1,7 +1,7 @@ void ConsoleSetup(const std::wstring console_name); void ConsoleTeardown(); -int InitializeClrAndGetEntryPoint( +HRESULT InitializeClrAndGetEntryPoint( void* calling_module, bool enable_etw, std::wstring runtimeconfig_path, From 0854b6d0257258568bdb21021ccdfd641700f7a5 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 13 Feb 2024 02:12:54 +0100 Subject: [PATCH 481/585] Update ClientStructs (#1643) 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 e3bd59106..cb30048d0 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e3bd5910678683a718e68f0f940c88b08c24eba5 +Subproject commit cb30048d00e9b9d0c24aa9f12c98de3590e72371 From 3b3823d4e6847b2501e1d1cf45882359fd4857fb Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:08:36 +0100 Subject: [PATCH 482/585] Update ClientStructs (#1644) 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 cb30048d0..4b13c01e2 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit cb30048d00e9b9d0c24aa9f12c98de3590e72371 +Subproject commit 4b13c01e2f60143f24698a6280255fb1aba7ab63 From 34daa73612b0b6419734c4d8c457a3933b6ca22b Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 14 Feb 2024 05:09:46 +0900 Subject: [PATCH 483/585] Implement FontChooserDialog (#1637) * Implement FontChooserDialog * Minor fixes * Fixes 2 * Add Reset default font button * Add failsafe * reduce uninteresting exception message * Add remarks to use AttachExtraGlyphsForDalamudLanguage * Support advanced font configuration options * fixes * Shift ui elements * more fixes * Add To(Localized)String for IFontSpec * Untie GlobalFontScale from default font size * Layout fixes * Make UiBuilder.DefaultFontSize point to user configured value * Update example for NewDelegateFontHandle * Font interfaces: write notes on not intended for plugins to implement * Update default gamma to 1.7 to match closer to prev behavior (1.4**2) * Fix console window layout --- .../Internal/DalamudConfiguration.cs | 9 +- .../DalamudAssetFontAndFamilyId.cs | 87 ++ .../DalamudDefaultFontAndFamilyId.cs | 77 ++ .../FontIdentifier/GameFontAndFamilyId.cs | 81 ++ .../Interface/FontIdentifier/IFontFamilyId.cs | 102 ++ Dalamud/Interface/FontIdentifier/IFontId.cs | 40 + Dalamud/Interface/FontIdentifier/IFontSpec.cs | 50 + .../IObjectWithLocalizableName.cs | 76 ++ .../FontIdentifier/SingleFontSpec.cs | 155 +++ .../FontIdentifier/SystemFontFamilyId.cs | 181 +++ .../Interface/FontIdentifier/SystemFontId.cs | 163 +++ .../SingleFontChooserDialog.cs | 1117 +++++++++++++++++ .../Interface/Internal/InterfaceManager.cs | 9 +- .../Internal/Windows/ConsoleWindow.cs | 21 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 87 +- .../Windows/Settings/SettingsWindow.cs | 4 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 79 +- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 9 +- .../IFontAtlasBuildToolkit.cs | 3 +- .../IFontAtlasBuildToolkitPostBuild.cs | 3 +- .../IFontAtlasBuildToolkitPreBuild.cs | 15 +- .../Interface/ManagedFontAtlas/IFontHandle.cs | 3 +- .../ManagedFontAtlas/ILockedImFont.cs | 3 +- .../FontAtlasFactory.BuildToolkit.cs | 42 +- .../FontAtlasFactory.Implementation.cs | 3 +- .../Internals/FontAtlasFactory.cs | 24 +- .../ManagedFontAtlas/SafeFontConfig.cs | 2 +- Dalamud/Interface/UiBuilder.cs | 10 +- Dalamud/Interface/Utility/ImGuiHelpers.cs | 19 + Dalamud/Utility/ArrayExtensions.cs | 72 ++ Dalamud/Utility/Util.cs | 13 + 31 files changed, 2478 insertions(+), 81 deletions(-) create mode 100644 Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/IFontFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/IFontId.cs create mode 100644 Dalamud/Interface/FontIdentifier/IFontSpec.cs create mode 100644 Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs create mode 100644 Dalamud/Interface/FontIdentifier/SingleFontSpec.cs create mode 100644 Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs create mode 100644 Dalamud/Interface/FontIdentifier/SystemFontId.cs create mode 100644 Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 66c2745c5..957be12b9 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using Dalamud.Game.Text; +using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Style; using Dalamud.IoC.Internal; @@ -145,7 +146,13 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets a value indicating whether to use AXIS fonts from the game. /// - public bool UseAxisFontsFromGame { get; set; } = false; + [Obsolete($"See {nameof(DefaultFontSpec)}")] + public bool UseAxisFontsFromGame { get; set; } = true; + + /// + /// Gets or sets the default font spec. + /// + public IFontSpec? DefaultFontSpec { get; set; } /// /// Gets or sets the gamma value to apply for Dalamud fonts. Do not use. diff --git a/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs new file mode 100644 index 000000000..a6d40e4b7 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/DalamudAssetFontAndFamilyId.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; + +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Storage.Assets; + +using ImGuiNET; + +using Newtonsoft.Json; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a font from Dalamud assets. +/// +public sealed class DalamudAssetFontAndFamilyId : IFontFamilyId, IFontId +{ + /// + /// Initializes a new instance of the class. + /// + /// The font asset. + public DalamudAssetFontAndFamilyId(DalamudAsset asset) + { + if (asset.GetPurpose() != DalamudAssetPurpose.Font) + throw new ArgumentOutOfRangeException(nameof(asset), asset, "The specified asset is not a font asset."); + this.Asset = asset; + } + + /// + /// Gets the font asset. + /// + [JsonProperty] + public DalamudAsset Asset { get; init; } + + /// + [JsonIgnore] + public string EnglishName => $"Dalamud: {this.Asset}"; + + /// + [JsonIgnore] + public IReadOnlyDictionary? LocaleNames => null; + + /// + [JsonIgnore] + public IReadOnlyList Fonts => new List { this }.AsReadOnly(); + + /// + [JsonIgnore] + public IFontFamilyId Family => this; + + /// + [JsonIgnore] + public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + + /// + [JsonIgnore] + public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + + /// + [JsonIgnore] + public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL; + + public static bool operator ==(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) => + Equals(left, right); + + public static bool operator !=(DalamudAssetFontAndFamilyId? left, DalamudAssetFontAndFamilyId? right) => + !Equals(left, right); + + /// + public override bool Equals(object? obj) => obj is DalamudAssetFontAndFamilyId other && this.Equals(other); + + /// + public override int GetHashCode() => (int)this.Asset; + + /// + public override string ToString() => $"{nameof(DalamudAssetFontAndFamilyId)}:{this.Asset}"; + + /// + public int FindBestMatch(int weight, int stretch, int style) => 0; + + /// + public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) => + tk.AddDalamudAssetFont(this.Asset, config); + + private bool Equals(DalamudAssetFontAndFamilyId other) => this.Asset == other.Asset; +} diff --git a/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs new file mode 100644 index 000000000..7c6a69622 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/DalamudDefaultFontAndFamilyId.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; + +using Dalamud.Interface.ManagedFontAtlas; + +using ImGuiNET; + +using Newtonsoft.Json; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents the default Dalamud font. +/// +public sealed class DalamudDefaultFontAndFamilyId : IFontId, IFontFamilyId +{ + /// + /// The shared instance of . + /// + public static readonly DalamudDefaultFontAndFamilyId Instance = new(); + + private DalamudDefaultFontAndFamilyId() + { + } + + /// + [JsonIgnore] + public string EnglishName => "(Default)"; + + /// + [JsonIgnore] + public IReadOnlyDictionary? LocaleNames => null; + + /// + [JsonIgnore] + public IFontFamilyId Family => this; + + /// + [JsonIgnore] + public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + + /// + [JsonIgnore] + public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + + /// + [JsonIgnore] + public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL; + + /// + [JsonIgnore] + public IReadOnlyList Fonts => new List { this }.AsReadOnly(); + + public static bool operator ==(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) => + left is null == right is null; + + public static bool operator !=(DalamudDefaultFontAndFamilyId? left, DalamudDefaultFontAndFamilyId? right) => + left is null != right is null; + + /// + public override bool Equals(object? obj) => obj is DalamudDefaultFontAndFamilyId; + + /// + public override int GetHashCode() => 12345678; + + /// + public override string ToString() => nameof(DalamudDefaultFontAndFamilyId); + + /// + public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) + => tk.AddDalamudDefaultFont(config.SizePx, config.GlyphRanges); + // TODO: mergeFont + + /// + public int FindBestMatch(int weight, int stretch, int style) => 0; +} diff --git a/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs b/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs new file mode 100644 index 000000000..dd4ba0d66 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/GameFontAndFamilyId.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; + +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ManagedFontAtlas; + +using ImGuiNET; + +using Newtonsoft.Json; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a font from the game. +/// +public sealed class GameFontAndFamilyId : IFontId, IFontFamilyId +{ + /// + /// Initializes a new instance of the class. + /// + /// The game font family. + public GameFontAndFamilyId(GameFontFamily family) => this.GameFontFamily = family; + + /// + /// Gets the game font family. + /// + [JsonProperty] + public GameFontFamily GameFontFamily { get; init; } + + /// + [JsonIgnore] + public string EnglishName => $"Game: {Enum.GetName(this.GameFontFamily) ?? throw new NotSupportedException()}"; + + /// + [JsonIgnore] + public IReadOnlyDictionary? LocaleNames => null; + + /// + [JsonIgnore] + public IFontFamilyId Family => this; + + /// + [JsonIgnore] + public int Weight => (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + + /// + [JsonIgnore] + public int Stretch => (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + + /// + [JsonIgnore] + public int Style => (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL; + + /// + [JsonIgnore] + public IReadOnlyList Fonts => new List { this }.AsReadOnly(); + + public static bool operator ==(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => Equals(left, right); + + public static bool operator !=(GameFontAndFamilyId? left, GameFontAndFamilyId? right) => !Equals(left, right); + + /// + public override bool Equals(object? obj) => + ReferenceEquals(this, obj) || (obj is GameFontAndFamilyId other && this.Equals(other)); + + /// + public override int GetHashCode() => (int)this.GameFontFamily; + + /// + public int FindBestMatch(int weight, int stretch, int style) => 0; + + /// + public override string ToString() => $"{nameof(GameFontAndFamilyId)}:{this.GameFontFamily}"; + + /// + public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) => + tk.AddGameGlyphs(new(this.GameFontFamily, config.SizePx), config.GlyphRanges, config.MergeFont); + + private bool Equals(GameFontAndFamilyId other) => this.GameFontFamily == other.GameFontFamily; +} diff --git a/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs new file mode 100644 index 000000000..991716f74 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/IFontFamilyId.cs @@ -0,0 +1,102 @@ +using System.Collections.Generic; + +using Dalamud.Interface.GameFonts; +using Dalamud.Utility; + +using Newtonsoft.Json; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a font family identifier.
+/// Not intended for plugins to implement. +///
+public interface IFontFamilyId : IObjectWithLocalizableName +{ + /// + /// Gets the list of fonts under this family. + /// + [JsonIgnore] + IReadOnlyList Fonts { get; } + + /// + /// Finds the index of the font inside that best matches the given parameters. + /// + /// The weight of the font. + /// The stretch of the font. + /// The style of the font. + /// The index of the font. Guaranteed to be a valid index. + int FindBestMatch(int weight, int stretch, int style); + + /// + /// Gets the list of Dalamud-provided fonts. + /// + /// The list of fonts. + public static List ListDalamudFonts() => + new() + { + new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium), + new DalamudAssetFontAndFamilyId(DalamudAsset.InconsolataRegular), + new DalamudAssetFontAndFamilyId(DalamudAsset.FontAwesomeFreeSolid), + }; + + /// + /// Gets the list of Game-provided fonts. + /// + /// The list of fonts. + public static List ListGameFonts() => new() + { + new GameFontAndFamilyId(GameFontFamily.Axis), + new GameFontAndFamilyId(GameFontFamily.Jupiter), + new GameFontAndFamilyId(GameFontFamily.JupiterNumeric), + new GameFontAndFamilyId(GameFontFamily.Meidinger), + new GameFontAndFamilyId(GameFontFamily.MiedingerMid), + new GameFontAndFamilyId(GameFontFamily.TrumpGothic), + }; + + /// + /// Gets the list of System-provided fonts. + /// + /// If true, try to refresh the list. + /// The list of fonts. + public static unsafe List ListSystemFonts(bool refresh) + { + using var dwf = default(ComPtr); + fixed (Guid* piid = &IID.IID_IDWriteFactory) + { + DirectX.DWriteCreateFactory( + DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED, + piid, + (IUnknown**)dwf.GetAddressOf()).ThrowOnError(); + } + + using var sfc = default(ComPtr); + dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), refresh).ThrowOnError(); + + var count = (int)sfc.Get()->GetFontFamilyCount(); + var result = new List(count); + for (var i = 0; i < count; i++) + { + using var ff = default(ComPtr); + if (sfc.Get()->GetFontFamily((uint)i, ff.GetAddressOf()).FAILED) + { + // Ignore errors, if any + continue; + } + + try + { + result.Add(SystemFontFamilyId.FromDWriteFamily(ff)); + } + catch + { + // ignore + } + } + + return result; + } +} diff --git a/Dalamud/Interface/FontIdentifier/IFontId.cs b/Dalamud/Interface/FontIdentifier/IFontId.cs new file mode 100644 index 000000000..4c611edf8 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/IFontId.cs @@ -0,0 +1,40 @@ +using Dalamud.Interface.ManagedFontAtlas; + +using ImGuiNET; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a font identifier.
+/// Not intended for plugins to implement. +///
+public interface IFontId : IObjectWithLocalizableName +{ + /// + /// Gets the associated font family. + /// + IFontFamilyId Family { get; } + + /// + /// Gets the font weight, ranging from 1 to 999. + /// + int Weight { get; } + + /// + /// Gets the font stretch, ranging from 1 to 9. + /// + int Stretch { get; } + + /// + /// Gets the font style. Treat as an opaque value. + /// + int Style { get; } + + /// + /// Adds this font to the given font build toolkit. + /// + /// The font build toolkit. + /// The font configuration. Some parameters may be ignored. + /// The added font. + ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config); +} diff --git a/Dalamud/Interface/FontIdentifier/IFontSpec.cs b/Dalamud/Interface/FontIdentifier/IFontSpec.cs new file mode 100644 index 000000000..e4d931605 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/IFontSpec.cs @@ -0,0 +1,50 @@ +using Dalamud.Interface.ManagedFontAtlas; + +using ImGuiNET; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a user's choice of font(s).
+/// Not intended for plugins to implement. +///
+public interface IFontSpec +{ + /// + /// Gets the font size in pixels. + /// + float SizePx { get; } + + /// + /// Gets the font size in points. + /// + float SizePt { get; } + + /// + /// Gets the line height in pixels. + /// + float LineHeightPx { get; } + + /// + /// Creates a font handle corresponding to this font specification. + /// + /// The atlas to bind this font handle to. + /// Optional callback to be called after creating the font handle. + /// The new font handle. + IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null); + + /// + /// Adds this font to the given font build toolkit. + /// + /// The font build toolkit. + /// The font to merge to. + /// The added font. + ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default); + + /// + /// Represents this font specification, preferrably in the requested locale. + /// + /// The locale code. Must be in lowercase(invariant). + /// The value. + string ToLocalizedString(string localeCode); +} diff --git a/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs new file mode 100644 index 000000000..2b970a5fd --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/IObjectWithLocalizableName.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; + +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents an object with localizable names. +/// +public interface IObjectWithLocalizableName +{ + /// + /// Gets the name, preferrably in English. + /// + string EnglishName { get; } + + /// + /// Gets the names per locales. + /// + IReadOnlyDictionary? LocaleNames { get; } + + /// + /// Gets the name in the requested locale if available; otherwise, . + /// + /// The locale code. Must be in lowercase(invariant). + /// The value. + string GetLocalizedName(string localeCode) + { + if (this.LocaleNames is null) + return this.EnglishName; + if (this.LocaleNames.TryGetValue(localeCode, out var v)) + return v; + foreach (var (a, b) in this.LocaleNames) + { + if (a.StartsWith(localeCode)) + return b; + } + + return this.EnglishName; + } + + /// + /// Resolves all names per locales. + /// + /// The names. + /// A new dictionary mapping from locale code to localized names. + internal static unsafe IReadOnlyDictionary GetLocaleNames(IDWriteLocalizedStrings* fn) + { + var count = fn->GetCount(); + var maxStrLen = 0u; + for (var i = 0u; i < count; i++) + { + var length = 0u; + fn->GetStringLength(i, &length).ThrowOnError(); + maxStrLen = Math.Max(maxStrLen, length); + fn->GetLocaleNameLength(i, &length).ThrowOnError(); + maxStrLen = Math.Max(maxStrLen, length); + } + + maxStrLen++; + var buf = stackalloc char[(int)maxStrLen]; + var result = new Dictionary((int)count); + for (var i = 0u; i < count; i++) + { + fn->GetLocaleName(i, (ushort*)buf, maxStrLen).ThrowOnError(); + var key = new string(buf); + fn->GetString(i, (ushort*)buf, maxStrLen).ThrowOnError(); + var value = new string(buf); + result[key.ToLowerInvariant()] = value; + } + + return result; + } +} diff --git a/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs new file mode 100644 index 000000000..0604b22ea --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs @@ -0,0 +1,155 @@ +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Text; + +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; + +using ImGuiNET; + +using Newtonsoft.Json; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a user's choice of a single font. +/// +[SuppressMessage( + "StyleCop.CSharp.OrderingRules", + "SA1206:Declaration keywords should follow order", + Justification = "public required")] +public record SingleFontSpec : IFontSpec +{ + /// + /// Gets the font id. + /// + [JsonProperty] + public required IFontId FontId { get; init; } + + /// + [JsonProperty] + public float SizePx { get; init; } = 16; + + /// + [JsonIgnore] + public float SizePt + { + get => (this.SizePx * 3) / 4; + init => this.SizePx = (value * 4) / 3; + } + + /// + [JsonIgnore] + public float LineHeightPx => MathF.Round(this.SizePx * this.LineHeight); + + /// + /// Gets the line height ratio to the font size. + /// + [JsonProperty] + public float LineHeight { get; init; } = 1f; + + /// + /// Gets the glyph offset in pixels. + /// + [JsonProperty] + public Vector2 GlyphOffset { get; init; } + + /// + /// Gets the letter spacing in pixels. + /// + [JsonProperty] + public float LetterSpacing { get; init; } + + /// + /// Gets the glyph ranges. + /// + [JsonProperty] + public ushort[]? GlyphRanges { get; init; } + + /// + public string ToLocalizedString(string localeCode) + { + var sb = new StringBuilder(); + sb.Append(this.FontId.Family.GetLocalizedName(localeCode)); + sb.Append($"({this.FontId.GetLocalizedName(localeCode)}, {this.SizePt}pt"); + if (Math.Abs(this.LineHeight - 1f) > 0.000001f) + sb.Append($", LH={this.LineHeight:0.##}"); + if (this.GlyphOffset != default) + sb.Append($", O={this.GlyphOffset.X:0.##},{this.GlyphOffset.Y:0.##}"); + if (this.LetterSpacing != 0f) + sb.Append($", LS={this.LetterSpacing:0.##}"); + sb.Append(')'); + return sb.ToString(); + } + + /// + public override string ToString() => this.ToLocalizedString("en"); + + /// + public IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null) => + atlas.NewDelegateFontHandle(tk => + { + tk.OnPreBuild(e => e.Font = this.AddToBuildToolkit(e)); + callback?.Invoke(tk); + }); + + /// + public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, ImFontPtr mergeFont = default) + { + var font = this.FontId.AddToBuildToolkit( + tk, + new() + { + SizePx = this.SizePx, + GlyphRanges = this.GlyphRanges, + MergeFont = mergeFont, + }); + + tk.RegisterPostBuild( + () => + { + var roundUnit = tk.IsGlobalScaleIgnored(font) ? 1 : 1 / tk.Scale; + var newAscent = MathF.Round((font.Ascent * this.LineHeight) / roundUnit) * roundUnit; + var newFontSize = MathF.Round((font.FontSize * this.LineHeight) / roundUnit) * roundUnit; + var shiftDown = MathF.Round((newFontSize - font.FontSize) / 2f / roundUnit) * roundUnit; + + font.Ascent = newAscent; + font.FontSize = newFontSize; + font.Descent = newFontSize - font.Ascent; + + var lookup = new BitArray(ushort.MaxValue + 1, this.GlyphRanges is null); + if (this.GlyphRanges is not null) + { + for (var i = 0; i < this.GlyphRanges.Length && this.GlyphRanges[i] != 0; i += 2) + { + var to = (int)this.GlyphRanges[i + 1]; + for (var j = this.GlyphRanges[i]; j <= to; j++) + lookup[j] = true; + } + } + + // `/ roundUnit` = `* scale` + var dax = MathF.Round(this.LetterSpacing / roundUnit / roundUnit) * roundUnit; + var dxy0 = this.GlyphOffset / roundUnit; + + dxy0 /= roundUnit; + dxy0.X = MathF.Round(dxy0.X); + dxy0.Y = MathF.Round(dxy0.Y); + dxy0 *= roundUnit; + + dxy0.Y += shiftDown; + var dxy = new Vector4(dxy0, dxy0.X, dxy0.Y); + foreach (ref var glyphReal in font.GlyphsWrapped().DataSpan) + { + if (!lookup[glyphReal.Codepoint]) + continue; + + glyphReal.XY += dxy; + glyphReal.AdvanceX += dax; + } + }); + + return font; + } +} diff --git a/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs new file mode 100644 index 000000000..420ee77a4 --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/SystemFontFamilyId.cs @@ -0,0 +1,181 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Dalamud.Utility; + +using Newtonsoft.Json; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a font from system. +/// +public sealed class SystemFontFamilyId : IFontFamilyId +{ + [JsonIgnore] + private IReadOnlyList? fontsLazy; + + /// + /// Initializes a new instance of the class. + /// + /// The font name in English. + /// The localized font name for display purposes. + [JsonConstructor] + internal SystemFontFamilyId(string englishName, IReadOnlyDictionary localeNames) + { + this.EnglishName = englishName; + this.LocaleNames = localeNames; + } + + /// + /// Initializes a new instance of the class. + /// + /// The localized font name for display purposes. + internal SystemFontFamilyId(IReadOnlyDictionary localeNames) + { + if (localeNames.TryGetValue("en-us", out var name)) + this.EnglishName = name; + else if (localeNames.TryGetValue("en", out name)) + this.EnglishName = name; + else + this.EnglishName = localeNames.Values.First(); + this.LocaleNames = localeNames; + } + + /// + [JsonProperty] + public string EnglishName { get; init; } + + /// + [JsonProperty] + public IReadOnlyDictionary? LocaleNames { get; } + + /// + [JsonIgnore] + public IReadOnlyList Fonts => this.fontsLazy ??= this.GetFonts(); + + public static bool operator ==(SystemFontFamilyId? left, SystemFontFamilyId? right) => Equals(left, right); + + public static bool operator !=(SystemFontFamilyId? left, SystemFontFamilyId? right) => !Equals(left, right); + + /// + public int FindBestMatch(int weight, int stretch, int style) + { + using var matchingFont = default(ComPtr); + + var candidates = this.Fonts.ToList(); + var minGap = int.MaxValue; + foreach (var c in candidates) + minGap = Math.Min(minGap, Math.Abs(c.Weight - weight)); + candidates.RemoveAll(c => Math.Abs(c.Weight - weight) != minGap); + + minGap = int.MaxValue; + foreach (var c in candidates) + minGap = Math.Min(minGap, Math.Abs(c.Stretch - stretch)); + candidates.RemoveAll(c => Math.Abs(c.Stretch - stretch) != minGap); + + if (candidates.Any(x => x.Style == style)) + candidates.RemoveAll(x => x.Style != style); + else if (candidates.Any(x => x.Style == (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL)) + candidates.RemoveAll(x => x.Style != (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL); + + if (!candidates.Any()) + return 0; + + for (var i = 0; i < this.Fonts.Count; i++) + { + if (Equals(this.Fonts[i], candidates[0])) + return i; + } + + return 0; + } + + /// + public override string ToString() => $"{nameof(SystemFontFamilyId)}:{this.EnglishName}"; + + /// + public override bool Equals(object? obj) => + ReferenceEquals(this, obj) || (obj is SystemFontFamilyId other && this.Equals(other)); + + /// + public override int GetHashCode() => this.EnglishName.GetHashCode(); + + /// + /// Create a new instance of from an . + /// + /// The family. + /// The new instance. + internal static unsafe SystemFontFamilyId FromDWriteFamily(ComPtr family) + { + using var fn = default(ComPtr); + family.Get()->GetFamilyNames(fn.GetAddressOf()).ThrowOnError(); + return new(IObjectWithLocalizableName.GetLocaleNames(fn)); + } + + private unsafe IReadOnlyList GetFonts() + { + using var dwf = default(ComPtr); + fixed (Guid* piid = &IID.IID_IDWriteFactory) + { + DirectX.DWriteCreateFactory( + DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED, + piid, + (IUnknown**)dwf.GetAddressOf()).ThrowOnError(); + } + + using var sfc = default(ComPtr); + dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError(); + + var familyIndex = 0u; + BOOL exists = false; + fixed (void* pName = this.EnglishName) + sfc.Get()->FindFamilyName((ushort*)pName, &familyIndex, &exists).ThrowOnError(); + if (!exists) + throw new FileNotFoundException($"Font \"{this.EnglishName}\" not found."); + + using var family = default(ComPtr); + sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError(); + + var fontCount = (int)family.Get()->GetFontCount(); + var fonts = new List(fontCount); + for (var i = 0; i < fontCount; i++) + { + using var font = default(ComPtr); + if (family.Get()->GetFont((uint)i, font.GetAddressOf()).FAILED) + { + // Ignore errors, if any + continue; + } + + if (font.Get()->GetSimulations() != DWRITE_FONT_SIMULATIONS.DWRITE_FONT_SIMULATIONS_NONE) + { + // No simulation support + continue; + } + + fonts.Add(new SystemFontId(this, font)); + } + + fonts.Sort( + (a, b) => + { + var comp = a.Weight.CompareTo(b.Weight); + if (comp != 0) + return comp; + + comp = a.Stretch.CompareTo(b.Stretch); + if (comp != 0) + return comp; + + return a.Style.CompareTo(b.Style); + }); + return fonts; + } + + private bool Equals(SystemFontFamilyId other) => this.EnglishName == other.EnglishName; +} diff --git a/Dalamud/Interface/FontIdentifier/SystemFontId.cs b/Dalamud/Interface/FontIdentifier/SystemFontId.cs new file mode 100644 index 000000000..0a350fc3a --- /dev/null +++ b/Dalamud/Interface/FontIdentifier/SystemFontId.cs @@ -0,0 +1,163 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; + +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Utility; + +using ImGuiNET; + +using Newtonsoft.Json; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.FontIdentifier; + +/// +/// Represents a font installed in the system. +/// +public sealed class SystemFontId : IFontId +{ + /// + /// Initializes a new instance of the class. + /// + /// The parent font family. + /// The font. + internal unsafe SystemFontId(SystemFontFamilyId family, ComPtr font) + { + this.Family = family; + this.Weight = (int)font.Get()->GetWeight(); + this.Stretch = (int)font.Get()->GetStretch(); + this.Style = (int)font.Get()->GetStyle(); + + using var fn = default(ComPtr); + font.Get()->GetFaceNames(fn.GetAddressOf()).ThrowOnError(); + this.LocaleNames = IObjectWithLocalizableName.GetLocaleNames(fn); + if (this.LocaleNames.TryGetValue("en-us", out var name)) + this.EnglishName = name; + else if (this.LocaleNames.TryGetValue("en", out name)) + this.EnglishName = name; + else + this.EnglishName = this.LocaleNames.Values.First(); + } + + [JsonConstructor] + private SystemFontId(string englishName, IReadOnlyDictionary localeNames, IFontFamilyId family) + { + this.EnglishName = englishName; + this.LocaleNames = localeNames; + this.Family = family; + } + + /// + [JsonProperty] + public string EnglishName { get; init; } + + /// + [JsonProperty] + public IReadOnlyDictionary? LocaleNames { get; } + + /// + [JsonProperty] + public IFontFamilyId Family { get; init; } + + /// + [JsonProperty] + public int Weight { get; init; } = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + + /// + [JsonProperty] + public int Stretch { get; init; } = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + + /// + [JsonProperty] + public int Style { get; init; } = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL; + + public static bool operator ==(SystemFontId? left, SystemFontId? right) => Equals(left, right); + + public static bool operator !=(SystemFontId? left, SystemFontId? right) => !Equals(left, right); + + /// + public override bool Equals(object? obj) => + ReferenceEquals(this, obj) || (obj is SystemFontId other && this.Equals(other)); + + /// + public override int GetHashCode() => HashCode.Combine(this.Family, this.Weight, this.Stretch, this.Style); + + /// + public override string ToString() => + $"{nameof(SystemFontId)}:{this.Weight}:{this.Stretch}:{this.Style}:{this.Family}"; + + /// + public ImFontPtr AddToBuildToolkit(IFontAtlasBuildToolkitPreBuild tk, in SafeFontConfig config) + { + var (path, index) = this.GetFileAndIndex(); + return tk.AddFontFromFile(path, config with { FontNo = index }); + } + + /// + /// Gets the file containing this font, and the font index within. + /// + /// The path and index. + public unsafe (string Path, int Index) GetFileAndIndex() + { + using var dwf = default(ComPtr); + fixed (Guid* piid = &IID.IID_IDWriteFactory) + { + DirectX.DWriteCreateFactory( + DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED, + piid, + (IUnknown**)dwf.GetAddressOf()).ThrowOnError(); + } + + using var sfc = default(ComPtr); + dwf.Get()->GetSystemFontCollection(sfc.GetAddressOf(), false).ThrowOnError(); + + var familyIndex = 0u; + BOOL exists = false; + fixed (void* name = this.Family.EnglishName) + sfc.Get()->FindFamilyName((ushort*)name, &familyIndex, &exists).ThrowOnError(); + if (!exists) + throw new FileNotFoundException($"Font \"{this.Family.EnglishName}\" not found."); + + using var family = default(ComPtr); + sfc.Get()->GetFontFamily(familyIndex, family.GetAddressOf()).ThrowOnError(); + + using var font = default(ComPtr); + family.Get()->GetFirstMatchingFont( + (DWRITE_FONT_WEIGHT)this.Weight, + (DWRITE_FONT_STRETCH)this.Stretch, + (DWRITE_FONT_STYLE)this.Style, + font.GetAddressOf()).ThrowOnError(); + + using var fface = default(ComPtr); + font.Get()->CreateFontFace(fface.GetAddressOf()).ThrowOnError(); + var fileCount = 0; + fface.Get()->GetFiles((uint*)&fileCount, null).ThrowOnError(); + if (fileCount != 1) + throw new NotSupportedException(); + + using var ffile = default(ComPtr); + fface.Get()->GetFiles((uint*)&fileCount, ffile.GetAddressOf()).ThrowOnError(); + void* refKey; + var refKeySize = 0u; + ffile.Get()->GetReferenceKey(&refKey, &refKeySize).ThrowOnError(); + + using var floader = default(ComPtr); + ffile.Get()->GetLoader(floader.GetAddressOf()).ThrowOnError(); + + using var flocal = default(ComPtr); + floader.As(&flocal).ThrowOnError(); + + var pathSize = 0u; + flocal.Get()->GetFilePathLengthFromKey(refKey, refKeySize, &pathSize).ThrowOnError(); + + var path = stackalloc char[(int)pathSize + 1]; + flocal.Get()->GetFilePathFromKey(refKey, refKeySize, (ushort*)path, pathSize + 1).ThrowOnError(); + return (new(path, 0, (int)pathSize), (int)fface.Get()->GetIndex()); + } + + private bool Equals(SystemFontId other) => this.Family.Equals(other.Family) && this.Weight == other.Weight && + this.Stretch == other.Stretch && this.Style == other.Style; +} diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs new file mode 100644 index 000000000..410bf7d18 --- /dev/null +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -0,0 +1,1117 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Colors; +using Dalamud.Interface.FontIdentifier; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.ImGuiFontChooserDialog; + +/// +/// A dialog for choosing a font and its size. +/// +[SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed blocks")] +public sealed class SingleFontChooserDialog : IDisposable +{ + private const float MinFontSizePt = 1; + + private const float MaxFontSizePt = 127; + + private static readonly List EmptyIFontList = new(); + + private static readonly (string Name, float Value)[] FontSizeList = + { + ("9.6", 9.6f), + ("10", 10f), + ("12", 12f), + ("14", 14f), + ("16", 16f), + ("18", 18f), + ("18.4", 18.4f), + ("20", 20), + ("23", 23), + ("34", 34), + ("36", 36), + ("40", 40), + ("45", 45), + ("46", 46), + ("68", 68), + ("90", 90), + }; + + private static int counterStatic; + + private readonly int counter; + private readonly byte[] fontPreviewText = new byte[2048]; + private readonly TaskCompletionSource tcs = new(); + private readonly IFontAtlas atlas; + + private string popupImGuiName; + private string title; + + private bool firstDraw = true; + private bool firstDrawAfterRefresh; + private int setFocusOn = -1; + + private bool useAdvancedOptions; + private AdvancedOptionsUiState advUiState; + + private Task>? fontFamilies; + private int selectedFamilyIndex = -1; + private int selectedFontIndex = -1; + private int selectedFontWeight = (int)DWRITE_FONT_WEIGHT.DWRITE_FONT_WEIGHT_NORMAL; + private int selectedFontStretch = (int)DWRITE_FONT_STRETCH.DWRITE_FONT_STRETCH_NORMAL; + private int selectedFontStyle = (int)DWRITE_FONT_STYLE.DWRITE_FONT_STYLE_NORMAL; + + private string familySearch = string.Empty; + private string fontSearch = string.Empty; + private string fontSizeSearch = "12"; + private IFontHandle? fontHandle; + private SingleFontSpec selectedFont; + + /// + /// Initializes a new instance of the class. + /// + /// A new instance of created using + /// as its auto-rebuild mode. + public SingleFontChooserDialog(IFontAtlas newAsyncAtlas) + { + this.counter = Interlocked.Increment(ref counterStatic); + this.title = "Choose a font..."; + this.popupImGuiName = $"{this.title}##{nameof(SingleFontChooserDialog)}[{this.counter}]"; + this.atlas = newAsyncAtlas; + this.selectedFont = new() { FontId = DalamudDefaultFontAndFamilyId.Instance }; + Encoding.UTF8.GetBytes("Font preview.\n0123456789!", this.fontPreviewText); + } + + /// + /// Gets or sets the title of this font chooser dialog popup. + /// + public string Title + { + get => this.title; + set + { + this.title = value; + this.popupImGuiName = $"{this.title}##{nameof(SingleFontChooserDialog)}[{this.counter}]"; + } + } + + /// + /// Gets or sets the preview text. A text too long may be truncated on assignment. + /// + public string PreviewText + { + get + { + var n = this.fontPreviewText.AsSpan().IndexOf((byte)0); + return n < 0 + ? Encoding.UTF8.GetString(this.fontPreviewText) + : Encoding.UTF8.GetString(this.fontPreviewText, 0, n); + } + set => Encoding.UTF8.GetBytes(value, this.fontPreviewText); + } + + /// + /// Gets the task that resolves upon choosing a font or cancellation. + /// + public Task ResultTask => this.tcs.Task; + + /// + /// Gets or sets the selected family and font. + /// + public SingleFontSpec SelectedFont + { + get => this.selectedFont; + set + { + this.selectedFont = value; + + var familyName = value.FontId.Family.ToString() ?? string.Empty; + var fontName = value.FontId.ToString() ?? string.Empty; + this.familySearch = this.ExtractName(value.FontId.Family); + this.fontSearch = this.ExtractName(value.FontId); + if (this.fontFamilies?.IsCompletedSuccessfully is true) + this.UpdateSelectedFamilyAndFontIndices(this.fontFamilies.Result, familyName, fontName); + this.fontSizeSearch = $"{value.SizePt:0.##}"; + this.advUiState = new(value); + this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001; + this.useAdvancedOptions |= value.GlyphOffset != default; + this.useAdvancedOptions |= value.LetterSpacing != 0f; + } + } + + /// + /// Gets or sets the font family exclusion filter predicate. + /// + public Predicate? FontFamilyExcludeFilter { get; set; } + + /// + /// Gets or sets a value indicating whether to ignore the global scale on preview text input. + /// + public bool IgnorePreviewGlobalScale { get; set; } + + /// + /// Creates a new instance of that will automatically draw and dispose itself as + /// needed. + /// + /// An instance of . + /// The new instance of . + public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder) + { + var fcd = new SingleFontChooserDialog(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async)); + uiBuilder.Draw += fcd.Draw; + fcd.tcs.Task.ContinueWith( + r => + { + _ = r.Exception; + uiBuilder.Draw -= fcd.Draw; + fcd.Dispose(); + }); + + return fcd; + } + + /// + public void Dispose() + { + this.fontHandle?.Dispose(); + this.atlas.Dispose(); + } + + /// + /// Cancels this dialog. + /// + public void Cancel() + { + this.tcs.SetCanceled(); + ImGui.GetIO().WantCaptureKeyboard = false; + ImGui.GetIO().WantTextInput = false; + } + + /// + /// Draws this dialog. + /// + public void Draw() + { + if (this.firstDraw) + ImGui.OpenPopup(this.popupImGuiName); + + ImGui.GetIO().WantCaptureKeyboard = true; + ImGui.GetIO().WantTextInput = true; + if (ImGui.IsKeyPressed(ImGuiKey.Escape)) + { + this.Cancel(); + return; + } + + var open = true; + ImGui.SetNextWindowSize(new(640, 480), ImGuiCond.Appearing); + if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open) || !open) + { + this.Cancel(); + return; + } + + var framePad = ImGui.GetStyle().FramePadding; + var windowPad = ImGui.GetStyle().WindowPadding; + var baseOffset = ImGui.GetCursorPos() - windowPad; + + var actionSize = Vector2.Zero; + actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("OK")); + actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Cancel")); + actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Refresh")); + actionSize = Vector2.Max(actionSize, ImGui.CalcTextSize("Reset")); + actionSize += framePad * 2; + + var bodySize = ImGui.GetContentRegionAvail(); + ImGui.SetCursorPos(baseOffset + windowPad); + if (ImGui.BeginChild( + "##choicesBlock", + bodySize with { X = bodySize.X - windowPad.X - actionSize.X }, + false, + ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)) + { + this.DrawChoices(); + } + + ImGui.EndChild(); + + ImGui.SetCursorPos(baseOffset + windowPad + new Vector2(bodySize.X - actionSize.X, 0)); + + if (ImGui.BeginChild("##actionsBlock", bodySize with { X = actionSize.X })) + { + this.DrawActionButtons(actionSize); + } + + ImGui.EndChild(); + + ImGui.EndPopup(); + + this.firstDraw = false; + this.firstDrawAfterRefresh = false; + } + + private void DrawChoices() + { + var lineHeight = ImGui.GetTextLineHeight(); + var previewHeight = (ImGui.GetFrameHeightWithSpacing() - lineHeight) + + Math.Max(lineHeight, this.selectedFont.LineHeightPx * 2); + + var advancedOptionsHeight = ImGui.GetFrameHeightWithSpacing() * (this.useAdvancedOptions ? 4 : 1); + + var tableSize = ImGui.GetContentRegionAvail() - + new Vector2(0, ImGui.GetStyle().WindowPadding.Y + previewHeight + advancedOptionsHeight); + if (ImGui.BeginChild( + "##tableContainer", + tableSize, + false, + ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) + && ImGui.BeginTable("##table", 3, ImGuiTableFlags.None)) + { + ImGui.PushStyleColor(ImGuiCol.TableHeaderBg, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, Vector4.Zero); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, Vector4.Zero); + ImGui.TableSetupColumn( + "Font:##familyColumn", + ImGuiTableColumnFlags.WidthStretch, + 0.4f); + ImGui.TableSetupColumn( + "Style:##fontColumn", + ImGuiTableColumnFlags.WidthStretch, + 0.4f); + ImGui.TableSetupColumn( + "Size:##sizeColumn", + ImGuiTableColumnFlags.WidthStretch, + 0.2f); + ImGui.TableHeadersRow(); + ImGui.PopStyleColor(3); + + ImGui.TableNextRow(); + + var pad = (int)MathF.Round(8 * ImGuiHelpers.GlobalScale); + ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(pad)); + ImGui.TableNextColumn(); + var changed = this.DrawFamilyListColumn(); + + ImGui.TableNextColumn(); + changed |= this.DrawFontListColumn(changed); + + ImGui.TableNextColumn(); + changed |= this.DrawSizeListColumn(); + + if (changed) + { + this.fontHandle?.Dispose(); + this.fontHandle = null; + } + + ImGui.PopStyleVar(); + + ImGui.EndTable(); + } + + ImGui.EndChild(); + + ImGui.Checkbox("Show advanced options", ref this.useAdvancedOptions); + if (this.useAdvancedOptions) + { + if (this.DrawAdvancedOptions()) + { + this.fontHandle?.Dispose(); + this.fontHandle = null; + } + } + + if (this.IgnorePreviewGlobalScale) + { + this.fontHandle ??= this.selectedFont.CreateFontHandle( + this.atlas, + tk => + tk.OnPreBuild(e => e.IgnoreGlobalScale(e.Font)) + .OnPostBuild(e => e.Font.AdjustGlyphMetrics(1f / e.Scale))); + } + else + { + this.fontHandle ??= this.selectedFont.CreateFontHandle(this.atlas); + } + + if (this.fontHandle is null) + { + ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding); + ImGui.TextUnformatted("Select a font."); + } + else if (this.fontHandle.LoadException is { } loadException) + { + ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding); + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.TextUnformatted(loadException.Message); + ImGui.PopStyleColor(); + } + else if (!this.fontHandle.Available) + { + ImGui.SetCursorPos(ImGui.GetCursorPos() + ImGui.GetStyle().FramePadding); + ImGui.TextUnformatted("Loading font..."); + } + else + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + using (this.fontHandle?.Push()) + { + unsafe + { + fixed (byte* buf = this.fontPreviewText) + fixed (byte* label = "##fontPreviewText"u8) + { + ImGuiNative.igInputTextMultiline( + label, + buf, + (uint)this.fontPreviewText.Length, + ImGui.GetContentRegionAvail(), + ImGuiInputTextFlags.None, + null, + null); + } + } + } + } + } + + private unsafe bool DrawFamilyListColumn() + { + if (this.fontFamilies?.IsCompleted is not true) + { + ImGui.SetScrollY(0); + ImGui.TextUnformatted("Loading..."); + return false; + } + + if (!this.fontFamilies.IsCompletedSuccessfully) + { + ImGui.SetScrollY(0); + ImGui.TextUnformatted("Error: " + this.fontFamilies.Exception); + return false; + } + + var families = this.fontFamilies.Result; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + + if (this.setFocusOn == 0) + { + this.setFocusOn = -1; + ImGui.SetKeyboardFocusHere(); + } + + var changed = false; + if (ImGui.InputText( + "##familySearch", + ref this.familySearch, + 255, + ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory, + data => + { + if (families.Count == 0) + return 0; + + var baseIndex = this.selectedFamilyIndex; + if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen) + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFamilyIndex = (this.selectedFamilyIndex + 1) % families.Count; + changed = true; + break; + case ImGuiKey.UpArrow: + this.selectedFamilyIndex = + (this.selectedFamilyIndex + families.Count - 1) % families.Count; + changed = true; + break; + } + + if (changed) + { + ImGuiHelpers.SetTextFromCallback( + data, + this.ExtractName(families[this.selectedFamilyIndex])); + } + } + else + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFamilyIndex = families.FindIndex( + baseIndex + 1, + x => this.TestName(x, this.familySearch)); + if (this.selectedFamilyIndex < 0) + { + this.selectedFamilyIndex = families.FindIndex( + 0, + baseIndex + 1, + x => this.TestName(x, this.familySearch)); + } + + changed = true; + break; + case ImGuiKey.UpArrow: + if (baseIndex > 0) + { + this.selectedFamilyIndex = families.FindLastIndex( + baseIndex - 1, + x => this.TestName(x, this.familySearch)); + } + + if (this.selectedFamilyIndex < 0) + { + if (baseIndex < 0) + baseIndex = 0; + this.selectedFamilyIndex = families.FindLastIndex( + families.Count - 1, + families.Count - baseIndex, + x => this.TestName(x, this.familySearch)); + } + + changed = true; + break; + } + } + + return 0; + })) + { + if (!string.IsNullOrWhiteSpace(this.familySearch) && !changed) + { + this.selectedFamilyIndex = families.FindIndex(x => this.TestName(x, this.familySearch)); + changed = true; + } + } + + if (ImGui.BeginChild("##familyList", ImGui.GetContentRegionAvail())) + { + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + + if ((changed || this.firstDrawAfterRefresh) && this.selectedFamilyIndex != -1) + { + ImGui.SetScrollY( + (lineHeight * this.selectedFamilyIndex) - + ((ImGui.GetContentRegionAvail().Y - lineHeight) / 2)); + } + + clipper.Begin(families.Count, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + if (i < 0) + { + ImGui.TextUnformatted(" "); + continue; + } + + var selected = this.selectedFamilyIndex == i; + if (ImGui.Selectable( + this.ExtractName(families[i]), + ref selected, + ImGuiSelectableFlags.DontClosePopups)) + { + this.selectedFamilyIndex = families.IndexOf(families[i]); + this.familySearch = this.ExtractName(families[i]); + this.setFocusOn = 0; + changed = true; + } + } + } + + clipper.Destroy(); + } + + if (changed && this.selectedFamilyIndex >= 0) + { + var family = families[this.selectedFamilyIndex]; + using var matchingFont = default(ComPtr); + this.selectedFontIndex = family.FindBestMatch( + this.selectedFontWeight, + this.selectedFontStretch, + this.selectedFontStyle); + this.selectedFont = this.selectedFont with { FontId = family.Fonts[this.selectedFontIndex] }; + } + + ImGui.EndChild(); + return changed; + } + + private unsafe bool DrawFontListColumn(bool changed) + { + if (this.fontFamilies?.IsCompleted is not true) + { + ImGui.TextUnformatted("Loading..."); + return changed; + } + + if (!this.fontFamilies.IsCompletedSuccessfully) + { + ImGui.TextUnformatted("Error: " + this.fontFamilies.Exception); + return changed; + } + + var families = this.fontFamilies.Result; + var family = this.selectedFamilyIndex >= 0 + && this.selectedFamilyIndex < families.Count + ? families[this.selectedFamilyIndex] + : null; + var fonts = family?.Fonts ?? EmptyIFontList; + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + + if (this.setFocusOn == 1) + { + this.setFocusOn = -1; + ImGui.SetKeyboardFocusHere(); + } + + if (ImGui.InputText( + "##fontSearch", + ref this.fontSearch, + 255, + ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory, + data => + { + if (fonts.Count == 0) + return 0; + + var baseIndex = this.selectedFontIndex; + if (data->SelectionStart == 0 && data->SelectionEnd == data->BufTextLen) + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFontIndex = (this.selectedFontIndex + 1) % fonts.Count; + changed = true; + break; + case ImGuiKey.UpArrow: + this.selectedFontIndex = (this.selectedFontIndex + fonts.Count - 1) % fonts.Count; + changed = true; + break; + } + + if (changed) + { + ImGuiHelpers.SetTextFromCallback( + data, + this.ExtractName(fonts[this.selectedFontIndex])); + } + } + else + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFontIndex = fonts.FindIndex( + baseIndex + 1, + x => this.TestName(x, this.fontSearch)); + if (this.selectedFontIndex < 0) + { + this.selectedFontIndex = fonts.FindIndex( + 0, + baseIndex + 1, + x => this.TestName(x, this.fontSearch)); + } + + changed = true; + break; + case ImGuiKey.UpArrow: + if (baseIndex > 0) + { + this.selectedFontIndex = fonts.FindLastIndex( + baseIndex - 1, + x => this.TestName(x, this.fontSearch)); + } + + if (this.selectedFontIndex < 0) + { + if (baseIndex < 0) + baseIndex = 0; + this.selectedFontIndex = fonts.FindLastIndex( + fonts.Count - 1, + fonts.Count - baseIndex, + x => this.TestName(x, this.fontSearch)); + } + + changed = true; + break; + } + } + + return 0; + })) + { + if (!string.IsNullOrWhiteSpace(this.fontSearch) && !changed) + { + this.selectedFontIndex = fonts.FindIndex(x => this.TestName(x, this.fontSearch)); + changed = true; + } + } + + if (ImGui.BeginChild("##fontList")) + { + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + + if ((changed || this.firstDrawAfterRefresh) && this.selectedFontIndex != -1) + { + ImGui.SetScrollY( + (lineHeight * this.selectedFontIndex) - + ((ImGui.GetContentRegionAvail().Y - lineHeight) / 2)); + } + + clipper.Begin(fonts.Count, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + if (i < 0) + { + ImGui.TextUnformatted(" "); + continue; + } + + var selected = this.selectedFontIndex == i; + if (ImGui.Selectable( + this.ExtractName(fonts[i]), + ref selected, + ImGuiSelectableFlags.DontClosePopups)) + { + this.selectedFontIndex = fonts.IndexOf(fonts[i]); + this.fontSearch = this.ExtractName(fonts[i]); + this.setFocusOn = 1; + changed = true; + } + } + } + + clipper.Destroy(); + } + + ImGui.EndChild(); + + if (changed && family is not null && this.selectedFontIndex >= 0) + { + var font = family.Fonts[this.selectedFontIndex]; + this.selectedFontWeight = font.Weight; + this.selectedFontStretch = font.Stretch; + this.selectedFontStyle = font.Style; + this.selectedFont = this.selectedFont with { FontId = font }; + } + + return changed; + } + + private unsafe bool DrawSizeListColumn() + { + var changed = false; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + + if (this.setFocusOn == 2) + { + this.setFocusOn = -1; + ImGui.SetKeyboardFocusHere(); + } + + if (ImGui.InputText( + "##fontSizeSearch", + ref this.fontSizeSearch, + 255, + ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory | + ImGuiInputTextFlags.CharsDecimal, + data => + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + this.selectedFont = this.selectedFont with + { + SizePt = Math.Min(MaxFontSizePt, MathF.Floor(this.selectedFont.SizePt) + 1), + }; + changed = true; + break; + case ImGuiKey.UpArrow: + this.selectedFont = this.selectedFont with + { + SizePt = Math.Max(MinFontSizePt, MathF.Ceiling(this.selectedFont.SizePt) - 1), + }; + changed = true; + break; + } + + if (changed) + ImGuiHelpers.SetTextFromCallback(data, $"{this.selectedFont.SizePt:0.##}"); + + return 0; + })) + { + if (float.TryParse(this.fontSizeSearch, out var fontSizePt1)) + { + this.selectedFont = this.selectedFont with { SizePt = fontSizePt1 }; + changed = true; + } + } + + if (ImGui.BeginChild("##fontSizeList")) + { + var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + var lineHeight = ImGui.GetTextLineHeightWithSpacing(); + + if (changed && this.selectedFontIndex != -1) + { + ImGui.SetScrollY( + (lineHeight * this.selectedFontIndex) - + ((ImGui.GetContentRegionAvail().Y - lineHeight) / 2)); + } + + clipper.Begin(FontSizeList.Length, lineHeight); + while (clipper.Step()) + { + for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + { + if (i < 0) + { + ImGui.TextUnformatted(" "); + continue; + } + + var selected = Equals(FontSizeList[i].Value, this.selectedFont.SizePt); + if (ImGui.Selectable( + FontSizeList[i].Name, + ref selected, + ImGuiSelectableFlags.DontClosePopups)) + { + this.selectedFont = this.selectedFont with { SizePt = FontSizeList[i].Value }; + this.setFocusOn = 2; + changed = true; + } + } + } + + clipper.Destroy(); + } + + ImGui.EndChild(); + + if (this.selectedFont.SizePt < MinFontSizePt) + { + this.selectedFont = this.selectedFont with { SizePt = MinFontSizePt }; + changed = true; + } + + if (this.selectedFont.SizePt > MaxFontSizePt) + { + this.selectedFont = this.selectedFont with { SizePt = MaxFontSizePt }; + changed = true; + } + + if (changed) + this.fontSizeSearch = $"{this.selectedFont.SizePt:0.##}"; + + return changed; + } + + private bool DrawAdvancedOptions() + { + var changed = false; + + if (!ImGui.BeginTable("##advancedOptions", 4)) + return changed; + + var labelWidth = ImGui.CalcTextSize("Letter Spacing:").X; + labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Offset:").X); + labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Line Height:").X); + labelWidth += ImGui.GetStyle().FramePadding.X; + + var inputWidth = ImGui.CalcTextSize("000.000").X + (ImGui.GetStyle().FramePadding.X * 2); + ImGui.TableSetupColumn( + "##inputLabelColumn", + ImGuiTableColumnFlags.WidthFixed, + labelWidth); + ImGui.TableSetupColumn( + "##input1Column", + ImGuiTableColumnFlags.WidthFixed, + inputWidth); + ImGui.TableSetupColumn( + "##input2Column", + ImGuiTableColumnFlags.WidthFixed, + inputWidth); + ImGui.TableSetupColumn( + "##fillerColumn", + ImGuiTableColumnFlags.WidthStretch, + 1f); + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Offset:"); + + ImGui.TableNextColumn(); + if (FloatInputText( + "##glyphOffsetXInput", + ref this.advUiState.OffsetXText, + this.selectedFont.GlyphOffset.X) is { } newGlyphOffsetX) + { + changed = true; + this.selectedFont = this.selectedFont with + { + GlyphOffset = this.selectedFont.GlyphOffset with { X = newGlyphOffsetX }, + }; + } + + ImGui.TableNextColumn(); + if (FloatInputText( + "##glyphOffsetYInput", + ref this.advUiState.OffsetYText, + this.selectedFont.GlyphOffset.Y) is { } newGlyphOffsetY) + { + changed = true; + this.selectedFont = this.selectedFont with + { + GlyphOffset = this.selectedFont.GlyphOffset with { Y = newGlyphOffsetY }, + }; + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Letter Spacing:"); + + ImGui.TableNextColumn(); + if (FloatInputText( + "##letterSpacingXInput", + ref this.advUiState.LetterSpacingText, + this.selectedFont.LetterSpacing) is { } newLetterSpacing) + { + changed = true; + this.selectedFont = this.selectedFont with { LetterSpacing = newLetterSpacing }; + } + + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Line Height:"); + + ImGui.TableNextColumn(); + if (FloatInputText( + "##lineHeightInput", + ref this.advUiState.LineHeightText, + this.selectedFont.LineHeight, + 0.05f, + 0.1f, + 3f) is { } newLineHeight) + { + changed = true; + this.selectedFont = this.selectedFont with { LineHeight = newLineHeight }; + } + + ImGui.EndTable(); + return changed; + + static unsafe float? FloatInputText( + string label, ref string buf, float value, float step = 1f, float min = -127, float max = 127) + { + var stylePushed = value < min || value > max || !float.TryParse(buf, out _); + if (stylePushed) + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + + var changed2 = false; + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + var changed1 = ImGui.InputText( + label, + ref buf, + 255, + ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.CallbackHistory | + ImGuiInputTextFlags.CharsDecimal, + data => + { + switch (data->EventKey) + { + case ImGuiKey.DownArrow: + changed2 = true; + value = Math.Min(max, (MathF.Round(value / step) * step) + step); + ImGuiHelpers.SetTextFromCallback(data, $"{value:0.##}"); + break; + case ImGuiKey.UpArrow: + changed2 = true; + value = Math.Max(min, (MathF.Round(value / step) * step) - step); + ImGuiHelpers.SetTextFromCallback(data, $"{value:0.##}"); + break; + } + + return 0; + }); + + if (stylePushed) + ImGui.PopStyleColor(); + + if (!changed1 && !changed2) + return null; + + if (!float.TryParse(buf, out var parsed)) + return null; + + if (min > parsed || parsed > max) + return null; + + return parsed; + } + } + + private void DrawActionButtons(Vector2 buttonSize) + { + if (this.fontHandle?.Available is not true + || this.FontFamilyExcludeFilter?.Invoke(this.selectedFont.FontId.Family) is true) + { + ImGui.BeginDisabled(); + ImGui.Button("OK", buttonSize); + ImGui.EndDisabled(); + } + else if (ImGui.Button("OK", buttonSize)) + { + this.tcs.SetResult(this.selectedFont); + } + + if (ImGui.Button("Cancel", buttonSize)) + { + this.Cancel(); + } + + var doRefresh = false; + var isFirst = false; + if (this.fontFamilies?.IsCompleted is not true) + { + isFirst = doRefresh = this.fontFamilies is null; + ImGui.BeginDisabled(); + ImGui.Button("Refresh", buttonSize); + ImGui.EndDisabled(); + } + else if (ImGui.Button("Refresh", buttonSize)) + { + doRefresh = true; + } + + if (doRefresh) + { + this.fontFamilies = + this.fontFamilies?.ContinueWith(_ => RefreshBody()) + ?? Task.Run(RefreshBody); + this.fontFamilies.ContinueWith(_ => this.firstDrawAfterRefresh = true); + + List RefreshBody() + { + var familyName = this.selectedFont.FontId.Family.ToString() ?? string.Empty; + var fontName = this.selectedFont.FontId.ToString() ?? string.Empty; + + var newFonts = new List { DalamudDefaultFontAndFamilyId.Instance }; + newFonts.AddRange(IFontFamilyId.ListDalamudFonts()); + newFonts.AddRange(IFontFamilyId.ListGameFonts()); + var systemFonts = IFontFamilyId.ListSystemFonts(!isFirst); + systemFonts.Sort( + (a, b) => string.Compare( + this.ExtractName(a), + this.ExtractName(b), + StringComparison.CurrentCultureIgnoreCase)); + newFonts.AddRange(systemFonts); + if (this.FontFamilyExcludeFilter is not null) + newFonts.RemoveAll(this.FontFamilyExcludeFilter); + + this.UpdateSelectedFamilyAndFontIndices(newFonts, familyName, fontName); + return newFonts; + } + } + + if (this.useAdvancedOptions) + { + if (ImGui.Button("Reset", buttonSize)) + { + this.selectedFont = this.selectedFont with + { + LineHeight = 1f, + GlyphOffset = default, + LetterSpacing = default, + }; + + this.advUiState = new(this.selectedFont); + this.fontHandle?.Dispose(); + this.fontHandle = null; + } + } + } + + private void UpdateSelectedFamilyAndFontIndices( + IReadOnlyList fonts, + string familyName, + string fontName) + { + this.selectedFamilyIndex = fonts.FindIndex(x => x.ToString() == familyName); + if (this.selectedFamilyIndex == -1) + { + this.selectedFontIndex = -1; + } + else + { + this.selectedFontIndex = -1; + var family = fonts[this.selectedFamilyIndex]; + for (var i = 0; i < family.Fonts.Count; i++) + { + if (family.Fonts[i].ToString() == fontName) + { + this.selectedFontIndex = i; + break; + } + } + + if (this.selectedFontIndex == -1) + this.selectedFontIndex = 0; + this.selectedFont = this.selectedFont with + { + FontId = fonts[this.selectedFamilyIndex].Fonts[this.selectedFontIndex], + }; + } + } + + private string ExtractName(IObjectWithLocalizableName what) => + what.GetLocalizedName(Service.Get().EffectiveLanguage); + // Note: EffectiveLanguage can be incorrect but close enough for now + + private bool TestName(IObjectWithLocalizableName what, string search) => + this.ExtractName(what).Contains(search, StringComparison.CurrentCultureIgnoreCase); + + private struct AdvancedOptionsUiState + { + public string OffsetXText; + public string OffsetYText; + public string LetterSpacingText; + public string LineHeightText; + + public AdvancedOptionsUiState(SingleFontSpec spec) + { + this.OffsetXText = $"{spec.GlyphOffset.X:0.##}"; + this.OffsetYText = $"{spec.GlyphOffset.Y:0.##}"; + this.LetterSpacingText = $"{spec.LetterSpacing:0.##}"; + this.LineHeightText = $"{spec.LineHeight:0.##}"; + } + } +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6cf4a8b90..6d93b4bd7 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -705,13 +705,13 @@ internal class InterfaceManager : IDisposable, IServiceType using (this.dalamudAtlas.SuppressAutoRebuild()) { this.DefaultFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( - e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(DefaultFontSizePx))); + e => e.OnPreBuild(tk => tk.AddDalamudDefaultFont(-1))); this.IconFontHandle = (FontHandle)this.dalamudAtlas.NewDelegateFontHandle( e => e.OnPreBuild( tk => tk.AddFontAwesomeIconFont( new() { - SizePx = DefaultFontSizePx, + SizePx = Service.Get().DefaultFontSpec.SizePx, GlyphMinAdvanceX = DefaultFontSizePx, GlyphMaxAdvanceX = DefaultFontSizePx, }))); @@ -719,7 +719,10 @@ internal class InterfaceManager : IDisposable, IServiceType e => e.OnPreBuild( tk => tk.AddDalamudAssetFont( DalamudAsset.InconsolataRegular, - new() { SizePx = DefaultFontSizePx }))); + new() + { + SizePx = Service.Get().DefaultFontSpec.SizePx, + }))); this.dalamudAtlas.BuildStepChange += e => e.OnPostBuild( tk => { diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 53821d9df..1b9890a75 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -152,8 +152,11 @@ internal class ConsoleWindow : Window, IDisposable ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f); ImGui.TextColored(ImGuiColors.DalamudRed, regexErrorString); } - - ImGui.BeginChild("scrolling", new Vector2(0, ImGui.GetFrameHeightWithSpacing() - 55 * ImGuiHelpers.GlobalScale), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); + + var sendButtonSize = ImGui.CalcTextSize("Send") + + ((new Vector2(16, 0) + (ImGui.GetStyle().FramePadding * 2)) * ImGuiHelpers.GlobalScale); + var scrollingHeight = ImGui.GetContentRegionAvail().Y - sendButtonSize.Y; + ImGui.BeginChild("scrolling", new Vector2(0, scrollingHeight), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); if (this.clearLog) this.Clear(); @@ -173,9 +176,10 @@ internal class ConsoleWindow : Window, IDisposable var childDrawList = ImGui.GetWindowDrawList(); var childSize = ImGui.GetWindowSize(); - var cursorDiv = ImGuiHelpers.GlobalScale * 93; - var cursorLogLevel = ImGuiHelpers.GlobalScale * 100; - var cursorLogLine = ImGuiHelpers.GlobalScale * 135; + var cursorDiv = ImGui.CalcTextSize("00:00:00.000 ").X; + var cursorLogLevel = ImGui.CalcTextSize("00:00:00.000 | ").X; + var dividerOffset = ImGui.CalcTextSize("00:00:00.000 | AAA ").X + (ImGui.CalcTextSize(" ").X / 2); + var cursorLogLine = ImGui.CalcTextSize("00:00:00.000 | AAA | ").X; lock (this.renderLock) { @@ -242,8 +246,7 @@ internal class ConsoleWindow : Window, IDisposable } // Draw dividing line - var offset = ImGuiHelpers.GlobalScale * 127; - childDrawList.AddLine(new Vector2(childPos.X + offset, childPos.Y), new Vector2(childPos.X + offset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f); + childDrawList.AddLine(new Vector2(childPos.X + dividerOffset, childPos.Y), new Vector2(childPos.X + dividerOffset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f); ImGui.EndChild(); @@ -261,7 +264,7 @@ internal class ConsoleWindow : Window, IDisposable } } - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - (80.0f * ImGuiHelpers.GlobalScale) - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale)); + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - sendButtonSize.X - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale)); var getFocus = false; unsafe @@ -280,7 +283,7 @@ internal class ConsoleWindow : Window, IDisposable if (hadColor) ImGui.PopStyleColor(); - if (ImGui.Button("Send", ImGuiHelpers.ScaledVector2(80.0f, 23.0f))) + if (ImGui.Button("Send", sendButtonSize)) { this.ProcessCommand(); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index b486cc7d9..84682e7c2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -5,7 +5,10 @@ using System.Numerics; using System.Text; using System.Threading.Tasks; +using Dalamud.Game; +using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiFontChooserDialog; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; @@ -24,6 +27,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { private ImVectorWrapper testStringBuffer; private IFontAtlas? privateAtlas; + private SingleFontSpec fontSpec = new() { FontId = DalamudDefaultFontAndFamilyId.Instance }; + private IFontHandle? fontDialogHandle; private IReadOnlyDictionary Handle)[]>? fontHandles; private bool useGlobalScale; private bool useWordWrap; @@ -111,29 +116,32 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable if (ImGui.Button("Test Lock")) Task.Run(this.TestLock); - fixed (byte* labelPtr = "Test Input"u8) + ImGui.SameLine(); + if (ImGui.Button("Choose Editor Font")) { - if (ImGuiNative.igInputTextMultiline( - labelPtr, - this.testStringBuffer.Data, - (uint)this.testStringBuffer.Capacity, - new(ImGui.GetContentRegionAvail().X, 32 * ImGuiHelpers.GlobalScale), - 0, - null, - null) != 0) - { - var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); - if (len + 4 >= this.testStringBuffer.Capacity) - this.testStringBuffer.EnsureCapacityExponential(len + 4); - if (len < this.testStringBuffer.Capacity) - { - this.testStringBuffer.LengthUnsafe = len; - this.testStringBuffer.StorageSpan[len] = default; - } + var fcd = new SingleFontChooserDialog( + Service.Get().CreateFontAtlas( + $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont", + FontAtlasAutoRebuildMode.Async)); + fcd.SelectedFont = this.fontSpec; + fcd.IgnorePreviewGlobalScale = !this.useGlobalScale; + Service.Get().Draw += fcd.Draw; + fcd.ResultTask.ContinueWith( + r => Service.Get().RunOnFrameworkThread( + () => + { + Service.Get().Draw -= fcd.Draw; + fcd.Dispose(); - if (this.useMinimumBuild) - _ = this.privateAtlas?.BuildFontsAsync(); - } + _ = r.Exception; + if (!r.IsCompletedSuccessfully) + return; + + this.fontSpec = r.Result; + Log.Information("Selected font: {font}", this.fontSpec); + this.fontDialogHandle?.Dispose(); + this.fontDialogHandle = null; + })); } this.privateAtlas ??= @@ -141,6 +149,41 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable nameof(GamePrebakedFontsTestWidget), FontAtlasAutoRebuildMode.Async, this.useGlobalScale); + this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(this.privateAtlas); + + fixed (byte* labelPtr = "Test Input"u8) + { + if (!this.useGlobalScale) + ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); + using (this.fontDialogHandle.Push()) + { + if (ImGuiNative.igInputTextMultiline( + labelPtr, + this.testStringBuffer.Data, + (uint)this.testStringBuffer.Capacity, + new(ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 3), + 0, + null, + null) != 0) + { + var len = this.testStringBuffer.StorageSpan.IndexOf((byte)0); + if (len + 4 >= this.testStringBuffer.Capacity) + this.testStringBuffer.EnsureCapacityExponential(len + 4); + if (len < this.testStringBuffer.Capacity) + { + this.testStringBuffer.LengthUnsafe = len; + this.testStringBuffer.StorageSpan[len] = default; + } + + if (this.useMinimumBuild) + _ = this.privateAtlas?.BuildFontsAsync(); + } + } + + if (!this.useGlobalScale) + ImGuiNative.igSetWindowFontScale(1); + } + this.fontHandles ??= Enum.GetValues() .Where(x => x.GetAttribute() is not null) @@ -227,6 +270,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable this.fontHandles?.Values.SelectMany(x => x.Where(y => y.Handle.IsValueCreated).Select(y => y.Handle.Value)) .AggregateToDisposable().Dispose(); this.fontHandles = null; + this.fontDialogHandle?.Dispose(); + this.fontDialogHandle = null; this.privateAtlas?.Dispose(); this.privateAtlas = null; } diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index c325028e1..47ba2c65f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -68,11 +68,11 @@ internal class SettingsWindow : Window var interfaceManager = Service.Get(); var fontAtlasFactory = Service.Get(); - var rebuildFont = fontAtlasFactory.UseAxis != configuration.UseAxisFontsFromGame; + var rebuildFont = !Equals(fontAtlasFactory.DefaultFontSpec, configuration.DefaultFontSpec); rebuildFont |= !Equals(ImGui.GetIO().FontGlobalScale, configuration.GlobalUiScale); ImGui.GetIO().FontGlobalScale = configuration.GlobalUiScale; - fontAtlasFactory.UseAxisOverride = null; + fontAtlasFactory.DefaultFontSpecOverride = null; if (rebuildFont) interfaceManager.RebuildFonts(); diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 5293e13c4..ea6400121 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -5,9 +5,14 @@ using System.Text; using CheapLoc; using Dalamud.Configuration.Internal; +using Dalamud.Game; using Dalamud.Interface.Colors; +using Dalamud.Interface.FontIdentifier; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiFontChooserDialog; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; +using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; @@ -21,31 +26,19 @@ public class SettingsTabLook : SettingsTab { private static readonly (string, float)[] GlobalUiScalePresets = { - ("9.6pt##DalamudSettingsGlobalUiScaleReset96", 9.6f / InterfaceManager.DefaultFontSizePt), - ("12pt##DalamudSettingsGlobalUiScaleReset12", 12f / InterfaceManager.DefaultFontSizePt), - ("14pt##DalamudSettingsGlobalUiScaleReset14", 14f / InterfaceManager.DefaultFontSizePt), - ("18pt##DalamudSettingsGlobalUiScaleReset18", 18f / InterfaceManager.DefaultFontSizePt), - ("24pt##DalamudSettingsGlobalUiScaleReset24", 24f / InterfaceManager.DefaultFontSizePt), - ("36pt##DalamudSettingsGlobalUiScaleReset36", 36f / InterfaceManager.DefaultFontSizePt), + ("80%##DalamudSettingsGlobalUiScaleReset96", 0.8f), + ("100%##DalamudSettingsGlobalUiScaleReset12", 1f), + ("117%##DalamudSettingsGlobalUiScaleReset14", 14 / 12f), + ("150%##DalamudSettingsGlobalUiScaleReset18", 1.5f), + ("200%##DalamudSettingsGlobalUiScaleReset24", 2f), + ("300%##DalamudSettingsGlobalUiScaleReset36", 3f), }; private float globalUiScale; + private IFontSpec defaultFontSpec = null!; public override SettingsEntry[] Entries { get; } = { - new GapSettingsEntry(5), - - new SettingsEntry( - Loc.Localize("DalamudSettingToggleAxisFonts", "Use AXIS fonts as default Dalamud font"), - Loc.Localize("DalamudSettingToggleUiAxisFontsHint", "Use AXIS fonts (the game's main UI fonts) as default Dalamud font."), - c => c.UseAxisFontsFromGame, - (v, c) => c.UseAxisFontsFromGame = v, - v => - { - Service.Get().UseAxisOverride = v; - Service.Get().RebuildFonts(); - }), - new GapSettingsEntry(5, true), new ButtonSettingsEntry( @@ -178,10 +171,10 @@ public class SettingsTabLook : SettingsTab } } - var globalUiScaleInPt = 12f * this.globalUiScale; - if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPt, 0.1f, 9.6f, 36f, "%.1fpt", ImGuiSliderFlags.AlwaysClamp)) + var globalUiScaleInPct = 100f * this.globalUiScale; + if (ImGui.DragFloat("##DalamudSettingsGlobalUiScaleDrag", ref globalUiScaleInPct, 1f, 80f, 300f, "%.0f%%", ImGuiSliderFlags.AlwaysClamp)) { - this.globalUiScale = globalUiScaleInPt / 12f; + this.globalUiScale = globalUiScaleInPct / 100f; ImGui.GetIO().FontGlobalScale = this.globalUiScale; interfaceManager.RebuildFonts(); } @@ -201,12 +194,53 @@ public class SettingsTabLook : SettingsTab } } + ImGuiHelpers.ScaledDummy(5); + + if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font"))) + { + var faf = Service.Get(); + var fcd = new SingleFontChooserDialog( + faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async)); + fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec; + fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId; + interfaceManager.Draw += fcd.Draw; + fcd.ResultTask.ContinueWith( + r => Service.Get().RunOnFrameworkThread( + () => + { + interfaceManager.Draw -= fcd.Draw; + fcd.Dispose(); + + _ = r.Exception; + if (!r.IsCompletedSuccessfully) + return; + + faf.DefaultFontSpecOverride = this.defaultFontSpec = r.Result; + interfaceManager.RebuildFonts(); + })); + } + + ImGui.SameLine(); + + using (interfaceManager.MonoFontHandle?.Push()) + { + if (ImGui.Button(Loc.Localize("DalamudSettingResetDefaultFont", "Reset Default Font"))) + { + var faf = Service.Get(); + faf.DefaultFontSpecOverride = + this.defaultFontSpec = + new SingleFontSpec { FontId = new GameFontAndFamilyId(GameFontFamily.Axis) }; + interfaceManager.RebuildFonts(); + } + } + base.Draw(); } public override void Load() { this.globalUiScale = Service.Get().GlobalUiScale; + this.defaultFontSpec = Service.Get().DefaultFontSpec; base.Load(); } @@ -214,6 +248,7 @@ public class SettingsTabLook : SettingsTab public override void Save() { Service.Get().GlobalUiScale = this.globalUiScale; + Service.Get().DefaultFontSpec = this.defaultFontSpec; base.Save(); } diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index a9c21f94e..0445499c8 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -8,7 +8,8 @@ using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; /// -/// Wrapper for . +/// Wrapper for .
+/// Not intended for plugins to implement. ///
public interface IFontAtlas : IDisposable { @@ -93,11 +94,15 @@ public interface IFontAtlas : IDisposable ///
/// Callback for . /// Handle to a font that may or may not be ready yet. + /// + /// Consider calling to support + /// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users. + /// /// /// On initialization: /// /// this.fontHandle = atlas.NewDelegateFontHandle(e => e.OnPreBuild(tk => { - /// var config = new SafeFontConfig { SizePx = 16 }; + /// var config = new SafeFontConfig { SizePx = UiBuilder.DefaultFontSizePx }; /// config.MergeFont = tk.AddFontFromFile(@"C:\Windows\Fonts\comic.ttf", config); /// tk.AddGameSymbol(config); /// tk.AddExtraGlyphsForDalamudLanguage(config); diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs index f75ed4686..158366b12 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkit.cs @@ -9,7 +9,8 @@ using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; /// -/// Common stuff for and . +/// Common stuff for and .
+/// Not intended for plugins to implement. ///
public interface IFontAtlasBuildToolkit { diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs index eb7c7e08c..d824eca52 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -5,7 +5,8 @@ using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; /// -/// Toolkit for use when the build state is . +/// Toolkit for use when the build state is .
+/// Not intended for plugins to implement. ///
public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit { diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index 38d8d2fe8..9ab480374 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -1,6 +1,7 @@ using System.IO; using System.Runtime.InteropServices; +using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; @@ -10,6 +11,7 @@ namespace Dalamud.Interface.ManagedFontAtlas; ///
/// Toolkit for use when the build state is .
+/// Not intended for plugins to implement.
///
/// After returns, /// either must be set, @@ -52,6 +54,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// True if ignored. bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + /// + /// Registers a function to be run after build. + /// + /// The action to run. + void RegisterPostBuild(Action action); + /// /// Adds a font from memory region allocated using .
/// It WILL crash if you try to use a memory pointer allocated in some other way.
@@ -134,7 +142,12 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// As this involves adding multiple fonts, calling this function will set /// as the return value of this function, if it was empty before. ///
- /// Font size in pixels. + /// + /// Font size in pixels. + /// If a negative value is supplied, + /// (. * ) will be + /// used as the font size. Specify -1 to use the default font size. + /// /// The glyph ranges. Use .ToGlyphRange to build. /// A font returned from . ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges = null); diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 11c26616b..70799bb9c 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -5,7 +5,8 @@ using ImGuiNET; namespace Dalamud.Interface.ManagedFontAtlas; /// -/// Represents a reference counting handle for fonts. +/// Represents a reference counting handle for fonts.
+/// Not intended for plugins to implement. ///
public interface IFontHandle : IDisposable { diff --git a/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs index 9136d2723..a4cc3afa7 100644 --- a/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs +++ b/Dalamud/Interface/ManagedFontAtlas/ILockedImFont.cs @@ -4,7 +4,8 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// /// The wrapper for , guaranteeing that the associated data will be available as long as -/// this struct is not disposed. +/// this struct is not disposed.
+/// Not intended for plugins to implement. ///
public interface ILockedImFont : IDisposable { diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index e2b096701..396c8b26a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using System.Text.Unicode; using Dalamud.Configuration.Internal; +using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; @@ -42,6 +43,7 @@ internal sealed partial class FontAtlasFactory private readonly GamePrebakedFontHandle.HandleSubstance gameFontHandleSubstance; private readonly FontAtlasFactory factory; private readonly FontAtlasBuiltData data; + private readonly List registeredPostBuildActions = new(); ///
/// Initializes a new instance of the class. @@ -162,6 +164,9 @@ internal sealed partial class FontAtlasFactory /// public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => this.data.AddNewTexture(textureWrap, disposeOnError); + + /// + public void RegisterPostBuild(Action action) => this.registeredPostBuildActions.Add(action); /// public unsafe ImFontPtr AddFontFromImGuiHeapAllocatedMemory( @@ -314,18 +319,32 @@ internal sealed partial class FontAtlasFactory /// public ImFontPtr AddDalamudDefaultFont(float sizePx, ushort[]? glyphRanges) { - ImFontPtr font; + ImFontPtr font = default; glyphRanges ??= this.factory.DefaultGlyphRanges; - if (this.factory.UseAxis) + + var dfid = this.factory.DefaultFontSpec; + if (sizePx < 0f) + sizePx *= -dfid.SizePx; + + if (dfid is SingleFontSpec sfs) { - font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); + if (sfs.FontId is DalamudDefaultFontAndFamilyId) + { + // invalid; calling sfs.AddToBuildToolkit calls this function, causing infinite recursion + } + else + { + sfs = sfs with { SizePx = sizePx }; + font = sfs.AddToBuildToolkit(this); + if (sfs.FontId is not GameFontAndFamilyId { GameFontFamily: GameFontFamily.Axis }) + this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font }); + } } - else + + if (font.IsNull()) { - font = this.AddDalamudAssetFont( - DalamudAsset.NotoSansJpMedium, - new() { SizePx = sizePx, GlyphRanges = glyphRanges }); - this.AddGameSymbol(new() { SizePx = sizePx, MergeFont = font }); + // fall back to AXIS fonts + font = this.AddGameGlyphs(new(GameFontFamily.Axis, sizePx), glyphRanges, default); } this.AttachExtraGlyphsForDalamudLanguage(new() { SizePx = sizePx, MergeFont = font }); @@ -531,6 +550,13 @@ internal sealed partial class FontAtlasFactory substance.OnPostBuild(this); } + public void PostBuildCallbacks() + { + foreach (var ac in this.registeredPostBuildActions) + ac.InvokeSafely(); + this.registeredPostBuildActions.Clear(); + } + public unsafe void UploadTextures() { var buf = Array.Empty(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 4d636b8cf..4968bc891 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -658,7 +658,7 @@ internal sealed partial class FontAtlasFactory toolkit = res.CreateToolkit(this.factory, isAsync); // PreBuildSubstances deals with toolkit.Add... function family. Do this first. - var defaultFont = toolkit.AddDalamudDefaultFont(InterfaceManager.DefaultFontSizePx, null); + var defaultFont = toolkit.AddDalamudDefaultFont(-1, null); this.BuildStepChange?.Invoke(toolkit); toolkit.PreBuildSubstances(); @@ -679,6 +679,7 @@ internal sealed partial class FontAtlasFactory toolkit.PostBuild(); toolkit.PostBuildSubstances(); + toolkit.PostBuildCallbacks(); this.BuildStepChange?.Invoke(toolkit); foreach (var font in toolkit.Fonts) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 358ccd845..d3bc976f2 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; +using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Storage.Assets; @@ -108,14 +109,29 @@ internal sealed partial class FontAtlasFactory } /// - /// Gets or sets a value indicating whether to override configuration for UseAxis. + /// Gets or sets a value indicating whether to override configuration for . /// - public bool? UseAxisOverride { get; set; } = null; + public IFontSpec? DefaultFontSpecOverride { get; set; } = null; /// - /// Gets a value indicating whether to use AXIS fonts. + /// Gets the default font ID. /// - public bool UseAxis => this.UseAxisOverride ?? Service.Get().UseAxisFontsFromGame; + public IFontSpec DefaultFontSpec => + this.DefaultFontSpecOverride + ?? Service.Get().DefaultFontSpec +#pragma warning disable CS0618 // Type or member is obsolete + ?? (Service.Get().UseAxisFontsFromGame +#pragma warning restore CS0618 // Type or member is obsolete + ? new() + { + FontId = new GameFontAndFamilyId(GameFontFamily.Axis), + SizePx = InterfaceManager.DefaultFontSizePx, + } + : new SingleFontSpec + { + FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium), + SizePx = InterfaceManager.DefaultFontSizePx + 1, + }); /// /// Gets the service instance of . diff --git a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs index cb7f7c65a..caa686856 100644 --- a/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs +++ b/Dalamud/Interface/ManagedFontAtlas/SafeFontConfig.cs @@ -26,7 +26,7 @@ public struct SafeFontConfig this.PixelSnapH = true; this.GlyphMaxAdvanceX = float.MaxValue; this.RasterizerMultiply = 1f; - this.RasterizerGamma = 1.4f; + this.RasterizerGamma = 1.7f; this.EllipsisChar = unchecked((char)-1); this.Raw.FontDataOwnedByAtlas = 1; } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 55e11dfac..7a3eb6fb6 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -7,6 +7,7 @@ using Dalamud.Game; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; +using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; @@ -173,12 +174,12 @@ public sealed class UiBuilder : IDisposable /// /// Gets the default Dalamud font size in points. /// - public static float DefaultFontSizePt => InterfaceManager.DefaultFontSizePt; + public static float DefaultFontSizePt => Service.Get().DefaultFontSpec.SizePt; /// /// Gets the default Dalamud font size in pixels. /// - public static float DefaultFontSizePx => InterfaceManager.DefaultFontSizePx; + public static float DefaultFontSizePx => Service.Get().DefaultFontSpec.SizePx; /// /// Gets the default Dalamud font - supporting all game languages and icons.
@@ -198,6 +199,11 @@ public sealed class UiBuilder : IDisposable ///
public static ImFontPtr MonoFont => InterfaceManager.MonoFont; + /// + /// Gets the default font specifications. + /// + public IFontSpec DefaultFontSpec => Service.Get().DefaultFontSpec; + /// /// Gets the handle to the default Dalamud font - supporting all game languages and icons. /// diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index 444463d41..f02effe1d 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using System.Reactive.Disposables; using System.Runtime.InteropServices; +using System.Text; using System.Text.Unicode; using Dalamud.Configuration.Internal; @@ -543,6 +544,24 @@ public static class ImGuiHelpers var pageIndex = unchecked((ushort)(codepoint / 4096)); font.NativePtr->Used4kPagesMap[pageIndex >> 3] |= unchecked((byte)(1 << (pageIndex & 7))); } + + /// + /// Sets the text for a text input, during the callback. + /// + /// The callback data. + /// The new text. + internal static unsafe void SetTextFromCallback(ImGuiInputTextCallbackData* data, string s) + { + if (data->BufTextLen != 0) + ImGuiNative.ImGuiInputTextCallbackData_DeleteChars(data, 0, data->BufTextLen); + + var len = Encoding.UTF8.GetByteCount(s); + var buf = len < 1024 ? stackalloc byte[len] : new byte[len]; + Encoding.UTF8.GetBytes(s, buf); + fixed (byte* pBuf = buf) + ImGuiNative.ImGuiInputTextCallbackData_InsertChars(data, 0, pBuf, pBuf + len); + ImGuiNative.ImGuiInputTextCallbackData_SelectAll(data); + } /// /// Finds the corresponding ImGui viewport ID for the given window handle. diff --git a/Dalamud/Utility/ArrayExtensions.cs b/Dalamud/Utility/ArrayExtensions.cs index fa6e3dbe9..5b6ce2332 100644 --- a/Dalamud/Utility/ArrayExtensions.cs +++ b/Dalamud/Utility/ArrayExtensions.cs @@ -97,4 +97,76 @@ internal static class ArrayExtensions /// casted as a if it is one; otherwise the result of . public static IReadOnlyCollection AsReadOnlyCollection(this IEnumerable array) => array as IReadOnlyCollection ?? array.ToArray(); + + /// + public static int FindIndex(this IReadOnlyList list, Predicate match) + => list.FindIndex(0, list.Count, match); + + /// + public static int FindIndex(this IReadOnlyList list, int startIndex, Predicate match) + => list.FindIndex(startIndex, list.Count - startIndex, match); + + /// + public static int FindIndex(this IReadOnlyList list, int startIndex, int count, Predicate match) + { + if ((uint)startIndex > (uint)list.Count) + throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null); + + if (count < 0 || startIndex > list.Count - count) + throw new ArgumentOutOfRangeException(nameof(count), count, null); + + if (match == null) + throw new ArgumentNullException(nameof(match)); + + var endIndex = startIndex + count; + for (var i = startIndex; i < endIndex; i++) + { + if (match(list[i])) return i; + } + + return -1; + } + + /// + public static int FindLastIndex(this IReadOnlyList list, Predicate match) + => list.FindLastIndex(list.Count - 1, list.Count, match); + + /// + public static int FindLastIndex(this IReadOnlyList list, int startIndex, Predicate match) + => list.FindLastIndex(startIndex, startIndex + 1, match); + + /// + public static int FindLastIndex(this IReadOnlyList list, int startIndex, int count, Predicate match) + { + if (match == null) + throw new ArgumentNullException(nameof(match)); + + if (list.Count == 0) + { + // Special case for 0 length List + if (startIndex != -1) + throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null); + } + else + { + // Make sure we're not out of range + if ((uint)startIndex >= (uint)list.Count) + throw new ArgumentOutOfRangeException(nameof(startIndex), startIndex, null); + } + + // 2nd have of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0. + if (count < 0 || startIndex - count + 1 < 0) + throw new ArgumentOutOfRangeException(nameof(count), count, null); + + var endIndex = startIndex - count; + for (var i = startIndex; i > endIndex; i--) + { + if (match(list[i])) + { + return i; + } + } + + return -1; + } } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index d53c2fe19..f5ad8b999 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -22,6 +22,9 @@ using Dalamud.Logging.Internal; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Serilog; + +using TerraFX.Interop.Windows; + using Windows.Win32.Storage.FileSystem; namespace Dalamud.Utility; @@ -684,6 +687,16 @@ public static class Util return names.ElementAt(rng.Next(0, names.Count() - 1)).Singular.RawString; } + /// + /// Throws a corresponding exception if is true. + /// + /// The result value. + internal static void ThrowOnError(this HRESULT hr) + { + if (hr.FAILED) + Marshal.ThrowExceptionForHR(hr.Value); + } + /// /// Print formatted GameObject Information to ImGui. /// From 3283d0cc114867a2e66af8bc420664a4208d03f6 Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 14 Feb 2024 06:09:23 +0900 Subject: [PATCH 484/585] Turn IDalamudAssetManager public (#1638) --- Dalamud/Storage/Assets/IDalamudAssetManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs index 1202891b8..643eef18c 100644 --- a/Dalamud/Storage/Assets/IDalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs @@ -16,7 +16,7 @@ namespace Dalamud.Storage.Assets; /// Think of C++ [[nodiscard]]. Also, like the intended meaning of the attribute, such methods will not have /// externally visible state changes. /// -internal interface IDalamudAssetManager +public interface IDalamudAssetManager { /// /// Gets the shared texture wrap for . From 86504dfd9e1989cd28006e32ed56359d78b60ba5 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Tue, 13 Feb 2024 23:25:18 +0100 Subject: [PATCH 485/585] build: 9.0.0.18 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index f58a0c47a..55710cf0b 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.17 + 9.0.0.18 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From cf3091b4099b9ca365360088c8409c93a71a2361 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 14 Feb 2024 13:05:45 +0900 Subject: [PATCH 486/585] If docked, force default font on window decoration ImGui docking functions are called outside our drawing context (from ImGui::NewFrame), which includes most of dock-related drawing calls. However, ImGui::RenderWindowDecoration is called from ImGui::Begin, which may be under the effect of other pushed font. As IG::RWD references to the ImDrawList irrelevant to the global shared state, it was trying to draw a rectangle referring to a pixel that is not guaranteed to be a white pixel. This commit fixes that by forcing the use of the default font for IG::RWD when the window is docked. --- .../ImGuiClipboardFunctionProvider.cs | 1 - .../Internals/ImGuiDockNodeUpdateForceFont.cs | 78 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index 1746fb1c4..bbf665405 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -52,7 +52,6 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis private ImGuiClipboardFunctionProvider(InterfaceManager.InterfaceManagerWithScene imws) { // Effectively waiting for ImGui to become available. - _ = imws; Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); var io = ImGui.GetIO(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs new file mode 100644 index 000000000..a2a30429a --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs @@ -0,0 +1,78 @@ +using System.Diagnostics; +using System.Linq; + +using Dalamud.Hooking; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; + +namespace Dalamud.Interface.ManagedFontAtlas.Internals; + +/// +/// Forces ImGui::RenderWindowDecorations to use the default font. +/// Fixes dock node draw using shared data across different draw lists. +/// TODO: figure out how to synchronize ImDrawList::_Data and ImDrawList::Push/PopTextureID across different instances. +/// It might be better to just special-case that particular function, +/// as no other code touches ImDrawList that is irrelevant to the global shared state, +/// with the exception of Dock... functions which are called from ImGui::NewFrame, +/// which are guaranteed to use the global default font. +/// +[ServiceManager.EarlyLoadedService] +internal class ImGuiRenderWindowDecorationsForceFont : IServiceType, IDisposable +{ + private const int CImGuiRenderWindowDecorationsOffset = 0x461B0; + private const int CImGuiWindowDockIsActiveOffset = 0x401; + + private readonly Hook hook; + + [ServiceManager.ServiceConstructor] + private ImGuiRenderWindowDecorationsForceFont(InterfaceManager.InterfaceManagerWithScene imws) + { + // Effectively waiting for ImGui to become available. + Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); + + var cimgui = Process.GetCurrentProcess().Modules.Cast() + .First(x => x.ModuleName == "cimgui.dll") + .BaseAddress; + this.hook = Hook.FromAddress( + cimgui + CImGuiRenderWindowDecorationsOffset, + this.ImGuiRenderWindowDecorationsDetour); + this.hook.Enable(); + } + + private delegate void ImGuiRenderWindowDecorationsDelegate( + nint window, + nint titleBarRectPtr, + byte titleBarIsHighlight, + byte handleBordersAndResizeGrips, + int resizeGripCount, + nint resizeGripColPtr, + float resizeGripDrawSize); + + /// + public void Dispose() => this.hook.Dispose(); + + private unsafe void ImGuiRenderWindowDecorationsDetour( + nint window, + nint titleBarRectPtr, + byte titleBarIsHighlight, + byte handleBordersAndResizeGrips, + int resizeGripCount, + nint resizeGripColPtr, + float resizeGripDrawSize) + { + using ( + ((byte*)window)![CImGuiWindowDockIsActiveOffset] != 0 + ? Service.Get().DefaultFontHandle?.Push() + : null) + { + this.hook.Original( + window, + titleBarRectPtr, + titleBarIsHighlight, + handleBordersAndResizeGrips, + resizeGripCount, + resizeGripColPtr, + resizeGripDrawSize); + } + } +} From 2de9c8ed5b88e4f4ba0ecbc05c1bfc46cc10495b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 14 Feb 2024 21:59:20 +0900 Subject: [PATCH 487/585] Fix insufficient ImDrawList implementation --- Dalamud/Interface/Internal/DalamudIme.cs | 4 +- .../Internal/ImGuiDrawListFixProvider.cs | 124 ++++++++++++++++++ .../ManagedAsserts/ImGuiContextOffsets.cs | 2 + .../Internals/ImGuiDockNodeUpdateForceFont.cs | 78 ----------- 4 files changed, 128 insertions(+), 80 deletions(-) create mode 100644 Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs delete mode 100644 Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 28a9075bd..1ee248b17 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -11,6 +11,7 @@ using System.Text.Unicode; using Dalamud.Game.Text; using Dalamud.Hooking.WndProcHook; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; @@ -28,7 +29,6 @@ namespace Dalamud.Interface.Internal; [ServiceManager.BlockingEarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType { - private const int ImGuiContextTextStateOffset = 0x4588; private const int CImGuiStbTextCreateUndoOffset = 0xB57A0; private const int CImGuiStbTextUndoOffset = 0xB59C0; @@ -178,7 +178,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType internal char InputModeIcon { get; private set; } private static ImGuiInputTextState* TextState => - (ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextTextStateOffset); + (ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset); /// public void Dispose() diff --git a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs new file mode 100644 index 000000000..cdf7ab23e --- /dev/null +++ b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs @@ -0,0 +1,124 @@ +using System.Diagnostics; +using System.Linq; +using System.Numerics; + +using Dalamud.Hooking; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal; + +/// +/// Fixes ImDrawList not correctly dealing with the current texture for that draw list not in tune with the global +/// state. Currently, ImDrawList::AddPolyLine and ImDrawList::AddRectFilled are affected. +/// +/// * The implementation for AddRectFilled is entirely replaced with the hook below. +/// * The implementation for AddPolyLine is wrapped with Push/PopTextureID. +/// +/// TODO: +/// * imgui_draw.cpp:1433 ImDrawList::AddRectFilled +/// The if block needs a PushTextureID(_Data->TexIdCommon)/PopTextureID() block, +/// if _Data->TexIdCommon != _CmdHeader.TextureId. +/// * imgui_draw.cpp:729 ImDrawList::AddPolyLine +/// The if block always needs to call PushTextureID if the abovementioned condition is not met. +/// Change push_texture_id to only have one condition. +/// +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposable +{ + private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0; + private const int CImGuiImDrawListAddRectFilled = 0x59FD0; + private const int CImGuiImDrawListSharedDataTexIdCommonOffset = 0; + + private readonly Hook hookImDrawListAddPolyline; + private readonly Hook hookImDrawListAddRectFilled; + + [ServiceManager.ServiceConstructor] + private ImGuiDrawListFixProvider(InterfaceManager.InterfaceManagerWithScene imws) + { + // Force cimgui.dll to be loaded. + _ = ImGui.GetCurrentContext(); + var cimgui = Process.GetCurrentProcess().Modules.Cast() + .First(x => x.ModuleName == "cimgui.dll") + .BaseAddress; + + this.hookImDrawListAddPolyline = Hook.FromAddress( + cimgui + CImGuiImDrawListAddPolyLineOffset, + this.ImDrawListAddPolylineDetour); + this.hookImDrawListAddRectFilled = Hook.FromAddress( + cimgui + CImGuiImDrawListAddRectFilled, + this.ImDrawListAddRectFilledDetour); + this.hookImDrawListAddPolyline.Enable(); + this.hookImDrawListAddRectFilled.Enable(); + } + + private delegate void ImDrawListAddPolyLine( + ImDrawListPtr drawListPtr, + ref Vector2 points, + int pointsCount, + uint color, + ImDrawFlags flags, + float thickness); + + private delegate void ImDrawListAddRectFilled( + ImDrawListPtr drawListPtr, + ref Vector2 min, + ref Vector2 max, + uint col, + float rounding, + ImDrawFlags flags); + + /// + public void Dispose() + { + this.hookImDrawListAddPolyline.Dispose(); + this.hookImDrawListAddRectFilled.Dispose(); + } + + private void ImDrawListAddRectFilledDetour( + ImDrawListPtr drawListPtr, + ref Vector2 min, + ref Vector2 max, + uint col, + float rounding, + ImDrawFlags flags) + { + if (rounding < 0 || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask) + { + var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset); + var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId; + if (pushTextureId) + drawListPtr.PushTextureID(texIdCommon); + + drawListPtr.PrimReserve(6, 4); + drawListPtr.PrimRect(min, max, col); + + if (pushTextureId) + drawListPtr.PopTextureID(); + } + else + { + drawListPtr.PathRect(min, max, rounding, flags); + drawListPtr.PathFillConvex(col); + } + } + + private void ImDrawListAddPolylineDetour( + ImDrawListPtr drawListPtr, + ref Vector2 points, + int pointsCount, + uint color, + ImDrawFlags flags, + float thickness) + { + var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset); + var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId; + if (pushTextureId) + drawListPtr.PushTextureID(texIdCommon); + + this.hookImDrawListAddPolyline.Original(drawListPtr, ref points, pointsCount, color, flags, thickness); + + if (pushTextureId) + drawListPtr.PopTextureID(); + } +} diff --git a/Dalamud/Interface/Internal/ManagedAsserts/ImGuiContextOffsets.cs b/Dalamud/Interface/Internal/ManagedAsserts/ImGuiContextOffsets.cs index fd203192f..89e23ab78 100644 --- a/Dalamud/Interface/Internal/ManagedAsserts/ImGuiContextOffsets.cs +++ b/Dalamud/Interface/Internal/ManagedAsserts/ImGuiContextOffsets.cs @@ -18,4 +18,6 @@ internal static class ImGuiContextOffsets public const int FontStackOffset = 0x7A4; public const int BeginPopupStackOffset = 0x7B8; + + public const int TextStateOffset = 0x4588; } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs deleted file mode 100644 index a2a30429a..000000000 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/ImGuiDockNodeUpdateForceFont.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Diagnostics; -using System.Linq; - -using Dalamud.Hooking; -using Dalamud.Interface.Internal; -using Dalamud.Interface.Utility; - -namespace Dalamud.Interface.ManagedFontAtlas.Internals; - -/// -/// Forces ImGui::RenderWindowDecorations to use the default font. -/// Fixes dock node draw using shared data across different draw lists. -/// TODO: figure out how to synchronize ImDrawList::_Data and ImDrawList::Push/PopTextureID across different instances. -/// It might be better to just special-case that particular function, -/// as no other code touches ImDrawList that is irrelevant to the global shared state, -/// with the exception of Dock... functions which are called from ImGui::NewFrame, -/// which are guaranteed to use the global default font. -/// -[ServiceManager.EarlyLoadedService] -internal class ImGuiRenderWindowDecorationsForceFont : IServiceType, IDisposable -{ - private const int CImGuiRenderWindowDecorationsOffset = 0x461B0; - private const int CImGuiWindowDockIsActiveOffset = 0x401; - - private readonly Hook hook; - - [ServiceManager.ServiceConstructor] - private ImGuiRenderWindowDecorationsForceFont(InterfaceManager.InterfaceManagerWithScene imws) - { - // Effectively waiting for ImGui to become available. - Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); - - var cimgui = Process.GetCurrentProcess().Modules.Cast() - .First(x => x.ModuleName == "cimgui.dll") - .BaseAddress; - this.hook = Hook.FromAddress( - cimgui + CImGuiRenderWindowDecorationsOffset, - this.ImGuiRenderWindowDecorationsDetour); - this.hook.Enable(); - } - - private delegate void ImGuiRenderWindowDecorationsDelegate( - nint window, - nint titleBarRectPtr, - byte titleBarIsHighlight, - byte handleBordersAndResizeGrips, - int resizeGripCount, - nint resizeGripColPtr, - float resizeGripDrawSize); - - /// - public void Dispose() => this.hook.Dispose(); - - private unsafe void ImGuiRenderWindowDecorationsDetour( - nint window, - nint titleBarRectPtr, - byte titleBarIsHighlight, - byte handleBordersAndResizeGrips, - int resizeGripCount, - nint resizeGripColPtr, - float resizeGripDrawSize) - { - using ( - ((byte*)window)![CImGuiWindowDockIsActiveOffset] != 0 - ? Service.Get().DefaultFontHandle?.Push() - : null) - { - this.hook.Original( - window, - titleBarRectPtr, - titleBarIsHighlight, - handleBordersAndResizeGrips, - resizeGripCount, - resizeGripColPtr, - resizeGripDrawSize); - } - } -} From edc5826fe032c3063b53343e26152fe18a47f333 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 15 Feb 2024 03:55:00 +0900 Subject: [PATCH 488/585] Dalamud.Boot: use unicode::convert --- Dalamud.Boot/utils.cpp | 10 ---------- Dalamud.Boot/utils.h | 2 -- Dalamud.Boot/veh.cpp | 14 +++++++------- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Dalamud.Boot/utils.cpp b/Dalamud.Boot/utils.cpp index 62a9d7055..9dc296c5f 100644 --- a/Dalamud.Boot/utils.cpp +++ b/Dalamud.Boot/utils.cpp @@ -578,16 +578,6 @@ std::vector utils::get_env_list(const wchar_t* pcszName) { return res; } -std::wstring utils::to_wstring(const std::string& str) { - if (str.empty()) return std::wstring(); - size_t convertedChars = 0; - size_t newStrSize = str.size() + 1; - std::wstring wstr(newStrSize, L'\0'); - mbstowcs_s(&convertedChars, &wstr[0], newStrSize, str.c_str(), _TRUNCATE); - wstr.resize(convertedChars - 1); - return wstr; -} - std::filesystem::path utils::get_module_path(HMODULE hModule) { std::wstring buf(MAX_PATH, L'\0'); while (true) { diff --git a/Dalamud.Boot/utils.h b/Dalamud.Boot/utils.h index ebf48a294..85509cf38 100644 --- a/Dalamud.Boot/utils.h +++ b/Dalamud.Boot/utils.h @@ -264,8 +264,6 @@ namespace utils { return get_env_list(unicode::convert(pcszName).c_str()); } - std::wstring to_wstring(const std::string& str); - std::filesystem::path get_module_path(HMODULE hModule); /// @brief Find the game main window. diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index eb27acce7..fc8689af7 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -110,13 +110,13 @@ static void append_injector_launch_args(std::vector& args) case DalamudStartInfo::LoadMethod::DllInject: args.emplace_back(L"--mode=inject"); } - args.emplace_back(L"--logpath=\"" + utils::to_wstring(g_startInfo.BootLogPath) + L"\""); - args.emplace_back(L"--dalamud-working-directory=\"" + utils::to_wstring(g_startInfo.WorkingDirectory) + L"\""); - args.emplace_back(L"--dalamud-configuration-path=\"" + utils::to_wstring(g_startInfo.ConfigurationPath) + L"\""); - args.emplace_back(L"--dalamud-plugin-directory=\"" + utils::to_wstring(g_startInfo.PluginDirectory) + L"\""); - args.emplace_back(L"--dalamud-asset-directory=\"" + utils::to_wstring(g_startInfo.AssetDirectory) + L"\""); - args.emplace_back(L"--dalamud-client-language=" + std::to_wstring(static_cast(g_startInfo.Language))); - args.emplace_back(L"--dalamud-delay-initialize=" + std::to_wstring(g_startInfo.DelayInitializeMs)); + args.emplace_back(L"--logpath=\"" + unicode::convert(g_startInfo.BootLogPath) + L"\""); + args.emplace_back(L"--dalamud-working-directory=\"" + unicode::convert(g_startInfo.WorkingDirectory) + L"\""); + args.emplace_back(L"--dalamud-configuration-path=\"" + unicode::convert(g_startInfo.ConfigurationPath) + 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(std::format(L"--dalamud-client-language={}", static_cast(g_startInfo.Language))); + args.emplace_back(std::format(L"--dalamud-delay-initialize={}", g_startInfo.DelayInitializeMs)); if (g_startInfo.BootShowConsole) args.emplace_back(L"--console"); if (g_startInfo.BootEnableEtw) From cce4f03403aecf5d72bac29f6b71673e390dea86 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 15 Feb 2024 04:14:23 +0900 Subject: [PATCH 489/585] Adjust logDir if logDir points to a .log file --- DalamudCrashHandler/DalamudCrashHandler.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 1930b6fb4..4e2f01708 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -518,6 +518,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s mz_throw_if_failed(mz_zip_writer_init_v2(&zipa, 0, 0), "mz_zip_writer_init_v2"); mz_throw_if_failed(mz_zip_writer_add_mem(&zipa, "trouble.json", troubleshootingPackData.data(), troubleshootingPackData.size(), MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_BEST_COMPRESSION), "mz_zip_writer_add_mem: trouble.json"); mz_throw_if_failed(mz_zip_writer_add_mem(&zipa, "crash.log", crashLog.data(), crashLog.size(), MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_BEST_COMPRESSION), "mz_zip_writer_add_mem: crash.log"); + std::string logExportLog; struct HandleAndBaseOffset { HANDLE h; @@ -534,8 +535,12 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s }; for (const auto& pcszLogFileName : SourceLogFiles) { const auto logFilePath = logDir / pcszLogFileName; - if (!exists(logFilePath)) + if (!exists(logFilePath)) { + logExportLog += std::format("File does not exist: {}\n", ws_to_u8(logFilePath.wstring())); continue; + } else { + logExportLog += std::format("Including: {}\n", ws_to_u8(logFilePath.wstring())); + } const auto hLogFile = CreateFileW(logFilePath.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, nullptr); if (hLogFile == INVALID_HANDLE_VALUE) @@ -574,6 +579,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s ), std::format("mz_zip_writer_add_read_buf_callback({})", ws_to_u8(logFilePath.wstring()))); } + mz_throw_if_failed(mz_zip_writer_add_mem(&zipa, "logexport.log", logExportLog.data(), logExportLog.size(), MZ_ZIP_FLAG_WRITE_HEADER_SET_SIZE | MZ_BEST_COMPRESSION), "mz_zip_writer_add_mem: logexport.log"); mz_throw_if_failed(mz_zip_writer_finalize_archive(&zipa), "mz_zip_writer_finalize_archive"); mz_throw_if_failed(mz_zip_writer_end(&zipa), "mz_zip_writer_end"); @@ -710,6 +716,13 @@ int main() { return InvalidParameter; } + if (logDir.filename().wstring().ends_with(L".log")) { + std::wcout << L"logDir seems to be pointing to a file; stripping the last path component.\n" << std::endl; + std::wcout << L"Previous: " << logDir.wstring() << std::endl; + logDir = logDir.parent_path(); + std::wcout << L"Stripped: " << logDir.wstring() << std::endl; + } + while (true) { std::cout << "Waiting for crash...\n"; From ea43d656361174ae729e8956bd153a4dbeb2ee55 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 15 Feb 2024 07:52:40 +0900 Subject: [PATCH 490/585] Fix memory ownership on AddFontFromImGuiHeapAllocatedMemory (#1651) --- .../FontAtlasFactory.BuildToolkit.cs | 19 ++++++++++++++++++- .../FontAtlasFactory.Implementation.cs | 6 ++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index 396c8b26a..a57e6d036 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -185,6 +185,7 @@ internal sealed partial class FontAtlasFactory dataSize, debugTag); + var font = default(ImFontPtr); try { fontConfig.ThrowOnInvalidValues(); @@ -192,6 +193,7 @@ internal sealed partial class FontAtlasFactory var raw = fontConfig.Raw with { FontData = dataPointer, + FontDataOwnedByAtlas = 1, FontDataSize = dataSize, }; @@ -203,7 +205,7 @@ internal sealed partial class FontAtlasFactory TrueTypeUtils.CheckImGuiCompatibleOrThrow(raw); - var font = this.NewImAtlas.AddFont(&raw); + font = this.NewImAtlas.AddFont(&raw); var dataHash = default(HashCode); dataHash.AddBytes(new(dataPointer, dataSize)); @@ -240,8 +242,23 @@ internal sealed partial class FontAtlasFactory } catch { + if (!font.IsNull()) + { + // Note that for both RemoveAt calls, corresponding destructors will be called. + + var configIndex = this.data.ConfigData.FindIndex(x => x.DstFont == font.NativePtr); + if (configIndex >= 0) + this.data.ConfigData.RemoveAt(configIndex); + + var index = this.Fonts.IndexOf(font); + if (index >= 0) + this.Fonts.RemoveAt(index); + } + + // ImFontConfig has no destructor, and does not free the data. if (freeOnException) ImGuiNative.igMemFree(dataPointer); + throw; } } diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 4968bc891..883fcbbfc 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -46,6 +46,9 @@ internal sealed partial class FontAtlasFactory private class FontAtlasBuiltData : IRefCountable { + // Field for debugging. + private static int numActiveInstances; + private readonly List wraps; private readonly List substances; @@ -73,6 +76,9 @@ internal sealed partial class FontAtlasFactory this.Garbage.Add(() => ImGuiNative.ImFontAtlas_destroy(atlasPtr)); this.IsBuildInProgress = true; + + Interlocked.Increment(ref numActiveInstances); + this.Garbage.Add(() => Interlocked.Decrement(ref numActiveInstances)); } catch { From a8bb8cbec5e21517ec5d4fd658f0cda10531200e Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Wed, 14 Feb 2024 16:34:45 -0800 Subject: [PATCH 491/585] feat: Add "deref nullptr in hook" crash item - Move all crash items into submenu --- .../Interface/Internal/DalamudInterface.cs | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index b8ca98584..00bef19af 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -12,6 +12,7 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Gui; using Dalamud.Game.Internal; +using Dalamud.Hooking; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.ManagedAsserts; @@ -89,7 +90,7 @@ internal class DalamudInterface : IDisposable, IServiceType private bool isImPlotDrawDemoWindow = false; private bool isImGuiTestWindowsInMonospace = false; private bool isImGuiDrawMetricsWindow = false; - + [ServiceManager.ServiceConstructor] private DalamudInterface( Dalamud dalamud, @@ -188,7 +189,9 @@ internal class DalamudInterface : IDisposable, IServiceType this.creditsDarkeningAnimation.Point1 = Vector2.Zero; this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha); } - + + private delegate nint CrashDebugDelegate(nint self); + /// /// Gets the number of frames since Dalamud has loaded. /// @@ -744,28 +747,48 @@ internal class DalamudInterface : IDisposable, IServiceType } ImGui.Separator(); - - if (ImGui.MenuItem("Access Violation")) + + if (ImGui.BeginMenu("Crash game")) { - Marshal.ReadByte(IntPtr.Zero); - } - - if (ImGui.MenuItem("Crash game (nullptr)")) - { - unsafe + if (ImGui.MenuItem("Access Violation")) { - var framework = Framework.Instance(); - framework->UIModule = (UIModule*)0; - } - } - - if (ImGui.MenuItem("Crash game (non-nullptr)")) - { - unsafe + Marshal.ReadByte(IntPtr.Zero); + } + + if (ImGui.MenuItem("Set UiModule to NULL")) { - var framework = Framework.Instance(); - framework->UIModule = (UIModule*)0x12345678; + unsafe + { + var framework = Framework.Instance(); + framework->UIModule = (UIModule*)0; + } } + + if (ImGui.MenuItem("Set UiModule to invalid ptr")) + { + unsafe + { + var framework = Framework.Instance(); + framework->UIModule = (UIModule*)0x12345678; + } + } + + if (ImGui.MenuItem("Deref nullptr in Hook")) + { + unsafe + { + var hook = Hook.FromAddress( + (nint)UIModule.StaticVTable.GetUIInputData, + self => + { + _ = *(byte*)0; + return (nint)UIModule.Instance()->GetUIInputData(); + }); + hook.Enable(); + } + } + + ImGui.EndMenu(); } if (ImGui.MenuItem("Report crashes at shutdown", null, this.configuration.ReportShutdownCrashes)) From 914cd363fd1b1b70880d7f0d590876406f0919d5 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 15 Feb 2024 01:45:10 +0100 Subject: [PATCH 492/585] Bump Lumina to 3.16.0 --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 2 +- Dalamud/Dalamud.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index d7eb8499c..bf315d99e 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -27,7 +27,7 @@ - + diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 55710cf0b..208e6d4ea 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -68,7 +68,7 @@ - + From c8be22e2848ef451c8f356ea8071134e1376729b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 15 Feb 2024 15:42:30 +0900 Subject: [PATCH 493/585] Do FlushInstructionCache after WriteProcessMemory While not calling this will work on native x64 machines as it's likely a no-op under x64, it is possible that the function does something under emulated environments. As there is no downside to calling this function, this commit makes the behavior more correct. --- Dalamud.Boot/rewrite_entrypoint.cpp | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/Dalamud.Boot/rewrite_entrypoint.cpp b/Dalamud.Boot/rewrite_entrypoint.cpp index 6ece3665c..3a1672af7 100644 --- a/Dalamud.Boot/rewrite_entrypoint.cpp +++ b/Dalamud.Boot/rewrite_entrypoint.cpp @@ -303,6 +303,7 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ // Overwrite remote process' entry point with a thunk that will load our DLLs and call our trampoline function. last_operation = std::format(L"write_process_memory_or_throw(entrypoint={:X}, {}b)", reinterpret_cast(entrypoint), buffer.size()); write_process_memory_or_throw(hProcess, entrypoint, entrypoint_replacement.data(), entrypoint_replacement.size()); + FlushInstructionCache(hProcess, entrypoint, entrypoint_replacement.size()); return S_OK; } catch (const std::exception& e) { @@ -332,26 +333,6 @@ extern "C" HRESULT WINAPI RewriteRemoteEntryPointW(HANDLE hProcess, const wchar_ } } -static void AbortRewrittenEntryPoint(DWORD err, const std::wstring& clue) { - wchar_t* pwszMsg = nullptr; - FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | - FORMAT_MESSAGE_FROM_SYSTEM | - FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, - err, - MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), - reinterpret_cast(&pwszMsg), - 0, - nullptr); - - if (MessageBoxW(nullptr, std::format( - L"Failed to load Dalamud. Load game without Dalamud(yes) or abort(no)?\n\n{}\n\n{}", - utils::format_win32_error(err), - clue).c_str(), - L"Dalamud.Boot", MB_OK | MB_YESNO) == IDNO) - ExitProcess(-1); -} - /// @brief Entry point function "called" instead of game's original main entry point. /// @param params Parameters set up from RewriteRemoteEntryPoint. extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointParameters & params) { @@ -369,6 +350,7 @@ extern "C" void WINAPI RewrittenEntryPoint_AdjustedStack(RewrittenEntryPointPara // Use WriteProcessMemory instead of memcpy to avoid having to fiddle with VirtualProtect. last_operation = L"restore original entry point"; write_process_memory_or_throw(GetCurrentProcess(), params.pEntrypoint, pOriginalEntryPointBytes, params.entrypointLength); + FlushInstructionCache(GetCurrentProcess(), params.pEntrypoint, params.entrypointLength); hMainThreadContinue = CreateEventW(nullptr, true, false, nullptr); last_operation = L"hMainThreadContinue = CreateEventW"; From f4af8e509b34ddeca5042badfbba3b9437b74888 Mon Sep 17 00:00:00 2001 From: marzent Date: Thu, 15 Feb 2024 00:05:18 +0100 Subject: [PATCH 494/585] make Dalamud handle top-level SEH --- Dalamud.Boot/veh.cpp | 61 +++++++++++++++++++++++----------- Dalamud.Boot/xivfixes.cpp | 45 ------------------------- Dalamud.Boot/xivfixes.h | 1 - Dalamud.Injector/EntryPoint.cs | 5 ++- 4 files changed, 45 insertions(+), 67 deletions(-) diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index fc8689af7..4eeddba88 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -6,6 +6,7 @@ #include "logging.h" #include "utils.h" +#include "hooks.h" #include "crashhandler_shared.h" #include "DalamudStartInfo.h" @@ -24,6 +25,7 @@ PVOID g_veh_handle = nullptr; bool g_veh_do_full_dump = false; +std::optional> g_HookSetUnhandledExceptionFilter; HANDLE g_crashhandler_process = nullptr; HANDLE g_crashhandler_event = nullptr; @@ -143,21 +145,7 @@ static void append_injector_launch_args(std::vector& args) LONG exception_handler(EXCEPTION_POINTERS* ex) { - if (ex->ExceptionRecord->ExceptionCode == 0x12345678) - { - // pass - } - else - { - if (!is_whitelist_exception(ex->ExceptionRecord->ExceptionCode)) - return EXCEPTION_CONTINUE_SEARCH; - - if (!is_ffxiv_address(L"ffxiv_dx11.exe", ex->ContextRecord->Rip) && - !is_ffxiv_address(L"cimgui.dll", ex->ContextRecord->Rip)) - return EXCEPTION_CONTINUE_SEARCH; - } - - // block any other exceptions hitting the veh while the messagebox is open + // block any other exceptions hitting the handler while the messagebox is open const auto lock = std::lock_guard(g_exception_handler_mutex); exception_info exinfo{}; @@ -167,7 +155,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) exinfo.ExceptionRecord = ex->ExceptionRecord ? *ex->ExceptionRecord : EXCEPTION_RECORD{}; const auto time_now = std::chrono::system_clock::now(); auto lifetime = std::chrono::duration_cast( - time_now.time_since_epoch()).count() + time_now.time_since_epoch()).count() - std::chrono::duration_cast( g_time_start.time_since_epoch()).count(); exinfo.nLifetime = lifetime; @@ -178,7 +166,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) if (void* fn; const auto err = static_cast(g_clr->get_function_pointer( L"Dalamud.EntryPoint, Dalamud", L"VehCallback", - L"Dalamud.EntryPoint+VehDelegate, Dalamud", + L"Dalamud.EntryPoint+VehDelegate, Dalamud", nullptr, nullptr, &fn))) { stackTrace = std::format(L"Failed to read stack trace: 0x{:08x}", err); @@ -188,7 +176,7 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) stackTrace = static_cast(fn)(); // Don't free it, as the program's going to be quit anyway } - + exinfo.dwStackTraceLength = static_cast(stackTrace.size()); exinfo.dwTroubleshootingPackDataLength = static_cast(g_startInfo.TroubleshootingPackData.size()); if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &exinfo, static_cast(sizeof exinfo), &written, nullptr) || sizeof exinfo != written) @@ -217,13 +205,44 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) return EXCEPTION_CONTINUE_SEARCH; } +LONG WINAPI structured_exception_handler(EXCEPTION_POINTERS* ex) +{ + return exception_handler(ex); +} + +LONG WINAPI vectored_exception_handler(EXCEPTION_POINTERS* ex) +{ + if (ex->ExceptionRecord->ExceptionCode == 0x12345678) + { + // pass + } + else + { + if (!is_whitelist_exception(ex->ExceptionRecord->ExceptionCode)) + return EXCEPTION_CONTINUE_SEARCH; + + 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_handler(ex); +} + bool veh::add_handler(bool doFullDump, const std::string& workingDirectory) { if (g_veh_handle) return false; - g_veh_handle = AddVectoredExceptionHandler(1, exception_handler); - SetUnhandledExceptionFilter(nullptr); + g_veh_handle = AddVectoredExceptionHandler(TRUE, vectored_exception_handler); + + g_HookSetUnhandledExceptionFilter.emplace("kernel32.dll!SetUnhandledExceptionFilter (lpTopLevelExceptionFilter)", "kernel32.dll", "SetUnhandledExceptionFilter", 0); + g_HookSetUnhandledExceptionFilter->set_detour([](LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) -> LPTOP_LEVEL_EXCEPTION_FILTER + { + logging::I("Overwriting UnhandledExceptionFilter from {} to {}", reinterpret_cast(lpTopLevelExceptionFilter), reinterpret_cast(structured_exception_handler)); + return g_HookSetUnhandledExceptionFilter->call_original(structured_exception_handler); + }); + SetUnhandledExceptionFilter(structured_exception_handler); g_veh_do_full_dump = doFullDump; g_time_start = std::chrono::system_clock::now(); @@ -355,6 +374,8 @@ bool veh::remove_handler() if (g_veh_handle && RemoveVectoredExceptionHandler(g_veh_handle) != 0) { g_veh_handle = nullptr; + g_HookSetUnhandledExceptionFilter.reset(); + SetUnhandledExceptionFilter(nullptr); return true; } return false; diff --git a/Dalamud.Boot/xivfixes.cpp b/Dalamud.Boot/xivfixes.cpp index e16dd6e5a..39cce53c9 100644 --- a/Dalamud.Boot/xivfixes.cpp +++ b/Dalamud.Boot/xivfixes.cpp @@ -513,50 +513,6 @@ void xivfixes::backup_userdata_save(bool bApply) { } } -void xivfixes::clr_failfast_hijack(bool bApply) -{ - static const char* LogTag = "[xivfixes:clr_failfast_hijack]"; - static std::optional> s_HookClrFatalError; - static std::optional> s_HookSetUnhandledExceptionFilter; - - if (bApply) - { - if (!g_startInfo.BootEnabledGameFixes.contains("clr_failfast_hijack")) { - logging::I("{} Turned off via environment variable.", LogTag); - return; - } - - s_HookClrFatalError.emplace("kernel32.dll!RaiseFailFastException (import, backup_userdata_save)", "kernel32.dll", "RaiseFailFastException", 0); - s_HookSetUnhandledExceptionFilter.emplace("kernel32.dll!SetUnhandledExceptionFilter (lpTopLevelExceptionFilter)", "kernel32.dll", "SetUnhandledExceptionFilter", 0); - - s_HookClrFatalError->set_detour([](PEXCEPTION_RECORD pExceptionRecord, - _In_opt_ PCONTEXT pContextRecord, - _In_ DWORD dwFlags) - { - MessageBoxW(nullptr, L"An error in a Dalamud plugin was detected and the game cannot continue.\n\nPlease take a screenshot of this error message and let us know about it.", L"Dalamud", MB_OK | MB_ICONERROR); - - return s_HookClrFatalError->call_original(pExceptionRecord, pContextRecord, dwFlags); - }); - - s_HookSetUnhandledExceptionFilter->set_detour([](LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter) -> LPTOP_LEVEL_EXCEPTION_FILTER - { - logging::I("{} SetUnhandledExceptionFilter", LogTag); - return nullptr; - }); - - logging::I("{} Enable", LogTag); - } - else - { - if (s_HookClrFatalError) { - logging::I("{} Disable ClrFatalError", LogTag); - s_HookClrFatalError.reset(); - s_HookSetUnhandledExceptionFilter.reset(); - } - } -} - - void xivfixes::prevent_icmphandle_crashes(bool bApply) { static const char* LogTag = "[xivfixes:prevent_icmphandle_crashes]"; @@ -598,7 +554,6 @@ void xivfixes::apply_all(bool bApply) { { "disable_game_openprocess_access_check", &disable_game_openprocess_access_check }, { "redirect_openprocess", &redirect_openprocess }, { "backup_userdata_save", &backup_userdata_save }, - { "clr_failfast_hijack", &clr_failfast_hijack }, { "prevent_icmphandle_crashes", &prevent_icmphandle_crashes } } ) { diff --git a/Dalamud.Boot/xivfixes.h b/Dalamud.Boot/xivfixes.h index 701913c88..f534ad7dd 100644 --- a/Dalamud.Boot/xivfixes.h +++ b/Dalamud.Boot/xivfixes.h @@ -6,7 +6,6 @@ namespace xivfixes { void disable_game_openprocess_access_check(bool bApply); void redirect_openprocess(bool bApply); void backup_userdata_save(bool bApply); - void clr_failfast_hijack(bool bApply); void prevent_icmphandle_crashes(bool bApply); void apply_all(bool bApply); diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 9e2b95657..c784ec1d1 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -394,7 +394,10 @@ namespace Dalamud.Injector startInfo.BootShowConsole = args.Contains("--console"); startInfo.BootEnableEtw = args.Contains("--etw"); startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName); - startInfo.BootEnabledGameFixes = new List { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes" }; + startInfo.BootEnabledGameFixes = new List { + "prevent_devicechange_crashes", "disable_game_openprocess_access_check", + "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes", + }; startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0; From 1020f8a85bea7fe37c33f2f856cae03895bf5749 Mon Sep 17 00:00:00 2001 From: marzent Date: Thu, 15 Feb 2024 23:39:08 +0100 Subject: [PATCH 495/585] add progress dialog to crash handler --- DalamudCrashHandler/DalamudCrashHandler.cpp | 49 +++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 4e2f01708..258ec923d 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #pragma comment(lib, "comctl32.lib") @@ -670,6 +671,7 @@ int main() { std::filesystem::path assetDir, logDir; std::optional> launcherArgs; auto fullDump = false; + CoInitializeEx(nullptr, COINIT_MULTITHREADED); std::vector args; if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) { @@ -753,6 +755,35 @@ int main() { std::cout << "Crash triggered" << std::endl; + std::cout << "Creating progress window" << std::endl; + IProgressDialog* pProgressDialog = NULL; + if (SUCCEEDED(CoCreateInstance(CLSID_ProgressDialog, NULL, CLSCTX_ALL, IID_IProgressDialog, (void**)&pProgressDialog)) && pProgressDialog) { + pProgressDialog->SetTitle(L"Dalamud"); + pProgressDialog->SetLine(1, L"The game has crashed", FALSE, NULL); + pProgressDialog->SetLine(2, L"Dalamud is collecting further information", FALSE, NULL); + pProgressDialog->SetLine(3, L"Refreshing Game Module List", FALSE, NULL); + pProgressDialog->StartProgressDialog(NULL, NULL, PROGDLG_MARQUEEPROGRESS | PROGDLG_NOCANCEL | PROGDLG_NOMINIMIZE, NULL); + IOleWindow* pOleWindow; + HRESULT hr = pProgressDialog->QueryInterface(IID_IOleWindow, (LPVOID*)&pOleWindow); + if (SUCCEEDED(hr)) + { + HWND hwndProgressDialog = NULL; + hr = pOleWindow->GetWindow(&hwndProgressDialog); + if (SUCCEEDED(hr)) + { + SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + } + + pOleWindow->Release(); + } + + } + else { + std::cerr << "Failed to create progress window" << std::endl; + pProgressDialog = NULL; + } + auto shutup_mutex = CreateMutex(NULL, false, L"DALAMUD_CRASHES_NO_MORE"); bool shutup = false; if (shutup_mutex == NULL && GetLastError() == ERROR_ALREADY_EXISTS) @@ -791,6 +822,9 @@ int main() { std::wcerr << std::format(L"SymInitialize error: 0x{:x}", GetLastError()) << std::endl; } + if (pProgressDialog) + pProgressDialog->SetLine(3, L"Reading troubleshooting data", FALSE, NULL); + std::wstring stackTrace(exinfo.dwStackTraceLength, L'\0'); if (exinfo.dwStackTraceLength) { if (DWORD read; !ReadFile(hPipeRead, &stackTrace[0], 2 * exinfo.dwStackTraceLength, &read, nullptr)) { @@ -805,6 +839,9 @@ int main() { } } + if (pProgressDialog) + pProgressDialog->SetLine(3, fullDump ? L"Creating full dump" : L"Creating minidump", FALSE, NULL); + SYSTEMTIME st; GetLocalTime(&st); const auto dalamudLogPath = logDir.empty() ? std::filesystem::path() : logDir / L"Dalamud.log"; @@ -857,6 +894,9 @@ int main() { log << L"System Time: " << std::chrono::system_clock::now() << std::endl; log << L"\n" << stackTrace << std::endl; + if (pProgressDialog) + pProgressDialog->SetLine(3, L"Refreshing Module List", FALSE, NULL); + SymRefreshModuleList(GetCurrentProcess()); print_exception_info(exinfo.hThreadHandle, exinfo.ExceptionPointers, exinfo.ContextRecord, log); const auto window_log_str = log.str(); @@ -993,11 +1033,20 @@ int main() { }; config.lpCallbackData = reinterpret_cast(&callback); + if (pProgressDialog) + pProgressDialog->SetLine(3, L"Submitting Metrics", FALSE, NULL); + if (submitThread.joinable()) { submitThread.join(); submitThread = {}; } + if (pProgressDialog) { + pProgressDialog->StopProgressDialog(); + pProgressDialog->Release(); + pProgressDialog = NULL; + } + if (shutup) { TerminateProcess(g_hProcess, exinfo.ExceptionRecord.ExceptionCode); return 0; From 6497c626222b4a639d18f59591609aca40f3560f Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 17 Feb 2024 01:16:21 +0900 Subject: [PATCH 496/585] Change MemoryHelper to allocate less (#1657) * Change MemoryHelper to allocate less * Use StringBuilder pool for ReadSeStringAsString * fix * Use CreateReadOnlySpanFromNullTerminated where possible --- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 2 +- Dalamud/Interface/Internal/UiDebug.cs | 27 +- Dalamud/Memory/MemoryHelper.cs | 520 ++++++++++++++++------ 3 files changed, 400 insertions(+), 149 deletions(-) diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 4eb605a76..30fab6b1b 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -107,7 +107,7 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private IntPtr AtkUnitBaseReceiveGlobalEventDetour(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* arg) { - // Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", Marshal.PtrToStringAnsi(new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus); + // Log.Information("{0}: cmd#{1} a3#{2} - HasAnyFocus:{3}", MemoryHelper.ReadSeStringAsString(out _, new IntPtr(thisPtr->Name)), cmd, a3, WindowSystem.HasAnyWindowSystemFocus); // "SendHotkey" // 3 == Close diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 14f062e01..d93b90799 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -6,6 +6,7 @@ using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Game.Gui; using Dalamud.Interface.Utility; +using Dalamud.Memory; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; @@ -82,7 +83,7 @@ internal unsafe class UiDebug private void DrawUnitBase(AtkUnitBase* atkUnitBase) { var isVisible = (atkUnitBase->Flags & 0x20) == 0x20; - var addonName = Marshal.PtrToStringAnsi(new IntPtr(atkUnitBase->Name)); + var addonName = MemoryHelper.ReadSeStringAsString(out _, new IntPtr(atkUnitBase->Name)); var agent = Service.Get().FindAgentInterface(atkUnitBase); ImGui.Text($"{addonName}"); @@ -204,7 +205,7 @@ internal unsafe class UiDebug { case NodeType.Text: var textNode = (AtkTextNode*)node; - ImGui.Text($"text: {Marshal.PtrToStringAnsi(new IntPtr(textNode->NodeText.StringPtr))}"); + ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)textNode->NodeText.StringPtr)}"); ImGui.InputText($"Replace Text##{(ulong)textNode:X}", new IntPtr(textNode->NodeText.StringPtr), (uint)textNode->NodeText.BufSize); @@ -231,7 +232,7 @@ internal unsafe class UiDebug break; case NodeType.Counter: var counterNode = (AtkCounterNode*)node; - ImGui.Text($"text: {Marshal.PtrToStringAnsi(new IntPtr(counterNode->NodeText.StringPtr))}"); + ImGui.Text($"text: {MemoryHelper.ReadSeStringAsString(out _, (nint)counterNode->NodeText.StringPtr)}"); break; case NodeType.Image: var imageNode = (AtkImageNode*)node; @@ -250,8 +251,8 @@ internal unsafe class UiDebug { var texFileNameStdString = &textureInfo->AtkTexture.Resource->TexFileResourceHandle->ResourceHandle.FileName; var texString = texFileNameStdString->Length < 16 - ? Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->Buffer) - : Marshal.PtrToStringAnsi((IntPtr)texFileNameStdString->BufferPtr); + ? MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->Buffer) + : MemoryHelper.ReadSeStringAsString(out _, (nint)texFileNameStdString->BufferPtr); ImGui.Text($"texture path: {texString}"); var kernelTexture = textureInfo->AtkTexture.Resource->KernelTextureObject; @@ -352,13 +353,13 @@ internal unsafe class UiDebug { case ComponentType.TextInput: var textInputComponent = (AtkComponentTextInput*)compNode->Component; - ImGui.Text($"InputBase Text1: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}"); - ImGui.Text($"InputBase Text2: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}"); - ImGui.Text($"Text1: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText1.StringPtr))}"); - ImGui.Text($"Text2: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText2.StringPtr))}"); - ImGui.Text($"Text3: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText3.StringPtr))}"); - ImGui.Text($"Text4: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText4.StringPtr))}"); - ImGui.Text($"Text5: {Marshal.PtrToStringAnsi(new IntPtr(textInputComponent->UnkText5.StringPtr))}"); + ImGui.Text($"InputBase Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText1.StringPtr))}"); + ImGui.Text($"InputBase Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->AtkComponentInputBase.UnkText2.StringPtr))}"); + ImGui.Text($"Text1: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText1.StringPtr))}"); + ImGui.Text($"Text2: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText2.StringPtr))}"); + ImGui.Text($"Text3: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText3.StringPtr))}"); + ImGui.Text($"Text4: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText4.StringPtr))}"); + ImGui.Text($"Text5: {MemoryHelper.ReadSeStringAsString(out _, new IntPtr(textInputComponent->UnkText5.StringPtr))}"); break; } @@ -474,7 +475,7 @@ internal unsafe class UiDebug foundSelected = true; } - var name = Marshal.PtrToStringAnsi(new IntPtr(unitBase->Name)); + var name = MemoryHelper.ReadSeStringAsString(out _, new IntPtr(unitBase->Name)); if (searching) { if (name == null || !name.ToLower().Contains(searchStr.ToLower())) continue; diff --git a/Dalamud/Memory/MemoryHelper.cs b/Dalamud/Memory/MemoryHelper.cs index 552817646..09f45e2d3 100644 --- a/Dalamud/Memory/MemoryHelper.cs +++ b/Dalamud/Memory/MemoryHelper.cs @@ -1,15 +1,21 @@ -using System; +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Memory.Exceptions; + using FFXIVClientStructs.FFXIV.Client.System.Memory; using FFXIVClientStructs.FFXIV.Client.System.String; +using Microsoft.Extensions.ObjectPool; + using static Dalamud.NativeFunctions; +using LPayloadType = Lumina.Text.Payloads.PayloadType; +using LSeString = Lumina.Text.SeString; + // Heavily inspired from Reloaded (https://github.com/Reloaded-Project/Reloaded.Memory) namespace Dalamud.Memory; @@ -19,6 +25,47 @@ namespace Dalamud.Memory; /// public static unsafe class MemoryHelper { + private static readonly ObjectPool StringBuilderPool = + ObjectPool.Create(new StringBuilderPooledObjectPolicy()); + + #region Cast + + /// Casts the given memory address as the reference to the live object. + /// The memory address. + /// The unmanaged type. + /// The reference to the live object. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T Cast(nint memoryAddress) where T : unmanaged => ref *(T*)memoryAddress; + + /// Casts the given memory address as the span of the live object(s). + /// The memory address. + /// The number of items. + /// The unmanaged type. + /// The span containing reference to the live object(s). + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Span Cast(nint memoryAddress, int length) where T : unmanaged => + new((void*)memoryAddress, length); + + /// Casts the given memory address as the span of the live object(s), until it encounters a zero. + /// The memory address. + /// The maximum number of items. + /// The unmanaged type. + /// The span containing reference to the live object(s). + /// If is byte or char and is not + /// specified, consider using or + /// . + public static Span CastNullTerminated(nint memoryAddress, int maxLength = int.MaxValue) + where T : unmanaged, IEquatable + { + var typedPointer = (T*)memoryAddress; + var length = 0; + while (length < maxLength && !default(T).Equals(*typedPointer++)) + length++; + return new((void*)memoryAddress, length); + } + + #endregion + #region Read ///
@@ -27,7 +74,9 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The memory address to read from. /// The read in struct. - public static T Read(IntPtr memoryAddress) where T : unmanaged + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Read(nint memoryAddress) where T : unmanaged => Read(memoryAddress, false); /// @@ -37,12 +86,13 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// Set this to true to enable struct marshalling. /// The read in struct. - public static T Read(IntPtr memoryAddress, bool marshal) - { - return marshal - ? Marshal.PtrToStructure(memoryAddress) - : Unsafe.Read((void*)memoryAddress); - } + /// If you do not need to make a copy and is false, + /// use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Read(nint memoryAddress, bool marshal) => + marshal + ? Marshal.PtrToStructure(memoryAddress) + : Unsafe.Read((void*)memoryAddress); /// /// Reads a byte array from a specified memory address. @@ -50,12 +100,9 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of bytes to read starting from the memoryAddress. /// The read in byte array. - public static byte[] ReadRaw(IntPtr memoryAddress, int length) - { - var value = new byte[length]; - Marshal.Copy(memoryAddress, value, 0, value.Length); - return value; - } + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] ReadRaw(nint memoryAddress, int length) => Cast(memoryAddress, length).ToArray(); /// /// Reads a generic type array from a specified memory address. @@ -64,8 +111,10 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of array items to read. /// The read in struct array. - public static T[] Read(IntPtr memoryAddress, int arrayLength) where T : unmanaged - => Read(memoryAddress, arrayLength, false); + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T[] Read(nint memoryAddress, int arrayLength) where T : unmanaged + => Cast(memoryAddress, arrayLength).ToArray(); /// /// Reads a generic type array from a specified memory address. @@ -75,16 +124,18 @@ public static unsafe class MemoryHelper /// The amount of array items to read. /// Set this to true to enable struct marshalling. /// The read in struct array. - public static T[] Read(IntPtr memoryAddress, int arrayLength, bool marshal) + /// If you do not need to make a copy and is false, + /// use instead. + public static T[] Read(nint memoryAddress, int arrayLength, bool marshal) { var structSize = SizeOf(marshal); var value = new T[arrayLength]; for (var i = 0; i < arrayLength; i++) { - var address = memoryAddress + (structSize * i); - Read(address, out T result, marshal); + Read(memoryAddress, out T result, marshal); value[i] = result; + memoryAddress += structSize; } return value; @@ -95,16 +146,10 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in byte array. - public static unsafe byte[] ReadRawNullTerminated(IntPtr memoryAddress) - { - var byteCount = 0; - while (*(byte*)(memoryAddress + byteCount) != 0x00) - { - byteCount++; - } - - return ReadRaw(memoryAddress, byteCount); - } + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] ReadRawNullTerminated(nint memoryAddress) => + MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress).ToArray(); #endregion @@ -116,7 +161,9 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The memory address to read from. /// Local variable to receive the read in struct. - public static void Read(IntPtr memoryAddress, out T value) where T : unmanaged + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Read(nint memoryAddress, out T value) where T : unmanaged => value = Read(memoryAddress); /// @@ -126,7 +173,10 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// Local variable to receive the read in struct. /// Set this to true to enable struct marshalling. - public static void Read(IntPtr memoryAddress, out T value, bool marshal) + /// If you do not need to make a copy and is false, + /// use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Read(nint memoryAddress, out T value, bool marshal) => value = Read(memoryAddress, marshal); /// @@ -135,7 +185,9 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of bytes to read starting from the memoryAddress. /// Local variable to receive the read in bytes. - public static void ReadRaw(IntPtr memoryAddress, int length, out byte[] value) + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadRaw(nint memoryAddress, int length, out byte[] value) => value = ReadRaw(memoryAddress, length); /// @@ -145,7 +197,9 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of array items to read. /// The read in struct array. - public static void Read(IntPtr memoryAddress, int arrayLength, out T[] value) where T : unmanaged + /// If you do not need to make a copy, use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Read(nint memoryAddress, int arrayLength, out T[] value) where T : unmanaged => value = Read(memoryAddress, arrayLength); /// @@ -156,7 +210,10 @@ public static unsafe class MemoryHelper /// The amount of array items to read. /// Set this to true to enable struct marshalling. /// The read in struct array. - public static void Read(IntPtr memoryAddress, int arrayLength, bool marshal, out T[] value) + /// If you do not need to make a copy and is false, + /// use instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Read(nint memoryAddress, int arrayLength, bool marshal, out T[] value) => value = Read(memoryAddress, arrayLength, marshal); #endregion @@ -184,15 +241,27 @@ public static unsafe class MemoryHelper var length = 0; while (length < maxLength && pmem[length] != 0) length++; - + var mem = new Span(pmem, length); var memCharCount = encoding.GetCharCount(mem); if (memCharCount != charSpan.Length) return false; - Span chars = stackalloc char[memCharCount]; - encoding.GetChars(mem, chars); - return charSpan.SequenceEqual(chars); + if (memCharCount < 1024) + { + Span chars = stackalloc char[memCharCount]; + encoding.GetChars(mem, chars); + return charSpan.SequenceEqual(chars); + } + else + { + var rented = ArrayPool.Shared.Rent(memCharCount); + var chars = rented.AsSpan(0, memCharCount); + encoding.GetChars(mem, chars); + var equals = charSpan.SequenceEqual(chars); + ArrayPool.Shared.Return(rented); + return equals; + } } /// @@ -203,8 +272,9 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in string. - public static string ReadStringNullTerminated(IntPtr memoryAddress) - => ReadStringNullTerminated(memoryAddress, Encoding.UTF8); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ReadStringNullTerminated(nint memoryAddress) + => Encoding.UTF8.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress)); /// /// Read a string with the given encoding from a specified memory address. @@ -215,10 +285,25 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The encoding to use to decode the string. /// The read in string. - public static string ReadStringNullTerminated(IntPtr memoryAddress, Encoding encoding) + public static string ReadStringNullTerminated(nint memoryAddress, Encoding encoding) { - var buffer = ReadRawNullTerminated(memoryAddress); - return encoding.GetString(buffer); + switch (encoding) + { + case UTF8Encoding: + case var _ when encoding.IsSingleByte: + return encoding.GetString(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress)); + case UnicodeEncoding: + // Note that it may be in little or big endian, so using `new string(...)` is not always correct. + return encoding.GetString( + MemoryMarshal.Cast( + MemoryMarshal.CreateReadOnlySpanFromNullTerminated((char*)memoryAddress))); + case UTF32Encoding: + return encoding.GetString(MemoryMarshal.Cast(CastNullTerminated(memoryAddress))); + default: + // For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a + // non-null character, then this branch can be merged with UTF8Encoding one. + return encoding.GetString(ReadRawNullTerminated(memoryAddress)); + } } /// @@ -228,10 +313,12 @@ public static unsafe class MemoryHelper /// Attention! If this is an , use the applicable helper methods to decode. /// /// The memory address to read from. - /// The maximum length of the string. + /// The maximum number of bytes to read. + /// Note that this is NOT the maximum length of the returned string. /// The read in string. - public static string ReadString(IntPtr memoryAddress, int maxLength) - => ReadString(memoryAddress, Encoding.UTF8, maxLength); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string ReadString(nint memoryAddress, int maxLength) + => Encoding.UTF8.GetString(CastNullTerminated(memoryAddress, maxLength)); /// /// Read a string with the given encoding from a specified memory address. @@ -241,18 +328,32 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The encoding to use to decode the string. - /// The maximum length of the string. + /// The maximum number of bytes to read. + /// Note that this is NOT the maximum length of the returned string. /// The read in string. - public static string ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength) + public static string ReadString(nint memoryAddress, Encoding encoding, int maxLength) { if (maxLength <= 0) return string.Empty; - ReadRaw(memoryAddress, maxLength, out var buffer); - - var data = encoding.GetString(buffer); - var eosPos = data.IndexOf('\0'); - return eosPos >= 0 ? data.Substring(0, eosPos) : data; + switch (encoding) + { + case UTF8Encoding: + case var _ when encoding.IsSingleByte: + return encoding.GetString(CastNullTerminated(memoryAddress, maxLength)); + case UnicodeEncoding: + return encoding.GetString( + MemoryMarshal.Cast(CastNullTerminated(memoryAddress, maxLength / 2))); + case UTF32Encoding: + return encoding.GetString( + MemoryMarshal.Cast(CastNullTerminated(memoryAddress, maxLength / 4))); + default: + // For correctness' sake; if there does not exist an encoding which will contain a (byte)0 for a + // non-null character, then this branch can be merged with UTF8Encoding one. + var data = encoding.GetString(Cast(memoryAddress, maxLength)); + var eosPos = data.IndexOf('\0'); + return eosPos >= 0 ? data[..eosPos] : data; + } } /// @@ -260,11 +361,9 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in string. - public static SeString ReadSeStringNullTerminated(IntPtr memoryAddress) - { - var buffer = ReadRawNullTerminated(memoryAddress); - return SeString.Parse(buffer); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SeString ReadSeStringNullTerminated(nint memoryAddress) => + SeString.Parse(MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)memoryAddress)); /// /// Read an SeString from a specified memory address. @@ -272,40 +371,165 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The maximum length of the string. /// The read in string. - public static SeString ReadSeString(IntPtr memoryAddress, int maxLength) - { - ReadRaw(memoryAddress, maxLength, out var buffer); - - var eos = Array.IndexOf(buffer, (byte)0); - if (eos < 0) - { - return SeString.Parse(buffer); - } - else - { - var newBuffer = new byte[eos]; - Buffer.BlockCopy(buffer, 0, newBuffer, 0, eos); - return SeString.Parse(newBuffer); - } - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SeString ReadSeString(nint memoryAddress, int maxLength) => + // Note that a valid SeString never contains a null character, other than for the sequence terminator purpose. + SeString.Parse(CastNullTerminated(memoryAddress, maxLength)); /// /// Read an SeString from a specified Utf8String structure. /// /// The memory address to read from. /// The read in string. - public static unsafe SeString ReadSeString(Utf8String* utf8String) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static SeString ReadSeString(Utf8String* utf8String) => + utf8String == null ? string.Empty : SeString.Parse(utf8String->AsSpan()); + + /// + /// Reads an SeString from a specified memory address, and extracts the outermost string.
+ /// If the SeString is malformed, behavior is undefined. + ///
+ /// Whether the SeString contained a non-represented payload. + /// The memory address to read from. + /// The maximum length of the string. + /// Stop reading on encountering the first non-represented payload. + /// What payloads are represented via this function may change. + /// Replacement for non-represented payloads. + /// The read in string. + public static string ReadSeStringAsString( + out bool containsNonRepresentedPayload, + nint memoryAddress, + int maxLength = int.MaxValue, + bool stopOnFirstNonRepresentedPayload = false, + string nonRepresentedPayloadReplacement = "*") { - if (utf8String == null) - return string.Empty; + var sb = StringBuilderPool.Get(); + sb.EnsureCapacity(maxLength = CastNullTerminated(memoryAddress, maxLength).Length); - var ptr = utf8String->StringPtr; - if (ptr == null) - return string.Empty; + // 1 utf-8 codepoint can spill up to 2 characters. + Span tmp = stackalloc char[2]; - var len = Math.Max(utf8String->BufUsed, utf8String->StringLength); + var pin = (byte*)memoryAddress; + containsNonRepresentedPayload = false; + while (*pin != 0 && maxLength > 0) + { + if (*pin != LSeString.StartByte) + { + var len = *pin switch + { + < 0x80 => 1, + >= 0b11000000 and <= 0b11011111 => 2, + >= 0b11100000 and <= 0b11101111 => 3, + >= 0b11110000 and <= 0b11110111 => 4, + _ => 0, + }; + if (len == 0 || len > maxLength) + break; - return ReadSeString((IntPtr)ptr, (int)len); + var numChars = Encoding.UTF8.GetChars(new(pin, len), tmp); + sb.Append(tmp[..numChars]); + pin += len; + maxLength -= len; + continue; + } + + // Start byte + ++pin; + --maxLength; + + // Payload type + var payloadType = (LPayloadType)(*pin++); + + // Payload length + if (!ReadIntExpression(ref pin, ref maxLength, out var expressionLength)) + break; + if (expressionLength > maxLength) + break; + pin += expressionLength; + maxLength -= unchecked((int)expressionLength); + + // End byte + if (*pin++ != LSeString.EndByte) + break; + --maxLength; + + switch (payloadType) + { + case LPayloadType.NewLine: + sb.AppendLine(); + break; + case LPayloadType.Hyphen: + sb.Append('–'); + break; + case LPayloadType.SoftHyphen: + sb.Append('\u00AD'); + break; + default: + sb.Append(nonRepresentedPayloadReplacement); + containsNonRepresentedPayload = true; + if (stopOnFirstNonRepresentedPayload) + maxLength = 0; + break; + } + } + + var res = sb.ToString(); + StringBuilderPool.Return(sb); + return res; + + static bool ReadIntExpression(ref byte* p, ref int maxLength, out uint value) + { + if (maxLength <= 0) + { + value = 0; + return false; + } + + var typeByte = *p++; + --maxLength; + + switch (typeByte) + { + case > 0 and < 0xD0: + value = (uint)typeByte - 1; + return true; + case >= 0xF0 and <= 0xFE: + ++typeByte; + value = 0u; + if ((typeByte & 8) != 0) + { + if (maxLength <= 0 || *p == 0) + return false; + value |= (uint)*p++ << 24; + } + + if ((typeByte & 4) != 0) + { + if (maxLength <= 0 || *p == 0) + return false; + value |= (uint)*p++ << 16; + } + + if ((typeByte & 2) != 0) + { + if (maxLength <= 0 || *p == 0) + return false; + value |= (uint)*p++ << 8; + } + + if ((typeByte & 1) != 0) + { + if (maxLength <= 0 || *p == 0) + return false; + value |= *p++; + } + + return true; + default: + value = 0; + return false; + } + } } #endregion @@ -320,7 +544,8 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in string. - public static void ReadStringNullTerminated(IntPtr memoryAddress, out string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadStringNullTerminated(nint memoryAddress, out string value) => value = ReadStringNullTerminated(memoryAddress); /// @@ -332,7 +557,8 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The encoding to use to decode the string. /// The read in string. - public static void ReadStringNullTerminated(IntPtr memoryAddress, Encoding encoding, out string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadStringNullTerminated(nint memoryAddress, Encoding encoding, out string value) => value = ReadStringNullTerminated(memoryAddress, encoding); /// @@ -344,7 +570,8 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The read in string. /// The maximum length of the string. - public static void ReadString(IntPtr memoryAddress, out string value, int maxLength) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadString(nint memoryAddress, out string value, int maxLength) => value = ReadString(memoryAddress, maxLength); /// @@ -357,7 +584,8 @@ public static unsafe class MemoryHelper /// The encoding to use to decode the string. /// The maximum length of the string. /// The read in string. - public static void ReadString(IntPtr memoryAddress, Encoding encoding, int maxLength, out string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadString(nint memoryAddress, Encoding encoding, int maxLength, out string value) => value = ReadString(memoryAddress, encoding, maxLength); /// @@ -365,7 +593,8 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in SeString. - public static void ReadSeStringNullTerminated(IntPtr memoryAddress, out SeString value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadSeStringNullTerminated(nint memoryAddress, out SeString value) => value = ReadSeStringNullTerminated(memoryAddress); /// @@ -374,7 +603,8 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The maximum length of the string. /// The read in SeString. - public static void ReadSeString(IntPtr memoryAddress, int maxLength, out SeString value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadSeString(nint memoryAddress, int maxLength, out SeString value) => value = ReadSeString(memoryAddress, maxLength); /// @@ -382,6 +612,7 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in string. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static unsafe void ReadSeString(Utf8String* utf8String, out SeString value) => value = ReadSeString(utf8String); @@ -395,7 +626,8 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The memory address to read from. /// The item to write to the address. - public static void Write(IntPtr memoryAddress, T item) where T : unmanaged + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Write(nint memoryAddress, T item) where T : unmanaged => Write(memoryAddress, item, false); /// @@ -405,7 +637,7 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The item to write to the address. /// Set this to true to enable struct marshalling. - public static void Write(IntPtr memoryAddress, T item, bool marshal) + public static void Write(nint memoryAddress, T item, bool marshal) { if (marshal) Marshal.StructureToPtr(item, memoryAddress, false); @@ -418,10 +650,8 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The bytes to write to memoryAddress. - public static void WriteRaw(IntPtr memoryAddress, byte[] data) - { - Marshal.Copy(data, 0, memoryAddress, data.Length); - } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteRaw(nint memoryAddress, byte[] data) => Marshal.Copy(data, 0, memoryAddress, data.Length); /// /// Writes a generic type array to a specified memory address. @@ -429,7 +659,8 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The memory address to write to. /// The array of items to write to the address. - public static void Write(IntPtr memoryAddress, T[] items) where T : unmanaged + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Write(nint memoryAddress, T[] items) where T : unmanaged => Write(memoryAddress, items, false); /// @@ -439,7 +670,8 @@ public static unsafe class MemoryHelper /// The memory address to write to. /// The array of items to write to the address. /// Set this to true to enable struct marshalling. - public static void Write(IntPtr memoryAddress, T[] items, bool marshal) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Write(nint memoryAddress, T[] items, bool marshal) { var structSize = SizeOf(marshal); @@ -462,7 +694,8 @@ public static unsafe class MemoryHelper /// /// The memory address to write to. /// The string to write. - public static void WriteString(IntPtr memoryAddress, string value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteString(nint memoryAddress, string? value) => WriteString(memoryAddress, value, Encoding.UTF8); /// @@ -474,14 +707,12 @@ public static unsafe class MemoryHelper /// The memory address to write to. /// The string to write. /// The encoding to use. - public static void WriteString(IntPtr memoryAddress, string value, Encoding encoding) + public static void WriteString(nint memoryAddress, string? value, Encoding encoding) { - if (string.IsNullOrEmpty(value)) - return; - - var bytes = encoding.GetBytes(value + '\0'); - - WriteRaw(memoryAddress, bytes); + var ptr = 0; + if (value is not null) + ptr = encoding.GetBytes(value, Cast(memoryAddress, encoding.GetMaxByteCount(value.Length))); + encoding.GetBytes("\0", Cast(memoryAddress + ptr, 4)); } /// @@ -489,7 +720,8 @@ public static unsafe class MemoryHelper /// /// The memory address to write to. /// The SeString to write. - public static void WriteSeString(IntPtr memoryAddress, SeString value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteSeString(nint memoryAddress, SeString? value) { if (value is null) return; @@ -507,15 +739,16 @@ public static unsafe class MemoryHelper /// /// Amount of bytes to be allocated. /// Address to the newly allocated memory. - public static IntPtr Allocate(int length) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint Allocate(int length) { var address = VirtualAlloc( - IntPtr.Zero, - (UIntPtr)length, + nint.Zero, + (nuint)length, AllocationType.Commit | AllocationType.Reserve, MemoryProtection.ExecuteReadWrite); - if (address == IntPtr.Zero) + if (address == nint.Zero) throw new MemoryAllocationException($"Unable to allocate {length} bytes."); return address; @@ -527,7 +760,8 @@ public static unsafe class MemoryHelper /// /// Amount of bytes to be allocated. /// Address to the newly allocated memory. - public static void Allocate(int length, out IntPtr memoryAddress) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Allocate(int length, out nint memoryAddress) => memoryAddress = Allocate(length); /// @@ -535,9 +769,10 @@ public static unsafe class MemoryHelper /// /// The address of the memory to free. /// True if the operation is successful. - public static bool Free(IntPtr memoryAddress) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool Free(nint memoryAddress) { - return VirtualFree(memoryAddress, UIntPtr.Zero, AllocationType.Release); + return VirtualFree(memoryAddress, nuint.Zero, AllocationType.Release); } /// @@ -547,9 +782,9 @@ public static unsafe class MemoryHelper /// The region size for which to change permissions for. /// The new permissions to set. /// The old page permissions. - public static MemoryProtection ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions) + public static MemoryProtection ChangePermission(nint memoryAddress, int length, MemoryProtection newPermissions) { - var result = VirtualProtect(memoryAddress, (UIntPtr)length, newPermissions, out var oldPermissions); + var result = VirtualProtect(memoryAddress, (nuint)length, newPermissions, out var oldPermissions); if (!result) throw new MemoryPermissionException($"Unable to change permissions at 0x{memoryAddress.ToInt64():X} of length {length} and permission {newPermissions} (result={result})"); @@ -568,7 +803,9 @@ public static unsafe class MemoryHelper /// The region size for which to change permissions for. /// The new permissions to set. /// The old page permissions. - public static void ChangePermission(IntPtr memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ChangePermission( + nint memoryAddress, int length, MemoryProtection newPermissions, out MemoryProtection oldPermissions) => oldPermissions = ChangePermission(memoryAddress, length, newPermissions); /// @@ -580,7 +817,9 @@ public static unsafe class MemoryHelper /// The new permissions to set. /// Set to true to calculate the size of the struct after marshalling instead of before. /// The old page permissions. - public static MemoryProtection ChangePermission(IntPtr memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static MemoryProtection ChangePermission( + nint memoryAddress, ref T baseElement, MemoryProtection newPermissions, bool marshal) => ChangePermission(memoryAddress, SizeOf(marshal), newPermissions); /// @@ -590,7 +829,8 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of bytes to read starting from the memoryAddress. /// The read in bytes. - public static byte[] ReadProcessMemory(IntPtr memoryAddress, int length) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte[] ReadProcessMemory(nint memoryAddress, int length) { var value = new byte[length]; ReadProcessMemory(memoryAddress, ref value); @@ -604,7 +844,8 @@ public static unsafe class MemoryHelper /// The memory address to read from. /// The amount of bytes to read starting from the memoryAddress. /// The read in bytes. - public static void ReadProcessMemory(IntPtr memoryAddress, int length, out byte[] value) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ReadProcessMemory(nint memoryAddress, int length, out byte[] value) => value = ReadProcessMemory(memoryAddress, length); /// @@ -613,12 +854,12 @@ public static unsafe class MemoryHelper /// /// The memory address to read from. /// The read in bytes. - public static void ReadProcessMemory(IntPtr memoryAddress, ref byte[] value) + public static void ReadProcessMemory(nint memoryAddress, ref byte[] value) { unchecked { var length = value.Length; - var result = NativeFunctions.ReadProcessMemory((IntPtr)0xFFFFFFFF, memoryAddress, value, length, out _); + var result = NativeFunctions.ReadProcessMemory((nint)0xFFFFFFFF, memoryAddress, value, length, out _); if (!result) throw new MemoryReadException($"Unable to read memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})"); @@ -635,12 +876,12 @@ public static unsafe class MemoryHelper /// /// The memory address to write to. /// The bytes to write to memoryAddress. - public static void WriteProcessMemory(IntPtr memoryAddress, byte[] data) + public static void WriteProcessMemory(nint memoryAddress, byte[] data) { unchecked { var length = data.Length; - var result = NativeFunctions.WriteProcessMemory((IntPtr)0xFFFFFFFF, memoryAddress, data, length, out _); + var result = NativeFunctions.WriteProcessMemory((nint)0xFFFFFFFF, memoryAddress, data, length, out _); if (!result) throw new MemoryWriteException($"Unable to write memory at 0x{memoryAddress.ToInt64():X} of length {length} (result={result})"); @@ -660,6 +901,7 @@ public static unsafe class MemoryHelper /// /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The size of the primitive or struct. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SizeOf() => SizeOf(false); @@ -669,6 +911,7 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// If set to true; will return the size of an element after marshalling. /// The size of the primitive or struct. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SizeOf(bool marshal) => marshal ? Marshal.SizeOf() : Unsafe.SizeOf(); @@ -678,6 +921,7 @@ public static unsafe class MemoryHelper /// An individual struct type of a class with an explicit StructLayout.LayoutKind attribute. /// The number of array elements present. /// The size of the primitive or struct array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SizeOf(int elementCount) where T : unmanaged => SizeOf() * elementCount; @@ -688,6 +932,7 @@ public static unsafe class MemoryHelper /// The number of array elements present. /// If set to true; will return the size of an element after marshalling. /// The size of the primitive or struct array. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SizeOf(int elementCount, bool marshal) => SizeOf(marshal) * elementCount; @@ -701,9 +946,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - public static IntPtr GameAllocateUi(ulong size, ulong alignment = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint GameAllocateUi(ulong size, ulong alignment = 0) { - return new IntPtr(IMemorySpace.GetUISpace()->Malloc(size, alignment)); + return new nint(IMemorySpace.GetUISpace()->Malloc(size, alignment)); } /// @@ -712,9 +958,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - public static IntPtr GameAllocateDefault(ulong size, ulong alignment = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint GameAllocateDefault(ulong size, ulong alignment = 0) { - return new IntPtr(IMemorySpace.GetDefaultSpace()->Malloc(size, alignment)); + return new nint(IMemorySpace.GetDefaultSpace()->Malloc(size, alignment)); } /// @@ -723,9 +970,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - public static IntPtr GameAllocateAnimation(ulong size, ulong alignment = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint GameAllocateAnimation(ulong size, ulong alignment = 0) { - return new IntPtr(IMemorySpace.GetAnimationSpace()->Malloc(size, alignment)); + return new nint(IMemorySpace.GetAnimationSpace()->Malloc(size, alignment)); } /// @@ -734,9 +982,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - public static IntPtr GameAllocateApricot(ulong size, ulong alignment = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint GameAllocateApricot(ulong size, ulong alignment = 0) { - return new IntPtr(IMemorySpace.GetApricotSpace()->Malloc(size, alignment)); + return new nint(IMemorySpace.GetApricotSpace()->Malloc(size, alignment)); } /// @@ -745,9 +994,10 @@ public static unsafe class MemoryHelper /// Amount of bytes to allocate. /// The alignment of the allocation. /// Pointer to the allocated region. - public static IntPtr GameAllocateSound(ulong size, ulong alignment = 0) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint GameAllocateSound(ulong size, ulong alignment = 0) { - return new IntPtr(IMemorySpace.GetSoundSpace()->Malloc(size, alignment)); + return new nint(IMemorySpace.GetSoundSpace()->Malloc(size, alignment)); } /// @@ -756,15 +1006,15 @@ public static unsafe class MemoryHelper /// The memory you are freeing must be allocated with game allocators. /// Position at which the memory to be freed is located. /// Amount of bytes to free. - public static void GameFree(ref IntPtr ptr, ulong size) + public static void GameFree(ref nint ptr, ulong size) { - if (ptr == IntPtr.Zero) + if (ptr == nint.Zero) { return; } IMemorySpace.Free((void*)ptr, size); - ptr = IntPtr.Zero; + ptr = nint.Zero; } #endregion From 307f0fcbe834f16d879f1d58014ee6f0ec3a2771 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 17 Feb 2024 01:19:42 +0900 Subject: [PATCH 497/585] Warn if font files' hashes are unexpected (#1659) --- .../Notifications/NotificationManager.cs | 68 +++++++- .../Internals/FontAtlasFactory.cs | 165 +++++++++++++++++- 2 files changed, 216 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs index 67ad3ee8f..34e07be8f 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Numerics; using Dalamud.Interface.Colors; +using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -68,15 +69,22 @@ internal class NotificationManager : IServiceType /// The title of the notification. /// The type of the notification. /// The time the notification should be displayed for. - public void AddNotification(string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = NotifyDefaultDismiss) + /// The added notification. + public Notification AddNotification( + string content, + string? title = null, + NotificationType type = NotificationType.None, + uint msDelay = NotifyDefaultDismiss) { - this.notifications.Add(new Notification + var n = new Notification { Content = content, Title = title, NotificationType = type, DurationMs = msDelay, - }); + }; + this.notifications.Add(n); + return n; } /// @@ -97,6 +105,10 @@ internal class NotificationManager : IServiceType continue; } + using var pushedFont = tn.UseMonospaceFont + ? Service.Get().MonoFontHandle?.Push() + : null; + var opacity = tn.GetFadePercent(); var iconColor = tn.Color; @@ -107,8 +119,12 @@ internal class NotificationManager : IServiceType ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowBgAlpha(opacity); ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One); - ImGui.Begin(windowName, NotifyToastFlags); + if (tn.Actions.Count == 0) + ImGui.Begin(windowName, NotifyToastFlags); + else + ImGui.Begin(windowName, NotifyToastFlags & ~ImGuiWindowFlags.NoInputs); + ImGui.PushID(tn.NotificationId); ImGui.PushTextWrapPos(viewportSize.X / 3.0f); var wasTitleRendered = false; @@ -162,10 +178,22 @@ internal class NotificationManager : IServiceType ImGui.TextUnformatted(tn.Content); } + foreach (var (caption, action) in tn.Actions) + { + if (ImGui.Button(caption)) + action.InvokeSafely(); + ImGui.SameLine(); + } + + // break ImGui.SameLine(); + ImGui.TextUnformatted(string.Empty); + ImGui.PopStyleColor(); ImGui.PopTextWrapPos(); + ImGui.PopID(); + height += ImGui.GetWindowHeight() + NotifyPaddingMessageY; ImGui.End(); @@ -177,6 +205,8 @@ internal class NotificationManager : IServiceType /// internal class Notification { + private static int notificationIdCounter; + /// /// Possible notification phases. /// @@ -203,20 +233,40 @@ internal class NotificationManager : IServiceType Expired, } + /// + /// Gets the notification ID. + /// + internal int NotificationId { get; } = notificationIdCounter++; + /// /// Gets the type of the notification. /// internal NotificationType NotificationType { get; init; } /// - /// Gets the title of the notification. + /// Gets or sets a value indicating whether to force the use of monospace font. /// - internal string? Title { get; init; } + internal bool UseMonospaceFont { get; set; } /// - /// Gets the content of the notification. + /// Gets the action buttons to attach to this notification. /// - internal string Content { get; init; } + internal List<(string Text, Action ClickCallback)> Actions { get; } = new(); + + /// + /// Gets or sets a value indicating whether this notification has been dismissed. + /// + internal bool Dismissed { get; set; } + + /// + /// Gets or sets the title of the notification. + /// + internal string? Title { get; set; } + + /// + /// Gets or sets the content of the notification. + /// + internal string? Content { get; set; } /// /// Gets the duration of the notification in milliseconds. @@ -283,7 +333,7 @@ internal class NotificationManager : IServiceType { var elapsed = (int)this.ElapsedTime.TotalMilliseconds; - if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime) + if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime || this.Dismissed) return Phase.Expired; else if (elapsed > NotifyFadeInOutTime + this.DurationMs) return Phase.FadeOut; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index d3bc976f2..021fc953f 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -1,6 +1,7 @@ using System.Buffers; using System.Collections.Generic; using System.Collections.Immutable; +using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -8,9 +9,13 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; +using Dalamud.Game.ClientState; +using Dalamud.Game.Gui; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -18,7 +23,11 @@ using ImGuiNET; using ImGuiScene; +using Lumina.Data; using Lumina.Data.Files; +using Lumina.Misc; + +using Newtonsoft.Json; using SharpDX; using SharpDX.Direct3D11; @@ -33,9 +42,43 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; internal sealed partial class FontAtlasFactory : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable { + private static readonly Dictionary KnownFontFileDataHashes = new() + { + ["common/font/AXIS_96.fdt"] = 1486212503, + ["common/font/AXIS_12.fdt"] = 1370045105, + ["common/font/AXIS_14.fdt"] = 645957730, + ["common/font/AXIS_18.fdt"] = 899094094, + ["common/font/AXIS_36.fdt"] = 2537048938, + ["common/font/Jupiter_16.fdt"] = 1642196098, + ["common/font/Jupiter_20.fdt"] = 3053628263, + ["common/font/Jupiter_23.fdt"] = 1536194944, + ["common/font/Jupiter_45.fdt"] = 3473589216, + ["common/font/Jupiter_46.fdt"] = 1370962087, + ["common/font/Jupiter_90.fdt"] = 3661420529, + ["common/font/Meidinger_16.fdt"] = 3700692128, + ["common/font/Meidinger_20.fdt"] = 441419856, + ["common/font/Meidinger_40.fdt"] = 203848091, + ["common/font/MiedingerMid_10.fdt"] = 499375313, + ["common/font/MiedingerMid_12.fdt"] = 1925552591, + ["common/font/MiedingerMid_14.fdt"] = 1919733827, + ["common/font/MiedingerMid_18.fdt"] = 1635778987, + ["common/font/MiedingerMid_36.fdt"] = 1190559864, + ["common/font/TrumpGothic_184.fdt"] = 973994576, + ["common/font/TrumpGothic_23.fdt"] = 1967289381, + ["common/font/TrumpGothic_34.fdt"] = 1777971886, + ["common/font/TrumpGothic_68.fdt"] = 1170173741, + ["common/font/font0.tex"] = 514269927, + ["common/font/font1.tex"] = 3616607606, + ["common/font/font2.tex"] = 4166651000, + ["common/font/font3.tex"] = 1264942640, + ["common/font/font4.tex"] = 3534300885, + ["common/font/font5.tex"] = 1041916216, + ["common/font/font6.tex"] = 1247097672, + }; + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); private readonly CancellationTokenSource cancellationTokenSource = new(); - private readonly IReadOnlyDictionary> fdtFiles; + private readonly IReadOnlyDictionary> fdtFiles; private readonly IReadOnlyDictionary[]>> texFiles; private readonly IReadOnlyDictionary> prebakedTextureWraps; private readonly Task defaultGlyphRanges; @@ -67,7 +110,7 @@ internal sealed partial class FontAtlasFactory this.fdtFiles = gffasInfo.ToImmutableDictionary( x => x.Font, - x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); + x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!)); var channelCountsTask = texPaths.ToImmutableDictionary( x => x, x => Task.WhenAll( @@ -79,8 +122,8 @@ internal sealed partial class FontAtlasFactory { unsafe { - using var pin = file.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Length); + using var pin = file.Data.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Data.Length); return fdt.MaxTextureIndex; } }))); @@ -101,11 +144,13 @@ internal sealed partial class FontAtlasFactory { unsafe { - using var pin = file.Result.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Result.Length); + using var pin = file.Result.Data.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Result.Data.Length); return fdt.ToGlyphRanges(); } }); + + Task.Run(this.CheckSanity); } /// @@ -203,12 +248,12 @@ internal sealed partial class FontAtlasFactory /// /// The font family and size. /// The . - public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); + public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas]).Data); /// public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) { - var arr = ExtractResult(this.fdtFiles[gffas]); + var arr = ExtractResult(this.fdtFiles[gffas]).Data; var handle = arr.AsMemory().Pin(); try { @@ -340,6 +385,110 @@ internal sealed partial class FontAtlasFactory } } + private async Task CheckSanity() + { + var invalidFiles = new Dictionary(); + var texFileTasks = new Dictionary>(); + var foundHashes = new Dictionary(); + foreach (var (gffas, fdtTask) in this.fdtFiles) + { + var fontAttr = gffas.GetAttribute()!; + try + { + foundHashes[fontAttr.Path] = Crc32.Get((await fdtTask).Data); + + foreach (var (task, index) in + (await this.texFiles[fontAttr.TexPathFormat]).Select((x, i) => (x, i))) + texFileTasks[fontAttr.TexPathFormat.Format(index)] = task; + } + catch (Exception e) + { + invalidFiles[fontAttr.Path] = e; + } + } + + foreach (var (path, texTask) in texFileTasks) + { + try + { + var hc = default(HashCode); + hc.AddBytes((await texTask).Data); + foundHashes[path] = Crc32.Get((await texTask).Data); + } + catch (Exception e) + { + invalidFiles[path] = e; + } + } + + foreach (var (path, hashCode) in foundHashes) + { + if (!KnownFontFileDataHashes.TryGetValue(path, out var expectedHashCode)) + continue; + if (expectedHashCode != hashCode) + { + invalidFiles[path] = new InvalidDataException( + $"Expected 0x{expectedHashCode:X08}; got 0x{hashCode:X08}"); + } + } + + var dconf = await Service.GetAsync(); + var nm = await Service.GetAsync(); + var intm = (await Service.GetAsync()).Manager; + var ggui = await Service.GetAsync(); + var cstate = await Service.GetAsync(); + + if (invalidFiles.Any()) + { + Log.Warning("Found {n} font related file(s) with unexpected hash code values.", invalidFiles.Count); + foreach (var (path, ex) in invalidFiles) + Log.Warning(ex, "\t=> {path}", path); + Log.Verbose(JsonConvert.SerializeObject(foundHashes)); + if (this.DefaultFontSpec is not SingleFontSpec { FontId: GameFontAndFamilyId }) + return; + + this.Framework.Update += FrameworkOnUpdate; + + void FrameworkOnUpdate(IFramework framework) + { + var charaSelect = ggui.GetAddonByName("CharaSelect", 1); + var charaMake = ggui.GetAddonByName("CharaMake", 1); + var titleDcWorldMap = ggui.GetAddonByName("TitleDCWorldMap", 1); + + // Show notification when TSM is visible, so that user can check whether a font looks bad + if (cstate.IsLoggedIn + || charaMake != IntPtr.Zero + || charaSelect != IntPtr.Zero + || titleDcWorldMap != IntPtr.Zero) + return; + + this.Framework.Update -= FrameworkOnUpdate; + + var n = nm.AddNotification( + "Non-default game fonts detected. If things do not look right, you can use a different font. Running repairs from XIVLauncher is recommended.", + "Modded font warning", + NotificationType.Warning, + 10000); + n.UseMonospaceFont = true; + n.Actions.Add( + ( + "Use Noto Sans", + () => + { + dconf.DefaultFontSpec = + new SingleFontSpec + { + FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium), + SizePx = 17, + }; + dconf.QueueSave(); + intm.RebuildFonts(); + })); + n.Actions.Add(("Dismiss", () => n.Dismissed = true)); + } + } + } + private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) { var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); From 1c059aae7c33003b28664cae10702d8cf96d1cde Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 16 Feb 2024 20:30:07 +0100 Subject: [PATCH 498/585] Update ClientStructs (#1648) 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 4b13c01e2..b12028fbc 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 4b13c01e2f60143f24698a6280255fb1aba7ab63 +Subproject commit b12028fbca6c950db0cb3d10d3185d959067e901 From 4b601f15c75ff564476576f4d2f663f90967d8e7 Mon Sep 17 00:00:00 2001 From: marzent Date: Fri, 16 Feb 2024 22:19:10 +0100 Subject: [PATCH 499/585] Merge pull request #1660 * make extra sure progress dialog crash handler is in the foregroud --- Dalamud.Boot/veh.cpp | 2 ++ DalamudCrashHandler/DalamudCrashHandler.cpp | 1 + 2 files changed, 3 insertions(+) diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 4eeddba88..ade295d02 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -188,6 +188,8 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &g_startInfo.TroubleshootingPackData[0], static_cast(std::span(g_startInfo.TroubleshootingPackData).size_bytes()), &written, nullptr) || std::span(g_startInfo.TroubleshootingPackData).size_bytes() != written) return EXCEPTION_CONTINUE_SEARCH; + AllowSetForegroundWindow(GetProcessId(g_crashhandler_process)); + HANDLE waitHandles[] = { g_crashhandler_process, g_crashhandler_event }; DWORD waitResult = WaitForMultipleObjects(2, waitHandles, FALSE, INFINITE); diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 258ec923d..09e14d722 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -773,6 +773,7 @@ int main() { { SetWindowPos(hwndProgressDialog, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + SetForegroundWindow(hwndProgressDialog); } pOleWindow->Release(); From 24e6bf3dc8f95524ee8c740b1ea9352fb528759e Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Fri, 16 Feb 2024 13:22:15 -0800 Subject: [PATCH 500/585] Remove metric submission on crashes - Rename the progress dialog, add punctuation to messaging --- DalamudCrashHandler/DalamudCrashHandler.cpp | 57 ++------------------- 1 file changed, 4 insertions(+), 53 deletions(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 09e14d722..03c5c29ee 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -758,9 +758,9 @@ int main() { std::cout << "Creating progress window" << std::endl; IProgressDialog* pProgressDialog = NULL; if (SUCCEEDED(CoCreateInstance(CLSID_ProgressDialog, NULL, CLSCTX_ALL, IID_IProgressDialog, (void**)&pProgressDialog)) && pProgressDialog) { - pProgressDialog->SetTitle(L"Dalamud"); - pProgressDialog->SetLine(1, L"The game has crashed", FALSE, NULL); - pProgressDialog->SetLine(2, L"Dalamud is collecting further information", FALSE, NULL); + pProgressDialog->SetTitle(L"Dalamud Crash Handler"); + pProgressDialog->SetLine(1, L"The game has crashed!", FALSE, NULL); + pProgressDialog->SetLine(2, L"Dalamud is collecting further information...", FALSE, NULL); pProgressDialog->SetLine(3, L"Refreshing Game Module List", FALSE, NULL); pProgressDialog->StartProgressDialog(NULL, NULL, PROGDLG_MARQUEEPROGRESS | PROGDLG_NOCANCEL | PROGDLG_NOMINIMIZE, NULL); IOleWindow* pOleWindow; @@ -904,47 +904,6 @@ int main() { print_exception_info_extended(exinfo.ExceptionPointers, exinfo.ContextRecord, log); std::wofstream(logPath) << log.str(); - std::thread submitThread; - if (!getenv("DALAMUD_NO_METRIC")) { - auto url = std::format(L"/Dalamud/Metric/ReportCrash?lt={}&code={:x}", exinfo.nLifetime, exinfo.ExceptionRecord.ExceptionCode); - - submitThread = std::thread([url = std::move(url)] { - const auto hInternet = WinHttpOpen(L"Dalamud Crash Handler/1.0", - WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, - WINHTTP_NO_PROXY_NAME, - WINHTTP_NO_PROXY_BYPASS, 0); - const auto hConnect = !hInternet ? nullptr : WinHttpConnect(hInternet, L"kamori.goats.dev", INTERNET_DEFAULT_HTTP_PORT, 0); - const auto hRequest = !hConnect ? nullptr : WinHttpOpenRequest(hConnect, L"GET", url.c_str(), NULL, WINHTTP_NO_REFERER, - WINHTTP_DEFAULT_ACCEPT_TYPES, - 0); - if (hRequest) WinHttpAddRequestHeaders(hRequest, L"Host: kamori.goats.dev", (ULONG)-1L, WINHTTP_ADDREQ_FLAG_ADD); - const auto bSent = !hRequest ? false : WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, - 0, WINHTTP_NO_REQUEST_DATA, 0, - 0, 0); - - if (!bSent) - std::cerr << std::format("Failed to send metric: 0x{:x}", GetLastError()) << std::endl; - - if (WinHttpReceiveResponse(hRequest, nullptr)) - { - DWORD dwStatusCode = 0; - DWORD dwStatusCodeSize = sizeof(DWORD); - - WinHttpQueryHeaders(hRequest, - WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, - WINHTTP_HEADER_NAME_BY_INDEX, - &dwStatusCode, &dwStatusCodeSize, WINHTTP_NO_HEADER_INDEX); - - if (dwStatusCode != 200) - std::cerr << std::format("Failed to send metric: {}", dwStatusCode) << std::endl; - } - - if (hRequest) WinHttpCloseHandle(hRequest); - if (hConnect) WinHttpCloseHandle(hConnect); - if (hInternet) WinHttpCloseHandle(hInternet); - }); - } - TASKDIALOGCONFIG config = { 0 }; const TASKDIALOG_BUTTON radios[]{ @@ -1033,15 +992,7 @@ int main() { return (*reinterpret_cast(dwRefData))(hwnd, uNotification, wParam, lParam); }; config.lpCallbackData = reinterpret_cast(&callback); - - if (pProgressDialog) - pProgressDialog->SetLine(3, L"Submitting Metrics", FALSE, NULL); - - if (submitThread.joinable()) { - submitThread.join(); - submitThread = {}; - } - + if (pProgressDialog) { pProgressDialog->StopProgressDialog(); pProgressDialog->Release(); From 2afc692eca9dd6f46b5aa26d0f21ecf68f3adeef Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Fri, 16 Feb 2024 15:03:22 -0800 Subject: [PATCH 501/585] Add save tspack button to crash dialog - Require the user manually choose a restart mode before the Restart button can be clicked. - Rename window to Dalamud Crash Handler --- DalamudCrashHandler/DalamudCrashHandler.cpp | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 03c5c29ee..e12ecdc50 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -610,6 +610,7 @@ enum { IdRadioRestartWithoutDalamud, IdButtonRestart = 201, + IdButtonSaveTsPack = 202, IdButtonHelp = IDHELP, IdButtonExit = IDCANCEL, }; @@ -907,20 +908,21 @@ int main() { TASKDIALOGCONFIG config = { 0 }; const TASKDIALOG_BUTTON radios[]{ - {IdRadioRestartNormal, L"Restart"}, - {IdRadioRestartWithout3pPlugins, L"Restart without 3rd party plugins"}, + {IdRadioRestartNormal, L"Restart normally"}, + {IdRadioRestartWithout3pPlugins, L"Restart without custom repository plugins"}, {IdRadioRestartWithoutPlugins, L"Restart without any plugins"}, {IdRadioRestartWithoutDalamud, L"Restart without Dalamud"}, }; const TASKDIALOG_BUTTON buttons[]{ - {IdButtonRestart, L"Restart\nRestart the game, optionally without plugins or Dalamud."}, + {IdButtonRestart, L"Restart\nRestart the game with the above-selected option."}, + {IdButtonSaveTsPack, L"Save Troubleshooting Pack\nSave a .tspack file containing information about this crash for analysis."}, {IdButtonExit, L"Exit\nExit the game."}, }; config.cbSize = sizeof(config); config.hInstance = GetModuleHandleW(nullptr); - config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS; + config.dwFlags = TDF_ENABLE_HYPERLINKS | TDF_CAN_BE_MINIMIZED | TDF_ALLOW_DIALOG_CANCELLATION | TDF_USE_COMMAND_LINKS | TDF_NO_DEFAULT_RADIO_BUTTON; config.pszMainIcon = MAKEINTRESOURCE(IDI_ICON1); config.pszMainInstruction = L"An error in the game occurred"; config.pszContent = (L"" @@ -928,7 +930,7 @@ int main() { "\n" R"aa(Try running a game repair in XIVLauncher by right clicking the login button, and disabling plugins you don't need. Please also check your antivirus, see our help site for more information.)aa" "\n" "\n" - R"aa(Upload this file (click here) if you want to ask for help in our Discord server.)aa" "\n" + R"aa(For further assistance, please upload a troubleshooting pack to our Discord server.)aa" "\n" ); config.pButtons = buttons; @@ -937,10 +939,9 @@ int main() { config.pszExpandedControlText = L"Hide stack trace"; config.pszCollapsedControlText = L"Stack trace for plugin developers"; config.pszExpandedInformation = window_log_str.c_str(); - config.pszWindowTitle = L"Dalamud Error"; + config.pszWindowTitle = L"Dalamud Crash Handler"; config.pRadioButtons = radios; config.cRadioButtons = ARRAYSIZE(radios); - config.nDefaultRadioButton = IdRadioRestartNormal; config.cxWidth = 300; #if _DEBUG @@ -962,6 +963,7 @@ int main() { case TDN_CREATED: { SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); + SendMessage(hwnd, TDM_ENABLE_BUTTON, IdButtonRestart, 0); return S_OK; } case TDN_HYPERLINK_CLICKED: @@ -983,6 +985,18 @@ int main() { } return S_OK; } + case TDN_RADIO_BUTTON_CLICKED: + SendMessage(hwnd, TDM_ENABLE_BUTTON, IdButtonRestart, 1); + return S_OK; + case TDN_BUTTON_CLICKED: + const auto button = static_cast(wParam); + if (button == IdButtonSaveTsPack) + { + export_tspack(hwnd, logDir, ws_to_u8(log.str()), troubleshootingPackData); + return S_FALSE; // keep the dialog open + } + + return S_OK; } return S_OK; From 82d9cd016d93033486ee90f2242ff5e34e667a8b Mon Sep 17 00:00:00 2001 From: Kaz Wolfe Date: Fri, 16 Feb 2024 15:56:51 -0800 Subject: [PATCH 502/585] Prefer "Save Troubleshooting Info" over "... Pack" --- DalamudCrashHandler/DalamudCrashHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index e12ecdc50..74e770ec0 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -916,7 +916,7 @@ int main() { const TASKDIALOG_BUTTON buttons[]{ {IdButtonRestart, L"Restart\nRestart the game with the above-selected option."}, - {IdButtonSaveTsPack, L"Save Troubleshooting Pack\nSave a .tspack file containing information about this crash for analysis."}, + {IdButtonSaveTsPack, L"Save Troubleshooting Info\nSave a .tspack file containing information about this crash for analysis."}, {IdButtonExit, L"Exit\nExit the game."}, }; From 0bb69cbd5f8f4633faef1181d067286ac5fc5116 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 17 Feb 2024 01:25:43 -0800 Subject: [PATCH 503/585] feat: Add /xlprofiler command (#1662) Make it easier for end users to open the profiler to pull load time reports. --- Dalamud/Interface/Internal/DalamudCommands.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index 4654a019d..ace8887f1 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -141,6 +141,13 @@ internal class DalamudCommands : IServiceType "Toggle Dalamud UI display modes. Native UI modifications may also be affected by this, but that depends on the plugin."), }); + commandManager.AddHandler("/xlprofiler", new CommandInfo(this.OnOpenProfilerCommand) + { + HelpMessage = Loc.Localize( + "DalamudProfilerHelp", + "Open Dalamud's startup timing profiler."), + }); + commandManager.AddHandler("/imdebug", new CommandInfo(this.OnDebugImInfoCommand) { HelpMessage = "ImGui DEBUG", @@ -409,4 +416,9 @@ internal class DalamudCommands : IServiceType } } } + + private void OnOpenProfilerCommand(string command, string arguments) + { + Service.Get().ToggleProfilerWindow(); + } } From 47da75df24636d6659fb0fdf770e03848b70a096 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 17 Feb 2024 21:38:43 +0900 Subject: [PATCH 504/585] Some DCH correctness --- Dalamud.Boot/veh.cpp | 20 +++- Dalamud.sln | 118 +------------------- DalamudCrashHandler/DalamudCrashHandler.cpp | 40 +++++-- 3 files changed, 47 insertions(+), 131 deletions(-) diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index ade295d02..059189202 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -163,7 +163,11 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) DuplicateHandle(GetCurrentProcess(), g_crashhandler_event, g_crashhandler_process, &exinfo.hEventHandle, 0, TRUE, DUPLICATE_SAME_ACCESS); std::wstring stackTrace; - if (void* fn; const auto err = static_cast(g_clr->get_function_pointer( + if (!g_clr) + { + stackTrace = L"(no CLR stack trace available)"; + } + else if (void* fn; const auto err = static_cast(g_clr->get_function_pointer( L"Dalamud.EntryPoint, Dalamud", L"VehCallback", L"Dalamud.EntryPoint+VehDelegate, Dalamud", @@ -182,11 +186,17 @@ LONG exception_handler(EXCEPTION_POINTERS* ex) if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &exinfo, static_cast(sizeof exinfo), &written, nullptr) || sizeof exinfo != written) return EXCEPTION_CONTINUE_SEARCH; - if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &stackTrace[0], static_cast(std::span(stackTrace).size_bytes()), &written, nullptr) || std::span(stackTrace).size_bytes() != written) - return EXCEPTION_CONTINUE_SEARCH; + if (const auto nb = static_cast(std::span(stackTrace).size_bytes())) + { + if (DWORD written; !WriteFile(g_crashhandler_pipe_write, stackTrace.data(), nb, &written, nullptr) || nb != written) + return EXCEPTION_CONTINUE_SEARCH; + } - if (DWORD written; !WriteFile(g_crashhandler_pipe_write, &g_startInfo.TroubleshootingPackData[0], static_cast(std::span(g_startInfo.TroubleshootingPackData).size_bytes()), &written, nullptr) || std::span(g_startInfo.TroubleshootingPackData).size_bytes() != written) - return EXCEPTION_CONTINUE_SEARCH; + if (const auto nb = static_cast(std::span(g_startInfo.TroubleshootingPackData).size_bytes())) + { + if (DWORD written; !WriteFile(g_crashhandler_pipe_write, g_startInfo.TroubleshootingPackData.data(), nb, &written, nullptr) || nb != written) + return EXCEPTION_CONTINUE_SEARCH; + } AllowSetForegroundWindow(GetProcessId(g_crashhandler_process)); diff --git a/Dalamud.sln b/Dalamud.sln index 200238a83..93089b9a6 100644 --- a/Dalamud.sln +++ b/Dalamud.sln @@ -6,8 +6,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitignore = .gitignore - targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets targets\Dalamud.Plugin.Bootstrap.targets = targets\Dalamud.Plugin.Bootstrap.targets + targets\Dalamud.Plugin.targets = targets\Dalamud.Plugin.targets EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "build", "build\build.csproj", "{94E5B016-02B1-459B-97D9-E783F28764B2}" @@ -38,184 +38,70 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FFXIVClientStructs.InteropS EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "DalamudCrashHandler", "DalamudCrashHandler\DalamudCrashHandler.vcxproj", "{317A264C-920B-44A1-8A34-F3A6827B0705}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dalamud.Common", "Dalamud.Common\Dalamud.Common.csproj", "{F21B13D2-D7D0-4456-B70F-3F8D695064E2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.ActiveCfg = Debug|Any CPU - {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x64.Build.0 = Debug|Any CPU - {94E5B016-02B1-459B-97D9-E783F28764B2}.Debug|x86.ActiveCfg = Debug|Any CPU {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.ActiveCfg = Release|Any CPU {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|Any CPU.Build.0 = Release|Any CPU - {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.ActiveCfg = Release|Any CPU - {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x64.Build.0 = Release|Any CPU - {94E5B016-02B1-459B-97D9-E783F28764B2}.Release|x86.ActiveCfg = Release|Any CPU {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.ActiveCfg = Debug|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|Any CPU.Build.0 = Debug|x64 - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.ActiveCfg = Debug|x64 - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x64.Build.0 = Debug|x64 - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.ActiveCfg = Debug|Any CPU - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Debug|x86.Build.0 = Debug|Any CPU {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.ActiveCfg = Release|x64 {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|Any CPU.Build.0 = Release|x64 - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.ActiveCfg = Release|x64 - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x64.Build.0 = Release|x64 - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.ActiveCfg = Release|Any CPU - {B92DAB43-2279-4A2C-96E3-D9D5910EDBEA}.Release|x86.Build.0 = Release|Any CPU {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.ActiveCfg = Debug|x64 {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|Any CPU.Build.0 = Debug|x64 - {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.ActiveCfg = Debug|x64 - {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x64.Build.0 = Debug|x64 - {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x86.ActiveCfg = Debug|x64 - {55198DC3-A03D-408E-A8EB-2077780C8576}.Debug|x86.Build.0 = Debug|x64 {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.ActiveCfg = Release|x64 {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|Any CPU.Build.0 = Release|x64 - {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.ActiveCfg = Release|x64 - {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x64.Build.0 = Release|x64 - {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x86.ActiveCfg = Release|x64 - {55198DC3-A03D-408E-A8EB-2077780C8576}.Release|x86.Build.0 = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.ActiveCfg = Debug|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|Any CPU.Build.0 = Debug|x64 - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.ActiveCfg = Debug|x64 - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x64.Build.0 = Debug|x64 - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.ActiveCfg = Debug|Any CPU - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Debug|x86.Build.0 = Debug|Any CPU {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.ActiveCfg = Release|x64 {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|Any CPU.Build.0 = Release|x64 - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.ActiveCfg = Release|x64 - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x64.Build.0 = Release|x64 - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.ActiveCfg = Release|Any CPU - {5B832F73-5F54-4ADC-870F-D0095EF72C9A}.Release|x86.Build.0 = Release|Any CPU {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}.Debug|x64.ActiveCfg = Debug|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x64.Build.0 = Debug|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x86.ActiveCfg = Debug|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Debug|x86.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 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.ActiveCfg = Release|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x64.Build.0 = Release|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x86.ActiveCfg = Release|x64 - {8874326B-E755-4D13-90B4-59AB263A3E6B}.Release|x86.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}.Debug|x64.ActiveCfg = Debug|x64 - {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x64.Build.0 = Debug|x64 - {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.ActiveCfg = Debug|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Debug|x86.Build.0 = Debug|Any CPU {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.ActiveCfg = Release|x64 {C8004563-1806-4329-844F-0EF6274291FC}.Release|Any CPU.Build.0 = Release|x64 - {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.ActiveCfg = Release|x64 - {C8004563-1806-4329-844F-0EF6274291FC}.Release|x64.Build.0 = Release|x64 - {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.ActiveCfg = Release|Any CPU - {C8004563-1806-4329-844F-0EF6274291FC}.Release|x86.Build.0 = Release|Any CPU {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.ActiveCfg = Debug|x64 {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|Any CPU.Build.0 = Debug|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.ActiveCfg = Debug|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x64.Build.0 = Debug|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.ActiveCfg = Debug|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Debug|x86.Build.0 = Debug|x64 {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.ActiveCfg = Release|x64 {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|Any CPU.Build.0 = Release|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.ActiveCfg = Release|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x64.Build.0 = Release|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.ActiveCfg = Release|x64 - {0483026E-C6CE-4B1A-AA68-46544C08140B}.Release|x86.Build.0 = Release|x64 {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.ActiveCfg = Debug|x64 {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|Any CPU.Build.0 = Debug|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.ActiveCfg = Debug|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x64.Build.0 = Debug|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.ActiveCfg = Debug|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Debug|x86.Build.0 = Debug|x64 {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.ActiveCfg = Release|x64 {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|Any CPU.Build.0 = Release|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.ActiveCfg = Release|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x64.Build.0 = Release|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.ActiveCfg = Release|x64 - {C0E7E797-4FBF-4F46-BC57-463F3719BA7A}.Release|x86.Build.0 = Release|x64 {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.ActiveCfg = Debug|x64 {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|Any CPU.Build.0 = Debug|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.ActiveCfg = Debug|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x64.Build.0 = Debug|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x86.ActiveCfg = Debug|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Debug|x86.Build.0 = Debug|x64 {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.ActiveCfg = Release|x64 {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|Any CPU.Build.0 = Release|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.ActiveCfg = Release|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x64.Build.0 = Release|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x86.ActiveCfg = Release|x64 - {2F7FF0A8-B619-4572-86C7-71E46FE22FB8}.Release|x86.Build.0 = Release|x64 {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.ActiveCfg = Debug|x64 {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|Any CPU.Build.0 = Debug|x64 - {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.ActiveCfg = Debug|x64 - {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x64.Build.0 = Debug|x64 - {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x86.ActiveCfg = Debug|x64 - {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Debug|x86.Build.0 = Debug|x64 {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.ActiveCfg = Release|x64 {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|Any CPU.Build.0 = Release|x64 - {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.ActiveCfg = Release|x64 - {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x64.Build.0 = Release|x64 - {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x86.ActiveCfg = Release|x64 - {4AFDB34A-7467-4D41-B067-53BC4101D9D0}.Release|x86.Build.0 = Release|x64 {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.ActiveCfg = Debug|Any CPU - {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x64.Build.0 = Debug|Any CPU - {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x86.ActiveCfg = Debug|Any CPU - {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Debug|x86.Build.0 = Debug|Any CPU {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.ActiveCfg = Release|Any CPU {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|Any CPU.Build.0 = Release|Any CPU - {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.ActiveCfg = Release|Any CPU - {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x64.Build.0 = Release|Any CPU - {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x86.ActiveCfg = Release|Any CPU - {C9B87BD7-AF49-41C3-91F1-D550ADEB7833}.Release|x86.Build.0 = Release|Any CPU {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.ActiveCfg = Debug|Any CPU - {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x64.Build.0 = Debug|Any CPU - {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x86.ActiveCfg = Debug|Any CPU - {05AB2F46-268B-4915-806F-DDF813E2D59D}.Debug|x86.Build.0 = Debug|Any CPU {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.ActiveCfg = Release|Any CPU {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|Any CPU.Build.0 = Release|Any CPU - {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.ActiveCfg = Release|Any CPU - {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x64.Build.0 = Release|Any CPU - {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x86.ActiveCfg = Release|Any CPU - {05AB2F46-268B-4915-806F-DDF813E2D59D}.Release|x86.Build.0 = Release|Any CPU {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.ActiveCfg = Debug|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|Any CPU.Build.0 = Debug|x64 - {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x64.ActiveCfg = Debug|x64 - {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x64.Build.0 = Debug|x64 - {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x86.ActiveCfg = Debug|x64 - {317A264C-920B-44A1-8A34-F3A6827B0705}.Debug|x86.Build.0 = Debug|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.ActiveCfg = Release|x64 {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|Any CPU.Build.0 = Release|x64 - {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.ActiveCfg = Release|x64 - {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x64.Build.0 = Release|x64 - {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.ActiveCfg = Release|x64 - {317A264C-920B-44A1-8A34-F3A6827B0705}.Release|x86.Build.0 = Release|x64 {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x64.ActiveCfg = Debug|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x64.Build.0 = Debug|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x86.ActiveCfg = Debug|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Debug|x86.Build.0 = Debug|Any CPU {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.ActiveCfg = Release|Any CPU {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|Any CPU.Build.0 = Release|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x64.ActiveCfg = Release|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x64.Build.0 = Release|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x86.ActiveCfg = Release|Any CPU - {F21B13D2-D7D0-4456-B70F-3F8D695064E2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 74e770ec0..82aa76569 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #pragma comment(lib, "comctl32.lib") #pragma comment(linker, "/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") @@ -153,7 +152,7 @@ std::wstring describe_module(const std::filesystem::path& path) { WORD wLanguage; WORD wCodePage; }; - const auto langs = std::span(reinterpret_cast(lpBuffer), size / sizeof(LANGANDCODEPAGE)); + const auto langs = std::span(static_cast(lpBuffer), size / sizeof(LANGANDCODEPAGE)); for (const auto& lang : langs) { if (!VerQueryValueW(block.data(), std::format(L"\\StringFileInfo\\{:04x}{:04x}\\FileDescription", lang.wLanguage, lang.wCodePage).c_str(), &lpBuffer, &size)) continue; @@ -442,6 +441,26 @@ std::wstring escape_shell_arg(const std::wstring& arg) { return res; } +void open_folder_and_select_items(HWND hwndOpener, const std::wstring& path) { + const auto piid = ILCreateFromPathW(path.c_str()); + if (!piid + || FAILED(SHOpenFolderAndSelectItems(piid, 0, nullptr, 0))) { + const auto args = std::format(L"/select,{}", escape_shell_arg(path)); + SHELLEXECUTEINFOW seiw{ + .cbSize = sizeof seiw, + .hwnd = hwndOpener, + .lpFile = L"explorer.exe", + .lpParameters = args.c_str(), + .nShow = SW_SHOW, + }; + if (!ShellExecuteExW(&seiw)) + throw_last_error("ShellExecuteExW"); + } + + if (piid) + ILFree(piid); +} + void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const std::string& crashLog, const std::string& troubleshootingPackData) { static const char* SourceLogFiles[] = { "output.log", @@ -458,7 +477,6 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s }}; std::optional filePath; - std::fstream fileStream; try { IShellItemPtr pItem; SYSTEMTIME st; @@ -483,7 +501,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s pItem.Release(); filePath.emplace(pFilePath); - fileStream.open(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc); + std::fstream fileStream(*filePath, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc); mz_zip_archive zipa{}; zipa.m_pIO_opaque = &fileStream; @@ -526,7 +544,7 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s int64_t off; }; const auto fnHandleReader = [](void* pOpaque, mz_uint64 file_ofs, void* pBuf, size_t n) -> size_t { - const auto& info = *reinterpret_cast(pOpaque); + const auto& info = *static_cast(pOpaque); if (!SetFilePointerEx(info.h, { .QuadPart = static_cast(info.off + file_ofs) }, nullptr, SEEK_SET)) throw_last_error("fnHandleReader: SetFilePointerEx"); if (DWORD read; !ReadFile(info.h, pBuf, static_cast(n), &read, nullptr)) @@ -586,7 +604,6 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s } catch (const std::exception& e) { MessageBoxW(hWndParent, std::format(L"Failed to save file: {}", u8_to_ws(e.what())).c_str(), get_window_string(hWndParent).c_str(), MB_OK | MB_ICONERROR); - fileStream.close(); if (filePath) { try { std::filesystem::remove(*filePath); @@ -597,9 +614,10 @@ void export_tspack(HWND hWndParent, const std::filesystem::path& logDir, const s return; } - fileStream.close(); if (filePath) { - ShellExecuteW(hWndParent, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", *filePath)).c_str(), nullptr, SW_SHOW); + // Not sure why, but without the wait, the selected file momentarily disappears and reappears + Sleep(1000); + open_folder_and_select_items(hWndParent, *filePath); } } @@ -672,7 +690,9 @@ int main() { std::filesystem::path assetDir, logDir; std::optional> launcherArgs; auto fullDump = false; - CoInitializeEx(nullptr, COINIT_MULTITHREADED); + + // IFileSaveDialog only works on STA + CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); std::vector args; if (int argc = 0; const auto argv = CommandLineToArgvW(GetCommandLineW(), &argc)) { @@ -972,7 +992,7 @@ int main() { if (link == L"help") { ShellExecuteW(hwnd, nullptr, L"https://goatcorp.github.io/faq?utm_source=vectored", nullptr, nullptr, SW_SHOW); } else if (link == L"logdir") { - ShellExecuteW(hwnd, nullptr, L"explorer.exe", escape_shell_arg(std::format(L"/select,{}", logPath.wstring())).c_str(), nullptr, SW_SHOW); + open_folder_and_select_items(hwnd, logPath.wstring()); } else if (link == L"logfile") { ShellExecuteW(hwnd, nullptr, logPath.c_str(), nullptr, nullptr, SW_SHOW); } else if (link == L"exporttspack") { From f825e86e861a901dc46164970888ce878e045494 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 17 Feb 2024 22:44:33 +0900 Subject: [PATCH 505/585] Fix B4G4R4A4->B8G8R8A8 channel extraction On systems without support for B4G4R4A4 pixel format (when DirectX feature level 11_1 is missing), the conversion will take place; a copy-and-paste error made the read pointer advance twice as fast as it should have been. --- .../Internals/FontAtlasFactory.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 021fc953f..6a3d3e1a3 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -290,6 +290,25 @@ internal sealed partial class FontAtlasFactory private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); + /// + /// Clones a texture wrap, by getting a new reference to the underlying and the + /// texture behind. + /// + /// The to clone from. + /// The cloned . + private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) + { + var srv = CppObject.FromPointer(wrap.ImGuiHandle); + using var res = srv.Resource; + using var tex2D = res.QueryInterface(); + var description = tex2D.Description; + return new DalamudTextureWrap( + new D3DTextureWrap( + srv.QueryInterface(), + description.Width, + description.Height)); + } + private static unsafe void ExtractChannelFromB8G8R8A8( Span target, ReadOnlySpan source, @@ -327,25 +346,6 @@ internal sealed partial class FontAtlasFactory } } - /// - /// Clones a texture wrap, by getting a new reference to the underlying and the - /// texture behind. - /// - /// The to clone from. - /// The cloned . - private static IDalamudTextureWrap CloneTextureWrap(IDalamudTextureWrap wrap) - { - var srv = CppObject.FromPointer(wrap.ImGuiHandle); - using var res = srv.Resource; - using var tex2D = res.QueryInterface(); - var description = tex2D.Description; - return new DalamudTextureWrap( - new D3DTextureWrap( - srv.QueryInterface(), - description.Width, - description.Height)); - } - private static unsafe void ExtractChannelFromB4G4R4A4( Span target, ReadOnlySpan source, @@ -378,7 +378,7 @@ internal sealed partial class FontAtlasFactory v |= v << 4; *wptr = (uint)((v << 24) | 0x00FFFFFF); wptr++; - rptr += 4; + rptr += 2; } } } From 6bb4033b3596df1cac8c0abfcdd8c923ec97ee48 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 17 Feb 2024 15:05:18 +0100 Subject: [PATCH 506/585] crashhandler: only keep the last 3 minidumps --- DalamudCrashHandler/DalamudCrashHandler.cpp | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 82aa76569..3a69198b8 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -746,6 +746,35 @@ int main() { std::wcout << L"Stripped: " << logDir.wstring() << std::endl; } + // Only keep the last 3 minidumps + if (!logDir.empty()) + { + std::vector> minidumps; + for (const auto& entry : std::filesystem::directory_iterator(logDir)) { + if (entry.path().filename().wstring().ends_with(L".dmp")) { + minidumps.emplace_back(entry.path(), std::filesystem::last_write_time(entry)); + } + } + + if (minidumps.size() > 3) + { + std::sort(minidumps.begin(), minidumps.end(), [](const auto& a, const auto& b) { return a.second < b.second; }); + for (size_t i = 0; i < minidumps.size() - 3; i++) { + if (std::filesystem::exists(minidumps[i].first)) + { + std::wcout << std::format(L"Removing old minidump: {}", minidumps[i].first.wstring()) << std::endl; + std::filesystem::remove(minidumps[i].first); + } + + // Also remove corresponding .log, if it exists + if (const auto logPath = minidumps[i].first.replace_extension(L".log"); std::filesystem::exists(logPath)) { + std::wcout << std::format(L"Removing corresponding log: {}", logPath.wstring()) << std::endl; + std::filesystem::remove(logPath); + } + } + } + } + while (true) { std::cout << "Waiting for crash...\n"; From cdaa538e1a90e923c4520e1fd0815c650520099b Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 17 Feb 2024 23:07:49 +0900 Subject: [PATCH 507/585] Revert "Warn if font files' hashes are unexpected (#1659)" This reverts commit 307f0fcbe834f16d879f1d58014ee6f0ec3a2771. --- .../Notifications/NotificationManager.cs | 68 +------- .../Internals/FontAtlasFactory.cs | 165 +----------------- 2 files changed, 17 insertions(+), 216 deletions(-) diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs index 34e07be8f..67ad3ee8f 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Numerics; using Dalamud.Interface.Colors; -using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Utility; using ImGuiNET; @@ -69,22 +68,15 @@ internal class NotificationManager : IServiceType /// The title of the notification. /// The type of the notification. /// The time the notification should be displayed for. - /// The added notification. - public Notification AddNotification( - string content, - string? title = null, - NotificationType type = NotificationType.None, - uint msDelay = NotifyDefaultDismiss) + public void AddNotification(string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = NotifyDefaultDismiss) { - var n = new Notification + this.notifications.Add(new Notification { Content = content, Title = title, NotificationType = type, DurationMs = msDelay, - }; - this.notifications.Add(n); - return n; + }); } /// @@ -105,10 +97,6 @@ internal class NotificationManager : IServiceType continue; } - using var pushedFont = tn.UseMonospaceFont - ? Service.Get().MonoFontHandle?.Push() - : null; - var opacity = tn.GetFadePercent(); var iconColor = tn.Color; @@ -119,12 +107,8 @@ internal class NotificationManager : IServiceType ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowBgAlpha(opacity); ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One); - if (tn.Actions.Count == 0) - ImGui.Begin(windowName, NotifyToastFlags); - else - ImGui.Begin(windowName, NotifyToastFlags & ~ImGuiWindowFlags.NoInputs); + ImGui.Begin(windowName, NotifyToastFlags); - ImGui.PushID(tn.NotificationId); ImGui.PushTextWrapPos(viewportSize.X / 3.0f); var wasTitleRendered = false; @@ -178,22 +162,10 @@ internal class NotificationManager : IServiceType ImGui.TextUnformatted(tn.Content); } - foreach (var (caption, action) in tn.Actions) - { - if (ImGui.Button(caption)) - action.InvokeSafely(); - ImGui.SameLine(); - } - - // break ImGui.SameLine(); - ImGui.TextUnformatted(string.Empty); - ImGui.PopStyleColor(); ImGui.PopTextWrapPos(); - ImGui.PopID(); - height += ImGui.GetWindowHeight() + NotifyPaddingMessageY; ImGui.End(); @@ -205,8 +177,6 @@ internal class NotificationManager : IServiceType /// internal class Notification { - private static int notificationIdCounter; - /// /// Possible notification phases. /// @@ -233,40 +203,20 @@ internal class NotificationManager : IServiceType Expired, } - /// - /// Gets the notification ID. - /// - internal int NotificationId { get; } = notificationIdCounter++; - /// /// Gets the type of the notification. /// internal NotificationType NotificationType { get; init; } /// - /// Gets or sets a value indicating whether to force the use of monospace font. + /// Gets the title of the notification. /// - internal bool UseMonospaceFont { get; set; } + internal string? Title { get; init; } /// - /// Gets the action buttons to attach to this notification. + /// Gets the content of the notification. /// - internal List<(string Text, Action ClickCallback)> Actions { get; } = new(); - - /// - /// Gets or sets a value indicating whether this notification has been dismissed. - /// - internal bool Dismissed { get; set; } - - /// - /// Gets or sets the title of the notification. - /// - internal string? Title { get; set; } - - /// - /// Gets or sets the content of the notification. - /// - internal string? Content { get; set; } + internal string Content { get; init; } /// /// Gets the duration of the notification in milliseconds. @@ -333,7 +283,7 @@ internal class NotificationManager : IServiceType { var elapsed = (int)this.ElapsedTime.TotalMilliseconds; - if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime || this.Dismissed) + if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime) return Phase.Expired; else if (elapsed > NotifyFadeInOutTime + this.DurationMs) return Phase.FadeOut; diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 6a3d3e1a3..3e0fd1394 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -1,7 +1,6 @@ using System.Buffers; using System.Collections.Generic; using System.Collections.Immutable; -using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -9,13 +8,9 @@ using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; -using Dalamud.Game.ClientState; -using Dalamud.Game.Gui; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; -using Dalamud.Interface.Internal.Notifications; -using Dalamud.Plugin.Services; using Dalamud.Storage.Assets; using Dalamud.Utility; @@ -23,11 +18,7 @@ using ImGuiNET; using ImGuiScene; -using Lumina.Data; using Lumina.Data.Files; -using Lumina.Misc; - -using Newtonsoft.Json; using SharpDX; using SharpDX.Direct3D11; @@ -42,43 +33,9 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; internal sealed partial class FontAtlasFactory : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable { - private static readonly Dictionary KnownFontFileDataHashes = new() - { - ["common/font/AXIS_96.fdt"] = 1486212503, - ["common/font/AXIS_12.fdt"] = 1370045105, - ["common/font/AXIS_14.fdt"] = 645957730, - ["common/font/AXIS_18.fdt"] = 899094094, - ["common/font/AXIS_36.fdt"] = 2537048938, - ["common/font/Jupiter_16.fdt"] = 1642196098, - ["common/font/Jupiter_20.fdt"] = 3053628263, - ["common/font/Jupiter_23.fdt"] = 1536194944, - ["common/font/Jupiter_45.fdt"] = 3473589216, - ["common/font/Jupiter_46.fdt"] = 1370962087, - ["common/font/Jupiter_90.fdt"] = 3661420529, - ["common/font/Meidinger_16.fdt"] = 3700692128, - ["common/font/Meidinger_20.fdt"] = 441419856, - ["common/font/Meidinger_40.fdt"] = 203848091, - ["common/font/MiedingerMid_10.fdt"] = 499375313, - ["common/font/MiedingerMid_12.fdt"] = 1925552591, - ["common/font/MiedingerMid_14.fdt"] = 1919733827, - ["common/font/MiedingerMid_18.fdt"] = 1635778987, - ["common/font/MiedingerMid_36.fdt"] = 1190559864, - ["common/font/TrumpGothic_184.fdt"] = 973994576, - ["common/font/TrumpGothic_23.fdt"] = 1967289381, - ["common/font/TrumpGothic_34.fdt"] = 1777971886, - ["common/font/TrumpGothic_68.fdt"] = 1170173741, - ["common/font/font0.tex"] = 514269927, - ["common/font/font1.tex"] = 3616607606, - ["common/font/font2.tex"] = 4166651000, - ["common/font/font3.tex"] = 1264942640, - ["common/font/font4.tex"] = 3534300885, - ["common/font/font5.tex"] = 1041916216, - ["common/font/font6.tex"] = 1247097672, - }; - private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); private readonly CancellationTokenSource cancellationTokenSource = new(); - private readonly IReadOnlyDictionary> fdtFiles; + private readonly IReadOnlyDictionary> fdtFiles; private readonly IReadOnlyDictionary[]>> texFiles; private readonly IReadOnlyDictionary> prebakedTextureWraps; private readonly Task defaultGlyphRanges; @@ -110,7 +67,7 @@ internal sealed partial class FontAtlasFactory this.fdtFiles = gffasInfo.ToImmutableDictionary( x => x.Font, - x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!)); + x => Task.Run(() => dataManager.GetFile(x.Attr.Path)!.Data)); var channelCountsTask = texPaths.ToImmutableDictionary( x => x, x => Task.WhenAll( @@ -122,8 +79,8 @@ internal sealed partial class FontAtlasFactory { unsafe { - using var pin = file.Data.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Data.Length); + using var pin = file.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Length); return fdt.MaxTextureIndex; } }))); @@ -144,13 +101,11 @@ internal sealed partial class FontAtlasFactory { unsafe { - using var pin = file.Result.Data.AsMemory().Pin(); - var fdt = new FdtFileView(pin.Pointer, file.Result.Data.Length); + using var pin = file.Result.AsMemory().Pin(); + var fdt = new FdtFileView(pin.Pointer, file.Result.Length); return fdt.ToGlyphRanges(); } }); - - Task.Run(this.CheckSanity); } /// @@ -248,12 +203,12 @@ internal sealed partial class FontAtlasFactory /// /// The font family and size. /// The . - public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas]).Data); + public FdtReader GetFdtReader(GameFontFamilyAndSize gffas) => new(ExtractResult(this.fdtFiles[gffas])); /// public unsafe MemoryHandle CreateFdtFileView(GameFontFamilyAndSize gffas, out FdtFileView fdtFileView) { - var arr = ExtractResult(this.fdtFiles[gffas]).Data; + var arr = ExtractResult(this.fdtFiles[gffas]); var handle = arr.AsMemory().Pin(); try { @@ -385,110 +340,6 @@ internal sealed partial class FontAtlasFactory } } - private async Task CheckSanity() - { - var invalidFiles = new Dictionary(); - var texFileTasks = new Dictionary>(); - var foundHashes = new Dictionary(); - foreach (var (gffas, fdtTask) in this.fdtFiles) - { - var fontAttr = gffas.GetAttribute()!; - try - { - foundHashes[fontAttr.Path] = Crc32.Get((await fdtTask).Data); - - foreach (var (task, index) in - (await this.texFiles[fontAttr.TexPathFormat]).Select((x, i) => (x, i))) - texFileTasks[fontAttr.TexPathFormat.Format(index)] = task; - } - catch (Exception e) - { - invalidFiles[fontAttr.Path] = e; - } - } - - foreach (var (path, texTask) in texFileTasks) - { - try - { - var hc = default(HashCode); - hc.AddBytes((await texTask).Data); - foundHashes[path] = Crc32.Get((await texTask).Data); - } - catch (Exception e) - { - invalidFiles[path] = e; - } - } - - foreach (var (path, hashCode) in foundHashes) - { - if (!KnownFontFileDataHashes.TryGetValue(path, out var expectedHashCode)) - continue; - if (expectedHashCode != hashCode) - { - invalidFiles[path] = new InvalidDataException( - $"Expected 0x{expectedHashCode:X08}; got 0x{hashCode:X08}"); - } - } - - var dconf = await Service.GetAsync(); - var nm = await Service.GetAsync(); - var intm = (await Service.GetAsync()).Manager; - var ggui = await Service.GetAsync(); - var cstate = await Service.GetAsync(); - - if (invalidFiles.Any()) - { - Log.Warning("Found {n} font related file(s) with unexpected hash code values.", invalidFiles.Count); - foreach (var (path, ex) in invalidFiles) - Log.Warning(ex, "\t=> {path}", path); - Log.Verbose(JsonConvert.SerializeObject(foundHashes)); - if (this.DefaultFontSpec is not SingleFontSpec { FontId: GameFontAndFamilyId }) - return; - - this.Framework.Update += FrameworkOnUpdate; - - void FrameworkOnUpdate(IFramework framework) - { - var charaSelect = ggui.GetAddonByName("CharaSelect", 1); - var charaMake = ggui.GetAddonByName("CharaMake", 1); - var titleDcWorldMap = ggui.GetAddonByName("TitleDCWorldMap", 1); - - // Show notification when TSM is visible, so that user can check whether a font looks bad - if (cstate.IsLoggedIn - || charaMake != IntPtr.Zero - || charaSelect != IntPtr.Zero - || titleDcWorldMap != IntPtr.Zero) - return; - - this.Framework.Update -= FrameworkOnUpdate; - - var n = nm.AddNotification( - "Non-default game fonts detected. If things do not look right, you can use a different font. Running repairs from XIVLauncher is recommended.", - "Modded font warning", - NotificationType.Warning, - 10000); - n.UseMonospaceFont = true; - n.Actions.Add( - ( - "Use Noto Sans", - () => - { - dconf.DefaultFontSpec = - new SingleFontSpec - { - FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansJpMedium), - SizePx = 17, - }; - dconf.QueueSave(); - intm.RebuildFonts(); - })); - n.Actions.Add(("Dismiss", () => n.Dismissed = true)); - } - } - } - private IDalamudTextureWrap GetChannelTexture(string texPathFormat, int fileIndex, int channelIndex) { var texFile = ExtractResult(ExtractResult(this.texFiles[texPathFormat])[fileIndex]); From e21b64969f4ae3ac164790dbd3827c5283318a2e Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:22:27 +0100 Subject: [PATCH 508/585] build: 9.0.0.19 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 208e6d4ea..aadf736a2 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.18 + 9.0.0.19 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From c19e1f0fcd224ec4b3cef14a48d921e34eff8375 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Sat, 17 Feb 2024 10:06:41 -0800 Subject: [PATCH 509/585] Default Minimum/Maximum WindowSizeConstraints (#1574) * feat: Default Minimum/Maximum WindowSizeConstraints If `MinimumSize` or `MaximumSize` are not set when defining a `WindowSizeConstraints`, they will be effectively unbounded. * chore: Make internal windows unbounded on max size * Ignore max value if it's smaller than minimum in any dimension --- .../Internal/Windows/ConsoleWindow.cs | 1 - .../PluginInstaller/PluginInstallerWindow.cs | 1 - .../Windows/StyleEditor/StyleEditorWindow.cs | 1 - Dalamud/Interface/Windowing/Window.cs | 29 +++++++++++++++++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 1b9890a75..63924365d 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -71,7 +71,6 @@ internal class ConsoleWindow : Window, IDisposable this.SizeConstraints = new WindowSizeConstraints { MinimumSize = new Vector2(600.0f, 200.0f), - MaximumSize = new Vector2(9999.0f, 9999.0f), }; this.RespectCloseHotkey = false; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 83d819634..95c227662 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -148,7 +148,6 @@ internal class PluginInstallerWindow : Window, IDisposable this.SizeConstraints = new WindowSizeConstraints { MinimumSize = this.Size.Value, - MaximumSize = new Vector2(5000, 5000), }; Service.GetAsync().ContinueWith(pluginManagerTask => diff --git a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs index c202a36ce..9ee4123cd 100644 --- a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs +++ b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs @@ -43,7 +43,6 @@ public class StyleEditorWindow : Window this.SizeConstraints = new WindowSizeConstraints { MinimumSize = new Vector2(890, 560), - MaximumSize = new Vector2(10000, 10000), }; } diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 59cb4d570..a7565c294 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -623,15 +623,38 @@ public abstract class Window /// public struct WindowSizeConstraints { + private Vector2 internalMaxSize = new(float.MaxValue); + + /// + /// Initializes a new instance of the struct. + /// + public WindowSizeConstraints() + { + } + /// /// Gets or sets the minimum size of the window. /// - public Vector2 MinimumSize { get; set; } - + public Vector2 MinimumSize { get; set; } = new(0); + /// /// Gets or sets the maximum size of the window. /// - public Vector2 MaximumSize { get; set; } + public Vector2 MaximumSize + { + get => this.GetSafeMaxSize(); + set => this.internalMaxSize = value; + } + + private Vector2 GetSafeMaxSize() + { + var currentMin = this.MinimumSize; + + if (this.internalMaxSize.X < currentMin.X || this.internalMaxSize.Y < currentMin.Y) + return new Vector2(float.MaxValue); + + return this.internalMaxSize; + } } /// From 7da47a8a334a7cdd9553b2d8074c015c80008710 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sat, 17 Feb 2024 19:07:18 +0100 Subject: [PATCH 510/585] build: 9.0.0.20 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index aadf736a2..321ee30a0 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.19 + 9.0.0.20 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 7dc99c9307dc950f5442d0d06ac8cbaa6594aaf4 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 18 Feb 2024 16:03:51 +0900 Subject: [PATCH 511/585] Fix AddRectFilledDetour typo (#1667) * Fix AddRectFilledDetour typo * Skip drawing if zero opacity is specified for drawing --- .../Interface/Internal/ImGuiDrawListFixProvider.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs index cdf7ab23e..f2d6ed244 100644 --- a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs @@ -83,8 +83,14 @@ internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposabl float rounding, ImDrawFlags flags) { - if (rounding < 0 || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask) + // Skip drawing if we're drawing something with alpha value of 0. + if ((col & 0xFF000000) == 0) + return; + + if (rounding < 0.5f || (flags & ImDrawFlags.RoundCornersMask) == ImDrawFlags.RoundCornersMask) { + // Take the fast path of drawing two triangles if no rounded corners are required. + var texIdCommon = *(nint*)(drawListPtr._Data + CImGuiImDrawListSharedDataTexIdCommonOffset); var pushTextureId = texIdCommon != drawListPtr._CmdHeader.TextureId; if (pushTextureId) @@ -98,6 +104,9 @@ internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposabl } else { + // Defer drawing rectangle with rounded corners to path drawing operations. + // Note that this may have a slightly different extent behaviors from the above if case. + // This is how it is in imgui_draw.cpp. drawListPtr.PathRect(min, max, rounding, flags); drawListPtr.PathFillConvex(col); } From 2d8b71c647e1b9ccea9f8746146fd8eaafee2791 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 18 Feb 2024 23:08:07 +0900 Subject: [PATCH 512/585] Add SetFontScaleMode(ImFontPtr, FontScaleMode) (#1666) * Add SetFontScaleMode(ImFontPtr, FontScaleMode) `IgnoreGlobalScale` was advertised as "excludes the given font from global scaling", but the intent I had in mind was "excludes the given font from being scaled in any manner". As the latter functionality is needed, obsoleted `IgnoreGlobalScale` and added `SetFontScaleMode`. * Make it correct * Name consistency --- Dalamud/Interface/FontIdentifier/IFontSpec.cs | 2 + .../FontIdentifier/SingleFontSpec.cs | 13 ++-- .../SingleFontChooserDialog.cs | 6 +- .../Widgets/GamePrebakedFontsTestWidget.cs | 68 +++++++++++++------ .../ManagedFontAtlas/FontScaleMode.cs | 33 +++++++++ .../IFontAtlasBuildToolkitPostBuild.cs | 14 ++-- .../IFontAtlasBuildToolkitPreBuild.cs | 28 +++++++- .../FontAtlasFactory.BuildToolkit.cs | 22 +++--- .../Internals/GamePrebakedFontHandle.cs | 46 +++++++++---- 9 files changed, 166 insertions(+), 66 deletions(-) create mode 100644 Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs diff --git a/Dalamud/Interface/FontIdentifier/IFontSpec.cs b/Dalamud/Interface/FontIdentifier/IFontSpec.cs index e4d931605..4d0719d4e 100644 --- a/Dalamud/Interface/FontIdentifier/IFontSpec.cs +++ b/Dalamud/Interface/FontIdentifier/IFontSpec.cs @@ -31,6 +31,8 @@ public interface IFontSpec /// The atlas to bind this font handle to. /// Optional callback to be called after creating the font handle. /// The new font handle. + /// will be set when is invoked. + /// IFontHandle CreateFontHandle(IFontAtlas atlas, FontAtlasBuildStepDelegate? callback = null); /// diff --git a/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs index 0604b22ea..946215b85 100644 --- a/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs +++ b/Dalamud/Interface/FontIdentifier/SingleFontSpec.cs @@ -109,7 +109,9 @@ public record SingleFontSpec : IFontSpec tk.RegisterPostBuild( () => { - var roundUnit = tk.IsGlobalScaleIgnored(font) ? 1 : 1 / tk.Scale; + // Multiplication by scale will be done with global scale, outside of this handling. + var scale = tk.GetFontScaleMode(font) == FontScaleMode.UndoGlobalScale ? 1 / tk.Scale : 1; + var roundUnit = tk.GetFontScaleMode(font) == FontScaleMode.SkipHandling ? 1 : 1 / tk.Scale; var newAscent = MathF.Round((font.Ascent * this.LineHeight) / roundUnit) * roundUnit; var newFontSize = MathF.Round((font.FontSize * this.LineHeight) / roundUnit) * roundUnit; var shiftDown = MathF.Round((newFontSize - font.FontSize) / 2f / roundUnit) * roundUnit; @@ -129,13 +131,10 @@ public record SingleFontSpec : IFontSpec } } - // `/ roundUnit` = `* scale` - var dax = MathF.Round(this.LetterSpacing / roundUnit / roundUnit) * roundUnit; - var dxy0 = this.GlyphOffset / roundUnit; - + var dax = MathF.Round((this.LetterSpacing * scale) / roundUnit) * roundUnit; + var dxy0 = this.GlyphOffset * scale; dxy0 /= roundUnit; - dxy0.X = MathF.Round(dxy0.X); - dxy0.Y = MathF.Round(dxy0.Y); + dxy0 = new(MathF.Round(dxy0.X), MathF.Round(dxy0.Y)); dxy0 *= roundUnit; dxy0.Y += shiftDown; diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs index 410bf7d18..ca75e5ce0 100644 --- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -342,9 +342,7 @@ public sealed class SingleFontChooserDialog : IDisposable { this.fontHandle ??= this.selectedFont.CreateFontHandle( this.atlas, - tk => - tk.OnPreBuild(e => e.IgnoreGlobalScale(e.Font)) - .OnPostBuild(e => e.Font.AdjustGlyphMetrics(1f / e.Scale))); + tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale))); } else { @@ -837,7 +835,7 @@ public sealed class SingleFontChooserDialog : IDisposable var changed = false; if (!ImGui.BeginTable("##advancedOptions", 4)) - return changed; + return false; var labelWidth = ImGui.CalcTextSize("Letter Spacing:").X; labelWidth = Math.Max(labelWidth, ImGui.CalcTextSize("Offset:").X); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index 84682e7c2..8bb999557 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -25,12 +25,20 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable { + private static readonly string[] FontScaleModes = + { + nameof(FontScaleMode.Default), + nameof(FontScaleMode.SkipHandling), + nameof(FontScaleMode.UndoGlobalScale), + }; + private ImVectorWrapper testStringBuffer; private IFontAtlas? privateAtlas; private SingleFontSpec fontSpec = new() { FontId = DalamudDefaultFontAndFamilyId.Instance }; private IFontHandle? fontDialogHandle; private IReadOnlyDictionary Handle)[]>? fontHandles; - private bool useGlobalScale; + private bool atlasScaleMode = true; + private int fontScaleMode = (int)FontScaleMode.UndoGlobalScale; private bool useWordWrap; private bool useItalic; private bool useBold; @@ -52,12 +60,14 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable public unsafe void Draw() { ImGui.AlignTextToFramePadding(); - fixed (byte* labelPtr = "Global Scale"u8) + if (ImGui.Combo("Global Scale per Font", ref this.fontScaleMode, FontScaleModes, FontScaleModes.Length)) + this.ClearAtlas(); + fixed (byte* labelPtr = "Global Scale for Atlas"u8) { - var v = (byte)(this.useGlobalScale ? 1 : 0); + var v = (byte)(this.atlasScaleMode ? 1 : 0); if (ImGuiNative.igCheckbox(labelPtr, &v) != 0) { - this.useGlobalScale = v != 0; + this.atlasScaleMode = v != 0; this.ClearAtlas(); } } @@ -124,7 +134,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont", FontAtlasAutoRebuildMode.Async)); fcd.SelectedFont = this.fontSpec; - fcd.IgnorePreviewGlobalScale = !this.useGlobalScale; + fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode; Service.Get().Draw += fcd.Draw; fcd.ResultTask.ContinueWith( r => Service.Get().RunOnFrameworkThread( @@ -148,12 +158,14 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable Service.Get().CreateFontAtlas( nameof(GamePrebakedFontsTestWidget), FontAtlasAutoRebuildMode.Async, - this.useGlobalScale); - this.fontDialogHandle ??= this.fontSpec.CreateFontHandle(this.privateAtlas); + this.atlasScaleMode); + this.fontDialogHandle ??= this.fontSpec.CreateFontHandle( + this.privateAtlas, + e => e.OnPreBuild(tk => tk.SetFontScaleMode(tk.Font, (FontScaleMode)this.fontScaleMode))); fixed (byte* labelPtr = "Test Input"u8) { - if (!this.useGlobalScale) + if (!this.atlasScaleMode) ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); using (this.fontDialogHandle.Push()) { @@ -180,7 +192,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable } } - if (!this.useGlobalScale) + if (!this.atlasScaleMode) ImGuiNative.igSetWindowFontScale(1); } @@ -192,17 +204,29 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable .ToImmutableDictionary( x => x.Key, x => x.Select( - y => (y, new Lazy( - () => this.useMinimumBuild - ? this.privateAtlas.NewDelegateFontHandle( - e => - e.OnPreBuild( - tk => tk.AddGameGlyphs( - y, - Encoding.UTF8.GetString( - this.testStringBuffer.DataSpan).ToGlyphRange(), - default))) - : this.privateAtlas.NewGameFontHandle(y)))) + y => + { + var range = Encoding.UTF8.GetString(this.testStringBuffer.DataSpan).ToGlyphRange(); + + Lazy l; + if (this.useMinimumBuild + || (this.atlasScaleMode && this.fontScaleMode != (int)FontScaleMode.Default)) + { + l = new( + () => this.privateAtlas!.NewDelegateFontHandle( + e => + e.OnPreBuild( + tk => tk.SetFontScaleMode( + tk.AddGameGlyphs(y, range, default), + (FontScaleMode)this.fontScaleMode)))); + } + else + { + l = new(() => this.privateAtlas!.NewGameFontHandle(y)); + } + + return (y, l); + }) .ToArray()); var offsetX = ImGui.CalcTextSize("99.9pt").X + (ImGui.GetStyle().FramePadding.X * 2); @@ -230,7 +254,7 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable } else { - if (!this.useGlobalScale) + if (!this.atlasScaleMode) ImGuiNative.igSetWindowFontScale(1 / ImGuiHelpers.GlobalScale); if (counter++ % 2 == 0) { @@ -251,8 +275,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable } finally { - ImGuiNative.igPopTextWrapPos(); ImGuiNative.igSetWindowFontScale(1); + ImGuiNative.igPopTextWrapPos(); } } } diff --git a/Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs b/Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs new file mode 100644 index 000000000..b30d5c26c --- /dev/null +++ b/Dalamud/Interface/ManagedFontAtlas/FontScaleMode.cs @@ -0,0 +1,33 @@ +using Dalamud.Interface.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ManagedFontAtlas; + +/// +/// Specifies how should global font scale affect a font. +/// +public enum FontScaleMode +{ + /// + /// Do the default handling. Dalamud will load the sufficienty large font that will accomodate the global scale, + /// and stretch the loaded glyphs so that they look pixel-perfect after applying global scale on drawing. + /// Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes. + /// + Default, + + /// + /// Do nothing with the font. Dalamud will load the font with the size that is exactly as specified. + /// On drawing, the font will look blurry due to stretching. + /// Intended for use with custom scale handling. + /// + SkipHandling, + + /// + /// Stretch the glyphs of the loaded font by the inverse of the global scale. + /// On drawing, the font will always render exactly as the requested size without blurring, as long as + /// and do not affect the scale any + /// further. Note that bitmap fonts and game fonts will always look blurry if they're not in their original sizes. + /// + UndoGlobalScale, +} diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs index d824eca52..827187063 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPostBuild.cs @@ -1,4 +1,5 @@ using Dalamud.Interface.Internal; +using Dalamud.Utility; using ImGuiNET; @@ -10,12 +11,13 @@ namespace Dalamud.Interface.ManagedFontAtlas; /// public interface IFontAtlasBuildToolkitPostBuild : IFontAtlasBuildToolkit { - /// - /// Gets whether global scaling is ignored for the given font. - /// - /// The font. - /// True if ignored. - bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + /// + [Obsolete($"Use {nameof(this.GetFontScaleMode)}")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale; + + /// + FontScaleMode GetFontScaleMode(ImFontPtr fontPtr); /// /// Stores a texture to be managed with the atlas. diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs index 9ab480374..9b80d27ff 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlasBuildToolkitPreBuild.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; +using Dalamud.Utility; using ImGuiNET; @@ -45,14 +46,37 @@ public interface IFontAtlasBuildToolkitPreBuild : IFontAtlasBuildToolkit /// /// The font. /// Same with . - ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr); + [Obsolete( + $"Use {nameof(this.SetFontScaleMode)} with {nameof(FontScaleMode)}.{nameof(FontScaleMode.UndoGlobalScale)}")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) => this.SetFontScaleMode(fontPtr, FontScaleMode.UndoGlobalScale); /// /// Gets whether global scaling is ignored for the given font. /// /// The font. /// True if ignored. - bool IsGlobalScaleIgnored(ImFontPtr fontPtr); + [Obsolete($"Use {nameof(this.GetFontScaleMode)}")] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => this.GetFontScaleMode(fontPtr) == FontScaleMode.UndoGlobalScale; + + /// + /// Sets the scaling mode for the given font. + /// + /// The font, returned from and alike. + /// Note that property is not guaranteed to be automatically updated upon + /// calling font adding functions. Pass the return value from font adding functions, not + /// property. + /// The scaling mode. + /// . + ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode mode); + + /// + /// Gets the scaling mode for the given font. + /// + /// The font. + /// The scaling mode. + FontScaleMode GetFontScaleMode(ImFontPtr fontPtr); /// /// Registers a function to be run after build. diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs index a57e6d036..55af20329 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.BuildToolkit.cs @@ -83,9 +83,9 @@ internal sealed partial class FontAtlasFactory public ImVectorWrapper Fonts => this.data.Fonts; /// - /// Gets the list of fonts to ignore global scale. + /// Gets the font scale modes. /// - public List GlobalScaleExclusions { get; } = new(); + private Dictionary FontScaleModes { get; } = new(); /// public void Dispose() => this.disposeAfterBuild.Dispose(); @@ -151,15 +151,15 @@ internal sealed partial class FontAtlasFactory } /// - public ImFontPtr IgnoreGlobalScale(ImFontPtr fontPtr) + public ImFontPtr SetFontScaleMode(ImFontPtr fontPtr, FontScaleMode scaleMode) { - this.GlobalScaleExclusions.Add(fontPtr); + this.FontScaleModes[fontPtr] = scaleMode; return fontPtr; } - /// - public bool IsGlobalScaleIgnored(ImFontPtr fontPtr) => - this.GlobalScaleExclusions.Contains(fontPtr); + /// + public FontScaleMode GetFontScaleMode(ImFontPtr fontPtr) => + this.FontScaleModes.GetValueOrDefault(fontPtr, FontScaleMode.Default); /// public int StoreTexture(IDalamudTextureWrap textureWrap, bool disposeOnError) => @@ -496,17 +496,17 @@ internal sealed partial class FontAtlasFactory var configData = this.data.ConfigData; foreach (ref var config in configData.DataSpan) { - if (this.GlobalScaleExclusions.Contains(new(config.DstFont))) + if (this.GetFontScaleMode(config.DstFont) != FontScaleMode.Default) continue; config.SizePixels *= this.Scale; config.GlyphMaxAdvanceX *= this.Scale; - if (float.IsInfinity(config.GlyphMaxAdvanceX)) + if (float.IsInfinity(config.GlyphMaxAdvanceX) || float.IsNaN(config.GlyphMaxAdvanceX)) config.GlyphMaxAdvanceX = config.GlyphMaxAdvanceX > 0 ? float.MaxValue : -float.MaxValue; config.GlyphMinAdvanceX *= this.Scale; - if (float.IsInfinity(config.GlyphMinAdvanceX)) + if (float.IsInfinity(config.GlyphMinAdvanceX) || float.IsNaN(config.GlyphMinAdvanceX)) config.GlyphMinAdvanceX = config.GlyphMinAdvanceX > 0 ? float.MaxValue : -float.MaxValue; config.GlyphOffset *= this.Scale; @@ -536,7 +536,7 @@ internal sealed partial class FontAtlasFactory var scale = this.Scale; foreach (ref var font in this.Fonts.DataSpan) { - if (!this.GlobalScaleExclusions.Contains(font)) + if (this.GetFontScaleMode(font) != FontScaleMode.SkipHandling) font.AdjustGlyphMetrics(1 / scale, 1 / scale); foreach (var c in FallbackCodepoints) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs index b6c9817aa..1101e7119 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/GamePrebakedFontHandle.cs @@ -345,17 +345,36 @@ internal class GamePrebakedFontHandle : FontHandle { foreach (var (font, style, ranges) in this.attachments) { - var effectiveStyle = - toolkitPreBuild.IsGlobalScaleIgnored(font) - ? style.Scale(1 / toolkitPreBuild.Scale) - : style; if (!this.fonts.TryGetValue(style, out var plan)) { - plan = new( - effectiveStyle, - toolkitPreBuild.Scale, - this.handleManager.GameFontTextureProvider, - this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + switch (toolkitPreBuild.GetFontScaleMode(font)) + { + case FontScaleMode.Default: + default: + plan = new( + style, + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + break; + + case FontScaleMode.SkipHandling: + plan = new( + style, + 1f, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + break; + + case FontScaleMode.UndoGlobalScale: + plan = new( + style.Scale(1 / toolkitPreBuild.Scale), + toolkitPreBuild.Scale, + this.handleManager.GameFontTextureProvider, + this.CreateTemplateFont(toolkitPreBuild, style.SizePx)); + break; + } + this.fonts[style] = plan; } @@ -620,15 +639,14 @@ internal class GamePrebakedFontHandle : FontHandle public unsafe void CopyGlyphsToRanges(IFontAtlasBuildToolkitPostBuild toolkitPostBuild) { var scale = this.Style.SizePt / this.Fdt.FontHeader.Size; - var atlasScale = toolkitPostBuild.Scale; - var round = 1 / atlasScale; foreach (var (font, rangeBits) in this.Ranges) { if (font.NativePtr == this.FullRangeFont.NativePtr) continue; - var noGlobalScale = toolkitPostBuild.IsGlobalScaleIgnored(font); + var fontScaleMode = toolkitPostBuild.GetFontScaleMode(font); + var round = fontScaleMode == FontScaleMode.SkipHandling ? 1 : 1 / toolkitPostBuild.Scale; var lookup = font.IndexLookupWrapped(); var glyphs = font.GlyphsWrapped(); @@ -649,7 +667,7 @@ internal class GamePrebakedFontHandle : FontHandle ref var g = ref glyphs[glyphIndex]; g = sourceGlyph; - if (noGlobalScale) + if (fontScaleMode == FontScaleMode.SkipHandling) { g.XY *= scale; g.AdvanceX *= scale; @@ -673,7 +691,7 @@ internal class GamePrebakedFontHandle : FontHandle continue; if (!rangeBits[leftInt] || !rangeBits[rightInt]) continue; - if (noGlobalScale) + if (fontScaleMode == FontScaleMode.SkipHandling) { font.AddKerningPair((ushort)leftInt, (ushort)rightInt, k.RightOffset * scale); } From 2909c83521f19781875ca4f4cd78a19fa2bb7d12 Mon Sep 17 00:00:00 2001 From: goat <16760685+goaaats@users.noreply.github.com> Date: Sun, 18 Feb 2024 17:16:35 +0100 Subject: [PATCH 513/585] build: 9.0.0.21 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 321ee30a0..205681cb8 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 9.0.0.20 + 9.0.0.21 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From ac59f73b59ba497d9f83e1443839f350976644ec Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:15:55 +0100 Subject: [PATCH 514/585] [master] Update ClientStructs (#1669) 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 b12028fbc..d5673fcb4 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b12028fbca6c950db0cb3d10d3185d959067e901 +Subproject commit d5673fcb479b414d0c760213cda5f2a98d297cbf From c27422384f66868991814a0fd905c3bada90c271 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 20 Feb 2024 15:37:54 +0900 Subject: [PATCH 515/585] IGameConfig: fix load-time race condition As some public properties of `IGameConfig` are being set on the first `Framework` tick, there was a short window that those properties were null, which goes against the interface declaration. This commit fixes that, by making those properties block for the full initialization of the class. A possible side effect is that a plugin that is set to block the game from loading until it loads will now hang the game if it tries to access the game configuration from its constructor, instead of throwing a `NullReferenceException`. As it would mean that the plugin was buggy at the first place and it would have sometimes failed to load anyway, it might as well be a non-breaking change. --- Dalamud/Game/Config/GameConfig.cs | 87 ++++++++++++++++++++------ Dalamud/Plugin/Services/IGameConfig.cs | 22 ++++++- Dalamud/Utility/Util.cs | 40 ++++++++++++ 3 files changed, 126 insertions(+), 23 deletions(-) diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index b82d64f24..94a92a4da 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -1,4 +1,6 @@ -using Dalamud.Hooking; +using System.Threading.Tasks; + +using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; @@ -15,6 +17,11 @@ namespace Dalamud.Game.Config; [ServiceManager.BlockingEarlyLoadedService] internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable { + private readonly TaskCompletionSource tcsInitialization = new(); + private readonly TaskCompletionSource tcsSystem = new(); + private readonly TaskCompletionSource tcsUiConfig = new(); + private readonly TaskCompletionSource tcsUiControl = new(); + private readonly GameConfigAddressResolver address = new(); private Hook? configChangeHook; @@ -23,16 +30,32 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable { framework.RunOnTick(() => { - Log.Verbose("[GameConfig] Initializing"); - var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); - var commonConfig = &csFramework->SystemConfig.CommonSystemConfig; - this.System = new GameConfigSection("System", framework, &commonConfig->ConfigBase); - this.UiConfig = new GameConfigSection("UiConfig", framework, &commonConfig->UiConfig); - this.UiControl = new GameConfigSection("UiControl", framework, () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode ? &commonConfig->UiControlGamepadConfig : &commonConfig->UiControlConfig); - - this.address.Setup(sigScanner); - this.configChangeHook = Hook.FromAddress(this.address.ConfigChangeAddress, this.OnConfigChanged); - this.configChangeHook.Enable(); + try + { + Log.Verbose("[GameConfig] Initializing"); + var csFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); + var commonConfig = &csFramework->SystemConfig.CommonSystemConfig; + this.tcsSystem.SetResult(new("System", framework, &commonConfig->ConfigBase)); + this.tcsUiConfig.SetResult(new("UiConfig", framework, &commonConfig->UiConfig)); + this.tcsUiControl.SetResult( + new( + "UiControl", + framework, + () => this.UiConfig.TryGetBool("PadMode", out var padMode) && padMode + ? &commonConfig->UiControlGamepadConfig + : &commonConfig->UiControlConfig)); + + this.address.Setup(sigScanner); + this.configChangeHook = Hook.FromAddress( + this.address.ConfigChangeAddress, + this.OnConfigChanged); + this.configChangeHook.Enable(); + this.tcsInitialization.SetResult(); + } + catch (Exception ex) + { + this.tcsInitialization.SetExceptionIfIncomplete(ex); + } }); } @@ -59,13 +82,16 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable #pragma warning restore 67 /// - public GameConfigSection System { get; private set; } + public Task InitializationTask => this.tcsInitialization.Task; /// - public GameConfigSection UiConfig { get; private set; } + public GameConfigSection System => this.tcsSystem.Task.Result; /// - public GameConfigSection UiControl { get; private set; } + public GameConfigSection UiConfig => this.tcsUiConfig.Task.Result; + + /// + public GameConfigSection UiControl => this.tcsUiControl.Task.Result; /// public bool TryGet(SystemConfigOption option, out bool value) => this.System.TryGet(option.GetName(), out value); @@ -169,6 +195,11 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable /// void IDisposable.Dispose() { + var ode = new ObjectDisposedException(nameof(GameConfig)); + this.tcsInitialization.SetExceptionIfIncomplete(ode); + this.tcsSystem.SetExceptionIfIncomplete(ode); + this.tcsUiConfig.SetExceptionIfIncomplete(ode); + this.tcsUiControl.SetExceptionIfIncomplete(ode); this.configChangeHook?.Disable(); this.configChangeHook?.Dispose(); } @@ -226,9 +257,16 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig internal GameConfigPluginScoped() { this.gameConfigService.Changed += this.ConfigChangedForward; - this.gameConfigService.System.Changed += this.SystemConfigChangedForward; - this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward; - this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward; + this.InitializationTask = this.gameConfigService.InitializationTask.ContinueWith( + r => + { + if (!r.IsCompletedSuccessfully) + return r; + this.gameConfigService.System.Changed += this.SystemConfigChangedForward; + this.gameConfigService.UiConfig.Changed += this.UiConfigConfigChangedForward; + this.gameConfigService.UiControl.Changed += this.UiControlConfigChangedForward; + return Task.CompletedTask; + }).Unwrap(); } /// @@ -243,6 +281,9 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig /// public event EventHandler? UiControlChanged; + /// + public Task InitializationTask { get; } + /// public GameConfigSection System => this.gameConfigService.System; @@ -256,9 +297,15 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig public void Dispose() { this.gameConfigService.Changed -= this.ConfigChangedForward; - this.gameConfigService.System.Changed -= this.SystemConfigChangedForward; - this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward; - this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward; + this.InitializationTask.ContinueWith( + r => + { + if (!r.IsCompletedSuccessfully) + return; + this.gameConfigService.System.Changed -= this.SystemConfigChangedForward; + this.gameConfigService.UiConfig.Changed -= this.UiConfigConfigChangedForward; + this.gameConfigService.UiControl.Changed -= this.UiControlConfigChangedForward; + }); this.Changed = null; this.SystemChanged = null; diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index 8e9b48d83..8249aed76 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -1,14 +1,20 @@ -using System; -using System.Diagnostics; +using System.Diagnostics; +using System.Threading.Tasks; using Dalamud.Game.Config; -using FFXIVClientStructs.FFXIV.Common.Configuration; +using Dalamud.Plugin.Internal.Types; namespace Dalamud.Plugin.Services; /// /// This class represents the game's configuration. /// +/// +/// Avoid accessing configuration from your plugin constructor, especially if your plugin sets +/// to 2 and to true. +/// If property access from the plugin constructor is desired, do the value retrieval asynchronously via +/// ; do not wait for the result right away. +/// public interface IGameConfig { /// @@ -31,6 +37,16 @@ public interface IGameConfig /// public event EventHandler UiControlChanged; + /// + /// Gets a task representing the initialization state of this instance of . + /// + /// + /// Accessing -typed properties such as , directly or indirectly + /// via , + /// , or alike will block, if this task is incomplete. + /// + public Task InitializationTask { get; } + /// /// Gets the collection of config options that persist between characters. /// diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index f5ad8b999..65196b3ee 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -10,6 +10,7 @@ using System.Reflection.Emit; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Data; @@ -697,6 +698,45 @@ public static class Util Marshal.ThrowExceptionForHR(hr.Value); } + /// + /// Calls if the task is incomplete. + /// + /// The task. + /// The exception to set. + internal static void SetExceptionIfIncomplete(this TaskCompletionSource t, Exception ex) + { + if (t.Task.IsCompleted) + return; + try + { + t.SetException(ex); + } + catch + { + // ignore + } + } + + /// + /// Calls if the task is incomplete. + /// + /// The type of the result. + /// The task. + /// The exception to set. + internal static void SetExceptionIfIncomplete(this TaskCompletionSource t, Exception ex) + { + if (t.Task.IsCompleted) + return; + try + { + t.SetException(ex); + } + catch + { + // ignore + } + } + /// /// Print formatted GameObject Information to ImGui. /// From da969dec5cb4f7682acc1965dc8ddf77be8c129f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 20 Feb 2024 15:42:49 +0900 Subject: [PATCH 516/585] DAssetM cleanup --- Dalamud/Storage/Assets/DalamudAssetManager.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 7edb1c61d..69c7c32e8 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -194,12 +194,14 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA try { - await using var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write); - await url.DownloadAsync( - this.httpClient.SharedHttpClient, - tempPathStream, - this.cancellationTokenSource.Token); - tempPathStream.Dispose(); + await using (var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write)) + { + await url.DownloadAsync( + this.httpClient.SharedHttpClient, + tempPathStream, + this.cancellationTokenSource.Token); + } + for (var j = RenameAttemptCount; ; j--) { try @@ -265,7 +267,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA /// [Pure] public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) => - ExtractResult(this.GetDalamudTextureWrapAsync(asset)); + this.GetDalamudTextureWrapAsync(asset).Result; /// [Pure] @@ -332,8 +334,6 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA } } - private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); - private Task TransformImmediate(Task task, Func transformer) { if (task.IsCompletedSuccessfully) From 3909fb13fad3e030a511a598f79b2d0677726df5 Mon Sep 17 00:00:00 2001 From: AzureGem Date: Tue, 20 Feb 2024 14:19:24 -0500 Subject: [PATCH 517/585] Fix ConsoleWindow regex handling (#1674) --- Dalamud/Interface/Internal/Windows/ConsoleWindow.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 63924365d..bf559c4d7 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -731,8 +731,6 @@ internal class ConsoleWindow : Window, IDisposable return false; } - this.regexError = false; - // else we couldn't find a filter for this entry, if we have any filters, we need to block this entry. return !this.pluginFilters.Any(); } @@ -741,6 +739,7 @@ internal class ConsoleWindow : Window, IDisposable { lock (this.renderLock) { + this.regexError = false; this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList(); } } From a3217bb86d7b270ef4ac802dbae0c5953ef70f59 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 21 Feb 2024 16:34:53 +0900 Subject: [PATCH 518/585] Remove InitialiationTask from interface --- Dalamud/Game/Config/GameConfig.cs | 13 +++++++------ Dalamud/Plugin/Services/IGameConfig.cs | 15 ++++----------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 94a92a4da..162df9417 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -81,7 +81,9 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable public event EventHandler? UiControlChanged; #pragma warning restore 67 - /// + /// + /// Gets a task representing the initialization state of this class. + /// public Task InitializationTask => this.tcsInitialization.Task; /// @@ -251,13 +253,15 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig [ServiceManager.ServiceDependency] private readonly GameConfig gameConfigService = Service.Get(); + private readonly Task initializationTask; + /// /// Initializes a new instance of the class. /// internal GameConfigPluginScoped() { this.gameConfigService.Changed += this.ConfigChangedForward; - this.InitializationTask = this.gameConfigService.InitializationTask.ContinueWith( + this.initializationTask = this.gameConfigService.InitializationTask.ContinueWith( r => { if (!r.IsCompletedSuccessfully) @@ -281,9 +285,6 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig /// public event EventHandler? UiControlChanged; - /// - public Task InitializationTask { get; } - /// public GameConfigSection System => this.gameConfigService.System; @@ -297,7 +298,7 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig public void Dispose() { this.gameConfigService.Changed -= this.ConfigChangedForward; - this.InitializationTask.ContinueWith( + this.initializationTask.ContinueWith( r => { if (!r.IsCompletedSuccessfully) diff --git a/Dalamud/Plugin/Services/IGameConfig.cs b/Dalamud/Plugin/Services/IGameConfig.cs index 8249aed76..c69fa906a 100644 --- a/Dalamud/Plugin/Services/IGameConfig.cs +++ b/Dalamud/Plugin/Services/IGameConfig.cs @@ -10,7 +10,10 @@ namespace Dalamud.Plugin.Services; /// This class represents the game's configuration. /// /// -/// Avoid accessing configuration from your plugin constructor, especially if your plugin sets +/// Accessing -typed properties such as , directly or indirectly +/// via , +/// , or alike will block, if the game is not done loading.
+/// Therefore, avoid accessing configuration from your plugin constructor, especially if your plugin sets /// to 2 and to true. /// If property access from the plugin constructor is desired, do the value retrieval asynchronously via /// ; do not wait for the result right away. @@ -37,16 +40,6 @@ public interface IGameConfig ///
public event EventHandler UiControlChanged; - /// - /// Gets a task representing the initialization state of this instance of . - /// - /// - /// Accessing -typed properties such as , directly or indirectly - /// via , - /// , or alike will block, if this task is incomplete. - /// - public Task InitializationTask { get; } - /// /// Gets the collection of config options that persist between characters. /// From bf34dd2817e5c5b1c472ed2ee2f68b6cd5ac3eac Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 22 Feb 2024 05:04:16 +0100 Subject: [PATCH 519/585] Update ClientStructs (#1670) 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 d5673fcb4..efea20c57 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit d5673fcb479b414d0c760213cda5f2a98d297cbf +Subproject commit efea20c57e4fed3e2e70ef25dacebae2558e1bbf From db17a8658702d81bc40d5b5b96450d271926ab0a Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 22 Feb 2024 08:00:19 +0100 Subject: [PATCH 520/585] Update ClientStructs (#1677) 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 efea20c57..7f1f3bcaa 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit efea20c57e4fed3e2e70ef25dacebae2558e1bbf +Subproject commit 7f1f3bcaae6090d7779b34f62ae8b4b570bb5165 From 94cf1c82c49c44c5ac05b65a48929f96b27f127a Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 23 Feb 2024 13:27:07 +0900 Subject: [PATCH 521/585] Synchronize DalamudStartInfo between cpp and cs (#1679) Dalamud Boot was using BootLogPath in place of LogPath, resulting in wrong log path. --- Dalamud.Boot/DalamudStartInfo.cpp | 7 +++++-- Dalamud.Boot/DalamudStartInfo.h | 7 +++++-- Dalamud.Boot/veh.cpp | 5 ++++- Dalamud.Common/DalamudStartInfo.cs | 1 + Dalamud.Injector/EntryPoint.cs | 1 + DalamudCrashHandler/DalamudCrashHandler.cpp | 1 - 6 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Dalamud.Boot/DalamudStartInfo.cpp b/Dalamud.Boot/DalamudStartInfo.cpp index d20265bf8..f5632a2ea 100644 --- a/Dalamud.Boot/DalamudStartInfo.cpp +++ b/Dalamud.Boot/DalamudStartInfo.cpp @@ -89,13 +89,16 @@ void from_json(const nlohmann::json& json, DalamudStartInfo& config) { config.DalamudLoadMethod = json.value("LoadMethod", config.DalamudLoadMethod); config.WorkingDirectory = json.value("WorkingDirectory", config.WorkingDirectory); config.ConfigurationPath = json.value("ConfigurationPath", config.ConfigurationPath); + config.LogPath = json.value("LogPath", config.LogPath); + config.LogName = json.value("LogName", config.LogName); config.PluginDirectory = json.value("PluginDirectory", config.PluginDirectory); - config.DefaultPluginDirectory = json.value("DefaultPluginDirectory", config.DefaultPluginDirectory); config.AssetDirectory = json.value("AssetDirectory", config.AssetDirectory); config.Language = json.value("Language", config.Language); config.GameVersion = json.value("GameVersion", config.GameVersion); - config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs); config.TroubleshootingPackData = json.value("TroubleshootingPackData", std::string{}); + config.DelayInitializeMs = json.value("DelayInitializeMs", config.DelayInitializeMs); + config.NoLoadPlugins = json.value("NoLoadPlugins", config.NoLoadPlugins); + config.NoLoadThirdPartyPlugins = json.value("NoLoadThirdPartyPlugins", config.NoLoadThirdPartyPlugins); config.BootLogPath = json.value("BootLogPath", config.BootLogPath); config.BootShowConsole = json.value("BootShowConsole", config.BootShowConsole); diff --git a/Dalamud.Boot/DalamudStartInfo.h b/Dalamud.Boot/DalamudStartInfo.h index 5cee8f16b..e6cc54ab0 100644 --- a/Dalamud.Boot/DalamudStartInfo.h +++ b/Dalamud.Boot/DalamudStartInfo.h @@ -35,13 +35,16 @@ struct DalamudStartInfo { LoadMethod DalamudLoadMethod = LoadMethod::Entrypoint; std::string WorkingDirectory; std::string ConfigurationPath; + std::string LogPath; + std::string LogName; std::string PluginDirectory; - std::string DefaultPluginDirectory; std::string AssetDirectory; ClientLanguage Language = ClientLanguage::English; std::string GameVersion; - int DelayInitializeMs = 0; std::string TroubleshootingPackData; + int DelayInitializeMs = 0; + bool NoLoadPlugins; + bool NoLoadThirdPartyPlugins; std::string BootLogPath; bool BootShowConsole = false; diff --git a/Dalamud.Boot/veh.cpp b/Dalamud.Boot/veh.cpp index 059189202..58234783a 100644 --- a/Dalamud.Boot/veh.cpp +++ b/Dalamud.Boot/veh.cpp @@ -112,13 +112,16 @@ static void append_injector_launch_args(std::vector& args) case DalamudStartInfo::LoadMethod::DllInject: args.emplace_back(L"--mode=inject"); } - args.emplace_back(L"--logpath=\"" + unicode::convert(g_startInfo.BootLogPath) + L"\""); args.emplace_back(L"--dalamud-working-directory=\"" + unicode::convert(g_startInfo.WorkingDirectory) + L"\""); args.emplace_back(L"--dalamud-configuration-path=\"" + unicode::convert(g_startInfo.ConfigurationPath) + L"\""); + args.emplace_back(L"--logpath=\"" + unicode::convert(g_startInfo.LogPath) + L"\""); + args.emplace_back(L"--logname=\"" + unicode::convert(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(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 + if (g_startInfo.BootShowConsole) args.emplace_back(L"--console"); if (g_startInfo.BootEnableEtw) diff --git a/Dalamud.Common/DalamudStartInfo.cs b/Dalamud.Common/DalamudStartInfo.cs index edf21d174..a84d3b68f 100644 --- a/Dalamud.Common/DalamudStartInfo.cs +++ b/Dalamud.Common/DalamudStartInfo.cs @@ -5,6 +5,7 @@ namespace Dalamud.Common; /// /// Struct containing information needed to initialize Dalamud. +/// Modify DalamudStartInfo.h and DalamudStartInfo.cpp along with this record. /// [Serializable] public record DalamudStartInfo diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index c784ec1d1..2d776b043 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -389,6 +389,7 @@ namespace Dalamud.Injector #else startInfo.LogPath ??= xivlauncherDir; #endif + startInfo.LogName ??= string.Empty; // Set boot defaults startInfo.BootShowConsole = args.Contains("--console"); diff --git a/DalamudCrashHandler/DalamudCrashHandler.cpp b/DalamudCrashHandler/DalamudCrashHandler.cpp index 3a69198b8..d4e9f0a1c 100644 --- a/DalamudCrashHandler/DalamudCrashHandler.cpp +++ b/DalamudCrashHandler/DalamudCrashHandler.cpp @@ -896,7 +896,6 @@ int main() { SYSTEMTIME st; GetLocalTime(&st); const auto dalamudLogPath = logDir.empty() ? std::filesystem::path() : logDir / L"Dalamud.log"; - const auto dalamudBootLogPath = logDir.empty() ? std::filesystem::path() : logDir / L"Dalamud.boot.log"; const auto dumpPath = logDir.empty() ? std::filesystem::path() : logDir / std::format(L"dalamud_appcrash_{:04}{:02}{:02}_{:02}{:02}{:02}_{:03}_{}.dmp", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, dwProcessId); const auto logPath = logDir.empty() ? std::filesystem::path() : logDir / std::format(L"dalamud_appcrash_{:04}{:02}{:02}_{:02}{:02}{:02}_{:03}_{}.log", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, dwProcessId); std::wstring dumpError; From c1c85e52366d29d23a30fb63e0fe3663981a6035 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 25 Feb 2024 12:23:01 +0100 Subject: [PATCH 522/585] [master] Update ClientStructs (#1680) 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 7f1f3bcaa..4cafdfead 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 7f1f3bcaae6090d7779b34f62ae8b4b570bb5165 +Subproject commit 4cafdfead3e22bfe4ad811dfb32401f2faea428b From f6be80a5fb309a0aaf64feac786d7f11944dabeb Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 21:21:50 +0900 Subject: [PATCH 523/585] Make IDalamudTextureWrap ICloneable --- .../Interface/Internal/DalamudTextureWrap.cs | 33 +------- .../Interface/Internal/IDalamudTextureWrap.cs | 47 +++++++++++ .../Interface/Internal/InterfaceManager.cs | 4 +- .../Interface/Internal/UnknownTextureWrap.cs | 77 +++++++++++++++++++ .../Windows/Data/Widgets/TexWidget.cs | 4 + Dalamud/Utility/IDeferredDisposable.cs | 12 +++ 6 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 Dalamud/Interface/Internal/IDalamudTextureWrap.cs create mode 100644 Dalamud/Interface/Internal/UnknownTextureWrap.cs create mode 100644 Dalamud/Utility/IDeferredDisposable.cs diff --git a/Dalamud/Interface/Internal/DalamudTextureWrap.cs b/Dalamud/Interface/Internal/DalamudTextureWrap.cs index 9737d9f7b..b49c6f07b 100644 --- a/Dalamud/Interface/Internal/DalamudTextureWrap.cs +++ b/Dalamud/Interface/Internal/DalamudTextureWrap.cs @@ -1,41 +1,14 @@ -using System.Numerics; +using Dalamud.Utility; using ImGuiScene; namespace Dalamud.Interface.Internal; -/// -/// Base TextureWrap interface for all Dalamud-owned texture wraps. -/// Used to avoid referencing ImGuiScene. -/// -public interface IDalamudTextureWrap : IDisposable -{ - /// - /// Gets a texture handle suitable for direct use with ImGui functions. - /// - IntPtr ImGuiHandle { get; } - - /// - /// Gets the width of the texture. - /// - int Width { get; } - - /// - /// Gets the height of the texture. - /// - int Height { get; } - - /// - /// Gets the size vector of the texture using Width, Height. - /// - Vector2 Size => new(this.Width, this.Height); -} - /// /// Safety harness for ImGuiScene textures that will defer destruction until /// the end of the frame. /// -public class DalamudTextureWrap : IDalamudTextureWrap +public class DalamudTextureWrap : IDalamudTextureWrap, IDeferredDisposable { private readonly TextureWrap wrappedWrap; @@ -83,7 +56,7 @@ public class DalamudTextureWrap : IDalamudTextureWrap /// /// Actually dispose the wrapped texture. /// - internal void RealDispose() + void IDeferredDisposable.RealDispose() { this.wrappedWrap.Dispose(); } diff --git a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs b/Dalamud/Interface/Internal/IDalamudTextureWrap.cs new file mode 100644 index 000000000..60d96534d --- /dev/null +++ b/Dalamud/Interface/Internal/IDalamudTextureWrap.cs @@ -0,0 +1,47 @@ +using System.Numerics; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Internal; + +/// +/// Base TextureWrap interface for all Dalamud-owned texture wraps. +/// Used to avoid referencing ImGuiScene. +/// +public interface IDalamudTextureWrap : IDisposable, ICloneable +{ + /// + /// Gets a texture handle suitable for direct use with ImGui functions. + /// + IntPtr ImGuiHandle { get; } + + /// + /// Gets the width of the texture. + /// + int Width { get; } + + /// + /// Gets the height of the texture. + /// + int Height { get; } + + /// + /// Gets the size vector of the texture using Width, Height. + /// + Vector2 Size => new(this.Width, this.Height); + + /// + /// Creates a new reference to this texture wrap. + /// + /// The new reference to this texture wrap. + /// The default implementation will treat as an . + new unsafe IDalamudTextureWrap Clone() + { + // Dalamud specific: IDalamudTextureWrap always points to an ID3D11ShaderResourceView. + var handle = (IUnknown*)this.ImGuiHandle; + return new UnknownTextureWrap(handle, this.Width, this.Height, true); + } + + /// + object ICloneable.Clone() => this.Clone(); +} diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6d93b4bd7..3db799be0 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -62,7 +62,7 @@ internal class InterfaceManager : IDisposable, IServiceType ///
public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; - private readonly ConcurrentBag deferredDisposeTextures = new(); + private readonly ConcurrentBag deferredDisposeTextures = new(); private readonly ConcurrentBag deferredDisposeImFontLockeds = new(); [ServiceManager.ServiceDependency] @@ -402,7 +402,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// Enqueue a texture to be disposed at the end of the frame. ///
/// The texture. - public void EnqueueDeferredDispose(DalamudTextureWrap wrap) + public void EnqueueDeferredDispose(IDeferredDisposable wrap) { this.deferredDisposeTextures.Add(wrap); } diff --git a/Dalamud/Interface/Internal/UnknownTextureWrap.cs b/Dalamud/Interface/Internal/UnknownTextureWrap.cs new file mode 100644 index 000000000..41164f2c3 --- /dev/null +++ b/Dalamud/Interface/Internal/UnknownTextureWrap.cs @@ -0,0 +1,77 @@ +using System.Threading; + +using Dalamud.Utility; + +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Internal; + +/// +/// A texture wrap that is created by cloning the underlying . +/// +internal sealed unsafe class UnknownTextureWrap : IDalamudTextureWrap, IDeferredDisposable +{ + private IntPtr imGuiHandle; + + /// + /// Initializes a new instance of the class. + /// + /// The pointer to that is suitable for use with + /// . + /// The width of the texture. + /// The height of the texture. + /// If true, call . + public UnknownTextureWrap(IUnknown* unknown, int width, int height, bool callAddRef) + { + ObjectDisposedException.ThrowIf(unknown is null, typeof(IUnknown)); + this.imGuiHandle = (nint)unknown; + this.Width = width; + this.Height = height; + if (callAddRef) + unknown->AddRef(); + } + + /// + /// Finalizes an instance of the class. + /// + ~UnknownTextureWrap() => this.Dispose(false); + + /// + public nint ImGuiHandle => + this.imGuiHandle == nint.Zero + ? throw new ObjectDisposedException(nameof(UnknownTextureWrap)) + : this.imGuiHandle; + + /// + public int Width { get; } + + /// + public int Height { get; } + + /// + /// Queue the texture to be disposed once the frame ends. + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Actually dispose the wrapped texture. + /// + void IDeferredDisposable.RealDispose() + { + var handle = Interlocked.Exchange(ref this.imGuiHandle, nint.Zero); + if (handle != nint.Zero) + ((IUnknown*)handle)->Release(); + } + + private void Dispose(bool disposing) + { + if (disposing) + Service.GetNullable()?.EnqueueDeferredDispose(this); + else + ((IDeferredDisposable)this).RealDispose(); + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 0cbc401e7..173e5409a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -119,6 +119,10 @@ internal class TexWidget : IDataWindowWidget if (ImGui.Button($"X##{i}")) toRemove = tex; + + ImGui.SameLine(); + if (ImGui.Button($"Clone##{i}")) + this.addedTextures.Add(tex.Clone()); } } diff --git a/Dalamud/Utility/IDeferredDisposable.cs b/Dalamud/Utility/IDeferredDisposable.cs new file mode 100644 index 000000000..41a7dd8d3 --- /dev/null +++ b/Dalamud/Utility/IDeferredDisposable.cs @@ -0,0 +1,12 @@ +namespace Dalamud.Utility; + +/// +/// An extension of which makes queue +/// to be called at a later time. +/// +internal interface IDeferredDisposable : IDisposable +{ + /// Actually dispose the object. + /// Not to be called from the code that uses the end object. + void RealDispose(); +} From 9629a555be670fe87db8c2d51bceb285fe44776d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 03:20:28 +0900 Subject: [PATCH 524/585] Rename to CreateWrapSharingLowLevelResource --- .../Interface/Internal/IDalamudTextureWrap.cs | 22 +++++++++++++------ .../Windows/Data/Widgets/TexWidget.cs | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs b/Dalamud/Interface/Internal/IDalamudTextureWrap.cs index 60d96534d..8e2e56c26 100644 --- a/Dalamud/Interface/Internal/IDalamudTextureWrap.cs +++ b/Dalamud/Interface/Internal/IDalamudTextureWrap.cs @@ -8,7 +8,7 @@ namespace Dalamud.Interface.Internal; /// Base TextureWrap interface for all Dalamud-owned texture wraps. /// Used to avoid referencing ImGuiScene. ///
-public interface IDalamudTextureWrap : IDisposable, ICloneable +public interface IDalamudTextureWrap : IDisposable { /// /// Gets a texture handle suitable for direct use with ImGui functions. @@ -31,17 +31,25 @@ public interface IDalamudTextureWrap : IDisposable, ICloneable Vector2 Size => new(this.Width, this.Height); /// - /// Creates a new reference to this texture wrap. + /// Creates a new reference to the resource being pointed by this instance of . /// /// The new reference to this texture wrap. - /// The default implementation will treat as an . - new unsafe IDalamudTextureWrap Clone() + /// + /// On calling this function, a new instance of will be returned, but with + /// the same . The new instance must be d, as the backing + /// resource will stay alive until all the references are released. The old instance may be disposed as needed, + /// once this function returns; the new instance will stay alive regardless of whether the old instance has been + /// disposed.
+ /// Primary purpose of this function is to share textures across plugin boundaries. When texture wraps get passed + /// across plugin boundaries for use for an indeterminate duration, the receiver should call this function to + /// obtain a new reference to the texture received, so that it gets its own "copy" of the texture and the caller + /// may dispose the texture anytime without any care for the receiver.
+ /// The default implementation will treat as an . + ///
+ unsafe IDalamudTextureWrap CreateWrapSharingLowLevelResource() { // Dalamud specific: IDalamudTextureWrap always points to an ID3D11ShaderResourceView. var handle = (IUnknown*)this.ImGuiHandle; return new UnknownTextureWrap(handle, this.Width, this.Height, true); } - - /// - object ICloneable.Clone() => this.Clone(); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 173e5409a..8d6879ac1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -122,7 +122,7 @@ internal class TexWidget : IDataWindowWidget ImGui.SameLine(); if (ImGui.Button($"Clone##{i}")) - this.addedTextures.Add(tex.Clone()); + this.addedTextures.Add(tex.CreateWrapSharingLowLevelResource()); } } From 12e2fd3f60f733f5d20775a0b4dc1e8a91727273 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 27 Feb 2024 19:48:32 +0900 Subject: [PATCH 525/585] Miscellaneous fixes --- Dalamud/Interface/UiBuilder.cs | 6 +- Dalamud/Utility/DisposeSafety.cs | 159 ++++++++++++++++++++----------- 2 files changed, 109 insertions(+), 56 deletions(-) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 7a3eb6fb6..d260868a0 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -212,7 +212,7 @@ public sealed class UiBuilder : IDisposable /// /// fontAtlas.NewDelegateFontHandle( /// e => e.OnPreBuild( - /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePt))); + /// tk => tk.AddDalamudDefaultFont(UiBuilder.DefaultFontSizePx))); /// /// public IFontHandle DefaultFontHandle => @@ -231,6 +231,8 @@ public sealed class UiBuilder : IDisposable /// fontAtlas.NewDelegateFontHandle( /// e => e.OnPreBuild( /// tk => tk.AddFontAwesomeIconFont(new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// // or use + /// tk => tk.AddFontAwesomeIconFont(new() { SizePx = UiBuilder.DefaultFontSizePx }))); /// /// public IFontHandle IconFontHandle => @@ -251,6 +253,8 @@ public sealed class UiBuilder : IDisposable /// tk => tk.AddDalamudAssetFont( /// DalamudAsset.InconsolataRegular, /// new() { SizePt = UiBuilder.DefaultFontSizePt }))); + /// // or use + /// new() { SizePx = UiBuilder.DefaultFontSizePx }))); /// /// public IFontHandle MonoFontHandle => diff --git a/Dalamud/Utility/DisposeSafety.cs b/Dalamud/Utility/DisposeSafety.cs index 909c4e932..8ac891e0a 100644 --- a/Dalamud/Utility/DisposeSafety.cs +++ b/Dalamud/Utility/DisposeSafety.cs @@ -39,21 +39,23 @@ public static class DisposeSafety public static IDisposable ToDisposableIgnoreExceptions(this Task task) where T : IDisposable { - return Disposable.Create(() => task.ContinueWith(r => - { - _ = r.Exception; - if (r.IsCompleted) - { - try + return Disposable.Create( + () => task.ContinueWith( + r => { - r.Dispose(); - } - catch - { - // ignore - } - } - })); + _ = r.Exception; + if (r.IsCompleted) + { + try + { + r.Dispose(); + } + catch + { + // ignore + } + } + })); } /// @@ -102,25 +104,26 @@ public static class DisposeSafety if (disposables is not T[] array) array = disposables?.ToArray() ?? Array.Empty(); - return Disposable.Create(() => - { - List exceptions = null; - foreach (var d in array) + return Disposable.Create( + () => { - try + List exceptions = null; + foreach (var d in array) { - d?.Dispose(); + try + { + d?.Dispose(); + } + catch (Exception de) + { + exceptions ??= new(); + exceptions.Add(de); + } } - catch (Exception de) - { - exceptions ??= new(); - exceptions.Add(de); - } - } - if (exceptions is not null) - throw new AggregateException(exceptions); - }); + if (exceptions is not null) + throw new AggregateException(exceptions); + }); } /// @@ -137,7 +140,11 @@ public static class DisposeSafety public event Action? AfterDispose; /// - public void EnsureCapacity(int capacity) => this.objects.EnsureCapacity(capacity); + public void EnsureCapacity(int capacity) + { + lock (this.objects) + this.objects.EnsureCapacity(capacity); + } /// /// The parameter. @@ -145,7 +152,10 @@ public static class DisposeSafety public T? Add(T? d) where T : IDisposable { if (d is not null) - this.objects.Add(this.CheckAdd(d)); + { + lock (this.objects) + this.objects.Add(this.CheckAdd(d)); + } return d; } @@ -155,7 +165,10 @@ public static class DisposeSafety public Action? Add(Action? d) { if (d is not null) - this.objects.Add(this.CheckAdd(d)); + { + lock (this.objects) + this.objects.Add(this.CheckAdd(d)); + } return d; } @@ -165,7 +178,10 @@ public static class DisposeSafety public Func? Add(Func? d) { if (d is not null) - this.objects.Add(this.CheckAdd(d)); + { + lock (this.objects) + this.objects.Add(this.CheckAdd(d)); + } return d; } @@ -174,7 +190,10 @@ public static class DisposeSafety public GCHandle Add(GCHandle d) { if (d != default) - this.objects.Add(this.CheckAdd(d)); + { + lock (this.objects) + this.objects.Add(this.CheckAdd(d)); + } return d; } @@ -183,29 +202,41 @@ public static class DisposeSafety /// Queue all the given to be disposed later. /// /// Disposables. - public void AddRange(IEnumerable ds) => - this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + public void AddRange(IEnumerable ds) + { + lock (this.objects) + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + } /// /// Queue all the given to be run later. /// /// Actions. - public void AddRange(IEnumerable ds) => - this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + public void AddRange(IEnumerable ds) + { + lock (this.objects) + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + } /// /// Queue all the given returning to be run later. /// /// Func{Task}s. - public void AddRange(IEnumerable?> ds) => - this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + public void AddRange(IEnumerable?> ds) + { + lock (this.objects) + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + } /// /// Queue all the given to be disposed later. /// /// GCHandles. - public void AddRange(IEnumerable ds) => - this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d))); + public void AddRange(IEnumerable ds) + { + lock (this.objects) + this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d))); + } /// /// Cancel all pending disposals. @@ -213,9 +244,12 @@ public static class DisposeSafety /// Use this after successful initialization of multiple disposables. public void Cancel() { - foreach (var o in this.objects) - this.CheckRemove(o); - this.objects.Clear(); + lock (this.objects) + { + foreach (var o in this.objects) + this.CheckRemove(o); + this.objects.Clear(); + } } /// @@ -264,11 +298,17 @@ public static class DisposeSafety this.BeforeDispose?.InvokeSafely(this); List? exceptions = null; - while (this.objects.Any()) + while (true) { - var obj = this.objects[^1]; - this.objects.RemoveAt(this.objects.Count - 1); - + object obj; + lock (this.objects) + { + if (this.objects.Count == 0) + break; + obj = this.objects[^1]; + this.objects.RemoveAt(this.objects.Count - 1); + } + try { switch (obj) @@ -294,7 +334,8 @@ public static class DisposeSafety } } - this.objects.TrimExcess(); + lock (this.objects) + this.objects.TrimExcess(); if (exceptions is not null) { @@ -318,10 +359,16 @@ public static class DisposeSafety this.BeforeDispose?.InvokeSafely(this); List? exceptions = null; - while (this.objects.Any()) + while (true) { - var obj = this.objects[^1]; - this.objects.RemoveAt(this.objects.Count - 1); + object obj; + lock (this.objects) + { + if (this.objects.Count == 0) + break; + obj = this.objects[^1]; + this.objects.RemoveAt(this.objects.Count - 1); + } try { @@ -351,7 +398,8 @@ public static class DisposeSafety } } - this.objects.TrimExcess(); + lock (this.objects) + this.objects.TrimExcess(); if (exceptions is not null) { @@ -386,7 +434,8 @@ public static class DisposeSafety private void OnItemDisposed(IDisposeCallback obj) { obj.BeforeDispose -= this.OnItemDisposed; - this.objects.Remove(obj); + lock (this.objects) + this.objects.Remove(obj); } } } From e6c97f0f181e518278264e2ba92f5319a446c70e Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:03:09 +0100 Subject: [PATCH 526/585] Update ClientStructs (#1685) 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 4cafdfead..722a2c512 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 4cafdfead3e22bfe4ad811dfb32401f2faea428b +Subproject commit 722a2c512238ac4b5324e3d343b316d8c8633a02 From 0651c643b151389d0d313f1188cee256ab81af62 Mon Sep 17 00:00:00 2001 From: AzureGem Date: Tue, 27 Feb 2024 13:15:11 -0500 Subject: [PATCH 527/585] Limit console log lines held in memory (#1683) * Add AG.Collections.RollingList * Use RollingList for logs + Adaption changes * Create Dalamud.Utility.ThrowHelper * Create Dalamud.Utility.RollingList * ConsoleWindow: Remove dependency * Remove NuGet Dependency * Add Log Lines Limit configuration * Use Log Lines Limit configuration and handle changes * Make log lines limit configurable --- .../Internal/DalamudConfiguration.cs | 5 + .../Internal/Windows/ConsoleWindow.cs | 99 ++++++-- Dalamud/Utility/RollingList.cs | 234 ++++++++++++++++++ Dalamud/Utility/ThrowHelper.cs | 52 ++++ 4 files changed, 375 insertions(+), 15 deletions(-) create mode 100644 Dalamud/Utility/RollingList.cs create mode 100644 Dalamud/Utility/ThrowHelper.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 957be12b9..85a9507c9 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -215,6 +215,11 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// public bool LogOpenAtStartup { get; set; } + /// + /// Gets or sets the number of lines to keep for the Dalamud Console window. + /// + public int LogLinesLimit { get; set; } = 10000; + /// /// Gets or sets a value indicating whether or not the dev bar should open at startup. /// diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index bf559c4d7..f36d79222 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -6,6 +6,7 @@ using System.Numerics; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; +using System.Threading; using Dalamud.Configuration.Internal; using Dalamud.Game.Command; @@ -28,7 +29,11 @@ namespace Dalamud.Interface.Internal.Windows; /// internal class ConsoleWindow : Window, IDisposable { - private readonly List logText = new(); + private const int LogLinesMinimum = 100; + private const int LogLinesMaximum = 1000000; + + private readonly RollingList logText; + private volatile int newRolledLines; private readonly object renderLock = new(); private readonly List history = new(); @@ -42,12 +47,14 @@ internal class ConsoleWindow : Window, IDisposable private string pluginFilter = string.Empty; private bool filterShowUncaughtExceptions; + private bool settingsPopupWasOpen; private bool showFilterToolbar; private bool clearLog; private bool copyLog; private bool copyMode; private bool killGameArmed; private bool autoScroll; + private int logLinesLimit; private bool autoOpen; private bool regexError; @@ -74,9 +81,17 @@ internal class ConsoleWindow : Window, IDisposable }; this.RespectCloseHotkey = false; + + this.logLinesLimit = configuration.LogLinesLimit; + + var limit = Math.Max(LogLinesMinimum, this.logLinesLimit); + this.logText = new(limit); + this.FilteredLogEntries = new(limit); + + configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved; } - private List FilteredLogEntries { get; set; } = new(); + private RollingList FilteredLogEntries { get; set; } /// public override void OnOpen() @@ -91,6 +106,7 @@ internal class ConsoleWindow : Window, IDisposable public void Dispose() { SerilogEventSink.Instance.LogLine -= this.OnLogLine; + Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; } /// @@ -180,6 +196,9 @@ internal class ConsoleWindow : Window, IDisposable var dividerOffset = ImGui.CalcTextSize("00:00:00.000 | AAA ").X + (ImGui.CalcTextSize(" ").X / 2); var cursorLogLine = ImGui.CalcTextSize("00:00:00.000 | AAA | ").X; + var lastLinePosY = 0.0f; + var logLineHeight = 0.0f; + lock (this.renderLock) { clipper.Begin(this.FilteredLogEntries.Count); @@ -187,7 +206,8 @@ internal class ConsoleWindow : Window, IDisposable { for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) { - var line = this.FilteredLogEntries[i]; + var index = Math.Max(i - this.newRolledLines, 0); // Prevents flicker effect. Also workaround to avoid negative indexes. + var line = this.FilteredLogEntries[index]; if (!line.IsMultiline && !this.copyLog) ImGui.Separator(); @@ -228,6 +248,10 @@ internal class ConsoleWindow : Window, IDisposable ImGui.SetCursorPosX(cursorLogLine); ImGui.TextUnformatted(line.Line); + + var currentLinePosY = ImGui.GetCursorPosY(); + logLineHeight = currentLinePosY - lastLinePosY; + lastLinePosY = currentLinePosY; } } @@ -239,6 +263,12 @@ internal class ConsoleWindow : Window, IDisposable ImGui.PopStyleVar(); + var newRolledLinesCount = Interlocked.Exchange(ref this.newRolledLines, 0); + if (!this.autoScroll || ImGui.GetScrollY() < ImGui.GetScrollMaxY()) + { + ImGui.SetScrollY(ImGui.GetScrollY() - (logLineHeight * newRolledLinesCount)); + } + if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY()) { ImGui.SetScrollHereY(1.0f); @@ -363,21 +393,21 @@ internal class ConsoleWindow : Window, IDisposable ImGui.SameLine(); - this.autoScroll = configuration.LogAutoScroll; - if (this.DrawToggleButtonWithTooltip("auto_scroll", "Auto-scroll", FontAwesomeIcon.Sync, ref this.autoScroll)) + var settingsPopup = ImGui.BeginPopup("##console_settings"); + if (settingsPopup) { - configuration.LogAutoScroll = !configuration.LogAutoScroll; - configuration.QueueSave(); + this.DrawSettingsPopup(configuration); + ImGui.EndPopup(); + } + else if (this.settingsPopupWasOpen) + { + // Prevent side effects in case Apply wasn't clicked + this.logLinesLimit = configuration.LogLinesLimit; } - ImGui.SameLine(); + this.settingsPopupWasOpen = settingsPopup; - this.autoOpen = configuration.LogOpenAtStartup; - if (this.DrawToggleButtonWithTooltip("auto_open", "Open at startup", FontAwesomeIcon.WindowRestore, ref this.autoOpen)) - { - configuration.LogOpenAtStartup = !configuration.LogOpenAtStartup; - configuration.QueueSave(); - } + if (this.DrawToggleButtonWithTooltip("show_settings", "Show settings", FontAwesomeIcon.List, ref settingsPopup)) ImGui.OpenPopup("##console_settings"); ImGui.SameLine(); @@ -447,6 +477,33 @@ internal class ConsoleWindow : Window, IDisposable } } + private void DrawSettingsPopup(DalamudConfiguration configuration) + { + if (ImGui.Checkbox("Open at startup", ref this.autoOpen)) + { + configuration.LogOpenAtStartup = this.autoOpen; + configuration.QueueSave(); + } + + if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) + { + configuration.LogAutoScroll = this.autoScroll; + configuration.QueueSave(); + } + + ImGui.TextUnformatted("Logs buffer"); + ImGui.SliderInt("lines", ref this.logLinesLimit, LogLinesMinimum, LogLinesMaximum); + if (ImGui.Button("Apply")) + { + this.logLinesLimit = Math.Max(LogLinesMinimum, this.logLinesLimit); + + configuration.LogLinesLimit = this.logLinesLimit; + configuration.QueueSave(); + + ImGui.CloseCurrentPopup(); + } + } + private void DrawFilterToolbar() { if (!this.showFilterToolbar) return; @@ -686,8 +743,12 @@ internal class ConsoleWindow : Window, IDisposable this.logText.Add(entry); + var avoidScroll = this.FilteredLogEntries.Count == this.FilteredLogEntries.Size; if (this.IsFilterApplicable(entry)) + { this.FilteredLogEntries.Add(entry); + if (avoidScroll) Interlocked.Increment(ref this.newRolledLines); + } } private bool IsFilterApplicable(LogEntry entry) @@ -740,7 +801,7 @@ internal class ConsoleWindow : Window, IDisposable lock (this.renderLock) { this.regexError = false; - this.FilteredLogEntries = this.logText.Where(this.IsFilterApplicable).ToList(); + this.FilteredLogEntries = new RollingList(this.logText.Where(this.IsFilterApplicable), Math.Max(LogLinesMinimum, this.logLinesLimit)); } } @@ -789,6 +850,14 @@ internal class ConsoleWindow : Window, IDisposable return result; } + private void OnDalamudConfigurationSaved(DalamudConfiguration dalamudConfiguration) + { + this.logLinesLimit = dalamudConfiguration.LogLinesLimit; + var limit = Math.Max(LogLinesMinimum, this.logLinesLimit); + this.logText.Size = limit; + this.FilteredLogEntries.Size = limit; + } + private class LogEntry { public string Line { get; init; } = string.Empty; diff --git a/Dalamud/Utility/RollingList.cs b/Dalamud/Utility/RollingList.cs new file mode 100644 index 000000000..9ca012be4 --- /dev/null +++ b/Dalamud/Utility/RollingList.cs @@ -0,0 +1,234 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Dalamud.Utility +{ + /// + /// A list with limited capacity holding items of type . + /// Adding further items will result in the list rolling over. + /// + /// Item type. + /// + /// Implemented as a circular list using a internally. + /// Insertions and Removals are not supported. + /// Not thread-safe. + /// + internal class RollingList : IList + { + private List items; + private int size; + private int firstIndex; + + /// Initializes a new instance of the class. + /// size. + /// Internal initial capacity. + public RollingList(int size, int capacity) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0); + capacity = Math.Min(capacity, size); + this.size = size; + this.items = new List(capacity); + } + + /// Initializes a new instance of the class. + /// size. + public RollingList(int size) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(size), size, 0); + this.size = size; + this.items = new(); + } + + /// Initializes a new instance of the class. + /// Collection where elements are copied from. + /// size. + public RollingList(IEnumerable items, int size) + { + if (!items.TryGetNonEnumeratedCount(out var capacity)) capacity = 4; + capacity = Math.Min(capacity, size); + this.size = size; + this.items = new List(capacity); + this.AddRange(items); + } + + /// Initializes a new instance of the class. + /// Collection where elements are copied from. + /// size. + /// Internal initial capacity. + public RollingList(IEnumerable items, int size, int capacity) + { + if (items.TryGetNonEnumeratedCount(out var count) && count > capacity) capacity = count; + capacity = Math.Min(capacity, size); + this.size = size; + this.items = new List(capacity); + this.AddRange(items); + } + + /// Gets item count. + public int Count => this.items.Count; + + /// Gets or sets the internal list capacity. + public int Capacity + { + get => this.items.Capacity; + set => this.items.Capacity = Math.Min(value, this.size); + } + + /// Gets or sets rolling list size. + public int Size + { + get => this.size; + set + { + if (value == this.size) return; + if (value > this.size) + { + if (this.firstIndex > 0) + { + this.items = new List(this); + this.firstIndex = 0; + } + } + else // value < this._size + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(value), value, 0); + if (value < this.Count) + { + this.items = new List(this.TakeLast(value)); + this.firstIndex = 0; + } + } + + this.size = value; + } + } + + /// Gets a value indicating whether the item is read only. + public bool IsReadOnly => false; + + /// Gets or sets an item by index. + /// Item index. + /// Item at specified index. + public T this[int index] + { + get + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(nameof(index), index, this.Count); + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(index), index, 0); + return this.items[this.GetRealIndex(index)]; + } + + set + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(nameof(index), index, this.Count); + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(index), index, 0); + this.items[this.GetRealIndex(index)] = value; + } + } + + /// Adds an item to this . + /// Item to add. + public void Add(T item) + { + if (this.size == 0) return; + if (this.items.Count >= this.size) + { + this.items[this.firstIndex] = item; + this.firstIndex = (this.firstIndex + 1) % this.size; + } + else + { + if (this.items.Count == this.items.Capacity) + { + // Manual list capacity resize + var newCapacity = Math.Max(Math.Min(this.size, this.items.Capacity * 2), this.items.Capacity); + this.items.Capacity = newCapacity; + } + + this.items.Add(item); + } + + Debug.Assert(this.items.Count <= this.size, "Item count should be less than Size"); + } + + /// Add items to this . + /// Items to add. + public void AddRange(IEnumerable items) + { + if (this.size == 0) return; + foreach (var item in items) this.Add(item); + } + + /// Removes all elements from the + public void Clear() + { + this.items.Clear(); + this.firstIndex = 0; + } + + /// Find the index of a specific item. + /// item to find. + /// Index where is found. -1 if not found. + public int IndexOf(T item) + { + var index = this.items.IndexOf(item); + if (index == -1) return -1; + return this.GetVirtualIndex(index); + } + + /// Not supported. + [SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")] + void IList.Insert(int index, T item) => throw new NotSupportedException(); + + /// Not supported. + [SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")] + void IList.RemoveAt(int index) => throw new NotSupportedException(); + + /// Find wether an item exists. + /// item to find. + /// Wether is found. + public bool Contains(T item) => this.items.Contains(item); + + /// Copies the content of this list into an array. + /// Array to copy into. + /// index to start coping into. + public void CopyTo(T[] array, int arrayIndex) + { + ThrowHelper.ThrowArgumentOutOfRangeExceptionIfLessThan(nameof(arrayIndex), arrayIndex, 0); + if (array.Length - arrayIndex < this.Count) ThrowHelper.ThrowArgumentException("Not enough space"); + for (var index = 0; index < this.Count; index++) + { + array[arrayIndex++] = this[index]; + } + } + + /// Not supported. + [SuppressMessage("Documentation Rules", "SA1611", Justification = "Not supported")] + [SuppressMessage("Documentation Rules", "SA1615", Justification = "Not supported")] + bool ICollection.Remove(T item) => throw new NotSupportedException(); + + /// Gets an enumerator for this . + /// enumerator. + public IEnumerator GetEnumerator() + { + for (var index = 0; index < this.items.Count; index++) + { + yield return this.items[this.GetRealIndex(index)]; + } + } + + /// Gets an enumerator for this . + /// enumerator. + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetRealIndex(int index) => this.size > 0 ? (index + this.firstIndex) % this.size : 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int GetVirtualIndex(int index) => this.size > 0 ? (this.size + index - this.firstIndex) % this.size : 0; + } +} diff --git a/Dalamud/Utility/ThrowHelper.cs b/Dalamud/Utility/ThrowHelper.cs new file mode 100644 index 000000000..647aa92c0 --- /dev/null +++ b/Dalamud/Utility/ThrowHelper.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Dalamud.Utility +{ + /// Helper methods for throwing exceptions. + internal static class ThrowHelper + { + /// Throws a with a specified . + /// Message for the exception. + /// Thrown by this method. + [DoesNotReturn] + public static void ThrowArgumentException(string message) => throw new ArgumentException(message); + + /// Throws a with a specified for a specified . + /// Parameter name. + /// Message for the exception. + /// Thrown by this method. + [DoesNotReturn] + public static void ThrowArgumentOutOfRangeException(string paramName, string message) => throw new ArgumentOutOfRangeException(paramName, message); + + /// Throws a if the specified is less than . + /// value type. + /// Parameter name. + /// Value to compare from. + /// Value to compare with. + /// Thrown by this method if is less than . + public static void ThrowArgumentOutOfRangeExceptionIfLessThan(string paramName, T value, T comparand) where T : IComparable + { +#if NET8_0_OR_GREATER + ArgumentOutOfRangeException.ThrowIfLessThan(value, comparand); +#else + if (Comparer.Default.Compare(value, comparand) <= -1) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be greater than or equal {comparand}"); +#endif + } + + /// Throws a if the specified is greater than or equal to . + /// value type. + /// Parameter name. + /// Value to compare from. + /// Value to compare with. + /// Thrown by this method if is greater than or equal to. + public static void ThrowArgumentOutOfRangeExceptionIfGreaterThanOrEqual(string paramName, T value, T comparand) where T : IComparable + { +#if NET8_0_OR_GREATER + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(value, comparand); +#else + if (Comparer.Default.Compare(value, comparand) >= 0) ThrowArgumentOutOfRangeException(paramName, $"{paramName} must be less than {comparand}"); +#endif + } + } +} From 3ba395bd70c7cbe0d8630840e3e17d3e4a4bf9d3 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:31:13 +0900 Subject: [PATCH 528/585] Implement INotificationManager --- .../ImGuiNotification/IActiveNotification.cs | 109 ++++ .../ImGuiNotification/INotification.cs | 75 +++ .../ImGuiNotification/Notification.cs | 35 ++ .../NotificationDismissReason.cs | 22 + .../NotificationDismissedDelegate.cs | 10 + .../Notifications/ActiveNotification.cs | 508 ++++++++++++++++++ .../Notifications/NotificationConstants.cs | 74 +++ .../Notifications/NotificationManager.cs | 387 ++++--------- .../Windows/Data/Widgets/ImGuiWidget.cs | 82 ++- Dalamud/Interface/UiBuilder.cs | 51 +- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- .../Plugin/Services/INotificationManager.cs | 16 + 12 files changed, 1064 insertions(+), 307 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/IActiveNotification.cs create mode 100644 Dalamud/Interface/ImGuiNotification/INotification.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Notification.cs create mode 100644 Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs create mode 100644 Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs create mode 100644 Dalamud/Interface/Internal/Notifications/ActiveNotification.cs create mode 100644 Dalamud/Interface/Internal/Notifications/NotificationConstants.cs create mode 100644 Dalamud/Plugin/Services/INotificationManager.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs new file mode 100644 index 000000000..3e8aef196 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -0,0 +1,109 @@ +using System.Threading; + +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Represents an active notification. +/// +public interface IActiveNotification : INotification +{ + /// + /// The counter for field. + /// + private static long idCounter; + + /// + /// Invoked upon dismissing the notification. + /// + /// + /// The event callback will not be called, if a user interacts with the notification after the plugin is unloaded. + /// + event NotificationDismissedDelegate Dismiss; + + /// + /// Invoked upon clicking on the notification. + /// + /// + /// This event is not applicable when is set to false. + /// Note that this function may be called even after has been invoked. + /// Refer to . + /// + event Action Click; + + /// + /// Invoked when the mouse enters the notification window. + /// + /// + /// This event is applicable regardless of . + /// Note that this function may be called even after has been invoked. + /// Refer to . + /// + event Action MouseEnter; + + /// + /// Invoked when the mouse leaves the notification window. + /// + /// + /// This event is applicable regardless of . + /// Note that this function may be called even after has been invoked. + /// Refer to . + /// + event Action MouseLeave; + + /// + /// Invoked upon drawing the action bar of the notification. + /// + /// + /// This event is applicable regardless of . + /// Note that this function may be called even after has been invoked. + /// Refer to . + /// + event Action DrawActions; + + /// + /// Gets the ID of this notification. + /// + long Id { get; } + + /// + /// Gets a value indicating whether the mouse cursor is on the notification window. + /// + bool IsMouseHovered { get; } + + /// + /// Gets a value indicating whether the notification has been dismissed. + /// This includes when the hide animation is being played. + /// + bool IsDismissed { get; } + + /// + /// Clones this notification as a . + /// + /// A new instance of . + Notification CloneNotification(); + + /// + /// Dismisses this notification. + /// + void DismissNow(); + + /// + /// Updates the notification data. + /// + /// + /// Call to update the icon using the new . + /// + /// The new notification entry. + void Update(INotification newNotification); + + /// + /// Loads the icon again using . + /// + void UpdateIcon(); + + /// + /// Generates a new value to use for . + /// + /// The new value. + internal static long CreateNewId() => Interlocked.Increment(ref idCounter); +} diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs new file mode 100644 index 000000000..f5f66725c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -0,0 +1,75 @@ +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; + +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Represents a notification. +/// +public interface INotification +{ + /// + /// Gets the content body of the notification. + /// + string Content { get; } + + /// + /// Gets the title of the notification. + /// + string? Title { get; } + + /// + /// Gets the type of the notification. + /// + NotificationType Type { get; } + + /// + /// Gets the icon creator function for the notification.
+ /// Currently , , and types + /// are accepted. + ///
+ /// + /// The icon created by the task returned will be owned by Dalamud, + /// i.e. it will be d automatically as needed.
+ /// If null is supplied for this property or of the returned task + /// is false, then the corresponding icon with will be used.
+ /// Use if you have an instance of that you + /// can transfer ownership to Dalamud and is available for use right away. + ///
+ Func>? IconCreator { get; } + + /// + /// Gets the expiry. + /// + DateTime Expiry { get; } + + /// + /// Gets a value indicating whether this notification may be interacted. + /// + /// + /// Set this value to true if you want to respond to user inputs from + /// . + /// Note that the close buttons for notifications are always provided and interactible. + /// + bool Interactible { get; } + + /// + /// Gets a value indicating whether clicking on the notification window counts as dismissing the notification. + /// + /// + /// This property has no effect if is false. + /// + bool ClickIsDismiss { get; } + + /// + /// Gets the new duration for this notification if mouse cursor is on the notification window. + /// If set to or less, then this feature is turned off. + /// + /// + /// This property is applicable regardless of . + /// + TimeSpan HoverExtendDuration { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs new file mode 100644 index 000000000..fb2caa4f6 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.Internal.Notifications; + +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Represents a blueprint for a notification. +/// +public sealed record Notification : INotification +{ + /// + public string Content { get; set; } = string.Empty; + + /// + public string? Title { get; set; } + + /// + public NotificationType Type { get; set; } = NotificationType.None; + + /// + public Func>? IconCreator { get; set; } + + /// + public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + + /// + public bool Interactible { get; set; } + + /// + public bool ClickIsDismiss { get; set; } = true; + + /// + public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; +} diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs new file mode 100644 index 000000000..6e2fa338e --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs @@ -0,0 +1,22 @@ +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Specifies the reason of dismissal for a notification. +/// +public enum NotificationDismissReason +{ + /// + /// The notification is dismissed because the expiry specified from is met. + /// + Timeout = 1, + + /// + /// The notification is dismissed because the user clicked on the close button on a notification window. + /// + Manual = 2, + + /// + /// The notification is dismissed from calling . + /// + Programmatical = 3, +} diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs new file mode 100644 index 000000000..5e899c32c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs @@ -0,0 +1,10 @@ +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Delegate representing the dismissal of an active notification. +/// +/// The notification being dismissed. +/// The reason of dismissal. +public delegate void NotificationDismissedDelegate( + IActiveNotification notification, + NotificationDismissReason dismissReason); diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs new file mode 100644 index 000000000..182714157 --- /dev/null +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -0,0 +1,508 @@ +using System.Numerics; +using System.Runtime.Loader; +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.Animation; +using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.Colors; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using ImGuiNET; + +using Serilog; + +namespace Dalamud.Interface.Internal.Notifications; + +/// +/// Represents an active notification. +/// +internal sealed class ActiveNotification : IActiveNotification, IDisposable +{ + private readonly Easing showEasing; + private readonly Easing hideEasing; + + private Notification underlyingNotification; + + /// + /// Initializes a new instance of the class. + /// + /// The underlying notification. + /// The initiator plugin. Use null if originated by Dalamud. + public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) + { + this.underlyingNotification = underlyingNotification with { }; + this.InitiatorPlugin = initiatorPlugin; + this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); + this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); + + this.showEasing.Start(); + } + + /// + public event NotificationDismissedDelegate? Dismiss; + + /// + public event Action? Click; + + /// + public event Action? DrawActions; + + /// + public event Action? MouseEnter; + + /// + public event Action? MouseLeave; + + /// + public long Id { get; } = IActiveNotification.CreateNewId(); + + /// + /// Gets the tick of creating this notification. + /// + public long CreatedAt { get; } = Environment.TickCount64; + + /// + public string Content => this.underlyingNotification.Content; + + /// + public string? Title => this.underlyingNotification.Title; + + /// + public NotificationType Type => this.underlyingNotification.Type; + + /// + public Func>? IconCreator => this.underlyingNotification.IconCreator; + + /// + public DateTime Expiry => this.underlyingNotification.Expiry; + + /// + public bool Interactible => this.underlyingNotification.Interactible; + + /// + public bool ClickIsDismiss => this.underlyingNotification.ClickIsDismiss; + + /// + public TimeSpan HoverExtendDuration => this.underlyingNotification.HoverExtendDuration; + + /// + public bool IsMouseHovered { get; private set; } + + /// + public bool IsDismissed => this.hideEasing.IsRunning; + + /// + /// Gets or sets the plugin that initiated this notification. + /// + public LocalPlugin? InitiatorPlugin { get; set; } + + /// + /// Gets or sets the icon of this notification. + /// + public Task? IconTask { get; set; } + + /// + /// Gets the default color of the notification. + /// + private Vector4 DefaultIconColor => this.Type switch + { + NotificationType.None => ImGuiColors.DalamudWhite, + NotificationType.Success => ImGuiColors.HealerGreen, + NotificationType.Warning => ImGuiColors.DalamudOrange, + NotificationType.Error => ImGuiColors.DalamudRed, + NotificationType.Info => ImGuiColors.TankBlue, + _ => ImGuiColors.DalamudWhite, + }; + + /// + /// Gets the default icon of the notification. + /// + private string? DefaultIconString => this.Type switch + { + NotificationType.None => null, + NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconString(), + NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconString(), + NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconString(), + NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconString(), + _ => null, + }; + + /// + /// Gets the default title of the notification. + /// + private string? DefaultTitle => this.Type switch + { + NotificationType.None => null, + NotificationType.Success => NotificationType.Success.ToString(), + NotificationType.Warning => NotificationType.Warning.ToString(), + NotificationType.Error => NotificationType.Error.ToString(), + NotificationType.Info => NotificationType.Info.ToString(), + _ => null, + }; + + /// + public void Dispose() + { + this.ClearIconTask(); + this.underlyingNotification.IconCreator = null; + this.Dismiss = null; + this.Click = null; + this.DrawActions = null; + this.InitiatorPlugin = null; + } + + /// + public Notification CloneNotification() => this.underlyingNotification with { }; + + /// + public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); + + /// + /// Dismisses this notification. Multiple calls will be ignored. + /// + /// The reason of dismissal. + public void DismissNow(NotificationDismissReason reason) + { + if (this.hideEasing.IsRunning) + return; + + this.hideEasing.Start(); + try + { + this.Dismiss?.Invoke(this, reason); + } + catch (Exception e) + { + Log.Error( + e, + $"{nameof(this.Dismiss)} error; notification is owned by {this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); + } + } + + /// + /// Updates animations. + /// + /// true if the notification is over. + public bool UpdateAnimations() + { + this.showEasing.Update(); + this.hideEasing.Update(); + return this.hideEasing.IsRunning && this.hideEasing.IsDone; + } + + /// + /// Draws this notification. + /// + /// The maximum width of the notification window. + /// The offset from the bottom. + /// The height of the notification. + public float Draw(float maxWidth, float offsetY) + { + if (!this.IsDismissed + && DateTime.Now > this.Expiry + && (this.HoverExtendDuration <= TimeSpan.Zero || !this.IsMouseHovered)) + { + this.DismissNow(NotificationDismissReason.Timeout); + } + + var opacity = + Math.Clamp( + (float)(this.hideEasing.IsRunning + ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) + : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), + 0f, + 1f); + if (opacity <= 0) + return 0; + + var notificationManager = Service.Get(); + var interfaceManager = Service.Get(); + var unboundedWidth = NotificationConstants.ScaledWindowPadding * 3; + unboundedWidth += NotificationConstants.ScaledIconSize; + unboundedWidth += Math.Max( + ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X, + ImGui.CalcTextSize(this.Content).X); + + var width = Math.Min(maxWidth, unboundedWidth); + + var viewport = ImGuiHelpers.MainViewport; + var viewportPos = viewport.WorkPos; + var viewportSize = viewport.WorkSize; + + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue)); + ImGui.PushID(this.Id.GetHashCode()); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); + unsafe + { + ImGui.PushStyleColor( + ImGuiCol.WindowBg, + *ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4( + 1f, + 1f, + 1f, + NotificationConstants.BackgroundOpacity)); + } + + ImGui.Begin( + $"##NotifyWindow{this.Id}", + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDecoration | + (this.Interactible ? ImGuiWindowFlags.None : ImGuiWindowFlags.NoInputs) | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.NoFocusOnAppearing); + + var basePos = ImGui.GetCursorPos(); + this.DrawIcon( + notificationManager, + basePos, + basePos + new Vector2(NotificationConstants.ScaledIconSize)); + basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; + width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2); + this.DrawTitle(basePos, basePos + new Vector2(width, 0)); + basePos.Y = ImGui.GetCursorPosY(); + this.DrawContentBody(basePos, basePos + new Vector2(width, 0)); + if (ImGui.IsWindowHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + this.Click?.InvokeSafely(this); + if (this.ClickIsDismiss) + this.DismissNow(NotificationDismissReason.Manual); + } + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + + ImGui.End(); + + if (!this.IsDismissed) + this.DrawCloseButton(interfaceManager, windowPos); + + ImGui.PopStyleColor(); + ImGui.PopStyleVar(2); + ImGui.PopID(); + + if (windowPos.X <= ImGui.GetIO().MousePos.X + && windowPos.Y <= ImGui.GetIO().MousePos.Y + && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X + && ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y) + { + if (!this.IsMouseHovered) + { + this.IsMouseHovered = true; + this.MouseEnter.InvokeSafely(this); + } + } + else if (this.IsMouseHovered) + { + if (this.HoverExtendDuration > TimeSpan.Zero) + { + var newExpiry = DateTime.Now + this.HoverExtendDuration; + if (newExpiry > this.Expiry) + this.underlyingNotification.Expiry = newExpiry; + } + + this.IsMouseHovered = false; + this.MouseLeave.InvokeSafely(this); + } + + return windowSize.Y; + } + + /// + public void Update(INotification newNotification) + { + this.underlyingNotification.Content = newNotification.Content; + this.underlyingNotification.Title = newNotification.Title; + this.underlyingNotification.Type = newNotification.Type; + this.underlyingNotification.IconCreator = newNotification.IconCreator; + this.underlyingNotification.Expiry = newNotification.Expiry; + } + + /// + public void UpdateIcon() + { + this.ClearIconTask(); + this.IconTask = this.IconCreator?.Invoke(); + } + + /// + /// Removes non-Dalamud invocation targets from events. + /// + public void RemoveNonDalamudInvocations() + { + var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); + this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss); + this.Click = RemoveNonDalamudInvocationsCore(this.Click); + this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions); + this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); + this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); + + return; + + T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate + { + if (@delegate is null) + return null; + + foreach (var il in @delegate.GetInvocationList()) + { + if (il.Target is { } target && + AssemblyLoadContext.GetLoadContext(target.GetType().Assembly) != dalamudContext) + { + @delegate = (T)Delegate.Remove(@delegate, il); + } + } + + return @delegate; + } + } + + private void ClearIconTask() + { + _ = this.IconTask?.ContinueWith( + r => + { + if (r.IsCompletedSuccessfully && r.Result is IDisposable d) + d.Dispose(); + }); + this.IconTask = null; + } + + private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord) + { + string? iconString; + IFontHandle? fontHandle; + switch (this.IconTask?.IsCompletedSuccessfully is true ? this.IconTask.Result : null) + { + case IDalamudTextureWrap wrap: + { + var size = wrap.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.Image(wrap.ImGuiHandle, size); + return; + } + + case SeIconChar icon: + iconString = string.Empty + (char)icon; + fontHandle = notificationManager.IconAxisFontHandle; + break; + case FontAwesomeIcon icon: + iconString = icon.ToIconString(); + fontHandle = notificationManager.IconFontAwesomeFontHandle; + break; + default: + iconString = this.DefaultIconString; + fontHandle = notificationManager.IconFontAwesomeFontHandle; + break; + } + + if (string.IsNullOrWhiteSpace(iconString)) + return; + + using (fontHandle.Push()) + { + var size = ImGui.CalcTextSize(iconString); + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.PushStyleColor(ImGuiCol.Text, this.DefaultIconColor); + ImGui.TextUnformatted(iconString); + ImGui.PopStyleColor(); + } + } + + private void DrawTitle(Vector2 minCoord, Vector2 maxCoord) + { + ImGui.PushTextWrapPos(maxCoord.X); + + if ((this.Title ?? this.DefaultTitle) is { } title) + { + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); + ImGui.SetCursorPos(minCoord); + ImGui.TextUnformatted(title); + ImGui.PopStyleColor(); + } + + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); + ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); + ImGui.TextUnformatted(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator); + ImGui.PopStyleColor(); + + ImGui.PopTextWrapPos(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + } + + private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 screenCoord) + { + using (interfaceManager.IconFontHandle?.Push()) + { + var str = FontAwesomeIcon.Times.ToIconString(); + var size = NotificationConstants.ScaledCloseButtonMinSize; + var textSize = ImGui.CalcTextSize(str); + size = Math.Max(size, Math.Max(textSize.X, textSize.Y)); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + // ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos(screenCoord, ImGuiCond.Always, new(1, 0)); + ImGui.SetNextWindowSizeConstraints(new(size), new(size)); + ImGui.Begin( + $"##CloseButtonWindow{this.Id}", + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoBringToFrontOnFocus | + ImGuiWindowFlags.NoFocusOnAppearing); + + if (ImGui.Button(str, new(size))) + this.DismissNow(); + + ImGui.End(); + ImGui.PopStyleColor(2); + ImGui.PopStyleVar(4); + } + } + + private void DrawContentBody(Vector2 minCoord, Vector2 maxCoord) + { + ImGui.SetCursorPos(minCoord); + ImGui.PushTextWrapPos(maxCoord.X); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); + ImGui.TextUnformatted(this.Content); + ImGui.PopStyleColor(); + ImGui.PopTextWrapPos(); + if (this.DrawActions is not null) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + try + { + this.DrawActions.Invoke(this); + } + catch + { + // ignore + } + } + } +} diff --git a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs new file mode 100644 index 000000000..44b1fa832 --- /dev/null +++ b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs @@ -0,0 +1,74 @@ +using System.Numerics; + +using Dalamud.Interface.Utility; + +namespace Dalamud.Interface.Internal.Notifications; + +/// +/// Constants for drawing notification windows. +/// +internal static class NotificationConstants +{ + // ..............................[X] + // ..[i]..title title title title .. + // .. by this_plugin .. + // .. .. + // .. body body body body .. + // .. some more wrapped body .. + // .. .. + // .. action buttons .. + // ................................. + + /// The string to show in place of this_plugin if the notification is shown by Dalamud. + public const string DefaultInitiator = "Dalamud"; + + /// The size of the icon. + public const float IconSize = 32; + + /// The background opacity of a notification window. + public const float BackgroundOpacity = 0.82f; + + /// Duration of show animation. + public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); + + /// Default duration of the notification. + public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3); + + /// Default duration of the notification. + public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3); + + /// Duration of hide animation. + public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + + /// Text color for the close button [X]. + public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); + + /// Text color for the title. + public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); + + /// Text color for the name of the initiator. + public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); + + /// Text color for the body. + public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + + /// Gets the scaled padding of the window (dot(.) in the above diagram). + public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); + + /// Gets the distance from the right bottom border of the viewport + /// to the right bottom border of a notification window. + /// + public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); + + /// Gets the scaled gap between two notification windows. + public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); + + /// Gets the scaled gap between components. + public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); + + /// Gets the scaled size of the icon. + public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); + + /// Gets the scaled size of the close button. + public static float ScaledCloseButtonMinSize => MathF.Round(16 * ImGuiHelpers.GlobalScale); +} diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs index 67ad3ee8f..fd92c30df 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs @@ -1,12 +1,15 @@ -using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; -using System.Numerics; -using Dalamud.Interface.Colors; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; -using Dalamud.Utility; -using ImGuiNET; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; namespace Dalamud.Interface.Internal.Notifications; @@ -14,51 +17,66 @@ namespace Dalamud.Interface.Internal.Notifications; /// Class handling notifications/toasts in ImGui. /// Ported from https://github.com/patrickcjk/imgui-notify. /// +[InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal class NotificationManager : IServiceType +internal class NotificationManager : INotificationManager, IServiceType, IDisposable { - /// - /// Value indicating the bottom-left X padding. - /// - internal const float NotifyPaddingX = 20.0f; - - /// - /// Value indicating the bottom-left Y padding. - /// - internal const float NotifyPaddingY = 20.0f; - - /// - /// Value indicating the Y padding between each message. - /// - internal const float NotifyPaddingMessageY = 10.0f; - - /// - /// Value indicating the fade-in and out duration. - /// - internal const int NotifyFadeInOutTime = 500; - - /// - /// Value indicating the default time until the notification is dismissed. - /// - internal const int NotifyDefaultDismiss = 3000; - - /// - /// Value indicating the maximum opacity. - /// - internal const float NotifyOpacity = 0.82f; - - /// - /// Value indicating default window flags for the notifications. - /// - internal const ImGuiWindowFlags NotifyToastFlags = - ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoInputs | - ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoFocusOnAppearing; - - private readonly List notifications = new(); + private readonly List notifications = new(); + private readonly ConcurrentBag pendingNotifications = new(); [ServiceManager.ServiceConstructor] - private NotificationManager() + private NotificationManager(FontAtlasFactory fontAtlasFactory) { + this.PrivateAtlas = fontAtlasFactory.CreateFontAtlas( + nameof(NotificationManager), + FontAtlasAutoRebuildMode.Async); + this.IconAxisFontHandle = + this.PrivateAtlas.NewGameFontHandle(new(GameFontFamily.Axis, NotificationConstants.IconSize)); + this.IconFontAwesomeFontHandle = + this.PrivateAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont(new() { SizePx = NotificationConstants.IconSize }))); + } + + /// Gets the handle to AXIS fonts, sized for use as an icon. + public IFontHandle IconAxisFontHandle { get; } + + /// Gets the handle to FontAwesome fonts, sized for use as an icon. + public IFontHandle IconFontAwesomeFontHandle { get; } + + private IFontAtlas PrivateAtlas { get; } + + /// + public void Dispose() + { + this.PrivateAtlas.Dispose(); + foreach (var n in this.pendingNotifications) + n.Dispose(); + foreach (var n in this.notifications) + n.Dispose(); + this.pendingNotifications.Clear(); + this.notifications.Clear(); + } + + /// + public IActiveNotification AddNotification(Notification notification) + { + var an = new ActiveNotification(notification, null); + this.pendingNotifications.Add(an); + return an; + } + + /// + /// Adds a notification originating from a plugin. + /// + /// The notification. + /// The source plugin. + /// The new notification. + public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin) + { + var an = new ActiveNotification(notification, plugin); + this.pendingNotifications.Add(an); + return an; } /// @@ -67,252 +85,77 @@ internal class NotificationManager : IServiceType /// The content of the notification. /// The title of the notification. /// The type of the notification. - /// The time the notification should be displayed for. - public void AddNotification(string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = NotifyDefaultDismiss) - { - this.notifications.Add(new Notification - { - Content = content, - Title = title, - NotificationType = type, - DurationMs = msDelay, - }); - } + public void AddNotification( + string content, + string? title = null, + NotificationType type = NotificationType.None) => + this.AddNotification( + new() + { + Content = content, + Title = title, + Type = type, + }); /// /// Draw all currently queued notifications. /// public void Draw() { - var viewportSize = ImGuiHelpers.MainViewport.Size; + var viewportSize = ImGuiHelpers.MainViewport.WorkSize; var height = 0f; - for (var i = 0; i < this.notifications.Count; i++) - { - var tn = this.notifications.ElementAt(i); + while (this.pendingNotifications.TryTake(out var newNotification)) + this.notifications.Add(newNotification); - if (tn.GetPhase() == Notification.Phase.Expired) - { - this.notifications.RemoveAt(i); - continue; - } + var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); - var opacity = tn.GetFadePercent(); + this.notifications.RemoveAll(x => x.UpdateAnimations()); + foreach (var tn in this.notifications) + height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; + } +} - var iconColor = tn.Color; - iconColor.W = opacity; +/// +/// Plugin-scoped version of a service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class NotificationManagerPluginScoped : INotificationManager, IServiceType, IDisposable +{ + private readonly LocalPlugin localPlugin; + private readonly ConcurrentDictionary notifications = new(); - var windowName = $"##NOTIFY{i}"; + [ServiceManager.ServiceDependency] + private readonly NotificationManager notificationManagerService = Service.Get(); - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowBgAlpha(opacity); - ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One); - ImGui.Begin(windowName, NotifyToastFlags); + [ServiceManager.ServiceConstructor] + private NotificationManagerPluginScoped(LocalPlugin localPlugin) => + this.localPlugin = localPlugin; - ImGui.PushTextWrapPos(viewportSize.X / 3.0f); - - var wasTitleRendered = false; - - if (!tn.Icon.IsNullOrEmpty()) - { - wasTitleRendered = true; - ImGui.PushFont(InterfaceManager.IconFont); - ImGui.TextColored(iconColor, tn.Icon); - ImGui.PopFont(); - } - - var textColor = ImGuiColors.DalamudWhite; - textColor.W = opacity; - - ImGui.PushStyleColor(ImGuiCol.Text, textColor); - - if (!tn.Title.IsNullOrEmpty()) - { - if (!tn.Icon.IsNullOrEmpty()) - { - ImGui.SameLine(); - } - - ImGui.TextUnformatted(tn.Title); - wasTitleRendered = true; - } - else if (!tn.DefaultTitle.IsNullOrEmpty()) - { - if (!tn.Icon.IsNullOrEmpty()) - { - ImGui.SameLine(); - } - - ImGui.TextUnformatted(tn.DefaultTitle); - wasTitleRendered = true; - } - - if (wasTitleRendered && !tn.Content.IsNullOrEmpty()) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 5.0f); - } - - if (!tn.Content.IsNullOrEmpty()) - { - if (wasTitleRendered) - { - ImGui.Separator(); - } - - ImGui.TextUnformatted(tn.Content); - } - - ImGui.PopStyleColor(); - - ImGui.PopTextWrapPos(); - - height += ImGui.GetWindowHeight() + NotifyPaddingMessageY; - - ImGui.End(); - } + /// + public IActiveNotification AddNotification(Notification notification) + { + var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); + _ = this.notifications.TryAdd(an, 0); + an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); + return an; } - /// - /// Container class for notifications. - /// - internal class Notification + /// + public void Dispose() { - /// - /// Possible notification phases. - /// - internal enum Phase + while (!this.notifications.IsEmpty) { - /// - /// Phase indicating fade-in. - /// - FadeIn, - - /// - /// Phase indicating waiting until fade-out. - /// - Wait, - - /// - /// Phase indicating fade-out. - /// - FadeOut, - - /// - /// Phase indicating that the notification has expired. - /// - Expired, - } - - /// - /// Gets the type of the notification. - /// - internal NotificationType NotificationType { get; init; } - - /// - /// Gets the title of the notification. - /// - internal string? Title { get; init; } - - /// - /// Gets the content of the notification. - /// - internal string Content { get; init; } - - /// - /// Gets the duration of the notification in milliseconds. - /// - internal uint DurationMs { get; init; } - - /// - /// Gets the creation time of the notification. - /// - internal DateTime CreationTime { get; init; } = DateTime.Now; - - /// - /// Gets the default color of the notification. - /// - /// Thrown when is set to an out-of-range value. - internal Vector4 Color => this.NotificationType switch - { - NotificationType.None => ImGuiColors.DalamudWhite, - NotificationType.Success => ImGuiColors.HealerGreen, - NotificationType.Warning => ImGuiColors.DalamudOrange, - NotificationType.Error => ImGuiColors.DalamudRed, - NotificationType.Info => ImGuiColors.TankBlue, - _ => throw new ArgumentOutOfRangeException(), - }; - - /// - /// Gets the icon of the notification. - /// - /// Thrown when is set to an out-of-range value. - internal string? Icon => this.NotificationType switch - { - NotificationType.None => null, - NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconString(), - NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconString(), - NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconString(), - NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconString(), - _ => throw new ArgumentOutOfRangeException(), - }; - - /// - /// Gets the default title of the notification. - /// - /// Thrown when is set to an out-of-range value. - internal string? DefaultTitle => this.NotificationType switch - { - NotificationType.None => null, - NotificationType.Success => NotificationType.Success.ToString(), - NotificationType.Warning => NotificationType.Warning.ToString(), - NotificationType.Error => NotificationType.Error.ToString(), - NotificationType.Info => NotificationType.Info.ToString(), - _ => throw new ArgumentOutOfRangeException(), - }; - - /// - /// Gets the elapsed time since creating the notification. - /// - internal TimeSpan ElapsedTime => DateTime.Now - this.CreationTime; - - /// - /// Gets the phase of the notification. - /// - /// The phase of the notification. - internal Phase GetPhase() - { - var elapsed = (int)this.ElapsedTime.TotalMilliseconds; - - if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime) - return Phase.Expired; - else if (elapsed > NotifyFadeInOutTime + this.DurationMs) - return Phase.FadeOut; - else if (elapsed > NotifyFadeInOutTime) - return Phase.Wait; - else - return Phase.FadeIn; - } - - /// - /// Gets the opacity of the notification. - /// - /// The opacity, in a range from 0 to 1. - internal float GetFadePercent() - { - var phase = this.GetPhase(); - var elapsed = this.ElapsedTime.TotalMilliseconds; - - if (phase == Phase.FadeIn) + foreach (var n in this.notifications.Keys) { - return (float)elapsed / NotifyFadeInOutTime * NotifyOpacity; + this.notifications.TryRemove(n, out _); + ((ActiveNotification)n).RemoveNonDalamudInvocations(); } - else if (phase == Phase.FadeOut) - { - return (1.0f - (((float)elapsed - NotifyFadeInOutTime - this.DurationMs) / - NotifyFadeInOutTime)) * NotifyOpacity; - } - - return 1.0f * NotifyOpacity; } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 2c7ceb95b..ebf3157fa 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -44,32 +44,66 @@ internal class ImGuiWidget : IDataWindowWidget if (ImGui.Button("Add random notification")) { - var rand = new Random(); - - var title = rand.Next(0, 5) switch - { - 0 => "This is a toast", - 1 => "Truly, a toast", - 2 => "I am testing this toast", - 3 => "I hope this looks right", - 4 => "Good stuff", - 5 => "Nice", - _ => null, - }; - - var type = rand.Next(0, 4) switch - { - 0 => NotificationType.Error, - 1 => NotificationType.Warning, - 2 => NotificationType.Info, - 3 => NotificationType.Success, - 4 => NotificationType.None, - _ => NotificationType.None, - }; - const string text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; - notifications.AddNotification(text, title, type); + NewRandom(out var title, out var type); + var n = notifications.AddNotification( + new() + { + Content = text, + Title = title, + Type = type, + Interactible = true, + ClickIsDismiss = false, + }); + + var nclick = 0; + n.Click += _ => nclick++; + n.DrawActions += an => + { + if (ImGui.Button("Update in place")) + { + NewRandom(out title, out type); + an.Update(an.CloneNotification() with { Title = title, Type = type }); + } + + if (an.IsMouseHovered) + { + ImGui.SameLine(); + if (ImGui.Button("Dismiss")) + an.DismissNow(); + } + + ImGui.AlignTextToFramePadding(); + ImGui.SameLine(); + ImGui.TextUnformatted($"Clicked {nclick} time(s)"); + }; } } + + private static void NewRandom(out string? title, out NotificationType type) + { + var rand = new Random(); + + title = rand.Next(0, 5) switch + { + 0 => "This is a toast", + 1 => "Truly, a toast", + 2 => "I am testing this toast", + 3 => "I hope this looks right", + 4 => "Good stuff", + 5 => "Nice", + _ => null, + }; + + type = rand.Next(0, 4) switch + { + 0 => NotificationType.Error, + 1 => NotificationType.Warning, + 2 => NotificationType.Info, + 3 => NotificationType.Success, + 4 => NotificationType.None, + _ => NotificationType.None, + }; + } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index d260868a0..6da6ebc4a 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -9,12 +10,14 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -29,11 +32,13 @@ namespace Dalamud.Interface; /// public sealed class UiBuilder : IDisposable { + private readonly LocalPlugin localPlugin; private readonly Stopwatch stopwatch; private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); private readonly Framework framework = Service.Get(); + private readonly ConcurrentDictionary notifications = new(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); @@ -52,8 +57,10 @@ public sealed class UiBuilder : IDisposable /// You do not have to call this manually. /// /// The plugin namespace. - internal UiBuilder(string namespaceName) + /// The relevant local plugin. + internal UiBuilder(string namespaceName, LocalPlugin localPlugin) { + this.localPlugin = localPlugin; try { this.stopwatch = new Stopwatch(); @@ -556,22 +563,46 @@ public sealed class UiBuilder : IDisposable /// The title of the notification. /// The type of the notification. /// The time the notification should be displayed for. - public void AddNotification( - string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000) + [Obsolete($"Use {nameof(INotificationManager)}.", false)] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public async void AddNotification( + string content, + string? title = null, + NotificationType type = NotificationType.None, + uint msDelay = 3000) { - Service - .GetAsync() - .ContinueWith(task => + var nm = await Service.GetAsync(); + var an = nm.AddNotification( + new() { - if (task.IsCompletedSuccessfully) - task.Result.AddNotification(content, title, type, msDelay); - }); + Content = content, + Title = title, + Type = type, + Expiry = DateTime.Now + TimeSpan.FromMilliseconds(msDelay), + }, + this.localPlugin); + _ = this.notifications.TryAdd(an, 0); + an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); } /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); + void IDisposable.Dispose() + { + this.scopedFinalizer.Dispose(); + + // Taken from NotificationManagerPluginScoped. + // TODO: remove on API 10. + while (!this.notifications.IsEmpty) + { + foreach (var n in this.notifications.Keys) + { + this.notifications.TryRemove(n, out _); + ((ActiveNotification)n).RemoveNonDalamudInvocations(); + } + } + } /// /// Open the registered configuration UI, if it exists. diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 82f19aa49..5e103ecbe 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -52,7 +52,7 @@ public sealed class DalamudPluginInterface : IDisposable var dataManager = Service.Get(); var localization = Service.Get(); - this.UiBuilder = new UiBuilder(plugin.Name); + this.UiBuilder = new UiBuilder(plugin.Name, plugin); this.configs = Service.Get().PluginConfigs; this.Reason = reason; diff --git a/Dalamud/Plugin/Services/INotificationManager.cs b/Dalamud/Plugin/Services/INotificationManager.cs new file mode 100644 index 000000000..1d31ddd35 --- /dev/null +++ b/Dalamud/Plugin/Services/INotificationManager.cs @@ -0,0 +1,16 @@ +using Dalamud.Interface.ImGuiNotification; + +namespace Dalamud.Plugin.Services; + +/// +/// Manager for notifications provided by Dalamud using ImGui. +/// +public interface INotificationManager +{ + /// + /// Adds a notification. + /// + /// The new notification. + /// The added notification. + IActiveNotification AddNotification(Notification notification); +} From 034389711301c34c5af0901fedf8d79f9ed4bdec Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:31:32 +0900 Subject: [PATCH 529/585] Better error message for FontHandle --- .../ManagedFontAtlas/Internals/FontHandle.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 47254a5c9..ba890f7c2 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -24,7 +24,6 @@ internal abstract class FontHandle : IFontHandle private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); private static long nextNonMainThreadFontAccessWarningCheck; - private readonly InterfaceManager interfaceManager; private readonly List pushedFonts = new(8); private IFontHandleManager? manager; @@ -36,7 +35,6 @@ internal abstract class FontHandle : IFontHandle /// An instance of . protected FontHandle(IFontHandleManager manager) { - this.interfaceManager = Service.Get(); this.manager = manager; } @@ -58,7 +56,11 @@ internal abstract class FontHandle : IFontHandle /// Gets the associated . /// /// When the object has already been disposed. - protected IFontHandleManager Manager => this.manager ?? throw new ObjectDisposedException(this.GetType().Name); + protected IFontHandleManager Manager => + this.manager + ?? throw new ObjectDisposedException( + this.GetType().Name, + "Did you write `using (fontHandle)` instead of `using (fontHandle.Push())`?"); /// public void Dispose() @@ -122,7 +124,7 @@ internal abstract class FontHandle : IFontHandle } } - this.interfaceManager.EnqueueDeferredDispose(locked); + Service.Get().EnqueueDeferredDispose(locked); return locked.ImFont; } @@ -196,7 +198,7 @@ internal abstract class FontHandle : IFontHandle ThreadSafety.AssertMainThread(); // Warn if the client is not properly managing the pushed font stack. - var cumulativePresentCalls = this.interfaceManager.CumulativePresentCalls; + var cumulativePresentCalls = Service.Get().CumulativePresentCalls; if (this.lastCumulativePresentCalls != cumulativePresentCalls) { this.lastCumulativePresentCalls = cumulativePresentCalls; @@ -213,7 +215,7 @@ internal abstract class FontHandle : IFontHandle if (this.TryLock(out _) is { } locked) { font = locked.ImFont; - this.interfaceManager.EnqueueDeferredDispose(locked); + Service.Get().EnqueueDeferredDispose(locked); } var rented = SimplePushedFont.Rent(this.pushedFonts, font); From 2935d18c37b3cda53f6c9d11383bd8882555d219 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:45:06 +0900 Subject: [PATCH 530/585] Show plugin icons as fallback icon --- .../Notifications/ActiveNotification.cs | 71 +++++++++++++------ .../Windows/Data/Widgets/ImGuiWidget.cs | 5 +- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 182714157..e444e63ef 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -7,9 +7,11 @@ using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; using Dalamud.Utility; using ImGuiNET; @@ -383,23 +385,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord) { - string? iconString; - IFontHandle? fontHandle; + string? iconString = null; + IFontHandle? fontHandle = null; + IDalamudTextureWrap? iconTexture = null; switch (this.IconTask?.IsCompletedSuccessfully is true ? this.IconTask.Result : null) { case IDalamudTextureWrap wrap: - { - var size = wrap.Size; - if (size.X > maxCoord.X - minCoord.X) - size *= (maxCoord.X - minCoord.X) / size.X; - if (size.Y > maxCoord.Y - minCoord.Y) - size *= (maxCoord.Y - minCoord.Y) / size.Y; - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.Image(wrap.ImGuiHandle, size); - return; - } - + iconTexture = wrap; + break; case SeIconChar icon: iconString = string.Empty + (char)icon; fontHandle = notificationManager.IconAxisFontHandle; @@ -415,16 +408,52 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } if (string.IsNullOrWhiteSpace(iconString)) - return; - - using (fontHandle.Push()) { - var size = ImGui.CalcTextSize(iconString); + var dam = Service.Get(); + if (this.InitiatorPlugin is null) + { + iconTexture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); + } + else + { + if (!Service.Get().TryGetIcon( + this.InitiatorPlugin, + this.InitiatorPlugin.Manifest, + this.InitiatorPlugin.IsThirdParty, + out iconTexture) || iconTexture is null) + { + iconTexture = this.InitiatorPlugin switch + { + { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), + { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), + _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), + }; + } + } + } + + if (iconTexture is not null) + { + var size = iconTexture.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; var pos = ((minCoord + maxCoord) - size) / 2; ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, this.DefaultIconColor); - ImGui.TextUnformatted(iconString); - ImGui.PopStyleColor(); + ImGui.Image(iconTexture.ImGuiHandle, size); + } + else if (fontHandle is not null) + { + using (fontHandle.Push()) + { + var size = ImGui.CalcTextSize(iconString); + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.PushStyleColor(ImGuiCol.Text, this.DefaultIconColor); + ImGui.TextUnformatted(iconString); + ImGui.PopStyleColor(); + } } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index ebf3157fa..2eee81ee2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -55,6 +55,7 @@ internal class ImGuiWidget : IDataWindowWidget Type = type, Interactible = true, ClickIsDismiss = false, + Expiry = DateTime.MaxValue, }); var nclick = 0; @@ -85,7 +86,7 @@ internal class ImGuiWidget : IDataWindowWidget { var rand = new Random(); - title = rand.Next(0, 5) switch + title = rand.Next(0, 7) switch { 0 => "This is a toast", 1 => "Truly, a toast", @@ -96,7 +97,7 @@ internal class ImGuiWidget : IDataWindowWidget _ => null, }; - type = rand.Next(0, 4) switch + type = rand.Next(0, 5) switch { 0 => NotificationType.Error, 1 => NotificationType.Warning, From 8d15dfc031ef4cc907bee7e4105c164b32fcddc1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:57:22 +0900 Subject: [PATCH 531/585] Fix vertical offset when title is empty --- Dalamud/Interface/Internal/Notifications/ActiveNotification.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index e444e63ef..7feb989c3 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -461,10 +461,10 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable { ImGui.PushTextWrapPos(maxCoord.X); + ImGui.SetCursorPos(minCoord); if ((this.Title ?? this.DefaultTitle) is { } title) { ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); - ImGui.SetCursorPos(minCoord); ImGui.TextUnformatted(title); ImGui.PopStyleColor(); } From 97066b7442cd5a2d6e1e0beb8493166794512e2d Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 05:59:38 +0900 Subject: [PATCH 532/585] Fix layout --- .../Interface/Internal/Notifications/ActiveNotification.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 7feb989c3..178cdb041 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -227,7 +227,9 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable var unboundedWidth = NotificationConstants.ScaledWindowPadding * 3; unboundedWidth += NotificationConstants.ScaledIconSize; unboundedWidth += Math.Max( - ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X, + Math.Max( + ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X, + ImGui.CalcTextSize(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator).X), ImGui.CalcTextSize(this.Content).X); var width = Math.Min(maxWidth, unboundedWidth); From 54decfe7d3f09282eaab3c40783e46ee8ca7f7a0 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 06:15:56 +0900 Subject: [PATCH 533/585] Add expiry progressbar --- .../Notifications/ActiveNotification.cs | 51 ++++++++++++++++--- .../Notifications/NotificationConstants.cs | 5 +- 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 178cdb041..90c99ab11 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -64,9 +64,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable public long Id { get; } = IActiveNotification.CreateNewId(); /// - /// Gets the tick of creating this notification. + /// Gets the time of creating this notification. /// - public long CreatedAt { get; } = Environment.TickCount64; + public DateTime CreatedAt { get; } = DateTime.Now; + + /// + /// Gets the time of starting to count the timer for the expiration. + /// + public DateTime ExpiryRelativeToTime { get; private set; } = DateTime.Now; /// public string Content => this.underlyingNotification.Content; @@ -249,6 +254,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.PushID(this.Id.GetHashCode()); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); unsafe { ImGui.PushStyleColor( @@ -289,13 +295,36 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); + float expiryRatio; + if (this.IsDismissed) + { + expiryRatio = 0f; + } + else if (this.Expiry == DateTime.MaxValue || (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered)) + { + expiryRatio = 1f; + } + else + { + expiryRatio = (float)((this.Expiry - DateTime.Now).TotalMilliseconds / + (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + } + + expiryRatio = Math.Clamp(expiryRatio, 0f, 1f); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos + new Vector2(0, windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), + windowPos + windowSize with { X = windowSize.X * expiryRatio }, + ImGui.GetColorU32(this.DefaultIconColor)); + ImGui.PopClipRect(); + ImGui.End(); if (!this.IsDismissed) this.DrawCloseButton(interfaceManager, windowPos); ImGui.PopStyleColor(); - ImGui.PopStyleVar(2); + ImGui.PopStyleVar(3); ImGui.PopID(); if (windowPos.X <= ImGui.GetIO().MousePos.X @@ -315,7 +344,10 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable { var newExpiry = DateTime.Now + this.HoverExtendDuration; if (newExpiry > this.Expiry) + { this.underlyingNotification.Expiry = newExpiry; + this.ExpiryRelativeToTime = DateTime.Now; + } } this.IsMouseHovered = false; @@ -332,7 +364,15 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.underlyingNotification.Title = newNotification.Title; this.underlyingNotification.Type = newNotification.Type; this.underlyingNotification.IconCreator = newNotification.IconCreator; - this.underlyingNotification.Expiry = newNotification.Expiry; + if (this.underlyingNotification.Expiry != newNotification.Expiry) + { + this.underlyingNotification.Expiry = newNotification.Expiry; + this.ExpiryRelativeToTime = DateTime.Now; + } + + this.underlyingNotification.Interactible = newNotification.Interactible; + this.underlyingNotification.ClickIsDismiss = newNotification.ClickIsDismiss; + this.underlyingNotification.HoverExtendDuration = newNotification.HoverExtendDuration; } /// @@ -491,7 +531,6 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); ImGui.PushStyleColor(ImGuiCol.Button, 0); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); @@ -511,7 +550,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.End(); ImGui.PopStyleColor(2); - ImGui.PopStyleVar(4); + ImGui.PopStyleVar(3); } } diff --git a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs index 44b1fa832..bf71cd87e 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs @@ -70,5 +70,8 @@ internal static class NotificationConstants public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the scaled size of the close button. - public static float ScaledCloseButtonMinSize => MathF.Round(16 * ImGuiHelpers.GlobalScale); + public static float ScaledCloseButtonMinSize => MathF.Round(16 * ImGuiHelpers.GlobalScale); + + /// Gets the height of the expiry progress bar. + public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); } From 199722d29a4d85cb8020cac62fa6fbb2280c9329 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 06:22:54 +0900 Subject: [PATCH 534/585] Set IconTask on ActiveNotification ctor --- Dalamud/Interface/Internal/Notifications/ActiveNotification.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 90c99ab11..5c343288e 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -43,6 +43,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); this.showEasing.Start(); + this.UpdateIcon(); } /// From 04c6be567136f475a8fc854b2be97c16760a1d7b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 23:42:01 +0900 Subject: [PATCH 535/585] Add progressbar --- .../ImGuiNotification/IActiveNotification.cs | 98 ++-- .../ImGuiNotification/INotification.cs | 53 +- .../ImGuiNotification/Notification.cs | 4 +- .../Notifications/ActiveNotification.cs | 552 ++++++++++++------ .../Notifications/NotificationConstants.cs | 61 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 216 ++++++- 6 files changed, 698 insertions(+), 286 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 3e8aef196..d1aa1d95b 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,28 +1,24 @@ using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; -/// -/// Represents an active notification. -/// +/// Represents an active notification. public interface IActiveNotification : INotification { - /// - /// The counter for field. - /// + /// The counter for field. private static long idCounter; - /// - /// Invoked upon dismissing the notification. - /// - /// - /// The event callback will not be called, if a user interacts with the notification after the plugin is unloaded. - /// + /// Invoked upon dismissing the notification. + /// The event callback will not be called, + /// if a user interacts with the notification after the plugin is unloaded. event NotificationDismissedDelegate Dismiss; - /// - /// Invoked upon clicking on the notification. - /// + /// Invoked upon clicking on the notification. /// /// This event is not applicable when is set to false. /// Note that this function may be called even after has been invoked. @@ -30,9 +26,7 @@ public interface IActiveNotification : INotification /// event Action Click; - /// - /// Invoked when the mouse enters the notification window. - /// + /// Invoked when the mouse enters the notification window. /// /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. @@ -40,9 +34,7 @@ public interface IActiveNotification : INotification /// event Action MouseEnter; - /// - /// Invoked when the mouse leaves the notification window. - /// + /// Invoked when the mouse leaves the notification window. /// /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. @@ -50,9 +42,7 @@ public interface IActiveNotification : INotification /// event Action MouseLeave; - /// - /// Invoked upon drawing the action bar of the notification. - /// + /// Invoked upon drawing the action bar of the notification. /// /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. @@ -60,50 +50,60 @@ public interface IActiveNotification : INotification /// event Action DrawActions; - /// - /// Gets the ID of this notification. - /// + /// + new string Content { get; set; } + + /// + new string? Title { get; set; } + + /// + new NotificationType Type { get; set; } + + /// + new Func>? IconCreator { get; set; } + + /// + new DateTime Expiry { get; set; } + + /// + new bool Interactible { get; set; } + + /// + new TimeSpan HoverExtendDuration { get; set; } + + /// + new float Progress { get; set; } + + /// Gets the ID of this notification. long Id { get; } - /// - /// Gets a value indicating whether the mouse cursor is on the notification window. - /// + /// Gets a value indicating whether the mouse cursor is on the notification window. bool IsMouseHovered { get; } - /// - /// Gets a value indicating whether the notification has been dismissed. - /// This includes when the hide animation is being played. - /// + /// Gets a value indicating whether the notification has been dismissed. + /// This includes when the hide animation is being played. bool IsDismissed { get; } - /// - /// Clones this notification as a . - /// + /// Clones this notification as a . /// A new instance of . Notification CloneNotification(); - /// - /// Dismisses this notification. - /// + /// Dismisses this notification. void DismissNow(); - /// - /// Updates the notification data. - /// + /// Updates the notification data. /// /// Call to update the icon using the new . + /// If is true, then this function is a no-op. /// /// The new notification entry. void Update(INotification newNotification); - /// - /// Loads the icon again using . - /// + /// Loads the icon again using . + /// If is true, then this function is a no-op. void UpdateIcon(); - /// - /// Generates a new value to use for . - /// + /// Generates a new value to use for . /// The new value. internal static long CreateNewId() => Interlocked.Increment(ref idCounter); } diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index f5f66725c..cbd8ad633 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -6,31 +6,21 @@ using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; -/// -/// Represents a notification. -/// +/// Represents a notification. public interface INotification { - /// - /// Gets the content body of the notification. - /// + /// Gets the content body of the notification. string Content { get; } - /// - /// Gets the title of the notification. - /// + /// Gets the title of the notification. string? Title { get; } - /// - /// Gets the type of the notification. - /// + /// Gets the type of the notification. NotificationType Type { get; } - /// - /// Gets the icon creator function for the notification.
+ /// Gets the icon creator function for the notification.
/// Currently , , and types - /// are accepted. - ///
+ /// are accepted.
/// /// The icon created by the task returned will be owned by Dalamud, /// i.e. it will be d automatically as needed.
@@ -41,35 +31,30 @@ public interface INotification ///
Func>? IconCreator { get; } - /// - /// Gets the expiry. - /// + /// Gets the expiry. + /// Set to to make the notification not have an expiry time + /// (sticky, indeterminate, permanent, or persistent). DateTime Expiry { get; } - /// - /// Gets a value indicating whether this notification may be interacted. - /// + /// Gets a value indicating whether this notification may be interacted. /// /// Set this value to true if you want to respond to user inputs from /// . /// Note that the close buttons for notifications are always provided and interactible. + /// If set to true, then clicking on the notification itself will be interpreted as user-initiated dismissal, + /// unless is set. /// bool Interactible { get; } - - /// - /// Gets a value indicating whether clicking on the notification window counts as dismissing the notification. - /// - /// - /// This property has no effect if is false. - /// - bool ClickIsDismiss { get; } - /// - /// Gets the new duration for this notification if mouse cursor is on the notification window. - /// If set to or less, then this feature is turned off. - /// + /// Gets the new duration for this notification if mouse cursor is on the notification window. /// + /// If set to or less, then this feature is turned off. /// This property is applicable regardless of . /// TimeSpan HoverExtendDuration { get; } + + /// Gets the progress for the progress bar of the notification. + /// The progress should either be in the range between 0 and 1 or be a negative value. + /// Specifying a negative value will show an indeterminate progress bar. + float Progress { get; } } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index fb2caa4f6..ccfb250c3 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -28,8 +28,8 @@ public sealed record Notification : INotification public bool Interactible { get; set; } /// - public bool ClickIsDismiss { get; set; } = true; + public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; /// - public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; + public float Progress { get; set; } = 1f; } diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs index 5c343288e..c1fecdd3b 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs @@ -20,19 +20,25 @@ using Serilog; namespace Dalamud.Interface.Internal.Notifications; -/// -/// Represents an active notification. -/// +/// Represents an active notification. internal sealed class ActiveNotification : IActiveNotification, IDisposable { + private readonly Notification underlyingNotification; + private readonly Easing showEasing; private readonly Easing hideEasing; + private readonly Easing progressEasing; - private Notification underlyingNotification; + /// The progress before for the progress bar animation with . + private float progressBefore; - /// - /// Initializes a new instance of the class. - /// + /// Used for calculating correct dismissal progressbar animation (left edge). + private float prevProgressL; + + /// Used for calculating correct dismissal progressbar animation (right edge). + private float prevProgressR; + + /// Initializes a new instance of the class. /// The underlying notification. /// The initiator plugin. Use null if originated by Dalamud. public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) @@ -41,8 +47,10 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.InitiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); + this.progressEasing = new InOutCubic(NotificationConstants.ProgressAnimationDuration); this.showEasing.Start(); + this.progressEasing.Start(); this.UpdateIcon(); } @@ -64,39 +72,111 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public long Id { get; } = IActiveNotification.CreateNewId(); - /// - /// Gets the time of creating this notification. - /// + /// Gets the time of creating this notification. public DateTime CreatedAt { get; } = DateTime.Now; - /// - /// Gets the time of starting to count the timer for the expiration. - /// + /// Gets the time of starting to count the timer for the expiration. public DateTime ExpiryRelativeToTime { get; private set; } = DateTime.Now; - /// - public string Content => this.underlyingNotification.Content; + /// + public string Content + { + get => this.underlyingNotification.Content; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.Content = value; + } + } - /// - public string? Title => this.underlyingNotification.Title; + /// + public string? Title + { + get => this.underlyingNotification.Title; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.Title = value; + } + } - /// - public NotificationType Type => this.underlyingNotification.Type; + /// + public NotificationType Type + { + get => this.underlyingNotification.Type; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.Type = value; + } + } - /// - public Func>? IconCreator => this.underlyingNotification.IconCreator; + /// + public Func>? IconCreator + { + get => this.underlyingNotification.IconCreator; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.IconCreator = value; + } + } - /// - public DateTime Expiry => this.underlyingNotification.Expiry; + /// + public DateTime Expiry + { + get => this.underlyingNotification.Expiry; + set + { + if (this.underlyingNotification.Expiry == value || this.IsDismissed) + return; + this.underlyingNotification.Expiry = value; + this.ExpiryRelativeToTime = DateTime.Now; + } + } - /// - public bool Interactible => this.underlyingNotification.Interactible; + /// + public bool Interactible + { + get => this.underlyingNotification.Interactible; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.Interactible = value; + } + } - /// - public bool ClickIsDismiss => this.underlyingNotification.ClickIsDismiss; + /// + public TimeSpan HoverExtendDuration + { + get => this.underlyingNotification.HoverExtendDuration; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.HoverExtendDuration = value; + } + } - /// - public TimeSpan HoverExtendDuration => this.underlyingNotification.HoverExtendDuration; + /// + public float Progress + { + get => this.underlyingNotification.Progress; + set + { + if (this.IsDismissed) + return; + + this.progressBefore = this.ProgressEased; + this.underlyingNotification.Progress = value; + this.progressEasing.Restart(); + } + } /// public bool IsMouseHovered { get; private set; } @@ -104,19 +184,32 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public bool IsDismissed => this.hideEasing.IsRunning; - /// - /// Gets or sets the plugin that initiated this notification. - /// + /// Gets a value indicating whether has been unloaded. + public bool IsInitiatorUnloaded { get; private set; } + + /// Gets or sets the plugin that initiated this notification. public LocalPlugin? InitiatorPlugin { get; set; } - /// - /// Gets or sets the icon of this notification. - /// + /// Gets or sets the icon of this notification. public Task? IconTask { get; set; } - /// - /// Gets the default color of the notification. - /// + /// Gets the eased progress. + private float ProgressEased + { + get + { + if (this.Progress < 0) + return 0f; + + if (Math.Abs(this.Progress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) + return this.Progress; + + var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f); + return this.progressBefore + (state * (this.Progress - this.progressBefore)); + } + } + + /// Gets the default color of the notification. private Vector4 DefaultIconColor => this.Type switch { NotificationType.None => ImGuiColors.DalamudWhite, @@ -127,9 +220,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable _ => ImGuiColors.DalamudWhite, }; - /// - /// Gets the default icon of the notification. - /// + /// Gets the default icon of the notification. private string? DefaultIconString => this.Type switch { NotificationType.None => null, @@ -140,9 +231,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable _ => null, }; - /// - /// Gets the default title of the notification. - /// + /// Gets the default title of the notification. private string? DefaultTitle => this.Type switch { NotificationType.None => null, @@ -153,6 +242,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable _ => null, }; + /// Gets the string for the initiator field. + private string InitiatorString => + this.InitiatorPlugin is not { } initiatorPlugin + ? NotificationConstants.DefaultInitiator + : this.IsInitiatorUnloaded + ? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name) + : initiatorPlugin.Name; + /// public void Dispose() { @@ -170,9 +267,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); - /// - /// Dismisses this notification. Multiple calls will be ignored. - /// + /// Dismisses this notification. Multiple calls will be ignored. /// The reason of dismissal. public void DismissNow(NotificationDismissReason reason) { @@ -192,20 +287,17 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - /// - /// Updates animations. - /// + /// Updates animations. /// true if the notification is over. public bool UpdateAnimations() { this.showEasing.Update(); this.hideEasing.Update(); + this.progressEasing.Update(); return this.hideEasing.IsRunning && this.hideEasing.IsDone; } - /// - /// Draws this notification. - /// + /// Draws this notification. /// The maximum width of the notification window. /// The offset from the bottom. /// The height of the notification. @@ -230,13 +322,29 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable var notificationManager = Service.Get(); var interfaceManager = Service.Get(); - var unboundedWidth = NotificationConstants.ScaledWindowPadding * 3; + var unboundedWidth = ImGui.CalcTextSize(this.Content).X; + float closeButtonHorizontalSpaceReservation; + using (interfaceManager.IconFontHandle?.Push()) + { + closeButtonHorizontalSpaceReservation = ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X; + closeButtonHorizontalSpaceReservation += NotificationConstants.ScaledWindowPadding; + } + + unboundedWidth = Math.Max( + unboundedWidth, + ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X); + unboundedWidth = Math.Max( + unboundedWidth, + ImGui.CalcTextSize(this.InitiatorString).X); + unboundedWidth = Math.Max( + unboundedWidth, + ImGui.CalcTextSize(this.CreatedAt.FormatAbsoluteDateTime()).X + closeButtonHorizontalSpaceReservation); + unboundedWidth = Math.Max( + unboundedWidth, + ImGui.CalcTextSize(this.CreatedAt.FormatRelativeDateTime()).X + closeButtonHorizontalSpaceReservation); + + unboundedWidth += NotificationConstants.ScaledWindowPadding * 3; unboundedWidth += NotificationConstants.ScaledIconSize; - unboundedWidth += Math.Max( - Math.Max( - ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X, - ImGui.CalcTextSize(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator).X), - ImGui.CalcTextSize(this.Content).X); var width = Math.Min(maxWidth, unboundedWidth); @@ -244,16 +352,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable var viewportPos = viewport.WorkPos; var viewportSize = viewport.WorkSize; - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), - ImGuiCond.Always, - Vector2.One); - ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue)); ImGui.PushID(this.Id.GetHashCode()); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); unsafe @@ -267,67 +366,88 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable NotificationConstants.BackgroundOpacity)); } + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue)); + ImGui.PushStyleVar( + ImGuiStyleVar.WindowPadding, + new Vector2(NotificationConstants.ScaledWindowPadding, 0)); ImGui.Begin( - $"##NotifyWindow{this.Id}", + $"##NotifyMainWindow{this.Id}", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | - (this.Interactible ? ImGuiWindowFlags.None : ImGuiWindowFlags.NoInputs) | + (this.Interactible + ? ImGuiWindowFlags.None + : ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoBringToFrontOnFocus) | ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoBringToFrontOnFocus | - ImGuiWindowFlags.NoFocusOnAppearing); - - var basePos = ImGui.GetCursorPos(); - this.DrawIcon( - notificationManager, - basePos, - basePos + new Vector2(NotificationConstants.ScaledIconSize)); - basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; - width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2); - this.DrawTitle(basePos, basePos + new Vector2(width, 0)); - basePos.Y = ImGui.GetCursorPosY(); - this.DrawContentBody(basePos, basePos + new Vector2(width, 0)); - if (ImGui.IsWindowHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - { - this.Click?.InvokeSafely(this); - if (this.ClickIsDismiss) - this.DismissNow(NotificationDismissReason.Manual); - } + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoDocking); + this.DrawNotificationMainWindowContent(notificationManager, width); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); - - float expiryRatio; - if (this.IsDismissed) - { - expiryRatio = 0f; - } - else if (this.Expiry == DateTime.MaxValue || (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered)) - { - expiryRatio = 1f; - } - else - { - expiryRatio = (float)((this.Expiry - DateTime.Now).TotalMilliseconds / - (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); - } - - expiryRatio = Math.Clamp(expiryRatio, 0f, 1f); - ImGui.PushClipRect(windowPos, windowPos + windowSize, false); - ImGui.GetWindowDrawList().AddRectFilled( - windowPos + new Vector2(0, windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), - windowPos + windowSize with { X = windowSize.X * expiryRatio }, - ImGui.GetColorU32(this.DefaultIconColor)); - ImGui.PopClipRect(); + var hovered = ImGui.IsWindowHovered(); ImGui.End(); + ImGui.PopStyleVar(); - if (!this.IsDismissed) - this.DrawCloseButton(interfaceManager, windowPos); + offsetY += windowSize.Y; + + var actionWindowHeight = + // Content + ImGui.GetTextLineHeight() + + // Top and bottom padding + (NotificationConstants.ScaledWindowPadding * 2); + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints(new(width, actionWindowHeight), new(width, actionWindowHeight)); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); + ImGui.Begin( + $"##NotifyActionWindow{this.Id}", + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoDocking); + + this.DrawNotificationActionWindowContent(interfaceManager, width); + windowSize.Y += actionWindowHeight; + windowPos.Y -= actionWindowHeight; + hovered |= ImGui.IsWindowHovered(); + + ImGui.End(); + ImGui.PopStyleVar(); ImGui.PopStyleColor(); - ImGui.PopStyleVar(3); + ImGui.PopStyleVar(2); ImGui.PopID(); + if (hovered) + { + if (this.Click is null) + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + this.DismissNow(NotificationDismissReason.Manual); + } + else + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) + || ImGui.IsMouseClicked(ImGuiMouseButton.Right) + || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + this.Click.InvokeSafely(this); + } + } + if (windowPos.X <= ImGui.GetIO().MousePos.X && windowPos.Y <= ImGui.GetIO().MousePos.Y && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X @@ -361,31 +481,28 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public void Update(INotification newNotification) { - this.underlyingNotification.Content = newNotification.Content; - this.underlyingNotification.Title = newNotification.Title; - this.underlyingNotification.Type = newNotification.Type; - this.underlyingNotification.IconCreator = newNotification.IconCreator; - if (this.underlyingNotification.Expiry != newNotification.Expiry) - { - this.underlyingNotification.Expiry = newNotification.Expiry; - this.ExpiryRelativeToTime = DateTime.Now; - } - - this.underlyingNotification.Interactible = newNotification.Interactible; - this.underlyingNotification.ClickIsDismiss = newNotification.ClickIsDismiss; - this.underlyingNotification.HoverExtendDuration = newNotification.HoverExtendDuration; + if (this.IsDismissed) + return; + this.Content = newNotification.Content; + this.Title = newNotification.Title; + this.Type = newNotification.Type; + this.IconCreator = newNotification.IconCreator; + this.Expiry = newNotification.Expiry; + this.Interactible = newNotification.Interactible; + this.HoverExtendDuration = newNotification.HoverExtendDuration; + this.Progress = newNotification.Progress; } /// public void UpdateIcon() { + if (this.IsDismissed) + return; this.ClearIconTask(); this.IconTask = this.IconCreator?.Invoke(); } - /// - /// Removes non-Dalamud invocation targets from events. - /// + /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); @@ -395,6 +512,17 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); + this.underlyingNotification.Interactible = false; + this.IsInitiatorUnloaded = true; + + var now = DateTime.Now; + var newMaxExpiry = now + NotificationConstants.DefaultDisplayDuration; + if (this.underlyingNotification.Expiry > newMaxExpiry) + { + this.underlyingNotification.Expiry = newMaxExpiry; + this.ExpiryRelativeToTime = now; + } + return; T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate @@ -426,6 +554,84 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.IconTask = null; } + private void DrawNotificationMainWindowContent(NotificationManager notificationManager, float width) + { + var basePos = ImGui.GetCursorPos(); + this.DrawIcon( + notificationManager, + basePos, + basePos + new Vector2(NotificationConstants.ScaledIconSize)); + basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; + width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2); + this.DrawTitle(basePos, basePos + new Vector2(width, 0)); + basePos.Y = ImGui.GetCursorPosY(); + this.DrawContentBody(basePos, basePos + new Vector2(width, 0)); + + // Intention was to have left, right, and bottom have the window padding and top have the component gap, + // but as ImGui only allows horz/vert padding, we add the extra bottom padding. + // Top padding is zero, as the action window will add the padding. + ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding)); + + float progressL, progressR; + if (this.IsDismissed) + { + var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; + var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; + var length = (this.prevProgressR - this.prevProgressL) / 2f; + progressL = midpoint - (length * v); + progressR = midpoint + (length * v); + } + else if (this.Expiry == DateTime.MaxValue) + { + if (this.Progress >= 0) + { + progressL = 0f; + progressR = this.ProgressEased; + } + else + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.IndeterminateProgressbarLoopDuration) / + NotificationConstants.IndeterminateProgressbarLoopDuration); + progressL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); + progressR = Math.Min(elapsed, 2f / 3) / (2f / 3); + progressL = MathF.Pow(progressL, 3); + progressR = 1f - MathF.Pow(1f - progressR, 3); + } + + this.prevProgressL = progressL; + this.prevProgressR = progressR; + } + else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + { + progressL = 0f; + progressR = 1f; + this.prevProgressL = progressL; + this.prevProgressR = progressR; + } + else + { + progressL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / + (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + progressR = 1f; + this.prevProgressL = progressL; + this.prevProgressR = progressR; + } + + progressR = Math.Clamp(progressR, 0f, 1f); + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos + new Vector2( + windowSize.X * progressL, + windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), + windowPos + windowSize with { X = windowSize.X * progressR }, + ImGui.GetColorU32(this.DefaultIconColor)); + ImGui.PopClipRect(); + } + private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord) { string? iconString = null; @@ -486,8 +692,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.SetCursorPos(pos); ImGui.Image(iconTexture.ImGuiHandle, size); } - else if (fontHandle is not null) + else { + // Just making it extremely sure + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (fontHandle is null || iconString is null) + // ReSharper disable once HeuristicUnreachableCode + return; + using (fontHandle.Push()) { var size = ImGui.CalcTextSize(iconString); @@ -514,47 +726,13 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); - ImGui.TextUnformatted(this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator); + ImGui.TextUnformatted(this.InitiatorString); ImGui.PopStyleColor(); ImGui.PopTextWrapPos(); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); } - private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 screenCoord) - { - using (interfaceManager.IconFontHandle?.Push()) - { - var str = FontAwesomeIcon.Times.ToIconString(); - var size = NotificationConstants.ScaledCloseButtonMinSize; - var textSize = ImGui.CalcTextSize(str); - size = Math.Max(size, Math.Max(textSize.X, textSize.Y)); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - ImGui.PushStyleVar(ImGuiStyleVar.FrameRounding, 0f); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - ImGui.PushStyleColor(ImGuiCol.Button, 0); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); - - // ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos(screenCoord, ImGuiCond.Always, new(1, 0)); - ImGui.SetNextWindowSizeConstraints(new(size), new(size)); - ImGui.Begin( - $"##CloseButtonWindow{this.Id}", - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoBringToFrontOnFocus | - ImGuiWindowFlags.NoFocusOnAppearing); - - if (ImGui.Button(str, new(size))) - this.DismissNow(); - - ImGui.End(); - ImGui.PopStyleColor(2); - ImGui.PopStyleVar(3); - } - } - private void DrawContentBody(Vector2 minCoord, Vector2 maxCoord) { ImGui.SetCursorPos(minCoord); @@ -576,4 +754,44 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } } + + private void DrawNotificationActionWindowContent(InterfaceManager interfaceManager, float width) + { + ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted( + this.IsMouseHovered + ? this.CreatedAt.FormatAbsoluteDateTime() + : this.CreatedAt.FormatRelativeDateTime()); + ImGui.PopStyleColor(); + + this.DrawCloseButton( + interfaceManager, + new(width - NotificationConstants.ScaledWindowPadding, NotificationConstants.ScaledWindowPadding), + NotificationConstants.ScaledWindowPadding); + } + + private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 rt, float pad) + { + using (interfaceManager.IconFontHandle?.Push()) + { + var str = FontAwesomeIcon.Times.ToIconString(); + var textSize = ImGui.CalcTextSize(str); + var size = Math.Max(textSize.X, textSize.Y); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + if (!this.IsMouseHovered) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + ImGui.SetCursorPos(rt - new Vector2(size, 0) - new Vector2(pad)); + if (ImGui.Button(str, new(size + (pad * 2)))) + this.DismissNow(); + + ImGui.PopStyleColor(2); + if (!this.IsMouseHovered) + ImGui.PopStyleVar(); + ImGui.PopStyleVar(); + } + } } diff --git a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs index bf71cd87e..3592c2a00 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs @@ -9,7 +9,9 @@ namespace Dalamud.Interface.Internal.Notifications; /// internal static class NotificationConstants { - // ..............................[X] + // .............................[..] + // ..when.......................[XX] + // .. .. // ..[i]..title title title title .. // .. by this_plugin .. // .. .. @@ -28,6 +30,9 @@ internal static class NotificationConstants /// The background opacity of a notification window. public const float BackgroundOpacity = 0.82f; + /// The duration of indeterminate progress bar loop in milliseconds. + public const float IndeterminateProgressbarLoopDuration = 2000f; + /// Duration of show animation. public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); @@ -40,6 +45,12 @@ internal static class NotificationConstants /// Duration of hide animation. public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + /// Duration of hide animation. + public static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200); + + /// Text color for the when. + public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); + /// Text color for the close button [X]. public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -52,6 +63,21 @@ internal static class NotificationConstants /// Text color for the body. public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + /// Gets the relative time format strings. + private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = + { + (TimeSpan.FromDays(7), null), + (TimeSpan.FromDays(2), "{0:%d} days ago"), + (TimeSpan.FromDays(1), "yesterday"), + (TimeSpan.FromHours(2), "{0:%h} hours ago"), + (TimeSpan.FromHours(1), "an hour ago"), + (TimeSpan.FromMinutes(2), "{0:%m} minutes ago"), + (TimeSpan.FromMinutes(1), "a minute ago"), + (TimeSpan.FromSeconds(2), "{0:%s} seconds ago"), + (TimeSpan.FromSeconds(1), "a second ago"), + (TimeSpan.MinValue, "just now"), + }; + /// Gets the scaled padding of the window (dot(.) in the above diagram). public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); @@ -69,9 +95,36 @@ internal static class NotificationConstants /// Gets the scaled size of the icon. public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); - /// Gets the scaled size of the close button. - public static float ScaledCloseButtonMinSize => MathF.Round(16 * ImGuiHelpers.GlobalScale); - /// Gets the height of the expiry progress bar. public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); + + /// Gets the string format of the initiator name field, if the initiator is unloaded. + public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; + + /// + /// Formats an instance of as a relative time. + /// + /// When. + /// The formatted string. + public static string FormatRelativeDateTime(this DateTime when) + { + var ts = DateTime.Now - when; + foreach (var (minSpan, formatString) in RelativeFormatStrings) + { + if (ts < minSpan) + continue; + if (formatString is null) + break; + return string.Format(formatString, ts); + } + + return when.FormatAbsoluteDateTime(); + } + + /// + /// Formats an instance of as an absolute time. + /// + /// When. + /// The formatted string. + public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 2eee81ee2..060498ba7 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -1,5 +1,8 @@ -using Dalamud.Interface.Internal.Notifications; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -9,11 +12,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class ImGuiWidget : IDataWindowWidget { + private NotificationTemplate notificationTemplate; + /// public string[]? CommandShortcuts { get; init; } = { "imgui" }; - + /// - public string DisplayName { get; init; } = "ImGui"; + public string DisplayName { get; init; } = "ImGui"; /// public bool Ready { get; set; } @@ -22,6 +27,7 @@ internal class ImGuiWidget : IDataWindowWidget public void Load() { this.Ready = true; + this.notificationTemplate.Reset(); } /// @@ -38,51 +44,134 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Separator(); - ImGui.TextUnformatted($"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); + ImGui.TextUnformatted( + $"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); ImGui.Separator(); - if (ImGui.Button("Add random notification")) - { - const string text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + ImGui.Checkbox("##manualContent", ref this.notificationTemplate.ManualContent); + ImGui.SameLine(); + ImGui.InputText("Content##content", ref this.notificationTemplate.Content, 255); + + ImGui.Checkbox("##manualTitle", ref this.notificationTemplate.ManualTitle); + ImGui.SameLine(); + ImGui.InputText("Title##title", ref this.notificationTemplate.Title, 255); + + ImGui.Checkbox("##manualType", ref this.notificationTemplate.ManualType); + ImGui.SameLine(); + ImGui.Combo( + "Type##type", + ref this.notificationTemplate.TypeInt, + NotificationTemplate.TypeTitles, + NotificationTemplate.TypeTitles.Length); + + ImGui.Combo( + "Duration", + ref this.notificationTemplate.DurationInt, + NotificationTemplate.DurationTitles, + NotificationTemplate.DurationTitles.Length); + + ImGui.Combo( + "Progress", + ref this.notificationTemplate.ProgressMode, + NotificationTemplate.ProgressModeTitles, + NotificationTemplate.ProgressModeTitles.Length); + + ImGui.Checkbox("Interactible", ref this.notificationTemplate.Interactible); + + ImGui.Checkbox("Action Bar", ref this.notificationTemplate.ActionBar); + + if (ImGui.Button("Add notification")) + { + var text = + "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + + NewRandom(out var title, out var type, out var progress); + if (this.notificationTemplate.ManualTitle) + title = this.notificationTemplate.Title; + if (this.notificationTemplate.ManualContent) + text = this.notificationTemplate.Content; + if (this.notificationTemplate.ManualType) + type = (NotificationType)this.notificationTemplate.TypeInt; + + var duration = NotificationTemplate.Durations[this.notificationTemplate.DurationInt]; - NewRandom(out var title, out var type); var n = notifications.AddNotification( new() { Content = text, Title = title, Type = type, - Interactible = true, - ClickIsDismiss = false, - Expiry = DateTime.MaxValue, + Interactible = this.notificationTemplate.Interactible, + Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, + Progress = this.notificationTemplate.ProgressMode switch + { + 0 => 1f, + 1 => progress, + 2 => 0f, + 3 => 0f, + 4 => -1f, + _ => 0.5f, + }, }); - - var nclick = 0; - n.Click += _ => nclick++; - n.DrawActions += an => + switch (this.notificationTemplate.ProgressMode) { - if (ImGui.Button("Update in place")) - { - NewRandom(out title, out type); - an.Update(an.CloneNotification() with { Title = title, Type = type }); - } + case 2: + Task.Run( + async () => + { + for (var i = 0; i <= 10 && !n.IsDismissed; i++) + { + await Task.Delay(500); + n.Progress = i / 10f; + } + }); + break; + case 3: + Task.Run( + async () => + { + for (var i = 0; i <= 10 && !n.IsDismissed; i++) + { + await Task.Delay(500); + n.Progress = i / 10f; + } - if (an.IsMouseHovered) + n.Expiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + }); + break; + } + + if (this.notificationTemplate.ActionBar) + { + var nclick = 0; + n.Click += _ => nclick++; + n.DrawActions += an => { + if (ImGui.Button("Update in place")) + { + NewRandom(out title, out type, out progress); + an.Title = title; + an.Type = type; + an.Progress = progress; + } + + if (an.IsMouseHovered) + { + ImGui.SameLine(); + if (ImGui.Button("Dismiss")) + an.DismissNow(); + } + + ImGui.AlignTextToFramePadding(); ImGui.SameLine(); - if (ImGui.Button("Dismiss")) - an.DismissNow(); - } - - ImGui.AlignTextToFramePadding(); - ImGui.SameLine(); - ImGui.TextUnformatted($"Clicked {nclick} time(s)"); - }; + ImGui.TextUnformatted($"Clicked {nclick} time(s)"); + }; + } } } - private static void NewRandom(out string? title, out NotificationType type) + private static void NewRandom(out string? title, out NotificationType type, out float progress) { var rand = new Random(); @@ -106,5 +195,72 @@ internal class ImGuiWidget : IDataWindowWidget 4 => NotificationType.None, _ => NotificationType.None, }; + + if (rand.Next() % 2 == 0) + progress = -1; + else + progress = rand.NextSingle(); + } + + private struct NotificationTemplate + { + public static readonly string[] ProgressModeTitles = + { + "Default", + "Random", + "Increasing", + "Increasing & Auto Dismiss", + "Indeterminate", + }; + + public static readonly string[] TypeTitles = + { + nameof(NotificationType.None), + nameof(NotificationType.Success), + nameof(NotificationType.Warning), + nameof(NotificationType.Error), + nameof(NotificationType.Info), + }; + + public static readonly string[] DurationTitles = + { + "Infinite", + "1 seconds", + "3 seconds (default)", + "10 seconds", + }; + + public static readonly TimeSpan[] Durations = + { + TimeSpan.MaxValue, + TimeSpan.FromSeconds(1), + NotificationConstants.DefaultDisplayDuration, + TimeSpan.FromSeconds(10), + }; + + public bool ManualContent; + public string Content; + public bool ManualTitle; + public string Title; + public bool ManualType; + public int TypeInt; + public int DurationInt; + public bool Interactible; + public bool ActionBar; + public int ProgressMode; + + public void Reset() + { + this.ManualContent = false; + this.Content = string.Empty; + this.ManualTitle = false; + this.Title = string.Empty; + this.ManualType = false; + this.TypeInt = (int)NotificationType.None; + this.DurationInt = 2; + this.Interactible = true; + this.ActionBar = true; + this.ProgressMode = 0; + } } } From 7aba15ef5b5c68bba3fd0eb96ee4145fdd256b17 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sun, 25 Feb 2024 23:56:49 +0900 Subject: [PATCH 536/585] Normalize namespaces --- Dalamud.CorePlugin/PluginImpl.cs | 7 +- Dalamud.CorePlugin/PluginWindow.cs | 74 ++++++++++++++++++- Dalamud/Game/ChatHandlers.cs | 1 + .../ImGuiNotification/IActiveNotification.cs | 2 - .../ImGuiNotification/INotification.cs | 4 +- .../Internal}/ActiveNotification.cs | 1 + .../Internal}/NotificationConstants.cs | 4 +- .../Internal}/NotificationManager.cs | 4 +- .../ImGuiNotification/Notification.cs | 1 + .../NotificationDismissReason.cs | 20 ++--- .../NotificationDismissedDelegate.cs | 4 +- .../Interface/Internal/InterfaceManager.cs | 1 + .../Notifications/NotificationType.cs | 29 +++----- .../Windows/Data/Widgets/DataShareWidget.cs | 1 + .../Windows/Data/Widgets/ImGuiWidget.cs | 1 + .../PluginInstaller/PluginInstallerWindow.cs | 1 + .../PluginInstaller/ProfileManagerWidget.cs | 1 + .../Internal/Windows/PluginStatWindow.cs | 1 + Dalamud/Interface/UiBuilder.cs | 1 + .../Plugin/Internal/Types/LocalDevPlugin.cs | 1 + Dalamud/Utility/Api10ToDoAttribute.cs | 12 ++- 21 files changed, 124 insertions(+), 47 deletions(-) rename Dalamud/Interface/{Internal/Notifications => ImGuiNotification/Internal}/ActiveNotification.cs (99%) rename Dalamud/Interface/{Internal/Notifications => ImGuiNotification/Internal}/NotificationConstants.cs (98%) rename Dalamud/Interface/{Internal/Notifications => ImGuiNotification/Internal}/NotificationManager.cs (98%) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index cb9b4368a..afeaad426 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -56,15 +56,16 @@ namespace Dalamud.CorePlugin /// /// Dalamud plugin interface. /// Logging service. - public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log) + public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log, INotificationManager notificationManager) { + this.NotificationManager = notificationManager; try { // this.InitLoc(); this.Interface = pluginInterface; this.pluginLog = log; - this.windowSystem.AddWindow(new PluginWindow()); + this.windowSystem.AddWindow(new PluginWindow(this)); this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; @@ -84,6 +85,8 @@ namespace Dalamud.CorePlugin } } + public INotificationManager NotificationManager { get; } + /// /// Gets the plugin interface. /// diff --git a/Dalamud.CorePlugin/PluginWindow.cs b/Dalamud.CorePlugin/PluginWindow.cs index 27be82f41..33b8505c4 100644 --- a/Dalamud.CorePlugin/PluginWindow.cs +++ b/Dalamud.CorePlugin/PluginWindow.cs @@ -1,7 +1,9 @@ using System; using System.Numerics; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; + using ImGuiNET; namespace Dalamud.CorePlugin @@ -14,15 +16,19 @@ namespace Dalamud.CorePlugin /// /// Initializes a new instance of the class. /// - public PluginWindow() + /// + public PluginWindow(PluginImpl pluginImpl) : base("CorePlugin") { + this.PluginImpl = pluginImpl; this.IsOpen = true; this.Size = new Vector2(810, 520); this.SizeCondition = ImGuiCond.FirstUseEver; } + public PluginImpl PluginImpl { get; } + /// public void Dispose() { @@ -36,6 +42,72 @@ namespace Dalamud.CorePlugin /// public override void Draw() { + if (ImGui.Button("Legacy")) + this.PluginImpl.Interface.UiBuilder.AddNotification("asdf"); + if (ImGui.Button("Test")) + { + const string text = + "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + + NewRandom(out var title, out var type); + var n = this.PluginImpl.NotificationManager.AddNotification( + new() + { + Content = text, + Title = title, + Type = type, + Interactible = true, + Expiry = DateTime.MaxValue, + }); + + var nclick = 0; + n.Click += _ => nclick++; + n.DrawActions += an => + { + if (ImGui.Button("Update in place")) + { + NewRandom(out title, out type); + an.Update(an.CloneNotification() with { Title = title, Type = type }); + } + + if (an.IsMouseHovered) + { + ImGui.SameLine(); + if (ImGui.Button("Dismiss")) + an.DismissNow(); + } + + ImGui.AlignTextToFramePadding(); + ImGui.SameLine(); + ImGui.TextUnformatted($"Clicked {nclick} time(s)"); + }; + } + } + + private static void NewRandom(out string? title, out NotificationType type) + { + var rand = new Random(); + + title = rand.Next(0, 7) switch + { + 0 => "This is a toast", + 1 => "Truly, a toast", + 2 => "I am testing this toast", + 3 => "I hope this looks right", + 4 => "Good stuff", + 5 => "Nice", + _ => null, + }; + + type = rand.Next(0, 5) switch + { + 0 => NotificationType.Error, + 1 => NotificationType.Warning, + 2 => NotificationType.Info, + 3 => NotificationType.Success, + 4 => NotificationType.None, + _ => NotificationType.None, + }; } } } diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 836fb5ec8..5dd6ed3ba 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index d1aa1d95b..2e0c62783 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,8 +1,6 @@ using System.Threading; using System.Threading.Tasks; -using Dalamud.Game.Text; -using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index cbd8ad633..a5d56d783 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -35,7 +35,7 @@ public interface INotification /// Set to to make the notification not have an expiry time /// (sticky, indeterminate, permanent, or persistent). DateTime Expiry { get; } - + /// Gets a value indicating whether this notification may be interacted. /// /// Set this value to true if you want to respond to user inputs from @@ -52,7 +52,7 @@ public interface INotification /// This property is applicable regardless of . /// TimeSpan HoverExtendDuration { get; } - + /// Gets the progress for the progress bar of the notification. /// The progress should either be in the range between 0 and 1 or be a negative value. /// Specifying a negative value will show an indeterminate progress bar. diff --git a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs similarity index 99% rename from Dalamud/Interface/Internal/Notifications/ActiveNotification.cs rename to Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index c1fecdd3b..963b74b6c 100644 --- a/Dalamud/Interface/Internal/Notifications/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -7,6 +7,7 @@ using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Windows; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.Utility; diff --git a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs similarity index 98% rename from Dalamud/Interface/Internal/Notifications/NotificationConstants.cs rename to Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index 3592c2a00..a16fb904d 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -2,7 +2,7 @@ using System.Numerics; using Dalamud.Interface.Utility; -namespace Dalamud.Interface.Internal.Notifications; +namespace Dalamud.Interface.ImGuiNotification.Internal; /// /// Constants for drawing notification windows. @@ -94,7 +94,7 @@ internal static class NotificationConstants /// Gets the scaled size of the icon. public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); - + /// Gets the height of the expiry progress bar. public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs similarity index 98% rename from Dalamud/Interface/Internal/Notifications/NotificationManager.cs rename to Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index fd92c30df..b67605541 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -2,7 +2,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using Dalamud.Interface.GameFonts; -using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; @@ -11,7 +11,7 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; -namespace Dalamud.Interface.Internal.Notifications; +namespace Dalamud.Interface.ImGuiNotification.Internal; /// /// Class handling notifications/toasts in ImGui. diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index ccfb250c3..bab6f6f23 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs index 6e2fa338e..47e52b142 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs @@ -1,22 +1,16 @@ namespace Dalamud.Interface.ImGuiNotification; -/// -/// Specifies the reason of dismissal for a notification. -/// +/// Specifies the reason of dismissal for a notification. public enum NotificationDismissReason { - /// - /// The notification is dismissed because the expiry specified from is met. - /// + /// The notification is dismissed because the expiry specified from is + /// met. Timeout = 1, - - /// - /// The notification is dismissed because the user clicked on the close button on a notification window. + + /// The notification is dismissed because the user clicked on the close button on a notification window. /// Manual = 2, - - /// - /// The notification is dismissed from calling . - /// + + /// The notification is dismissed from calling . Programmatical = 3, } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs index 5e899c32c..09d6fd818 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs @@ -1,8 +1,6 @@ namespace Dalamud.Interface.ImGuiNotification; -/// -/// Delegate representing the dismissal of an active notification. -/// +/// Delegate representing the dismissal of an active notification. /// The notification being dismissed. /// The reason of dismissal. public delegate void NotificationDismissedDelegate( diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 3db799be0..c811e9287 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -14,6 +14,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Hooking.WndProcHook; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; diff --git a/Dalamud/Interface/Internal/Notifications/NotificationType.cs b/Dalamud/Interface/Internal/Notifications/NotificationType.cs index 1885ec809..5fffbe9af 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationType.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationType.cs @@ -1,32 +1,23 @@ -namespace Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; -/// -/// Possible notification types. -/// +namespace Dalamud.Interface.Internal.Notifications; + +/// Possible notification types. +[Api10ToDo(Api10ToDoAttribute.MoveNamespace, nameof(ImGuiNotification.Internal))] public enum NotificationType { - /// - /// No special type. - /// + /// No special type. None, - /// - /// Type indicating success. - /// + /// Type indicating success. Success, - /// - /// Type indicating a warning. - /// + /// Type indicating a warning. Warning, - /// - /// Type indicating an error. - /// + /// Type indicating an error. Error, - /// - /// Type indicating generic information. - /// + /// Type indicating generic information. Info, } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 92f340a7b..346255dfe 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Reflection; using System.Text; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 060498ba7..67a65f74f 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 95c227662..210290f17 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -15,6 +15,7 @@ using Dalamud.Game.Command; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index eafea9d16..857002771 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -7,6 +7,7 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index a1d93bb8c..bfa30cafd 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -7,6 +7,7 @@ using System.Reflection; using Dalamud.Game; using Dalamud.Hooking.Internal; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 6da6ebc4a..64ff0cc45 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Gui; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 580d5c161..1f9f503e0 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types.Manifest; diff --git a/Dalamud/Utility/Api10ToDoAttribute.cs b/Dalamud/Utility/Api10ToDoAttribute.cs index f397f8f0c..a13aaead5 100644 --- a/Dalamud/Utility/Api10ToDoAttribute.cs +++ b/Dalamud/Utility/Api10ToDoAttribute.cs @@ -11,9 +11,19 @@ internal sealed class Api10ToDoAttribute : Attribute /// public const string DeleteCompatBehavior = "Delete. This is for making API 9 plugins work."; + /// + /// Marks that this should be moved to an another namespace. + /// + public const string MoveNamespace = "Move to another namespace."; + /// /// Initializes a new instance of the class. /// /// The explanation. - public Api10ToDoAttribute(string what) => _ = what; + /// The explanation 2. + public Api10ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } } From 3a6aa13c3b23a4533333263af6a6e4716d5bf47e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:21:45 +0900 Subject: [PATCH 537/585] Add IconSource --- .../ImGuiNotification/IActiveNotification.cs | 11 +- .../ImGuiNotification/INotification.cs | 26 ++-- .../INotificationIconSource.cs | 20 +++ .../INotificationMaterializedIcon.cs | 18 +++ .../IconSource/FilePathIconSource.cs | 50 +++++++ .../IconSource/FontAwesomeIconIconSource.cs | 62 +++++++++ .../IconSource/GamePathIconSource.cs | 51 ++++++++ .../IconSource/SeIconCharIconSource.cs | 55 ++++++++ .../IconSource/TextureWrapTaskIconSource.cs | 59 +++++++++ .../Internal/ActiveNotification.cs | 122 ++++-------------- .../Internal/NotificationUtilities.cs | 63 +++++++++ .../ImGuiNotification/Notification.cs | 4 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 84 +++++++++++- 13 files changed, 506 insertions(+), 119 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 2e0c62783..fecccf092 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,5 +1,4 @@ using System.Threading; -using System.Threading.Tasks; using Dalamud.Interface.Internal.Notifications; @@ -57,8 +56,10 @@ public interface IActiveNotification : INotification /// new NotificationType Type { get; set; } - /// - new Func>? IconCreator { get; set; } + /// Gets or sets the icon source. + /// Setting a new value to this property does not change the icon. Use to do so. + /// + new INotificationIconSource? IconSource { get; set; } /// new DateTime Expiry { get; set; } @@ -91,13 +92,13 @@ public interface IActiveNotification : INotification /// Updates the notification data. /// - /// Call to update the icon using the new . + /// Call to update the icon using the new . /// If is true, then this function is a no-op. /// /// The new notification entry. void Update(INotification newNotification); - /// Loads the icon again using . + /// Loads the icon again using . /// If is true, then this function is a no-op. void UpdateIcon(); diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index a5d56d783..c4a7b46ac 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,7 +1,4 @@ -using System.Threading.Tasks; - -using Dalamud.Game.Text; -using Dalamud.Interface.Internal; +using Dalamud.Interface.ImGuiNotification.IconSource; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -18,18 +15,17 @@ public interface INotification /// Gets the type of the notification. NotificationType Type { get; } - /// Gets the icon creator function for the notification.
- /// Currently , , and types - /// are accepted.
- /// - /// The icon created by the task returned will be owned by Dalamud, - /// i.e. it will be d automatically as needed.
- /// If null is supplied for this property or of the returned task - /// is false, then the corresponding icon with will be used.
- /// Use if you have an instance of that you - /// can transfer ownership to Dalamud and is available for use right away. + /// Gets the icon source. + /// The following icon sources are currently available.
+ ///
    + ///
  • + ///
  • + ///
  • + ///
  • + ///
  • + ///
///
- Func>? IconCreator { get; } + INotificationIconSource? IconSource { get; } /// Gets the expiry. /// Set to to make the notification not have an expiry time diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs new file mode 100644 index 000000000..8a73e2a64 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Interface.ImGuiNotification; + +/// Icon source for . +/// Plugins should NOT implement this interface. +public interface INotificationIconSource : ICloneable, IDisposable +{ + /// The internal interface. + internal interface IInternal : INotificationIconSource + { + /// Materializes the icon resource. + /// The materialized resource. + INotificationMaterializedIcon Materialize(); + } + + /// + new INotificationIconSource Clone(); + + /// + object ICloneable.Clone() => this.Clone(); +} diff --git a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs new file mode 100644 index 000000000..9be498af1 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Interface.ImGuiNotification; + +/// +/// Represents a materialized icon. +/// +internal interface INotificationMaterializedIcon : IDisposable +{ + /// Draws the icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + /// The initiator plugin. + void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin); +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs new file mode 100644 index 000000000..b1886154a --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs @@ -0,0 +1,50 @@ +using System.IO; +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of a texture from a file as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +public readonly struct FilePathIconSource : INotificationIconSource.IInternal +{ + /// The path to a .tex file inside the game resources. + public readonly string FilePath; + + /// Initializes a new instance of the struct. + /// The path to a .tex file inside the game resources. + public FilePathIconSource(string filePath) => this.FilePath = filePath; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + new MaterializedIcon(this.FilePath); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private readonly FileInfo fileInfo; + + public MaterializedIcon(string filePath) => this.fileInfo = new(filePath); + + public void Dispose() + { + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawTexture( + Service.Get().GetTextureFromFile(this.fileInfo), + minCoord, + maxCoord, + initiatorPlugin); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs new file mode 100644 index 000000000..8e28940ba --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs @@ -0,0 +1,62 @@ +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Plugin.Internal.Types; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of as the icon of a notification. +public readonly struct FontAwesomeIconIconSource : INotificationIconSource.IInternal +{ + /// The icon character. + public readonly FontAwesomeIcon Char; + + /// Initializes a new instance of the struct. + /// The character. + public FontAwesomeIconIconSource(FontAwesomeIcon c) => this.Char = c; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => new MaterializedIcon(this.Char); + + /// Draws the icon. + /// The icon string. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + internal static void DrawIconStatic(string iconString, Vector2 minCoord, Vector2 maxCoord, Vector4 color) + { + using (Service.Get().IconFontAwesomeFontHandle.Push()) + { + var size = ImGui.CalcTextSize(iconString); + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(iconString); + ImGui.PopStyleColor(); + } + } + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private readonly string iconString; + + public MaterializedIcon(FontAwesomeIcon c) => this.iconString = c.ToIconString(); + + public void Dispose() + { + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + DrawIconStatic(this.iconString, minCoord, maxCoord, color); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs new file mode 100644 index 000000000..9b669e62a --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs @@ -0,0 +1,51 @@ +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of a game-shipped texture as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +public readonly struct GamePathIconSource : INotificationIconSource.IInternal +{ + /// The path to a .tex file inside the game resources. + public readonly string GamePath; + + /// Initializes a new instance of the struct. + /// The path to a .tex file inside the game resources. + /// Use to get the game path from icon IDs. + public GamePathIconSource(string gamePath) => this.GamePath = gamePath; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + new MaterializedIcon(this.GamePath); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private readonly string gamePath; + + public MaterializedIcon(string gamePath) => this.gamePath = gamePath; + + public void Dispose() + { + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawTexture( + Service.Get().GetTextureFromGame(this.gamePath), + minCoord, + maxCoord, + initiatorPlugin); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs new file mode 100644 index 000000000..d34b776bc --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs @@ -0,0 +1,55 @@ +using System.Numerics; + +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Plugin.Internal.Types; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of as the icon of a notification. +public readonly struct SeIconCharIconSource : INotificationIconSource.IInternal +{ + /// The icon character. + public readonly SeIconChar Char; + + /// Initializes a new instance of the struct. + /// The character. + public SeIconCharIconSource(SeIconChar c) => this.Char = c; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => new MaterializedIcon(this.Char); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private readonly string iconString; + + public MaterializedIcon(SeIconChar c) => this.iconString = c.ToIconString(); + + public void Dispose() + { + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) + { + using (Service.Get().IconAxisFontHandle.Push()) + { + var size = ImGui.CalcTextSize(this.iconString); + var pos = ((minCoord + maxCoord) - size) / 2; + ImGui.SetCursorPos(pos); + ImGui.PushStyleColor(ImGuiCol.Text, color); + ImGui.TextUnformatted(this.iconString); + ImGui.PopStyleColor(); + } + } + } +} diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs new file mode 100644 index 000000000..28fdc4d96 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs @@ -0,0 +1,59 @@ +using System.Numerics; +using System.Threading.Tasks; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of future as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +public readonly struct TextureWrapTaskIconSource : INotificationIconSource.IInternal +{ + /// The function that returns a task resulting in a new instance of . + /// + /// Dalamud will take ownership of the result. Do not call . + public readonly Func?>? TextureWrapTaskFunc; + + /// Gets the default materialized icon, for the purpose of displaying the plugin icon. + internal static readonly INotificationMaterializedIcon DefaultMaterializedIcon = new MaterializedIcon(null); + + /// Initializes a new instance of the struct. + /// The function. + public TextureWrapTaskIconSource(Func?>? taskFunc) => + this.TextureWrapTaskFunc = taskFunc; + + /// + public INotificationIconSource Clone() => this; + + /// + void IDisposable.Dispose() + { + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + new MaterializedIcon(this.TextureWrapTaskFunc); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private Task? task; + + public MaterializedIcon(Func?>? taskFunc) => this.task = taskFunc?.Invoke(); + + public void Dispose() + { + this.task?.ToContentDisposedTask(true); + this.task = null; + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawTexture( + this.task?.IsCompletedSuccessfully is true ? this.task.Result : null, + minCoord, + maxCoord, + initiatorPlugin); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 963b74b6c..64b812197 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -1,25 +1,21 @@ using System.Numerics; using System.Runtime.Loader; -using System.Threading.Tasks; -using Dalamud.Game.Text; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.Internal; -using Dalamud.Interface.Internal.Windows; -using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ImGuiNotification.IconSource; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; -using Dalamud.Storage.Assets; using Dalamud.Utility; using ImGuiNET; using Serilog; -namespace Dalamud.Interface.Internal.Notifications; +namespace Dalamud.Interface.ImGuiNotification.Internal; /// Represents an active notification. internal sealed class ActiveNotification : IActiveNotification, IDisposable @@ -115,15 +111,15 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - /// - public Func>? IconCreator + /// + public INotificationIconSource? IconSource { - get => this.underlyingNotification.IconCreator; + get => this.underlyingNotification.IconSource; set { if (this.IsDismissed) return; - this.underlyingNotification.IconCreator = value; + this.underlyingNotification.IconSource = value; } } @@ -192,7 +188,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable public LocalPlugin? InitiatorPlugin { get; set; } /// Gets or sets the icon of this notification. - public Task? IconTask { get; set; } + public INotificationMaterializedIcon? MaterializedIcon { get; set; } /// Gets the eased progress. private float ProgressEased @@ -255,7 +251,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable public void Dispose() { this.ClearIconTask(); - this.underlyingNotification.IconCreator = null; + this.underlyingNotification.IconSource = null; this.Dismiss = null; this.Click = null; this.DrawActions = null; @@ -487,7 +483,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.Content = newNotification.Content; this.Title = newNotification.Title; this.Type = newNotification.Type; - this.IconCreator = newNotification.IconCreator; + this.IconSource = newNotification.IconSource; this.Expiry = newNotification.Expiry; this.Interactible = newNotification.Interactible; this.HoverExtendDuration = newNotification.HoverExtendDuration; @@ -500,7 +496,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable if (this.IsDismissed) return; this.ClearIconTask(); - this.IconTask = this.IconCreator?.Invoke(); + this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); } /// Removes non-Dalamud invocation targets from events. @@ -546,20 +542,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable private void ClearIconTask() { - _ = this.IconTask?.ContinueWith( - r => - { - if (r.IsCompletedSuccessfully && r.Result is IDisposable d) - d.Dispose(); - }); - this.IconTask = null; + this.MaterializedIcon?.Dispose(); + this.MaterializedIcon = null; } private void DrawNotificationMainWindowContent(NotificationManager notificationManager, float width) { var basePos = ImGui.GetCursorPos(); this.DrawIcon( - notificationManager, basePos, basePos + new Vector2(NotificationConstants.ScaledIconSize)); basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; @@ -633,84 +623,26 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.PopClipRect(); } - private void DrawIcon(NotificationManager notificationManager, Vector2 minCoord, Vector2 maxCoord) + private void DrawIcon(Vector2 minCoord, Vector2 maxCoord) { - string? iconString = null; - IFontHandle? fontHandle = null; - IDalamudTextureWrap? iconTexture = null; - switch (this.IconTask?.IsCompletedSuccessfully is true ? this.IconTask.Result : null) + if (this.MaterializedIcon is not null) { - case IDalamudTextureWrap wrap: - iconTexture = wrap; - break; - case SeIconChar icon: - iconString = string.Empty + (char)icon; - fontHandle = notificationManager.IconAxisFontHandle; - break; - case FontAwesomeIcon icon: - iconString = icon.ToIconString(); - fontHandle = notificationManager.IconFontAwesomeFontHandle; - break; - default: - iconString = this.DefaultIconString; - fontHandle = notificationManager.IconFontAwesomeFontHandle; - break; + this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); + return; } - if (string.IsNullOrWhiteSpace(iconString)) + var defaultIconString = this.DefaultIconString; + if (!string.IsNullOrWhiteSpace(defaultIconString)) { - var dam = Service.Get(); - if (this.InitiatorPlugin is null) - { - iconTexture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); - } - else - { - if (!Service.Get().TryGetIcon( - this.InitiatorPlugin, - this.InitiatorPlugin.Manifest, - this.InitiatorPlugin.IsThirdParty, - out iconTexture) || iconTexture is null) - { - iconTexture = this.InitiatorPlugin switch - { - { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), - { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), - _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), - }; - } - } + FontAwesomeIconIconSource.DrawIconStatic(defaultIconString, minCoord, maxCoord, this.DefaultIconColor); + return; } - if (iconTexture is not null) - { - var size = iconTexture.Size; - if (size.X > maxCoord.X - minCoord.X) - size *= (maxCoord.X - minCoord.X) / size.X; - if (size.Y > maxCoord.Y - minCoord.Y) - size *= (maxCoord.Y - minCoord.Y) / size.Y; - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.Image(iconTexture.ImGuiHandle, size); - } - else - { - // Just making it extremely sure - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - if (fontHandle is null || iconString is null) - // ReSharper disable once HeuristicUnreachableCode - return; - - using (fontHandle.Push()) - { - var size = ImGui.CalcTextSize(iconString); - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, this.DefaultIconColor); - ImGui.TextUnformatted(iconString); - ImGui.PopStyleColor(); - } - } + TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( + minCoord, + maxCoord, + this.DefaultIconColor, + this.InitiatorPlugin); } private void DrawTitle(Vector2 minCoord, Vector2 maxCoord) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs new file mode 100644 index 000000000..f442ef553 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs @@ -0,0 +1,63 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Windows; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// +/// Utilities for implementing stuff under . +/// +internal static class NotificationUtilities +{ + /// + /// Draws the given texture, or the icon of the plugin if texture is null. + /// + /// The texture. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The initiator plugin. + public static void DrawTexture( + IDalamudTextureWrap? texture, + Vector2 minCoord, + Vector2 maxCoord, + LocalPlugin? initiatorPlugin) + { + if (texture is null) + { + var dam = Service.Get(); + if (initiatorPlugin is null) + { + texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); + } + else + { + if (!Service.Get().TryGetIcon( + initiatorPlugin, + initiatorPlugin.Manifest, + initiatorPlugin.IsThirdParty, + out texture) || texture is null) + { + texture = initiatorPlugin switch + { + { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), + { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), + _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), + }; + } + } + } + + var size = texture.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; + ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); + ImGui.Image(texture.ImGuiHandle, size); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index bab6f6f23..8f5ec2423 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,5 +1,3 @@ -using System.Threading.Tasks; - using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; @@ -20,7 +18,7 @@ public sealed record Notification : INotification public NotificationType Type { get; set; } = NotificationType.None; /// - public Func>? IconCreator { get; set; } + public INotificationIconSource? IconSource { get; set; } /// public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 67a65f74f..71cba3297 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -1,8 +1,13 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.IconSource; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; +using Dalamud.Storage.Assets; +using Dalamud.Utility; using ImGuiNET; @@ -66,6 +71,41 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.TypeTitles, NotificationTemplate.TypeTitles.Length); + ImGui.Combo( + "Icon Source##iconSourceCombo", + ref this.notificationTemplate.IconSourceInt, + NotificationTemplate.IconSourceTitles, + NotificationTemplate.IconSourceTitles.Length); + switch (this.notificationTemplate.IconSourceInt) + { + case 1: + case 2: + ImGui.InputText( + "Icon Text##iconSourceText", + ref this.notificationTemplate.IconSourceText, + 255); + break; + case 3: + ImGui.Combo( + "Icon Source##iconSourceAssetCombo", + ref this.notificationTemplate.IconSourceAssetInt, + NotificationTemplate.AssetSources, + NotificationTemplate.AssetSources.Length); + break; + case 4: + ImGui.InputText( + "Game Path##iconSourceText", + ref this.notificationTemplate.IconSourceText, + 255); + break; + case 5: + ImGui.InputText( + "File Path##iconSourceText", + ref this.notificationTemplate.IconSourceText, + 255); + break; + } + ImGui.Combo( "Duration", ref this.notificationTemplate.DurationInt, @@ -114,6 +154,26 @@ internal class ImGuiWidget : IDataWindowWidget 4 => -1f, _ => 0.5f, }, + IconSource = this.notificationTemplate.IconSourceInt switch + { + 1 => new SeIconCharIconSource( + (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 + ? 0 + : this.notificationTemplate.IconSourceText[0])), + 2 => new FontAwesomeIconIconSource( + (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 + ? 0 + : this.notificationTemplate.IconSourceText[0])), + 3 => new TextureWrapTaskIconSource( + () => + Service.Get().GetDalamudTextureWrapAsync( + Enum.Parse( + NotificationTemplate.AssetSources[ + this.notificationTemplate.IconSourceAssetInt]))), + 4 => new GamePathIconSource(this.notificationTemplate.IconSourceText), + 5 => new FilePathIconSource(this.notificationTemplate.IconSourceText), + _ => null, + }, }); switch (this.notificationTemplate.ProgressMode) { @@ -205,6 +265,22 @@ internal class ImGuiWidget : IDataWindowWidget private struct NotificationTemplate { + public static readonly string[] IconSourceTitles = + { + "None (use Type)", + "SeIconChar", + "FontAwesomeIcon", + "TextureWrapTask from DalamudAssets", + "GamePath", + "FilePath", + }; + + public static readonly string[] AssetSources = + Enum.GetValues() + .Where(x => x.GetAttribute()?.Purpose is DalamudAssetPurpose.TextureFromPng) + .Select(Enum.GetName) + .ToArray(); + public static readonly string[] ProgressModeTitles = { "Default", @@ -243,6 +319,9 @@ internal class ImGuiWidget : IDataWindowWidget public string Content; public bool ManualTitle; public string Title; + public int IconSourceInt; + public string IconSourceText; + public int IconSourceAssetInt; public bool ManualType; public int TypeInt; public int DurationInt; @@ -256,6 +335,9 @@ internal class ImGuiWidget : IDataWindowWidget this.Content = string.Empty; this.ManualTitle = false; this.Title = string.Empty; + this.IconSourceInt = 0; + this.IconSourceText = "ui/icon/000000/000004_hr1.tex"; + this.IconSourceAssetInt = 0; this.ManualType = false; this.TypeInt = (int)NotificationType.None; this.DurationInt = 2; From 06bbc558a8fcd36837437bf10f5c7b1d846f5c7c Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:23:01 +0900 Subject: [PATCH 538/585] Revert CorePlugin commits --- Dalamud.CorePlugin/PluginImpl.cs | 7 +-- Dalamud.CorePlugin/PluginWindow.cs | 74 +----------------------------- 2 files changed, 3 insertions(+), 78 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index afeaad426..cb9b4368a 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -56,16 +56,15 @@ namespace Dalamud.CorePlugin /// /// Dalamud plugin interface. /// Logging service. - public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log, INotificationManager notificationManager) + public PluginImpl(DalamudPluginInterface pluginInterface, IPluginLog log) { - this.NotificationManager = notificationManager; try { // this.InitLoc(); this.Interface = pluginInterface; this.pluginLog = log; - this.windowSystem.AddWindow(new PluginWindow(this)); + this.windowSystem.AddWindow(new PluginWindow()); this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; @@ -85,8 +84,6 @@ namespace Dalamud.CorePlugin } } - public INotificationManager NotificationManager { get; } - /// /// Gets the plugin interface. /// diff --git a/Dalamud.CorePlugin/PluginWindow.cs b/Dalamud.CorePlugin/PluginWindow.cs index 33b8505c4..27be82f41 100644 --- a/Dalamud.CorePlugin/PluginWindow.cs +++ b/Dalamud.CorePlugin/PluginWindow.cs @@ -1,9 +1,7 @@ using System; using System.Numerics; -using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; - using ImGuiNET; namespace Dalamud.CorePlugin @@ -16,19 +14,15 @@ namespace Dalamud.CorePlugin /// /// Initializes a new instance of the class. /// - /// - public PluginWindow(PluginImpl pluginImpl) + public PluginWindow() : base("CorePlugin") { - this.PluginImpl = pluginImpl; this.IsOpen = true; this.Size = new Vector2(810, 520); this.SizeCondition = ImGuiCond.FirstUseEver; } - public PluginImpl PluginImpl { get; } - /// public void Dispose() { @@ -42,72 +36,6 @@ namespace Dalamud.CorePlugin /// public override void Draw() { - if (ImGui.Button("Legacy")) - this.PluginImpl.Interface.UiBuilder.AddNotification("asdf"); - if (ImGui.Button("Test")) - { - const string text = - "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; - - NewRandom(out var title, out var type); - var n = this.PluginImpl.NotificationManager.AddNotification( - new() - { - Content = text, - Title = title, - Type = type, - Interactible = true, - Expiry = DateTime.MaxValue, - }); - - var nclick = 0; - n.Click += _ => nclick++; - n.DrawActions += an => - { - if (ImGui.Button("Update in place")) - { - NewRandom(out title, out type); - an.Update(an.CloneNotification() with { Title = title, Type = type }); - } - - if (an.IsMouseHovered) - { - ImGui.SameLine(); - if (ImGui.Button("Dismiss")) - an.DismissNow(); - } - - ImGui.AlignTextToFramePadding(); - ImGui.SameLine(); - ImGui.TextUnformatted($"Clicked {nclick} time(s)"); - }; - } - } - - private static void NewRandom(out string? title, out NotificationType type) - { - var rand = new Random(); - - title = rand.Next(0, 7) switch - { - 0 => "This is a toast", - 1 => "Truly, a toast", - 2 => "I am testing this toast", - 3 => "I hope this looks right", - 4 => "Good stuff", - 5 => "Nice", - _ => null, - }; - - type = rand.Next(0, 5) switch - { - 0 => NotificationType.Error, - 1 => NotificationType.Warning, - 2 => NotificationType.Info, - 3 => NotificationType.Success, - 4 => NotificationType.None, - _ => NotificationType.None, - }; } } } From 1685e15113a82eb5c2d130d16032f7fede48fd31 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:36:03 +0900 Subject: [PATCH 539/585] Not anymore --- .../Internal/NotificationManager.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index b67605541..e5a27550c 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -13,10 +13,7 @@ using Dalamud.Plugin.Services; namespace Dalamud.Interface.ImGuiNotification.Internal; -/// -/// Class handling notifications/toasts in ImGui. -/// Ported from https://github.com/patrickcjk/imgui-notify. -/// +/// Class handling notifications/toasts in ImGui. [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] internal class NotificationManager : INotificationManager, IServiceType, IDisposable @@ -66,9 +63,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos return an; } - /// - /// Adds a notification originating from a plugin. - /// + /// Adds a notification originating from a plugin. /// The notification. /// The source plugin. /// The new notification. @@ -79,9 +74,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos return an; } - /// - /// Add a notification to the notification queue. - /// + /// Add a notification to the notification queue. /// The content of the notification. /// The title of the notification. /// The type of the notification. @@ -97,9 +90,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos Type = type, }); - /// - /// Draw all currently queued notifications. - /// + /// Draw all currently queued notifications. public void Draw() { var viewportSize = ImGuiHelpers.MainViewport.WorkSize; @@ -116,9 +107,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos } } -/// -/// Plugin-scoped version of a service. -/// +/// Plugin-scoped version of a service. [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.ScopedService] From c12bdaabb31d9070a4c2597884d4b680a64bfef1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:42:46 +0900 Subject: [PATCH 540/585] Format --- .../INotificationMaterializedIcon.cs | 4 +--- .../Internal/NotificationConstants.cs | 12 +++--------- .../Internal/NotificationUtilities.cs | 8 ++------ Dalamud/Interface/ImGuiNotification/Notification.cs | 4 +--- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs index 9be498af1..0657a94a4 100644 --- a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs +++ b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs @@ -4,9 +4,7 @@ using Dalamud.Plugin.Internal.Types; namespace Dalamud.Interface.ImGuiNotification; -/// -/// Represents a materialized icon. -/// +/// Represents a materialized icon. internal interface INotificationMaterializedIcon : IDisposable { /// Draws the icon. diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index a16fb904d..1da979430 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -4,9 +4,7 @@ using Dalamud.Interface.Utility; namespace Dalamud.Interface.ImGuiNotification.Internal; -/// -/// Constants for drawing notification windows. -/// +/// Constants for drawing notification windows. internal static class NotificationConstants { // .............................[..] @@ -101,9 +99,7 @@ internal static class NotificationConstants /// Gets the string format of the initiator name field, if the initiator is unloaded. public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; - /// - /// Formats an instance of as a relative time. - /// + /// Formats an instance of as a relative time. /// When. /// The formatted string. public static string FormatRelativeDateTime(this DateTime when) @@ -121,9 +117,7 @@ internal static class NotificationConstants return when.FormatAbsoluteDateTime(); } - /// - /// Formats an instance of as an absolute time. - /// + /// Formats an instance of as an absolute time. /// When. /// The formatted string. public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs index f442ef553..3bf8add07 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs @@ -9,14 +9,10 @@ using ImGuiNET; namespace Dalamud.Interface.ImGuiNotification.Internal; -/// -/// Utilities for implementing stuff under . -/// +/// Utilities for implementing stuff under . internal static class NotificationUtilities { - /// - /// Draws the given texture, or the icon of the plugin if texture is null. - /// + /// Draws the given texture, or the icon of the plugin if texture is null. /// The texture. /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 8f5ec2423..be2b9237d 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -3,9 +3,7 @@ using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; -/// -/// Represents a blueprint for a notification. -/// +/// Represents a blueprint for a notification. public sealed record Notification : INotification { /// From 1ca2d2000ba47d411f532387dc0a22b72351216f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 01:56:06 +0900 Subject: [PATCH 541/585] Add UserDismissable --- .../ImGuiNotification/IActiveNotification.cs | 3 +++ .../ImGuiNotification/INotification.cs | 6 ++++- .../Internal/ActiveNotification.cs | 24 +++++++++++++++---- .../ImGuiNotification/Notification.cs | 3 +++ .../Windows/Data/Widgets/ImGuiWidget.cs | 11 +++++++-- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index fecccf092..cbe5d9e25 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -67,6 +67,9 @@ public interface IActiveNotification : INotification /// new bool Interactible { get; set; } + /// + new bool UserDismissable { get; set; } + /// new TimeSpan HoverExtendDuration { get; set; } diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index c4a7b46ac..92b28fb15 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -38,9 +38,13 @@ public interface INotification /// . /// Note that the close buttons for notifications are always provided and interactible. /// If set to true, then clicking on the notification itself will be interpreted as user-initiated dismissal, - /// unless is set. + /// unless is set or is unset. /// bool Interactible { get; } + + /// Gets a value indicating whether the user can dismiss the notification by themselves. + /// Consider adding a cancel button to . + bool UserDismissable { get; } /// Gets the new duration for this notification if mouse cursor is on the notification window. /// diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 64b812197..246c6cce5 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -148,6 +148,18 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } + /// + public bool UserDismissable + { + get => this.underlyingNotification.UserDismissable; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.UserDismissable = value; + } + } + /// public TimeSpan HoverExtendDuration { @@ -317,7 +329,6 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable if (opacity <= 0) return 0; - var notificationManager = Service.Get(); var interfaceManager = Service.Get(); var unboundedWidth = ImGui.CalcTextSize(this.Content).X; float closeButtonHorizontalSpaceReservation; @@ -386,7 +397,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); - this.DrawNotificationMainWindowContent(notificationManager, width); + this.DrawNotificationMainWindowContent(width); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); var hovered = ImGui.IsWindowHovered(); @@ -433,7 +444,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable { if (this.Click is null) { - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) this.DismissNow(NotificationDismissReason.Manual); } else @@ -546,7 +557,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.MaterializedIcon = null; } - private void DrawNotificationMainWindowContent(NotificationManager notificationManager, float width) + private void DrawNotificationMainWindowContent(float width) { var basePos = ImGui.GetCursorPos(); this.DrawIcon( @@ -706,6 +717,9 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 rt, float pad) { + if (!this.UserDismissable) + return; + using (interfaceManager.IconFontHandle?.Push()) { var str = FontAwesomeIcon.Times.ToIconString(); @@ -719,7 +733,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable ImGui.SetCursorPos(rt - new Vector2(size, 0) - new Vector2(pad)); if (ImGui.Button(str, new(size + (pad * 2)))) - this.DismissNow(); + this.DismissNow(NotificationDismissReason.Manual); ImGui.PopStyleColor(2); if (!this.IsMouseHovered) diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index be2b9237d..e082aaaed 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -24,6 +24,9 @@ public sealed record Notification : INotification /// public bool Interactible { get; set; } + /// + public bool UserDismissable { get; set; } + /// public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 71cba3297..6239c9749 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -120,7 +120,11 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Checkbox("Interactible", ref this.notificationTemplate.Interactible); - ImGui.Checkbox("Action Bar", ref this.notificationTemplate.ActionBar); + ImGui.Checkbox("User Dismissable", ref this.notificationTemplate.UserDismissable); + + ImGui.Checkbox( + "Action Bar (always on if not user dismissable for the example)", + ref this.notificationTemplate.ActionBar); if (ImGui.Button("Add notification")) { @@ -144,6 +148,7 @@ internal class ImGuiWidget : IDataWindowWidget Title = title, Type = type, Interactible = this.notificationTemplate.Interactible, + UserDismissable = this.notificationTemplate.UserDismissable, Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, Progress = this.notificationTemplate.ProgressMode switch { @@ -203,7 +208,7 @@ internal class ImGuiWidget : IDataWindowWidget break; } - if (this.notificationTemplate.ActionBar) + if (this.notificationTemplate.ActionBar || !this.notificationTemplate.UserDismissable) { var nclick = 0; n.Click += _ => nclick++; @@ -326,6 +331,7 @@ internal class ImGuiWidget : IDataWindowWidget public int TypeInt; public int DurationInt; public bool Interactible; + public bool UserDismissable; public bool ActionBar; public int ProgressMode; @@ -342,6 +348,7 @@ internal class ImGuiWidget : IDataWindowWidget this.TypeInt = (int)NotificationType.None; this.DurationInt = 2; this.Interactible = true; + this.UserDismissable = true; this.ActionBar = true; this.ProgressMode = 0; } From df9212ac5854ad804d17c204fe95b38bd4789f2b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 02:28:33 +0900 Subject: [PATCH 542/585] Expose DefaultDisplayDuration --- .../{Internal => }/NotificationConstants.cs | 70 +++++++++---------- .../Windows/Data/Widgets/ImGuiWidget.cs | 1 + 2 files changed, 36 insertions(+), 35 deletions(-) rename Dalamud/Interface/ImGuiNotification/{Internal => }/NotificationConstants.cs (66%) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs similarity index 66% rename from Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs rename to Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 1da979430..62d288836 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -2,10 +2,10 @@ using System.Numerics; using Dalamud.Interface.Utility; -namespace Dalamud.Interface.ImGuiNotification.Internal; +namespace Dalamud.Interface.ImGuiNotification; /// Constants for drawing notification windows. -internal static class NotificationConstants +public static class NotificationConstants { // .............................[..] // ..when.......................[XX] @@ -19,47 +19,47 @@ internal static class NotificationConstants // .. action buttons .. // ................................. - /// The string to show in place of this_plugin if the notification is shown by Dalamud. - public const string DefaultInitiator = "Dalamud"; - - /// The size of the icon. - public const float IconSize = 32; - - /// The background opacity of a notification window. - public const float BackgroundOpacity = 0.82f; - - /// The duration of indeterminate progress bar loop in milliseconds. - public const float IndeterminateProgressbarLoopDuration = 2000f; - - /// Duration of show animation. - public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); - /// Default duration of the notification. public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3); - /// Default duration of the notification. + /// Default duration of the notification, after the mouse cursor leaves the notification window. public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3); - /// Duration of hide animation. - public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + /// The string to show in place of this_plugin if the notification is shown by Dalamud. + internal const string DefaultInitiator = "Dalamud"; + + /// The size of the icon. + internal const float IconSize = 32; + + /// The background opacity of a notification window. + internal const float BackgroundOpacity = 0.82f; + + /// The duration of indeterminate progress bar loop in milliseconds. + internal const float IndeterminateProgressbarLoopDuration = 2000f; + + /// Duration of show animation. + internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of hide animation. - public static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200); + internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + + /// Duration of hide animation. + internal static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200); /// Text color for the when. - public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); + internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the close button [X]. - public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); + internal static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the title. - public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); + internal static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); /// Text color for the name of the initiator. - public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); + internal static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the body. - public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); /// Gets the relative time format strings. private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = @@ -77,32 +77,32 @@ internal static class NotificationConstants }; /// Gets the scaled padding of the window (dot(.) in the above diagram). - public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); + internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); /// Gets the distance from the right bottom border of the viewport /// to the right bottom border of a notification window. /// - public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); + internal static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between two notification windows. - public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); + internal static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between components. - public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); + internal static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); /// Gets the scaled size of the icon. - public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); + internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the height of the expiry progress bar. - public static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); + internal static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); /// Gets the string format of the initiator name field, if the initiator is unloaded. - public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; + internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; /// Formats an instance of as a relative time. /// When. /// The formatted string. - public static string FormatRelativeDateTime(this DateTime when) + internal static string FormatRelativeDateTime(this DateTime when) { var ts = DateTime.Now - when; foreach (var (minSpan, formatString) in RelativeFormatStrings) @@ -120,5 +120,5 @@ internal static class NotificationConstants /// Formats an instance of as an absolute time. /// When. /// The formatted string. - public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; + internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 6239c9749..65418cdbe 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.IconSource; using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; From eaf447164abec712e493fd02b842589eebe2e3c6 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 02:41:45 +0900 Subject: [PATCH 543/585] Ensure that crossthread progress update do not result in animation jerkiness --- .../Internal/ActiveNotification.cs | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 246c6cce5..c81bba7ff 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -35,6 +35,9 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// Used for calculating correct dismissal progressbar animation (right edge). private float prevProgressR; + /// New progress value to be updated on next call to . + private float? newProgress; + /// Initializes a new instance of the class. /// The underlying notification. /// The initiator plugin. Use null if originated by Dalamud. @@ -175,15 +178,13 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public float Progress { - get => this.underlyingNotification.Progress; + get => this.newProgress ?? this.underlyingNotification.Progress; set { if (this.IsDismissed) return; - this.progressBefore = this.ProgressEased; - this.underlyingNotification.Progress = value; - this.progressEasing.Restart(); + this.newProgress = value; } } @@ -207,14 +208,15 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable { get { - if (this.Progress < 0) + var underlyingProgress = this.underlyingNotification.Progress; + if (underlyingProgress < 0) return 0f; - if (Math.Abs(this.Progress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) - return this.Progress; + if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) + return underlyingProgress; var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f); - return this.progressBefore + (state * (this.Progress - this.progressBefore)); + return this.progressBefore + (state * (underlyingProgress - this.progressBefore)); } } @@ -271,7 +273,13 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } /// - public Notification CloneNotification() => this.underlyingNotification with { }; + public Notification CloneNotification() + { + var newValue = this.underlyingNotification with { }; + if (this.newProgress is { } p) + newValue.Progress = p; + return newValue; + } /// public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); @@ -303,6 +311,16 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.showEasing.Update(); this.hideEasing.Update(); this.progressEasing.Update(); + + if (this.newProgress is { } p) + { + this.progressBefore = this.ProgressEased; + this.underlyingNotification.Progress = p; + this.progressEasing.Restart(); + this.progressEasing.Update(); + this.newProgress = null; + } + return this.hideEasing.IsRunning && this.hideEasing.IsDone; } @@ -498,7 +516,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.Expiry = newNotification.Expiry; this.Interactible = newNotification.Interactible; this.HoverExtendDuration = newNotification.HoverExtendDuration; - this.Progress = newNotification.Progress; + this.newProgress = newNotification.Progress; } /// From cf54a0281243b7e126280918bae411f0ee93ab65 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 02:50:30 +0900 Subject: [PATCH 544/585] fixes --- .../ImGuiNotification/IActiveNotification.cs | 12 ++++----- .../ImGuiNotification/INotification.cs | 8 +++--- .../Internal/ActiveNotification.cs | 26 +++++++++---------- .../ImGuiNotification/Notification.cs | 5 ++-- .../Windows/Data/Widgets/ImGuiWidget.cs | 8 +++--- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index cbe5d9e25..0a8f656b9 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -17,7 +17,7 @@ public interface IActiveNotification : INotification /// Invoked upon clicking on the notification. /// - /// This event is not applicable when is set to false. + /// This event is not applicable when is set to false. /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -25,7 +25,7 @@ public interface IActiveNotification : INotification /// Invoked when the mouse enters the notification window. /// - /// This event is applicable regardless of . + /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -33,7 +33,7 @@ public interface IActiveNotification : INotification /// Invoked when the mouse leaves the notification window. /// - /// This event is applicable regardless of . + /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -41,7 +41,7 @@ public interface IActiveNotification : INotification /// Invoked upon drawing the action bar of the notification. /// - /// This event is applicable regardless of . + /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -64,8 +64,8 @@ public interface IActiveNotification : INotification /// new DateTime Expiry { get; set; } - /// - new bool Interactible { get; set; } + /// + new bool Interactable { get; set; } /// new bool UserDismissable { get; set; } diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 92b28fb15..6b47b69f4 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -36,12 +36,12 @@ public interface INotification /// /// Set this value to true if you want to respond to user inputs from /// . - /// Note that the close buttons for notifications are always provided and interactible. + /// Note that the close buttons for notifications are always provided and interactable. /// If set to true, then clicking on the notification itself will be interpreted as user-initiated dismissal, /// unless is set or is unset. /// - bool Interactible { get; } - + bool Interactable { get; } + /// Gets a value indicating whether the user can dismiss the notification by themselves. /// Consider adding a cancel button to . bool UserDismissable { get; } @@ -49,7 +49,7 @@ public interface INotification /// Gets the new duration for this notification if mouse cursor is on the notification window. /// /// If set to or less, then this feature is turned off. - /// This property is applicable regardless of . + /// This property is applicable regardless of . /// TimeSpan HoverExtendDuration { get; } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index c81bba7ff..2e82af6a3 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -139,15 +139,15 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - /// - public bool Interactible + /// + public bool Interactable { - get => this.underlyingNotification.Interactible; + get => this.underlyingNotification.Interactable; set { if (this.IsDismissed) return; - this.underlyingNotification.Interactible = value; + this.underlyingNotification.Interactable = value; } } @@ -407,7 +407,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable $"##NotifyMainWindow{this.Id}", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | - (this.Interactible + (this.Interactable ? ImGuiWindowFlags.None : ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoBringToFrontOnFocus) | ImGuiWindowFlags.NoNav | @@ -514,7 +514,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.Type = newNotification.Type; this.IconSource = newNotification.IconSource; this.Expiry = newNotification.Expiry; - this.Interactible = newNotification.Interactible; + this.Interactable = newNotification.Interactable; this.HoverExtendDuration = newNotification.HoverExtendDuration; this.newProgress = newNotification.Progress; } @@ -538,16 +538,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); - this.underlyingNotification.Interactible = false; + this.Interactable = true; this.IsInitiatorUnloaded = true; + this.UserDismissable = true; + this.HoverExtendDuration = NotificationConstants.DefaultHoverExtendDuration; - var now = DateTime.Now; - var newMaxExpiry = now + NotificationConstants.DefaultDisplayDuration; - if (this.underlyingNotification.Expiry > newMaxExpiry) - { - this.underlyingNotification.Expiry = newMaxExpiry; - this.ExpiryRelativeToTime = now; - } + var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + if (this.Expiry > newMaxExpiry) + this.Expiry = newMaxExpiry; return; diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index e082aaaed..97279d6c1 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,4 +1,3 @@ -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -22,10 +21,10 @@ public sealed record Notification : INotification public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; /// - public bool Interactible { get; set; } + public bool Interactable { get; set; } = true; /// - public bool UserDismissable { get; set; } + public bool UserDismissable { get; set; } = true; /// public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 65418cdbe..74dc8939c 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -119,7 +119,7 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.ProgressModeTitles, NotificationTemplate.ProgressModeTitles.Length); - ImGui.Checkbox("Interactible", ref this.notificationTemplate.Interactible); + ImGui.Checkbox("Interactable", ref this.notificationTemplate.Interactable); ImGui.Checkbox("User Dismissable", ref this.notificationTemplate.UserDismissable); @@ -148,7 +148,7 @@ internal class ImGuiWidget : IDataWindowWidget Content = text, Title = title, Type = type, - Interactible = this.notificationTemplate.Interactible, + Interactable = this.notificationTemplate.Interactable, UserDismissable = this.notificationTemplate.UserDismissable, Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, Progress = this.notificationTemplate.ProgressMode switch @@ -331,7 +331,7 @@ internal class ImGuiWidget : IDataWindowWidget public bool ManualType; public int TypeInt; public int DurationInt; - public bool Interactible; + public bool Interactable; public bool UserDismissable; public bool ActionBar; public int ProgressMode; @@ -348,7 +348,7 @@ internal class ImGuiWidget : IDataWindowWidget this.ManualType = false; this.TypeInt = (int)NotificationType.None; this.DurationInt = 2; - this.Interactible = true; + this.Interactable = true; this.UserDismissable = true; this.ActionBar = true; this.ProgressMode = 0; From 9644dd9922b608a2b180df700158924f6e086431 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 03:31:15 +0900 Subject: [PATCH 545/585] Ensure that TextureWrapTaskIconSource.Materialize do not throw --- .../IconSource/TextureWrapTaskIconSource.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs index 28fdc4d96..2a5473760 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs @@ -6,6 +6,8 @@ using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; +using Serilog; + namespace Dalamud.Interface.ImGuiNotification.IconSource; /// Represents the use of future as the icon of a notification. @@ -41,7 +43,18 @@ public readonly struct TextureWrapTaskIconSource : INotificationIconSource.IInte { private Task? task; - public MaterializedIcon(Func?>? taskFunc) => this.task = taskFunc?.Invoke(); + public MaterializedIcon(Func?>? taskFunc) + { + try + { + this.task = taskFunc?.Invoke(); + } + catch (Exception e) + { + Log.Error(e, $"{nameof(TextureWrapTaskIconSource)}: failed to materialize the icon texture."); + this.task = null; + } + } public void Dispose() { From 42b6f8fd4b5e8a7031922ccd3df56daf996e1783 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 04:16:36 +0900 Subject: [PATCH 546/585] fix disposes and add TextureWrapIconSource --- .../ImGuiNotification/IActiveNotification.cs | 25 ++----- .../ImGuiNotification/INotification.cs | 9 ++- .../IconSource/TextureWrapIconSource.cs | 63 ++++++++++++++++ .../Internal/ActiveNotification.cs | 72 ++++++++----------- .../Internal/NotificationManager.cs | 26 +++++-- .../Internal/NotificationUtilities.cs | 23 +++++- .../ImGuiNotification/Notification.cs | 7 ++ .../Windows/Data/Widgets/ImGuiWidget.cs | 34 ++++++--- Dalamud/Interface/UiBuilder.cs | 1 + .../Plugin/Services/INotificationManager.cs | 8 ++- 10 files changed, 187 insertions(+), 81 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 0a8f656b9..3ae1a76ce 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -56,11 +56,6 @@ public interface IActiveNotification : INotification /// new NotificationType Type { get; set; } - /// Gets or sets the icon source. - /// Setting a new value to this property does not change the icon. Use to do so. - /// - new INotificationIconSource? IconSource { get; set; } - /// new DateTime Expiry { get; set; } @@ -86,25 +81,19 @@ public interface IActiveNotification : INotification /// This includes when the hide animation is being played. bool IsDismissed { get; } - /// Clones this notification as a . - /// A new instance of . - Notification CloneNotification(); - /// Dismisses this notification. void DismissNow(); - /// Updates the notification data. - /// - /// Call to update the icon using the new . - /// If is true, then this function is a no-op. - /// - /// The new notification entry. - void Update(INotification newNotification); - - /// Loads the icon again using . + /// Loads the icon again using the same . /// If is true, then this function is a no-op. void UpdateIcon(); + /// Disposes the previous icon source, take ownership of the new icon source, + /// and calls . + /// Thew new icon source. + /// If is true, then this function is a no-op. + void UpdateIconSource(INotificationIconSource? newIconSource); + /// Generates a new value to use for . /// The new value. internal static long CreateNewId() => Interlocked.Increment(ref idCounter); diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 6b47b69f4..e80ff96c0 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -4,7 +4,7 @@ using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. -public interface INotification +public interface INotification : IDisposable { /// Gets the content body of the notification. string Content { get; } @@ -16,10 +16,15 @@ public interface INotification NotificationType Type { get; } /// Gets the icon source. - /// The following icon sources are currently available.
+ /// + /// The assigned value will be disposed upon the call on this instance of + /// .
+ ///
+ /// The following icon sources are currently available.
///
    ///
  • ///
  • + ///
  • ///
  • ///
  • ///
  • diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs new file mode 100644 index 000000000..b3d4392cf --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs @@ -0,0 +1,63 @@ +using System.Numerics; +using System.Threading; + +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; + +namespace Dalamud.Interface.ImGuiNotification.IconSource; + +/// Represents the use of future as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +public sealed class TextureWrapIconSource : INotificationIconSource.IInternal +{ + private IDalamudTextureWrap? wrap; + + /// Initializes a new instance of the class. + /// The texture wrap to handle over the ownership. + /// + /// If true, this class will own the passed , and you must not call + /// on the passed wrap. + /// If false, this class will create a new reference of the passed wrap, and you should call + /// on the passed wrap. + /// In both cases, this class must be disposed after use. + public TextureWrapIconSource(IDalamudTextureWrap? wrap, bool takeOwnership) => + this.wrap = takeOwnership ? wrap : wrap?.CreateWrapSharingLowLevelResource(); + + /// Gets the underlying texture wrap. + public IDalamudTextureWrap? Wrap => this.wrap; + + /// + public INotificationIconSource Clone() => new TextureWrapIconSource(this.wrap, false); + + /// + public void Dispose() + { + if (Interlocked.Exchange(ref this.wrap, null) is { } w) + w.Dispose(); + } + + /// + INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + new MaterializedIcon(this.wrap?.CreateWrapSharingLowLevelResource()); + + private sealed class MaterializedIcon : INotificationMaterializedIcon + { + private IDalamudTextureWrap? wrap; + + public MaterializedIcon(IDalamudTextureWrap? wrap) => this.wrap = wrap; + + public void Dispose() + { + if (Interlocked.Exchange(ref this.wrap, null) is { } w) + w.Dispose(); + } + + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawTexture( + this.wrap, + minCoord, + maxCoord, + initiatorPlugin); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 2e82af6a3..3f14ec50b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -43,7 +43,10 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// The initiator plugin. Use null if originated by Dalamud. public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) { - this.underlyingNotification = underlyingNotification with { }; + this.underlyingNotification = underlyingNotification with + { + IconSource = underlyingNotification.IconSource?.Clone(), + }; this.InitiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); @@ -51,7 +54,16 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable this.showEasing.Start(); this.progressEasing.Start(); - this.UpdateIcon(); + try + { + this.UpdateIcon(); + } + catch (Exception e) + { + // Ignore the one caused from ctor only; other UpdateIcon calls are from plugins, and they should handle the + // error accordingly. + Log.Error(e, $"{nameof(ActiveNotification)}#{this.Id} ctor: {nameof(this.UpdateIcon)} failed and ignored."); + } } /// @@ -114,17 +126,8 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - /// - public INotificationIconSource? IconSource - { - get => this.underlyingNotification.IconSource; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.IconSource = value; - } - } + /// + public INotificationIconSource? IconSource => this.underlyingNotification.IconSource; /// public DateTime Expiry @@ -264,23 +267,14 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable /// public void Dispose() { - this.ClearIconTask(); - this.underlyingNotification.IconSource = null; + this.ClearMaterializedIcon(); + this.underlyingNotification.Dispose(); this.Dismiss = null; this.Click = null; this.DrawActions = null; this.InitiatorPlugin = null; } - /// - public Notification CloneNotification() - { - var newValue = this.underlyingNotification with { }; - if (this.newProgress is { } p) - newValue.Progress = p; - return newValue; - } - /// public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); @@ -504,30 +498,26 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable return windowSize.Y; } - /// - public void Update(INotification newNotification) - { - if (this.IsDismissed) - return; - this.Content = newNotification.Content; - this.Title = newNotification.Title; - this.Type = newNotification.Type; - this.IconSource = newNotification.IconSource; - this.Expiry = newNotification.Expiry; - this.Interactable = newNotification.Interactable; - this.HoverExtendDuration = newNotification.HoverExtendDuration; - this.newProgress = newNotification.Progress; - } - /// public void UpdateIcon() { if (this.IsDismissed) return; - this.ClearIconTask(); + this.ClearMaterializedIcon(); this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); } + /// + public void UpdateIconSource(INotificationIconSource? newIconSource) + { + if (this.IsDismissed || this.underlyingNotification.IconSource == newIconSource) + return; + + this.underlyingNotification.IconSource?.Dispose(); + this.underlyingNotification.IconSource = newIconSource; + this.UpdateIcon(); + } + /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { @@ -567,7 +557,7 @@ internal sealed class ActiveNotification : IActiveNotification, IDisposable } } - private void ClearIconTask() + private void ClearMaterializedIcon() { this.MaterializedIcon?.Dispose(); this.MaterializedIcon = null; diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index e5a27550c..fdea6146a 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -56,8 +56,9 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos } /// - public IActiveNotification AddNotification(Notification notification) + public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) { + using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, null); this.pendingNotifications.Add(an); return an; @@ -65,10 +66,13 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos /// Adds a notification originating from a plugin. /// The notification. + /// Dispose when this function returns. /// The source plugin. - /// The new notification. - public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin) + /// The added notification. + /// will be honored even on exceptions. + public IActiveNotification AddNotification(Notification notification, bool disposeNotification, LocalPlugin plugin) { + using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, plugin); this.pendingNotifications.Add(an); return an; @@ -88,7 +92,8 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos Content = content, Title = title, Type = type, - }); + }, + true); /// Draw all currently queued notifications. public void Draw() @@ -101,7 +106,14 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); - this.notifications.RemoveAll(x => x.UpdateAnimations()); + this.notifications.RemoveAll(static x => + { + if (!x.UpdateAnimations()) + return false; + + x.Dispose(); + return true; + }); foreach (var tn in this.notifications) height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; } @@ -127,9 +139,9 @@ internal class NotificationManagerPluginScoped : INotificationManager, IServiceT this.localPlugin = localPlugin; /// - public IActiveNotification AddNotification(Notification notification) + public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) { - var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); + var an = this.notificationManagerService.AddNotification(notification, disposeNotification, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); return an; diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs index 3bf8add07..3e24f628c 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs @@ -23,7 +23,22 @@ internal static class NotificationUtilities Vector2 maxCoord, LocalPlugin? initiatorPlugin) { - if (texture is null) + var handle = nint.Zero; + var size = Vector2.Zero; + if (texture is not null) + { + try + { + handle = texture.ImGuiHandle; + size = texture.Size; + } + catch + { + // must have been disposed or something; ignore the texture + } + } + + if (handle == nint.Zero) { var dam = Service.Get(); if (initiatorPlugin is null) @@ -46,14 +61,16 @@ internal static class NotificationUtilities }; } } + + handle = texture.ImGuiHandle; + size = texture.Size; } - var size = texture.Size; if (size.X > maxCoord.X - minCoord.X) size *= (maxCoord.X - minCoord.X) / size.X; if (size.Y > maxCoord.Y - minCoord.Y) size *= (maxCoord.Y - minCoord.Y) / size.Y; ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); - ImGui.Image(texture.ImGuiHandle, size); + ImGui.Image(handle, size); } } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 97279d6c1..3b452bd2d 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -31,4 +31,11 @@ public sealed record Notification : INotification /// public float Progress { get; set; } = 1f; + + /// + public void Dispose() + { + this.IconSource?.Dispose(); + this.IconSource = null; + } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 74dc8939c..1093ca4b8 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -79,27 +79,26 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.IconSourceTitles.Length); switch (this.notificationTemplate.IconSourceInt) { - case 1: - case 2: + case 1 or 2: ImGui.InputText( "Icon Text##iconSourceText", ref this.notificationTemplate.IconSourceText, 255); break; - case 3: + case 3 or 4: ImGui.Combo( "Icon Source##iconSourceAssetCombo", ref this.notificationTemplate.IconSourceAssetInt, NotificationTemplate.AssetSources, NotificationTemplate.AssetSources.Length); break; - case 4: + case 5 or 7: ImGui.InputText( "Game Path##iconSourceText", ref this.notificationTemplate.IconSourceText, 255); break; - case 5: + case 6 or 8: ImGui.InputText( "File Path##iconSourceText", ref this.notificationTemplate.IconSourceText, @@ -170,17 +169,31 @@ internal class ImGuiWidget : IDataWindowWidget (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 3 => new TextureWrapTaskIconSource( + 3 => new TextureWrapIconSource( + Service.Get().GetDalamudTextureWrap( + Enum.Parse( + NotificationTemplate.AssetSources[ + this.notificationTemplate.IconSourceAssetInt])), + false), + 4 => new TextureWrapTaskIconSource( () => Service.Get().GetDalamudTextureWrapAsync( Enum.Parse( NotificationTemplate.AssetSources[ this.notificationTemplate.IconSourceAssetInt]))), - 4 => new GamePathIconSource(this.notificationTemplate.IconSourceText), - 5 => new FilePathIconSource(this.notificationTemplate.IconSourceText), + 5 => new GamePathIconSource(this.notificationTemplate.IconSourceText), + 6 => new FilePathIconSource(this.notificationTemplate.IconSourceText), + 7 => new TextureWrapIconSource( + Service.Get().GetTextureFromGame(this.notificationTemplate.IconSourceText), + false), + 8 => new TextureWrapIconSource( + Service.Get().GetTextureFromFile( + new(this.notificationTemplate.IconSourceText)), + false), _ => null, }, - }); + }, + true); switch (this.notificationTemplate.ProgressMode) { case 2: @@ -276,9 +289,12 @@ internal class ImGuiWidget : IDataWindowWidget "None (use Type)", "SeIconChar", "FontAwesomeIcon", + "TextureWrap from DalamudAssets", "TextureWrapTask from DalamudAssets", "GamePath", "FilePath", + "TextureWrap from GamePath", + "TextureWrap from FilePath", }; public static readonly string[] AssetSources = diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 64ff0cc45..1237c9c1f 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -581,6 +581,7 @@ public sealed class UiBuilder : IDisposable Type = type, Expiry = DateTime.Now + TimeSpan.FromMilliseconds(msDelay), }, + true, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); diff --git a/Dalamud/Plugin/Services/INotificationManager.cs b/Dalamud/Plugin/Services/INotificationManager.cs index 1d31ddd35..441cc31f7 100644 --- a/Dalamud/Plugin/Services/INotificationManager.cs +++ b/Dalamud/Plugin/Services/INotificationManager.cs @@ -11,6 +11,12 @@ public interface INotificationManager /// Adds a notification. /// /// The new notification. + /// + /// Dispose when this function returns, even if the function throws an exception. + /// Set to false to reuse for multiple calls to this function, in which case, + /// you should call on the value supplied to at a + /// later time. + /// /// The added notification. - IActiveNotification AddNotification(Notification notification); + IActiveNotification AddNotification(Notification notification, bool disposeNotification = true); } From f4349461375d3186144f10974fa7bf0885209c3b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 11:54:24 +0900 Subject: [PATCH 547/585] Turn impls of IconSource internal --- .../ImGuiNotification/INotification.cs | 2 +- .../INotificationIconSource.cs | 68 +++++++++++++++++++ .../Internal/ActiveNotification.cs | 4 +- .../IconSource/FilePathIconSource.cs | 17 +++-- .../IconSource/FontAwesomeIconIconSource.cs | 19 +++--- .../IconSource/GamePathIconSource.cs | 17 +++-- .../IconSource/SeIconCharIconSource.cs | 19 +++--- .../IconSource/TextureWrapIconSource.cs | 7 +- .../IconSource/TextureWrapTaskIconSource.cs | 21 +++--- .../{Internal => }/NotificationUtilities.cs | 29 +++++++- .../Windows/Data/Widgets/ImGuiWidget.cs | 18 ++--- 11 files changed, 153 insertions(+), 68 deletions(-) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/FilePathIconSource.cs (74%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/FontAwesomeIconIconSource.cs (73%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/GamePathIconSource.cs (76%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/SeIconCharIconSource.cs (70%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/TextureWrapIconSource.cs (89%) rename Dalamud/Interface/ImGuiNotification/{ => Internal}/IconSource/TextureWrapTaskIconSource.cs (80%) rename Dalamud/Interface/ImGuiNotification/{Internal => }/NotificationUtilities.cs (65%) diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index e80ff96c0..d8ac95c22 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,4 +1,4 @@ -using Dalamud.Interface.ImGuiNotification.IconSource; +using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs index 8a73e2a64..1fee67098 100644 --- a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs @@ -1,3 +1,10 @@ +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.Internal.IconSource; +using Dalamud.Interface.Internal; + namespace Dalamud.Interface.ImGuiNotification; /// Icon source for . @@ -12,6 +19,67 @@ public interface INotificationIconSource : ICloneable, IDisposable INotificationMaterializedIcon Materialize(); } + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource From(SeIconChar iconChar) => new SeIconCharIconSource(iconChar); + + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource From(FontAwesomeIcon iconChar) => new FontAwesomeIconIconSource(iconChar); + + /// Gets a new instance of that will source the icon from an + /// . + /// The texture wrap. + /// + /// If true, this class will own the passed , and you must not call + /// on the passed wrap. + /// If false, this class will create a new reference of the passed wrap, and you should call + /// on the passed wrap. + /// In both cases, the returned object must be disposed after use. + /// A new instance of that should be disposed after use. + /// If any errors are thrown or is null, the default icon will be displayed + /// instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource From(IDalamudTextureWrap? wrap, bool takeOwnership = true) => + new TextureWrapIconSource(wrap, takeOwnership); + + /// Gets a new instance of that will source the icon from an + /// returning a resulting in an + /// . + /// The function that returns a task that results a texture wrap. + /// A new instance of that should be disposed after use. + /// If any errors are thrown or is null, the default icon will be + /// displayed instead.
    + /// Use if you will have a wrap available without waiting.
    + /// should not contain a reference to a resource; if it does, the resource will be + /// released when all instances of derived from the returned object are freed + /// by the garbage collector, which will result in non-deterministic resource releases.
    + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource From(Func?>? wrapTaskFunc) => + new TextureWrapTaskIconSource(wrapTaskFunc); + + /// Gets a new instance of that will source the icon from a texture + /// file shipped as a part of the game resources. + /// The path to a texture file in the game virtual file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource FromGame(string gamePath) => new GamePathIconSource(gamePath); + + /// Gets a new instance of that will source the icon from an image + /// file from the file system. + /// The path to an image file in the file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource FromFile(string filePath) => new FilePathIconSource(filePath); + /// new INotificationIconSource Clone(); diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 3f14ec50b..53262c08e 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -4,7 +4,7 @@ using System.Runtime.Loader; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiNotification.IconSource; +using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; @@ -18,7 +18,7 @@ using Serilog; namespace Dalamud.Interface.ImGuiNotification.Internal; /// Represents an active notification. -internal sealed class ActiveNotification : IActiveNotification, IDisposable +internal sealed class ActiveNotification : IActiveNotification { private readonly Notification underlyingNotification; diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs similarity index 74% rename from Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs index b1886154a..a741931a5 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/FilePathIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs @@ -1,33 +1,32 @@ using System.IO; using System.Numerics; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of a texture from a file as the icon of a notification. /// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -public readonly struct FilePathIconSource : INotificationIconSource.IInternal +internal class FilePathIconSource : INotificationIconSource.IInternal { - /// The path to a .tex file inside the game resources. - public readonly string FilePath; - - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// The path to a .tex file inside the game resources. public FilePathIconSource(string filePath) => this.FilePath = filePath; + /// Gets the path to a .tex file inside the game resources. + public string FilePath { get; } + /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.FilePath); private sealed class MaterializedIcon : INotificationMaterializedIcon diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs similarity index 73% rename from Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs index 8e28940ba..86a6f835c 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/FontAwesomeIconIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs @@ -1,32 +1,31 @@ using System.Numerics; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Plugin.Internal.Types; using ImGuiNET; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. -public readonly struct FontAwesomeIconIconSource : INotificationIconSource.IInternal +internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal { - /// The icon character. - public readonly FontAwesomeIcon Char; + /// Initializes a new instance of the class. + /// The character. + public FontAwesomeIconIconSource(FontAwesomeIcon iconChar) => this.IconChar = iconChar; - /// Initializes a new instance of the struct. - /// The character. - public FontAwesomeIconIconSource(FontAwesomeIcon c) => this.Char = c; + /// Gets the icon character. + public FontAwesomeIcon IconChar { get; } /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => new MaterializedIcon(this.Char); + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); /// Draws the icon. /// The icon string. diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs similarity index 76% rename from Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs index 9b669e62a..974e60ee7 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/GamePathIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs @@ -1,34 +1,33 @@ using System.Numerics; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of a game-shipped texture as the icon of a notification. /// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -public readonly struct GamePathIconSource : INotificationIconSource.IInternal +internal class GamePathIconSource : INotificationIconSource.IInternal { - /// The path to a .tex file inside the game resources. - public readonly string GamePath; - - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// The path to a .tex file inside the game resources. /// Use to get the game path from icon IDs. public GamePathIconSource(string gamePath) => this.GamePath = gamePath; + /// Gets the path to a .tex file inside the game resources. + public string GamePath { get; } + /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.GamePath); private sealed class MaterializedIcon : INotificationMaterializedIcon diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs similarity index 70% rename from Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs index d34b776bc..83fd0bef6 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/SeIconCharIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs @@ -1,33 +1,32 @@ using System.Numerics; using Dalamud.Game.Text; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Plugin.Internal.Types; using ImGuiNET; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. -public readonly struct SeIconCharIconSource : INotificationIconSource.IInternal +internal class SeIconCharIconSource : INotificationIconSource.IInternal { - /// The icon character. - public readonly SeIconChar Char; - - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// The character. - public SeIconCharIconSource(SeIconChar c) => this.Char = c; + public SeIconCharIconSource(SeIconChar c) => this.IconChar = c; + + /// Gets the icon character. + public SeIconChar IconChar { get; } /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => new MaterializedIcon(this.Char); + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); private sealed class MaterializedIcon : INotificationMaterializedIcon { diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs similarity index 89% rename from Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs index b3d4392cf..a10b09bce 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs @@ -1,15 +1,14 @@ using System.Numerics; using System.Threading; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of future as the icon of a notification. /// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -public sealed class TextureWrapIconSource : INotificationIconSource.IInternal +internal class TextureWrapIconSource : INotificationIconSource.IInternal { private IDalamudTextureWrap? wrap; @@ -38,7 +37,7 @@ public sealed class TextureWrapIconSource : INotificationIconSource.IInternal } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.wrap?.CreateWrapSharingLowLevelResource()); private sealed class MaterializedIcon : INotificationMaterializedIcon diff --git a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs similarity index 80% rename from Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs rename to Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs index 2a5473760..4039b6955 100644 --- a/Dalamud/Interface/ImGuiNotification/IconSource/TextureWrapTaskIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs @@ -1,42 +1,41 @@ using System.Numerics; using System.Threading.Tasks; -using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Serilog; -namespace Dalamud.Interface.ImGuiNotification.IconSource; +namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of future as the icon of a notification. /// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -public readonly struct TextureWrapTaskIconSource : INotificationIconSource.IInternal +internal class TextureWrapTaskIconSource : INotificationIconSource.IInternal { - /// The function that returns a task resulting in a new instance of . - /// - /// Dalamud will take ownership of the result. Do not call . - public readonly Func?>? TextureWrapTaskFunc; - /// Gets the default materialized icon, for the purpose of displaying the plugin icon. internal static readonly INotificationMaterializedIcon DefaultMaterializedIcon = new MaterializedIcon(null); - /// Initializes a new instance of the struct. + /// Initializes a new instance of the class. /// The function. public TextureWrapTaskIconSource(Func?>? taskFunc) => this.TextureWrapTaskFunc = taskFunc; + /// Gets the function that returns a task resulting in a new instance of . + /// + /// Dalamud will take ownership of the result. Do not call . + public Func?>? TextureWrapTaskFunc { get; } + /// public INotificationIconSource Clone() => this; /// - void IDisposable.Dispose() + public void Dispose() { } /// - INotificationMaterializedIcon INotificationIconSource.IInternal.Materialize() => + public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.TextureWrapTaskFunc); private sealed class MaterializedIcon : INotificationMaterializedIcon diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs similarity index 65% rename from Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs rename to Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 3e24f628c..9b3602b68 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -1,5 +1,8 @@ +using System.IO; using System.Numerics; +using System.Runtime.CompilerServices; +using Dalamud.Game.Text; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows; using Dalamud.Plugin.Internal.Types; @@ -7,17 +10,37 @@ using Dalamud.Storage.Assets; using ImGuiNET; -namespace Dalamud.Interface.ImGuiNotification.Internal; +namespace Dalamud.Interface.ImGuiNotification; /// Utilities for implementing stuff under . -internal static class NotificationUtilities +public static class NotificationUtilities { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource ToIconSource(this SeIconChar iconChar) => + INotificationIconSource.From(iconChar); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) => + INotificationIconSource.From(iconChar); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource ToIconSource(this IDalamudTextureWrap? wrap, bool takeOwnership = true) => + INotificationIconSource.From(wrap, takeOwnership); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIconSource ToIconSource(this FileInfo fileInfo) => + INotificationIconSource.FromFile(fileInfo.FullName); + /// Draws the given texture, or the icon of the plugin if texture is null. /// The texture. /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. /// The initiator plugin. - public static void DrawTexture( + internal static void DrawTexture( IDalamudTextureWrap? texture, Vector2 minCoord, Vector2 maxCoord, diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 1093ca4b8..3b518af84 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -3,8 +3,8 @@ using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.ImGuiNotification; -using Dalamud.Interface.ImGuiNotification.IconSource; using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using Dalamud.Storage.Assets; @@ -161,32 +161,32 @@ internal class ImGuiWidget : IDataWindowWidget }, IconSource = this.notificationTemplate.IconSourceInt switch { - 1 => new SeIconCharIconSource( + 1 => INotificationIconSource.From( (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 2 => new FontAwesomeIconIconSource( + 2 => INotificationIconSource.From( (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 3 => new TextureWrapIconSource( + 3 => INotificationIconSource.From( Service.Get().GetDalamudTextureWrap( Enum.Parse( NotificationTemplate.AssetSources[ this.notificationTemplate.IconSourceAssetInt])), false), - 4 => new TextureWrapTaskIconSource( + 4 => INotificationIconSource.From( () => Service.Get().GetDalamudTextureWrapAsync( Enum.Parse( NotificationTemplate.AssetSources[ this.notificationTemplate.IconSourceAssetInt]))), - 5 => new GamePathIconSource(this.notificationTemplate.IconSourceText), - 6 => new FilePathIconSource(this.notificationTemplate.IconSourceText), - 7 => new TextureWrapIconSource( + 5 => INotificationIconSource.FromGame(this.notificationTemplate.IconSourceText), + 6 => INotificationIconSource.FromFile(this.notificationTemplate.IconSourceText), + 7 => INotificationIconSource.From( Service.Get().GetTextureFromGame(this.notificationTemplate.IconSourceText), false), - 8 => new TextureWrapIconSource( + 8 => INotificationIconSource.From( Service.Get().GetTextureFromFile( new(this.notificationTemplate.IconSourceText)), false), From e96089f8b20963b2934837aece761879bf77b43b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 13:04:17 +0900 Subject: [PATCH 548/585] Separate progress and expiry animations --- .../ImGuiNotification/IActiveNotification.cs | 3 + .../ImGuiNotification/INotification.cs | 9 +- .../Internal/ActiveNotification.cs | 119 +++++++++++++----- .../ImGuiNotification/Notification.cs | 3 + .../NotificationConstants.cs | 22 +++- .../Windows/Data/Widgets/ImGuiWidget.cs | 5 + 6 files changed, 122 insertions(+), 39 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 3ae1a76ce..e6355cd90 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -59,6 +59,9 @@ public interface IActiveNotification : INotification /// new DateTime Expiry { get; set; } + /// + new bool ShowIndeterminateIfNoExpiry { get; set; } + /// new bool Interactable { get; set; } diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index d8ac95c22..9d6167a95 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -37,6 +37,10 @@ public interface INotification : IDisposable /// (sticky, indeterminate, permanent, or persistent). DateTime Expiry { get; } + /// Gets a value indicating whether to show an indeterminate expiration animation if + /// is set to . + bool ShowIndeterminateIfNoExpiry { get; } + /// Gets a value indicating whether this notification may be interacted. /// /// Set this value to true if you want to respond to user inputs from @@ -58,8 +62,7 @@ public interface INotification : IDisposable /// TimeSpan HoverExtendDuration { get; } - /// Gets the progress for the progress bar of the notification. - /// The progress should either be in the range between 0 and 1 or be a negative value. - /// Specifying a negative value will show an indeterminate progress bar. + /// Gets the progress for the background progress bar of the notification. + /// The progress should be in the range between 0 and 1. float Progress { get; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 53262c08e..a71c35c49 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -50,7 +50,7 @@ internal sealed class ActiveNotification : IActiveNotification this.InitiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); - this.progressEasing = new InOutCubic(NotificationConstants.ProgressAnimationDuration); + this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration); this.showEasing.Start(); this.progressEasing.Start(); @@ -142,6 +142,18 @@ internal sealed class ActiveNotification : IActiveNotification } } + /// + public bool ShowIndeterminateIfNoExpiry + { + get => this.underlyingNotification.ShowIndeterminateIfNoExpiry; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.ShowIndeterminateIfNoExpiry = value; + } + } + /// public bool Interactable { @@ -212,9 +224,6 @@ internal sealed class ActiveNotification : IActiveNotification get { var underlyingProgress = this.underlyingNotification.Progress; - if (underlyingProgress < 0) - return 0f; - if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) return underlyingProgress; @@ -409,6 +418,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); + this.DrawWindowBackgroundProgressBar(); this.DrawNotificationMainWindowContent(width); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); @@ -440,6 +450,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); + this.DrawWindowBackgroundProgressBar(); this.DrawNotificationActionWindowContent(interfaceManager, width); windowSize.Y += actionWindowHeight; windowPos.Y -= actionWindowHeight; @@ -517,7 +528,7 @@ internal sealed class ActiveNotification : IActiveNotification this.underlyingNotification.IconSource = newIconSource; this.UpdateIcon(); } - + /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { @@ -563,6 +574,49 @@ internal sealed class ActiveNotification : IActiveNotification this.MaterializedIcon = null; } + private void DrawWindowBackgroundProgressBar() + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.ProgressWaveLoopDuration) / + NotificationConstants.ProgressWaveLoopDuration); + elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; + + var colorElapsed = + elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / + NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; + + elapsed = Math.Clamp(elapsed, 0f, 1f); + colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); + colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); + + var progress = Math.Clamp(this.ProgressEased, 0f, 1f); + if (progress >= 1f) + elapsed = colorElapsed = 0f; + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var rb = windowPos + windowSize; + var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed }; + var rp = windowPos + windowSize with { X = windowSize.X * progress }; + + ImGui.PushClipRect(windowPos, rb, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos, + midp, + ImGui.GetColorU32( + Vector4.Lerp( + NotificationConstants.BackgroundProgressColorMin, + NotificationConstants.BackgroundProgressColorMax, + colorElapsed))); + ImGui.GetWindowDrawList().AddRectFilled( + midp with { Y = 0 }, + rp, + ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin)); + ImGui.PopClipRect(); + } + private void DrawNotificationMainWindowContent(float width) { var basePos = ImGui.GetCursorPos(); @@ -580,62 +634,61 @@ internal sealed class ActiveNotification : IActiveNotification // Top padding is zero, as the action window will add the padding. ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding)); - float progressL, progressR; + float barL, barR; if (this.IsDismissed) { var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; var length = (this.prevProgressR - this.prevProgressL) / 2f; - progressL = midpoint - (length * v); - progressR = midpoint + (length * v); + barL = midpoint - (length * v); + barR = midpoint + (length * v); } else if (this.Expiry == DateTime.MaxValue) { - if (this.Progress >= 0) - { - progressL = 0f; - progressR = this.ProgressEased; - } - else + if (this.ShowIndeterminateIfNoExpiry) { var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % NotificationConstants.IndeterminateProgressbarLoopDuration) / NotificationConstants.IndeterminateProgressbarLoopDuration); - progressL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); - progressR = Math.Min(elapsed, 2f / 3) / (2f / 3); - progressL = MathF.Pow(progressL, 3); - progressR = 1f - MathF.Pow(1f - progressR, 3); + barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); + barR = Math.Min(elapsed, 2f / 3) / (2f / 3); + barL = MathF.Pow(barL, 3); + barR = 1f - MathF.Pow(1f - barR, 3); + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else + { + this.prevProgressL = barL = 0f; + this.prevProgressR = barR = 1f; } - - this.prevProgressL = progressL; - this.prevProgressR = progressR; } else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) { - progressL = 0f; - progressR = 1f; - this.prevProgressL = progressL; - this.prevProgressR = progressR; + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; } else { - progressL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / - (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); - progressR = 1f; - this.prevProgressL = progressL; - this.prevProgressR = progressR; + barL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / + (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; } - progressR = Math.Clamp(progressR, 0f, 1f); + barR = Math.Clamp(barR, 0f, 1f); var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); ImGui.PushClipRect(windowPos, windowPos + windowSize, false); ImGui.GetWindowDrawList().AddRectFilled( windowPos + new Vector2( - windowSize.X * progressL, + windowSize.X * barL, windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), - windowPos + windowSize with { X = windowSize.X * progressR }, + windowPos + windowSize with { X = windowSize.X * barR }, ImGui.GetColorU32(this.DefaultIconColor)); ImGui.PopClipRect(); } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 3b452bd2d..9c89dc305 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -20,6 +20,9 @@ public sealed record Notification : INotification /// public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + /// + public bool ShowIndeterminateIfNoExpiry { get; set; } = true; + /// public bool Interactable { get; set; } = true; diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 62d288836..800531f39 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -37,14 +37,24 @@ public static class NotificationConstants /// The duration of indeterminate progress bar loop in milliseconds. internal const float IndeterminateProgressbarLoopDuration = 2000f; + /// The duration of the progress wave animation in milliseconds. + internal const float ProgressWaveLoopDuration = 2000f; + + /// The time ratio of a progress wave loop where the animation is idle. + internal const float ProgressWaveIdleTimeRatio = 0.5f; + + /// The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque. + /// + internal const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + /// Duration of show animation. internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of hide animation. internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); - /// Duration of hide animation. - internal static readonly TimeSpan ProgressAnimationDuration = TimeSpan.FromMilliseconds(200); + /// Duration of progress change animation. + internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); /// Text color for the when. internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -61,6 +71,12 @@ public static class NotificationConstants /// Text color for the body. internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + /// Color for the background progress bar (determinate progress only). + internal static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); + + /// Color for the background progress bar (determinate progress only). + internal static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); + /// Gets the relative time format strings. private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = { @@ -94,7 +110,7 @@ public static class NotificationConstants internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the height of the expiry progress bar. - internal static float ScaledExpiryProgressBarHeight => MathF.Round(2 * ImGuiHelpers.GlobalScale); + internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); /// Gets the string format of the initiator name field, if the initiator is unloaded. internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 3b518af84..ae3f16576 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -120,6 +120,8 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Checkbox("Interactable", ref this.notificationTemplate.Interactable); + ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry); + ImGui.Checkbox("User Dismissable", ref this.notificationTemplate.UserDismissable); ImGui.Checkbox( @@ -147,6 +149,7 @@ internal class ImGuiWidget : IDataWindowWidget Content = text, Title = title, Type = type, + ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry, Interactable = this.notificationTemplate.Interactable, UserDismissable = this.notificationTemplate.UserDismissable, Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, @@ -347,6 +350,7 @@ internal class ImGuiWidget : IDataWindowWidget public bool ManualType; public int TypeInt; public int DurationInt; + public bool ShowIndeterminateIfNoExpiry; public bool Interactable; public bool UserDismissable; public bool ActionBar; @@ -364,6 +368,7 @@ internal class ImGuiWidget : IDataWindowWidget this.ManualType = false; this.TypeInt = (int)NotificationType.None; this.DurationInt = 2; + this.ShowIndeterminateIfNoExpiry = true; this.Interactable = true; this.UserDismissable = true; this.ActionBar = true; From 0040f611253add1cd7aabaffd26f12fbb95bb056 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 19:51:00 +0900 Subject: [PATCH 549/585] Make notifications minimizable, remove interactable --- .../ImGuiNotification/IActiveNotification.cs | 48 +- .../ImGuiNotification/INotification.cs | 101 +-- .../Internal/ActiveNotification.cs | 685 +++++++++++------- .../IconSource/FontAwesomeIconIconSource.cs | 31 +- .../IconSource/SeIconCharIconSource.cs | 25 +- .../Internal/NotificationManager.cs | 15 +- .../ImGuiNotification/Notification.cs | 64 +- .../NotificationConstants.cs | 31 + .../NotificationDismissReason.cs | 2 +- .../NotificationUtilities.cs | 42 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 66 +- Dalamud/Interface/UiBuilder.cs | 2 +- 12 files changed, 688 insertions(+), 424 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index e6355cd90..504c6d6d5 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,7 +1,5 @@ using System.Threading; -using Dalamud.Interface.Internal.Notifications; - namespace Dalamud.Interface.ImGuiNotification; /// Represents an active notification. @@ -17,7 +15,6 @@ public interface IActiveNotification : INotification /// Invoked upon clicking on the notification. /// - /// This event is not applicable when is set to false. /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -25,7 +22,6 @@ public interface IActiveNotification : INotification /// Invoked when the mouse enters the notification window. /// - /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -33,7 +29,6 @@ public interface IActiveNotification : INotification /// Invoked when the mouse leaves the notification window. /// - /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// @@ -41,42 +36,18 @@ public interface IActiveNotification : INotification /// Invoked upon drawing the action bar of the notification. /// - /// This event is applicable regardless of . /// Note that this function may be called even after has been invoked. /// Refer to . /// event Action DrawActions; - /// - new string Content { get; set; } - - /// - new string? Title { get; set; } - - /// - new NotificationType Type { get; set; } - - /// - new DateTime Expiry { get; set; } - - /// - new bool ShowIndeterminateIfNoExpiry { get; set; } - - /// - new bool Interactable { get; set; } - - /// - new bool UserDismissable { get; set; } - - /// - new TimeSpan HoverExtendDuration { get; set; } - - /// - new float Progress { get; set; } - /// Gets the ID of this notification. long Id { get; } + /// Gets the effective expiry time. + /// Contains if the notification does not expire. + DateTime EffectiveExpiry { get; } + /// Gets a value indicating whether the mouse cursor is on the notification window. bool IsMouseHovered { get; } @@ -87,16 +58,15 @@ public interface IActiveNotification : INotification /// Dismisses this notification. void DismissNow(); + /// Extends this notifiation. + /// The extension time. + /// This does not override . + void ExtendBy(TimeSpan extension); + /// Loads the icon again using the same . /// If is true, then this function is a no-op. void UpdateIcon(); - /// Disposes the previous icon source, take ownership of the new icon source, - /// and calls . - /// Thew new icon source. - /// If is true, then this function is a no-op. - void UpdateIconSource(INotificationIconSource? newIconSource); - /// Generates a new value to use for . /// The new value. internal static long CreateNewId() => Interlocked.Increment(ref idCounter); diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 9d6167a95..8f5a30e79 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,4 +1,3 @@ -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -6,63 +5,69 @@ namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. public interface INotification : IDisposable { - /// Gets the content body of the notification. - string Content { get; } + /// Gets or sets the content body of the notification. + string Content { get; set; } - /// Gets the title of the notification. - string? Title { get; } + /// Gets or sets the title of the notification. + string? Title { get; set; } - /// Gets the type of the notification. - NotificationType Type { get; } + /// Gets or sets the text to display when the notification is minimized. + string? MinimizedText { get; set; } - /// Gets the icon source. + /// Gets or sets the type of the notification. + NotificationType Type { get; set; } + + /// Gets or sets the icon source. /// - /// The assigned value will be disposed upon the call on this instance of - /// .
    - ///
    - /// The following icon sources are currently available.
    - ///
      - ///
    • - ///
    • - ///
    • - ///
    • - ///
    • - ///
    • - ///
    + /// Assigning a new value that does not equal to the previous value will dispose the old value. The ownership + /// of the new value is transferred to this . Even if the assignment throws an + /// exception, the ownership is transferred, causing the value to be disposed. Assignment should not throw an + /// exception though, so wrapping the assignment in try...catch block is not required. + /// The assigned value will be disposed upon the call on this instance of + /// , unless the same value is assigned, in which case it will do nothing. + /// If this is an , then updating this property + /// will change the icon being displayed (calls ), unless + /// is true. ///
    - INotificationIconSource? IconSource { get; } + INotificationIconSource? IconSource { get; set; } - /// Gets the expiry. - /// Set to to make the notification not have an expiry time - /// (sticky, indeterminate, permanent, or persistent). - DateTime Expiry { get; } - - /// Gets a value indicating whether to show an indeterminate expiration animation if - /// is set to . - bool ShowIndeterminateIfNoExpiry { get; } - - /// Gets a value indicating whether this notification may be interacted. + /// Gets or sets the hard expiry. /// - /// Set this value to true if you want to respond to user inputs from - /// . - /// Note that the close buttons for notifications are always provided and interactable. - /// If set to true, then clicking on the notification itself will be interpreted as user-initiated dismissal, - /// unless is set or is unset. + /// Setting this value will override and , in that + /// the notification will be dismissed when this expiry expires.
    + /// Set to to make only take effect.
    + /// If neither nor is not MaxValue, then the notification + /// will not expire after a set time. It must be explicitly dismissed by the user of via calling + /// .
    + /// Updating this value will reset the dismiss timer. ///
    - bool Interactable { get; } + DateTime HardExpiry { get; set; } - /// Gets a value indicating whether the user can dismiss the notification by themselves. + /// Gets or sets the initial duration. + /// Set to to make only take effect. + /// Updating this value will reset the dismiss timer. + TimeSpan InitialDuration { get; set; } + + /// Gets or sets the new duration for this notification once the mouse cursor leaves the window. + /// + /// If set to or less, then this feature is turned off, and hovering the mouse on the + /// notification will not make the notification stay.
    + /// Updating this value will reset the dismiss timer. + ///
    + TimeSpan HoverExtendDuration { get; set; } + + /// Gets or sets a value indicating whether to show an indeterminate expiration animation if + /// is set to . + bool ShowIndeterminateIfNoExpiry { get; set; } + + /// Gets or sets a value indicating whether the notification has been minimized. + bool Minimized { get; set; } + + /// Gets or sets a value indicating whether the user can dismiss the notification by themselves. /// Consider adding a cancel button to . - bool UserDismissable { get; } + bool UserDismissable { get; set; } - /// Gets the new duration for this notification if mouse cursor is on the notification window. - /// - /// If set to or less, then this feature is turned off. - /// This property is applicable regardless of . - /// - TimeSpan HoverExtendDuration { get; } - - /// Gets the progress for the background progress bar of the notification. + /// Gets or sets the progress for the background progress bar of the notification. /// The progress should be in the range between 0 and 1. - float Progress { get; } + float Progress { get; set; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index a71c35c49..a89ebeb0b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -25,6 +25,7 @@ internal sealed class ActiveNotification : IActiveNotification private readonly Easing showEasing; private readonly Easing hideEasing; private readonly Easing progressEasing; + private readonly Easing expandoEasing; /// The progress before for the progress bar animation with . private float progressBefore; @@ -38,6 +39,9 @@ internal sealed class ActiveNotification : IActiveNotification /// New progress value to be updated on next call to . private float? newProgress; + /// New minimized value to be updated on next call to . + private bool? newMinimized; + /// Initializes a new instance of the class. /// The underlying notification. /// The initiator plugin. Use null if originated by Dalamud. @@ -51,6 +55,7 @@ internal sealed class ActiveNotification : IActiveNotification this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration); + this.expandoEasing = new InOutCubic(NotificationConstants.ExpandoAnimationDuration); this.showEasing.Start(); this.progressEasing.Start(); @@ -88,9 +93,12 @@ internal sealed class ActiveNotification : IActiveNotification public DateTime CreatedAt { get; } = DateTime.Now; /// Gets the time of starting to count the timer for the expiration. - public DateTime ExpiryRelativeToTime { get; private set; } = DateTime.Now; + public DateTime HoverRelativeToTime { get; private set; } = DateTime.Now; - /// + /// Gets the extended expiration time from . + public DateTime ExtendedExpiry { get; private set; } = DateTime.Now; + + /// public string Content { get => this.underlyingNotification.Content; @@ -102,7 +110,7 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// + /// public string? Title { get => this.underlyingNotification.Title; @@ -114,7 +122,19 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// + /// + public string? MinimizedText + { + get => this.underlyingNotification.MinimizedText; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.MinimizedText = value; + } + } + + /// public NotificationType Type { get => this.underlyingNotification.Type; @@ -127,22 +147,98 @@ internal sealed class ActiveNotification : IActiveNotification } /// - public INotificationIconSource? IconSource => this.underlyingNotification.IconSource; - - /// - public DateTime Expiry + public INotificationIconSource? IconSource { - get => this.underlyingNotification.Expiry; + get => this.underlyingNotification.IconSource; set { - if (this.underlyingNotification.Expiry == value || this.IsDismissed) + if (this.IsDismissed) + { + value?.Dispose(); return; - this.underlyingNotification.Expiry = value; - this.ExpiryRelativeToTime = DateTime.Now; + } + + this.underlyingNotification.IconSource = value; + this.UpdateIcon(); } } - /// + /// + public DateTime HardExpiry + { + get => this.underlyingNotification.HardExpiry; + set + { + if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) + return; + this.underlyingNotification.HardExpiry = value; + this.HoverRelativeToTime = DateTime.Now; + } + } + + /// + public TimeSpan InitialDuration + { + get => this.underlyingNotification.InitialDuration; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.InitialDuration = value; + this.HoverRelativeToTime = DateTime.Now; + } + } + + /// + public TimeSpan HoverExtendDuration + { + get => this.underlyingNotification.HoverExtendDuration; + set + { + if (this.IsDismissed) + return; + this.underlyingNotification.HoverExtendDuration = value; + this.HoverRelativeToTime = DateTime.Now; + } + } + + /// + public DateTime EffectiveExpiry + { + get + { + var initialDuration = this.InitialDuration; + var expiryInitial = + initialDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.CreatedAt + initialDuration; + + DateTime expiry; + var hoverExtendDuration = this.HoverExtendDuration; + if (hoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + { + expiry = DateTime.MaxValue; + } + else + { + var expiryExtend = + hoverExtendDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.HoverRelativeToTime + hoverExtendDuration; + + expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; + if (expiry < this.ExtendedExpiry) + expiry = this.ExtendedExpiry; + } + + var he = this.HardExpiry; + if (he < expiry) + expiry = he; + return expiry; + } + } + + /// public bool ShowIndeterminateIfNoExpiry { get => this.underlyingNotification.ShowIndeterminateIfNoExpiry; @@ -154,19 +250,19 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// - public bool Interactable + /// + public bool Minimized { - get => this.underlyingNotification.Interactable; + get => this.newMinimized ?? this.underlyingNotification.Minimized; set { if (this.IsDismissed) return; - this.underlyingNotification.Interactable = value; + this.newMinimized = value; } } - /// + /// public bool UserDismissable { get => this.underlyingNotification.UserDismissable; @@ -178,19 +274,7 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// - public TimeSpan HoverExtendDuration - { - get => this.underlyingNotification.HoverExtendDuration; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.HoverExtendDuration = value; - } - } - - /// + /// public float Progress { get => this.newProgress ?? this.underlyingNotification.Progress; @@ -198,7 +282,6 @@ internal sealed class ActiveNotification : IActiveNotification { if (this.IsDismissed) return; - this.newProgress = value; } } @@ -244,13 +327,13 @@ internal sealed class ActiveNotification : IActiveNotification }; /// Gets the default icon of the notification. - private string? DefaultIconString => this.Type switch + private char? DefaultIconChar => this.Type switch { NotificationType.None => null, - NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconString(), - NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconString(), - NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconString(), - NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconString(), + NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), + NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), + NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), + NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), _ => null, }; @@ -273,6 +356,9 @@ internal sealed class ActiveNotification : IActiveNotification ? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name) : initiatorPlugin.Name; + /// Gets the effective text to display when minimized. + private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" "); + /// public void Dispose() { @@ -314,16 +400,38 @@ internal sealed class ActiveNotification : IActiveNotification this.showEasing.Update(); this.hideEasing.Update(); this.progressEasing.Update(); - - if (this.newProgress is { } p) + if (this.expandoEasing.IsRunning) { - this.progressBefore = this.ProgressEased; - this.underlyingNotification.Progress = p; - this.progressEasing.Restart(); - this.progressEasing.Update(); + this.expandoEasing.Update(); + if (this.expandoEasing.IsDone) + this.expandoEasing.Stop(); + } + + if (this.newProgress is { } newProgressValue) + { + if (Math.Abs(this.underlyingNotification.Progress - newProgressValue) > float.Epsilon) + { + this.progressBefore = this.ProgressEased; + this.underlyingNotification.Progress = newProgressValue; + this.progressEasing.Restart(); + this.progressEasing.Update(); + } + this.newProgress = null; } + if (this.newMinimized is { } newMinimizedValue) + { + if (this.underlyingNotification.Minimized != newMinimizedValue) + { + this.underlyingNotification.Minimized = newMinimizedValue; + this.expandoEasing.Restart(); + this.expandoEasing.Update(); + } + + this.newMinimized = null; + } + return this.hideEasing.IsRunning && this.hideEasing.IsDone; } @@ -333,12 +441,9 @@ internal sealed class ActiveNotification : IActiveNotification /// The height of the notification. public float Draw(float maxWidth, float offsetY) { - if (!this.IsDismissed - && DateTime.Now > this.Expiry - && (this.HoverExtendDuration <= TimeSpan.Zero || !this.IsMouseHovered)) - { + var effectiveExpiry = this.EffectiveExpiry; + if (!this.IsDismissed && DateTime.Now > effectiveExpiry) this.DismissNow(NotificationDismissReason.Timeout); - } var opacity = Math.Clamp( @@ -375,6 +480,12 @@ internal sealed class ActiveNotification : IActiveNotification unboundedWidth += NotificationConstants.ScaledWindowPadding * 3; unboundedWidth += NotificationConstants.ScaledIconSize; + var actionWindowHeight = + // Content + ImGui.GetTextLineHeight() + + // Top and bottom padding + (NotificationConstants.ScaledWindowPadding * 2); + var width = Math.Min(maxWidth, unboundedWidth); var viewport = ImGuiHelpers.MainViewport; @@ -384,6 +495,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.PushID(this.Id.GetHashCode()); ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); unsafe { ImGui.PushStyleColor( @@ -402,83 +514,49 @@ internal sealed class ActiveNotification : IActiveNotification new Vector2(0, offsetY), ImGuiCond.Always, Vector2.One); - ImGui.SetNextWindowSizeConstraints(new(width, 0), new(width, float.MaxValue)); - ImGui.PushStyleVar( - ImGuiStyleVar.WindowPadding, - new Vector2(NotificationConstants.ScaledWindowPadding, 0)); + ImGui.SetNextWindowSizeConstraints( + new(width, actionWindowHeight), + new( + width, + !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning + ? float.MaxValue + : actionWindowHeight)); ImGui.Begin( $"##NotifyMainWindow{this.Id}", ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | - (this.Interactable - ? ImGuiWindowFlags.None - : ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoBringToFrontOnFocus) | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); this.DrawWindowBackgroundProgressBar(); - this.DrawNotificationMainWindowContent(width); + this.DrawTopBar(interfaceManager, width, actionWindowHeight); + if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) + { + this.DrawContentArea(width, actionWindowHeight); + } + else if (this.expandoEasing.IsRunning) + { + if (this.underlyingNotification.Minimized) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); + else + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); + this.DrawContentArea(width, actionWindowHeight); + ImGui.PopStyleVar(); + } + + this.DrawExpiryBar(effectiveExpiry); + var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); var hovered = ImGui.IsWindowHovered(); - ImGui.End(); - ImGui.PopStyleVar(); - - offsetY += windowSize.Y; - - var actionWindowHeight = - // Content - ImGui.GetTextLineHeight() + - // Top and bottom padding - (NotificationConstants.ScaledWindowPadding * 2); - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), - ImGuiCond.Always, - Vector2.One); - ImGui.SetNextWindowSizeConstraints(new(width, actionWindowHeight), new(width, actionWindowHeight)); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, Vector2.Zero); - ImGui.Begin( - $"##NotifyActionWindow{this.Id}", - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoDocking); - - this.DrawWindowBackgroundProgressBar(); - this.DrawNotificationActionWindowContent(interfaceManager, width); - windowSize.Y += actionWindowHeight; - windowPos.Y -= actionWindowHeight; - hovered |= ImGui.IsWindowHovered(); - - ImGui.End(); - ImGui.PopStyleVar(); ImGui.PopStyleColor(); - ImGui.PopStyleVar(2); + ImGui.PopStyleVar(3); ImGui.PopID(); - if (hovered) - { - if (this.Click is null) - { - if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - this.DismissNow(NotificationDismissReason.Manual); - } - else - { - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) - || ImGui.IsMouseClicked(ImGuiMouseButton.Right) - || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) - this.Click.InvokeSafely(this); - } - } - if (windowPos.X <= ImGui.GetIO().MousePos.X && windowPos.Y <= ImGui.GetIO().MousePos.Y && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X @@ -489,19 +567,28 @@ internal sealed class ActiveNotification : IActiveNotification this.IsMouseHovered = true; this.MouseEnter.InvokeSafely(this); } + + if (this.HoverExtendDuration > TimeSpan.Zero) + this.HoverRelativeToTime = DateTime.Now; + + if (hovered) + { + if (this.Click is null) + { + if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + this.DismissNow(NotificationDismissReason.Manual); + } + else + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) + || ImGui.IsMouseClicked(ImGuiMouseButton.Right) + || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + this.Click.InvokeSafely(this); + } + } } else if (this.IsMouseHovered) { - if (this.HoverExtendDuration > TimeSpan.Zero) - { - var newExpiry = DateTime.Now + this.HoverExtendDuration; - if (newExpiry > this.Expiry) - { - this.underlyingNotification.Expiry = newExpiry; - this.ExpiryRelativeToTime = DateTime.Now; - } - } - this.IsMouseHovered = false; this.MouseLeave.InvokeSafely(this); } @@ -509,6 +596,14 @@ internal sealed class ActiveNotification : IActiveNotification return windowSize.Y; } + /// + public void ExtendBy(TimeSpan extension) + { + var newExpiry = DateTime.Now + extension; + if (this.ExtendedExpiry < newExpiry) + this.ExtendedExpiry = newExpiry; + } + /// public void UpdateIcon() { @@ -518,17 +613,6 @@ internal sealed class ActiveNotification : IActiveNotification this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); } - /// - public void UpdateIconSource(INotificationIconSource? newIconSource) - { - if (this.IsDismissed || this.underlyingNotification.IconSource == newIconSource) - return; - - this.underlyingNotification.IconSource?.Dispose(); - this.underlyingNotification.IconSource = newIconSource; - this.UpdateIcon(); - } - /// Removes non-Dalamud invocation targets from events. public void RemoveNonDalamudInvocations() { @@ -539,14 +623,13 @@ internal sealed class ActiveNotification : IActiveNotification this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); - this.Interactable = true; this.IsInitiatorUnloaded = true; this.UserDismissable = true; this.HoverExtendDuration = NotificationConstants.DefaultHoverExtendDuration; var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; - if (this.Expiry > newMaxExpiry) - this.Expiry = newMaxExpiry; + if (this.EffectiveExpiry > newMaxExpiry) + this.HardExpiry = newMaxExpiry; return; @@ -617,23 +700,209 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.PopClipRect(); } - private void DrawNotificationMainWindowContent(float width) + private void DrawTopBar(InterfaceManager interfaceManager, float width, float height) { - var basePos = ImGui.GetCursorPos(); + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + + var rtOffset = new Vector2(width, 0); + using (interfaceManager.IconFontHandle?.Push()) + { + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + if (this.UserDismissable) + { + if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height)) + this.DismissNow(NotificationDismissReason.Manual); + rtOffset.X -= height; + } + + if (this.underlyingNotification.Minimized) + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height)) + this.Minimized = false; + } + else + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height)) + this.Minimized = true; + } + + rtOffset.X -= height; + ImGui.PopClipRect(); + } + + float relativeOpacity; + if (this.expandoEasing.IsRunning) + { + relativeOpacity = + this.underlyingNotification.Minimized + ? 1f - (float)this.expandoEasing.Value + : (float)this.expandoEasing.Value; + } + else + { + relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; + } + + if (this.IsMouseHovered) + ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); + else + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + + if (relativeOpacity > 0) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity); + ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted( + this.IsMouseHovered + ? this.CreatedAt.FormatAbsoluteDateTime() + : this.CreatedAt.FormatRelativeDateTime()); + ImGui.PopStyleColor(); + ImGui.PopStyleVar(); + } + + if (relativeOpacity < 1) + { + rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); + + var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); + this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); + + ltOffset.X = height; + + var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); + var agoSize = ImGui.CalcTextSize(agoText); + rtOffset.X -= agoSize.X; + ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted(agoText); + ImGui.PopStyleColor(); + + rtOffset.X -= NotificationConstants.ScaledWindowPadding; + + ImGui.PushClipRect( + windowPos + ltOffset with { Y = 0 }, + windowPos + rtOffset with { Y = height }, + true); + ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.TextUnformatted(this.EffectiveMinimizedText); + ImGui.PopClipRect(); + + ImGui.PopStyleVar(); + } + + ImGui.PopClipRect(); + } + + private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size) + { + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + if (!this.IsMouseHovered) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + ImGui.SetCursorPos(rt - new Vector2(size, 0)); + var r = ImGui.Button(icon.ToIconString(), new(size)); + + ImGui.PopStyleColor(2); + if (!this.IsMouseHovered) + ImGui.PopStyleVar(); + ImGui.PopStyleVar(); + return r; + } + + private void DrawContentArea(float width, float actionWindowHeight) + { + var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; + var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; + var textColumnOffset = new Vector2(textColumnX, actionWindowHeight); + this.DrawIcon( - basePos, - basePos + new Vector2(NotificationConstants.ScaledIconSize)); - basePos.X += NotificationConstants.ScaledIconSize + NotificationConstants.ScaledWindowPadding; - width -= NotificationConstants.ScaledIconSize + (NotificationConstants.ScaledWindowPadding * 2); - this.DrawTitle(basePos, basePos + new Vector2(width, 0)); - basePos.Y = ImGui.GetCursorPosY(); - this.DrawContentBody(basePos, basePos + new Vector2(width, 0)); + new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), + new(NotificationConstants.ScaledIconSize)); - // Intention was to have left, right, and bottom have the window padding and top have the component gap, - // but as ImGui only allows horz/vert padding, we add the extra bottom padding. - // Top padding is zero, as the action window will add the padding. - ImGui.Dummy(new(NotificationConstants.ScaledWindowPadding)); + textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth); + textColumnOffset.Y += NotificationConstants.ScaledComponentGap; + this.DrawContentBody(textColumnOffset, textColumnWidth); + } + + private void DrawIcon(Vector2 minCoord, Vector2 size) + { + var maxCoord = minCoord + size; + if (this.MaterializedIcon is not null) + { + this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); + return; + } + + var defaultIconChar = this.DefaultIconChar; + if (defaultIconChar is not null) + { + NotificationUtilities.DrawIconString( + Service.Get().IconFontAwesomeFontHandle, + defaultIconChar.Value, + minCoord, + maxCoord, + this.DefaultIconColor); + return; + } + + TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( + minCoord, + maxCoord, + this.DefaultIconColor, + this.InitiatorPlugin); + } + + private float DrawTitle(Vector2 minCoord, float width) + { + ImGui.PushTextWrapPos(minCoord.X + width); + + ImGui.SetCursorPos(minCoord); + if ((this.Title ?? this.DefaultTitle) is { } title) + { + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); + ImGui.TextUnformatted(title); + ImGui.PopStyleColor(); + } + + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); + ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); + ImGui.TextUnformatted(this.InitiatorString); + ImGui.PopStyleColor(); + + ImGui.PopTextWrapPos(); + return ImGui.GetCursorPosY() - minCoord.Y; + } + + private void DrawContentBody(Vector2 minCoord, float width) + { + ImGui.SetCursorPos(minCoord); + ImGui.PushTextWrapPos(minCoord.X + width); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); + ImGui.TextUnformatted(this.Content); + ImGui.PopStyleColor(); + ImGui.PopTextWrapPos(); + if (this.DrawActions is not null) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + try + { + this.DrawActions.Invoke(this); + } + catch + { + // ignore + } + } + } + + private void DrawExpiryBar(DateTime effectiveExpiry) + { float barL, barR; if (this.IsDismissed) { @@ -643,7 +912,14 @@ internal sealed class ActiveNotification : IActiveNotification barL = midpoint - (length * v); barR = midpoint + (length * v); } - else if (this.Expiry == DateTime.MaxValue) + else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + { + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else if (effectiveExpiry == DateTime.MaxValue) { if (this.ShowIndeterminateIfNoExpiry) { @@ -663,17 +939,10 @@ internal sealed class ActiveNotification : IActiveNotification this.prevProgressR = barR = 1f; } } - else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) - { - barL = 0f; - barR = 1f; - this.prevProgressL = barL; - this.prevProgressR = barR; - } else { - barL = 1f - (float)((this.Expiry - DateTime.Now).TotalMilliseconds / - (this.Expiry - this.ExpiryRelativeToTime).TotalMilliseconds); + barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / + (effectiveExpiry - this.HoverRelativeToTime).TotalMilliseconds); barR = 1f; this.prevProgressL = barL; this.prevProgressR = barR; @@ -692,112 +961,4 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.GetColorU32(this.DefaultIconColor)); ImGui.PopClipRect(); } - - private void DrawIcon(Vector2 minCoord, Vector2 maxCoord) - { - if (this.MaterializedIcon is not null) - { - this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); - return; - } - - var defaultIconString = this.DefaultIconString; - if (!string.IsNullOrWhiteSpace(defaultIconString)) - { - FontAwesomeIconIconSource.DrawIconStatic(defaultIconString, minCoord, maxCoord, this.DefaultIconColor); - return; - } - - TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( - minCoord, - maxCoord, - this.DefaultIconColor, - this.InitiatorPlugin); - } - - private void DrawTitle(Vector2 minCoord, Vector2 maxCoord) - { - ImGui.PushTextWrapPos(maxCoord.X); - - ImGui.SetCursorPos(minCoord); - if ((this.Title ?? this.DefaultTitle) is { } title) - { - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); - ImGui.TextUnformatted(title); - ImGui.PopStyleColor(); - } - - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); - ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); - ImGui.TextUnformatted(this.InitiatorString); - ImGui.PopStyleColor(); - - ImGui.PopTextWrapPos(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - } - - private void DrawContentBody(Vector2 minCoord, Vector2 maxCoord) - { - ImGui.SetCursorPos(minCoord); - ImGui.PushTextWrapPos(maxCoord.X); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); - ImGui.TextUnformatted(this.Content); - ImGui.PopStyleColor(); - ImGui.PopTextWrapPos(); - if (this.DrawActions is not null) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - try - { - this.DrawActions.Invoke(this); - } - catch - { - // ignore - } - } - } - - private void DrawNotificationActionWindowContent(InterfaceManager interfaceManager, float width) - { - ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); - ImGui.TextUnformatted( - this.IsMouseHovered - ? this.CreatedAt.FormatAbsoluteDateTime() - : this.CreatedAt.FormatRelativeDateTime()); - ImGui.PopStyleColor(); - - this.DrawCloseButton( - interfaceManager, - new(width - NotificationConstants.ScaledWindowPadding, NotificationConstants.ScaledWindowPadding), - NotificationConstants.ScaledWindowPadding); - } - - private void DrawCloseButton(InterfaceManager interfaceManager, Vector2 rt, float pad) - { - if (!this.UserDismissable) - return; - - using (interfaceManager.IconFontHandle?.Push()) - { - var str = FontAwesomeIcon.Times.ToIconString(); - var textSize = ImGui.CalcTextSize(str); - var size = Math.Max(textSize.X, textSize.Y); - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - if (!this.IsMouseHovered) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); - ImGui.PushStyleColor(ImGuiCol.Button, 0); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); - - ImGui.SetCursorPos(rt - new Vector2(size, 0) - new Vector2(pad)); - if (ImGui.Button(str, new(size + (pad * 2)))) - this.DismissNow(NotificationDismissReason.Manual); - - ImGui.PopStyleColor(2); - if (!this.IsMouseHovered) - ImGui.PopStyleVar(); - ImGui.PopStyleVar(); - } - } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs index 86a6f835c..cfe790851 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs @@ -2,8 +2,6 @@ using System.Numerics; using Dalamud.Plugin.Internal.Types; -using ImGuiNET; - namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. @@ -27,35 +25,22 @@ internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal /// public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); - /// Draws the icon. - /// The icon string. - /// The coordinates of the top left of the icon area. - /// The coordinates of the bottom right of the icon area. - /// The foreground color. - internal static void DrawIconStatic(string iconString, Vector2 minCoord, Vector2 maxCoord, Vector4 color) - { - using (Service.Get().IconFontAwesomeFontHandle.Push()) - { - var size = ImGui.CalcTextSize(iconString); - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextUnformatted(iconString); - ImGui.PopStyleColor(); - } - } - private sealed class MaterializedIcon : INotificationMaterializedIcon { - private readonly string iconString; + private readonly char iconChar; - public MaterializedIcon(FontAwesomeIcon c) => this.iconString = c.ToIconString(); + public MaterializedIcon(FontAwesomeIcon c) => this.iconChar = c.ToIconChar(); public void Dispose() { } public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - DrawIconStatic(this.iconString, minCoord, maxCoord, color); + NotificationUtilities.DrawIconString( + Service.Get().IconFontAwesomeFontHandle, + this.iconChar, + minCoord, + maxCoord, + color); } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs index 83fd0bef6..19fe8e948 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs @@ -3,8 +3,6 @@ using System.Numerics; using Dalamud.Game.Text; using Dalamud.Plugin.Internal.Types; -using ImGuiNET; - namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; /// Represents the use of as the icon of a notification. @@ -30,25 +28,20 @@ internal class SeIconCharIconSource : INotificationIconSource.IInternal private sealed class MaterializedIcon : INotificationMaterializedIcon { - private readonly string iconString; + private readonly char iconChar; - public MaterializedIcon(SeIconChar c) => this.iconString = c.ToIconString(); + public MaterializedIcon(SeIconChar c) => this.iconChar = c.ToIconChar(); public void Dispose() { } - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) - { - using (Service.Get().IconAxisFontHandle.Push()) - { - var size = ImGui.CalcTextSize(this.iconString); - var pos = ((minCoord + maxCoord) - size) / 2; - ImGui.SetCursorPos(pos); - ImGui.PushStyleColor(ImGuiCol.Text, color); - ImGui.TextUnformatted(this.iconString); - ImGui.PopStyleColor(); - } - } + public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => + NotificationUtilities.DrawIconString( + Service.Get().IconAxisFontHandle, + this.iconChar, + minCoord, + maxCoord, + color); } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index fdea6146a..b457539a3 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -106,14 +106,15 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); - this.notifications.RemoveAll(static x => - { - if (!x.UpdateAnimations()) - return false; + this.notifications.RemoveAll( + static x => + { + if (!x.UpdateAnimations()) + return false; - x.Dispose(); - return true; - }); + x.Dispose(); + return true; + }); foreach (var tn in this.notifications) height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 9c89dc305..dd1d87c42 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,3 +1,5 @@ +using System.Threading; + using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -5,40 +7,88 @@ namespace Dalamud.Interface.ImGuiNotification; /// Represents a blueprint for a notification. public sealed record Notification : INotification { + private INotificationIconSource? iconSource; + + /// Initializes a new instance of the class. + public Notification() + { + } + + /// Initializes a new instance of the class. + /// The instance of to copy from. + public Notification(INotification notification) => this.CopyValuesFrom(notification); + + /// Initializes a new instance of the class. + /// The instance of to copy from. + public Notification(Notification notification) => this.CopyValuesFrom(notification); + /// public string Content { get; set; } = string.Empty; /// public string? Title { get; set; } + /// + public string? MinimizedText { get; set; } + /// public NotificationType Type { get; set; } = NotificationType.None; /// - public INotificationIconSource? IconSource { get; set; } + public INotificationIconSource? IconSource + { + get => this.iconSource; + set + { + var prevSource = Interlocked.Exchange(ref this.iconSource, value); + if (prevSource != value) + prevSource?.Dispose(); + } + } /// - public DateTime Expiry { get; set; } = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + public DateTime HardExpiry { get; set; } = DateTime.MaxValue; + + /// + public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; + + /// + public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; /// - public bool Interactable { get; set; } = true; + public bool Minimized { get; set; } = true; /// public bool UserDismissable { get; set; } = true; - /// - public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; - /// public float Progress { get; set; } = 1f; /// public void Dispose() { - this.IconSource?.Dispose(); + // Assign to the property; it will take care of disposing this.IconSource = null; } + + /// Copy values from the given instance of . + /// The instance of to copy from. + private void CopyValuesFrom(INotification copyFrom) + { + this.Content = copyFrom.Content; + this.Title = copyFrom.Title; + this.MinimizedText = copyFrom.MinimizedText; + this.Type = copyFrom.Type; + this.IconSource = copyFrom.IconSource?.Clone(); + this.HardExpiry = copyFrom.HardExpiry; + this.InitialDuration = copyFrom.InitialDuration; + this.HoverExtendDuration = copyFrom.HoverExtendDuration; + this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry; + this.Minimized = copyFrom.Minimized; + this.UserDismissable = copyFrom.UserDismissable; + this.Progress = copyFrom.Progress; + } } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 800531f39..08ef8aebd 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using System.Numerics; using Dalamud.Interface.Utility; @@ -56,6 +57,9 @@ public static class NotificationConstants /// Duration of progress change animation. internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); + /// Duration of expando animation. + internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); + /// Text color for the when. internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -92,6 +96,16 @@ public static class NotificationConstants (TimeSpan.MinValue, "just now"), }; + /// Gets the relative time format strings. + private static readonly (TimeSpan MinSpan, string FormatString)[] RelativeFormatStringsShort = + { + (TimeSpan.FromDays(1), "{0:%d}d"), + (TimeSpan.FromHours(1), "{0:%h}h"), + (TimeSpan.FromMinutes(1), "{0:%m}m"), + (TimeSpan.FromSeconds(1), "{0:%s}s"), + (TimeSpan.MinValue, "now"), + }; + /// Gets the scaled padding of the window (dot(.) in the above diagram). internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); @@ -137,4 +151,21 @@ public static class NotificationConstants /// When. /// The formatted string. internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; + + /// Formats an instance of as a relative time. + /// When. + /// The formatted string. + internal static string FormatRelativeDateTimeShort(this DateTime when) + { + var ts = DateTime.Now - when; + foreach (var (minSpan, formatString) in RelativeFormatStringsShort) + { + if (ts < minSpan) + continue; + return string.Format(formatString, ts); + } + + Debug.Assert(false, "must not reach here"); + return "???"; + } } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs index 47e52b142..2c9d6d2a4 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs @@ -3,7 +3,7 @@ namespace Dalamud.Interface.ImGuiNotification; /// Specifies the reason of dismissal for a notification. public enum NotificationDismissReason { - /// The notification is dismissed because the expiry specified from is + /// The notification is dismissed because the expiry specified from is /// met. Timeout = 1, diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 9b3602b68..016e9b793 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -5,6 +5,8 @@ using System.Runtime.CompilerServices; using Dalamud.Game.Text; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; using Dalamud.Storage.Assets; @@ -19,22 +21,56 @@ public static class NotificationUtilities [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this SeIconChar iconChar) => INotificationIconSource.From(iconChar); - + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) => INotificationIconSource.From(iconChar); - + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this IDalamudTextureWrap? wrap, bool takeOwnership = true) => INotificationIconSource.From(wrap, takeOwnership); - + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static INotificationIconSource ToIconSource(this FileInfo fileInfo) => INotificationIconSource.FromFile(fileInfo.FullName); + /// Draws an icon string. + /// The font handle to use. + /// The icon character. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + internal static unsafe void DrawIconString( + IFontHandle fontHandleLarge, + char c, + Vector2 minCoord, + Vector2 maxCoord, + Vector4 color) + { + var smallerDim = Math.Max(maxCoord.Y - minCoord.Y, maxCoord.X - minCoord.X); + using (fontHandleLarge.Push()) + { + var font = ImGui.GetFont(); + ref readonly var glyph = ref *(ImGuiHelpers.ImFontGlyphReal*)font.FindGlyph(c).NativePtr; + var size = glyph.XY1 - glyph.XY0; + var smallerSizeDim = Math.Min(size.X, size.Y); + var scale = smallerSizeDim > smallerDim ? smallerDim / smallerSizeDim : 1f; + size *= scale; + var pos = ((minCoord + maxCoord) - size) / 2; + pos += ImGui.GetWindowPos(); + ImGui.GetWindowDrawList().AddImage( + font.ContainerAtlas.Textures[glyph.TextureIndex].TexID, + pos, + pos + size, + glyph.UV0, + glyph.UV1, + ImGui.GetColorU32(color with { W = color.W * ImGui.GetStyle().Alpha })); + } + } + /// Draws the given texture, or the icon of the plugin if texture is null. /// The texture. /// The coordinates of the top left of the icon area. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index ae3f16576..4d3807417 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using Dalamud.Storage.Assets; @@ -64,6 +63,10 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.SameLine(); ImGui.InputText("Title##title", ref this.notificationTemplate.Title, 255); + ImGui.Checkbox("##manualMinimizedText", ref this.notificationTemplate.ManualMinimizedText); + ImGui.SameLine(); + ImGui.InputText("MinimizedText##minimizedText", ref this.notificationTemplate.MinimizedText, 255); + ImGui.Checkbox("##manualType", ref this.notificationTemplate.ManualType); ImGui.SameLine(); ImGui.Combo( @@ -107,10 +110,16 @@ internal class ImGuiWidget : IDataWindowWidget } ImGui.Combo( - "Duration", - ref this.notificationTemplate.DurationInt, - NotificationTemplate.DurationTitles, - NotificationTemplate.DurationTitles.Length); + "Initial Duration", + ref this.notificationTemplate.InitialDurationInt, + NotificationTemplate.InitialDurationTitles, + NotificationTemplate.InitialDurationTitles.Length); + + ImGui.Combo( + "Hover Extend Duration", + ref this.notificationTemplate.HoverExtendDurationInt, + NotificationTemplate.HoverExtendDurationTitles, + NotificationTemplate.HoverExtendDurationTitles.Length); ImGui.Combo( "Progress", @@ -118,7 +127,7 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.ProgressModeTitles, NotificationTemplate.ProgressModeTitles.Length); - ImGui.Checkbox("Interactable", ref this.notificationTemplate.Interactable); + ImGui.Checkbox("Minimized", ref this.notificationTemplate.Minimized); ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry); @@ -141,18 +150,26 @@ internal class ImGuiWidget : IDataWindowWidget if (this.notificationTemplate.ManualType) type = (NotificationType)this.notificationTemplate.TypeInt; - var duration = NotificationTemplate.Durations[this.notificationTemplate.DurationInt]; - var n = notifications.AddNotification( new() { Content = text, Title = title, + MinimizedText = this.notificationTemplate.ManualMinimizedText + ? this.notificationTemplate.MinimizedText + : null, Type = type, ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry, - Interactable = this.notificationTemplate.Interactable, + Minimized = this.notificationTemplate.Minimized, UserDismissable = this.notificationTemplate.UserDismissable, - Expiry = duration == TimeSpan.MaxValue ? DateTime.MaxValue : DateTime.Now + duration, + InitialDuration = + this.notificationTemplate.InitialDurationInt == 0 + ? TimeSpan.MaxValue + : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], + HoverExtendDuration = + this.notificationTemplate.HoverExtendDurationInt == 0 + ? TimeSpan.Zero + : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], Progress = this.notificationTemplate.ProgressMode switch { 0 => 1f, @@ -220,7 +237,8 @@ internal class ImGuiWidget : IDataWindowWidget n.Progress = i / 10f; } - n.Expiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; + n.ExtendBy(NotificationConstants.DefaultDisplayDuration); + n.InitialDuration = NotificationConstants.DefaultDisplayDuration; }); break; } @@ -324,7 +342,7 @@ internal class ImGuiWidget : IDataWindowWidget nameof(NotificationType.Info), }; - public static readonly string[] DurationTitles = + public static readonly string[] InitialDurationTitles = { "Infinite", "1 seconds", @@ -332,9 +350,17 @@ internal class ImGuiWidget : IDataWindowWidget "10 seconds", }; + public static readonly string[] HoverExtendDurationTitles = + { + "Disable", + "1 seconds", + "3 seconds (default)", + "10 seconds", + }; + public static readonly TimeSpan[] Durations = { - TimeSpan.MaxValue, + TimeSpan.Zero, TimeSpan.FromSeconds(1), NotificationConstants.DefaultDisplayDuration, TimeSpan.FromSeconds(10), @@ -344,14 +370,17 @@ internal class ImGuiWidget : IDataWindowWidget public string Content; public bool ManualTitle; public string Title; + public bool ManualMinimizedText; + public string MinimizedText; public int IconSourceInt; public string IconSourceText; public int IconSourceAssetInt; public bool ManualType; public int TypeInt; - public int DurationInt; + public int InitialDurationInt; + public int HoverExtendDurationInt; public bool ShowIndeterminateIfNoExpiry; - public bool Interactable; + public bool Minimized; public bool UserDismissable; public bool ActionBar; public int ProgressMode; @@ -362,14 +391,17 @@ internal class ImGuiWidget : IDataWindowWidget this.Content = string.Empty; this.ManualTitle = false; this.Title = string.Empty; + this.ManualMinimizedText = false; + this.MinimizedText = string.Empty; this.IconSourceInt = 0; this.IconSourceText = "ui/icon/000000/000004_hr1.tex"; this.IconSourceAssetInt = 0; this.ManualType = false; this.TypeInt = (int)NotificationType.None; - this.DurationInt = 2; + this.InitialDurationInt = 2; + this.HoverExtendDurationInt = 2; this.ShowIndeterminateIfNoExpiry = true; - this.Interactable = true; + this.Minimized = true; this.UserDismissable = true; this.ActionBar = true; this.ProgressMode = 0; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 1237c9c1f..417d77e7d 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -579,7 +579,7 @@ public sealed class UiBuilder : IDisposable Content = content, Title = title, Type = type, - Expiry = DateTime.Now + TimeSpan.FromMilliseconds(msDelay), + InitialDuration = TimeSpan.FromMilliseconds(msDelay), }, true, this.localPlugin); From e44180d4a2b029efe731be248eaeba1991826b47 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Mon, 26 Feb 2024 20:04:33 +0900 Subject: [PATCH 550/585] honor notification window focus --- .../ImGuiNotification/IActiveNotification.cs | 5 +- .../ImGuiNotification/INotification.cs | 9 ++- .../Internal/ActiveNotification.cs | 73 +++++++++++++------ .../ImGuiNotification/Notification.cs | 4 +- .../NotificationConstants.cs | 6 ++ .../Windows/Data/Widgets/ImGuiWidget.cs | 11 ++- 6 files changed, 74 insertions(+), 34 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 504c6d6d5..dd4101c92 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -49,7 +49,10 @@ public interface IActiveNotification : INotification DateTime EffectiveExpiry { get; } /// Gets a value indicating whether the mouse cursor is on the notification window. - bool IsMouseHovered { get; } + bool IsHovered { get; } + + /// Gets a value indicating whether the notification window is focused. + bool IsFocused { get; } /// Gets a value indicating whether the notification has been dismissed. /// This includes when the hide animation is being played. diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 8f5a30e79..349d66f72 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -33,7 +33,7 @@ public interface INotification : IDisposable /// Gets or sets the hard expiry. /// - /// Setting this value will override and , in that + /// Setting this value will override and , in that /// the notification will be dismissed when this expiry expires.
    /// Set to to make only take effect.
    /// If neither nor is not MaxValue, then the notification @@ -48,13 +48,14 @@ public interface INotification : IDisposable /// Updating this value will reset the dismiss timer. TimeSpan InitialDuration { get; set; } - /// Gets or sets the new duration for this notification once the mouse cursor leaves the window. + /// Gets or sets the new duration for this notification once the mouse cursor leaves the window and the + /// window is no longer focused. /// /// If set to or less, then this feature is turned off, and hovering the mouse on the - /// notification will not make the notification stay.
    + /// notification or focusing on it will not make the notification stay.
    /// Updating this value will reset the dismiss timer. ///
    - TimeSpan HoverExtendDuration { get; set; } + TimeSpan DurationSinceLastInterest { get; set; } /// Gets or sets a value indicating whether to show an indeterminate expiration animation if /// is set to . diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index a89ebeb0b..8591695a6 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -93,7 +93,7 @@ internal sealed class ActiveNotification : IActiveNotification public DateTime CreatedAt { get; } = DateTime.Now; /// Gets the time of starting to count the timer for the expiration. - public DateTime HoverRelativeToTime { get; private set; } = DateTime.Now; + public DateTime LastInterestTime { get; private set; } = DateTime.Now; /// Gets the extended expiration time from . public DateTime ExtendedExpiry { get; private set; } = DateTime.Now; @@ -172,7 +172,7 @@ internal sealed class ActiveNotification : IActiveNotification if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) return; this.underlyingNotification.HardExpiry = value; - this.HoverRelativeToTime = DateTime.Now; + this.LastInterestTime = DateTime.Now; } } @@ -185,20 +185,20 @@ internal sealed class ActiveNotification : IActiveNotification if (this.IsDismissed) return; this.underlyingNotification.InitialDuration = value; - this.HoverRelativeToTime = DateTime.Now; + this.LastInterestTime = DateTime.Now; } } /// - public TimeSpan HoverExtendDuration + public TimeSpan DurationSinceLastInterest { - get => this.underlyingNotification.HoverExtendDuration; + get => this.underlyingNotification.DurationSinceLastInterest; set { if (this.IsDismissed) return; - this.underlyingNotification.HoverExtendDuration = value; - this.HoverRelativeToTime = DateTime.Now; + this.underlyingNotification.DurationSinceLastInterest = value; + this.LastInterestTime = DateTime.Now; } } @@ -214,8 +214,8 @@ internal sealed class ActiveNotification : IActiveNotification : this.CreatedAt + initialDuration; DateTime expiry; - var hoverExtendDuration = this.HoverExtendDuration; - if (hoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + var hoverExtendDuration = this.DurationSinceLastInterest; + if (hoverExtendDuration > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) { expiry = DateTime.MaxValue; } @@ -224,7 +224,7 @@ internal sealed class ActiveNotification : IActiveNotification var expiryExtend = hoverExtendDuration == TimeSpan.MaxValue ? DateTime.MaxValue - : this.HoverRelativeToTime + hoverExtendDuration; + : this.LastInterestTime + hoverExtendDuration; expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; if (expiry < this.ExtendedExpiry) @@ -287,7 +287,10 @@ internal sealed class ActiveNotification : IActiveNotification } /// - public bool IsMouseHovered { get; private set; } + public bool IsHovered { get; private set; } + + /// + public bool IsFocused { get; private set; } /// public bool IsDismissed => this.hideEasing.IsRunning; @@ -529,8 +532,12 @@ internal sealed class ActiveNotification : IActiveNotification ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); + this.IsFocused = ImGui.IsWindowFocused(); + if (this.IsFocused) + this.LastInterestTime = DateTime.Now; this.DrawWindowBackgroundProgressBar(); + this.DrawFocusIndicator(); this.DrawTopBar(interfaceManager, width, actionWindowHeight); if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) { @@ -562,14 +569,14 @@ internal sealed class ActiveNotification : IActiveNotification && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X && ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y) { - if (!this.IsMouseHovered) + if (!this.IsHovered) { - this.IsMouseHovered = true; + this.IsHovered = true; this.MouseEnter.InvokeSafely(this); } - if (this.HoverExtendDuration > TimeSpan.Zero) - this.HoverRelativeToTime = DateTime.Now; + if (this.DurationSinceLastInterest > TimeSpan.Zero) + this.LastInterestTime = DateTime.Now; if (hovered) { @@ -587,9 +594,9 @@ internal sealed class ActiveNotification : IActiveNotification } } } - else if (this.IsMouseHovered) + else if (this.IsHovered) { - this.IsMouseHovered = false; + this.IsHovered = false; this.MouseLeave.InvokeSafely(this); } @@ -625,7 +632,7 @@ internal sealed class ActiveNotification : IActiveNotification this.IsInitiatorUnloaded = true; this.UserDismissable = true; - this.HoverExtendDuration = NotificationConstants.DefaultHoverExtendDuration; + this.DurationSinceLastInterest = NotificationConstants.DefaultHoverExtendDuration; var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; if (this.EffectiveExpiry > newMaxExpiry) @@ -700,6 +707,23 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.PopClipRect(); } + private void DrawFocusIndicator() + { + if (!this.IsFocused) + return; + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRect( + windowPos, + windowPos + windowSize, + ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)), + 0f, + ImDrawFlags.None, + NotificationConstants.FocusIndicatorThickness); + ImGui.PopClipRect(); + } + private void DrawTopBar(InterfaceManager interfaceManager, float width, float height) { var windowPos = ImGui.GetWindowPos(); @@ -744,7 +768,7 @@ internal sealed class ActiveNotification : IActiveNotification relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; } - if (this.IsMouseHovered) + if (this.IsHovered || this.IsFocused) ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); else ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); @@ -755,7 +779,7 @@ internal sealed class ActiveNotification : IActiveNotification ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); ImGui.TextUnformatted( - this.IsMouseHovered + this.IsHovered || this.IsFocused ? this.CreatedAt.FormatAbsoluteDateTime() : this.CreatedAt.FormatRelativeDateTime()); ImGui.PopStyleColor(); @@ -799,7 +823,8 @@ internal sealed class ActiveNotification : IActiveNotification private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size) { ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - if (!this.IsMouseHovered) + var alphaPush = !this.IsHovered && !this.IsFocused; + if (alphaPush) ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); ImGui.PushStyleColor(ImGuiCol.Button, 0); ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); @@ -808,7 +833,7 @@ internal sealed class ActiveNotification : IActiveNotification var r = ImGui.Button(icon.ToIconString(), new(size)); ImGui.PopStyleColor(2); - if (!this.IsMouseHovered) + if (alphaPush) ImGui.PopStyleVar(); ImGui.PopStyleVar(); return r; @@ -912,7 +937,7 @@ internal sealed class ActiveNotification : IActiveNotification barL = midpoint - (length * v); barR = midpoint + (length * v); } - else if (this.HoverExtendDuration > TimeSpan.Zero && this.IsMouseHovered) + else if (this.DurationSinceLastInterest > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) { barL = 0f; barR = 1f; @@ -942,7 +967,7 @@ internal sealed class ActiveNotification : IActiveNotification else { barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / - (effectiveExpiry - this.HoverRelativeToTime).TotalMilliseconds); + (effectiveExpiry - this.LastInterestTime).TotalMilliseconds); barR = 1f; this.prevProgressL = barL; this.prevProgressR = barR; diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index dd1d87c42..33a3ad974 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -53,7 +53,7 @@ public sealed record Notification : INotification public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; /// - public TimeSpan HoverExtendDuration { get; set; } = NotificationConstants.DefaultHoverExtendDuration; + public TimeSpan DurationSinceLastInterest { get; set; } = NotificationConstants.DefaultHoverExtendDuration; /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; @@ -85,7 +85,7 @@ public sealed record Notification : INotification this.IconSource = copyFrom.IconSource?.Clone(); this.HardExpiry = copyFrom.HardExpiry; this.InitialDuration = copyFrom.InitialDuration; - this.HoverExtendDuration = copyFrom.HoverExtendDuration; + this.DurationSinceLastInterest = copyFrom.DurationSinceLastInterest; this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry; this.Minimized = copyFrom.Minimized; this.UserDismissable = copyFrom.UserDismissable; diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs index 08ef8aebd..d02ff47f5 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs @@ -60,6 +60,9 @@ public static class NotificationConstants /// Duration of expando animation. internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); + /// Text color for the rectangular border when the notification is focused. + internal static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); + /// Text color for the when. internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); @@ -126,6 +129,9 @@ public static class NotificationConstants /// Gets the height of the expiry progress bar. internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); + /// Gets the thickness of the focus indicator rectangle. + internal static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); + /// Gets the string format of the initiator name field, if the initiator is unloaded. internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 4d3807417..dcd193496 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -166,7 +166,7 @@ internal class ImGuiWidget : IDataWindowWidget this.notificationTemplate.InitialDurationInt == 0 ? TimeSpan.MaxValue : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], - HoverExtendDuration = + DurationSinceLastInterest = this.notificationTemplate.HoverExtendDurationInt == 0 ? TimeSpan.Zero : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], @@ -246,10 +246,12 @@ internal class ImGuiWidget : IDataWindowWidget if (this.notificationTemplate.ActionBar || !this.notificationTemplate.UserDismissable) { var nclick = 0; + var testString = "input"; + n.Click += _ => nclick++; n.DrawActions += an => { - if (ImGui.Button("Update in place")) + if (ImGui.Button("Update")) { NewRandom(out title, out type, out progress); an.Title = title; @@ -257,7 +259,10 @@ internal class ImGuiWidget : IDataWindowWidget an.Progress = progress; } - if (an.IsMouseHovered) + ImGui.SameLine(); + ImGui.InputText("##input", ref testString, 255); + + if (an.IsHovered) { ImGui.SameLine(); if (ImGui.Button("Dismiss")) From a7d53807961e1df5939a536011d7b4d7cd4b2ed5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 27 Feb 2024 23:20:08 +0900 Subject: [PATCH 551/585] Cleanup --- .../ImGuiNotification/IActiveNotification.cs | 38 +- .../ImGuiNotification/INotification.cs | 25 +- .../ImGuiNotification/INotificationIcon.cs | 54 ++ .../INotificationIconSource.cs | 88 -- .../INotificationMaterializedIcon.cs | 16 - .../Internal/ActiveNotification.ImGui.cs | 494 +++++++++++ .../Internal/ActiveNotification.cs | 818 +++--------------- .../Internal/IconSource/FilePathIconSource.cs | 49 -- .../IconSource/FontAwesomeIconIconSource.cs | 46 - .../Internal/IconSource/GamePathIconSource.cs | 50 -- .../IconSource/SeIconCharIconSource.cs | 47 - .../IconSource/TextureWrapIconSource.cs | 62 -- .../IconSource/TextureWrapTaskIconSource.cs | 71 -- .../{ => Internal}/NotificationConstants.cs | 122 ++- .../FilePathNotificationIcon.cs | 34 + .../FontAwesomeIconNotificationIcon.cs | 31 + .../GamePathNotificationIcon.cs | 34 + .../SeIconCharNotificationIcon.cs | 33 + .../Internal/NotificationManager.cs | 39 +- .../ImGuiNotification/Notification.cs | 61 +- .../NotificationUtilities.cs | 156 ++-- .../Windows/Data/Widgets/ImGuiWidget.cs | 83 +- Dalamud/Interface/UiBuilder.cs | 1 - .../Plugin/Services/INotificationManager.cs | 16 +- 24 files changed, 1056 insertions(+), 1412 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/INotificationIcon.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs rename Dalamud/Interface/ImGuiNotification/{ => Internal}/NotificationConstants.cs (51%) create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index dd4101c92..340c052cd 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,5 +1,7 @@ using System.Threading; +using Dalamud.Interface.Internal; + namespace Dalamud.Interface.ImGuiNotification; /// Represents an active notification. @@ -20,20 +22,6 @@ public interface IActiveNotification : INotification ///
    event Action Click; - /// Invoked when the mouse enters the notification window. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action MouseEnter; - - /// Invoked when the mouse leaves the notification window. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action MouseLeave; - /// Invoked upon drawing the action bar of the notification. /// /// Note that this function may be called even after has been invoked. @@ -44,16 +32,13 @@ public interface IActiveNotification : INotification /// Gets the ID of this notification. long Id { get; } + /// Gets the time of creating this notification. + DateTime CreatedAt { get; } + /// Gets the effective expiry time. /// Contains if the notification does not expire. DateTime EffectiveExpiry { get; } - /// Gets a value indicating whether the mouse cursor is on the notification window. - bool IsHovered { get; } - - /// Gets a value indicating whether the notification window is focused. - bool IsFocused { get; } - /// Gets a value indicating whether the notification has been dismissed. /// This includes when the hide animation is being played. bool IsDismissed { get; } @@ -66,9 +51,16 @@ public interface IActiveNotification : INotification /// This does not override . void ExtendBy(TimeSpan extension); - /// Loads the icon again using the same . - /// If is true, then this function is a no-op. - void UpdateIcon(); + /// Sets the icon from , overriding the icon . + /// The new texture wrap to use, or null to clear and revert back to the icon specified + /// from . + /// + /// The texture passed will be disposed when the notification is dismissed or a new different texture is set + /// via another call to this function. You do not have to dispose it yourself. + /// If is true, then calling this function will simply dispose the passed + /// without actually updating the icon. + /// + void SetIconTexture(IDalamudTextureWrap? textureWrap); /// Generates a new value to use for . /// The new value. diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 349d66f72..e6861726f 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,9 +1,10 @@ using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin.Services; namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. -public interface INotification : IDisposable +public interface INotification { /// Gets or sets the content body of the notification. string Content { get; set; } @@ -18,22 +19,13 @@ public interface INotification : IDisposable NotificationType Type { get; set; } /// Gets or sets the icon source. - /// - /// Assigning a new value that does not equal to the previous value will dispose the old value. The ownership - /// of the new value is transferred to this . Even if the assignment throws an - /// exception, the ownership is transferred, causing the value to be disposed. Assignment should not throw an - /// exception though, so wrapping the assignment in try...catch block is not required. - /// The assigned value will be disposed upon the call on this instance of - /// , unless the same value is assigned, in which case it will do nothing. - /// If this is an , then updating this property - /// will change the icon being displayed (calls ), unless - /// is true. - /// - INotificationIconSource? IconSource { get; set; } + /// Use to use a texture, after calling + /// . + INotificationIcon? Icon { get; set; } /// Gets or sets the hard expiry. /// - /// Setting this value will override and , in that + /// Setting this value will override and , in that /// the notification will be dismissed when this expiry expires.
    /// Set to to make only take effect.
    /// If neither nor is not MaxValue, then the notification @@ -45,7 +37,8 @@ public interface INotification : IDisposable /// Gets or sets the initial duration. /// Set to to make only take effect. - /// Updating this value will reset the dismiss timer. + /// Updating this value will reset the dismiss timer, but the remaining duration will still be calculated + /// based on . TimeSpan InitialDuration { get; set; } /// Gets or sets the new duration for this notification once the mouse cursor leaves the window and the @@ -55,7 +48,7 @@ public interface INotification : IDisposable /// notification or focusing on it will not make the notification stay.
    /// Updating this value will reset the dismiss timer. /// - TimeSpan DurationSinceLastInterest { get; set; } + TimeSpan ExtensionDurationSinceLastInterest { get; set; } /// Gets or sets a value indicating whether to show an indeterminate expiration animation if /// is set to . diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs new file mode 100644 index 000000000..94c746b4f --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs @@ -0,0 +1,54 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +namespace Dalamud.Interface.ImGuiNotification; + +/// Icon source for . +/// Plugins implementing this interface are left to their own on managing the resources contained by the +/// instance of their implementation of . In other words, they should not expect to have +/// called if their implementation is an . Dalamud will not +/// call on any instance of . On plugin unloads, the +/// icon may be reverted back to the default, if the instance of is not provided by +/// Dalamud. +public interface INotificationIcon +{ + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon From(SeIconChar iconChar) => new SeIconCharNotificationIcon(iconChar); + + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon From(FontAwesomeIcon iconChar) => new FontAwesomeIconNotificationIcon(iconChar); + + /// Gets a new instance of that will source the icon from a texture + /// file shipped as a part of the game resources. + /// The path to a texture file in the game virtual file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon FromGame(string gamePath) => new GamePathNotificationIcon(gamePath); + + /// Gets a new instance of that will source the icon from an image + /// file from the file system. + /// The path to an image file in the file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon FromFile(string filePath) => new FilePathNotificationIcon(filePath); + + /// Draws the icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + /// true if anything has been drawn. + bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color); +} diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs b/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs deleted file mode 100644 index 1fee67098..000000000 --- a/Dalamud/Interface/ImGuiNotification/INotificationIconSource.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Threading.Tasks; - -using Dalamud.Game.Text; -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; -using Dalamud.Interface.Internal; - -namespace Dalamud.Interface.ImGuiNotification; - -/// Icon source for . -/// Plugins should NOT implement this interface. -public interface INotificationIconSource : ICloneable, IDisposable -{ - /// The internal interface. - internal interface IInternal : INotificationIconSource - { - /// Materializes the icon resource. - /// The materialized resource. - INotificationMaterializedIcon Materialize(); - } - - /// Gets a new instance of that will source the icon from an - /// . - /// The icon character. - /// A new instance of that should be disposed after use. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(SeIconChar iconChar) => new SeIconCharIconSource(iconChar); - - /// Gets a new instance of that will source the icon from an - /// . - /// The icon character. - /// A new instance of that should be disposed after use. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(FontAwesomeIcon iconChar) => new FontAwesomeIconIconSource(iconChar); - - /// Gets a new instance of that will source the icon from an - /// . - /// The texture wrap. - /// - /// If true, this class will own the passed , and you must not call - /// on the passed wrap. - /// If false, this class will create a new reference of the passed wrap, and you should call - /// on the passed wrap. - /// In both cases, the returned object must be disposed after use. - /// A new instance of that should be disposed after use. - /// If any errors are thrown or is null, the default icon will be displayed - /// instead. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(IDalamudTextureWrap? wrap, bool takeOwnership = true) => - new TextureWrapIconSource(wrap, takeOwnership); - - /// Gets a new instance of that will source the icon from an - /// returning a resulting in an - /// . - /// The function that returns a task that results a texture wrap. - /// A new instance of that should be disposed after use. - /// If any errors are thrown or is null, the default icon will be - /// displayed instead.
    - /// Use if you will have a wrap available without waiting.
    - /// should not contain a reference to a resource; if it does, the resource will be - /// released when all instances of derived from the returned object are freed - /// by the garbage collector, which will result in non-deterministic resource releases.
    - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource From(Func?>? wrapTaskFunc) => - new TextureWrapTaskIconSource(wrapTaskFunc); - - /// Gets a new instance of that will source the icon from a texture - /// file shipped as a part of the game resources. - /// The path to a texture file in the game virtual file system. - /// A new instance of that should be disposed after use. - /// If any errors are thrown, the default icon will be displayed instead. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource FromGame(string gamePath) => new GamePathIconSource(gamePath); - - /// Gets a new instance of that will source the icon from an image - /// file from the file system. - /// The path to an image file in the file system. - /// A new instance of that should be disposed after use. - /// If any errors are thrown, the default icon will be displayed instead. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource FromFile(string filePath) => new FilePathIconSource(filePath); - - /// - new INotificationIconSource Clone(); - - /// - object ICloneable.Clone() => this.Clone(); -} diff --git a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs deleted file mode 100644 index 0657a94a4..000000000 --- a/Dalamud/Interface/ImGuiNotification/INotificationMaterializedIcon.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Numerics; - -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification; - -/// Represents a materialized icon. -internal interface INotificationMaterializedIcon : IDisposable -{ - /// Draws the icon. - /// The coordinates of the top left of the icon area. - /// The coordinates of the bottom right of the icon area. - /// The foreground color. - /// The initiator plugin. - void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin); -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs new file mode 100644 index 000000000..99b924923 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -0,0 +1,494 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Represents an active notification. +internal sealed partial class ActiveNotification +{ + /// Draws this notification. + /// The maximum width of the notification window. + /// The offset from the bottom. + /// The height of the notification. + public float Draw(float width, float offsetY) + { + var opacity = + Math.Clamp( + (float)(this.hideEasing.IsRunning + ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) + : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), + 0f, + 1f); + if (opacity <= 0) + return 0; + + var actionWindowHeight = + // Content + ImGui.GetTextLineHeight() + + // Top and bottom padding + (NotificationConstants.ScaledWindowPadding * 2); + + var viewport = ImGuiHelpers.MainViewport; + var viewportPos = viewport.WorkPos; + var viewportSize = viewport.WorkSize; + + ImGui.PushID(this.Id.GetHashCode()); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); + unsafe + { + ImGui.PushStyleColor( + ImGuiCol.WindowBg, + *ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4( + 1f, + 1f, + 1f, + NotificationConstants.BackgroundOpacity)); + } + + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints( + new(width, actionWindowHeight), + new( + width, + !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning + ? float.MaxValue + : actionWindowHeight)); + ImGui.Begin( + $"##NotifyMainWindow{this.Id}", + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoDocking); + + var isTakingKeyboardInput = ImGui.IsWindowFocused() && ImGui.GetIO().WantTextInput; + var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + var warrantsExtension = + this.ExtensionDurationSinceLastInterest > TimeSpan.Zero + && (isHovered || isTakingKeyboardInput); + + this.EffectiveExpiry = this.CalculateEffectiveExpiry(ref warrantsExtension); + + if (!this.IsDismissed && DateTime.Now > this.EffectiveExpiry) + this.DismissNow(NotificationDismissReason.Timeout); + + if (this.ExtensionDurationSinceLastInterest > TimeSpan.Zero && warrantsExtension) + this.lastInterestTime = DateTime.Now; + + this.DrawWindowBackgroundProgressBar(); + this.DrawTopBar(width, actionWindowHeight, isHovered); + if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) + { + this.DrawContentArea(width, actionWindowHeight); + } + else if (this.expandoEasing.IsRunning) + { + if (this.underlyingNotification.Minimized) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); + else + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); + this.DrawContentArea(width, actionWindowHeight); + ImGui.PopStyleVar(); + } + + if (isTakingKeyboardInput) + this.DrawKeyboardInputIndicator(); + this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension); + + if (ImGui.IsWindowHovered()) + { + if (this.Click is null) + { + if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + this.DismissNow(NotificationDismissReason.Manual); + } + else + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) + || ImGui.IsMouseClicked(ImGuiMouseButton.Right) + || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + this.Click.InvokeSafely(this); + } + } + + var windowSize = ImGui.GetWindowSize(); + ImGui.End(); + + ImGui.PopStyleColor(); + ImGui.PopStyleVar(3); + ImGui.PopID(); + + return windowSize.Y; + } + + /// Calculates the effective expiry, taking ImGui window state into account. + /// Notification will not dismiss while this paramter is true. + /// The calculated effective expiry. + /// Expected to be called BETWEEN and . + private DateTime CalculateEffectiveExpiry(ref bool warrantsExtension) + { + DateTime expiry; + var initialDuration = this.InitialDuration; + var expiryInitial = + initialDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.CreatedAt + initialDuration; + + var extendDuration = this.ExtensionDurationSinceLastInterest; + if (warrantsExtension) + { + expiry = DateTime.MaxValue; + } + else + { + var expiryExtend = + extendDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.lastInterestTime + extendDuration; + + expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; + if (expiry < this.extendedExpiry) + expiry = this.extendedExpiry; + } + + var he = this.HardExpiry; + if (he < expiry) + { + expiry = he; + warrantsExtension = false; + } + + return expiry; + } + + private void DrawWindowBackgroundProgressBar() + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.ProgressWaveLoopDuration) / + NotificationConstants.ProgressWaveLoopDuration); + elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; + + var colorElapsed = + elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / + NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; + + elapsed = Math.Clamp(elapsed, 0f, 1f); + colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); + colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); + + var progress = Math.Clamp(this.ProgressEased, 0f, 1f); + if (progress >= 1f) + elapsed = colorElapsed = 0f; + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var rb = windowPos + windowSize; + var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed }; + var rp = windowPos + windowSize with { X = windowSize.X * progress }; + + ImGui.PushClipRect(windowPos, rb, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos, + midp, + ImGui.GetColorU32( + Vector4.Lerp( + NotificationConstants.BackgroundProgressColorMin, + NotificationConstants.BackgroundProgressColorMax, + colorElapsed))); + ImGui.GetWindowDrawList().AddRectFilled( + midp with { Y = 0 }, + rp, + ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin)); + ImGui.PopClipRect(); + } + + private void DrawKeyboardInputIndicator() + { + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRect( + windowPos, + windowPos + windowSize, + ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)), + 0f, + ImDrawFlags.None, + NotificationConstants.FocusIndicatorThickness); + ImGui.PopClipRect(); + } + + private void DrawTopBar(float width, float height, bool drawActionButtons) + { + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + + var rtOffset = new Vector2(width, 0); + using (Service.Get().IconFontHandle?.Push()) + { + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + if (this.UserDismissable) + { + if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons)) + this.DismissNow(NotificationDismissReason.Manual); + rtOffset.X -= height; + } + + if (this.underlyingNotification.Minimized) + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height, drawActionButtons)) + this.Minimized = false; + } + else + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height, drawActionButtons)) + this.Minimized = true; + } + + rtOffset.X -= height; + ImGui.PopClipRect(); + } + + float relativeOpacity; + if (this.expandoEasing.IsRunning) + { + relativeOpacity = + this.underlyingNotification.Minimized + ? 1f - (float)this.expandoEasing.Value + : (float)this.expandoEasing.Value; + } + else + { + relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; + } + + if (drawActionButtons) + ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); + else + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + + if (relativeOpacity > 0) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity); + ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted( + ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) + ? this.CreatedAt.FormatAbsoluteDateTime() + : this.CreatedAt.FormatRelativeDateTime()); + ImGui.PopStyleColor(); + ImGui.PopStyleVar(); + } + + if (relativeOpacity < 1) + { + rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); + + var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); + this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); + + ltOffset.X = height; + + var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); + var agoSize = ImGui.CalcTextSize(agoText); + rtOffset.X -= agoSize.X; + ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted(agoText); + ImGui.PopStyleColor(); + + rtOffset.X -= NotificationConstants.ScaledWindowPadding; + + ImGui.PushClipRect( + windowPos + ltOffset with { Y = 0 }, + windowPos + rtOffset with { Y = height }, + true); + ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.TextUnformatted(this.EffectiveMinimizedText); + ImGui.PopClipRect(); + + ImGui.PopStyleVar(); + } + + ImGui.PopClipRect(); + } + + private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size, bool drawActionButtons) + { + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + if (!drawActionButtons) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + ImGui.SetCursorPos(rt - new Vector2(size, 0)); + var r = ImGui.Button(icon.ToIconString(), new(size)); + + ImGui.PopStyleColor(2); + if (!drawActionButtons) + ImGui.PopStyleVar(); + ImGui.PopStyleVar(); + return r; + } + + private void DrawContentArea(float width, float actionWindowHeight) + { + var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; + var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; + var textColumnOffset = new Vector2(textColumnX, actionWindowHeight); + + this.DrawIcon( + new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), + new(NotificationConstants.ScaledIconSize)); + + textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth); + textColumnOffset.Y += NotificationConstants.ScaledComponentGap; + + this.DrawContentBody(textColumnOffset, textColumnWidth); + } + + private void DrawIcon(Vector2 minCoord, Vector2 size) + { + var maxCoord = minCoord + size; + var iconColor = this.Type.ToColor(); + + if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.iconTextureWrap)) + return; + + if (this.Icon?.DrawIcon(minCoord, maxCoord, iconColor) is true) + return; + + if (NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + this.Type.ToChar(), + Service.Get().IconFontAwesomeFontHandle, + iconColor)) + return; + + if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.initiatorPlugin)) + return; + + NotificationUtilities.DrawIconFromDalamudLogo(minCoord, maxCoord); + } + + private float DrawTitle(Vector2 minCoord, float width) + { + ImGui.PushTextWrapPos(minCoord.X + width); + + ImGui.SetCursorPos(minCoord); + if ((this.Title ?? this.Type.ToTitle()) is { } title) + { + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); + ImGui.TextUnformatted(title); + ImGui.PopStyleColor(); + } + + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); + ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); + ImGui.TextUnformatted(this.InitiatorString); + ImGui.PopStyleColor(); + + ImGui.PopTextWrapPos(); + return ImGui.GetCursorPosY() - minCoord.Y; + } + + private void DrawContentBody(Vector2 minCoord, float width) + { + ImGui.SetCursorPos(minCoord); + ImGui.PushTextWrapPos(minCoord.X + width); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); + ImGui.TextUnformatted(this.Content); + ImGui.PopStyleColor(); + ImGui.PopTextWrapPos(); + if (this.DrawActions is not null) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + try + { + this.DrawActions.Invoke(this); + } + catch + { + // ignore + } + } + } + + private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) + { + float barL, barR; + if (this.IsDismissed) + { + var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; + var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; + var length = (this.prevProgressR - this.prevProgressL) / 2f; + barL = midpoint - (length * v); + barR = midpoint + (length * v); + } + else if (warrantsExtension) + { + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else if (effectiveExpiry == DateTime.MaxValue) + { + if (this.ShowIndeterminateIfNoExpiry) + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.IndeterminateProgressbarLoopDuration) / + NotificationConstants.IndeterminateProgressbarLoopDuration); + barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); + barR = Math.Min(elapsed, 2f / 3) / (2f / 3); + barL = MathF.Pow(barL, 3); + barR = 1f - MathF.Pow(1f - barR, 3); + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else + { + this.prevProgressL = barL = 0f; + this.prevProgressR = barR = 1f; + } + } + else + { + barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / + (effectiveExpiry - this.lastInterestTime).TotalMilliseconds); + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + + barR = Math.Clamp(barR, 0f, 1f); + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos + new Vector2( + windowSize.X * barL, + windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), + windowPos + windowSize with { X = windowSize.X * barR }, + ImGui.GetColorU32(this.Type.ToColor())); + ImGui.PopClipRect(); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 8591695a6..357752f6e 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -1,24 +1,21 @@ using System.Numerics; using System.Runtime.Loader; +using System.Threading; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; -using Dalamud.Interface.ImGuiNotification.Internal.IconSource; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; -using Dalamud.Interface.Utility; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; -using ImGuiNET; - using Serilog; namespace Dalamud.Interface.ImGuiNotification.Internal; /// Represents an active notification. -internal sealed class ActiveNotification : IActiveNotification +internal sealed partial class ActiveNotification : IActiveNotification { private readonly Notification underlyingNotification; @@ -27,6 +24,21 @@ internal sealed class ActiveNotification : IActiveNotification private readonly Easing progressEasing; private readonly Easing expandoEasing; + /// Gets the time of starting to count the timer for the expiration. + private DateTime lastInterestTime; + + /// Gets the extended expiration time from . + private DateTime extendedExpiry; + + /// The icon texture to use if specified; otherwise, icon will be used from . + private IDalamudTextureWrap? iconTextureWrap; + + /// The plugin that initiated this notification. + private LocalPlugin? initiatorPlugin; + + /// Whether has been unloaded. + private bool isInitiatorUnloaded; + /// The progress before for the progress bar animation with . private float progressBefore; @@ -36,10 +48,10 @@ internal sealed class ActiveNotification : IActiveNotification /// Used for calculating correct dismissal progressbar animation (right edge). private float prevProgressR; - /// New progress value to be updated on next call to . + /// New progress value to be updated on next call to . private float? newProgress; - /// New minimized value to be updated on next call to . + /// New minimized value to be updated on next call to . private bool? newMinimized; /// Initializes a new instance of the class. @@ -47,28 +59,16 @@ internal sealed class ActiveNotification : IActiveNotification /// The initiator plugin. Use null if originated by Dalamud. public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) { - this.underlyingNotification = underlyingNotification with - { - IconSource = underlyingNotification.IconSource?.Clone(), - }; - this.InitiatorPlugin = initiatorPlugin; + this.underlyingNotification = underlyingNotification with { }; + this.initiatorPlugin = initiatorPlugin; this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration); this.expandoEasing = new InOutCubic(NotificationConstants.ExpandoAnimationDuration); + this.CreatedAt = this.lastInterestTime = this.extendedExpiry = DateTime.Now; this.showEasing.Start(); this.progressEasing.Start(); - try - { - this.UpdateIcon(); - } - catch (Exception e) - { - // Ignore the one caused from ctor only; other UpdateIcon calls are from plugins, and they should handle the - // error accordingly. - Log.Error(e, $"{nameof(ActiveNotification)}#{this.Id} ctor: {nameof(this.UpdateIcon)} failed and ignored."); - } } /// @@ -80,23 +80,11 @@ internal sealed class ActiveNotification : IActiveNotification /// public event Action? DrawActions; - /// - public event Action? MouseEnter; - - /// - public event Action? MouseLeave; - /// public long Id { get; } = IActiveNotification.CreateNewId(); - /// Gets the time of creating this notification. - public DateTime CreatedAt { get; } = DateTime.Now; - - /// Gets the time of starting to count the timer for the expiration. - public DateTime LastInterestTime { get; private set; } = DateTime.Now; - - /// Gets the extended expiration time from . - public DateTime ExtendedExpiry { get; private set; } = DateTime.Now; + /// + public DateTime CreatedAt { get; } /// public string Content @@ -147,19 +135,14 @@ internal sealed class ActiveNotification : IActiveNotification } /// - public INotificationIconSource? IconSource + public INotificationIcon? Icon { - get => this.underlyingNotification.IconSource; + get => this.underlyingNotification.Icon; set { if (this.IsDismissed) - { - value?.Dispose(); return; - } - - this.underlyingNotification.IconSource = value; - this.UpdateIcon(); + this.underlyingNotification.Icon = value; } } @@ -172,7 +155,7 @@ internal sealed class ActiveNotification : IActiveNotification if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) return; this.underlyingNotification.HardExpiry = value; - this.LastInterestTime = DateTime.Now; + this.lastInterestTime = DateTime.Now; } } @@ -185,58 +168,25 @@ internal sealed class ActiveNotification : IActiveNotification if (this.IsDismissed) return; this.underlyingNotification.InitialDuration = value; - this.LastInterestTime = DateTime.Now; + this.lastInterestTime = DateTime.Now; } } /// - public TimeSpan DurationSinceLastInterest + public TimeSpan ExtensionDurationSinceLastInterest { - get => this.underlyingNotification.DurationSinceLastInterest; + get => this.underlyingNotification.ExtensionDurationSinceLastInterest; set { if (this.IsDismissed) return; - this.underlyingNotification.DurationSinceLastInterest = value; - this.LastInterestTime = DateTime.Now; + this.underlyingNotification.ExtensionDurationSinceLastInterest = value; + this.lastInterestTime = DateTime.Now; } } /// - public DateTime EffectiveExpiry - { - get - { - var initialDuration = this.InitialDuration; - var expiryInitial = - initialDuration == TimeSpan.MaxValue - ? DateTime.MaxValue - : this.CreatedAt + initialDuration; - - DateTime expiry; - var hoverExtendDuration = this.DurationSinceLastInterest; - if (hoverExtendDuration > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) - { - expiry = DateTime.MaxValue; - } - else - { - var expiryExtend = - hoverExtendDuration == TimeSpan.MaxValue - ? DateTime.MaxValue - : this.LastInterestTime + hoverExtendDuration; - - expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; - if (expiry < this.ExtendedExpiry) - expiry = this.ExtendedExpiry; - } - - var he = this.HardExpiry; - if (he < expiry) - expiry = he; - return expiry; - } - } + public DateTime EffectiveExpiry { get; private set; } /// public bool ShowIndeterminateIfNoExpiry @@ -286,24 +236,9 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// - public bool IsHovered { get; private set; } - - /// - public bool IsFocused { get; private set; } - /// public bool IsDismissed => this.hideEasing.IsRunning; - /// Gets a value indicating whether has been unloaded. - public bool IsInitiatorUnloaded { get; private set; } - - /// Gets or sets the plugin that initiated this notification. - public LocalPlugin? InitiatorPlugin { get; set; } - - /// Gets or sets the icon of this notification. - public INotificationMaterializedIcon? MaterializedIcon { get; set; } - /// Gets the eased progress. private float ProgressEased { @@ -318,61 +253,17 @@ internal sealed class ActiveNotification : IActiveNotification } } - /// Gets the default color of the notification. - private Vector4 DefaultIconColor => this.Type switch - { - NotificationType.None => ImGuiColors.DalamudWhite, - NotificationType.Success => ImGuiColors.HealerGreen, - NotificationType.Warning => ImGuiColors.DalamudOrange, - NotificationType.Error => ImGuiColors.DalamudRed, - NotificationType.Info => ImGuiColors.TankBlue, - _ => ImGuiColors.DalamudWhite, - }; - - /// Gets the default icon of the notification. - private char? DefaultIconChar => this.Type switch - { - NotificationType.None => null, - NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), - NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), - NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), - NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), - _ => null, - }; - - /// Gets the default title of the notification. - private string? DefaultTitle => this.Type switch - { - NotificationType.None => null, - NotificationType.Success => NotificationType.Success.ToString(), - NotificationType.Warning => NotificationType.Warning.ToString(), - NotificationType.Error => NotificationType.Error.ToString(), - NotificationType.Info => NotificationType.Info.ToString(), - _ => null, - }; - /// Gets the string for the initiator field. private string InitiatorString => - this.InitiatorPlugin is not { } initiatorPlugin + this.initiatorPlugin is not { } plugin ? NotificationConstants.DefaultInitiator - : this.IsInitiatorUnloaded - ? NotificationConstants.UnloadedInitiatorNameFormat.Format(initiatorPlugin.Name) - : initiatorPlugin.Name; + : this.isInitiatorUnloaded + ? NotificationConstants.UnloadedInitiatorNameFormat.Format(plugin.Name) + : plugin.Name; /// Gets the effective text to display when minimized. private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" "); - /// - public void Dispose() - { - this.ClearMaterializedIcon(); - this.underlyingNotification.Dispose(); - this.Dismiss = null; - this.Click = null; - this.DrawActions = null; - this.InitiatorPlugin = null; - } - /// public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); @@ -392,13 +283,78 @@ internal sealed class ActiveNotification : IActiveNotification { Log.Error( e, - $"{nameof(this.Dismiss)} error; notification is owned by {this.InitiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); + $"{nameof(this.Dismiss)} error; notification is owned by {this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); } } - /// Updates animations. - /// true if the notification is over. - public bool UpdateAnimations() + /// + public void ExtendBy(TimeSpan extension) + { + var newExpiry = DateTime.Now + extension; + if (this.extendedExpiry < newExpiry) + this.extendedExpiry = newExpiry; + } + + /// + public void SetIconTexture(IDalamudTextureWrap? textureWrap) + { + if (this.IsDismissed) + { + textureWrap?.Dispose(); + return; + } + + // After replacing, if the old texture is not the old texture, then dispose the old texture. + if (Interlocked.Exchange(ref this.iconTextureWrap, textureWrap) is { } wrapToDispose && + wrapToDispose != textureWrap) + { + wrapToDispose.Dispose(); + } + } + + /// Removes non-Dalamud invocation targets from events. + internal void RemoveNonDalamudInvocations() + { + var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); + this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss); + this.Click = RemoveNonDalamudInvocationsCore(this.Click); + this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions); + + if (this.Icon is { } previousIcon && !IsOwnedByDalamud(previousIcon.GetType())) + this.Icon = null; + + this.isInitiatorUnloaded = true; + this.UserDismissable = true; + this.ExtensionDurationSinceLastInterest = NotificationConstants.DefaultDuration; + + var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDuration; + if (this.EffectiveExpiry > newMaxExpiry) + this.HardExpiry = newMaxExpiry; + + return; + + bool IsOwnedByDalamud(Type t) => AssemblyLoadContext.GetLoadContext(t.Assembly) == dalamudContext; + + T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate + { + if (@delegate is null) + return null; + + foreach (var il in @delegate.GetInvocationList()) + { + if (il.Target is { } target && !IsOwnedByDalamud(target.GetType())) + @delegate = (T)Delegate.Remove(@delegate, il); + } + + return @delegate; + } + } + + /// Updates the state of this notification, and release the relevant resource if this notification is no + /// longer in use. + /// true if the notification is over and relevant resources are released. + /// Intended to be called from the main thread only. + internal bool UpdateOrDisposeInternal() { this.showEasing.Update(); this.hideEasing.Update(); @@ -435,555 +391,21 @@ internal sealed class ActiveNotification : IActiveNotification this.newMinimized = null; } - return this.hideEasing.IsRunning && this.hideEasing.IsDone; + if (!this.hideEasing.IsRunning || !this.hideEasing.IsDone) + return false; + + this.DisposeInternal(); + return true; } - /// Draws this notification. - /// The maximum width of the notification window. - /// The offset from the bottom. - /// The height of the notification. - public float Draw(float maxWidth, float offsetY) + /// Clears the resources associated with this instance of . + internal void DisposeInternal() { - var effectiveExpiry = this.EffectiveExpiry; - if (!this.IsDismissed && DateTime.Now > effectiveExpiry) - this.DismissNow(NotificationDismissReason.Timeout); - - var opacity = - Math.Clamp( - (float)(this.hideEasing.IsRunning - ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) - : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), - 0f, - 1f); - if (opacity <= 0) - return 0; - - var interfaceManager = Service.Get(); - var unboundedWidth = ImGui.CalcTextSize(this.Content).X; - float closeButtonHorizontalSpaceReservation; - using (interfaceManager.IconFontHandle?.Push()) - { - closeButtonHorizontalSpaceReservation = ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X; - closeButtonHorizontalSpaceReservation += NotificationConstants.ScaledWindowPadding; - } - - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.Title ?? this.DefaultTitle ?? string.Empty).X); - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.InitiatorString).X); - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.CreatedAt.FormatAbsoluteDateTime()).X + closeButtonHorizontalSpaceReservation); - unboundedWidth = Math.Max( - unboundedWidth, - ImGui.CalcTextSize(this.CreatedAt.FormatRelativeDateTime()).X + closeButtonHorizontalSpaceReservation); - - unboundedWidth += NotificationConstants.ScaledWindowPadding * 3; - unboundedWidth += NotificationConstants.ScaledIconSize; - - var actionWindowHeight = - // Content - ImGui.GetTextLineHeight() + - // Top and bottom padding - (NotificationConstants.ScaledWindowPadding * 2); - - var width = Math.Min(maxWidth, unboundedWidth); - - var viewport = ImGuiHelpers.MainViewport; - var viewportPos = viewport.WorkPos; - var viewportSize = viewport.WorkSize; - - ImGui.PushID(this.Id.GetHashCode()); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); - ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); - ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); - unsafe - { - ImGui.PushStyleColor( - ImGuiCol.WindowBg, - *ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4( - 1f, - 1f, - 1f, - NotificationConstants.BackgroundOpacity)); - } - - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), - ImGuiCond.Always, - Vector2.One); - ImGui.SetNextWindowSizeConstraints( - new(width, actionWindowHeight), - new( - width, - !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning - ? float.MaxValue - : actionWindowHeight)); - ImGui.Begin( - $"##NotifyMainWindow{this.Id}", - ImGuiWindowFlags.AlwaysAutoResize | - ImGuiWindowFlags.NoDecoration | - ImGuiWindowFlags.NoNav | - ImGuiWindowFlags.NoMove | - ImGuiWindowFlags.NoFocusOnAppearing | - ImGuiWindowFlags.NoDocking); - this.IsFocused = ImGui.IsWindowFocused(); - if (this.IsFocused) - this.LastInterestTime = DateTime.Now; - - this.DrawWindowBackgroundProgressBar(); - this.DrawFocusIndicator(); - this.DrawTopBar(interfaceManager, width, actionWindowHeight); - if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) - { - this.DrawContentArea(width, actionWindowHeight); - } - else if (this.expandoEasing.IsRunning) - { - if (this.underlyingNotification.Minimized) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); - else - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); - this.DrawContentArea(width, actionWindowHeight); - ImGui.PopStyleVar(); - } - - this.DrawExpiryBar(effectiveExpiry); - - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - var hovered = ImGui.IsWindowHovered(); - ImGui.End(); - - ImGui.PopStyleColor(); - ImGui.PopStyleVar(3); - ImGui.PopID(); - - if (windowPos.X <= ImGui.GetIO().MousePos.X - && windowPos.Y <= ImGui.GetIO().MousePos.Y - && ImGui.GetIO().MousePos.X < windowPos.X + windowSize.X - && ImGui.GetIO().MousePos.Y < windowPos.Y + windowSize.Y) - { - if (!this.IsHovered) - { - this.IsHovered = true; - this.MouseEnter.InvokeSafely(this); - } - - if (this.DurationSinceLastInterest > TimeSpan.Zero) - this.LastInterestTime = DateTime.Now; - - if (hovered) - { - if (this.Click is null) - { - if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) - this.DismissNow(NotificationDismissReason.Manual); - } - else - { - if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) - || ImGui.IsMouseClicked(ImGuiMouseButton.Right) - || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) - this.Click.InvokeSafely(this); - } - } - } - else if (this.IsHovered) - { - this.IsHovered = false; - this.MouseLeave.InvokeSafely(this); - } - - return windowSize.Y; - } - - /// - public void ExtendBy(TimeSpan extension) - { - var newExpiry = DateTime.Now + extension; - if (this.ExtendedExpiry < newExpiry) - this.ExtendedExpiry = newExpiry; - } - - /// - public void UpdateIcon() - { - if (this.IsDismissed) - return; - this.ClearMaterializedIcon(); - this.MaterializedIcon = (this.IconSource as INotificationIconSource.IInternal)?.Materialize(); - } - - /// Removes non-Dalamud invocation targets from events. - public void RemoveNonDalamudInvocations() - { - var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); - this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss); - this.Click = RemoveNonDalamudInvocationsCore(this.Click); - this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions); - this.MouseEnter = RemoveNonDalamudInvocationsCore(this.MouseEnter); - this.MouseLeave = RemoveNonDalamudInvocationsCore(this.MouseLeave); - - this.IsInitiatorUnloaded = true; - this.UserDismissable = true; - this.DurationSinceLastInterest = NotificationConstants.DefaultHoverExtendDuration; - - var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDisplayDuration; - if (this.EffectiveExpiry > newMaxExpiry) - this.HardExpiry = newMaxExpiry; - - return; - - T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate - { - if (@delegate is null) - return null; - - foreach (var il in @delegate.GetInvocationList()) - { - if (il.Target is { } target && - AssemblyLoadContext.GetLoadContext(target.GetType().Assembly) != dalamudContext) - { - @delegate = (T)Delegate.Remove(@delegate, il); - } - } - - return @delegate; - } - } - - private void ClearMaterializedIcon() - { - this.MaterializedIcon?.Dispose(); - this.MaterializedIcon = null; - } - - private void DrawWindowBackgroundProgressBar() - { - var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % - NotificationConstants.ProgressWaveLoopDuration) / - NotificationConstants.ProgressWaveLoopDuration); - elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; - - var colorElapsed = - elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio - ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio - : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / - NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; - - elapsed = Math.Clamp(elapsed, 0f, 1f); - colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); - colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); - - var progress = Math.Clamp(this.ProgressEased, 0f, 1f); - if (progress >= 1f) - elapsed = colorElapsed = 0f; - - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - var rb = windowPos + windowSize; - var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed }; - var rp = windowPos + windowSize with { X = windowSize.X * progress }; - - ImGui.PushClipRect(windowPos, rb, false); - ImGui.GetWindowDrawList().AddRectFilled( - windowPos, - midp, - ImGui.GetColorU32( - Vector4.Lerp( - NotificationConstants.BackgroundProgressColorMin, - NotificationConstants.BackgroundProgressColorMax, - colorElapsed))); - ImGui.GetWindowDrawList().AddRectFilled( - midp with { Y = 0 }, - rp, - ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin)); - ImGui.PopClipRect(); - } - - private void DrawFocusIndicator() - { - if (!this.IsFocused) - return; - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - ImGui.PushClipRect(windowPos, windowPos + windowSize, false); - ImGui.GetWindowDrawList().AddRect( - windowPos, - windowPos + windowSize, - ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)), - 0f, - ImDrawFlags.None, - NotificationConstants.FocusIndicatorThickness); - ImGui.PopClipRect(); - } - - private void DrawTopBar(InterfaceManager interfaceManager, float width, float height) - { - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - - var rtOffset = new Vector2(width, 0); - using (interfaceManager.IconFontHandle?.Push()) - { - ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); - if (this.UserDismissable) - { - if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height)) - this.DismissNow(NotificationDismissReason.Manual); - rtOffset.X -= height; - } - - if (this.underlyingNotification.Minimized) - { - if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height)) - this.Minimized = false; - } - else - { - if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height)) - this.Minimized = true; - } - - rtOffset.X -= height; - ImGui.PopClipRect(); - } - - float relativeOpacity; - if (this.expandoEasing.IsRunning) - { - relativeOpacity = - this.underlyingNotification.Minimized - ? 1f - (float)this.expandoEasing.Value - : (float)this.expandoEasing.Value; - } - else - { - relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; - } - - if (this.IsHovered || this.IsFocused) - ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); - else - ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); - - if (relativeOpacity > 0) - { - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity); - ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); - ImGui.TextUnformatted( - this.IsHovered || this.IsFocused - ? this.CreatedAt.FormatAbsoluteDateTime() - : this.CreatedAt.FormatRelativeDateTime()); - ImGui.PopStyleColor(); - ImGui.PopStyleVar(); - } - - if (relativeOpacity < 1) - { - rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); - - var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); - this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); - - ltOffset.X = height; - - var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); - var agoSize = ImGui.CalcTextSize(agoText); - rtOffset.X -= agoSize.X; - ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); - ImGui.TextUnformatted(agoText); - ImGui.PopStyleColor(); - - rtOffset.X -= NotificationConstants.ScaledWindowPadding; - - ImGui.PushClipRect( - windowPos + ltOffset with { Y = 0 }, - windowPos + rtOffset with { Y = height }, - true); - ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); - ImGui.TextUnformatted(this.EffectiveMinimizedText); - ImGui.PopClipRect(); - - ImGui.PopStyleVar(); - } - - ImGui.PopClipRect(); - } - - private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size) - { - ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); - var alphaPush = !this.IsHovered && !this.IsFocused; - if (alphaPush) - ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); - ImGui.PushStyleColor(ImGuiCol.Button, 0); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); - - ImGui.SetCursorPos(rt - new Vector2(size, 0)); - var r = ImGui.Button(icon.ToIconString(), new(size)); - - ImGui.PopStyleColor(2); - if (alphaPush) - ImGui.PopStyleVar(); - ImGui.PopStyleVar(); - return r; - } - - private void DrawContentArea(float width, float actionWindowHeight) - { - var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; - var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; - var textColumnOffset = new Vector2(textColumnX, actionWindowHeight); - - this.DrawIcon( - new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), - new(NotificationConstants.ScaledIconSize)); - - textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth); - textColumnOffset.Y += NotificationConstants.ScaledComponentGap; - - this.DrawContentBody(textColumnOffset, textColumnWidth); - } - - private void DrawIcon(Vector2 minCoord, Vector2 size) - { - var maxCoord = minCoord + size; - if (this.MaterializedIcon is not null) - { - this.MaterializedIcon.DrawIcon(minCoord, maxCoord, this.DefaultIconColor, this.InitiatorPlugin); - return; - } - - var defaultIconChar = this.DefaultIconChar; - if (defaultIconChar is not null) - { - NotificationUtilities.DrawIconString( - Service.Get().IconFontAwesomeFontHandle, - defaultIconChar.Value, - minCoord, - maxCoord, - this.DefaultIconColor); - return; - } - - TextureWrapTaskIconSource.DefaultMaterializedIcon.DrawIcon( - minCoord, - maxCoord, - this.DefaultIconColor, - this.InitiatorPlugin); - } - - private float DrawTitle(Vector2 minCoord, float width) - { - ImGui.PushTextWrapPos(minCoord.X + width); - - ImGui.SetCursorPos(minCoord); - if ((this.Title ?? this.DefaultTitle) is { } title) - { - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); - ImGui.TextUnformatted(title); - ImGui.PopStyleColor(); - } - - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); - ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); - ImGui.TextUnformatted(this.InitiatorString); - ImGui.PopStyleColor(); - - ImGui.PopTextWrapPos(); - return ImGui.GetCursorPosY() - minCoord.Y; - } - - private void DrawContentBody(Vector2 minCoord, float width) - { - ImGui.SetCursorPos(minCoord); - ImGui.PushTextWrapPos(minCoord.X + width); - ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); - ImGui.TextUnformatted(this.Content); - ImGui.PopStyleColor(); - ImGui.PopTextWrapPos(); - if (this.DrawActions is not null) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - try - { - this.DrawActions.Invoke(this); - } - catch - { - // ignore - } - } - } - - private void DrawExpiryBar(DateTime effectiveExpiry) - { - float barL, barR; - if (this.IsDismissed) - { - var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; - var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; - var length = (this.prevProgressR - this.prevProgressL) / 2f; - barL = midpoint - (length * v); - barR = midpoint + (length * v); - } - else if (this.DurationSinceLastInterest > TimeSpan.Zero && (this.IsHovered || this.IsFocused)) - { - barL = 0f; - barR = 1f; - this.prevProgressL = barL; - this.prevProgressR = barR; - } - else if (effectiveExpiry == DateTime.MaxValue) - { - if (this.ShowIndeterminateIfNoExpiry) - { - var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % - NotificationConstants.IndeterminateProgressbarLoopDuration) / - NotificationConstants.IndeterminateProgressbarLoopDuration); - barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); - barR = Math.Min(elapsed, 2f / 3) / (2f / 3); - barL = MathF.Pow(barL, 3); - barR = 1f - MathF.Pow(1f - barR, 3); - this.prevProgressL = barL; - this.prevProgressR = barR; - } - else - { - this.prevProgressL = barL = 0f; - this.prevProgressR = barR = 1f; - } - } - else - { - barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / - (effectiveExpiry - this.LastInterestTime).TotalMilliseconds); - barR = 1f; - this.prevProgressL = barL; - this.prevProgressR = barR; - } - - barR = Math.Clamp(barR, 0f, 1f); - - var windowPos = ImGui.GetWindowPos(); - var windowSize = ImGui.GetWindowSize(); - ImGui.PushClipRect(windowPos, windowPos + windowSize, false); - ImGui.GetWindowDrawList().AddRectFilled( - windowPos + new Vector2( - windowSize.X * barL, - windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), - windowPos + windowSize with { X = windowSize.X * barR }, - ImGui.GetColorU32(this.DefaultIconColor)); - ImGui.PopClipRect(); + if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapToDispose) + wrapToDispose.Dispose(); + this.Dismiss = null; + this.Click = null; + this.DrawActions = null; + this.initiatorPlugin = null; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs deleted file mode 100644 index a741931a5..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FilePathIconSource.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.IO; -using System.Numerics; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of a texture from a file as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class FilePathIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The path to a .tex file inside the game resources. - public FilePathIconSource(string filePath) => this.FilePath = filePath; - - /// Gets the path to a .tex file inside the game resources. - public string FilePath { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.FilePath); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly FileInfo fileInfo; - - public MaterializedIcon(string filePath) => this.fileInfo = new(filePath); - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - Service.Get().GetTextureFromFile(this.fileInfo), - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs deleted file mode 100644 index cfe790851..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/FontAwesomeIconIconSource.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Numerics; - -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of as the icon of a notification. -internal class FontAwesomeIconIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The character. - public FontAwesomeIconIconSource(FontAwesomeIcon iconChar) => this.IconChar = iconChar; - - /// Gets the icon character. - public FontAwesomeIcon IconChar { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly char iconChar; - - public MaterializedIcon(FontAwesomeIcon c) => this.iconChar = c.ToIconChar(); - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawIconString( - Service.Get().IconFontAwesomeFontHandle, - this.iconChar, - minCoord, - maxCoord, - color); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs deleted file mode 100644 index 974e60ee7..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/GamePathIconSource.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Numerics; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Plugin.Services; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of a game-shipped texture as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class GamePathIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The path to a .tex file inside the game resources. - /// Use to get the game path from icon IDs. - public GamePathIconSource(string gamePath) => this.GamePath = gamePath; - - /// Gets the path to a .tex file inside the game resources. - public string GamePath { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.GamePath); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly string gamePath; - - public MaterializedIcon(string gamePath) => this.gamePath = gamePath; - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - Service.Get().GetTextureFromGame(this.gamePath), - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs deleted file mode 100644 index 19fe8e948..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/SeIconCharIconSource.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Numerics; - -using Dalamud.Game.Text; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of as the icon of a notification. -internal class SeIconCharIconSource : INotificationIconSource.IInternal -{ - /// Initializes a new instance of the class. - /// The character. - public SeIconCharIconSource(SeIconChar c) => this.IconChar = c; - - /// Gets the icon character. - public SeIconChar IconChar { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => new MaterializedIcon(this.IconChar); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private readonly char iconChar; - - public MaterializedIcon(SeIconChar c) => this.iconChar = c.ToIconChar(); - - public void Dispose() - { - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawIconString( - Service.Get().IconAxisFontHandle, - this.iconChar, - minCoord, - maxCoord, - color); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs deleted file mode 100644 index a10b09bce..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapIconSource.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.Numerics; -using System.Threading; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of future as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class TextureWrapIconSource : INotificationIconSource.IInternal -{ - private IDalamudTextureWrap? wrap; - - /// Initializes a new instance of the class. - /// The texture wrap to handle over the ownership. - /// - /// If true, this class will own the passed , and you must not call - /// on the passed wrap. - /// If false, this class will create a new reference of the passed wrap, and you should call - /// on the passed wrap. - /// In both cases, this class must be disposed after use. - public TextureWrapIconSource(IDalamudTextureWrap? wrap, bool takeOwnership) => - this.wrap = takeOwnership ? wrap : wrap?.CreateWrapSharingLowLevelResource(); - - /// Gets the underlying texture wrap. - public IDalamudTextureWrap? Wrap => this.wrap; - - /// - public INotificationIconSource Clone() => new TextureWrapIconSource(this.wrap, false); - - /// - public void Dispose() - { - if (Interlocked.Exchange(ref this.wrap, null) is { } w) - w.Dispose(); - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.wrap?.CreateWrapSharingLowLevelResource()); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private IDalamudTextureWrap? wrap; - - public MaterializedIcon(IDalamudTextureWrap? wrap) => this.wrap = wrap; - - public void Dispose() - { - if (Interlocked.Exchange(ref this.wrap, null) is { } w) - w.Dispose(); - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - this.wrap, - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs b/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs deleted file mode 100644 index 4039b6955..000000000 --- a/Dalamud/Interface/ImGuiNotification/Internal/IconSource/TextureWrapTaskIconSource.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Numerics; -using System.Threading.Tasks; - -using Dalamud.Interface.Internal; -using Dalamud.Plugin.Internal.Types; -using Dalamud.Utility; - -using Serilog; - -namespace Dalamud.Interface.ImGuiNotification.Internal.IconSource; - -/// Represents the use of future as the icon of a notification. -/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. -internal class TextureWrapTaskIconSource : INotificationIconSource.IInternal -{ - /// Gets the default materialized icon, for the purpose of displaying the plugin icon. - internal static readonly INotificationMaterializedIcon DefaultMaterializedIcon = new MaterializedIcon(null); - - /// Initializes a new instance of the class. - /// The function. - public TextureWrapTaskIconSource(Func?>? taskFunc) => - this.TextureWrapTaskFunc = taskFunc; - - /// Gets the function that returns a task resulting in a new instance of . - /// - /// Dalamud will take ownership of the result. Do not call . - public Func?>? TextureWrapTaskFunc { get; } - - /// - public INotificationIconSource Clone() => this; - - /// - public void Dispose() - { - } - - /// - public INotificationMaterializedIcon Materialize() => - new MaterializedIcon(this.TextureWrapTaskFunc); - - private sealed class MaterializedIcon : INotificationMaterializedIcon - { - private Task? task; - - public MaterializedIcon(Func?>? taskFunc) - { - try - { - this.task = taskFunc?.Invoke(); - } - catch (Exception e) - { - Log.Error(e, $"{nameof(TextureWrapTaskIconSource)}: failed to materialize the icon texture."); - this.task = null; - } - } - - public void Dispose() - { - this.task?.ToContentDisposedTask(true); - this.task = null; - } - - public void DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color, LocalPlugin? initiatorPlugin) => - NotificationUtilities.DrawTexture( - this.task?.IsCompletedSuccessfully is true ? this.task.Result : null, - minCoord, - maxCoord, - initiatorPlugin); - } -} diff --git a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs similarity index 51% rename from Dalamud/Interface/ImGuiNotification/NotificationConstants.cs rename to Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index d02ff47f5..f88eac53a 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -1,12 +1,14 @@ using System.Diagnostics; using System.Numerics; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; -namespace Dalamud.Interface.ImGuiNotification; +namespace Dalamud.Interface.ImGuiNotification.Internal; /// Constants for drawing notification windows. -public static class NotificationConstants +internal static class NotificationConstants { // .............................[..] // ..when.......................[XX] @@ -20,69 +22,74 @@ public static class NotificationConstants // .. action buttons .. // ................................. - /// Default duration of the notification. - public static readonly TimeSpan DefaultDisplayDuration = TimeSpan.FromSeconds(3); - - /// Default duration of the notification, after the mouse cursor leaves the notification window. - public static readonly TimeSpan DefaultHoverExtendDuration = TimeSpan.FromSeconds(3); - /// The string to show in place of this_plugin if the notification is shown by Dalamud. - internal const string DefaultInitiator = "Dalamud"; + public const string DefaultInitiator = "Dalamud"; + + /// The string to measure size of, to decide the width of notification windows. + public const string NotificationWidthMeasurementString = + "The width of this text will decide the width\n" + + "of the notification window."; + + /// The ratio of maximum notification window width w.r.t. main viewport width. + public const float MaxNotificationWindowWidthWrtMainViewportWidth = 2f / 3; /// The size of the icon. - internal const float IconSize = 32; + public const float IconSize = 32; /// The background opacity of a notification window. - internal const float BackgroundOpacity = 0.82f; + public const float BackgroundOpacity = 0.82f; /// The duration of indeterminate progress bar loop in milliseconds. - internal const float IndeterminateProgressbarLoopDuration = 2000f; + public const float IndeterminateProgressbarLoopDuration = 2000f; /// The duration of the progress wave animation in milliseconds. - internal const float ProgressWaveLoopDuration = 2000f; + public const float ProgressWaveLoopDuration = 2000f; /// The time ratio of a progress wave loop where the animation is idle. - internal const float ProgressWaveIdleTimeRatio = 0.5f; + public const float ProgressWaveIdleTimeRatio = 0.5f; /// The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque. /// - internal const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + + /// Default duration of the notification. + public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3); /// Duration of show animation. - internal static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of hide animation. - internal static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); /// Duration of progress change animation. - internal static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); + public static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); /// Duration of expando animation. - internal static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); + public static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); /// Text color for the rectangular border when the notification is focused. - internal static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); + public static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); /// Text color for the when. - internal static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); + public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the close button [X]. - internal static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); + public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the title. - internal static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); + public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); /// Text color for the name of the initiator. - internal static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); + public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); /// Text color for the body. - internal static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); /// Color for the background progress bar (determinate progress only). - internal static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); + public static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); /// Color for the background progress bar (determinate progress only). - internal static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); + public static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); /// Gets the relative time format strings. private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = @@ -110,35 +117,35 @@ public static class NotificationConstants }; /// Gets the scaled padding of the window (dot(.) in the above diagram). - internal static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); + public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); /// Gets the distance from the right bottom border of the viewport /// to the right bottom border of a notification window. /// - internal static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); + public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between two notification windows. - internal static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); + public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); /// Gets the scaled gap between components. - internal static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); + public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); /// Gets the scaled size of the icon. - internal static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); + public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); /// Gets the height of the expiry progress bar. - internal static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); + public static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); /// Gets the thickness of the focus indicator rectangle. - internal static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); + public static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); /// Gets the string format of the initiator name field, if the initiator is unloaded. - internal static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; + public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; /// Formats an instance of as a relative time. /// When. /// The formatted string. - internal static string FormatRelativeDateTime(this DateTime when) + public static string FormatRelativeDateTime(this DateTime when) { var ts = DateTime.Now - when; foreach (var (minSpan, formatString) in RelativeFormatStrings) @@ -156,12 +163,12 @@ public static class NotificationConstants /// Formats an instance of as an absolute time. /// When. /// The formatted string. - internal static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; + public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; /// Formats an instance of as a relative time. /// When. /// The formatted string. - internal static string FormatRelativeDateTimeShort(this DateTime when) + public static string FormatRelativeDateTimeShort(this DateTime when) { var ts = DateTime.Now - when; foreach (var (minSpan, formatString) in RelativeFormatStringsShort) @@ -174,4 +181,43 @@ public static class NotificationConstants Debug.Assert(false, "must not reach here"); return "???"; } + + /// Gets the color corresponding to the notification type. + /// The notification type. + /// The corresponding color. + public static Vector4 ToColor(this NotificationType type) => type switch + { + NotificationType.None => ImGuiColors.DalamudWhite, + NotificationType.Success => ImGuiColors.HealerGreen, + NotificationType.Warning => ImGuiColors.DalamudOrange, + NotificationType.Error => ImGuiColors.DalamudRed, + NotificationType.Info => ImGuiColors.TankBlue, + _ => ImGuiColors.DalamudWhite, + }; + + /// Gets the char value corresponding to the notification type. + /// The notification type. + /// The corresponding char, or null. + public static char ToChar(this NotificationType type) => type switch + { + NotificationType.None => '\0', + NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), + NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), + NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), + NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), + _ => '\0', + }; + + /// Gets the localized title string corresponding to the notification type. + /// The notification type. + /// The corresponding title. + public static string? ToTitle(this NotificationType type) => type switch + { + NotificationType.None => null, + NotificationType.Success => NotificationType.Success.ToString(), + NotificationType.Warning => NotificationType.Warning.ToString(), + NotificationType.Error => NotificationType.Error.ToString(), + NotificationType.Info => NotificationType.Info.ToString(), + _ => null, + }; } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs new file mode 100644 index 000000000..3aa712160 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Numerics; + +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of a texture from a file as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +internal class FilePathNotificationIcon : INotificationIcon +{ + private readonly FileInfo fileInfo; + + /// Initializes a new instance of the class. + /// The path to a .tex file inside the game resources. + public FilePathNotificationIcon(string filePath) => this.fileInfo = new(filePath); + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + Service.Get().GetTextureFromFile(this.fileInfo)); + + /// + public override bool Equals(object? obj) => + obj is FilePathNotificationIcon r && r.fileInfo.FullName == this.fileInfo.FullName; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.fileInfo.FullName); + + /// + public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.fileInfo.FullName})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs new file mode 100644 index 000000000..0acfdee4c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs @@ -0,0 +1,31 @@ +using System.Numerics; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of as the icon of a notification. +internal class FontAwesomeIconNotificationIcon : INotificationIcon +{ + private readonly char iconChar; + + /// Initializes a new instance of the class. + /// The character. + public FontAwesomeIconNotificationIcon(FontAwesomeIcon iconChar) => this.iconChar = (char)iconChar; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + this.iconChar, + Service.Get().IconFontAwesomeFontHandle, + color); + + /// + public override bool Equals(object? obj) => obj is FontAwesomeIconNotificationIcon r && r.iconChar == this.iconChar; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar); + + /// + public override string ToString() => $"{nameof(FontAwesomeIconNotificationIcon)}({this.iconChar})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs new file mode 100644 index 000000000..c1db8820c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs @@ -0,0 +1,34 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of a game-shipped texture as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +internal class GamePathNotificationIcon : INotificationIcon +{ + private readonly string gamePath; + + /// Initializes a new instance of the class. + /// The path to a .tex file inside the game resources. + /// Use to get the game path from icon IDs. + public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + Service.Get().GetTextureFromGame(this.gamePath)); + + /// + public override bool Equals(object? obj) => obj is GamePathNotificationIcon r && r.gamePath == this.gamePath; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.gamePath); + + /// + public override string ToString() => $"{nameof(GamePathNotificationIcon)}({this.gamePath})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs new file mode 100644 index 000000000..3bbd8dd81 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs @@ -0,0 +1,33 @@ +using System.Numerics; + +using Dalamud.Game.Text; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of as the icon of a notification. +internal class SeIconCharNotificationIcon : INotificationIcon +{ + private readonly SeIconChar iconChar; + + /// Initializes a new instance of the class. + /// The character. + public SeIconCharNotificationIcon(SeIconChar c) => this.iconChar = c; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + (char)this.iconChar, + Service.Get().IconAxisFontHandle, + color); + + /// + public override bool Equals(object? obj) => obj is SeIconCharNotificationIcon r && r.iconChar == this.iconChar; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar); + + /// + public override string ToString() => $"{nameof(SeIconCharNotificationIcon)}({this.iconChar})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index b457539a3..5ee9fed3e 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -11,6 +11,8 @@ using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using ImGuiNET; + namespace Dalamud.Interface.ImGuiNotification.Internal; /// Class handling notifications/toasts in ImGui. @@ -41,6 +43,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos /// Gets the handle to FontAwesome fonts, sized for use as an icon. public IFontHandle IconFontAwesomeFontHandle { get; } + /// Gets the private atlas for use with notification windows. private IFontAtlas PrivateAtlas { get; } /// @@ -48,17 +51,16 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos { this.PrivateAtlas.Dispose(); foreach (var n in this.pendingNotifications) - n.Dispose(); + n.DisposeInternal(); foreach (var n in this.notifications) - n.Dispose(); + n.DisposeInternal(); this.pendingNotifications.Clear(); this.notifications.Clear(); } /// - public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) + public IActiveNotification AddNotification(Notification notification) { - using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, null); this.pendingNotifications.Add(an); return an; @@ -66,13 +68,10 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos /// Adds a notification originating from a plugin. /// The notification. - /// Dispose when this function returns. /// The source plugin. /// The added notification. - /// will be honored even on exceptions. - public IActiveNotification AddNotification(Notification notification, bool disposeNotification, LocalPlugin plugin) + public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin) { - using var disposer = disposeNotification ? notification : null; var an = new ActiveNotification(notification, plugin); this.pendingNotifications.Add(an); return an; @@ -92,8 +91,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos Content = content, Title = title, Type = type, - }, - true); + }); /// Draw all currently queued notifications. public void Draw() @@ -104,19 +102,14 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos while (this.pendingNotifications.TryTake(out var newNotification)) this.notifications.Add(newNotification); - var maxWidth = Math.Max(320 * ImGuiHelpers.GlobalScale, viewportSize.X / 3); + var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; + width += NotificationConstants.ScaledWindowPadding * 3; + width += NotificationConstants.ScaledIconSize; + width = Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth); - this.notifications.RemoveAll( - static x => - { - if (!x.UpdateAnimations()) - return false; - - x.Dispose(); - return true; - }); + this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); foreach (var tn in this.notifications) - height += tn.Draw(maxWidth, height) + NotificationConstants.ScaledWindowGap; + height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap; } } @@ -140,9 +133,9 @@ internal class NotificationManagerPluginScoped : INotificationManager, IServiceT this.localPlugin = localPlugin; /// - public IActiveNotification AddNotification(Notification notification, bool disposeNotification = true) + public IActiveNotification AddNotification(Notification notification) { - var an = this.notificationManagerService.AddNotification(notification, disposeNotification, this.localPlugin); + var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); return an; diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 33a3ad974..612533cb8 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -1,5 +1,4 @@ -using System.Threading; - +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; namespace Dalamud.Interface.ImGuiNotification; @@ -7,20 +6,10 @@ namespace Dalamud.Interface.ImGuiNotification; /// Represents a blueprint for a notification. public sealed record Notification : INotification { - private INotificationIconSource? iconSource; - - /// Initializes a new instance of the class. - public Notification() - { - } - - /// Initializes a new instance of the class. - /// The instance of to copy from. - public Notification(INotification notification) => this.CopyValuesFrom(notification); - - /// Initializes a new instance of the class. - /// The instance of to copy from. - public Notification(Notification notification) => this.CopyValuesFrom(notification); + /// + /// Gets the default value for and . + /// + public static TimeSpan DefaultDuration => NotificationConstants.DefaultDuration; /// public string Content { get; set; } = string.Empty; @@ -35,25 +24,16 @@ public sealed record Notification : INotification public NotificationType Type { get; set; } = NotificationType.None; /// - public INotificationIconSource? IconSource - { - get => this.iconSource; - set - { - var prevSource = Interlocked.Exchange(ref this.iconSource, value); - if (prevSource != value) - prevSource?.Dispose(); - } - } + public INotificationIcon? Icon { get; set; } /// public DateTime HardExpiry { get; set; } = DateTime.MaxValue; /// - public TimeSpan InitialDuration { get; set; } = NotificationConstants.DefaultDisplayDuration; + public TimeSpan InitialDuration { get; set; } = DefaultDuration; /// - public TimeSpan DurationSinceLastInterest { get; set; } = NotificationConstants.DefaultHoverExtendDuration; + public TimeSpan ExtensionDurationSinceLastInterest { get; set; } = DefaultDuration; /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; @@ -66,29 +46,4 @@ public sealed record Notification : INotification /// public float Progress { get; set; } = 1f; - - /// - public void Dispose() - { - // Assign to the property; it will take care of disposing - this.IconSource = null; - } - - /// Copy values from the given instance of . - /// The instance of to copy from. - private void CopyValuesFrom(INotification copyFrom) - { - this.Content = copyFrom.Content; - this.Title = copyFrom.Title; - this.MinimizedText = copyFrom.MinimizedText; - this.Type = copyFrom.Type; - this.IconSource = copyFrom.IconSource?.Clone(); - this.HardExpiry = copyFrom.HardExpiry; - this.InitialDuration = copyFrom.InitialDuration; - this.DurationSinceLastInterest = copyFrom.DurationSinceLastInterest; - this.ShowIndeterminateIfNoExpiry = copyFrom.ShowIndeterminateIfNoExpiry; - this.Minimized = copyFrom.Minimized; - this.UserDismissable = copyFrom.UserDismissable; - this.Progress = copyFrom.Progress; - } } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 016e9b793..e82b95b75 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -17,44 +17,47 @@ namespace Dalamud.Interface.ImGuiNotification; /// Utilities for implementing stuff under . public static class NotificationUtilities { - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this SeIconChar iconChar) => - INotificationIconSource.From(iconChar); + public static INotificationIcon ToIconSource(this SeIconChar iconChar) => + INotificationIcon.From(iconChar); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this FontAwesomeIcon iconChar) => - INotificationIconSource.From(iconChar); + public static INotificationIcon ToIconSource(this FontAwesomeIcon iconChar) => + INotificationIcon.From(iconChar); - /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this IDalamudTextureWrap? wrap, bool takeOwnership = true) => - INotificationIconSource.From(wrap, takeOwnership); + public static INotificationIcon ToIconSource(this FileInfo fileInfo) => + INotificationIcon.FromFile(fileInfo.FullName); - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIconSource ToIconSource(this FileInfo fileInfo) => - INotificationIconSource.FromFile(fileInfo.FullName); - - /// Draws an icon string. - /// The font handle to use. - /// The icon character. + /// Draws an icon from an and a . /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. + /// The icon character. + /// The font handle to use. /// The foreground color. - internal static unsafe void DrawIconString( - IFontHandle fontHandleLarge, - char c, + /// true if anything has been drawn. + internal static unsafe bool DrawIconFrom( Vector2 minCoord, Vector2 maxCoord, + char c, + IFontHandle fontHandle, Vector4 color) { + if (c is '\0' or char.MaxValue) + return false; + var smallerDim = Math.Max(maxCoord.Y - minCoord.Y, maxCoord.X - minCoord.X); - using (fontHandleLarge.Push()) + using (fontHandle.Push()) { var font = ImGui.GetFont(); - ref readonly var glyph = ref *(ImGuiHelpers.ImFontGlyphReal*)font.FindGlyph(c).NativePtr; + var glyphPtr = (ImGuiHelpers.ImFontGlyphReal*)font.FindGlyphNoFallback(c).NativePtr; + if (glyphPtr is null) + return false; + + ref readonly var glyph = ref *glyphPtr; var size = glyph.XY1 - glyph.XY0; var smallerSizeDim = Math.Min(size.X, size.Y); var scale = smallerSizeDim > smallerDim ? smallerDim / smallerSizeDim : 1f; @@ -69,67 +72,72 @@ public static class NotificationUtilities glyph.UV1, ImGui.GetColorU32(color with { W = color.W * ImGui.GetStyle().Alpha })); } + + return true; } - /// Draws the given texture, or the icon of the plugin if texture is null. - /// The texture. + /// Draws an icon from an instance of . /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. - /// The initiator plugin. - internal static void DrawTexture( - IDalamudTextureWrap? texture, - Vector2 minCoord, - Vector2 maxCoord, - LocalPlugin? initiatorPlugin) + /// The texture. + /// true if anything has been drawn. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, IDalamudTextureWrap? texture) { - var handle = nint.Zero; - var size = Vector2.Zero; - if (texture is not null) + if (texture is null) + return false; + try { - try + var handle = texture.ImGuiHandle; + var size = texture.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; + ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); + ImGui.Image(handle, size); + return true; + } + catch + { + return false; + } + } + + /// Draws an icon from an instance of . + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The plugin. Dalamud icon will be drawn if null is given. + /// true if anything has been drawn. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, LocalPlugin? plugin) + { + var dam = Service.Get(); + if (plugin is null) + return false; + + if (!Service.Get().TryGetIcon( + plugin, + plugin.Manifest, + plugin.IsThirdParty, + out var texture) || texture is null) + { + texture = plugin switch { - handle = texture.ImGuiHandle; - size = texture.Size; - } - catch - { - // must have been disposed or something; ignore the texture - } + { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), + { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), + _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), + }; } - if (handle == nint.Zero) - { - var dam = Service.Get(); - if (initiatorPlugin is null) - { - texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); - } - else - { - if (!Service.Get().TryGetIcon( - initiatorPlugin, - initiatorPlugin.Manifest, - initiatorPlugin.IsThirdParty, - out texture) || texture is null) - { - texture = initiatorPlugin switch - { - { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), - { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), - _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), - }; - } - } + return DrawIconFrom(minCoord, maxCoord, texture); + } - handle = texture.ImGuiHandle; - size = texture.Size; - } - - if (size.X > maxCoord.X - minCoord.X) - size *= (maxCoord.X - minCoord.X) / size.X; - if (size.Y > maxCoord.Y - minCoord.Y) - size *= (maxCoord.Y - minCoord.Y) / size.Y; - ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); - ImGui.Image(handle, size); + /// Draws the Dalamud logo as an icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + internal static void DrawIconFromDalamudLogo(Vector2 minCoord, Vector2 maxCoord) + { + var dam = Service.Get(); + var texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); + DrawIconFrom(minCoord, maxCoord, texture); } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index dcd193496..6c94a2273 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -116,7 +116,7 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.InitialDurationTitles.Length); ImGui.Combo( - "Hover Extend Duration", + "Extension Duration", ref this.notificationTemplate.HoverExtendDurationInt, NotificationTemplate.HoverExtendDurationTitles, NotificationTemplate.HoverExtendDurationTitles.Length); @@ -166,7 +166,7 @@ internal class ImGuiWidget : IDataWindowWidget this.notificationTemplate.InitialDurationInt == 0 ? TimeSpan.MaxValue : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], - DurationSinceLastInterest = + ExtensionDurationSinceLastInterest = this.notificationTemplate.HoverExtendDurationInt == 0 ? TimeSpan.Zero : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], @@ -179,41 +179,40 @@ internal class ImGuiWidget : IDataWindowWidget 4 => -1f, _ => 0.5f, }, - IconSource = this.notificationTemplate.IconSourceInt switch + Icon = this.notificationTemplate.IconSourceInt switch { - 1 => INotificationIconSource.From( + 1 => INotificationIcon.From( (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 2 => INotificationIconSource.From( + 2 => INotificationIcon.From( (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 ? 0 : this.notificationTemplate.IconSourceText[0])), - 3 => INotificationIconSource.From( - Service.Get().GetDalamudTextureWrap( - Enum.Parse( - NotificationTemplate.AssetSources[ - this.notificationTemplate.IconSourceAssetInt])), - false), - 4 => INotificationIconSource.From( - () => - Service.Get().GetDalamudTextureWrapAsync( - Enum.Parse( - NotificationTemplate.AssetSources[ - this.notificationTemplate.IconSourceAssetInt]))), - 5 => INotificationIconSource.FromGame(this.notificationTemplate.IconSourceText), - 6 => INotificationIconSource.FromFile(this.notificationTemplate.IconSourceText), - 7 => INotificationIconSource.From( - Service.Get().GetTextureFromGame(this.notificationTemplate.IconSourceText), - false), - 8 => INotificationIconSource.From( - Service.Get().GetTextureFromFile( - new(this.notificationTemplate.IconSourceText)), - false), + 3 => INotificationIcon.FromGame(this.notificationTemplate.IconSourceText), + 4 => INotificationIcon.FromFile(this.notificationTemplate.IconSourceText), _ => null, }, - }, - true); + }); + + var dam = Service.Get(); + var tm = Service.Get(); + switch (this.notificationTemplate.IconSourceInt) + { + case 5: + n.SetIconTexture( + dam.GetDalamudTextureWrap( + Enum.Parse( + NotificationTemplate.AssetSources[this.notificationTemplate.IconSourceAssetInt]))); + break; + case 6: + n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconSourceText)); + break; + case 7: + n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconSourceText))); + break; + } + switch (this.notificationTemplate.ProgressMode) { case 2: @@ -237,8 +236,8 @@ internal class ImGuiWidget : IDataWindowWidget n.Progress = i / 10f; } - n.ExtendBy(NotificationConstants.DefaultDisplayDuration); - n.InitialDuration = NotificationConstants.DefaultDisplayDuration; + n.ExtendBy(NotificationConstants.DefaultDuration); + n.InitialDuration = NotificationConstants.DefaultDuration; }); break; } @@ -251,6 +250,10 @@ internal class ImGuiWidget : IDataWindowWidget n.Click += _ => nclick++; n.DrawActions += an => { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"{nclick}"); + + ImGui.SameLine(); if (ImGui.Button("Update")) { NewRandom(out title, out type, out progress); @@ -260,18 +263,11 @@ internal class ImGuiWidget : IDataWindowWidget } ImGui.SameLine(); - ImGui.InputText("##input", ref testString, 255); - - if (an.IsHovered) - { - ImGui.SameLine(); - if (ImGui.Button("Dismiss")) - an.DismissNow(); - } - - ImGui.AlignTextToFramePadding(); + if (ImGui.Button("Dismiss")) + an.DismissNow(); + ImGui.SameLine(); - ImGui.TextUnformatted($"Clicked {nclick} time(s)"); + ImGui.InputText("##input", ref testString, 255); }; } } @@ -315,10 +311,9 @@ internal class ImGuiWidget : IDataWindowWidget "None (use Type)", "SeIconChar", "FontAwesomeIcon", - "TextureWrap from DalamudAssets", - "TextureWrapTask from DalamudAssets", "GamePath", "FilePath", + "TextureWrap from DalamudAssets", "TextureWrap from GamePath", "TextureWrap from FilePath", }; @@ -367,7 +362,7 @@ internal class ImGuiWidget : IDataWindowWidget { TimeSpan.Zero, TimeSpan.FromSeconds(1), - NotificationConstants.DefaultDisplayDuration, + NotificationConstants.DefaultDuration, TimeSpan.FromSeconds(10), }; diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 417d77e7d..3a90d52c1 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -581,7 +581,6 @@ public sealed class UiBuilder : IDisposable Type = type, InitialDuration = TimeSpan.FromMilliseconds(msDelay), }, - true, this.localPlugin); _ = this.notifications.TryAdd(an, 0); an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); diff --git a/Dalamud/Plugin/Services/INotificationManager.cs b/Dalamud/Plugin/Services/INotificationManager.cs index 441cc31f7..7d9ccd0b0 100644 --- a/Dalamud/Plugin/Services/INotificationManager.cs +++ b/Dalamud/Plugin/Services/INotificationManager.cs @@ -2,21 +2,11 @@ using Dalamud.Interface.ImGuiNotification; namespace Dalamud.Plugin.Services; -/// -/// Manager for notifications provided by Dalamud using ImGui. -/// +/// Manager for notifications provided by Dalamud using ImGui. public interface INotificationManager { - /// - /// Adds a notification. - /// + /// Adds a notification. /// The new notification. - /// - /// Dispose when this function returns, even if the function throws an exception. - /// Set to false to reuse for multiple calls to this function, in which case, - /// you should call on the value supplied to at a - /// later time. - /// /// The added notification. - IActiveNotification AddNotification(Notification notification, bool disposeNotification = true); + IActiveNotification AddNotification(Notification notification); } From 92302ffd89596e8b940685f4d574d0e9afd21852 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 27 Feb 2024 23:54:57 +0900 Subject: [PATCH 552/585] More cleanup --- .../EventArgs/INotificationClickArgs.cs | 9 ++ .../EventArgs/INotificationDismissArgs.cs | 12 ++ .../EventArgs/INotificationDrawArgs.cs | 19 +++ .../ImGuiNotification/IActiveNotification.cs | 33 +++--- .../ImGuiNotification/INotification.cs | 1 + .../Internal/ActiveNotification.EventArgs.cs | 87 ++++++++++++++ .../Internal/ActiveNotification.ImGui.cs | 19 +-- .../Internal/ActiveNotification.cs | 108 ++++-------------- .../GamePathNotificationIcon.cs | 2 +- .../Internal/NotificationManager.cs | 2 +- .../NotificationDismissedDelegate.cs | 8 -- .../Windows/Data/Widgets/ImGuiWidget.cs | 13 ++- Dalamud/Interface/UiBuilder.cs | 2 +- 13 files changed, 183 insertions(+), 132 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs create mode 100644 Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs create mode 100644 Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs delete mode 100644 Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs diff --git a/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs new file mode 100644 index 000000000..b85a96004 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs @@ -0,0 +1,9 @@ +namespace Dalamud.Interface.ImGuiNotification.EventArgs; + +/// Arguments for use with . +/// Not to be implemented by plugins. +public interface INotificationClickArgs +{ + /// Gets the notification being clicked. + IActiveNotification Notification { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs new file mode 100644 index 000000000..7f664efa1 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs @@ -0,0 +1,12 @@ +namespace Dalamud.Interface.ImGuiNotification.EventArgs; + +/// Arguments for use with . +/// Not to be implemented by plugins. +public interface INotificationDismissArgs +{ + /// Gets the notification being dismissed. + IActiveNotification Notification { get; } + + /// Gets the dismiss reason. + NotificationDismissReason Reason { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs new file mode 100644 index 000000000..221f769e0 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs @@ -0,0 +1,19 @@ +using System.Numerics; + +namespace Dalamud.Interface.ImGuiNotification.EventArgs; + +/// Arguments for use with . +/// Not to be implemented by plugins. +public interface INotificationDrawArgs +{ + /// Gets the notification being drawn. + IActiveNotification Notification { get; } + + /// Gets the top left coordinates of the area being drawn. + Vector2 MinCoord { get; } + + /// Gets the bottom right coordinates of the area being drawn. + /// Note that can be , in which case there is no + /// vertical limits to the drawing region. + Vector2 MaxCoord { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index 340c052cd..c3ea2b9de 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,49 +1,48 @@ using System.Threading; +using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Interface.Internal; namespace Dalamud.Interface.ImGuiNotification; /// Represents an active notification. +/// Not to be implemented by plugins. public interface IActiveNotification : INotification { /// The counter for field. private static long idCounter; /// Invoked upon dismissing the notification. - /// The event callback will not be called, - /// if a user interacts with the notification after the plugin is unloaded. - event NotificationDismissedDelegate Dismiss; + /// The event callback will not be called, if it gets dismissed after plugin unload. + event Action Dismiss; /// Invoked upon clicking on the notification. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action Click; + /// Note that this function may be called even after has been invoked. + event Action Click; /// Invoked upon drawing the action bar of the notification. - /// - /// Note that this function may be called even after has been invoked. - /// Refer to . - /// - event Action DrawActions; + /// Note that this function may be called even after has been invoked. + event Action DrawActions; /// Gets the ID of this notification. + /// This value does not change. long Id { get; } /// Gets the time of creating this notification. + /// This value does not change. DateTime CreatedAt { get; } /// Gets the effective expiry time. /// Contains if the notification does not expire. + /// This value will change depending on property changes and user interactions. DateTime EffectiveExpiry { get; } - /// Gets a value indicating whether the notification has been dismissed. + /// Gets the reason how this notification got dismissed. null if not dismissed. /// This includes when the hide animation is being played. - bool IsDismissed { get; } + NotificationDismissReason? DismissReason { get; } /// Dismisses this notification. + /// If the notification has already been dismissed, this function does nothing. void DismissNow(); /// Extends this notifiation. @@ -57,8 +56,8 @@ public interface IActiveNotification : INotification /// /// The texture passed will be disposed when the notification is dismissed or a new different texture is set /// via another call to this function. You do not have to dispose it yourself. - /// If is true, then calling this function will simply dispose the passed - /// without actually updating the icon. + /// If is not null, then calling this function will simply dispose the + /// passed without actually updating the icon. /// void SetIconTexture(IDalamudTextureWrap? textureWrap); diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index e6861726f..2bc8e751c 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -4,6 +4,7 @@ using Dalamud.Plugin.Services; namespace Dalamud.Interface.ImGuiNotification; /// Represents a notification. +/// Not to be implemented by plugins. public interface INotification { /// Gets or sets the content body of the notification. diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs new file mode 100644 index 000000000..428d9103f --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs @@ -0,0 +1,87 @@ +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification.EventArgs; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Represents an active notification. +internal sealed partial class ActiveNotification : INotificationDismissArgs +{ + /// + public event Action? Dismiss; + + /// + IActiveNotification INotificationDismissArgs.Notification => this; + + /// + NotificationDismissReason INotificationDismissArgs.Reason => + this.DismissReason + ?? throw new InvalidOperationException("DismissReason must be set before using INotificationDismissArgs"); + + private void InvokeDismiss() + { + try + { + this.Dismiss?.Invoke(this); + } + catch (Exception e) + { + this.LogEventInvokeError(e, $"{nameof(this.Dismiss)} error"); + } + } +} + +/// Represents an active notification. +internal sealed partial class ActiveNotification : INotificationClickArgs +{ + /// + public event Action? Click; + + /// + IActiveNotification INotificationClickArgs.Notification => this; + + private void InvokeClick() + { + try + { + this.Click?.Invoke(this); + } + catch (Exception e) + { + this.LogEventInvokeError(e, $"{nameof(this.Click)} error"); + } + } +} + +/// Represents an active notification. +internal sealed partial class ActiveNotification : INotificationDrawArgs +{ + private Vector2 drawActionArgMinCoord; + private Vector2 drawActionArgMaxCoord; + + /// + public event Action? DrawActions; + + /// + IActiveNotification INotificationDrawArgs.Notification => this; + + /// + Vector2 INotificationDrawArgs.MinCoord => this.drawActionArgMinCoord; + + /// + Vector2 INotificationDrawArgs.MaxCoord => this.drawActionArgMaxCoord; + + private void InvokeDrawActions(Vector2 minCoord, Vector2 maxCoord) + { + this.drawActionArgMinCoord = minCoord; + this.drawActionArgMaxCoord = maxCoord; + try + { + this.DrawActions?.Invoke(this); + } + catch (Exception e) + { + this.LogEventInvokeError(e, $"{nameof(this.DrawActions)} error; event registration cancelled"); + } + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 99b924923..60e8e28e6 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -2,7 +2,6 @@ using System.Numerics; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; -using Dalamud.Utility; using ImGuiNET; @@ -83,7 +82,7 @@ internal sealed partial class ActiveNotification this.EffectiveExpiry = this.CalculateEffectiveExpiry(ref warrantsExtension); - if (!this.IsDismissed && DateTime.Now > this.EffectiveExpiry) + if (DateTime.Now > this.EffectiveExpiry) this.DismissNow(NotificationDismissReason.Timeout); if (this.ExtensionDurationSinceLastInterest > TimeSpan.Zero && warrantsExtension) @@ -121,7 +120,7 @@ internal sealed partial class ActiveNotification if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) || ImGui.IsMouseClicked(ImGuiMouseButton.Right) || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) - this.Click.InvokeSafely(this); + this.InvokeClick(); } } @@ -419,22 +418,16 @@ internal sealed partial class ActiveNotification ImGui.PopTextWrapPos(); if (this.DrawActions is not null) { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); - try - { - this.DrawActions.Invoke(this); - } - catch - { - // ignore - } + this.InvokeDrawActions( + minCoord with { Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap }, + new(minCoord.X + width, float.MaxValue)); } } private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) { float barL, barR; - if (this.IsDismissed) + if (this.DismissReason is not null) { var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 357752f6e..475ae7e68 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -1,16 +1,15 @@ -using System.Numerics; using System.Runtime.Loader; using System.Threading; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; -using Dalamud.Interface.Colors; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Serilog; +using Serilog.Events; namespace Dalamud.Interface.ImGuiNotification.Internal; @@ -71,15 +70,6 @@ internal sealed partial class ActiveNotification : IActiveNotification this.progressEasing.Start(); } - /// - public event NotificationDismissedDelegate? Dismiss; - - /// - public event Action? Click; - - /// - public event Action? DrawActions; - /// public long Id { get; } = IActiveNotification.CreateNewId(); @@ -90,60 +80,35 @@ internal sealed partial class ActiveNotification : IActiveNotification public string Content { get => this.underlyingNotification.Content; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.Content = value; - } + set => this.underlyingNotification.Content = value; } /// public string? Title { get => this.underlyingNotification.Title; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.Title = value; - } + set => this.underlyingNotification.Title = value; } /// public string? MinimizedText { get => this.underlyingNotification.MinimizedText; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.MinimizedText = value; - } + set => this.underlyingNotification.MinimizedText = value; } /// public NotificationType Type { get => this.underlyingNotification.Type; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.Type = value; - } + set => this.underlyingNotification.Type = value; } /// public INotificationIcon? Icon { get => this.underlyingNotification.Icon; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.Icon = value; - } + set => this.underlyingNotification.Icon = value; } /// @@ -152,7 +117,7 @@ internal sealed partial class ActiveNotification : IActiveNotification get => this.underlyingNotification.HardExpiry; set { - if (this.underlyingNotification.HardExpiry == value || this.IsDismissed) + if (this.underlyingNotification.HardExpiry == value) return; this.underlyingNotification.HardExpiry = value; this.lastInterestTime = DateTime.Now; @@ -165,8 +130,6 @@ internal sealed partial class ActiveNotification : IActiveNotification get => this.underlyingNotification.InitialDuration; set { - if (this.IsDismissed) - return; this.underlyingNotification.InitialDuration = value; this.lastInterestTime = DateTime.Now; } @@ -178,8 +141,6 @@ internal sealed partial class ActiveNotification : IActiveNotification get => this.underlyingNotification.ExtensionDurationSinceLastInterest; set { - if (this.IsDismissed) - return; this.underlyingNotification.ExtensionDurationSinceLastInterest = value; this.lastInterestTime = DateTime.Now; } @@ -188,57 +149,37 @@ internal sealed partial class ActiveNotification : IActiveNotification /// public DateTime EffectiveExpiry { get; private set; } + /// + public NotificationDismissReason? DismissReason { get; private set; } + /// public bool ShowIndeterminateIfNoExpiry { get => this.underlyingNotification.ShowIndeterminateIfNoExpiry; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.ShowIndeterminateIfNoExpiry = value; - } + set => this.underlyingNotification.ShowIndeterminateIfNoExpiry = value; } /// public bool Minimized { get => this.newMinimized ?? this.underlyingNotification.Minimized; - set - { - if (this.IsDismissed) - return; - this.newMinimized = value; - } + set => this.newMinimized = value; } /// public bool UserDismissable { get => this.underlyingNotification.UserDismissable; - set - { - if (this.IsDismissed) - return; - this.underlyingNotification.UserDismissable = value; - } + set => this.underlyingNotification.UserDismissable = value; } /// public float Progress { get => this.newProgress ?? this.underlyingNotification.Progress; - set - { - if (this.IsDismissed) - return; - this.newProgress = value; - } + set => this.newProgress = value; } - /// - public bool IsDismissed => this.hideEasing.IsRunning; - /// Gets the eased progress. private float ProgressEased { @@ -271,20 +212,12 @@ internal sealed partial class ActiveNotification : IActiveNotification /// The reason of dismissal. public void DismissNow(NotificationDismissReason reason) { - if (this.hideEasing.IsRunning) + if (this.DismissReason is not null) return; + this.DismissReason = reason; this.hideEasing.Start(); - try - { - this.Dismiss?.Invoke(this, reason); - } - catch (Exception e) - { - Log.Error( - e, - $"{nameof(this.Dismiss)} error; notification is owned by {this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}"); - } + this.InvokeDismiss(); } /// @@ -298,7 +231,7 @@ internal sealed partial class ActiveNotification : IActiveNotification /// public void SetIconTexture(IDalamudTextureWrap? textureWrap) { - if (this.IsDismissed) + if (this.DismissReason is not null) { textureWrap?.Dispose(); return; @@ -408,4 +341,9 @@ internal sealed partial class ActiveNotification : IActiveNotification this.DrawActions = null; this.initiatorPlugin = null; } + + private void LogEventInvokeError(Exception exception, string message) => + Log.Error( + exception, + $"[{nameof(ActiveNotification)}:{this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}] {message}"); } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs index c1db8820c..e0699e1b6 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs @@ -15,7 +15,7 @@ internal class GamePathNotificationIcon : INotificationIcon /// The path to a .tex file inside the game resources. /// Use to get the game path from icon IDs. public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath; - + /// public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => NotificationUtilities.DrawIconFrom( diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index 5ee9fed3e..973e93c72 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -137,7 +137,7 @@ internal class NotificationManagerPluginScoped : INotificationManager, IServiceT { var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); _ = this.notifications.TryAdd(an, 0); - an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); + an.Dismiss += a => this.notifications.TryRemove(a.Notification, out _); return an; } diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs deleted file mode 100644 index 09d6fd818..000000000 --- a/Dalamud/Interface/ImGuiNotification/NotificationDismissedDelegate.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Dalamud.Interface.ImGuiNotification; - -/// Delegate representing the dismissal of an active notification. -/// The notification being dismissed. -/// The reason of dismissal. -public delegate void NotificationDismissedDelegate( - IActiveNotification notification, - NotificationDismissReason dismissReason); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 6c94a2273..d51f18216 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -219,7 +219,7 @@ internal class ImGuiWidget : IDataWindowWidget Task.Run( async () => { - for (var i = 0; i <= 10 && !n.IsDismissed; i++) + for (var i = 0; i <= 10 && !n.DismissReason.HasValue; i++) { await Task.Delay(500); n.Progress = i / 10f; @@ -230,7 +230,7 @@ internal class ImGuiWidget : IDataWindowWidget Task.Run( async () => { - for (var i = 0; i <= 10 && !n.IsDismissed; i++) + for (var i = 0; i <= 10 && !n.DismissReason.HasValue; i++) { await Task.Delay(500); n.Progress = i / 10f; @@ -257,16 +257,17 @@ internal class ImGuiWidget : IDataWindowWidget if (ImGui.Button("Update")) { NewRandom(out title, out type, out progress); - an.Title = title; - an.Type = type; - an.Progress = progress; + an.Notification.Title = title; + an.Notification.Type = type; + an.Notification.Progress = progress; } ImGui.SameLine(); if (ImGui.Button("Dismiss")) - an.DismissNow(); + an.Notification.DismissNow(); ImGui.SameLine(); + ImGui.SetNextItemWidth(an.MaxCoord.X - ImGui.GetCursorPosX()); ImGui.InputText("##input", ref testString, 255); }; } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 3a90d52c1..2053d9354 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -583,7 +583,7 @@ public sealed class UiBuilder : IDisposable }, this.localPlugin); _ = this.notifications.TryAdd(an, 0); - an.Dismiss += (a, unused) => this.notifications.TryRemove(an, out _); + an.Dismiss += a => this.notifications.TryRemove(a.Notification, out _); } /// From edb13c18e38e5edcd386184f7c73081be56f86a4 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 00:00:10 +0900 Subject: [PATCH 553/585] more cleanup --- .../Internal/ActiveNotification.ImGui.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 60e8e28e6..ac10cc060 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -360,6 +360,10 @@ internal sealed partial class ActiveNotification textColumnOffset.Y += NotificationConstants.ScaledComponentGap; this.DrawContentBody(textColumnOffset, textColumnWidth); + textColumnOffset.Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap; + + ImGui.SetCursorPos(textColumnOffset); + this.InvokeDrawActions(textColumnOffset, new(textColumnX + textColumnWidth, float.MaxValue)); } private void DrawIcon(Vector2 minCoord, Vector2 size) @@ -416,12 +420,6 @@ internal sealed partial class ActiveNotification ImGui.TextUnformatted(this.Content); ImGui.PopStyleColor(); ImGui.PopTextWrapPos(); - if (this.DrawActions is not null) - { - this.InvokeDrawActions( - minCoord with { Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap }, - new(minCoord.X + width, float.MaxValue)); - } } private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) From 18c1084fe3e0e2196fd8ae2b8bf98a80e92d88e5 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 00:57:54 +0900 Subject: [PATCH 554/585] Make DateTime/TimeSpan localizable --- .../Internal/ActiveNotification.ImGui.cs | 7 +- .../Internal/NotificationConstants.cs | 66 --------- Dalamud/Localization.cs | 22 ++- Dalamud/Utility/DateTimeSpanExtensions.cs | 129 ++++++++++++++++++ 4 files changed, 154 insertions(+), 70 deletions(-) create mode 100644 Dalamud/Utility/DateTimeSpanExtensions.cs diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index ac10cc060..5d496963d 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -2,6 +2,7 @@ using System.Numerics; using Dalamud.Interface.Internal; using Dalamud.Interface.Utility; +using Dalamud.Utility; using ImGuiNET; @@ -288,8 +289,8 @@ internal sealed partial class ActiveNotification ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); ImGui.TextUnformatted( ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) - ? this.CreatedAt.FormatAbsoluteDateTime() - : this.CreatedAt.FormatRelativeDateTime()); + ? this.CreatedAt.LocAbsolute() + : this.CreatedAt.LocRelativePastLong()); ImGui.PopStyleColor(); ImGui.PopStyleVar(); } @@ -304,7 +305,7 @@ internal sealed partial class ActiveNotification ltOffset.X = height; - var agoText = this.CreatedAt.FormatRelativeDateTimeShort(); + var agoText = this.CreatedAt.LocRelativePastShort(); var agoSize = ImGui.CalcTextSize(agoText); rtOffset.X -= agoSize.X; ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index f88eac53a..50536baa3 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using System.Numerics; using Dalamud.Interface.Colors; @@ -91,31 +90,6 @@ internal static class NotificationConstants /// Color for the background progress bar (determinate progress only). public static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); - /// Gets the relative time format strings. - private static readonly (TimeSpan MinSpan, string? FormatString)[] RelativeFormatStrings = - { - (TimeSpan.FromDays(7), null), - (TimeSpan.FromDays(2), "{0:%d} days ago"), - (TimeSpan.FromDays(1), "yesterday"), - (TimeSpan.FromHours(2), "{0:%h} hours ago"), - (TimeSpan.FromHours(1), "an hour ago"), - (TimeSpan.FromMinutes(2), "{0:%m} minutes ago"), - (TimeSpan.FromMinutes(1), "a minute ago"), - (TimeSpan.FromSeconds(2), "{0:%s} seconds ago"), - (TimeSpan.FromSeconds(1), "a second ago"), - (TimeSpan.MinValue, "just now"), - }; - - /// Gets the relative time format strings. - private static readonly (TimeSpan MinSpan, string FormatString)[] RelativeFormatStringsShort = - { - (TimeSpan.FromDays(1), "{0:%d}d"), - (TimeSpan.FromHours(1), "{0:%h}h"), - (TimeSpan.FromMinutes(1), "{0:%m}m"), - (TimeSpan.FromSeconds(1), "{0:%s}s"), - (TimeSpan.MinValue, "now"), - }; - /// Gets the scaled padding of the window (dot(.) in the above diagram). public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); @@ -142,46 +116,6 @@ internal static class NotificationConstants /// Gets the string format of the initiator name field, if the initiator is unloaded. public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; - /// Formats an instance of as a relative time. - /// When. - /// The formatted string. - public static string FormatRelativeDateTime(this DateTime when) - { - var ts = DateTime.Now - when; - foreach (var (minSpan, formatString) in RelativeFormatStrings) - { - if (ts < minSpan) - continue; - if (formatString is null) - break; - return string.Format(formatString, ts); - } - - return when.FormatAbsoluteDateTime(); - } - - /// Formats an instance of as an absolute time. - /// When. - /// The formatted string. - public static string FormatAbsoluteDateTime(this DateTime when) => $"{when:G}"; - - /// Formats an instance of as a relative time. - /// When. - /// The formatted string. - public static string FormatRelativeDateTimeShort(this DateTime when) - { - var ts = DateTime.Now - when; - foreach (var (minSpan, formatString) in RelativeFormatStringsShort) - { - if (ts < minSpan) - continue; - return string.Format(formatString, ts); - } - - Debug.Assert(false, "must not reach here"); - return "???"; - } - /// Gets the color corresponding to the notification type. /// The notification type. /// The corresponding color. diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index b180f113a..39312ac52 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -36,6 +36,7 @@ public class Localization : IServiceType /// Use embedded loc resource files. public Localization(string locResourceDirectory, string locResourcePrefix = "", bool useEmbedded = false) { + this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture; this.locResourceDirectory = locResourceDirectory; this.locResourcePrefix = locResourcePrefix; this.useEmbedded = useEmbedded; @@ -61,7 +62,24 @@ public class Localization : IServiceType /// /// Event that occurs when the language is changed. /// - public event LocalizationChangedDelegate LocalizationChanged; + public event LocalizationChangedDelegate? LocalizationChanged; + + /// + /// Gets an instance of that corresponds to the language configured from Dalamud Settings. + /// + public CultureInfo DalamudLanguageCultureInfo { get; private set; } + + /// + /// Gets an instance of that corresponds to . + /// + /// The language code which should be in . + /// The corresponding instance of . + public static CultureInfo GetCultureInfoFromLangCode(string langCode) => + CultureInfo.GetCultureInfo(langCode switch + { + "tw" => "zh-tw", + _ => langCode, + }); /// /// Search the set-up localization data for the provided assembly for the given string key and return it. @@ -108,6 +126,7 @@ public class Localization : IServiceType /// public void SetupWithFallbacks() { + this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture; this.LocalizationChanged?.Invoke(FallbackLangCode); Loc.SetupWithFallbacks(this.assembly); } @@ -124,6 +143,7 @@ public class Localization : IServiceType return; } + this.DalamudLanguageCultureInfo = GetCultureInfoFromLangCode(langCode); this.LocalizationChanged?.Invoke(langCode); try diff --git a/Dalamud/Utility/DateTimeSpanExtensions.cs b/Dalamud/Utility/DateTimeSpanExtensions.cs new file mode 100644 index 000000000..8f6a2a7ec --- /dev/null +++ b/Dalamud/Utility/DateTimeSpanExtensions.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +using CheapLoc; + +using Dalamud.Logging.Internal; + +namespace Dalamud.Utility; + +/// +/// Utility functions for and . +/// +internal static class DateTimeSpanExtensions +{ + private static readonly ModuleLog Log = new(nameof(DateTimeSpanExtensions)); + + private static ParsedRelativeFormatStrings? relativeFormatStringLong; + + private static ParsedRelativeFormatStrings? relativeFormatStringShort; + + /// Formats an instance of as a localized absolute time. + /// When. + /// The formatted string. + /// The string will be formatted according to Square Enix Account region settings, if Dalamud default + /// language is English. + public static unsafe string LocAbsolute(this DateTime when) + { + var culture = Service.GetNullable()?.DalamudLanguageCultureInfo ?? CultureInfo.InvariantCulture; + if (!Equals(culture, CultureInfo.InvariantCulture)) + return when.ToString("G", culture); + + var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); + var region = 0; + if (framework is not null) + region = framework->Region; + switch (region) + { + case 0: // jp + default: + return when.ToString("yyyy-MM-dd HH:mm:ss"); + case 1: // na + return when.ToString("MM/dd/yyyy HH:mm:ss"); + case 2: // eu + return when.ToString("dd-mm-yyyy HH:mm:ss"); + } + } + + /// Formats an instance of as a localized relative time. + /// When. + /// The formatted string. + public static string LocRelativePastLong(this DateTime when) + { + var loc = Loc.Localize( + "DateTimeSpanExtensions.RelativeFormatStringsLong", + "172800,{0:%d} days ago\n86400,yesterday\n7200,{0:%h} hours ago\n3600,an hour ago\n120,{0:%m} minutes ago\n60,a minute ago\n2,{0:%s} seconds ago\n1,a second ago\n-Infinity,just now"); + Debug.Assert(loc != null, "loc != null"); + + if (relativeFormatStringLong?.FormatStringLoc != loc) + relativeFormatStringLong ??= new(loc); + + return relativeFormatStringLong.Format(DateTime.Now - when); + } + + /// Formats an instance of as a localized relative time. + /// When. + /// The formatted string. + public static string LocRelativePastShort(this DateTime when) + { + var loc = Loc.Localize( + "DateTimeSpanExtensions.RelativeFormatStringsShort", + "86400,{0:%d}d\n3600,{0:%h}h\n60,{0:%m}m\n1,{0:%s}s\n-Infinity,now"); + Debug.Assert(loc != null, "loc != null"); + + if (relativeFormatStringShort?.FormatStringLoc != loc) + relativeFormatStringShort = new(loc); + + return relativeFormatStringShort.Format(DateTime.Now - when); + } + + private sealed class ParsedRelativeFormatStrings + { + private readonly List<(float MinSeconds, string FormatString)> formatStrings = new(); + + public ParsedRelativeFormatStrings(string value) + { + this.FormatStringLoc = value; + foreach (var line in value.Split("\n")) + { + var sep = line.IndexOf(','); + if (sep < 0) + { + Log.Error("A line without comma has been found: {line}", line); + continue; + } + + if (!float.TryParse( + line.AsSpan(0, sep), + NumberStyles.Float, + CultureInfo.InvariantCulture, + out var seconds)) + { + Log.Error("Could not parse the duration: {line}", line); + continue; + } + + this.formatStrings.Add((seconds, line[(sep + 1)..])); + } + + this.formatStrings.Sort((a, b) => b.MinSeconds.CompareTo(a.MinSeconds)); + } + + public string FormatStringLoc { get; } + + /// Formats an instance of as a localized string. + /// The duration. + /// The formatted string. + public string Format(TimeSpan ts) + { + foreach (var (minSeconds, formatString) in this.formatStrings) + { + if (ts.TotalSeconds >= minSeconds) + return string.Format(formatString, ts); + } + + return this.formatStrings[^1].FormatString.Format(ts); + } + } +} From 62af691419622bf85d8c853c81b149cc3e34b419 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 01:05:47 +0900 Subject: [PATCH 555/585] More notification localizations --- .../Internal/NotificationConstants.cs | 20 +++++++++++-------- Dalamud/Localization.cs | 2 +- Dalamud/Utility/DateTimeSpanExtensions.cs | 16 ++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index 50536baa3..de212160c 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -1,5 +1,7 @@ using System.Numerics; +using CheapLoc; + using Dalamud.Interface.Colors; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; @@ -21,10 +23,8 @@ internal static class NotificationConstants // .. action buttons .. // ................................. - /// The string to show in place of this_plugin if the notification is shown by Dalamud. - public const string DefaultInitiator = "Dalamud"; - /// The string to measure size of, to decide the width of notification windows. + /// Probably not worth localizing. public const string NotificationWidthMeasurementString = "The width of this text will decide the width\n" + "of the notification window."; @@ -113,8 +113,12 @@ internal static class NotificationConstants /// Gets the thickness of the focus indicator rectangle. public static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); + /// Gets the string to show in place of this_plugin if the notification is shown by Dalamud. + public static string DefaultInitiator => Loc.Localize("NotificationConstants.DefaultInitiator", "Dalamud"); + /// Gets the string format of the initiator name field, if the initiator is unloaded. - public static string UnloadedInitiatorNameFormat => "{0} (unloaded)"; + public static string UnloadedInitiatorNameFormat => + Loc.Localize("NotificationConstants.UnloadedInitiatorNameFormat", "{0} (unloaded)"); /// Gets the color corresponding to the notification type. /// The notification type. @@ -148,10 +152,10 @@ internal static class NotificationConstants public static string? ToTitle(this NotificationType type) => type switch { NotificationType.None => null, - NotificationType.Success => NotificationType.Success.ToString(), - NotificationType.Warning => NotificationType.Warning.ToString(), - NotificationType.Error => NotificationType.Error.ToString(), - NotificationType.Info => NotificationType.Info.ToString(), + NotificationType.Success => Loc.Localize("NotificationConstants.Title.Success", "Success"), + NotificationType.Warning => Loc.Localize("NotificationConstants.Title.Warning", "Warning"), + NotificationType.Error => Loc.Localize("NotificationConstants.Title.Error", "Error"), + NotificationType.Info => Loc.Localize("NotificationConstants.Title.Info", "Info"), _ => null, }; } diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index 39312ac52..a9b0cf93d 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -70,7 +70,7 @@ public class Localization : IServiceType public CultureInfo DalamudLanguageCultureInfo { get; private set; } /// - /// Gets an instance of that corresponds to . + /// Gets an instance of that corresponds to . /// /// The language code which should be in . /// The corresponding instance of . diff --git a/Dalamud/Utility/DateTimeSpanExtensions.cs b/Dalamud/Utility/DateTimeSpanExtensions.cs index 8f6a2a7ec..8422a4a26 100644 --- a/Dalamud/Utility/DateTimeSpanExtensions.cs +++ b/Dalamud/Utility/DateTimeSpanExtensions.cs @@ -11,7 +11,7 @@ namespace Dalamud.Utility; /// /// Utility functions for and . /// -internal static class DateTimeSpanExtensions +public static class DateTimeSpanExtensions { private static readonly ModuleLog Log = new(nameof(DateTimeSpanExtensions)); @@ -34,16 +34,12 @@ internal static class DateTimeSpanExtensions var region = 0; if (framework is not null) region = framework->Region; - switch (region) + return region switch { - case 0: // jp - default: - return when.ToString("yyyy-MM-dd HH:mm:ss"); - case 1: // na - return when.ToString("MM/dd/yyyy HH:mm:ss"); - case 2: // eu - return when.ToString("dd-mm-yyyy HH:mm:ss"); - } + 1 => when.ToString("MM/dd/yyyy HH:mm:ss"), // na + 2 => when.ToString("dd-mm-yyyy HH:mm:ss"), // eu + _ => when.ToString("yyyy-MM-dd HH:mm:ss"), // jp(0), cn(3), kr(4), and other possible errorneous cases + }; } /// Formats an instance of as a localized relative time. From a4a990cf3d296995fe9126ce27d8a9fb561dbaac Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 01:06:48 +0900 Subject: [PATCH 556/585] Reformat code --- .../ImGuiNotification/Internal/ActiveNotification.ImGui.cs | 2 +- .../Interface/ImGuiNotification/Internal/ActiveNotification.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 5d496963d..9363d97d9 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -362,7 +362,7 @@ internal sealed partial class ActiveNotification this.DrawContentBody(textColumnOffset, textColumnWidth); textColumnOffset.Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap; - + ImGui.SetCursorPos(textColumnOffset); this.InvokeDrawActions(textColumnOffset, new(textColumnX + textColumnWidth, float.MaxValue)); } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 475ae7e68..a9950745d 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -9,7 +9,6 @@ using Dalamud.Plugin.Internal.Types; using Dalamud.Utility; using Serilog; -using Serilog.Events; namespace Dalamud.Interface.ImGuiNotification.Internal; From a1e2473774742db15154e8bff850a029806f075e Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 01:09:07 +0900 Subject: [PATCH 557/585] Normalize names --- .../NotificationUtilities.cs | 6 +- .../Windows/Data/Widgets/ImGuiWidget.cs | 62 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index e82b95b75..0ec2561fd 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -19,17 +19,17 @@ public static class NotificationUtilities { /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIcon ToIconSource(this SeIconChar iconChar) => + public static INotificationIcon ToNotificationIcon(this SeIconChar iconChar) => INotificationIcon.From(iconChar); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIcon ToIconSource(this FontAwesomeIcon iconChar) => + public static INotificationIcon ToNotificationIcon(this FontAwesomeIcon iconChar) => INotificationIcon.From(iconChar); /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static INotificationIcon ToIconSource(this FileInfo fileInfo) => + public static INotificationIcon ToNotificationIcon(this FileInfo fileInfo) => INotificationIcon.FromFile(fileInfo.FullName); /// Draws an icon from an and a . diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index d51f18216..47c5993cd 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -76,35 +76,35 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.TypeTitles.Length); ImGui.Combo( - "Icon Source##iconSourceCombo", - ref this.notificationTemplate.IconSourceInt, - NotificationTemplate.IconSourceTitles, - NotificationTemplate.IconSourceTitles.Length); - switch (this.notificationTemplate.IconSourceInt) + "Icon##iconCombo", + ref this.notificationTemplate.IconInt, + NotificationTemplate.IconTitles, + NotificationTemplate.IconTitles.Length); + switch (this.notificationTemplate.IconInt) { case 1 or 2: ImGui.InputText( - "Icon Text##iconSourceText", - ref this.notificationTemplate.IconSourceText, + "Icon Text##iconText", + ref this.notificationTemplate.IconText, 255); break; case 3 or 4: ImGui.Combo( - "Icon Source##iconSourceAssetCombo", - ref this.notificationTemplate.IconSourceAssetInt, + "Asset##iconAssetCombo", + ref this.notificationTemplate.IconAssetInt, NotificationTemplate.AssetSources, NotificationTemplate.AssetSources.Length); break; case 5 or 7: ImGui.InputText( - "Game Path##iconSourceText", - ref this.notificationTemplate.IconSourceText, + "Game Path##iconText", + ref this.notificationTemplate.IconText, 255); break; case 6 or 8: ImGui.InputText( - "File Path##iconSourceText", - ref this.notificationTemplate.IconSourceText, + "File Path##iconText", + ref this.notificationTemplate.IconText, 255); break; } @@ -179,37 +179,37 @@ internal class ImGuiWidget : IDataWindowWidget 4 => -1f, _ => 0.5f, }, - Icon = this.notificationTemplate.IconSourceInt switch + Icon = this.notificationTemplate.IconInt switch { 1 => INotificationIcon.From( - (SeIconChar)(this.notificationTemplate.IconSourceText.Length == 0 + (SeIconChar)(this.notificationTemplate.IconText.Length == 0 ? 0 - : this.notificationTemplate.IconSourceText[0])), + : this.notificationTemplate.IconText[0])), 2 => INotificationIcon.From( - (FontAwesomeIcon)(this.notificationTemplate.IconSourceText.Length == 0 + (FontAwesomeIcon)(this.notificationTemplate.IconText.Length == 0 ? 0 - : this.notificationTemplate.IconSourceText[0])), - 3 => INotificationIcon.FromGame(this.notificationTemplate.IconSourceText), - 4 => INotificationIcon.FromFile(this.notificationTemplate.IconSourceText), + : this.notificationTemplate.IconText[0])), + 3 => INotificationIcon.FromGame(this.notificationTemplate.IconText), + 4 => INotificationIcon.FromFile(this.notificationTemplate.IconText), _ => null, }, }); var dam = Service.Get(); var tm = Service.Get(); - switch (this.notificationTemplate.IconSourceInt) + switch (this.notificationTemplate.IconInt) { case 5: n.SetIconTexture( dam.GetDalamudTextureWrap( Enum.Parse( - NotificationTemplate.AssetSources[this.notificationTemplate.IconSourceAssetInt]))); + NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); break; case 6: - n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconSourceText)); + n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconText)); break; case 7: - n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconSourceText))); + n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconText))); break; } @@ -307,7 +307,7 @@ internal class ImGuiWidget : IDataWindowWidget private struct NotificationTemplate { - public static readonly string[] IconSourceTitles = + public static readonly string[] IconTitles = { "None (use Type)", "SeIconChar", @@ -373,9 +373,9 @@ internal class ImGuiWidget : IDataWindowWidget public string Title; public bool ManualMinimizedText; public string MinimizedText; - public int IconSourceInt; - public string IconSourceText; - public int IconSourceAssetInt; + public int IconInt; + public string IconText; + public int IconAssetInt; public bool ManualType; public int TypeInt; public int InitialDurationInt; @@ -394,9 +394,9 @@ internal class ImGuiWidget : IDataWindowWidget this.Title = string.Empty; this.ManualMinimizedText = false; this.MinimizedText = string.Empty; - this.IconSourceInt = 0; - this.IconSourceText = "ui/icon/000000/000004_hr1.tex"; - this.IconSourceAssetInt = 0; + this.IconInt = 0; + this.IconText = "ui/icon/000000/000004_hr1.tex"; + this.IconAssetInt = 0; this.ManualType = false; this.TypeInt = (int)NotificationType.None; this.InitialDurationInt = 2; From 2a2fded520ee895dd33f8653491acd1d730f009a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 01:16:43 +0900 Subject: [PATCH 558/585] Fix user actions offset --- .../Internal/ActiveNotification.ImGui.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 9363d97d9..5f7d1a0fd 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -93,7 +93,7 @@ internal sealed partial class ActiveNotification this.DrawTopBar(width, actionWindowHeight, isHovered); if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) { - this.DrawContentArea(width, actionWindowHeight); + this.DrawContentAndActions(width, actionWindowHeight); } else if (this.expandoEasing.IsRunning) { @@ -101,7 +101,7 @@ internal sealed partial class ActiveNotification ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); else ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); - this.DrawContentArea(width, actionWindowHeight); + this.DrawContentAndActions(width, actionWindowHeight); ImGui.PopStyleVar(); } @@ -347,7 +347,7 @@ internal sealed partial class ActiveNotification return r; } - private void DrawContentArea(float width, float actionWindowHeight) + private void DrawContentAndActions(float width, float actionWindowHeight) { var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; @@ -361,10 +361,17 @@ internal sealed partial class ActiveNotification textColumnOffset.Y += NotificationConstants.ScaledComponentGap; this.DrawContentBody(textColumnOffset, textColumnWidth); - textColumnOffset.Y = ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap; - ImGui.SetCursorPos(textColumnOffset); - this.InvokeDrawActions(textColumnOffset, new(textColumnX + textColumnWidth, float.MaxValue)); + if (this.DrawActions is null) + return; + + var userActionOffset = new Vector2( + NotificationConstants.ScaledWindowPadding, + ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + ImGui.SetCursorPos(userActionOffset); + this.InvokeDrawActions( + userActionOffset, + new(width - NotificationConstants.ScaledWindowPadding, float.MaxValue)); } private void DrawIcon(Vector2 minCoord, Vector2 size) From 6b875bbcb58f288ea0c614f223591106a36d2498 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 28 Feb 2024 17:27:19 +0900 Subject: [PATCH 559/585] Support SetIconTexture(Task?) --- .../ImGuiNotification/IActiveNotification.cs | 18 +++++++++++++++- .../ImGuiNotification/INotification.cs | 9 ++++++-- .../Internal/ActiveNotification.cs | 21 ++++++++++++------- .../NotificationUtilities.cs | 11 ++++++++++ .../Windows/Data/Widgets/ImGuiWidget.cs | 15 +++++++++---- 5 files changed, 60 insertions(+), 14 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs index c3ea2b9de..e677471b4 100644 --- a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -1,4 +1,5 @@ using System.Threading; +using System.Threading.Tasks; using Dalamud.Interface.ImGuiNotification.EventArgs; using Dalamud.Interface.Internal; @@ -50,7 +51,7 @@ public interface IActiveNotification : INotification /// This does not override . void ExtendBy(TimeSpan extension); - /// Sets the icon from , overriding the icon . + /// Sets the icon from , overriding the icon. /// The new texture wrap to use, or null to clear and revert back to the icon specified /// from . /// @@ -61,6 +62,21 @@ public interface IActiveNotification : INotification /// void SetIconTexture(IDalamudTextureWrap? textureWrap); + /// Sets the icon from , overriding the icon, once the given task + /// completes. + /// The task that will result in a new texture wrap to use, or null to clear and + /// revert back to the icon specified from . + /// + /// The texture resulted from the passed will be disposed when the notification + /// is dismissed or a new different texture is set via another call to this function. You do not have to dispose the + /// resulted instance of yourself. + /// If the task fails for any reason, the exception will be silently ignored and the icon specified from + /// will be used instead. + /// If is not null, then calling this function will simply dispose the + /// result of the passed without actually updating the icon. + /// + void SetIconTexture(Task? textureWrapTask); + /// Generates a new value to use for . /// The new value. internal static long CreateNewId() => Interlocked.Increment(ref idCounter); diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 2bc8e751c..207722c56 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -1,3 +1,6 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Plugin.Services; @@ -20,8 +23,10 @@ public interface INotification NotificationType Type { get; set; } /// Gets or sets the icon source. - /// Use to use a texture, after calling - /// . + /// Use or + /// to use a texture, after calling + /// . Call either of those functions with null to revert + /// the effective icon back to this property. INotificationIcon? Icon { get; set; } /// Gets or sets the hard expiry. diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index a9950745d..c54a9c6fa 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -1,5 +1,6 @@ using System.Runtime.Loader; using System.Threading; +using System.Threading.Tasks; using Dalamud.Interface.Animation; using Dalamud.Interface.Animation.EasingFunctions; @@ -29,7 +30,7 @@ internal sealed partial class ActiveNotification : IActiveNotification private DateTime extendedExpiry; /// The icon texture to use if specified; otherwise, icon will be used from . - private IDalamudTextureWrap? iconTextureWrap; + private Task? iconTextureWrap; /// The plugin that initiated this notification. private LocalPlugin? initiatorPlugin; @@ -229,18 +230,24 @@ internal sealed partial class ActiveNotification : IActiveNotification /// public void SetIconTexture(IDalamudTextureWrap? textureWrap) + { + this.SetIconTexture(textureWrap is null ? null : Task.FromResult(textureWrap)); + } + + /// + public void SetIconTexture(Task? textureWrapTask) { if (this.DismissReason is not null) { - textureWrap?.Dispose(); + textureWrapTask?.ToContentDisposedTask(true); return; } // After replacing, if the old texture is not the old texture, then dispose the old texture. - if (Interlocked.Exchange(ref this.iconTextureWrap, textureWrap) is { } wrapToDispose && - wrapToDispose != textureWrap) + if (Interlocked.Exchange(ref this.iconTextureWrap, textureWrapTask) is { } wrapTaskToDispose && + wrapTaskToDispose != textureWrapTask) { - wrapToDispose.Dispose(); + wrapTaskToDispose.ToContentDisposedTask(true); } } @@ -333,8 +340,8 @@ internal sealed partial class ActiveNotification : IActiveNotification /// Clears the resources associated with this instance of . internal void DisposeInternal() { - if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapToDispose) - wrapToDispose.Dispose(); + if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapTaskToDispose) + wrapTaskToDispose.ToContentDisposedTask(true); this.Dismiss = null; this.Click = null; this.DrawActions = null; diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 0ec2561fd..0ed552b42 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -1,6 +1,7 @@ using System.IO; using System.Numerics; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface.Internal; @@ -103,6 +104,16 @@ public static class NotificationUtilities } } + /// Draws an icon from an instance of that results in an + /// . + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The task that results in a texture. + /// true if anything has been drawn. + /// Exceptions from the task will be treated as if no texture is provided. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, Task? textureTask) => + textureTask?.IsCompletedSuccessfully is true && DrawIconFrom(minCoord, maxCoord, textureTask.Result); + /// Draws an icon from an instance of . /// The coordinates of the top left of the icon area. /// The coordinates of the bottom right of the icon area. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 47c5993cd..95119bb48 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -88,20 +88,20 @@ internal class ImGuiWidget : IDataWindowWidget ref this.notificationTemplate.IconText, 255); break; - case 3 or 4: + case 5 or 6: ImGui.Combo( "Asset##iconAssetCombo", ref this.notificationTemplate.IconAssetInt, NotificationTemplate.AssetSources, NotificationTemplate.AssetSources.Length); break; - case 5 or 7: + case 3 or 7: ImGui.InputText( "Game Path##iconText", ref this.notificationTemplate.IconText, 255); break; - case 6 or 8: + case 4 or 8: ImGui.InputText( "File Path##iconText", ref this.notificationTemplate.IconText, @@ -206,9 +206,15 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); break; case 6: - n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconText)); + n.SetIconTexture( + dam.GetDalamudTextureWrapAsync( + Enum.Parse( + NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); break; case 7: + n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconText)); + break; + case 8: n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconText))); break; } @@ -315,6 +321,7 @@ internal class ImGuiWidget : IDataWindowWidget "GamePath", "FilePath", "TextureWrap from DalamudAssets", + "TextureWrap from DalamudAssets(Async)", "TextureWrap from GamePath", "TextureWrap from FilePath", }; From 16022ea46affad6604b1a71c3f799bd50560a8ae Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Fri, 1 Mar 2024 00:46:23 +0900 Subject: [PATCH 560/585] Always show focus indicator if focused --- .../Internal/ActiveNotification.ImGui.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index 5f7d1a0fd..d4a08ff69 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -75,14 +75,21 @@ internal sealed partial class ActiveNotification ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoDocking); - var isTakingKeyboardInput = ImGui.IsWindowFocused() && ImGui.GetIO().WantTextInput; + var isFocused = ImGui.IsWindowFocused(); var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + var isTakingKeyboardInput = isFocused && ImGui.GetIO().WantTextInput; var warrantsExtension = this.ExtensionDurationSinceLastInterest > TimeSpan.Zero && (isHovered || isTakingKeyboardInput); this.EffectiveExpiry = this.CalculateEffectiveExpiry(ref warrantsExtension); + if (!isTakingKeyboardInput && !isHovered && isFocused) + { + ImGui.SetWindowFocus(null); + isFocused = false; + } + if (DateTime.Now > this.EffectiveExpiry) this.DismissNow(NotificationDismissReason.Timeout); @@ -105,8 +112,8 @@ internal sealed partial class ActiveNotification ImGui.PopStyleVar(); } - if (isTakingKeyboardInput) - this.DrawKeyboardInputIndicator(); + if (isFocused) + this.DrawFocusIndicator(); this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension); if (ImGui.IsWindowHovered()) @@ -218,7 +225,7 @@ internal sealed partial class ActiveNotification ImGui.PopClipRect(); } - private void DrawKeyboardInputIndicator() + private void DrawFocusIndicator() { var windowPos = ImGui.GetWindowPos(); var windowSize = ImGui.GetWindowSize(); From 3d59fa3da0a2d5292d0517c5d5ba59bdc26a3638 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 1 Mar 2024 08:13:33 +0900 Subject: [PATCH 561/585] Sanitize PDB root name from loaded modules (#1687) --- Dalamud.Boot/Dalamud.Boot.vcxproj | 4 +- Dalamud.Boot/Dalamud.Boot.vcxproj.filters | 6 ++ Dalamud.Boot/hooks.cpp | 32 +------ Dalamud.Boot/hooks.h | 1 - Dalamud.Boot/ntdll.cpp | 15 +++ Dalamud.Boot/ntdll.h | 33 +++++++ Dalamud.Boot/pch.h | 7 ++ Dalamud.Boot/xivfixes.cpp | 109 +++++++++++++++++++++- Dalamud.Boot/xivfixes.h | 1 + Dalamud.Injector/EntryPoint.cs | 12 ++- 10 files changed, 181 insertions(+), 39 deletions(-) create mode 100644 Dalamud.Boot/ntdll.cpp create mode 100644 Dalamud.Boot/ntdll.h diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj b/Dalamud.Boot/Dalamud.Boot.vcxproj index ab68c1ec0..298edbcbc 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj @@ -58,7 +58,7 @@ Windows true false - Version.lib;%(AdditionalDependencies) + Version.lib;Shlwapi.lib;%(AdditionalDependencies) ..\lib\CoreCLR;%(AdditionalLibraryDirectories) @@ -137,6 +137,7 @@ NotUsing NotUsing + NotUsing NotUsing @@ -176,6 +177,7 @@ + diff --git a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters index a1b1650e2..87eaf6fcc 100644 --- a/Dalamud.Boot/Dalamud.Boot.vcxproj.filters +++ b/Dalamud.Boot/Dalamud.Boot.vcxproj.filters @@ -73,6 +73,9 @@ Dalamud.Boot DLL + + Dalamud.Boot DLL + @@ -140,6 +143,9 @@ + + Dalamud.Boot DLL + diff --git a/Dalamud.Boot/hooks.cpp b/Dalamud.Boot/hooks.cpp index 7cf489195..1b1280cf0 100644 --- a/Dalamud.Boot/hooks.cpp +++ b/Dalamud.Boot/hooks.cpp @@ -2,39 +2,9 @@ #include "hooks.h" +#include "ntdll.h" #include "logging.h" -enum { - LDR_DLL_NOTIFICATION_REASON_LOADED = 1, - LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2, -}; - -struct LDR_DLL_UNLOADED_NOTIFICATION_DATA { - ULONG Flags; //Reserved. - const UNICODE_STRING* FullDllName; //The full path name of the DLL module. - const UNICODE_STRING* BaseDllName; //The base file name of the DLL module. - PVOID DllBase; //A pointer to the base address for the DLL in memory. - ULONG SizeOfImage; //The size of the DLL image, in bytes. -}; - -struct LDR_DLL_LOADED_NOTIFICATION_DATA { - ULONG Flags; //Reserved. - const UNICODE_STRING* FullDllName; //The full path name of the DLL module. - const UNICODE_STRING* BaseDllName; //The base file name of the DLL module. - PVOID DllBase; //A pointer to the base address for the DLL in memory. - ULONG SizeOfImage; //The size of the DLL image, in bytes. -}; - -union LDR_DLL_NOTIFICATION_DATA { - LDR_DLL_LOADED_NOTIFICATION_DATA Loaded; - LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded; -}; - -using PLDR_DLL_NOTIFICATION_FUNCTION = VOID CALLBACK(_In_ ULONG NotificationReason, _In_ const LDR_DLL_NOTIFICATION_DATA* NotificationData, _In_opt_ PVOID Context); - -static const auto LdrRegisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function("LdrRegisterDllNotification"); -static const auto LdrUnregisterDllNotification = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function("LdrUnregisterDllNotification"); - hooks::getprocaddress_singleton_import_hook::getprocaddress_singleton_import_hook() : m_pfnGetProcAddress(GetProcAddress) , m_thunk("kernel32!GetProcAddress(Singleton Import Hook)", diff --git a/Dalamud.Boot/hooks.h b/Dalamud.Boot/hooks.h index ad3b2cc6c..f6ad370d1 100644 --- a/Dalamud.Boot/hooks.h +++ b/Dalamud.Boot/hooks.h @@ -1,6 +1,5 @@ #pragma once -#include #include #include "utils.h" diff --git a/Dalamud.Boot/ntdll.cpp b/Dalamud.Boot/ntdll.cpp new file mode 100644 index 000000000..9bda0e1c4 --- /dev/null +++ b/Dalamud.Boot/ntdll.cpp @@ -0,0 +1,15 @@ +#include "pch.h" + +#include "ntdll.h" + +#include "utils.h" + +NTSTATUS LdrRegisterDllNotification(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie) { + static const auto pfn = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function("LdrRegisterDllNotification"); + return pfn(Flags, NotificationFunction, Context, Cookie); +} + +NTSTATUS LdrUnregisterDllNotification(PVOID Cookie) { + static const auto pfn = utils::loaded_module(GetModuleHandleW(L"ntdll.dll")).get_exported_function("LdrUnregisterDllNotification"); + return pfn(Cookie); +} diff --git a/Dalamud.Boot/ntdll.h b/Dalamud.Boot/ntdll.h new file mode 100644 index 000000000..c631475d1 --- /dev/null +++ b/Dalamud.Boot/ntdll.h @@ -0,0 +1,33 @@ +#pragma once + +// ntdll exports +enum { + LDR_DLL_NOTIFICATION_REASON_LOADED = 1, + LDR_DLL_NOTIFICATION_REASON_UNLOADED = 2, +}; + +struct LDR_DLL_UNLOADED_NOTIFICATION_DATA { + ULONG Flags; //Reserved. + const UNICODE_STRING* FullDllName; //The full path name of the DLL module. + const UNICODE_STRING* BaseDllName; //The base file name of the DLL module. + PVOID DllBase; //A pointer to the base address for the DLL in memory. + ULONG SizeOfImage; //The size of the DLL image, in bytes. +}; + +struct LDR_DLL_LOADED_NOTIFICATION_DATA { + ULONG Flags; //Reserved. + const UNICODE_STRING* FullDllName; //The full path name of the DLL module. + const UNICODE_STRING* BaseDllName; //The base file name of the DLL module. + PVOID DllBase; //A pointer to the base address for the DLL in memory. + ULONG SizeOfImage; //The size of the DLL image, in bytes. +}; + +union LDR_DLL_NOTIFICATION_DATA { + LDR_DLL_LOADED_NOTIFICATION_DATA Loaded; + LDR_DLL_UNLOADED_NOTIFICATION_DATA Unloaded; +}; + +using PLDR_DLL_NOTIFICATION_FUNCTION = VOID CALLBACK(_In_ ULONG NotificationReason, _In_ const LDR_DLL_NOTIFICATION_DATA* NotificationData, _In_opt_ PVOID Context); + +NTSTATUS LdrRegisterDllNotification(ULONG Flags, PLDR_DLL_NOTIFICATION_FUNCTION NotificationFunction, PVOID Context, PVOID* Cookie); +NTSTATUS LdrUnregisterDllNotification(PVOID Cookie); diff --git a/Dalamud.Boot/pch.h b/Dalamud.Boot/pch.h index a09882c74..c2194c157 100644 --- a/Dalamud.Boot/pch.h +++ b/Dalamud.Boot/pch.h @@ -15,14 +15,20 @@ #include // Windows Header Files (2) +#include #include #include +#include #include #include #include +#include #include #include +// Windows Header Files (3) +#include // Must be loaded after iphlpapi.h + // MSVC Compiler Intrinsic #include @@ -30,6 +36,7 @@ #include // C++ Standard Libraries +#include #include #include #include diff --git a/Dalamud.Boot/xivfixes.cpp b/Dalamud.Boot/xivfixes.cpp index 39cce53c9..f3b6aaa2c 100644 --- a/Dalamud.Boot/xivfixes.cpp +++ b/Dalamud.Boot/xivfixes.cpp @@ -5,9 +5,8 @@ #include "DalamudStartInfo.h" #include "hooks.h" #include "logging.h" +#include "ntdll.h" #include "utils.h" -#include -#include template static std::span assume_nonempty_span(std::span t, const char* descr) { @@ -546,6 +545,109 @@ void xivfixes::prevent_icmphandle_crashes(bool bApply) { } } +void xivfixes::symbol_load_patches(bool bApply) { + static const char* LogTag = "[xivfixes:symbol_load_patches]"; + + static std::optional> s_hookSymInitialize; + static PVOID s_dllNotificationCookie = nullptr; + + static const auto RemoveFullPathPdbInfo = [](const utils::loaded_module& mod) { + const auto ddva = mod.data_directory(IMAGE_DIRECTORY_ENTRY_DEBUG).VirtualAddress; + if (!ddva) + return; + + const auto& ddir = mod.ref_as(ddva); + if (ddir.Type == IMAGE_DEBUG_TYPE_CODEVIEW) { + // The Visual C++ debug information. + // Ghidra calls it "DotNetPdbInfo". + static constexpr DWORD DotNetPdbInfoSignatureValue = 0x53445352; + struct DotNetPdbInfo { + DWORD Signature; // RSDS + GUID Guid; + DWORD Age; + char PdbPath[1]; + }; + + const auto& pdbref = mod.ref_as(ddir.AddressOfRawData); + if (pdbref.Signature == DotNetPdbInfoSignatureValue) { + const auto pathSpan = std::string_view(pdbref.PdbPath, strlen(pdbref.PdbPath)); + const auto pathWide = unicode::convert(pathSpan); + std::wstring windowsDirectory(GetWindowsDirectoryW(nullptr, 0) + 1, L'\0'); + windowsDirectory.resize( + GetWindowsDirectoryW(windowsDirectory.data(), static_cast(windowsDirectory.size()))); + if (!PathIsRelativeW(pathWide.c_str()) && !PathIsSameRootW(windowsDirectory.c_str(), pathWide.c_str())) { + utils::memory_tenderizer pathOverwrite(&pdbref.PdbPath, pathSpan.size(), PAGE_READWRITE); + auto sep = std::find(pathSpan.rbegin(), pathSpan.rend(), '/'); + if (sep == pathSpan.rend()) + sep = std::find(pathSpan.rbegin(), pathSpan.rend(), '\\'); + if (sep != pathSpan.rend()) { + logging::I( + "{} Stripping pdb path folder: {} to {}", + LogTag, + pathSpan, + &*sep + 1); + memmove(const_cast(pathSpan.data()), &*sep + 1, sep - pathSpan.rbegin() + 1); + } else { + logging::I("{} Leaving pdb path unchanged: {}", LogTag, pathSpan); + } + } else { + logging::I("{} Leaving pdb path unchanged: {}", LogTag, pathSpan); + } + } else { + logging::I("{} CODEVIEW struct signature mismatch: got {:08X} instead.", LogTag, pdbref.Signature); + } + } else { + logging::I("{} Debug directory: type {} is unsupported.", LogTag, ddir.Type); + } + }; + + if (bApply) { + if (!g_startInfo.BootEnabledGameFixes.contains("symbol_load_patches")) { + logging::I("{} Turned off via environment variable.", LogTag); + return; + } + + for (const auto& mod : utils::loaded_module::all_modules()) + RemoveFullPathPdbInfo(mod); + + if (!s_dllNotificationCookie) { + const auto res = LdrRegisterDllNotification( + 0, + [](ULONG notiReason, const LDR_DLL_NOTIFICATION_DATA* pData, void* /* context */) { + if (notiReason == LDR_DLL_NOTIFICATION_REASON_LOADED) + RemoveFullPathPdbInfo(pData->Loaded.DllBase); + }, + nullptr, + &s_dllNotificationCookie); + + if (res != STATUS_SUCCESS) { + logging::E("{} LdrRegisterDllNotification failure: 0x{:08X}", LogTag, res); + s_dllNotificationCookie = nullptr; + } + } + + s_hookSymInitialize.emplace("dbghelp.dll!SymInitialize (import, symbol_load_patches)", "dbghelp.dll", "SymInitialize", 0); + s_hookSymInitialize->set_detour([](HANDLE hProcess, PCSTR UserSearchPath, BOOL fInvadeProcess) noexcept { + logging::I("{} Suppressed SymInitialize.", LogTag); + SetLastError(ERROR_NOT_SUPPORTED); + return FALSE; + }); + + logging::I("{} Enable", LogTag); + } + else { + if (s_hookSymInitialize) { + logging::I("{} Disable", LogTag); + s_hookSymInitialize.reset(); + } + + if (s_dllNotificationCookie) { + (void)LdrUnregisterDllNotification(s_dllNotificationCookie); + s_dllNotificationCookie = nullptr; + } + } +} + void xivfixes::apply_all(bool bApply) { for (const auto& [taskName, taskFunction] : std::initializer_list> { @@ -554,7 +656,8 @@ void xivfixes::apply_all(bool bApply) { { "disable_game_openprocess_access_check", &disable_game_openprocess_access_check }, { "redirect_openprocess", &redirect_openprocess }, { "backup_userdata_save", &backup_userdata_save }, - { "prevent_icmphandle_crashes", &prevent_icmphandle_crashes } + { "prevent_icmphandle_crashes", &prevent_icmphandle_crashes }, + { "symbol_load_patches", &symbol_load_patches }, } ) { try { diff --git a/Dalamud.Boot/xivfixes.h b/Dalamud.Boot/xivfixes.h index f534ad7dd..afe2edb45 100644 --- a/Dalamud.Boot/xivfixes.h +++ b/Dalamud.Boot/xivfixes.h @@ -7,6 +7,7 @@ namespace xivfixes { void redirect_openprocess(bool bApply); void backup_userdata_save(bool bApply); void prevent_icmphandle_crashes(bool bApply); + void symbol_load_patches(bool bApply); void apply_all(bool bApply); } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 2d776b043..9085eae04 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -395,9 +395,15 @@ namespace Dalamud.Injector startInfo.BootShowConsole = args.Contains("--console"); startInfo.BootEnableEtw = args.Contains("--etw"); startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName); - startInfo.BootEnabledGameFixes = new List { - "prevent_devicechange_crashes", "disable_game_openprocess_access_check", - "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes", + startInfo.BootEnabledGameFixes = new() + { + // See: xivfixes.h, xivfixes.cpp + "prevent_devicechange_crashes", + "disable_game_openprocess_access_check", + "redirect_openprocess", + "backup_userdata_save", + "prevent_icmphandle_crashes", + "symbol_load_patches", }; startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; From 5f62c703bff4137a1f887553fc1e0bd932d6dc6e Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Thu, 29 Feb 2024 15:15:02 -0800 Subject: [PATCH 562/585] Add IContextMenu service (#1682) --- Dalamud/Game/Gui/ContextMenu/ContextMenu.cs | 560 ++++++++++++++++++ .../Game/Gui/ContextMenu/ContextMenuType.cs | 18 + Dalamud/Game/Gui/ContextMenu/MenuArgs.cs | 77 +++ Dalamud/Game/Gui/ContextMenu/MenuItem.cs | 91 +++ .../Gui/ContextMenu/MenuItemClickedArgs.cs | 44 ++ .../Game/Gui/ContextMenu/MenuOpenedArgs.cs | 34 ++ Dalamud/Game/Gui/ContextMenu/MenuTarget.cs | 9 + .../Game/Gui/ContextMenu/MenuTargetDefault.cs | 67 +++ .../Gui/ContextMenu/MenuTargetInventory.cs | 36 ++ Dalamud/Game/Inventory/GameInventoryItem.cs | 12 +- .../Structures/InfoProxy/CharacterData.cs | 197 ++++++ .../AgingSteps/ContextMenuAgingStep.cs | 333 ++++++----- Dalamud/Plugin/Services/IContextMenu.cs | 37 ++ Dalamud/Utility/EventHandlerExtensions.cs | 18 + 14 files changed, 1387 insertions(+), 146 deletions(-) create mode 100644 Dalamud/Game/Gui/ContextMenu/ContextMenu.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuArgs.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuItem.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuTarget.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs create mode 100644 Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs create mode 100644 Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs create mode 100644 Dalamud/Plugin/Services/IContextMenu.cs diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs new file mode 100644 index 000000000..65c9b2760 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -0,0 +1,560 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Memory; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +using FFXIVClientStructs.FFXIV.Client.System.Memory; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; +using FFXIVClientStructs.Interop; + +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// This class handles interacting with the game's (right-click) context menu. +/// +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu +{ + private static readonly ModuleLog Log = new("ContextMenu"); + + private readonly Hook raptureAtkModuleOpenAddonByAgentHook; + private readonly Hook addonContextMenuOnMenuSelectedHook; + private readonly RaptureAtkModuleOpenAddonDelegate raptureAtkModuleOpenAddon; + + [ServiceManager.ServiceConstructor] + private ContextMenu() + { + this.raptureAtkModuleOpenAddonByAgentHook = Hook.FromAddress((nint)RaptureAtkModule.Addresses.OpenAddonByAgent.Value, this.RaptureAtkModuleOpenAddonByAgentDetour); + this.addonContextMenuOnMenuSelectedHook = Hook.FromAddress((nint)AddonContextMenu.StaticVTable.OnMenuSelected, this.AddonContextMenuOnMenuSelectedDetour); + this.raptureAtkModuleOpenAddon = Marshal.GetDelegateForFunctionPointer((nint)RaptureAtkModule.Addresses.OpenAddon.Value); + + this.raptureAtkModuleOpenAddonByAgentHook.Enable(); + this.addonContextMenuOnMenuSelectedHook.Enable(); + } + + private unsafe delegate ushort RaptureAtkModuleOpenAddonByAgentDelegate(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId); + + private unsafe delegate bool AddonContextMenuOnMenuSelectedDelegate(AddonContextMenu* addon, int selectedIdx, byte a3); + + private unsafe delegate ushort RaptureAtkModuleOpenAddonDelegate(RaptureAtkModule* a1, uint addonNameId, uint valueCount, AtkValue* values, AgentInterface* parentAgent, ulong unk, ushort parentAddonId, int unk2); + + /// + public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened; + + private Dictionary> MenuItems { get; } = new(); + + private object MenuItemsLock { get; } = new(); + + private AgentInterface* SelectedAgent { get; set; } + + private ContextMenuType? SelectedMenuType { get; set; } + + private List? SelectedItems { get; set; } + + private HashSet SelectedEventInterfaces { get; } = new(); + + private AtkUnitBase* SelectedParentAddon { get; set; } + + // -1 -> -inf: native items + // 0 -> inf: selected items + private List MenuCallbackIds { get; } = new(); + + private IReadOnlyList? SubmenuItems { get; set; } + + /// + public void Dispose() + { + var manager = RaptureAtkUnitManager.Instance(); + var menu = manager->GetAddonByName("ContextMenu"); + var submenu = manager->GetAddonByName("AddonContextSub"); + if (menu->IsVisible) + menu->FireCallbackInt(-1); + if (submenu->IsVisible) + submenu->FireCallbackInt(-1); + + this.raptureAtkModuleOpenAddonByAgentHook.Dispose(); + this.addonContextMenuOnMenuSelectedHook.Dispose(); + } + + /// + public void AddMenuItem(ContextMenuType menuType, MenuItem item) + { + lock (this.MenuItemsLock) + { + if (!this.MenuItems.TryGetValue(menuType, out var items)) + this.MenuItems[menuType] = items = new(); + items.Add(item); + } + } + + /// + public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item) + { + lock (this.MenuItemsLock) + { + if (!this.MenuItems.TryGetValue(menuType, out var items)) + return false; + return items.Remove(item); + } + } + + private AtkValue* ExpandContextMenuArray(Span oldValues, int newSize) + { + // if the array has enough room, don't reallocate + if (oldValues.Length >= newSize) + return (AtkValue*)Unsafe.AsPointer(ref oldValues[0]); + + var size = (sizeof(AtkValue) * newSize) + 8; + var newArray = (nint)IMemorySpace.GetUISpace()->Malloc((ulong)size, 0); + if (newArray == nint.Zero) + throw new OutOfMemoryException(); + NativeMemory.Fill((void*)newArray, (nuint)size, 0); + + *(ulong*)newArray = (ulong)newSize; + + // copy old memory if existing + if (!oldValues.IsEmpty) + oldValues.CopyTo(new((void*)(newArray + 8), oldValues.Length)); + + return (AtkValue*)(newArray + 8); + } + + private void FreeExpandedContextMenuArray(AtkValue* newValues, int newSize) => + IMemorySpace.Free((void*)((nint)newValues - 8), (ulong)((newSize * sizeof(AtkValue)) + 8)); + + private AtkValue* CreateEmptySubmenuContextMenuArray(SeString name, int x, int y, out int valueCount) + { + // 0: UInt = ContextItemCount + // 1: String = Name + // 2: Int = PositionX + // 3: Int = PositionY + // 4: Bool = false + // 5: UInt = ContextItemSubmenuMask + // 6: UInt = ReturnArrowMask (_gap_0x6BC ? 1 << (ContextItemCount - 1) : 0) + // 7: UInt = 1 + + valueCount = 8; + var values = this.ExpandContextMenuArray(Span.Empty, valueCount); + values[0].ChangeType(ValueType.UInt); + values[0].UInt = 0; + values[1].ChangeType(ValueType.String); + values[1].SetString(name.Encode().NullTerminate()); + values[2].ChangeType(ValueType.Int); + values[2].Int = x; + values[3].ChangeType(ValueType.Int); + values[3].Int = y; + values[4].ChangeType(ValueType.Bool); + values[4].Byte = 0; + values[5].ChangeType(ValueType.UInt); + values[5].UInt = 0; + values[6].ChangeType(ValueType.UInt); + values[6].UInt = 0; + values[7].ChangeType(ValueType.UInt); + values[7].UInt = 1; + return values; + } + + private void SetupGenericMenu(int headerCount, int sizeHeaderIdx, int returnHeaderIdx, int submenuHeaderIdx, IReadOnlyList items, ref int valueCount, ref AtkValue* values) + { + var itemsWithIdx = items.Select((item, idx) => (item, idx)).OrderBy(i => i.item.Priority); + var prefixItems = itemsWithIdx.Where(i => i.item.Priority < 0).ToArray(); + var suffixItems = itemsWithIdx.Where(i => i.item.Priority >= 0).ToArray(); + + var nativeMenuSize = (int)values[sizeHeaderIdx].UInt; + var prefixMenuSize = prefixItems.Length; + var suffixMenuSize = suffixItems.Length; + + var hasGameDisabled = valueCount - headerCount - nativeMenuSize > 0; + + var hasCustomDisabled = items.Any(item => !item.IsEnabled); + var hasAnyDisabled = hasGameDisabled || hasCustomDisabled; + + values = this.ExpandContextMenuArray( + new(values, valueCount), + valueCount = (nativeMenuSize + items.Count) * (hasAnyDisabled ? 2 : 1) + headerCount); + var offsetData = new Span(values, headerCount); + var nameData = new Span(values + headerCount, nativeMenuSize + items.Count); + var disabledData = hasAnyDisabled ? new Span(values + headerCount + nativeMenuSize + items.Count, nativeMenuSize + items.Count) : Span.Empty; + + var returnMask = offsetData[returnHeaderIdx].UInt; + var submenuMask = offsetData[submenuHeaderIdx].UInt; + + nameData[..nativeMenuSize].CopyTo(nameData.Slice(prefixMenuSize, nativeMenuSize)); + if (hasAnyDisabled) + { + if (hasGameDisabled) + { + // copy old disabled data + var oldDisabledData = new Span(values + headerCount + nativeMenuSize, nativeMenuSize); + oldDisabledData.CopyTo(disabledData.Slice(prefixMenuSize, nativeMenuSize)); + } + else + { + // enable all + for (var i = prefixMenuSize; i < prefixMenuSize + nativeMenuSize; ++i) + { + disabledData[i].ChangeType(ValueType.Int); + disabledData[i].Int = 0; + } + } + } + + returnMask <<= prefixMenuSize; + submenuMask <<= prefixMenuSize; + + void FillData(Span disabledData, Span nameData, int i, MenuItem item, int idx) + { + this.MenuCallbackIds.Add(idx); + + if (hasAnyDisabled) + { + disabledData[i].ChangeType(ValueType.Int); + disabledData[i].Int = item.IsEnabled ? 0 : 1; + } + + if (item.IsReturn) + returnMask |= 1u << i; + if (item.IsSubmenu) + submenuMask |= 1u << i; + + nameData[i].ChangeType(ValueType.String); + nameData[i].SetString(item.PrefixedName.Encode().NullTerminate()); + } + + for (var i = 0; i < prefixMenuSize; ++i) + { + var (item, idx) = prefixItems[i]; + FillData(disabledData, nameData, i, item, idx); + } + + this.MenuCallbackIds.AddRange(Enumerable.Range(0, nativeMenuSize).Select(i => -i - 1)); + + for (var i = prefixMenuSize + nativeMenuSize; i < prefixMenuSize + nativeMenuSize + suffixMenuSize; ++i) + { + var (item, idx) = suffixItems[i - prefixMenuSize - nativeMenuSize]; + FillData(disabledData, nameData, i, item, idx); + } + + offsetData[returnHeaderIdx].UInt = returnMask; + offsetData[submenuHeaderIdx].UInt = submenuMask; + + offsetData[sizeHeaderIdx].UInt += (uint)items.Count; + } + + private void SetupContextMenu(IReadOnlyList items, ref int valueCount, ref AtkValue* values) + { + // 0: UInt = Item Count + // 1: UInt = 0 (probably window name, just unused) + // 2: UInt = Return Mask (?) + // 3: UInt = Submenu Mask + // 4: UInt = OpenAtCursorPosition ? 2 : 1 + // 5: UInt = 0 + // 6: UInt = 0 + + foreach (var item in items) + { + if (!item.Prefix.HasValue) + { + item.PrefixChar = 'D'; + item.PrefixColor = 539; + Log.Warning($"Menu item \"{item.Name}\" has no prefix, defaulting to Dalamud's. Menu items outside of a submenu must have a prefix."); + } + } + + this.SetupGenericMenu(7, 0, 2, 3, items, ref valueCount, ref values); + } + + private void SetupContextSubMenu(IReadOnlyList items, ref int valueCount, ref AtkValue* values) + { + // 0: UInt = ContextItemCount + // 1: skipped? + // 2: Int = PositionX + // 3: Int = PositionY + // 4: Bool = false + // 5: UInt = ContextItemSubmenuMask + // 6: UInt = _gap_0x6BC ? 1 << (ContextItemCount - 1) : 0 + // 7: UInt = 1 + + this.SetupGenericMenu(8, 0, 6, 5, items, ref valueCount, ref values); + } + + private ushort RaptureAtkModuleOpenAddonByAgentDetour(RaptureAtkModule* module, byte* addonName, AtkUnitBase* addon, int valueCount, AtkValue* values, AgentInterface* agent, nint a7, ushort parentAddonId) + { + var oldValues = values; + + if (MemoryHelper.EqualsZeroTerminatedString("ContextMenu", (nint)addonName)) + { + this.MenuCallbackIds.Clear(); + this.SelectedAgent = agent; + this.SelectedParentAddon = module->RaptureAtkUnitManager.GetAddonById(parentAddonId); + this.SelectedEventInterfaces.Clear(); + if (this.SelectedAgent == AgentInventoryContext.Instance()) + { + this.SelectedMenuType = ContextMenuType.Inventory; + } + else if (this.SelectedAgent == AgentContext.Instance()) + { + this.SelectedMenuType = ContextMenuType.Default; + + var menu = AgentContext.Instance()->CurrentContextMenu; + var handlers = new Span>(menu->EventHandlerArray, 32); + var ids = new Span(menu->EventIdArray, 32); + var count = (int)values[0].UInt; + handlers = handlers.Slice(7, count); + ids = ids.Slice(7, count); + for (var i = 0; i < count; ++i) + { + if (ids[i] <= 106) + continue; + this.SelectedEventInterfaces.Add((nint)handlers[i].Value); + } + } + else + { + this.SelectedMenuType = null; + } + + this.SubmenuItems = null; + + if (this.SelectedMenuType is { } menuType) + { + lock (this.MenuItemsLock) + { + if (this.MenuItems.TryGetValue(menuType, out var items)) + this.SelectedItems = new(items); + else + this.SelectedItems = new(); + } + + var args = new MenuOpenedArgs(this.SelectedItems.Add, this.SelectedParentAddon, this.SelectedAgent, this.SelectedMenuType.Value, this.SelectedEventInterfaces); + this.OnMenuOpened?.InvokeSafely(args); + this.SelectedItems = this.FixupMenuList(this.SelectedItems, (int)values[0].UInt); + this.SetupContextMenu(this.SelectedItems, ref valueCount, ref values); + Log.Verbose($"Opening {this.SelectedMenuType} context menu with {this.SelectedItems.Count} custom items."); + } + else + { + this.SelectedItems = null; + } + } + else if (MemoryHelper.EqualsZeroTerminatedString("AddonContextSub", (nint)addonName)) + { + this.MenuCallbackIds.Clear(); + if (this.SubmenuItems != null) + { + this.SubmenuItems = this.FixupMenuList(this.SubmenuItems.ToList(), (int)values[0].UInt); + + this.SetupContextSubMenu(this.SubmenuItems, ref valueCount, ref values); + Log.Verbose($"Opening {this.SelectedMenuType} submenu with {this.SubmenuItems.Count} custom items."); + } + } + + var ret = this.raptureAtkModuleOpenAddonByAgentHook.Original(module, addonName, addon, valueCount, values, agent, a7, parentAddonId); + if (values != oldValues) + this.FreeExpandedContextMenuArray(values, valueCount); + return ret; + } + + private List FixupMenuList(List items, int nativeMenuSize) + { + // The in game menu actually supports 32 items, but the last item can't have a visible submenu arrow. + // As such, we'll only work with 31 items. + const int MaxMenuItems = 31; + if (items.Count + nativeMenuSize > MaxMenuItems) + { + Log.Warning($"Menu size exceeds {MaxMenuItems} items, truncating."); + var orderedItems = items.OrderBy(i => i.Priority).ToArray(); + var newItems = orderedItems[..(MaxMenuItems - nativeMenuSize - 1)]; + var submenuItems = orderedItems[(MaxMenuItems - nativeMenuSize - 1)..]; + return newItems.Append(new MenuItem + { + Prefix = SeIconChar.BoxedLetterD, + PrefixColor = 539, + IsSubmenu = true, + Priority = int.MaxValue, + Name = $"See More ({submenuItems.Length})", + OnClicked = a => a.OpenSubmenu(submenuItems), + }).ToList(); + } + + return items; + } + + private void OpenSubmenu(SeString name, IReadOnlyList submenuItems, int posX, int posY) + { + if (submenuItems.Count == 0) + throw new ArgumentException("Submenu must not be empty", nameof(submenuItems)); + + this.SubmenuItems = submenuItems; + + var module = RaptureAtkModule.Instance(); + var values = this.CreateEmptySubmenuContextMenuArray(name, posX, posY, out var valueCount); + + switch (this.SelectedMenuType) + { + case ContextMenuType.Default: + { + var ownerAddonId = ((AgentContext*)this.SelectedAgent)->OwnerAddon; + this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 71, checked((ushort)ownerAddonId), 4); + break; + } + + case ContextMenuType.Inventory: + { + var ownerAddonId = ((AgentInventoryContext*)this.SelectedAgent)->OwnerAddonId; + this.raptureAtkModuleOpenAddon(module, 445, (uint)valueCount, values, this.SelectedAgent, 0, checked((ushort)ownerAddonId), 4); + break; + } + + default: + Log.Warning($"Unknown context menu type (agent: {(nint)this.SelectedAgent}, cannot open submenu"); + break; + } + + this.FreeExpandedContextMenuArray(values, valueCount); + } + + private bool AddonContextMenuOnMenuSelectedDetour(AddonContextMenu* addon, int selectedIdx, byte a3) + { + var items = this.SubmenuItems ?? this.SelectedItems; + if (items == null) + goto original; + if (this.MenuCallbackIds.Count == 0) + goto original; + if (selectedIdx < 0) + goto original; + if (selectedIdx >= this.MenuCallbackIds.Count) + goto original; + + var callbackId = this.MenuCallbackIds[selectedIdx]; + + if (callbackId < 0) + { + selectedIdx = -callbackId - 1; + goto original; + } + else + { + var item = items[callbackId]; + var openedSubmenu = false; + + try + { + if (item.OnClicked == null) + throw new InvalidOperationException("Item has no OnClicked handler"); + item.OnClicked.InvokeSafely(new( + (name, items) => + { + short x, y; + addon->AtkUnitBase.GetPosition(&x, &y); + this.OpenSubmenu(name ?? item.Name, items, x, y); + openedSubmenu = true; + }, + this.SelectedParentAddon, + this.SelectedAgent, + this.SelectedMenuType.Value, + this.SelectedEventInterfaces)); + } + catch (Exception e) + { + Log.Error(e, "Error while handling context menu click"); + } + + // Close with clicky sound + if (!openedSubmenu) + addon->AtkUnitBase.FireCallbackInt(-2); + return false; + } + +original: + // Eventually handled by inventorycontext here: 14022BBD0 (6.51) + return this.addonContextMenuOnMenuSelectedHook.Original(addon, selectedIdx, a3); + } +} + +/// +/// Plugin-scoped version of a service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu +{ + [ServiceManager.ServiceDependency] + private readonly ContextMenu parentService = Service.Get(); + + private ContextMenuPluginScoped() + { + this.parentService.OnMenuOpened += this.OnMenuOpenedForward; + } + + /// + public event IContextMenu.OnMenuOpenedDelegate OnMenuOpened; + + private Dictionary> MenuItems { get; } = new(); + + private object MenuItemsLock { get; } = new(); + + /// + public void Dispose() + { + this.parentService.OnMenuOpened -= this.OnMenuOpenedForward; + + this.OnMenuOpened = null; + + lock (this.MenuItemsLock) + { + foreach (var (menuType, items) in this.MenuItems) + { + foreach (var item in items) + this.parentService.RemoveMenuItem(menuType, item); + } + } + } + + /// + public void AddMenuItem(ContextMenuType menuType, MenuItem item) + { + lock (this.MenuItemsLock) + { + if (!this.MenuItems.TryGetValue(menuType, out var items)) + this.MenuItems[menuType] = items = new(); + items.Add(item); + } + + this.parentService.AddMenuItem(menuType, item); + } + + /// + public bool RemoveMenuItem(ContextMenuType menuType, MenuItem item) + { + lock (this.MenuItemsLock) + { + if (this.MenuItems.TryGetValue(menuType, out var items)) + items.Remove(item); + } + + return this.parentService.RemoveMenuItem(menuType, item); + } + + private void OnMenuOpenedForward(MenuOpenedArgs args) => + this.OnMenuOpened?.Invoke(args); +} diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs new file mode 100644 index 000000000..2cd52a4b7 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenuType.cs @@ -0,0 +1,18 @@ +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// The type of context menu. +/// Each one has a different associated . +/// +public enum ContextMenuType +{ + /// + /// The default context menu. + /// + Default, + + /// + /// The inventory context menu. Used when right-clicked on an item. + /// + Inventory, +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs b/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs new file mode 100644 index 000000000..d0d8ec0dc --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuArgs.cs @@ -0,0 +1,77 @@ +using System.Collections.Generic; + +using Dalamud.Memory; +using Dalamud.Plugin.Services; + +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Base class for menu args. +/// +public abstract unsafe class MenuArgs +{ + private IReadOnlySet? eventInterfaces; + + /// + /// Initializes a new instance of the class. + /// + /// Addon associated with the context menu. + /// Agent associated with the context menu. + /// The type of context menu. + /// List of AtkEventInterfaces associated with the context menu. + protected internal MenuArgs(AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet? eventInterfaces) + { + this.AddonName = addon != null ? MemoryHelper.ReadString((nint)addon->Name, 32) : null; + this.AddonPtr = (nint)addon; + this.AgentPtr = (nint)agent; + this.MenuType = type; + this.eventInterfaces = eventInterfaces; + this.Target = type switch + { + ContextMenuType.Default => new MenuTargetDefault((AgentContext*)agent), + ContextMenuType.Inventory => new MenuTargetInventory((AgentInventoryContext*)agent), + _ => throw new ArgumentException("Invalid context menu type", nameof(type)), + }; + } + + /// + /// Gets the name of the addon that opened the context menu. + /// + public string? AddonName { get; } + + /// + /// Gets the memory pointer of the addon that opened the context menu. + /// + public nint AddonPtr { get; } + + /// + /// Gets the memory pointer of the agent that opened the context menu. + /// + public nint AgentPtr { get; } + + /// + /// Gets the type of the context menu. + /// + public ContextMenuType MenuType { get; } + + /// + /// Gets the target info of the context menu. The actual type depends on . + /// signifies a . + /// signifies a . + /// + public MenuTarget Target { get; } + + /// + /// Gets a list of AtkEventInterface pointers associated with the context menu. + /// Only available with . + /// Almost always an agent pointer. You can use this to find out what type of context menu it is. + /// + /// Thrown when the context menu is not a . + public IReadOnlySet EventInterfaces => + this.MenuType != ContextMenuType.Default ? + this.eventInterfaces : + throw new InvalidOperationException("Not a default context menu"); +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuItem.cs b/Dalamud/Game/Gui/ContextMenu/MenuItem.cs new file mode 100644 index 000000000..fdeb64d13 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuItem.cs @@ -0,0 +1,91 @@ +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; + +using Lumina.Excel.GeneratedSheets; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// A menu item that can be added to a context menu. +/// +public sealed record MenuItem +{ + /// + /// Gets or sets the display name of the menu item. + /// + public SeString Name { get; set; } = SeString.Empty; + + /// + /// Gets or sets the prefix attached to the beginning of . + /// + public SeIconChar? Prefix { get; set; } + + /// + /// Sets the character to prefix the with. Will be converted into a fancy boxed letter icon. Must be an uppercase letter. + /// + /// must be an uppercase letter. + public char? PrefixChar + { + set + { + if (value is { } prefix) + { + if (!char.IsAsciiLetterUpper(prefix)) + throw new ArgumentException("Prefix must be an uppercase letter", nameof(value)); + + this.Prefix = SeIconChar.BoxedLetterA + prefix - 'A'; + } + else + { + this.Prefix = null; + } + } + } + + /// + /// Gets or sets the color of the . Specifies a row id. + /// + public ushort PrefixColor { get; set; } + + /// + /// Gets or sets the callback to be invoked when the menu item is clicked. + /// + public Action? OnClicked { get; set; } + + /// + /// Gets or sets the priority (or order) with which the menu item should be displayed in descending order. + /// Priorities below 0 will be displayed above the native menu items. + /// Other priorities will be displayed below the native menu items. + /// + public int Priority { get; set; } + + /// + /// Gets or sets a value indicating whether the menu item is enabled. + /// Disabled items will be faded and cannot be clicked on. + /// + public bool IsEnabled { get; set; } = true; + + /// + /// Gets or sets a value indicating whether the menu item is a submenu. + /// This value is purely visual. Submenu items will have an arrow to its right. + /// + public bool IsSubmenu { get; set; } + + /// + /// Gets or sets a value indicating whether the menu item is a return item. + /// This value is purely visual. Return items will have a back arrow to its left. + /// If both and are true, the return arrow will take precedence. + /// + public bool IsReturn { get; set; } + + /// + /// Gets the name with the given prefix. + /// + internal SeString PrefixedName => + this.Prefix is { } prefix + ? new SeStringBuilder() + .AddUiForeground($"{prefix.ToIconString()} ", this.PrefixColor) + .Append(this.Name) + .Build() + : this.Name; +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs b/Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs new file mode 100644 index 000000000..bec16590d --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuItemClickedArgs.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; + +using Dalamud.Game.Text.SeStringHandling; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Callback args used when a menu item is clicked. +/// +public sealed unsafe class MenuItemClickedArgs : MenuArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// Callback for opening a submenu. + /// Addon associated with the context menu. + /// Agent associated with the context menu. + /// The type of context menu. + /// List of AtkEventInterfaces associated with the context menu. + internal MenuItemClickedArgs(Action> openSubmenu, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet eventInterfaces) + : base(addon, agent, type, eventInterfaces) + { + this.OnOpenSubmenu = openSubmenu; + } + + private Action> OnOpenSubmenu { get; } + + /// + /// Opens a submenu with the given name and items. + /// + /// The name of the submenu, displayed at the top. + /// The items to display in the submenu. + public void OpenSubmenu(SeString name, IReadOnlyList items) => + this.OnOpenSubmenu(name, items); + + /// + /// Opens a submenu with the given items. + /// + /// The items to display in the submenu. + public void OpenSubmenu(IReadOnlyList items) => + this.OnOpenSubmenu(null, items); +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs b/Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs new file mode 100644 index 000000000..de3347f63 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuOpenedArgs.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Callback args used when a menu item is opened. +/// +public sealed unsafe class MenuOpenedArgs : MenuArgs +{ + /// + /// Initializes a new instance of the class. + /// + /// Callback for adding a custom menu item. + /// Addon associated with the context menu. + /// Agent associated with the context menu. + /// The type of context menu. + /// List of AtkEventInterfaces associated with the context menu. + internal MenuOpenedArgs(Action addMenuItem, AtkUnitBase* addon, AgentInterface* agent, ContextMenuType type, IReadOnlySet eventInterfaces) + : base(addon, agent, type, eventInterfaces) + { + this.OnAddMenuItem = addMenuItem; + } + + private Action OnAddMenuItem { get; } + + /// + /// Adds a custom menu item to the context menu. + /// + /// The menu item to add. + public void AddMenuItem(MenuItem item) => + this.OnAddMenuItem(item); +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuTarget.cs b/Dalamud/Game/Gui/ContextMenu/MenuTarget.cs new file mode 100644 index 000000000..c486a3b9b --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuTarget.cs @@ -0,0 +1,9 @@ +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Base class for contexts. +/// Discriminated based on . +/// +public abstract class MenuTarget +{ +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs b/Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs new file mode 100644 index 000000000..d87bc36b6 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuTargetDefault.cs @@ -0,0 +1,67 @@ +using Dalamud.Game.ClientState.Objects; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Game.ClientState.Resolvers; +using Dalamud.Game.Network.Structures.InfoProxy; + +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +using Lumina.Excel.GeneratedSheets; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Target information on a default context menu. +/// +public sealed unsafe class MenuTargetDefault : MenuTarget +{ + /// + /// Initializes a new instance of the class. + /// + /// The agent associated with the context menu. + internal MenuTargetDefault(AgentContext* context) + { + this.Context = context; + } + + /// + /// Gets the name of the target. + /// + public string TargetName => this.Context->TargetName.ToString(); + + /// + /// Gets the object id of the target. + /// + public ulong TargetObjectId => this.Context->TargetObjectId; + + /// + /// Gets the target object. + /// + public GameObject? TargetObject => Service.Get().SearchById(this.TargetObjectId); + + /// + /// Gets the content id of the target. + /// + public ulong TargetContentId => this.Context->TargetContentId; + + /// + /// Gets the home world id of the target. + /// + public ExcelResolver TargetHomeWorld => new((uint)this.Context->TargetHomeWorldId); + + /// + /// Gets the currently targeted character. Only shows up for specific targets, like friends, party finder listings, or party members. + /// Just because this is doesn't mean the target isn't a character. + /// + public CharacterData? TargetCharacter + { + get + { + var target = this.Context->CurrentContextMenuTarget; + if (target != null) + return new(target); + return null; + } + } + + private AgentContext* Context { get; } +} diff --git a/Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs b/Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs new file mode 100644 index 000000000..dee550370 --- /dev/null +++ b/Dalamud/Game/Gui/ContextMenu/MenuTargetInventory.cs @@ -0,0 +1,36 @@ +using Dalamud.Game.Inventory; + +using FFXIVClientStructs.FFXIV.Client.UI.Agent; + +namespace Dalamud.Game.Gui.ContextMenu; + +/// +/// Target information on an inventory context menu. +/// +public sealed unsafe class MenuTargetInventory : MenuTarget +{ + /// + /// Initializes a new instance of the class. + /// + /// The agent associated with the context menu. + internal MenuTargetInventory(AgentInventoryContext* context) + { + this.Context = context; + } + + /// + /// Gets the target item. + /// + public GameInventoryItem? TargetItem + { + get + { + var target = this.Context->TargetInventorySlot; + if (target != null) + return new(*target); + return null; + } + } + + private AgentInventoryContext* Context { get; } +} diff --git a/Dalamud/Game/Inventory/GameInventoryItem.cs b/Dalamud/Game/Inventory/GameInventoryItem.cs index 912b91f53..d37e1081f 100644 --- a/Dalamud/Game/Inventory/GameInventoryItem.cs +++ b/Dalamud/Game/Inventory/GameInventoryItem.cs @@ -1,7 +1,10 @@ -using System.Diagnostics; +using System.Diagnostics; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.Game; namespace Dalamud.Game.Inventory; @@ -103,8 +106,10 @@ public unsafe struct GameInventoryItem : IEquatable /// /// Gets the array of materia grades. /// + // TODO: Replace with MateriaGradeBytes + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] public ReadOnlySpan MateriaGrade => - new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5); + this.MateriaGradeBytes.ToArray().Select(g => (ushort)g).ToArray().AsSpan(); /// /// Gets the address of native inventory item in the game.
    @@ -146,6 +151,9 @@ public unsafe struct GameInventoryItem : IEquatable ///
    internal ulong CrafterContentId => this.InternalItem.CrafterContentID; + private ReadOnlySpan MateriaGradeBytes => + new(Unsafe.AsPointer(ref Unsafe.AsRef(in this.InternalItem.MateriaGrade[0])), 5); + public static bool operator ==(in GameInventoryItem l, in GameInventoryItem r) => l.Equals(r); public static bool operator !=(in GameInventoryItem l, in GameInventoryItem r) => !l.Equals(r); diff --git a/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs b/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs new file mode 100644 index 000000000..0ca35d672 --- /dev/null +++ b/Dalamud/Game/Network/Structures/InfoProxy/CharacterData.cs @@ -0,0 +1,197 @@ +using System.Collections.Generic; + +using Dalamud.Game.ClientState.Resolvers; +using Dalamud.Memory; + +using FFXIVClientStructs.FFXIV.Client.UI.Info; + +using Lumina.Excel.GeneratedSheets; + +namespace Dalamud.Game.Network.Structures.InfoProxy; + +/// +/// Dalamud wrapper around a client structs . +/// +public unsafe class CharacterData +{ + /// + /// Initializes a new instance of the class. + /// + /// Character data to wrap. + internal CharacterData(InfoProxyCommonList.CharacterData* data) + { + this.Address = (nint)data; + } + + /// + /// Gets the address of the in memory. + /// + public nint Address { get; } + + /// + /// Gets the content id of the character. + /// + public ulong ContentId => this.Struct->ContentId; + + /// + /// Gets the status mask of the character. + /// + public ulong StatusMask => (ulong)this.Struct->State; + + /// + /// Gets the applicable statues of the character. + /// + public IReadOnlyList> Statuses + { + get + { + var statuses = new List>(); + for (var i = 0; i < 64; i++) + { + if ((this.StatusMask & (1UL << i)) != 0) + statuses.Add(new((uint)i)); + } + + return statuses; + } + } + + /// + /// Gets the display group of the character. + /// + public DisplayGroup DisplayGroup => (DisplayGroup)this.Struct->Group; + + /// + /// Gets a value indicating whether the character's home world is different from the current world. + /// + public bool IsFromOtherServer => this.Struct->IsOtherServer; + + /// + /// Gets the sort order of the character. + /// + public byte Sort => this.Struct->Sort; + + /// + /// Gets the current world of the character. + /// + public ExcelResolver CurrentWorld => new(this.Struct->CurrentWorld); + + /// + /// Gets the home world of the character. + /// + public ExcelResolver HomeWorld => new(this.Struct->HomeWorld); + + /// + /// Gets the location of the character. + /// + public ExcelResolver Location => new(this.Struct->Location); + + /// + /// Gets the grand company of the character. + /// + public ExcelResolver GrandCompany => new((uint)this.Struct->GrandCompany); + + /// + /// Gets the primary client language of the character. + /// + public ClientLanguage ClientLanguage => (ClientLanguage)this.Struct->ClientLanguage; + + /// + /// Gets the supported language mask of the character. + /// + public byte LanguageMask => (byte)this.Struct->Languages; + + /// + /// Gets the supported languages the character supports. + /// + public IReadOnlyList Languages + { + get + { + var languages = new List(); + for (var i = 0; i < 4; i++) + { + if ((this.LanguageMask & (1 << i)) != 0) + languages.Add((ClientLanguage)i); + } + + return languages; + } + } + + /// + /// Gets the gender of the character. + /// + public byte Gender => this.Struct->Sex; + + /// + /// Gets the job of the character. + /// + public ExcelResolver ClassJob => new(this.Struct->Job); + + /// + /// Gets the name of the character. + /// + public string Name => MemoryHelper.ReadString((nint)this.Struct->Name, 32); + + /// + /// Gets the free company tag of the character. + /// + public string FCTag => MemoryHelper.ReadString((nint)this.Struct->Name, 6); + + /// + /// Gets the underlying struct. + /// + internal InfoProxyCommonList.CharacterData* Struct => (InfoProxyCommonList.CharacterData*)this.Address; +} + +/// +/// Display group of a character. Used for friends. +/// +public enum DisplayGroup : sbyte +{ + /// + /// All display groups. + /// + All = -1, + + /// + /// No display group. + /// + None, + + /// + /// Star display group. + /// + Star, + + /// + /// Circle display group. + /// + Circle, + + /// + /// Triangle display group. + /// + Triangle, + + /// + /// Diamond display group. + /// + Diamond, + + /// + /// Heart display group. + /// + Heart, + + /// + /// Spade display group. + /// + Spade, + + /// + /// Club display group. + /// + Club, +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs index 570e362ef..579f8357b 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/ContextMenuAgingStep.cs @@ -1,10 +1,17 @@ -/*using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; using Dalamud.Data; +using Dalamud.Game.ClientState.Objects.SubKinds; +using Dalamud.Game.Gui.ContextMenu; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Utility; using ImGuiNET; +using Lumina.Excel; using Lumina.Excel.GeneratedSheets; -using Serilog;*/ +using Serilog; namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; @@ -13,31 +20,22 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; ///
    internal class ContextMenuAgingStep : IAgingStep { - /* private SubStep currentSubStep; - private uint clickedItemId; - private bool clickedItemHq; - private uint clickedItemCount; + private bool? targetInventorySubmenuOpened; + private PlayerCharacter? targetCharacter; - private string? clickedPlayerName; - private ushort? clickedPlayerWorld; - private ulong? clickedPlayerCid; - private uint? clickedPlayerId; - - private bool multipleTriggerOne; - private bool multipleTriggerTwo; + private ExcelSheet itemSheet; + private ExcelSheet materiaSheet; + private ExcelSheet stainSheet; private enum SubStep { Start, - TestItem, - TestGameObject, - TestSubMenu, - TestMultiple, + TestInventoryAndSubmenu, + TestDefault, Finish, } - */ /// public string Name => "Test Context Menu"; @@ -45,23 +43,24 @@ internal class ContextMenuAgingStep : IAgingStep /// public SelfTestStepResult RunStep() { - /* var contextMenu = Service.Get(); var dataMgr = Service.Get(); + this.itemSheet = dataMgr.GetExcelSheet()!; + this.materiaSheet = dataMgr.GetExcelSheet()!; + this.stainSheet = dataMgr.GetExcelSheet()!; ImGui.Text(this.currentSubStep.ToString()); switch (this.currentSubStep) { case SubStep.Start: - contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened; + contextMenu.OnMenuOpened += this.OnMenuOpened; this.currentSubStep++; break; - case SubStep.TestItem: - if (this.clickedItemId != 0) + case SubStep.TestInventoryAndSubmenu: + if (this.targetInventorySubmenuOpened == true) { - var item = dataMgr.GetExcelSheet()!.GetRow(this.clickedItemId); - ImGui.Text($"Did you click \"{item!.Name.RawString}\", hq:{this.clickedItemHq}, count:{this.clickedItemCount}?"); + ImGui.Text($"Is the data in the submenu correct?"); if (ImGui.Button("Yes")) this.currentSubStep++; @@ -73,7 +72,7 @@ internal class ContextMenuAgingStep : IAgingStep } else { - ImGui.Text("Right-click an item."); + ImGui.Text("Right-click an item and select \"Self Test\"."); if (ImGui.Button("Skip")) this.currentSubStep++; @@ -81,10 +80,10 @@ internal class ContextMenuAgingStep : IAgingStep break; - case SubStep.TestGameObject: - if (!this.clickedPlayerName.IsNullOrEmpty()) + case SubStep.TestDefault: + if (this.targetCharacter is { } character) { - ImGui.Text($"Did you click \"{this.clickedPlayerName}\", world:{this.clickedPlayerWorld}, cid:{this.clickedPlayerCid}, id:{this.clickedPlayerId}?"); + ImGui.Text($"Did you click \"{character.Name}\" ({character.ClassJob.GameData!.Abbreviation.ToDalamudString()})?"); if (ImGui.Button("Yes")) this.currentSubStep++; @@ -103,149 +102,195 @@ internal class ContextMenuAgingStep : IAgingStep } break; - case SubStep.TestSubMenu: - if (this.multipleTriggerOne && this.multipleTriggerTwo) - { - this.currentSubStep++; - this.multipleTriggerOne = this.multipleTriggerTwo = false; - } - else - { - ImGui.Text("Right-click a character and select both options in the submenu."); + case SubStep.Finish: + return SelfTestStepResult.Pass; - if (ImGui.Button("Skip")) - this.currentSubStep++; - } - - break; - - case SubStep.TestMultiple: - if (this.multipleTriggerOne && this.multipleTriggerTwo) - { - this.currentSubStep = SubStep.Finish; - return SelfTestStepResult.Pass; - } - - ImGui.Text("Select both options on any context menu."); - if (ImGui.Button("Skip")) - this.currentSubStep++; - break; default: throw new ArgumentOutOfRangeException(); } return SelfTestStepResult.Waiting; - */ - - return SelfTestStepResult.Pass; } - + /// public void CleanUp() { - /* var contextMenu = Service.Get(); - contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; + contextMenu.OnMenuOpened -= this.OnMenuOpened; this.currentSubStep = SubStep.Start; - this.clickedItemId = 0; - this.clickedPlayerName = null; - this.multipleTriggerOne = this.multipleTriggerTwo = false; - */ + this.targetInventorySubmenuOpened = null; + this.targetCharacter = null; } - /* - private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args) + private void OnMenuOpened(MenuOpenedArgs args) { - Log.Information("Got context menu with parent addon: {ParentAddonName}, title:{Title}, itemcnt:{ItemCount}", args.ParentAddonName, args.Title, args.Items.Count); - if (args.GameObjectContext != null) - { - Log.Information(" => GameObject:{GameObjectName} world:{World} cid:{Cid} id:{Id}", args.GameObjectContext.Name, args.GameObjectContext.WorldId, args.GameObjectContext.ContentId, args.GameObjectContext.Id); - } - - if (args.InventoryItemContext != null) - { - Log.Information(" => Inventory:{ItemId} hq:{Hq} count:{Count}", args.InventoryItemContext.Id, args.InventoryItemContext.IsHighQuality, args.InventoryItemContext.Count); - } + LogMenuOpened(args); switch (this.currentSubStep) { - case SubStep.TestSubMenu: - args.AddCustomSubMenu("Aging Submenu", openedArgs => + case SubStep.TestInventoryAndSubmenu: + if (args.MenuType == ContextMenuType.Inventory) { - openedArgs.AddCustomItem("Submenu Item 1", _ => + args.AddMenuItem(new() { - this.multipleTriggerOne = true; - }); - - openedArgs.AddCustomItem("Submenu Item 2", _ => - { - this.multipleTriggerTwo = true; - }); - }); - - return; - case SubStep.TestMultiple: - args.AddCustomItem("Aging Item 1", _ => - { - this.multipleTriggerOne = true; - }); - - args.AddCustomItem("Aging Item 2", _ => - { - this.multipleTriggerTwo = true; - }); - - return; - case SubStep.Finish: - return; - - default: - switch (args.ParentAddonName) - { - case "Inventory": - if (this.currentSubStep != SubStep.TestItem) - return; - - args.AddCustomItem("Aging Item", _ => + Name = "Self Test", + Prefix = SeIconChar.Hyadelyn, + PrefixColor = 56, + Priority = -1, + IsSubmenu = true, + OnClicked = (MenuItemClickedArgs a) => { - this.clickedItemId = args.InventoryItemContext!.Id; - this.clickedItemHq = args.InventoryItemContext!.IsHighQuality; - this.clickedItemCount = args.InventoryItemContext!.Count; - Log.Warning("Clicked item: {Id} hq:{Hq} count:{Count}", this.clickedItemId, this.clickedItemHq, this.clickedItemCount); - }); - break; + SeString name; + uint count; + var targetItem = (a.Target as MenuTargetInventory).TargetItem; + if (targetItem is { } item) + { + name = (this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty); + count = item.Quantity; + } + else + { + name = "None"; + count = 0; + } - case null: - case "_PartyList": - case "ChatLog": - case "ContactList": - case "ContentMemberList": - case "CrossWorldLinkshell": - case "FreeCompany": - case "FriendList": - case "LookingForGroup": - case "LinkShell": - case "PartyMemberList": - case "SocialList": - if (this.currentSubStep != SubStep.TestGameObject || args.GameObjectContext == null || args.GameObjectContext.Name.IsNullOrEmpty()) - return; + a.OpenSubmenu(new MenuItem[] + { + new() + { + Name = "Name: " + name, + IsEnabled = false, + }, + new() + { + Name = $"Count: {count}", + IsEnabled = false, + }, + }); - args.AddCustomItem("Aging Character", _ => - { - this.clickedPlayerName = args.GameObjectContext.Name!; - this.clickedPlayerWorld = args.GameObjectContext.WorldId; - this.clickedPlayerCid = args.GameObjectContext.ContentId; - this.clickedPlayerId = args.GameObjectContext.Id; - - Log.Warning("Clicked player: {Name} world:{World} cid:{Cid} id:{Id}", this.clickedPlayerName, this.clickedPlayerWorld, this.clickedPlayerCid, this.clickedPlayerId); - }); - - break; + this.targetInventorySubmenuOpened = true; + }, + }); } break; + + case SubStep.TestDefault: + if (args.Target is MenuTargetDefault { TargetObject: PlayerCharacter { } character }) + this.targetCharacter = character; + break; + + case SubStep.Finish: + return; + } + } + + private void LogMenuOpened(MenuOpenedArgs args) + { + Log.Verbose($"Got {args.MenuType} context menu with addon 0x{args.AddonPtr:X8} ({args.AddonName}) and agent 0x{args.AgentPtr:X8}"); + if (args.Target is MenuTargetDefault targetDefault) + { + { + var b = new StringBuilder(); + b.AppendLine($"Target: {targetDefault.TargetName}"); + b.AppendLine($"Home World: {targetDefault.TargetHomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({targetDefault.TargetHomeWorld.Id})"); + b.AppendLine($"Content Id: 0x{targetDefault.TargetContentId:X8}"); + b.AppendLine($"Object Id: 0x{targetDefault.TargetObjectId:X8}"); + Log.Verbose(b.ToString()); + } + + if (targetDefault.TargetCharacter is { } character) + { + var b = new StringBuilder(); + b.AppendLine($"Character: {character.Name}"); + + b.AppendLine($"Name: {character.Name}"); + b.AppendLine($"Content Id: 0x{character.ContentId:X8}"); + b.AppendLine($"FC Tag: {character.FCTag}"); + + b.AppendLine($"Job: {character.ClassJob.GameData?.Abbreviation.ToDalamudString() ?? "Unknown"} ({character.ClassJob.Id})"); + b.AppendLine($"Statuses: {string.Join(", ", character.Statuses.Select(s => s.GameData?.Name.ToDalamudString() ?? s.Id.ToString()))}"); + b.AppendLine($"Home World: {character.HomeWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.HomeWorld.Id})"); + b.AppendLine($"Current World: {character.CurrentWorld.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.CurrentWorld.Id})"); + b.AppendLine($"Is From Other Server: {character.IsFromOtherServer}"); + + b.Append("Location: "); + if (character.Location.GameData is { } location) + b.Append($"{location.PlaceNameRegion.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceNameZone.Value?.Name.ToDalamudString() ?? "Unknown"}/{location.PlaceName.Value?.Name.ToDalamudString() ?? "Unknown"}"); + else + b.Append("Unknown"); + b.AppendLine($" ({character.Location.Id})"); + + b.AppendLine($"Grand Company: {character.GrandCompany.GameData?.Name.ToDalamudString() ?? "Unknown"} ({character.GrandCompany.Id})"); + b.AppendLine($"Client Language: {character.ClientLanguage}"); + b.AppendLine($"Languages: {string.Join(", ", character.Languages)}"); + b.AppendLine($"Gender: {character.Gender}"); + b.AppendLine($"Display Group: {character.DisplayGroup}"); + b.AppendLine($"Sort: {character.Sort}"); + + Log.Verbose(b.ToString()); + } + else + { + Log.Verbose($"Character: null"); + } + } + else if (args.Target is MenuTargetInventory targetInventory) + { + if (targetInventory.TargetItem is { } item) + { + var b = new StringBuilder(); + b.AppendLine($"Item: {(item.IsEmpty ? "None" : this.itemSheet.GetRow(item.ItemId)?.Name.ToDalamudString())} ({item.ItemId})"); + b.AppendLine($"Container: {item.ContainerType}"); + b.AppendLine($"Slot: {item.InventorySlot}"); + b.AppendLine($"Quantity: {item.Quantity}"); + b.AppendLine($"{(item.IsCollectable ? "Collectability" : "Spiritbond")}: {item.Spiritbond}"); + b.AppendLine($"Condition: {item.Condition / 300f:0.00}% ({item.Condition})"); + b.AppendLine($"Is HQ: {item.IsHq}"); + b.AppendLine($"Is Company Crest Applied: {item.IsCompanyCrestApplied}"); + b.AppendLine($"Is Relic: {item.IsRelic}"); + b.AppendLine($"Is Collectable: {item.IsCollectable}"); + + b.Append("Materia: "); + var materias = new List(); + foreach (var (materiaId, materiaGrade) in item.Materia.ToArray().Zip(item.MateriaGrade.ToArray()).Where(m => m.First != 0)) + { + Log.Verbose($"{materiaId} {materiaGrade}"); + if (this.materiaSheet.GetRow(materiaId) is { } materia && + materia.Item[materiaGrade].Value is { } materiaItem) + materias.Add($"{materiaItem.Name.ToDalamudString()}"); + else + materias.Add($"Unknown (Id: {materiaId}, Grade: {materiaGrade})"); + } + + if (materias.Count == 0) + b.AppendLine("None"); + else + b.AppendLine(string.Join(", ", materias)); + + b.Append($"Dye/Stain: "); + if (item.Stain != 0) + b.AppendLine($"{this.stainSheet.GetRow(item.Stain)?.Name.ToDalamudString() ?? "Unknown"} ({item.Stain})"); + else + b.AppendLine("None"); + + b.Append("Glamoured Item: "); + if (item.GlamourId != 0) + b.AppendLine($"{this.itemSheet.GetRow(item.GlamourId)?.Name.ToDalamudString() ?? "Unknown"} ({item.GlamourId})"); + else + b.AppendLine("None"); + + Log.Verbose(b.ToString()); + } + else + { + Log.Verbose("Item: null"); + } + } + else + { + Log.Verbose($"Target: Unknown ({args.Target?.GetType().Name ?? "null"})"); } } - */ } diff --git a/Dalamud/Plugin/Services/IContextMenu.cs b/Dalamud/Plugin/Services/IContextMenu.cs new file mode 100644 index 000000000..4d792116d --- /dev/null +++ b/Dalamud/Plugin/Services/IContextMenu.cs @@ -0,0 +1,37 @@ +using Dalamud.Game.Gui.ContextMenu; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; + +namespace Dalamud.Plugin.Services; + +/// +/// This class provides methods for interacting with the game's context menu. +/// +public interface IContextMenu +{ + /// + /// A delegate type used for the event. + /// + /// Information about the currently opening menu. + public delegate void OnMenuOpenedDelegate(MenuOpenedArgs args); + + /// + /// Event that gets fired every time the game framework updates. + /// + event OnMenuOpenedDelegate OnMenuOpened; + + /// + /// Adds a menu item to a context menu. + /// + /// The type of context menu to add the item to. + /// The item to add. + void AddMenuItem(ContextMenuType menuType, MenuItem item); + + /// + /// Removes a menu item from a context menu. + /// + /// The type of context menu to remove the item from. + /// The item to add. + /// if the item was removed, if it was not found. + bool RemoveMenuItem(ContextMenuType menuType, MenuItem item); +} diff --git a/Dalamud/Utility/EventHandlerExtensions.cs b/Dalamud/Utility/EventHandlerExtensions.cs index d05ad6ea5..9bb35a8f1 100644 --- a/Dalamud/Utility/EventHandlerExtensions.cs +++ b/Dalamud/Utility/EventHandlerExtensions.cs @@ -1,6 +1,7 @@ using System.Linq; using Dalamud.Game; +using Dalamud.Game.Gui.ContextMenu; using Dalamud.Plugin.Services; using Serilog; @@ -99,6 +100,23 @@ internal static class EventHandlerExtensions } } + /// + /// Replacement for Invoke() on OnMenuOpenedDelegate to catch exceptions that stop event propagation in case + /// of a thrown Exception inside of an invocation. + /// + /// The OnMenuOpenedDelegate in question. + /// Templated argument for Action. + public static void InvokeSafely(this IContextMenu.OnMenuOpenedDelegate? openedDelegate, MenuOpenedArgs argument) + { + if (openedDelegate == null) + return; + + foreach (var action in openedDelegate.GetInvocationList().Cast()) + { + HandleInvoke(() => action(argument)); + } + } + private static void HandleInvoke(Action act) { try From 8a21fc721f58c1955e2aff81a25d2d77677ec686 Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Mon, 4 Mar 2024 18:51:25 +0100 Subject: [PATCH 563/585] feat: add AdjustedTotalCastTime to BattleChara (#1694) * feat: add AdjustedTotalCastTime to BattleChara * Update Dalamud/Game/ClientState/Objects/Types/BattleChara.cs Co-authored-by: KazWolfe --------- Co-authored-by: KazWolfe --- .../Game/ClientState/Objects/Types/BattleChara.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs index 63a5b828a..0c5d16675 100644 --- a/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs +++ b/Dalamud/Game/ClientState/Objects/Types/BattleChara.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Game.ClientState.Statuses; +using Dalamud.Utility; namespace Dalamud.Game.ClientState.Objects.Types; @@ -57,8 +58,22 @@ public unsafe class BattleChara : Character /// /// Gets the total casting time of the spell being cast by the chara. /// + /// + /// This can only be a portion of the total cast for some actions. + /// Use AdjustedTotalCastTime if you always need the total cast time. + /// + [Api10ToDo("Rename so it is not confused with AdjustedTotalCastTime")] public float TotalCastTime => this.Struct->GetCastInfo->TotalCastTime; + /// + /// Gets the plus any adjustments from the game, such as Action offset 2B. Used for display purposes. + /// + /// + /// This is the actual total cast time for all actions. + /// + [Api10ToDo("Rename so it is not confused with TotalCastTime")] + public float AdjustedTotalCastTime => this.Struct->GetCastInfo->AdjustedTotalCastTime; + /// /// Gets the underlying structure. /// From 2cdc1f017177ad4ab0ffa2283c51bcd9051e278f Mon Sep 17 00:00:00 2001 From: Asriel Camora Date: Tue, 5 Mar 2024 09:13:43 -0800 Subject: [PATCH 564/585] Fix duty pop chat message italics (#1697) --- Dalamud/Game/Network/Internal/NetworkHandlers.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 76d3b5659..8d5ec1344 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -12,6 +12,7 @@ using Dalamud.Game.Gui; using Dalamud.Game.Network.Internal.MarketBoardUploaders; using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis; using Dalamud.Game.Network.Structures; +using Dalamud.Game.Text.SeStringHandling; using Dalamud.Hooking; using Dalamud.Networking.Http; using Dalamud.Utility; @@ -268,8 +269,8 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType return result; } - var cfcName = cfCondition.Name.ToString(); - if (cfcName.IsNullOrEmpty()) + var cfcName = cfCondition.Name.ToDalamudString(); + if (cfcName.Payloads.Count == 0) { cfcName = "Duty Roulette"; cfCondition.Image = 112324; @@ -279,7 +280,10 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType { if (this.configuration.DutyFinderChatMessage) { - Service.GetNullable()?.Print($"Duty pop: {cfcName}"); + var b = new SeStringBuilder(); + b.Append("Duty pop: "); + b.Append(cfcName); + Service.GetNullable()?.Print(b.Build()); } this.CfPop.InvokeSafely(cfCondition); From c326537f9f21574af97323bc8a0503f37c0ef399 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 7 Mar 2024 00:37:46 +0900 Subject: [PATCH 565/585] test --- Dalamud/Interface/Internal/DalamudCommands.cs | 11 - Dalamud/Interface/Internal/DalamudIme.cs | 512 +++++++++++++----- .../Interface/Internal/DalamudInterface.cs | 18 - .../Interface/Internal/InterfaceManager.cs | 5 - .../Internal/Windows/DalamudImeWindow.cs | 266 --------- Dalamud/ServiceManager.cs | 15 +- 6 files changed, 383 insertions(+), 444 deletions(-) delete mode 100644 Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index ace8887f1..b64df8f19 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -96,12 +96,6 @@ internal class DalamudCommands : IServiceType ShowInHelp = false, }); - commandManager.AddHandler("/xlime", new CommandInfo(this.OnDebugDrawIMEPanel) - { - HelpMessage = Loc.Localize("DalamudIMEPanelHelp", "Draw IME panel"), - ShowInHelp = false, - }); - commandManager.AddHandler("/xllog", new CommandInfo(this.OnOpenLog) { HelpMessage = Loc.Localize("DalamudDevLogHelp", "Open dev log DEBUG"), @@ -308,11 +302,6 @@ internal class DalamudCommands : IServiceType dalamudInterface.ToggleDataWindow(arguments); } - private void OnDebugDrawIMEPanel(string command, string arguments) - { - Service.Get().OpenImeWindow(); - } - private void OnOpenLog(string command, string arguments) { Service.Get().ToggleLogWindow(); diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 1ee248b17..6c01b74d7 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -17,6 +18,8 @@ using Dalamud.Interface.Utility; using ImGuiNET; +using Serilog; + using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; @@ -26,12 +29,21 @@ namespace Dalamud.Interface.Internal; /// /// This class handles CJK IME. /// -[ServiceManager.BlockingEarlyLoadedService] +[ServiceManager.EarlyLoadedService] internal sealed unsafe class DalamudIme : IDisposable, IServiceType { private const int CImGuiStbTextCreateUndoOffset = 0xB57A0; private const int CImGuiStbTextUndoOffset = 0xB59C0; + private const int ImePageSize = 9; + + private static readonly Dictionary WmNames = + typeof(WM).GetFields(BindingFlags.Public | BindingFlags.Static) + .Where(x => x.IsLiteral && !x.IsInitOnly && x.FieldType == typeof(int)) + .Select(x => ((int)x.GetRawConstantValue()!, x.Name)) + .DistinctBy(x => x.Item1) + .ToDictionary(x => x.Item1, x => x.Name); + private static readonly UnicodeRange[] HanRange = { UnicodeRanges.CjkRadicalsSupplement, @@ -57,8 +69,41 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private static readonly delegate* unmanaged StbTextUndo; + [ServiceManager.ServiceDependency] + private readonly WndProcHookManager wndProcHookManager = Service.Get(); + + private readonly InterfaceManager interfaceManager; + private readonly ImGuiSetPlatformImeDataDelegate setPlatformImeDataDelegate; + /// The candidates. + private readonly List candidateStrings = new(); + + /// The selected imm component. + private string compositionString = string.Empty; + + /// The cursor position in screen coordinates. + private Vector2 cursorScreenPos; + + /// The associated viewport. + private ImGuiViewportPtr associatedViewport; + + /// The index of the first imm candidate in relation to the full list. + private CANDIDATELIST immCandNative; + + /// The partial conversion from-range. + private int partialConversionFrom; + + /// The partial conversion to-range. + private int partialConversionTo; + + /// The cursor offset in the composition string. + private int compositionCursorOffset; + + /// The input mode icon from . + private char inputModeIcon; + + /// Undo range for modifying the buffer while composition is in progress. private (int Start, int End, int Cursor)? temporaryUndoSelection; [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1003:Symbols should be spaced correctly", Justification = ".")] @@ -87,7 +132,17 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } [ServiceManager.ServiceConstructor] - private DalamudIme() => this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData; + private DalamudIme(InterfaceManager.InterfaceManagerWithScene imws) + { + Debug.Assert(ImGuiHelpers.IsImGuiInitialized, "IMWS initialized but IsImGuiInitialized is false?"); + + this.interfaceManager = imws.Manager; + this.setPlatformImeDataDelegate = this.ImGuiSetPlatformImeData; + + ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + this.interfaceManager.Draw += this.Draw; + this.wndProcHookManager.PreWndProc += this.WndProcHookManagerOnPreWndProc; + } /// /// Finalizes an instance of the class. @@ -109,7 +164,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType /// /// Gets a value indicating whether to display the cursor in input text. This also deals with blinking. /// - internal static bool ShowCursorInInputText + private static bool ShowCursorInInputText { get { @@ -126,63 +181,21 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } } - /// - /// Gets the cursor position, in screen coordinates. - /// - internal Vector2 CursorPos { get; private set; } - - /// - /// Gets the associated viewport. - /// - internal ImGuiViewportPtr AssociatedViewport { get; private set; } - - /// - /// Gets the index of the first imm candidate in relation to the full list. - /// - internal CANDIDATELIST ImmCandNative { get; private set; } - - /// - /// Gets the imm candidates. - /// - internal List ImmCand { get; private set; } = new(); - - /// - /// Gets the selected imm component. - /// - internal string ImmComp { get; private set; } = string.Empty; - - /// - /// Gets the partial conversion from-range. - /// - internal int PartialConversionFrom { get; private set; } - - /// - /// Gets the partial conversion to-range. - /// - internal int PartialConversionTo { get; private set; } - - /// - /// Gets the cursor offset in the composition string. - /// - internal int CompositionCursorOffset { get; private set; } - - /// - /// Gets a value indicating whether to display partial conversion status. - /// - internal bool ShowPartialConversion => this.PartialConversionFrom != 0 || - this.PartialConversionTo != this.ImmComp.Length; - - /// - /// Gets the input mode icon from . - /// - internal char InputModeIcon { get; private set; } - private static ImGuiInputTextState* TextState => (ImGuiInputTextState*)(ImGui.GetCurrentContext() + ImGuiContextOffsets.TextStateOffset); + /// Gets a value indicating whether to display partial conversion status. + private bool ShowPartialConversion => this.partialConversionFrom != 0 || + this.partialConversionTo != this.compositionString.Length; + + /// Gets a value indicating whether to draw. + private bool ShouldDraw => + this.candidateStrings.Count != 0 || this.ShowPartialConversion || this.inputModeIcon != default; + /// public void Dispose() { + this.interfaceManager.Draw -= this.Draw; this.ReleaseUnmanagedResources(); GC.SuppressFinalize(this); } @@ -195,13 +208,13 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { foreach (var chr in str) { - if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + if (!this.EncounteredHan) { - if (Service.Get() - ?.GetFdtReader(GameFontFamilyAndSize.Axis12) - .FindGlyph(chr) is null) + if (HanRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { - if (!this.EncounteredHan) + if (Service.Get() + ?.GetFdtReader(GameFontFamilyAndSize.Axis12) + .FindGlyph(chr) is null) { this.EncounteredHan = true; Service.Get().RebuildFonts(); @@ -209,9 +222,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } } - if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) + if (!this.EncounteredHangul) { - if (!this.EncounteredHangul) + if (HangulRange.Any(x => x.FirstCodePoint <= chr && chr < x.FirstCodePoint + x.Length)) { this.EncounteredHangul = true; Service.Get().RebuildFonts(); @@ -220,11 +233,24 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } } - /// - /// Processes window messages. - /// - /// The arguments. - public void ProcessImeMessage(WndProcEventArgs args) + private static string ImmGetCompositionString(HIMC hImc, uint comp) + { + var numBytes = ImmGetCompositionStringW(hImc, comp, null, 0); + if (numBytes == 0) + return string.Empty; + + var data = stackalloc char[numBytes / 2]; + _ = ImmGetCompositionStringW(hImc, comp, data, (uint)numBytes); + return new(data, 0, numBytes / 2); + } + + private void ReleaseUnmanagedResources() + { + if (ImGuiHelpers.IsImGuiInitialized) + ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; + } + + private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) { if (!ImGuiHelpers.IsImGuiInitialized) return; @@ -246,7 +272,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType case WM.WM_IME_NOTIFY when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: - this.UpdateImeWindowStatus(hImc); + this.UpdateCandidates(hImc); args.SuppressWithValue(0); break; @@ -260,22 +286,22 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType else this.ReplaceCompositionString(hImc, (uint)args.LParam); - // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.ImmComp}"); + // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithValue(0); break; case WM.WM_IME_ENDCOMPOSITION: - // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithValue(0); break; case WM.WM_IME_CONTROL: - // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithValue(0); break; case WM.WM_IME_REQUEST: - // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithValue(0); break; @@ -283,12 +309,12 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // Hide candidate and composition windows. args.LParam = (LPARAM)((nint)args.LParam & ~(ISC_SHOWUICOMPOSITIONWINDOW | 0xF)); - // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.ImmComp}"); + // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithDefault(); break; case WM.WM_IME_NOTIFY: - // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.ImmComp}"); + // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.compositionString}"); break; case WM.WM_KEYDOWN when (int)args.WParam is @@ -302,12 +328,14 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType or VK.VK_RIGHT or VK.VK_DOWN or VK.VK_RETURN: - if (this.ImmCand.Count != 0) + if (this.candidateStrings.Count != 0) { this.ClearState(hImc); args.WParam = VK.VK_PROCESSKEY; } + this.UpdateCandidates(hImc); + break; case WM.WM_LBUTTONDOWN: @@ -316,9 +344,15 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType case WM.WM_XBUTTONDOWN: ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); break; + + // default: + // Log.Verbose($"{(WmNames.TryGetValue((int)args.Message, out var v) ? v : args.Message.ToString())}({(nint)args.WParam:X}, {(nint)args.LParam:X})"); + // break; } this.UpdateInputLanguage(hImc); + if (this.inputModeIcon == (char)SeIconChar.ImeKoreanHangul) + this.UpdateCandidates(hImc); } finally { @@ -326,23 +360,6 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } } - private static string ImmGetCompositionString(HIMC hImc, uint comp) - { - var numBytes = ImmGetCompositionStringW(hImc, comp, null, 0); - if (numBytes == 0) - return string.Empty; - - var data = stackalloc char[numBytes / 2]; - _ = ImmGetCompositionStringW(hImc, comp, data, (uint)numBytes); - return new(data, 0, numBytes / 2); - } - - private void ReleaseUnmanagedResources() - { - if (ImGuiHelpers.IsImGuiInitialized) - ImGui.GetIO().SetPlatformImeDataFn = nint.Zero; - } - private void UpdateInputLanguage(HIMC hImc) { uint conv, sent; @@ -359,41 +376,39 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { case LANG.LANG_KOREAN: if (native) - this.InputModeIcon = (char)SeIconChar.ImeKoreanHangul; + this.inputModeIcon = (char)SeIconChar.ImeKoreanHangul; else if (fullwidth) - this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; + this.inputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; + this.inputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_JAPANESE: // wtf // see the function called from: 48 8b 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 8b d8 e9 ?? 00 00 0 if (open && native && katakana && fullwidth) - this.InputModeIcon = (char)SeIconChar.ImeKatakana; + this.inputModeIcon = (char)SeIconChar.ImeKatakana; else if (open && native && katakana) - this.InputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth; + this.inputModeIcon = (char)SeIconChar.ImeKatakanaHalfWidth; else if (open && native) - this.InputModeIcon = (char)SeIconChar.ImeHiragana; + this.inputModeIcon = (char)SeIconChar.ImeHiragana; else if (open && fullwidth) - this.InputModeIcon = (char)SeIconChar.ImeAlphanumeric; + this.inputModeIcon = (char)SeIconChar.ImeAlphanumeric; else - this.InputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; + this.inputModeIcon = (char)SeIconChar.ImeAlphanumericHalfWidth; break; case LANG.LANG_CHINESE: if (native) - this.InputModeIcon = (char)SeIconChar.ImeChineseHan; + this.inputModeIcon = (char)SeIconChar.ImeChineseHan; else - this.InputModeIcon = (char)SeIconChar.ImeChineseLatin; + this.inputModeIcon = (char)SeIconChar.ImeChineseLatin; break; default: - this.InputModeIcon = default; + this.inputModeIcon = default; break; } - - this.UpdateImeWindowStatus(hImc); } private void ReplaceCompositionString(HIMC hImc, uint comp) @@ -425,14 +440,14 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return; } - this.ImmComp = newString; - this.CompositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); + this.compositionString = newString; + this.compositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); if ((comp & GCS.GCS_COMPATTR) != 0) { var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); var attrPtr = stackalloc byte[attrLength]; - var attr = new Span(attrPtr, Math.Min(this.ImmComp.Length, attrLength)); + var attr = new Span(attrPtr, Math.Min(this.compositionString.Length, attrLength)); _ = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, attrPtr, (uint)attrLength); var l = 0; while (l < attr.Length && attr[l] is not ATTR_TARGET_CONVERTED and not ATTR_TARGET_NOTCONVERTED) @@ -442,37 +457,37 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType while (r < attr.Length && attr[r] is ATTR_TARGET_CONVERTED or ATTR_TARGET_NOTCONVERTED) r++; - if (r == 0 || l == this.ImmComp.Length) - (l, r) = (0, this.ImmComp.Length); + if (r == 0 || l == this.compositionString.Length) + (l, r) = (0, this.compositionString.Length); - (this.PartialConversionFrom, this.PartialConversionTo) = (l, r); + (this.partialConversionFrom, this.partialConversionTo) = (l, r); } else { - this.PartialConversionFrom = 0; - this.PartialConversionTo = this.ImmComp.Length; + this.partialConversionFrom = 0; + this.partialConversionTo = this.compositionString.Length; } - this.UpdateImeWindowStatus(hImc); + this.UpdateCandidates(hImc); } private void ClearState(HIMC hImc) { - this.ImmComp = string.Empty; - this.PartialConversionFrom = this.PartialConversionTo = 0; - this.CompositionCursorOffset = 0; + this.compositionString = string.Empty; + this.partialConversionFrom = this.partialConversionTo = 0; + this.compositionCursorOffset = 0; this.temporaryUndoSelection = null; TextState->Stb.SelectStart = TextState->Stb.Cursor = TextState->Stb.SelectEnd; ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); - this.UpdateImeWindowStatus(default); + this.UpdateCandidates(default); // Log.Information($"{nameof(this.ClearState)}"); } - private void LoadCand(HIMC hImc) + private void UpdateCandidates(HIMC hImc) { - this.ImmCand.Clear(); - this.ImmCandNative = default; + this.candidateStrings.Clear(); + this.immCandNative = default; if (hImc == default) return; @@ -486,7 +501,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return; ref var candlist = ref *(CANDIDATELIST*)pStorage; - this.ImmCandNative = candlist; + this.immCandNative = candlist; if (candlist.dwPageSize == 0 || candlist.dwCount == 0) return; @@ -495,39 +510,250 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType (int)candlist.dwPageStart, (int)Math.Min(candlist.dwCount - candlist.dwPageStart, candlist.dwPageSize))) { - this.ImmCand.Add(new((char*)(pStorage + candlist.dwOffset[i]))); - this.ReflectCharacterEncounters(this.ImmCand[^1]); + this.candidateStrings.Add(new((char*)(pStorage + candlist.dwOffset[i]))); + this.ReflectCharacterEncounters(this.candidateStrings[^1]); } } - private void UpdateImeWindowStatus(HIMC hImc) - { - if (Service.GetNullable() is not { } di) - return; - - this.LoadCand(hImc); - if (this.ImmCand.Count != 0 || this.ShowPartialConversion || this.InputModeIcon != default) - di.OpenImeWindow(); - else - di.CloseImeWindow(); - } - private void ImGuiSetPlatformImeData(ImGuiViewportPtr viewport, ImGuiPlatformImeDataPtr data) { - this.CursorPos = data.InputPos; - this.AssociatedViewport = data.WantVisible ? viewport : default; + this.cursorScreenPos = data.InputPos; + this.associatedViewport = data.WantVisible ? viewport : default; } - [ServiceManager.CallWhenServicesReady("Effectively waiting for cimgui context initialization.")] - private void ContinueConstruction(InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) + private void Draw() { - if (!ImGuiHelpers.IsImGuiInitialized) + if (!this.ShouldDraw) + return; + + if (Service.GetNullable() is not { } ime) + return; + + var viewport = ime.associatedViewport; + if (viewport.NativePtr is null) + return; + + var drawCand = ime.candidateStrings.Count != 0; + var drawConv = drawCand || ime.ShowPartialConversion; + var drawIme = ime.inputModeIcon != 0; + var imeIconFont = InterfaceManager.DefaultFont; + + var pad = ImGui.GetStyle().WindowPadding; + var candTextSize = ImGui.CalcTextSize(ime.compositionString == string.Empty ? " " : ime.compositionString); + + var native = ime.immCandNative; + var totalIndex = native.dwSelection + 1; + var totalSize = native.dwCount; + + var pageStart = native.dwPageStart; + var pageIndex = (pageStart / ImePageSize) + 1; + var pageCount = (totalSize / ImePageSize) + 1; + var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})"; + + // Calc the window size. + var maxTextWidth = 0f; + for (var i = 0; i < ime.candidateStrings.Count; i++) { - throw new InvalidOperationException( - $"Expected {nameof(InterfaceManager.InterfaceManagerWithScene)} to have initialized ImGui."); + var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.candidateStrings[i]}"); + maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X; } - ImGui.GetIO().SetPlatformImeDataFn = Marshal.GetFunctionPointerForDelegate(this.setPlatformImeDataDelegate); + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X; + maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.compositionString).X + ? maxTextWidth + : ImGui.CalcTextSize(ime.compositionString).X; + + var numEntries = (drawCand ? ime.candidateStrings.Count + 1 : 0) + 1 + (drawIme ? 1 : 0); + var spaceY = ImGui.GetStyle().ItemSpacing.Y; + var imeWindowHeight = (spaceY * (numEntries - 1)) + (candTextSize.Y * numEntries); + var windowSize = new Vector2(maxTextWidth, imeWindowHeight) + (pad * 2); + + // 1. Figure out the expanding direction. + var expandUpward = ime.cursorScreenPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y; + var windowPos = ime.cursorScreenPos - pad; + if (expandUpward) + { + windowPos.Y -= windowSize.Y - candTextSize.Y - (pad.Y * 2); + if (drawIme) + windowPos.Y += candTextSize.Y + spaceY; + } + else + { + if (drawIme) + windowPos.Y -= candTextSize.Y + spaceY; + } + + // 2. Contain within the viewport. Do not use clamp, as the target window might be too small. + if (windowPos.X < viewport.WorkPos.X) + windowPos.X = viewport.WorkPos.X; + else if (windowPos.X + windowSize.X > viewport.WorkPos.X + viewport.WorkSize.X) + windowPos.X = (viewport.WorkPos.X + viewport.WorkSize.X) - windowSize.X; + if (windowPos.Y < viewport.WorkPos.Y) + windowPos.Y = viewport.WorkPos.Y; + else if (windowPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y) + windowPos.Y = (viewport.WorkPos.Y + viewport.WorkSize.Y) - windowSize.Y; + + var cursor = windowPos + pad; + + // Draw the ime window. + var drawList = ImGui.GetForegroundDrawList(viewport); + + // Draw the background rect for candidates. + if (drawCand) + { + Vector2 candRectLt, candRectRb; + if (!expandUpward) + { + candRectLt = windowPos + candTextSize with { X = 0 } + pad with { X = 0 }; + candRectRb = windowPos + windowSize; + if (drawIme) + candRectLt.Y += spaceY + candTextSize.Y; + } + else + { + candRectLt = windowPos; + candRectRb = windowPos + (windowSize - candTextSize with { X = 0 } - pad with { X = 0 }); + if (drawIme) + candRectRb.Y -= spaceY + candTextSize.Y; + } + + drawList.AddRectFilled( + candRectLt, + candRectRb, + ImGui.GetColorU32(ImGuiCol.WindowBg), + ImGui.GetStyle().WindowRounding); + } + + if (!expandUpward && drawIme) + { + for (var dx = -2; dx <= 2; dx++) + { + for (var dy = -2; dy <= 2; dy++) + { + if (dx != 0 || dy != 0) + { + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor + new Vector2(dx, dy), + ImGui.GetColorU32(ImGuiCol.WindowBg), + ime.inputModeIcon); + } + } + } + + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor, + ImGui.GetColorU32(ImGuiCol.Text), + ime.inputModeIcon); + cursor.Y += candTextSize.Y + spaceY; + } + + if (!expandUpward && drawConv) + { + DrawTextBeingConverted(); + cursor.Y += candTextSize.Y + spaceY; + + // Add a separator. + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + } + + if (drawCand) + { + // Add the candidate words. + for (var i = 0; i < ime.candidateStrings.Count; i++) + { + var selected = i == (native.dwSelection % ImePageSize); + var color = ImGui.GetColorU32(ImGuiCol.Text); + if (selected) + color = ImGui.GetColorU32(ImGuiCol.NavHighlight); + + drawList.AddText(cursor, color, $"{i + 1}. {ime.candidateStrings[i]}"); + cursor.Y += candTextSize.Y + spaceY; + } + + // Add a separator + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + + // Add the pages infomation. + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), pageInfo); + cursor.Y += candTextSize.Y + spaceY; + } + + if (expandUpward && drawConv) + { + // Add a separator. + drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); + + DrawTextBeingConverted(); + cursor.Y += candTextSize.Y + spaceY; + } + + if (expandUpward && drawIme) + { + for (var dx = -2; dx <= 2; dx++) + { + for (var dy = -2; dy <= 2; dy++) + { + if (dx != 0 || dy != 0) + { + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor + new Vector2(dx, dy), + ImGui.GetColorU32(ImGuiCol.WindowBg), + ime.inputModeIcon); + } + } + } + + imeIconFont.RenderChar( + drawList, + imeIconFont.FontSize, + cursor, + ImGui.GetColorU32(ImGuiCol.Text), + ime.inputModeIcon); + } + + return; + + void DrawTextBeingConverted() + { + // Draw the text background. + drawList.AddRectFilled( + cursor - (pad / 2), + cursor + candTextSize + (pad / 2), + ImGui.GetColorU32(ImGuiCol.WindowBg)); + + // If only a part of the full text is marked for conversion, then draw background for the part being edited. + if (ime.partialConversionFrom != 0 || ime.partialConversionTo != ime.compositionString.Length) + { + var part1 = ime.compositionString[..ime.partialConversionFrom]; + var part2 = ime.compositionString[..ime.partialConversionTo]; + var size1 = ImGui.CalcTextSize(part1); + var size2 = ImGui.CalcTextSize(part2); + drawList.AddRectFilled( + cursor + size1 with { Y = 0 }, + cursor + size2, + ImGui.GetColorU32(ImGuiCol.TextSelectedBg)); + } + + // Add the text being converted. + drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.compositionString); + + // Draw the caret inside the composition string. + if (DalamudIme.ShowCursorInInputText) + { + var partBeforeCaret = ime.compositionString[..ime.compositionCursorOffset]; + var sizeBeforeCaret = ImGui.CalcTextSize(partBeforeCaret); + drawList.AddLine( + cursor + sizeBeforeCaret with { Y = 0 }, + cursor + sizeBeforeCaret, + ImGui.GetColorU32(ImGuiCol.Text)); + } + } } /// diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 00bef19af..1a07cd6ae 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -61,7 +61,6 @@ internal class DalamudInterface : IDisposable, IServiceType private readonly ComponentDemoWindow componentDemoWindow; private readonly DataWindow dataWindow; private readonly GamepadModeNotifierWindow gamepadModeNotifierWindow; - private readonly DalamudImeWindow imeWindow; private readonly ConsoleWindow consoleWindow; private readonly PluginStatWindow pluginStatWindow; private readonly PluginInstallerWindow pluginWindow; @@ -114,7 +113,6 @@ internal class DalamudInterface : IDisposable, IServiceType this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; this.dataWindow = new DataWindow() { IsOpen = false }; this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow() { IsOpen = false }; - this.imeWindow = new DalamudImeWindow() { IsOpen = false }; this.consoleWindow = new ConsoleWindow(configuration) { IsOpen = configuration.LogOpenAtStartup }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; this.pluginWindow = new PluginInstallerWindow(pluginImageCache, configuration) { IsOpen = false }; @@ -142,7 +140,6 @@ internal class DalamudInterface : IDisposable, IServiceType this.WindowSystem.AddWindow(this.componentDemoWindow); this.WindowSystem.AddWindow(this.dataWindow); this.WindowSystem.AddWindow(this.gamepadModeNotifierWindow); - this.WindowSystem.AddWindow(this.imeWindow); this.WindowSystem.AddWindow(this.consoleWindow); this.WindowSystem.AddWindow(this.pluginStatWindow); this.WindowSystem.AddWindow(this.pluginWindow); @@ -265,11 +262,6 @@ internal class DalamudInterface : IDisposable, IServiceType /// public void OpenGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.IsOpen = true; - /// - /// Opens the . - /// - public void OpenImeWindow() => this.imeWindow.IsOpen = true; - /// /// Opens the . /// @@ -365,11 +357,6 @@ internal class DalamudInterface : IDisposable, IServiceType #region Close - /// - /// Closes the . - /// - public void CloseImeWindow() => this.imeWindow.IsOpen = false; - /// /// Closes the . /// @@ -417,11 +404,6 @@ internal class DalamudInterface : IDisposable, IServiceType /// public void ToggleGamepadModeNotifierWindow() => this.gamepadModeNotifierWindow.Toggle(); - /// - /// Toggles the . - /// - public void ToggleImeWindow() => this.imeWindow.Toggle(); - /// /// Toggles the . /// diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 3db799be0..126097ed3 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -67,9 +67,6 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly DalamudIme dalamudIme = Service.Get(); private readonly SwapChainVtableResolver address = new(); private readonly Hook setCursorHook; @@ -627,8 +624,6 @@ internal class InterfaceManager : IDisposable, IServiceType var r = this.scene?.ProcessWndProcW(args.Hwnd, (User32.WindowMessage)args.Message, args.WParam, args.LParam); if (r is not null) args.SuppressWithValue(r.Value); - - this.dalamudIme.ProcessImeMessage(args); } /* diff --git a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs b/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs deleted file mode 100644 index ecaa522e5..000000000 --- a/Dalamud/Interface/Internal/Windows/DalamudImeWindow.cs +++ /dev/null @@ -1,266 +0,0 @@ -using System.Numerics; - -using Dalamud.Interface.Windowing; - -using ImGuiNET; - -namespace Dalamud.Interface.Internal.Windows; - -/// -/// A window for displaying IME details. -/// -internal unsafe class DalamudImeWindow : Window -{ - private const int ImePageSize = 9; - - /// - /// Initializes a new instance of the class. - /// - public DalamudImeWindow() - : base( - "Dalamud IME", - ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoBackground) - { - this.Size = default(Vector2); - - this.RespectCloseHotkey = false; - } - - /// - public override void Draw() - { - } - - /// - public override void PostDraw() - { - if (Service.GetNullable() is not { } ime) - return; - - var viewport = ime.AssociatedViewport; - if (viewport.NativePtr is null) - return; - - var drawCand = ime.ImmCand.Count != 0; - var drawConv = drawCand || ime.ShowPartialConversion; - var drawIme = ime.InputModeIcon != 0; - var imeIconFont = InterfaceManager.DefaultFont; - - var pad = ImGui.GetStyle().WindowPadding; - var candTextSize = ImGui.CalcTextSize(ime.ImmComp == string.Empty ? " " : ime.ImmComp); - - var native = ime.ImmCandNative; - var totalIndex = native.dwSelection + 1; - var totalSize = native.dwCount; - - var pageStart = native.dwPageStart; - var pageIndex = (pageStart / ImePageSize) + 1; - var pageCount = (totalSize / ImePageSize) + 1; - var pageInfo = $"{totalIndex}/{totalSize} ({pageIndex}/{pageCount})"; - - // Calc the window size. - var maxTextWidth = 0f; - for (var i = 0; i < ime.ImmCand.Count; i++) - { - var textSize = ImGui.CalcTextSize($"{i + 1}. {ime.ImmCand[i]}"); - maxTextWidth = maxTextWidth > textSize.X ? maxTextWidth : textSize.X; - } - - maxTextWidth = maxTextWidth > ImGui.CalcTextSize(pageInfo).X ? maxTextWidth : ImGui.CalcTextSize(pageInfo).X; - maxTextWidth = maxTextWidth > ImGui.CalcTextSize(ime.ImmComp).X - ? maxTextWidth - : ImGui.CalcTextSize(ime.ImmComp).X; - - var numEntries = (drawCand ? ime.ImmCand.Count + 1 : 0) + 1 + (drawIme ? 1 : 0); - var spaceY = ImGui.GetStyle().ItemSpacing.Y; - var imeWindowHeight = (spaceY * (numEntries - 1)) + (candTextSize.Y * numEntries); - var windowSize = new Vector2(maxTextWidth, imeWindowHeight) + (pad * 2); - - // 1. Figure out the expanding direction. - var expandUpward = ime.CursorPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y; - var windowPos = ime.CursorPos - pad; - if (expandUpward) - { - windowPos.Y -= windowSize.Y - candTextSize.Y - (pad.Y * 2); - if (drawIme) - windowPos.Y += candTextSize.Y + spaceY; - } - else - { - if (drawIme) - windowPos.Y -= candTextSize.Y + spaceY; - } - - // 2. Contain within the viewport. Do not use clamp, as the target window might be too small. - if (windowPos.X < viewport.WorkPos.X) - windowPos.X = viewport.WorkPos.X; - else if (windowPos.X + windowSize.X > viewport.WorkPos.X + viewport.WorkSize.X) - windowPos.X = (viewport.WorkPos.X + viewport.WorkSize.X) - windowSize.X; - if (windowPos.Y < viewport.WorkPos.Y) - windowPos.Y = viewport.WorkPos.Y; - else if (windowPos.Y + windowSize.Y > viewport.WorkPos.Y + viewport.WorkSize.Y) - windowPos.Y = (viewport.WorkPos.Y + viewport.WorkSize.Y) - windowSize.Y; - - var cursor = windowPos + pad; - - // Draw the ime window. - var drawList = ImGui.GetForegroundDrawList(viewport); - - // Draw the background rect for candidates. - if (drawCand) - { - Vector2 candRectLt, candRectRb; - if (!expandUpward) - { - candRectLt = windowPos + candTextSize with { X = 0 } + pad with { X = 0 }; - candRectRb = windowPos + windowSize; - if (drawIme) - candRectLt.Y += spaceY + candTextSize.Y; - } - else - { - candRectLt = windowPos; - candRectRb = windowPos + (windowSize - candTextSize with { X = 0 } - pad with { X = 0 }); - if (drawIme) - candRectRb.Y -= spaceY + candTextSize.Y; - } - - drawList.AddRectFilled( - candRectLt, - candRectRb, - ImGui.GetColorU32(ImGuiCol.WindowBg), - ImGui.GetStyle().WindowRounding); - } - - if (!expandUpward && drawIme) - { - for (var dx = -2; dx <= 2; dx++) - { - for (var dy = -2; dy <= 2; dy++) - { - if (dx != 0 || dy != 0) - { - imeIconFont.RenderChar( - drawList, - imeIconFont.FontSize, - cursor + new Vector2(dx, dy), - ImGui.GetColorU32(ImGuiCol.WindowBg), - ime.InputModeIcon); - } - } - } - - imeIconFont.RenderChar( - drawList, - imeIconFont.FontSize, - cursor, - ImGui.GetColorU32(ImGuiCol.Text), - ime.InputModeIcon); - cursor.Y += candTextSize.Y + spaceY; - } - - if (!expandUpward && drawConv) - { - DrawTextBeingConverted(); - cursor.Y += candTextSize.Y + spaceY; - - // Add a separator. - drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); - } - - if (drawCand) - { - // Add the candidate words. - for (var i = 0; i < ime.ImmCand.Count; i++) - { - var selected = i == (native.dwSelection % ImePageSize); - var color = ImGui.GetColorU32(ImGuiCol.Text); - if (selected) - color = ImGui.GetColorU32(ImGuiCol.NavHighlight); - - drawList.AddText(cursor, color, $"{i + 1}. {ime.ImmCand[i]}"); - cursor.Y += candTextSize.Y + spaceY; - } - - // Add a separator - drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); - - // Add the pages infomation. - drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), pageInfo); - cursor.Y += candTextSize.Y + spaceY; - } - - if (expandUpward && drawConv) - { - // Add a separator. - drawList.AddLine(cursor, cursor + new Vector2(maxTextWidth, 0), ImGui.GetColorU32(ImGuiCol.Separator)); - - DrawTextBeingConverted(); - cursor.Y += candTextSize.Y + spaceY; - } - - if (expandUpward && drawIme) - { - for (var dx = -2; dx <= 2; dx++) - { - for (var dy = -2; dy <= 2; dy++) - { - if (dx != 0 || dy != 0) - { - imeIconFont.RenderChar( - drawList, - imeIconFont.FontSize, - cursor + new Vector2(dx, dy), - ImGui.GetColorU32(ImGuiCol.WindowBg), - ime.InputModeIcon); - } - } - } - - imeIconFont.RenderChar( - drawList, - imeIconFont.FontSize, - cursor, - ImGui.GetColorU32(ImGuiCol.Text), - ime.InputModeIcon); - } - - return; - - void DrawTextBeingConverted() - { - // Draw the text background. - drawList.AddRectFilled( - cursor - (pad / 2), - cursor + candTextSize + (pad / 2), - ImGui.GetColorU32(ImGuiCol.WindowBg)); - - // If only a part of the full text is marked for conversion, then draw background for the part being edited. - if (ime.PartialConversionFrom != 0 || ime.PartialConversionTo != ime.ImmComp.Length) - { - var part1 = ime.ImmComp[..ime.PartialConversionFrom]; - var part2 = ime.ImmComp[..ime.PartialConversionTo]; - var size1 = ImGui.CalcTextSize(part1); - var size2 = ImGui.CalcTextSize(part2); - drawList.AddRectFilled( - cursor + size1 with { Y = 0 }, - cursor + size2, - ImGui.GetColorU32(ImGuiCol.TextSelectedBg)); - } - - // Add the text being converted. - drawList.AddText(cursor, ImGui.GetColorU32(ImGuiCol.Text), ime.ImmComp); - - // Draw the caret inside the composition string. - if (DalamudIme.ShowCursorInInputText) - { - var partBeforeCaret = ime.ImmComp[..ime.CompositionCursorOffset]; - var sizeBeforeCaret = ImGui.CalcTextSize(partBeforeCaret); - drawList.AddLine( - cursor + sizeBeforeCaret with { Y = 0 }, - cursor + sizeBeforeCaret, - ImGui.GetColorU32(ImGuiCol.Text)); - } - } - } -} diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 3ff7cde76..acd7c2b6f 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -165,6 +165,7 @@ internal static class ServiceManager var earlyLoadingServices = new HashSet(); var blockingEarlyLoadingServices = new HashSet(); + var providedServices = new HashSet(); var dependencyServicesMap = new Dictionary>(); var getAsyncTaskMap = new Dictionary(); @@ -197,7 +198,10 @@ internal static class ServiceManager // We don't actually need to load provided services, something else does if (serviceKind.HasFlag(ServiceKind.ProvidedService)) + { + providedServices.Add(serviceType); continue; + } Debug.Assert( serviceKind.HasFlag(ServiceKind.EarlyLoadedService) || @@ -340,7 +344,16 @@ internal static class ServiceManager } if (!tasks.Any()) - throw new InvalidOperationException("Unresolvable dependency cycle detected"); + { + // No more services we can start loading for now. + // Either we're waiting for provided services, or there's a dependency cycle. + providedServices.RemoveWhere(x => getAsyncTaskMap[x].IsCompleted); + if (providedServices.Any()) + await Task.WhenAny(providedServices.Select(x => getAsyncTaskMap[x])); + else + throw new InvalidOperationException("Unresolvable dependency cycle detected"); + continue; + } if (servicesToLoad.Any()) { From 4c0f7b7eba4613f3444afdb276f121e952d97052 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Fri, 8 Mar 2024 02:13:30 +0100 Subject: [PATCH 566/585] Update ClientStructs (#1691) 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 722a2c512..ac2ced26f 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 722a2c512238ac4b5324e3d343b316d8c8633a02 +Subproject commit ac2ced26fc98153c65f5b8f0eaf0f464258ff683 From 88a8d457989bab22b06295192382a0eccfce2eab Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 8 Mar 2024 10:47:11 +0900 Subject: [PATCH 567/585] Accommodate nested AddonLifecycle event calls (#1698) * Accommodate nested AddonLifecycle event calls The game is free to call event handlers of another addon from one addon, but the previous code was written under the assumption that only one function may be called at a time. This changes the recycled addon args into pooled args. * Always clear addon name cache --- Dalamud/Dalamud.csproj | 4 - .../Game/Addon/AddonLifecyclePooledArgs.cs | 107 ++++++++++++++++++ .../Game/Addon/Events/AddonEventManager.cs | 2 +- .../Lifecycle/AddonArgTypes/AddonArgs.cs | 6 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 84 +++++++------- .../AddonLifecycleReceiveEventListener.cs | 31 +++-- 6 files changed, 165 insertions(+), 69 deletions(-) create mode 100644 Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 205681cb8..7e166d8b3 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -112,10 +112,6 @@ - - - - diff --git a/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs new file mode 100644 index 000000000..14def2036 --- /dev/null +++ b/Dalamud/Game/Addon/AddonLifecyclePooledArgs.cs @@ -0,0 +1,107 @@ +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/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 4231b0d09..8ee09bed8 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Events; /// Service provider for addon event management. ///
    [InterfaceVersion("1.0")] -[ServiceManager.BlockingEarlyLoadedService] +[ServiceManager.EarlyLoadedService] internal unsafe class AddonEventManager : IDisposable, IServiceType { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs index 4ab3de5ca..1095202cc 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonArgTypes/AddonArgs.cs @@ -44,10 +44,10 @@ public abstract unsafe class AddonArgs get => this.addon; set { - if (this.addon == value) - return; - this.addon = value; + + // Note: always clear addonName on updating the addon being pointed. + // Same address may point to a different addon. this.addonName = null; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index beaab7fcd..37f12ce3a 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; @@ -19,7 +18,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// This class provides events for in-game addon lifecycles. /// [InterfaceVersion("1.0")] -[ServiceManager.BlockingEarlyLoadedService] +[ServiceManager.EarlyLoadedService] internal unsafe class AddonLifecycle : IDisposable, IServiceType { private static readonly ModuleLog Log = new("AddonLifecycle"); @@ -27,6 +26,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); + private readonly nint disallowedReceiveEventAddress; private readonly AddonLifecycleAddressResolver address; @@ -38,18 +40,6 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private readonly Hook onAddonRefreshHook; private readonly CallHook onAddonRequestedUpdateHook; - // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet - // package, and these events are always called from the main thread, this is fine. -#pragma warning disable CS0618 // Type or member is obsolete - // TODO: turn constructors of these internal - private readonly AddonSetupArgs recyclingSetupArgs = new(); - private readonly AddonFinalizeArgs recyclingFinalizeArgs = new(); - private readonly AddonDrawArgs recyclingDrawArgs = new(); - private readonly AddonUpdateArgs recyclingUpdateArgs = new(); - private readonly AddonRefreshArgs recyclingRefreshArgs = new(); - private readonly AddonRequestedUpdateArgs recyclingRequestedUpdateArgs = new(); -#pragma warning restore CS0618 // Type or member is obsolete - [ServiceManager.ServiceConstructor] private AddonLifecycle(TargetSigScanner sigScanner) { @@ -253,12 +243,13 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonSetup ReceiveEvent Registration."); } - this.recyclingSetupArgs.AddonInternal = (nint)addon; - this.recyclingSetupArgs.AtkValueCount = valueCount; - this.recyclingSetupArgs.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreSetup, this.recyclingSetupArgs); - valueCount = this.recyclingSetupArgs.AtkValueCount; - values = (AtkValue*)this.recyclingSetupArgs.AtkValues; + using var returner = this.argsPool.Rent(out AddonSetupArgs arg); + arg.AddonInternal = (nint)addon; + arg.AtkValueCount = valueCount; + arg.AtkValues = (nint)values; + this.InvokeListenersSafely(AddonEvent.PreSetup, arg); + valueCount = arg.AtkValueCount; + values = (AtkValue*)arg.AtkValues; try { @@ -269,7 +260,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType 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, this.recyclingSetupArgs); + this.InvokeListenersSafely(AddonEvent.PostSetup, arg); } private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) @@ -284,8 +275,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType Log.Error(e, "Exception in OnAddonFinalize ReceiveEvent Removal."); } - this.recyclingFinalizeArgs.AddonInternal = (nint)atkUnitBase[0]; - this.InvokeListenersSafely(AddonEvent.PreFinalize, this.recyclingFinalizeArgs); + using var returner = this.argsPool.Rent(out AddonFinalizeArgs arg); + arg.AddonInternal = (nint)atkUnitBase[0]; + this.InvokeListenersSafely(AddonEvent.PreFinalize, arg); try { @@ -299,8 +291,9 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType private void OnAddonDraw(AtkUnitBase* addon) { - this.recyclingDrawArgs.AddonInternal = (nint)addon; - this.InvokeListenersSafely(AddonEvent.PreDraw, this.recyclingDrawArgs); + using var returner = this.argsPool.Rent(out AddonDrawArgs arg); + arg.AddonInternal = (nint)addon; + this.InvokeListenersSafely(AddonEvent.PreDraw, arg); try { @@ -311,14 +304,15 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType 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, this.recyclingDrawArgs); + this.InvokeListenersSafely(AddonEvent.PostDraw, arg); } private void OnAddonUpdate(AtkUnitBase* addon, float delta) { - this.recyclingUpdateArgs.AddonInternal = (nint)addon; - this.recyclingUpdateArgs.TimeDeltaInternal = delta; - this.InvokeListenersSafely(AddonEvent.PreUpdate, this.recyclingUpdateArgs); + using var returner = this.argsPool.Rent(out AddonUpdateArgs arg); + arg.AddonInternal = (nint)addon; + arg.TimeDeltaInternal = delta; + this.InvokeListenersSafely(AddonEvent.PreUpdate, arg); try { @@ -329,19 +323,20 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType 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, this.recyclingUpdateArgs); + this.InvokeListenersSafely(AddonEvent.PostUpdate, arg); } private byte OnAddonRefresh(AtkUnitManager* atkUnitManager, AtkUnitBase* addon, uint valueCount, AtkValue* values) { byte result = 0; - this.recyclingRefreshArgs.AddonInternal = (nint)addon; - this.recyclingRefreshArgs.AtkValueCount = valueCount; - this.recyclingRefreshArgs.AtkValues = (nint)values; - this.InvokeListenersSafely(AddonEvent.PreRefresh, this.recyclingRefreshArgs); - valueCount = this.recyclingRefreshArgs.AtkValueCount; - values = (AtkValue*)this.recyclingRefreshArgs.AtkValues; + using var returner = this.argsPool.Rent(out AddonRefreshArgs arg); + arg.AddonInternal = (nint)addon; + arg.AtkValueCount = valueCount; + arg.AtkValues = (nint)values; + this.InvokeListenersSafely(AddonEvent.PreRefresh, arg); + valueCount = arg.AtkValueCount; + values = (AtkValue*)arg.AtkValues; try { @@ -352,18 +347,19 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType 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, this.recyclingRefreshArgs); + this.InvokeListenersSafely(AddonEvent.PostRefresh, arg); return result; } private void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) { - this.recyclingRequestedUpdateArgs.AddonInternal = (nint)addon; - this.recyclingRequestedUpdateArgs.NumberArrayData = (nint)numberArrayData; - this.recyclingRequestedUpdateArgs.StringArrayData = (nint)stringArrayData; - this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, this.recyclingRequestedUpdateArgs); - numberArrayData = (NumberArrayData**)this.recyclingRequestedUpdateArgs.NumberArrayData; - stringArrayData = (StringArrayData**)this.recyclingRequestedUpdateArgs.StringArrayData; + using var returner = this.argsPool.Rent(out AddonRequestedUpdateArgs arg); + arg.AddonInternal = (nint)addon; + arg.NumberArrayData = (nint)numberArrayData; + arg.StringArrayData = (nint)stringArrayData; + this.InvokeListenersSafely(AddonEvent.PreRequestedUpdate, arg); + numberArrayData = (NumberArrayData**)arg.NumberArrayData; + stringArrayData = (StringArrayData**)arg.StringArrayData; try { @@ -374,7 +370,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType 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, this.recyclingRequestedUpdateArgs); + this.InvokeListenersSafely(AddonEvent.PostRequestedUpdate, arg); } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs index 43aa71661..fd3b5d79d 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycleReceiveEventListener.cs @@ -16,12 +16,8 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable { private static readonly ModuleLog Log = new("AddonLifecycle"); - // Note: these can be sourced from ObjectPool of appropriate types instead, but since we don't import that NuGet - // package, and these events are always called from the main thread, this is fine. -#pragma warning disable CS0618 // Type or member is obsolete - // TODO: turn constructors of these internal - private readonly AddonReceiveEventArgs recyclingReceiveEventArgs = new(); -#pragma warning restore CS0618 // Type or member is obsolete + [ServiceManager.ServiceDependency] + private readonly AddonLifecyclePooledArgs argsPool = Service.Get(); /// /// Initializes a new instance of the class. @@ -82,16 +78,17 @@ internal unsafe class AddonLifecycleReceiveEventListener : IDisposable return; } - this.recyclingReceiveEventArgs.AddonInternal = (nint)addon; - this.recyclingReceiveEventArgs.AtkEventType = (byte)eventType; - this.recyclingReceiveEventArgs.EventParam = eventParam; - this.recyclingReceiveEventArgs.AtkEvent = (IntPtr)atkEvent; - this.recyclingReceiveEventArgs.Data = data; - this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, this.recyclingReceiveEventArgs); - eventType = (AtkEventType)this.recyclingReceiveEventArgs.AtkEventType; - eventParam = this.recyclingReceiveEventArgs.EventParam; - atkEvent = (AtkEvent*)this.recyclingReceiveEventArgs.AtkEvent; - data = this.recyclingReceiveEventArgs.Data; + using var returner = this.argsPool.Rent(out AddonReceiveEventArgs arg); + arg.AddonInternal = (nint)addon; + arg.AtkEventType = (byte)eventType; + arg.EventParam = eventParam; + arg.AtkEvent = (IntPtr)atkEvent; + arg.Data = data; + this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PreReceiveEvent, arg); + eventType = (AtkEventType)arg.AtkEventType; + eventParam = arg.EventParam; + atkEvent = (AtkEvent*)arg.AtkEvent; + data = arg.Data; try { @@ -102,6 +99,6 @@ internal unsafe class AddonLifecycleReceiveEventListener : 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.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, this.recyclingReceiveEventArgs); + this.AddonLifecycle.InvokeListenersSafely(AddonEvent.PostReceiveEvent, arg); } } From 637ba78956553714d66004913b4c939527c580bf Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Mar 2024 00:01:20 +0900 Subject: [PATCH 568/585] At least make it not drop character after conversion with google IME --- Dalamud/Interface/Internal/DalamudIme.cs | 166 +++++++++++++++++++---- 1 file changed, 138 insertions(+), 28 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index 6c01b74d7..bbfe819a8 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -18,7 +18,9 @@ using Dalamud.Interface.Utility; using ImGuiNET; +#if IMEDEBUG using Serilog; +#endif using TerraFX.Interop.Windows; @@ -267,6 +269,50 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType { var invalidTarget = TextState->Id == 0 || (TextState->Flags & ImGuiInputTextFlags.ReadOnly) != 0; +#if IMEDEBUG + switch (args.Message) + { + case WM.WM_IME_NOTIFY: + Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({ImeDebug.ImnName((int)args.WParam)}, 0x{args.LParam:X})"); + break; + case WM.WM_IME_CONTROL: + Log.Verbose( + $"{nameof(WM.WM_IME_CONTROL)}({ImeDebug.ImcName((int)args.WParam)}, 0x{args.LParam:X})"); + break; + case WM.WM_IME_REQUEST: + Log.Verbose( + $"{nameof(WM.WM_IME_REQUEST)}({ImeDebug.ImrName((int)args.WParam)}, 0x{args.LParam:X})"); + break; + case WM.WM_IME_SELECT: + Log.Verbose($"{nameof(WM.WM_IME_SELECT)}({(int)args.WParam != 0}, 0x{args.LParam:X})"); + break; + case WM.WM_IME_STARTCOMPOSITION: + Log.Verbose($"{nameof(WM.WM_IME_STARTCOMPOSITION)}()"); + break; + case WM.WM_IME_COMPOSITION: + Log.Verbose( + $"{nameof(WM.WM_IME_COMPOSITION)}({(char)args.WParam}, {ImeDebug.GcsName((int)args.LParam)})"); + break; + case WM.WM_IME_COMPOSITIONFULL: + Log.Verbose($"{nameof(WM.WM_IME_COMPOSITIONFULL)}()"); + break; + case WM.WM_IME_ENDCOMPOSITION: + Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}()"); + break; + case WM.WM_IME_CHAR: + Log.Verbose($"{nameof(WM.WM_IME_CHAR)}({(char)args.WParam}, 0x{args.LParam:X})"); + break; + case WM.WM_IME_KEYDOWN: + Log.Verbose($"{nameof(WM.WM_IME_KEYDOWN)}({(char)args.WParam}, 0x{args.LParam:X})"); + break; + case WM.WM_IME_KEYUP: + Log.Verbose($"{nameof(WM.WM_IME_KEYUP)}({(char)args.WParam}, 0x{args.LParam:X})"); + break; + case WM.WM_IME_SETCONTEXT: + Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(int)args.WParam != 0}, 0x{args.LParam:X})"); + break; + } +#endif switch (args.Message) { case WM.WM_IME_NOTIFY @@ -286,22 +332,15 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType else this.ReplaceCompositionString(hImc, (uint)args.LParam); - // Log.Verbose($"{nameof(WM.WM_IME_COMPOSITION)}({(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithValue(0); break; case WM.WM_IME_ENDCOMPOSITION: - // Log.Verbose($"{nameof(WM.WM_IME_ENDCOMPOSITION)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); + this.ClearState(hImc, false); args.SuppressWithValue(0); break; - case WM.WM_IME_CONTROL: - // Log.Verbose($"{nameof(WM.WM_IME_CONTROL)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); - args.SuppressWithValue(0); - break; - case WM.WM_IME_REQUEST: - // Log.Verbose($"{nameof(WM.WM_IME_REQUEST)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithValue(0); break; @@ -309,14 +348,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // Hide candidate and composition windows. args.LParam = (LPARAM)((nint)args.LParam & ~(ISC_SHOWUICOMPOSITIONWINDOW | 0xF)); - // Log.Verbose($"{nameof(WM.WM_IME_SETCONTEXT)}({(nint)args.WParam:X}, {(nint)args.LParam:X}): {this.compositionString}"); args.SuppressWithDefault(); break; - case WM.WM_IME_NOTIFY: - // Log.Verbose($"{nameof(WM.WM_IME_NOTIFY)}({(nint)args.WParam:X}): {this.compositionString}"); - break; - case WM.WM_KEYDOWN when (int)args.WParam is VK.VK_TAB or VK.VK_PRIOR @@ -335,7 +369,11 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } this.UpdateCandidates(hImc); + break; + case WM.WM_KEYDOWN when (int)args.WParam is VK.VK_ESCAPE && this.candidateStrings.Count != 0: + this.ClearState(hImc); + args.SuppressWithDefault(); break; case WM.WM_LBUTTONDOWN: @@ -344,15 +382,14 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType case WM.WM_XBUTTONDOWN: ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); break; - - // default: - // Log.Verbose($"{(WmNames.TryGetValue((int)args.Message, out var v) ? v : args.Message.ToString())}({(nint)args.WParam:X}, {(nint)args.LParam:X})"); - // break; } - this.UpdateInputLanguage(hImc); - if (this.inputModeIcon == (char)SeIconChar.ImeKoreanHangul) - this.UpdateCandidates(hImc); + if (args.Message != WM.WM_MOUSEMOVE) + { + this.UpdateInputLanguage(hImc); + if (this.inputModeIcon == (char)SeIconChar.ImeKoreanHangul) + this.UpdateCandidates(hImc); + } } finally { @@ -367,8 +404,6 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType var lang = GetKeyboardLayout(0); var open = ImmGetOpenStatus(hImc) != false; - // Log.Verbose($"{nameof(this.UpdateInputLanguage)}: conv={conv:X} sent={sent:X} open={open} lang={lang:X}"); - var native = (conv & 1) != 0; var katakana = (conv & 2) != 0; var fullwidth = (conv & 8) != 0; @@ -418,6 +453,10 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); +#if IMEDEBUG + Log.Verbose($"{nameof(this.ReplaceCompositionString)}({newString})"); +#endif + this.ReflectCharacterEncounters(newString); if (this.temporaryUndoSelection is not null) @@ -436,8 +475,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType if (finalCommit) { - this.ClearState(hImc); - return; + this.ClearState(hImc, false); + newString = string.Empty; } this.compositionString = newString; @@ -471,17 +510,21 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.UpdateCandidates(hImc); } - private void ClearState(HIMC hImc) + private void ClearState(HIMC hImc, bool invokeCancel = true) { this.compositionString = string.Empty; this.partialConversionFrom = this.partialConversionTo = 0; this.compositionCursorOffset = 0; this.temporaryUndoSelection = null; TextState->Stb.SelectStart = TextState->Stb.Cursor = TextState->Stb.SelectEnd; - ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); - this.UpdateCandidates(default); + this.candidateStrings.Clear(); + this.immCandNative = default; + if (invokeCancel) + ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); - // Log.Information($"{nameof(this.ClearState)}"); +#if IMEDEBUG + Log.Information($"{nameof(this.ClearState)}({invokeCancel})"); +#endif } private void UpdateCandidates(HIMC hImc) @@ -932,4 +975,71 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType return true; } } + +#if IMEDEBUG + private static class ImeDebug + { + private static readonly (int Value, string Name)[] GcsFields = + { + (GCS.GCS_COMPREADSTR, nameof(GCS.GCS_COMPREADSTR)), + (GCS.GCS_COMPREADATTR, nameof(GCS.GCS_COMPREADATTR)), + (GCS.GCS_COMPREADCLAUSE, nameof(GCS.GCS_COMPREADCLAUSE)), + (GCS.GCS_COMPSTR, nameof(GCS.GCS_COMPSTR)), + (GCS.GCS_COMPATTR, nameof(GCS.GCS_COMPATTR)), + (GCS.GCS_COMPCLAUSE, nameof(GCS.GCS_COMPCLAUSE)), + (GCS.GCS_CURSORPOS, nameof(GCS.GCS_CURSORPOS)), + (GCS.GCS_DELTASTART, nameof(GCS.GCS_DELTASTART)), + (GCS.GCS_RESULTREADSTR, nameof(GCS.GCS_RESULTREADSTR)), + (GCS.GCS_RESULTREADCLAUSE, nameof(GCS.GCS_RESULTREADCLAUSE)), + (GCS.GCS_RESULTSTR, nameof(GCS.GCS_RESULTSTR)), + (GCS.GCS_RESULTCLAUSE, nameof(GCS.GCS_RESULTCLAUSE)), + }; + + private static readonly IReadOnlyDictionary ImnFields = + typeof(IMN) + .GetFields(BindingFlags.Static | BindingFlags.Public) + .Where(x => x.IsLiteral) + .ToDictionary(x => (int)x.GetRawConstantValue()!, x => x.Name); + + public static string GcsName(int val) + { + var sb = new StringBuilder(); + foreach (var (value, name) in GcsFields) + { + if ((val & value) != 0) + { + if (sb.Length != 0) + sb.Append(" | "); + sb.Append(name); + val &= ~value; + } + } + + if (val != 0) + { + if (sb.Length != 0) + sb.Append(" | "); + sb.Append($"0x{val:X}"); + } + + return sb.ToString(); + } + + public static string ImcName(int val) => ImnFields.TryGetValue(val, out var name) ? name : $"0x{val:X}"; + + public static string ImnName(int val) => ImnFields.TryGetValue(val, out var name) ? name : $"0x{val:X}"; + + public static string ImrName(int val) => val switch + { + IMR_CANDIDATEWINDOW => nameof(IMR_CANDIDATEWINDOW), + IMR_COMPOSITIONFONT => nameof(IMR_COMPOSITIONFONT), + IMR_COMPOSITIONWINDOW => nameof(IMR_COMPOSITIONWINDOW), + IMR_CONFIRMRECONVERTSTRING => nameof(IMR_CONFIRMRECONVERTSTRING), + IMR_DOCUMENTFEED => nameof(IMR_DOCUMENTFEED), + IMR_QUERYCHARPOSITION => nameof(IMR_QUERYCHARPOSITION), + IMR_RECONVERTSTRING => nameof(IMR_RECONVERTSTRING), + _ => $"0x{val:X}", + }; + } +#endif } From e7815c59d551645e3a5fe5a5ecfd9d189101202b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Mar 2024 00:16:46 +0900 Subject: [PATCH 569/585] fix? --- Dalamud/Interface/Internal/DalamudIme.cs | 64 +++++++++++++++++++----- 1 file changed, 52 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index bbfe819a8..caf014885 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -1,3 +1,5 @@ +// #define IMEDEBUG + using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -108,6 +110,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType /// Undo range for modifying the buffer while composition is in progress. private (int Start, int End, int Cursor)? temporaryUndoSelection; + private bool updateInputLanguage = true; + private bool updateImeStatusAgain; + [SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1003:Symbols should be spaced correctly", Justification = ".")] static DalamudIme() { @@ -255,15 +260,24 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType private void WndProcHookManagerOnPreWndProc(WndProcEventArgs args) { if (!ImGuiHelpers.IsImGuiInitialized) + { + this.updateInputLanguage = true; return; + } // Are we not the target of text input? if (!ImGui.GetIO().WantTextInput) + { + this.updateInputLanguage = true; return; + } var hImc = ImmGetContext(args.Hwnd); if (hImc == nint.Zero) + { + this.updateInputLanguage = true; return; + } try { @@ -313,16 +327,36 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType break; } #endif + if (this.updateInputLanguage + || (args.Message == WM.WM_IME_NOTIFY + && (int)args.WParam + is IMN.IMN_SETCONVERSIONMODE + or IMN.IMN_OPENSTATUSWINDOW + or IMN.IMN_CLOSESTATUSWINDOW)) + { + this.UpdateInputLanguage(hImc); + this.updateInputLanguage = false; + } + + if (this.updateImeStatusAgain) + { + this.ReplaceCompositionString(hImc, false); + this.UpdateCandidates(hImc); + this.updateImeStatusAgain = false; + } + switch (args.Message) { case WM.WM_IME_NOTIFY when (nint)args.WParam is IMN.IMN_OPENCANDIDATE or IMN.IMN_CLOSECANDIDATE or IMN.IMN_CHANGECANDIDATE: this.UpdateCandidates(hImc); + this.updateImeStatusAgain = true; args.SuppressWithValue(0); break; case WM.WM_IME_STARTCOMPOSITION: + this.updateImeStatusAgain = true; args.SuppressWithValue(0); break; @@ -330,17 +364,24 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType if (invalidTarget) ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_CANCEL, 0); else - this.ReplaceCompositionString(hImc, (uint)args.LParam); + this.ReplaceCompositionString(hImc, ((int)args.LParam & GCS.GCS_RESULTSTR) != 0); + this.updateImeStatusAgain = true; args.SuppressWithValue(0); break; case WM.WM_IME_ENDCOMPOSITION: this.ClearState(hImc, false); + this.updateImeStatusAgain = true; args.SuppressWithValue(0); break; + + case WM.WM_IME_CHAR: + case WM.WM_IME_KEYDOWN: + case WM.WM_IME_KEYUP: case WM.WM_IME_CONTROL: case WM.WM_IME_REQUEST: + this.updateImeStatusAgain = true; args.SuppressWithValue(0); break; @@ -348,9 +389,16 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType // Hide candidate and composition windows. args.LParam = (LPARAM)((nint)args.LParam & ~(ISC_SHOWUICOMPOSITIONWINDOW | 0xF)); + this.updateImeStatusAgain = true; args.SuppressWithDefault(); break; + case WM.WM_IME_NOTIFY: + case WM.WM_IME_COMPOSITIONFULL: + case WM.WM_IME_SELECT: + this.updateImeStatusAgain = true; + break; + case WM.WM_KEYDOWN when (int)args.WParam is VK.VK_TAB or VK.VK_PRIOR @@ -383,13 +431,6 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType ImmNotifyIME(hImc, NI.NI_COMPOSITIONSTR, CPS_COMPLETE, 0); break; } - - if (args.Message != WM.WM_MOUSEMOVE) - { - this.UpdateInputLanguage(hImc); - if (this.inputModeIcon == (char)SeIconChar.ImeKoreanHangul) - this.UpdateCandidates(hImc); - } } finally { @@ -446,9 +487,8 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType } } - private void ReplaceCompositionString(HIMC hImc, uint comp) + private void ReplaceCompositionString(HIMC hImc, bool finalCommit) { - var finalCommit = (comp & GCS.GCS_RESULTSTR) != 0; var newString = finalCommit ? ImmGetCompositionString(hImc, GCS.GCS_RESULTSTR) : ImmGetCompositionString(hImc, GCS.GCS_COMPSTR); @@ -482,9 +522,9 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.compositionString = newString; this.compositionCursorOffset = ImmGetCompositionStringW(hImc, GCS.GCS_CURSORPOS, null, 0); - if ((comp & GCS.GCS_COMPATTR) != 0) + var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); + if (attrLength > 0) { - var attrLength = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, null, 0); var attrPtr = stackalloc byte[attrLength]; var attr = new Span(attrPtr, Math.Min(this.compositionString.Length, attrLength)); _ = ImmGetCompositionStringW(hImc, GCS.GCS_COMPATTR, attrPtr, (uint)attrLength); From 14a5e5b652e4bf00de6ff470cf81a3155f725374 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Sat, 9 Mar 2024 04:09:29 +0900 Subject: [PATCH 570/585] ConsoleWindow racecon fix and highlight RollingList is not thread safe, but the lock around it was inconsistent, resulting in occasional null value in the log list. Fixed by utilizing ConcurrentQueue so that logs can be added from any thread without locks, and reading from the queue and adding to the list from the framework thread. Also, added log line highlight feature. --- .../Internal/Windows/ConsoleWindow.cs | 789 ++++++++++++------ Dalamud/Utility/ThreadSafety.cs | 12 + 2 files changed, 537 insertions(+), 264 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index f36d79222..1957ab720 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -1,24 +1,28 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Drawing; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; -using System.Threading; using Dalamud.Configuration.Internal; +using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; +using Dalamud.Plugin.Services; using Dalamud.Utility; + using ImGuiNET; + using Serilog; using Serilog.Events; @@ -31,39 +35,48 @@ internal class ConsoleWindow : Window, IDisposable { private const int LogLinesMinimum = 100; private const int LogLinesMaximum = 1000000; - + + // Only this field may be touched from any thread. + private readonly ConcurrentQueue<(string Line, LogEvent LogEvent)> newLogEntries; + + // Fields below should be touched only from the main thread. private readonly RollingList logText; - private volatile int newRolledLines; - private readonly object renderLock = new(); + private readonly RollingList filteredLogEntries; private readonly List history = new(); private readonly List pluginFilters = new(); + private int newRolledLines; + private bool pendingRefilter; + private bool pendingClearLog; + private bool? lastCmdSuccess; + private ImGuiListClipperPtr clipperPtr; private string commandText = string.Empty; private string textFilter = string.Empty; + private string textHighlight = string.Empty; private string selectedSource = "DalamudInternal"; private string pluginFilter = string.Empty; + private Regex? compiledLogFilter; + private Regex? compiledLogHighlight; + private Exception? exceptionLogFilter; + private Exception? exceptionLogHighlight; + private bool filterShowUncaughtExceptions; private bool settingsPopupWasOpen; private bool showFilterToolbar; - private bool clearLog; - private bool copyLog; private bool copyMode; private bool killGameArmed; private bool autoScroll; private int logLinesLimit; private bool autoOpen; - private bool regexError; private int historyPos; private int copyStart = -1; - /// - /// Initializes a new instance of the class. - /// + /// Initializes a new instance of the class. /// An instance of . public ConsoleWindow(DalamudConfiguration configuration) : base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) @@ -72,6 +85,8 @@ internal class ConsoleWindow : Window, IDisposable this.autoOpen = configuration.LogOpenAtStartup; SerilogEventSink.Instance.LogLine += this.OnLogLine; + Service.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate); + this.Size = new Vector2(500, 400); this.SizeCondition = ImGuiCond.FirstUseEver; @@ -85,13 +100,17 @@ internal class ConsoleWindow : Window, IDisposable this.logLinesLimit = configuration.LogLinesLimit; var limit = Math.Max(LogLinesMinimum, this.logLinesLimit); + this.newLogEntries = new(); this.logText = new(limit); - this.FilteredLogEntries = new(limit); + this.filteredLogEntries = new(limit); configuration.DalamudConfigurationSaved += this.OnDalamudConfigurationSaved; - } - private RollingList FilteredLogEntries { get; set; } + unsafe + { + this.clipperPtr = new(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); + } + } /// public override void OnOpen() @@ -100,58 +119,16 @@ internal class ConsoleWindow : Window, IDisposable base.OnOpen(); } - /// - /// Dispose of managed and unmanaged resources. - /// + /// public void Dispose() { SerilogEventSink.Instance.LogLine -= this.OnLogLine; Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; - } + if (Service.GetNullable() is { } framework) + framework.Update -= this.FrameworkOnUpdate; - /// - /// Clear the window of all log entries. - /// - public void Clear() - { - lock (this.renderLock) - { - this.logText.Clear(); - this.FilteredLogEntries.Clear(); - this.clearLog = false; - } - } - - /// - /// Copies the entire log contents to clipboard. - /// - public void CopyLog() - { - ImGui.LogToClipboard(); - } - - /// - /// Add a single log line to the display. - /// - /// The line to add. - /// The Serilog event associated with this line. - public void HandleLogLine(string line, LogEvent logEvent) - { - if (line.IndexOfAny(new[] { '\n', '\r' }) != -1) - { - var subLines = line.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); - - this.AddAndFilter(subLines[0], logEvent, false); - - for (var i = 1; i < subLines.Length; i++) - { - this.AddAndFilter(subLines[i], logEvent, true); - } - } - else - { - this.AddAndFilter(line, logEvent, false); - } + this.clipperPtr.Destroy(); + this.clipperPtr = default; } /// @@ -161,112 +138,126 @@ internal class ConsoleWindow : Window, IDisposable this.DrawFilterToolbar(); - if (this.regexError) + if (this.exceptionLogFilter is not null) { - const string regexErrorString = "Regex Filter Error"; - ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X / 2.0f - ImGui.CalcTextSize(regexErrorString).X / 2.0f); - ImGui.TextColored(ImGuiColors.DalamudRed, regexErrorString); + ImGui.TextColored( + ImGuiColors.DalamudRed, + $"Regex Filter Error: {this.exceptionLogFilter.GetType().Name}"); + ImGui.TextUnformatted(this.exceptionLogFilter.Message); + } + + if (this.exceptionLogHighlight is not null) + { + ImGui.TextColored( + ImGuiColors.DalamudRed, + $"Regex Highlight Error: {this.exceptionLogHighlight.GetType().Name}"); + ImGui.TextUnformatted(this.exceptionLogHighlight.Message); } var sendButtonSize = ImGui.CalcTextSize("Send") + ((new Vector2(16, 0) + (ImGui.GetStyle().FramePadding * 2)) * ImGuiHelpers.GlobalScale); var scrollingHeight = ImGui.GetContentRegionAvail().Y - sendButtonSize.Y; - ImGui.BeginChild("scrolling", new Vector2(0, scrollingHeight), false, ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); - - if (this.clearLog) this.Clear(); - - if (this.copyLog) this.CopyLog(); + ImGui.BeginChild( + "scrolling", + new Vector2(0, scrollingHeight), + false, + ImGuiWindowFlags.AlwaysHorizontalScrollbar | ImGuiWindowFlags.AlwaysVerticalScrollbar); ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - ImGuiListClipperPtr clipper; - unsafe - { - clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper()); - } - ImGui.PushFont(InterfaceManager.MonoFont); var childPos = ImGui.GetWindowPos(); var childDrawList = ImGui.GetWindowDrawList(); var childSize = ImGui.GetWindowSize(); - var cursorDiv = ImGui.CalcTextSize("00:00:00.000 ").X; - var cursorLogLevel = ImGui.CalcTextSize("00:00:00.000 | ").X; - var dividerOffset = ImGui.CalcTextSize("00:00:00.000 | AAA ").X + (ImGui.CalcTextSize(" ").X / 2); - var cursorLogLine = ImGui.CalcTextSize("00:00:00.000 | AAA | ").X; + var timestampWidth = ImGui.CalcTextSize("00:00:00.000").X; + var levelWidth = ImGui.CalcTextSize("AAA").X; + var separatorWidth = ImGui.CalcTextSize(" | ").X; + var cursorLogLevel = timestampWidth + separatorWidth; + var cursorLogLine = cursorLogLevel + levelWidth + separatorWidth; var lastLinePosY = 0.0f; var logLineHeight = 0.0f; - lock (this.renderLock) + this.clipperPtr.Begin(this.filteredLogEntries.Count); + while (this.clipperPtr.Step()) { - clipper.Begin(this.FilteredLogEntries.Count); - while (clipper.Step()) + for (var i = this.clipperPtr.DisplayStart; i < this.clipperPtr.DisplayEnd; i++) { - for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) + var index = Math.Max( + i - this.newRolledLines, + 0); // Prevents flicker effect. Also workaround to avoid negative indexes. + var line = this.filteredLogEntries[index]; + + if (!line.IsMultiline) + ImGui.Separator(); + + if (line.SelectedForCopy) { - var index = Math.Max(i - this.newRolledLines, 0); // Prevents flicker effect. Also workaround to avoid negative indexes. - var line = this.FilteredLogEntries[index]; + ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGrey); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, ImGuiColors.ParsedGrey); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, ImGuiColors.ParsedGrey); + } + else + { + ImGui.PushStyleColor(ImGuiCol.Header, GetColorForLogEventLevel(line.Level)); + ImGui.PushStyleColor(ImGuiCol.HeaderActive, GetColorForLogEventLevel(line.Level)); + ImGui.PushStyleColor(ImGuiCol.HeaderHovered, GetColorForLogEventLevel(line.Level)); + } - if (!line.IsMultiline && !this.copyLog) - ImGui.Separator(); - - if (line.SelectedForCopy) - { - ImGui.PushStyleColor(ImGuiCol.Header, ImGuiColors.ParsedGrey); - ImGui.PushStyleColor(ImGuiCol.HeaderActive, ImGuiColors.ParsedGrey); - ImGui.PushStyleColor(ImGuiCol.HeaderHovered, ImGuiColors.ParsedGrey); - } - else - { - ImGui.PushStyleColor(ImGuiCol.Header, this.GetColorForLogEventLevel(line.Level)); - ImGui.PushStyleColor(ImGuiCol.HeaderActive, this.GetColorForLogEventLevel(line.Level)); - ImGui.PushStyleColor(ImGuiCol.HeaderHovered, this.GetColorForLogEventLevel(line.Level)); - } + ImGui.Selectable( + "###console_null", + true, + ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns); - ImGui.Selectable("###console_null", true, ImGuiSelectableFlags.AllowItemOverlap | ImGuiSelectableFlags.SpanAllColumns); + // This must be after ImGui.Selectable, it uses ImGui.IsItem... functions + this.HandleCopyMode(i, line); - // This must be after ImGui.Selectable, it uses ImGui.IsItem... functions - this.HandleCopyMode(i, line); - + ImGui.SameLine(); + + ImGui.PopStyleColor(3); + + if (!line.IsMultiline) + { + ImGui.TextUnformatted(line.TimestampString); ImGui.SameLine(); - ImGui.PopStyleColor(3); - - if (!line.IsMultiline) - { - ImGui.TextUnformatted(line.TimeStamp.ToString("HH:mm:ss.fff")); - ImGui.SameLine(); - ImGui.SetCursorPosX(cursorDiv); - ImGui.TextUnformatted("|"); - ImGui.SameLine(); - ImGui.SetCursorPosX(cursorLogLevel); - ImGui.TextUnformatted(this.GetTextForLogEventLevel(line.Level)); - ImGui.SameLine(); - } - - ImGui.SetCursorPosX(cursorLogLine); - ImGui.TextUnformatted(line.Line); - - var currentLinePosY = ImGui.GetCursorPosY(); - logLineHeight = currentLinePosY - lastLinePosY; - lastLinePosY = currentLinePosY; + ImGui.SetCursorPosX(cursorLogLevel); + ImGui.TextUnformatted(GetTextForLogEventLevel(line.Level)); + ImGui.SameLine(); } - } - clipper.End(); - clipper.Destroy(); + ImGui.SetCursorPosX(cursorLogLine); + line.HighlightMatches ??= (this.compiledLogHighlight ?? this.compiledLogFilter)?.Matches(line.Line); + if (line.HighlightMatches is { } matches) + { + this.DrawHighlighted( + line.Line, + matches, + ImGui.GetColorU32(ImGuiCol.Text), + ImGui.GetColorU32(ImGuiColors.HealerGreen)); + } + else + { + ImGui.TextUnformatted(line.Line); + } + + var currentLinePosY = ImGui.GetCursorPosY(); + logLineHeight = currentLinePosY - lastLinePosY; + lastLinePosY = currentLinePosY; + } } + this.clipperPtr.End(); + ImGui.PopFont(); ImGui.PopStyleVar(); - var newRolledLinesCount = Interlocked.Exchange(ref this.newRolledLines, 0); if (!this.autoScroll || ImGui.GetScrollY() < ImGui.GetScrollMaxY()) { - ImGui.SetScrollY(ImGui.GetScrollY() - (logLineHeight * newRolledLinesCount)); + ImGui.SetScrollY(ImGui.GetScrollY() - (logLineHeight * this.newRolledLines)); } if (this.autoScroll && ImGui.GetScrollY() >= ImGui.GetScrollMaxY()) @@ -274,8 +265,19 @@ internal class ConsoleWindow : Window, IDisposable ImGui.SetScrollHereY(1.0f); } - // Draw dividing line - childDrawList.AddLine(new Vector2(childPos.X + dividerOffset, childPos.Y), new Vector2(childPos.X + dividerOffset, childPos.Y + childSize.Y), 0x4FFFFFFF, 1.0f); + // Draw dividing lines + var div1Offset = MathF.Round((timestampWidth + (separatorWidth / 2)) - ImGui.GetScrollX()); + var div2Offset = MathF.Round((cursorLogLevel + levelWidth + (separatorWidth / 2)) - ImGui.GetScrollX()); + childDrawList.AddLine( + new(childPos.X + div1Offset, childPos.Y), + new(childPos.X + div1Offset, childPos.Y + childSize.Y), + 0x4FFFFFFF, + 1.0f); + childDrawList.AddLine( + new(childPos.X + div2Offset, childPos.Y), + new(childPos.X + div2Offset, childPos.Y + childSize.Y), + 0x4FFFFFFF, + 1.0f); ImGui.EndChild(); @@ -293,12 +295,20 @@ internal class ConsoleWindow : Window, IDisposable } } - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - sendButtonSize.X - (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale)); + ImGui.SetNextItemWidth( + ImGui.GetContentRegionAvail().X - sendButtonSize.X - + (ImGui.GetStyle().ItemSpacing.X * ImGuiHelpers.GlobalScale)); var getFocus = false; unsafe { - if (ImGui.InputText("##command_box", ref this.commandText, 255, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | ImGuiInputTextFlags.CallbackHistory, this.CommandInputCallback)) + if (ImGui.InputText( + "##command_box", + ref this.commandText, + 255, + ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.CallbackCompletion | + ImGuiInputTextFlags.CallbackHistory, + this.CommandInputCallback)) { this.ProcessCommand(); getFocus = true; @@ -316,14 +326,62 @@ internal class ConsoleWindow : Window, IDisposable { this.ProcessCommand(); } - - this.copyLog = false; } - + + private static string GetTextForLogEventLevel(LogEventLevel level) => level switch + { + LogEventLevel.Error => "ERR", + LogEventLevel.Verbose => "VRB", + LogEventLevel.Debug => "DBG", + LogEventLevel.Information => "INF", + LogEventLevel.Warning => "WRN", + LogEventLevel.Fatal => "FTL", + _ => "???", + }; + + private static uint GetColorForLogEventLevel(LogEventLevel level) => level switch + { + LogEventLevel.Error => 0x800000EE, + LogEventLevel.Verbose => 0x00000000, + LogEventLevel.Debug => 0x00000000, + LogEventLevel.Information => 0x00000000, + LogEventLevel.Warning => 0x8A0070EE, + LogEventLevel.Fatal => 0xFF00000A, + _ => 0x30FFFFFF, + }; + + private void FrameworkOnUpdate(IFramework framework) + { + if (this.pendingClearLog) + { + this.pendingClearLog = false; + this.logText.Clear(); + this.filteredLogEntries.Clear(); + this.newLogEntries.Clear(); + } + + if (this.pendingRefilter) + { + this.pendingRefilter = false; + this.filteredLogEntries.Clear(); + foreach (var log in this.logText) + { + if (this.IsFilterApplicable(log)) + this.filteredLogEntries.Add(log); + } + } + + var numPrevFilteredLogEntries = this.filteredLogEntries.Count; + var addedLines = 0; + while (this.newLogEntries.TryDequeue(out var logLine)) + addedLines += this.HandleLogLine(logLine.Line, logLine.LogEvent); + this.newRolledLines = addedLines - (this.filteredLogEntries.Count - numPrevFilteredLogEntries); + } + private void HandleCopyMode(int i, LogEntry line) { var selectionChanged = false; - + // If copyStart is -1, it means a drag has not been started yet, let's start one, and select the starting spot. if (this.copyMode && this.copyStart == -1 && ImGui.IsItemClicked()) { @@ -334,19 +392,20 @@ internal class ConsoleWindow : Window, IDisposable } // Update the selected range when dragging over entries - if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && + ImGui.IsMouseDragging(ImGuiMouseButton.Left)) { if (!line.SelectedForCopy) { - foreach (var index in Enumerable.Range(0, this.FilteredLogEntries.Count)) + foreach (var index in Enumerable.Range(0, this.filteredLogEntries.Count)) { if (this.copyStart < i) { - this.FilteredLogEntries[index].SelectedForCopy = index >= this.copyStart && index <= i; + this.filteredLogEntries[index].SelectedForCopy = index >= this.copyStart && index <= i; } else { - this.FilteredLogEntries[index].SelectedForCopy = index >= i && index <= this.copyStart; + this.filteredLogEntries[index].SelectedForCopy = index >= i && index <= this.copyStart; } } @@ -355,19 +414,37 @@ internal class ConsoleWindow : Window, IDisposable } // Finish the drag, we should have already marked all dragged entries as selected by now. - if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + if (this.copyMode && this.copyStart != -1 && ImGui.IsItemHovered() && + ImGui.IsMouseReleased(ImGuiMouseButton.Left)) { this.copyStart = -1; } if (selectionChanged) - { - var allSelectedLines = this.FilteredLogEntries - .Where(entry => entry.SelectedForCopy) - .Select(entry => $"{entry.TimeStamp:HH:mm:ss.fff} {this.GetTextForLogEventLevel(entry.Level)} | {entry.Line}"); + this.CopyFilteredLogEntries(true); + } - ImGui.SetClipboardText(string.Join("\n", allSelectedLines)); + private void CopyFilteredLogEntries(bool selectedOnly) + { + var sb = new StringBuilder(); + var n = 0; + foreach (var entry in this.filteredLogEntries) + { + if (selectedOnly && !entry.SelectedForCopy) + continue; + + n++; + sb.AppendLine(entry.ToString()); } + + if (n == 0) + return; + + ImGui.SetClipboardText(sb.ToString()); + Service.Get().AddNotification( + $"{n:n0} line(s) copied.", + this.WindowName, + NotificationType.Success); } private void DrawOptionsToolbar() @@ -384,7 +461,7 @@ internal class ConsoleWindow : Window, IDisposable EntryPoint.LogLevelSwitch.MinimumLevel = value; configuration.LogLevel = value; configuration.QueueSave(); - this.Refilter(); + this.QueueRefilter(); } } @@ -407,18 +484,27 @@ internal class ConsoleWindow : Window, IDisposable this.settingsPopupWasOpen = settingsPopup; - if (this.DrawToggleButtonWithTooltip("show_settings", "Show settings", FontAwesomeIcon.List, ref settingsPopup)) ImGui.OpenPopup("##console_settings"); + if (this.DrawToggleButtonWithTooltip("show_settings", "Show settings", FontAwesomeIcon.List, ref settingsPopup)) + ImGui.OpenPopup("##console_settings"); ImGui.SameLine(); - if (this.DrawToggleButtonWithTooltip("show_filters", "Show filter toolbar", FontAwesomeIcon.Search, ref this.showFilterToolbar)) + if (this.DrawToggleButtonWithTooltip( + "show_filters", + "Show filter toolbar", + FontAwesomeIcon.Search, + ref this.showFilterToolbar)) { this.showFilterToolbar = !this.showFilterToolbar; } ImGui.SameLine(); - if (this.DrawToggleButtonWithTooltip("show_uncaught_exceptions", "Show uncaught exception while filtering", FontAwesomeIcon.Bug, ref this.filterShowUncaughtExceptions)) + if (this.DrawToggleButtonWithTooltip( + "show_uncaught_exceptions", + "Show uncaught exception while filtering", + FontAwesomeIcon.Bug, + ref this.filterShowUncaughtExceptions)) { this.filterShowUncaughtExceptions = !this.filterShowUncaughtExceptions; } @@ -427,28 +513,33 @@ internal class ConsoleWindow : Window, IDisposable if (ImGuiComponents.IconButton("clear_log", FontAwesomeIcon.Trash)) { - this.clearLog = true; + this.QueueClear(); } if (ImGui.IsItemHovered()) ImGui.SetTooltip("Clear Log"); ImGui.SameLine(); - if (this.DrawToggleButtonWithTooltip("copy_mode", "Enable Copy Mode\nRight-click to copy entire log", FontAwesomeIcon.Copy, ref this.copyMode)) + if (this.DrawToggleButtonWithTooltip( + "copy_mode", + "Enable Copy Mode\nRight-click to copy entire log", + FontAwesomeIcon.Copy, + ref this.copyMode)) { this.copyMode = !this.copyMode; if (!this.copyMode) { - foreach (var entry in this.FilteredLogEntries) + foreach (var entry in this.filteredLogEntries) { entry.SelectedForCopy = false; } } } - if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) this.copyLog = true; - + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) + this.CopyFilteredLogEntries(false); + ImGui.SameLine(); if (this.killGameArmed) { @@ -464,16 +555,59 @@ internal class ConsoleWindow : Window, IDisposable if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game"); ImGui.SameLine(); - ImGui.SetCursorPosX(ImGui.GetContentRegionMax().X - (200.0f * ImGuiHelpers.GlobalScale)); + ImGui.SetCursorPosX( + ImGui.GetContentRegionMax().X - (2 * 200.0f * ImGuiHelpers.GlobalScale) - ImGui.GetStyle().ItemSpacing.X); + ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale); - if (ImGui.InputTextWithHint("##global_filter", "regex global filter", ref this.textFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)) + if (ImGui.InputTextWithHint( + "##textHighlight", + "regex highlight", + ref this.textHighlight, + 2048, + ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll) + || ImGui.IsItemDeactivatedAfterEdit()) { - this.Refilter(); + this.compiledLogHighlight = null; + this.exceptionLogHighlight = null; + try + { + if (this.textHighlight != string.Empty) + this.compiledLogHighlight = new(this.textHighlight, RegexOptions.IgnoreCase); + } + catch (Exception e) + { + this.exceptionLogHighlight = e; + } + + foreach (var log in this.logText) + log.HighlightMatches = null; } - if (ImGui.IsItemDeactivatedAfterEdit()) + ImGui.SameLine(); + ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale); + if (ImGui.InputTextWithHint( + "##textFilter", + "regex global filter", + ref this.textFilter, + 2048, + ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll) + || ImGui.IsItemDeactivatedAfterEdit()) { - this.Refilter(); + this.compiledLogFilter = null; + this.exceptionLogFilter = null; + try + { + this.compiledLogFilter = new(this.textFilter, RegexOptions.IgnoreCase); + + this.QueueRefilter(); + } + catch (Exception e) + { + this.exceptionLogFilter = e; + } + + foreach (var log in this.logText) + log.HighlightMatches = null; } } @@ -509,9 +643,12 @@ internal class ConsoleWindow : Window, IDisposable if (!this.showFilterToolbar) return; PluginFilterEntry? removalEntry = null; - using var table = ImRaii.Table("plugin_filter_entries", 4, ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV); + using var table = ImRaii.Table( + "plugin_filter_entries", + 4, + ImGuiTableFlags.Resizable | ImGuiTableFlags.BordersInnerV); if (!table) return; - + ImGui.TableSetupColumn("##remove_button", ImGuiTableColumnFlags.WidthFixed, 25.0f * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("##source_name", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); ImGui.TableSetupColumn("##log_level", ImGuiTableColumnFlags.WidthFixed, 150.0f * ImGuiHelpers.GlobalScale); @@ -522,15 +659,16 @@ internal class ConsoleWindow : Window, IDisposable { if (this.pluginFilters.All(entry => entry.Source != this.selectedSource)) { - this.pluginFilters.Add(new PluginFilterEntry - { - Source = this.selectedSource, - Filter = string.Empty, - Level = LogEventLevel.Debug, - }); + this.pluginFilters.Add( + new PluginFilterEntry + { + Source = this.selectedSource, + Filter = string.Empty, + Level = LogEventLevel.Debug, + }); } - this.Refilter(); + this.QueueRefilter(); } ImGui.TableNextColumn(); @@ -541,13 +679,17 @@ internal class ConsoleWindow : Window, IDisposable .Select(p => p.Manifest.InternalName) .OrderBy(s => s) .Prepend("DalamudInternal") - .Where(name => this.pluginFilter is "" || new FuzzyMatcher(this.pluginFilter.ToLowerInvariant(), MatchMode.Fuzzy).Matches(name.ToLowerInvariant()) != 0) + .Where( + name => this.pluginFilter is "" || new FuzzyMatcher( + this.pluginFilter.ToLowerInvariant(), + MatchMode.Fuzzy).Matches(name.ToLowerInvariant()) != + 0) .ToList(); ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); ImGui.InputTextWithHint("##PluginSearchFilter", "Filter Plugin List", ref this.pluginFilter, 2048); ImGui.Separator(); - + if (!sourceNames.Any()) { ImGui.TextColored(ImGuiColors.DalamudRed, "No Results"); @@ -569,25 +711,27 @@ internal class ConsoleWindow : Window, IDisposable foreach (var entry in this.pluginFilters) { + ImGui.PushID(entry.Source); + ImGui.TableNextColumn(); - if (ImGuiComponents.IconButton($"remove{entry.Source}", FontAwesomeIcon.Trash)) + if (ImGuiComponents.IconButton(FontAwesomeIcon.Trash)) { removalEntry = entry; } ImGui.TableNextColumn(); ImGui.Text(entry.Source); - + ImGui.TableNextColumn(); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.BeginCombo($"##levels{entry.Source}", $"{entry.Level}+")) + if (ImGui.BeginCombo("##levels", $"{entry.Level}+")) { foreach (var value in Enum.GetValues()) { if (ImGui.Selectable(value.ToString(), value == entry.Level)) { entry.Level = value; - this.Refilter(); + this.QueueRefilter(); } } @@ -597,19 +741,26 @@ internal class ConsoleWindow : Window, IDisposable ImGui.TableNextColumn(); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); var entryFilter = entry.Filter; - if (ImGui.InputTextWithHint($"##filter{entry.Source}", $"{entry.Source} regex filter", ref entryFilter, 2048, ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll)) + if (ImGui.InputTextWithHint( + "##filter", + $"{entry.Source} regex filter", + ref entryFilter, + 2048, + ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll) + || ImGui.IsItemDeactivatedAfterEdit()) { entry.Filter = entryFilter; - this.Refilter(); + if (entry.FilterException is null) + this.QueueRefilter(); } - if (ImGui.IsItemDeactivatedAfterEdit()) this.Refilter(); + ImGui.PopID(); } if (removalEntry is { } toRemove) { this.pluginFilters.Remove(toRemove); - this.Refilter(); + this.QueueRefilter(); } } @@ -636,7 +787,7 @@ internal class ConsoleWindow : Window, IDisposable if (this.commandText is "clear" or "cls") { - this.Clear(); + this.QueueClear(); return; } @@ -717,16 +868,22 @@ internal class ConsoleWindow : Window, IDisposable return 0; } - private void AddAndFilter(string line, LogEvent logEvent, bool isMultiline) + /// Add a log entry to the display. + /// The line to add. + /// The Serilog event associated with this line. + /// Number of lines added to . + private int HandleLogLine(string line, LogEvent logEvent) { - if (line.StartsWith("TROUBLESHOOTING:") || line.StartsWith("LASTEXCEPTION:")) - return; + ThreadSafety.DebugAssertMainThread(); + // These lines are too huge, and only useful for troubleshooting after the game exist. + if (line.StartsWith("TROUBLESHOOTING:") || line.StartsWith("LASTEXCEPTION:")) + return 0; + + // Create a log entry template. var entry = new LogEntry { - IsMultiline = isMultiline, Level = logEvent.Level, - Line = line, TimeStamp = logEvent.Timestamp, HasException = logEvent.Exception != null, }; @@ -741,98 +898,118 @@ internal class ConsoleWindow : Window, IDisposable entry.Source = sourceValue; } + var ssp = line.AsSpan(); + var numLines = 0; + while (true) + { + var next = ssp.IndexOfAny('\r', '\n'); + if (next == -1) + { + // Last occurrence; transfer the ownership of the new entry to the queue. + entry.Line = ssp.ToString(); + numLines += this.AddAndFilter(entry); + break; + } + + // There will be more; create a clone of the entry with the current line. + numLines += this.AddAndFilter(entry with { Line = ssp[..next].ToString() }); + + // Mark further lines as multiline. + entry.IsMultiline = true; + + // Skip the detected line break. + ssp = ssp[next..]; + ssp = ssp.StartsWith("\r\n") ? ssp[2..] : ssp[1..]; + } + + return numLines; + } + + /// Adds a line to the log list and the filtered log list accordingly. + /// The new log entry to add. + /// Number of lines added to . + private int AddAndFilter(LogEntry entry) + { + ThreadSafety.DebugAssertMainThread(); + this.logText.Add(entry); - var avoidScroll = this.FilteredLogEntries.Count == this.FilteredLogEntries.Size; - if (this.IsFilterApplicable(entry)) - { - this.FilteredLogEntries.Add(entry); - if (avoidScroll) Interlocked.Increment(ref this.newRolledLines); - } + if (!this.IsFilterApplicable(entry)) + return 0; + + this.filteredLogEntries.Add(entry); + return 1; } + /// Determines if a log entry passes the user-specified filter. + /// The entry to test. + /// true if it passes the filter. private bool IsFilterApplicable(LogEntry entry) { - if (this.regexError) + ThreadSafety.DebugAssertMainThread(); + + if (this.exceptionLogFilter is not null) return false; - try + // If this entry is below a newly set minimum level, fail it + if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level) + return false; + + // Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught) + // After log levels because uncaught exceptions should *never* fall below Error. + if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null) + return true; + + // (global filter) && (plugin filter) must be satisfied. + var wholeCond = true; + + // If we have a global filter, check that first + if (this.compiledLogFilter is { } logFilter) { - // If this entry is below a newly set minimum level, fail it - if (EntryPoint.LogLevelSwitch.MinimumLevel > entry.Level) - return false; - - // Show exceptions that weren't properly tagged with a Source (generally meaning they were uncaught) - // After log levels because uncaught exceptions should *never* fall below Error. - if (this.filterShowUncaughtExceptions && entry.HasException && entry.Source == null) - return true; + // Someone will definitely try to just text filter a source without using the actual filters, should allow that. + var matchesSource = entry.Source is not null && logFilter.IsMatch(entry.Source); + var matchesContent = logFilter.IsMatch(entry.Line); - // If we have a global filter, check that first - if (!this.textFilter.IsNullOrEmpty()) + wholeCond &= matchesSource || matchesContent; + } + + // If this entry has a filter, check the filter + if (this.pluginFilters.Count > 0) + { + var matchesAny = false; + + foreach (var filterEntry in this.pluginFilters) { - // Someone will definitely try to just text filter a source without using the actual filters, should allow that. - var matchesSource = entry.Source is not null && Regex.IsMatch(entry.Source, this.textFilter, RegexOptions.IgnoreCase); - var matchesContent = Regex.IsMatch(entry.Line, this.textFilter, RegexOptions.IgnoreCase); + if (!string.Equals(filterEntry.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase)) + continue; - return matchesSource || matchesContent; - } - - // If this entry has a filter, check the filter - if (this.pluginFilters.FirstOrDefault(filter => string.Equals(filter.Source, entry.Source, StringComparison.InvariantCultureIgnoreCase)) is { } filterEntry) - { var allowedLevel = filterEntry.Level <= entry.Level; - var matchesContent = filterEntry.Filter.IsNullOrEmpty() || Regex.IsMatch(entry.Line, filterEntry.Filter, RegexOptions.IgnoreCase); + var matchesContent = filterEntry.FilterRegex?.IsMatch(entry.Line) is not false; - return allowedLevel && matchesContent; + matchesAny |= allowedLevel && matchesContent; + if (matchesAny) + break; } - } - catch (Exception) - { - this.regexError = true; - return false; + + wholeCond &= matchesAny; } - // else we couldn't find a filter for this entry, if we have any filters, we need to block this entry. - return !this.pluginFilters.Any(); + return wholeCond; } - private void Refilter() - { - lock (this.renderLock) - { - this.regexError = false; - this.FilteredLogEntries = new RollingList(this.logText.Where(this.IsFilterApplicable), Math.Max(LogLinesMinimum, this.logLinesLimit)); - } - } + /// Queues clearing the window of all log entries, before next call to . + private void QueueClear() => this.pendingClearLog = true; - private string GetTextForLogEventLevel(LogEventLevel level) => level switch - { - LogEventLevel.Error => "ERR", - LogEventLevel.Verbose => "VRB", - LogEventLevel.Debug => "DBG", - LogEventLevel.Information => "INF", - LogEventLevel.Warning => "WRN", - LogEventLevel.Fatal => "FTL", - _ => throw new ArgumentOutOfRangeException(level.ToString(), "Invalid LogEventLevel"), - }; + /// Queues filtering the log entries again, before next call to . + private void QueueRefilter() => this.pendingRefilter = true; - private uint GetColorForLogEventLevel(LogEventLevel level) => level switch - { - LogEventLevel.Error => 0x800000EE, - LogEventLevel.Verbose => 0x00000000, - LogEventLevel.Debug => 0x00000000, - LogEventLevel.Information => 0x00000000, - LogEventLevel.Warning => 0x8A0070EE, - LogEventLevel.Fatal => 0xFF00000A, - _ => throw new ArgumentOutOfRangeException(level.ToString(), "Invalid LogEventLevel"), - }; + /// Enqueues the new log line to the log-to-be-processed queue. + /// See for the handler for the queued log entries. + private void OnLogLine(object sender, (string Line, LogEvent LogEvent) logEvent) => + this.newLogEntries.Enqueue(logEvent); - private void OnLogLine(object sender, (string Line, LogEvent LogEvent) logEvent) - { - this.HandleLogLine(logEvent.Line, logEvent.LogEvent); - } - - private bool DrawToggleButtonWithTooltip(string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState) + private bool DrawToggleButtonWithTooltip( + string buttonId, string tooltip, FontAwesomeIcon icon, ref bool enabledState) { var result = false; @@ -855,36 +1032,120 @@ internal class ConsoleWindow : Window, IDisposable this.logLinesLimit = dalamudConfiguration.LogLinesLimit; var limit = Math.Max(LogLinesMinimum, this.logLinesLimit); this.logText.Size = limit; - this.FilteredLogEntries.Size = limit; + this.filteredLogEntries.Size = limit; } - private class LogEntry + private unsafe void DrawHighlighted( + ReadOnlySpan line, + MatchCollection matches, + uint col, + uint highlightCol) { - public string Line { get; init; } = string.Empty; + Span charOffsets = stackalloc int[(matches.Count * 2) + 2]; + var charOffsetsIndex = 1; + for (var j = 0; j < matches.Count; j++) + { + var g = matches[j].Groups[0]; + charOffsets[charOffsetsIndex++] = g.Index; + charOffsets[charOffsetsIndex++] = g.Index + g.Length; + } + + charOffsets[charOffsetsIndex++] = line.Length; + + var screenPos = ImGui.GetCursorScreenPos(); + var drawList = ImGui.GetWindowDrawList().NativePtr; + var font = ImGui.GetFont(); + var size = ImGui.GetFontSize(); + var scale = size / font.FontSize; + var hotData = font.IndexedHotDataWrapped(); + var lookup = font.IndexLookupWrapped(); + var kern = (ImGui.GetIO().ConfigFlags & ImGuiConfigFlags.NoKerning) == 0; + var lastc = '\0'; + for (var i = 0; i < charOffsetsIndex - 1; i++) + { + var begin = charOffsets[i]; + var end = charOffsets[i + 1]; + if (begin == end) + continue; + + for (var j = begin; j < end; j++) + { + var currc = line[j]; + if (currc >= lookup.Length || lookup[currc] == ushort.MaxValue) + currc = (char)font.FallbackChar; + + if (kern) + screenPos.X += scale * ImGui.GetFont().GetDistanceAdjustmentForPair(lastc, currc); + font.RenderChar(drawList, size, screenPos, i % 2 == 1 ? highlightCol : col, currc); + + screenPos.X += scale * hotData[currc].AdvanceX; + lastc = currc; + } + } + } + + private record LogEntry + { + public string Line { get; set; } = string.Empty; public LogEventLevel Level { get; init; } public DateTimeOffset TimeStamp { get; init; } - public bool IsMultiline { get; init; } + public bool IsMultiline { get; set; } /// /// Gets or sets the system responsible for generating this log entry. Generally will be a plugin's /// InternalName. /// public string? Source { get; set; } - + public bool SelectedForCopy { get; set; } public bool HasException { get; init; } + + public MatchCollection? HighlightMatches { get; set; } + + public string TimestampString => this.TimeStamp.ToString("HH:mm:ss.fff"); + + public override string ToString() => + this.IsMultiline + ? $"\t{this.Line}" + : $"{this.TimestampString} | {GetTextForLogEventLevel(this.Level)} | {this.Line}"; } private class PluginFilterEntry { + private string filter = string.Empty; + public string Source { get; init; } = string.Empty; - public string Filter { get; set; } = string.Empty; - + public string Filter + { + get => this.filter; + set + { + this.filter = value; + this.FilterRegex = null; + this.FilterException = null; + if (value == string.Empty) + return; + + try + { + this.FilterRegex = new(value, RegexOptions.IgnoreCase); + } + catch (Exception e) + { + this.FilterException = e; + } + } + } + public LogEventLevel Level { get; set; } + + public Regex? FilterRegex { get; private set; } + + public Exception? FilterException { get; private set; } } } diff --git a/Dalamud/Utility/ThreadSafety.cs b/Dalamud/Utility/ThreadSafety.cs index 7c4b0dfcb..ce3ddc602 100644 --- a/Dalamud/Utility/ThreadSafety.cs +++ b/Dalamud/Utility/ThreadSafety.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; namespace Dalamud.Utility; @@ -19,6 +20,7 @@ public static class ThreadSafety /// Throws an exception when the current thread is not the main thread. /// /// Thrown when the current thread is not the main thread. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AssertMainThread() { if (!threadStaticIsMainThread) @@ -31,6 +33,7 @@ public static class ThreadSafety /// Throws an exception when the current thread is the main thread. ///
    /// Thrown when the current thread is the main thread. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AssertNotMainThread() { if (threadStaticIsMainThread) @@ -39,6 +42,15 @@ public static class ThreadSafety } } + /// , but only on debug compilation mode. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void DebugAssertMainThread() + { +#if DEBUG + AssertMainThread(); +#endif + } + /// /// Marks a thread as the main thread. /// From 666feede4c444ede746399bd2e7c085a95ae2e56 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 14 Mar 2024 07:36:43 +0900 Subject: [PATCH 571/585] Suppress DAssetM dispose exceptions (#1707) Whether an asset being unavailable should be an error is decided on Dalamud startup time. This suppresses assets unavailable exceptions on Dispose. --- Dalamud/Storage/Assets/DalamudAssetManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 69c7c32e8..68be78352 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -75,7 +75,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Where(x => x is not DalamudAsset.Empty4X4) .Where(x => x.GetAttribute()?.Required is false) .Select(this.CreateStreamAsync) - .Select(x => x.ToContentDisposedTask())) + .Select(x => x.ToContentDisposedTask(true))) .ContinueWith(r => Log.Verbose($"Optional assets load state: {r}")); } @@ -99,6 +99,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Concat(this.fileStreams.Values) .Concat(this.textureWraps.Values) .Where(x => x is not null) + .Select(x => x.ContinueWith(r => { _ = r.Exception; })) .ToArray()); this.scopedFinalizer.Dispose(); } From a26bb58fdbb79032b26b85488a04d49d0e40b8d0 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 14 Mar 2024 08:36:38 +0900 Subject: [PATCH 572/585] Use custom TaskScheduler for Framework.RunOnTick (#1597) * Use custom TaskScheduler for Framework.RunOnTick * TaskSchedulerWidget: add example --- Dalamud/Game/Framework.cs | 262 +++++++----------- .../Data/Widgets/TaskSchedulerWidget.cs | 157 ++++++++++- Dalamud/Plugin/Services/IFramework.cs | 15 + Dalamud/Utility/ThreadBoundTaskScheduler.cs | 90 ++++++ 4 files changed, 353 insertions(+), 171 deletions(-) create mode 100644 Dalamud/Utility/ThreadBoundTaskScheduler.cs diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index ce34f2c06..6520ca5c8 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -41,11 +42,13 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); - private readonly object runOnNextTickTaskListSync = new(); - private List runOnNextTickTaskList = new(); - private List runOnNextTickTaskList2 = new(); + private readonly CancellationTokenSource frameworkDestroy; + private readonly ThreadBoundTaskScheduler frameworkThreadTaskScheduler; - private Thread? frameworkUpdateThread; + private readonly ConcurrentDictionary + tickDelayedTaskCompletionSources = new(); + + private ulong tickCounter; [ServiceManager.ServiceConstructor] private Framework(TargetSigScanner sigScanner, GameLifecycle lifecycle) @@ -56,6 +59,14 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework this.addressResolver = new FrameworkAddressResolver(); this.addressResolver.Setup(sigScanner); + this.frameworkDestroy = new(); + this.frameworkThreadTaskScheduler = new(); + this.FrameworkThreadTaskFactory = new( + this.frameworkDestroy.Token, + TaskCreationOptions.None, + TaskContinuationOptions.None, + this.frameworkThreadTaskScheduler); + this.updateHook = Hook.FromAddress(this.addressResolver.TickAddress, this.HandleFrameworkUpdate); this.destroyHook = Hook.FromAddress(this.addressResolver.DestroyAddress, this.HandleFrameworkDestroy); @@ -92,14 +103,17 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework /// public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue; + /// + public TaskFactory FrameworkThreadTaskFactory { get; } + /// public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; /// - public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread; + public bool IsInFrameworkUpdateThread => this.frameworkThreadTaskScheduler.IsOnBoundThread; /// - public bool IsFrameworkUnloading { get; internal set; } + public bool IsFrameworkUnloading => this.frameworkDestroy.IsCancellationRequested; /// /// Gets the list of update sub-delegates that didn't get updated this frame. @@ -111,6 +125,19 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework /// internal bool DispatchUpdateEvents { get; set; } = true; + /// + public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) + { + if (this.frameworkDestroy.IsCancellationRequested) + return Task.FromCanceled(this.frameworkDestroy.Token); + if (numTicks <= 0) + return Task.CompletedTask; + + var tcs = new TaskCompletionSource(); + this.tickDelayedTaskCompletionSources[tcs] = (this.tickCounter + (ulong)numTicks, cancellationToken); + return tcs.Task; + } + /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func); @@ -157,20 +184,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework return Task.FromCanceled(cts.Token); } - var tcs = new TaskCompletionSource(); - lock (this.runOnNextTickTaskListSync) - { - this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc() + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.ContinueWhenAll( + new[] { - RemainingTicks = delayTicks, - RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), - CancellationToken = cancellationToken, - TaskCompletionSource = tcs, - Func = func, - }); - } - - return tcs.Task; + Task.Delay(delay, cancellationToken), + this.DelayTicks(delayTicks, cancellationToken), + }, + _ => func(), + cancellationToken); } /// @@ -186,20 +209,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework return Task.FromCanceled(cts.Token); } - var tcs = new TaskCompletionSource(); - lock (this.runOnNextTickTaskListSync) - { - this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction() + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.ContinueWhenAll( + new[] { - RemainingTicks = delayTicks, - RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), - CancellationToken = cancellationToken, - TaskCompletionSource = tcs, - Action = action, - }); - } - - return tcs.Task; + Task.Delay(delay, cancellationToken), + this.DelayTicks(delayTicks, cancellationToken), + }, + _ => action(), + cancellationToken); } /// @@ -215,20 +234,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework return Task.FromCanceled(cts.Token); } - var tcs = new TaskCompletionSource>(); - lock (this.runOnNextTickTaskListSync) - { - this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc>() + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.ContinueWhenAll( + new[] { - RemainingTicks = delayTicks, - RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), - CancellationToken = cancellationToken, - TaskCompletionSource = tcs, - Func = func, - }); - } - - return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap(); + Task.Delay(delay, cancellationToken), + this.DelayTicks(delayTicks, cancellationToken), + }, + _ => func(), + cancellationToken).Unwrap(); } /// @@ -244,20 +259,16 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework return Task.FromCanceled(cts.Token); } - var tcs = new TaskCompletionSource(); - lock (this.runOnNextTickTaskListSync) - { - this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc() + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.ContinueWhenAll( + new[] { - RemainingTicks = delayTicks, - RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), - CancellationToken = cancellationToken, - TaskCompletionSource = tcs, - Func = func, - }); - } - - return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap(); + Task.Delay(delay, cancellationToken), + this.DelayTicks(delayTicks, cancellationToken), + }, + _ => func(), + cancellationToken).Unwrap(); } /// @@ -333,23 +344,9 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework } } - private void RunPendingTickTasks() - { - if (this.runOnNextTickTaskList.Count == 0 && this.runOnNextTickTaskList2.Count == 0) - return; - - for (var i = 0; i < 2; i++) - { - lock (this.runOnNextTickTaskListSync) - (this.runOnNextTickTaskList, this.runOnNextTickTaskList2) = (this.runOnNextTickTaskList2, this.runOnNextTickTaskList); - - this.runOnNextTickTaskList2.RemoveAll(x => x.Run()); - } - } - private bool HandleFrameworkUpdate(IntPtr framework) { - this.frameworkUpdateThread ??= Thread.CurrentThread; + this.frameworkThreadTaskScheduler.BoundThread ??= Thread.CurrentThread; ThreadSafety.MarkMainThread(); @@ -381,18 +378,30 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework this.LastUpdate = DateTime.Now; this.LastUpdateUTC = DateTime.UtcNow; + this.tickCounter++; + foreach (var (k, (expiry, ct)) in this.tickDelayedTaskCompletionSources) + { + if (ct.IsCancellationRequested) + k.SetCanceled(ct); + else if (expiry <= this.tickCounter) + k.SetResult(); + else + continue; + + this.tickDelayedTaskCompletionSources.Remove(k, out _); + } if (StatsEnabled) { StatsStopwatch.Restart(); - this.RunPendingTickTasks(); + this.frameworkThreadTaskScheduler.Run(); StatsStopwatch.Stop(); - AddToStats(nameof(this.RunPendingTickTasks), StatsStopwatch.Elapsed.TotalMilliseconds); + AddToStats(nameof(this.frameworkThreadTaskScheduler), StatsStopwatch.Elapsed.TotalMilliseconds); } else { - this.RunPendingTickTasks(); + this.frameworkThreadTaskScheduler.Run(); } if (StatsEnabled && this.Update != null) @@ -404,7 +413,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework // Cleanup handlers that are no longer being called foreach (var key in this.NonUpdatedSubDelegates) { - if (key == nameof(this.RunPendingTickTasks)) + if (key == nameof(this.FrameworkThreadTaskFactory)) continue; if (StatsHistory[key].Count > 0) @@ -431,8 +440,11 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework private bool HandleFrameworkDestroy(IntPtr framework) { - this.IsFrameworkUnloading = true; + this.frameworkDestroy.Cancel(); this.DispatchUpdateEvents = false; + foreach (var k in this.tickDelayedTaskCompletionSources.Keys) + k.SetCanceled(this.frameworkDestroy.Token); + this.tickDelayedTaskCompletionSources.Clear(); // All the same, for now... this.lifecycle.SetShuttingDown(); @@ -440,95 +452,12 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework Log.Information("Framework::Destroy!"); Service.Get().Unload(); - this.RunPendingTickTasks(); + this.frameworkThreadTaskScheduler.Run(); ServiceManager.WaitForServiceUnload(); Log.Information("Framework::Destroy OK!"); return this.destroyHook.OriginalDisposeSafe(framework); } - - private abstract class RunOnNextTickTaskBase - { - internal int RemainingTicks { get; set; } - - internal long RunAfterTickCount { get; init; } - - internal CancellationToken CancellationToken { get; init; } - - internal bool Run() - { - if (this.CancellationToken.IsCancellationRequested) - { - this.CancelImpl(); - return true; - } - - if (this.RemainingTicks > 0) - this.RemainingTicks -= 1; - if (this.RemainingTicks > 0) - return false; - - if (this.RunAfterTickCount > Environment.TickCount64) - return false; - - this.RunImpl(); - - return true; - } - - protected abstract void RunImpl(); - - protected abstract void CancelImpl(); - } - - private class RunOnNextTickTaskFunc : RunOnNextTickTaskBase - { - internal TaskCompletionSource TaskCompletionSource { get; init; } - - internal Func Func { get; init; } - - protected override void RunImpl() - { - try - { - this.TaskCompletionSource.SetResult(this.Func()); - } - catch (Exception ex) - { - this.TaskCompletionSource.SetException(ex); - } - } - - protected override void CancelImpl() - { - this.TaskCompletionSource.SetCanceled(); - } - } - - private class RunOnNextTickTaskAction : RunOnNextTickTaskBase - { - internal TaskCompletionSource TaskCompletionSource { get; init; } - - internal Action Action { get; init; } - - protected override void RunImpl() - { - try - { - this.Action(); - this.TaskCompletionSource.SetResult(); - } - catch (Exception ex) - { - this.TaskCompletionSource.SetException(ex); - } - } - - protected override void CancelImpl() - { - this.TaskCompletionSource.SetCanceled(); - } - } } /// @@ -561,7 +490,10 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework /// public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC; - + + /// + public TaskFactory FrameworkThreadTaskFactory => this.frameworkService.FrameworkThreadTaskFactory; + /// public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta; @@ -579,6 +511,10 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework this.Update = null; } + /// + public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) => + this.frameworkService.DelayTicks(numTicks, cancellationToken); + /// public Task RunOnFrameworkThread(Func func) => this.frameworkService.RunOnFrameworkThread(func); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index d1ac51ad5..c6d8c4e8b 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -1,13 +1,22 @@ // ReSharper disable MethodSupportsCancellation // Using alternative method of cancelling tasks by throwing exceptions. +using System.IO; +using System.Linq; +using System.Net.Http; using System.Reflection; +using System.Text; using System.Threading; using System.Threading.Tasks; using Dalamud.Game; using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; using Dalamud.Logging.Internal; +using Dalamud.Utility; + using ImGuiNET; using Serilog; @@ -18,6 +27,12 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class TaskSchedulerWidget : IDataWindowWidget { + private readonly FileDialogManager fileDialogManager = new(); + private readonly byte[] urlBytes = new byte[2048]; + private readonly byte[] localPathBytes = new byte[2048]; + + private Task? downloadTask = null; + private (long Downloaded, long Total, float Percentage) downloadState; private CancellationTokenSource taskSchedulerCancelSource = new(); /// @@ -33,11 +48,16 @@ internal class TaskSchedulerWidget : IDataWindowWidget public void Load() { this.Ready = true; + Encoding.UTF8.GetBytes( + "https://geo.mirror.pkgbuild.com/iso/2024.01.01/archlinux-2024.01.01-x86_64.iso", + this.urlBytes); } /// public void Draw() { + var framework = Service.Get(); + if (ImGui.Button("Clear list")) { TaskTracker.Clear(); @@ -84,8 +104,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget { Thread.Sleep(200); - string a = null; - a.Contains("dalamud"); // Intentional null exception. + _ = ((string)null)!.Contains("dalamud"); // Intentional null exception. }); } @@ -94,36 +113,156 @@ internal class TaskSchedulerWidget : IDataWindowWidget if (ImGui.Button("ASAP")) { - Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token)); + _ = framework.RunOnTick(() => Log.Information("Framework.Update - ASAP"), cancellationToken: this.taskSchedulerCancelSource.Token); } ImGui.SameLine(); if (ImGui.Button("In 1s")) { - Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 1s"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)); } ImGui.SameLine(); if (ImGui.Button("In 60f")) { - Task.Run(async () => await Service.Get().RunOnTick(() => { }, cancellationToken: this.taskSchedulerCancelSource.Token, delayTicks: 60)); + _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delayTicks: 60); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 1s+120f")) + { + _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 1s+120f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1), delayTicks: 120); + } + + ImGui.SameLine(); + + if (ImGui.Button("In 2s+60f")) + { + _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 2s+60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(2), delayTicks: 60); + } + + ImGui.SameLine(); + + if (ImGui.Button("Every 60 frames")) + { + _ = framework.RunOnTick( + async () => + { + for (var i = 0L; ; i++) + { + Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + await framework.DelayTicks(60, this.taskSchedulerCancelSource.Token); + } + }, + cancellationToken: this.taskSchedulerCancelSource.Token); } ImGui.SameLine(); if (ImGui.Button("Error in 1s")) { - Task.Run(async () => await Service.Get().RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1))); + _ = framework.RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)); } ImGui.SameLine(); if (ImGui.Button("As long as it's in Framework Thread")) { - Task.Run(async () => await Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); - Service.Get().RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + Task.Run(async () => await framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); + framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + } + + if (ImGui.CollapsingHeader("Download")) + { + ImGui.InputText("URL", this.urlBytes, (uint)this.urlBytes.Length); + ImGui.InputText("Local Path", this.localPathBytes, (uint)this.localPathBytes.Length); + ImGui.SameLine(); + + if (ImGuiComponents.IconButton("##localpathpicker", FontAwesomeIcon.File)) + { + var defaultFileName = Encoding.UTF8.GetString(this.urlBytes).Split('\0', 2)[0].Split('/').Last(); + this.fileDialogManager.SaveFileDialog( + "Choose a local path", + "*", + defaultFileName, + string.Empty, + (accept, newPath) => + { + if (accept) + { + this.localPathBytes.AsSpan().Clear(); + Encoding.UTF8.GetBytes(newPath, this.localPathBytes.AsSpan()); + } + }); + } + + ImGui.TextUnformatted($"{this.downloadState.Downloaded:##,###}/{this.downloadState.Total:##,###} ({this.downloadState.Percentage:0.00}%)"); + + using var disabled = + ImRaii.Disabled(this.downloadTask?.IsCompleted is false || this.localPathBytes[0] == 0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted("Download"); + ImGui.SameLine(); + var downloadUsingGlobalScheduler = ImGui.Button("using default scheduler"); + ImGui.SameLine(); + var downloadUsingFramework = ImGui.Button("using Framework.Update"); + if (downloadUsingGlobalScheduler || downloadUsingFramework) + { + var url = Encoding.UTF8.GetString(this.urlBytes).Split('\0', 2)[0]; + var localPath = Encoding.UTF8.GetString(this.localPathBytes).Split('\0', 2)[0]; + var ct = this.taskSchedulerCancelSource.Token; + this.downloadState = default; + var factory = downloadUsingGlobalScheduler + ? Task.Factory + : framework.FrameworkThreadTaskFactory; + this.downloadState = default; + this.downloadTask = factory.StartNew( + async () => + { + try + { + await using var to = File.Create(localPath); + using var client = new HttpClient(); + using var conn = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct); + this.downloadState.Total = conn.Content.Headers.ContentLength ?? -1L; + await using var from = conn.Content.ReadAsStream(ct); + var buffer = new byte[8192]; + while (true) + { + if (downloadUsingFramework) + ThreadSafety.AssertMainThread(); + if (downloadUsingGlobalScheduler) + ThreadSafety.AssertNotMainThread(); + var len = await from.ReadAsync(buffer, ct); + if (len == 0) + break; + await to.WriteAsync(buffer.AsMemory(0, len), ct); + this.downloadState.Downloaded += len; + if (this.downloadState.Total >= 0) + { + this.downloadState.Percentage = + (100f * this.downloadState.Downloaded) / this.downloadState.Total; + } + } + } + catch (Exception e) + { + Log.Error(e, "Failed to download {from} to {to}.", url, localPath); + try + { + File.Delete(localPath); + } + catch + { + // ignore + } + } + }, + cancellationToken: ct).Unwrap(); + } } if (ImGui.Button("Drown in tasks")) @@ -244,6 +383,8 @@ internal class TaskSchedulerWidget : IDataWindowWidget ImGui.PopStyleColor(1); } + + this.fileDialogManager.Draw(); } private async Task TestTaskInTaskDelay(CancellationToken token) diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs index ca33c5867..a93abd252 100644 --- a/Dalamud/Plugin/Services/IFramework.cs +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -29,6 +29,11 @@ public interface IFramework /// public DateTime LastUpdateUTC { get; } + /// + /// Gets a that runs tasks during Framework Update event. + /// + public TaskFactory FrameworkThreadTaskFactory { get; } + /// /// Gets the delta between the last Framework Update and the currently executing one. /// @@ -44,6 +49,14 @@ public interface IFramework /// public bool IsFrameworkUnloading { get; } + /// + /// Returns a task that completes after the given number of ticks. + /// + /// Number of ticks to delay. + /// The cancellation token. + /// A new that gets resolved after specified number of ticks happen. + public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default); + /// /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. /// @@ -65,6 +78,7 @@ public interface IFramework /// Return type. /// Function to call. /// Task representing the pending or already completed function. + [Obsolete($"Use {nameof(RunOnTick)} instead.")] public Task RunOnFrameworkThread(Func> func); /// @@ -72,6 +86,7 @@ public interface IFramework /// /// Function to call. /// Task representing the pending or already completed function. + [Obsolete($"Use {nameof(RunOnTick)} instead.")] public Task RunOnFrameworkThread(Func func); /// diff --git a/Dalamud/Utility/ThreadBoundTaskScheduler.cs b/Dalamud/Utility/ThreadBoundTaskScheduler.cs new file mode 100644 index 000000000..4b6de29ff --- /dev/null +++ b/Dalamud/Utility/ThreadBoundTaskScheduler.cs @@ -0,0 +1,90 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Dalamud.Utility; + +/// +/// A task scheduler that runs tasks on a specific thread. +/// +internal class ThreadBoundTaskScheduler : TaskScheduler +{ + private const byte Scheduled = 0; + private const byte Running = 1; + + private readonly ConcurrentDictionary scheduledTasks = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The thread to bind this task scheduelr to. + public ThreadBoundTaskScheduler(Thread? boundThread = null) + { + this.BoundThread = boundThread; + } + + /// + /// Gets or sets the thread this task scheduler is bound to. + /// + public Thread? BoundThread { get; set; } + + /// + /// Gets a value indicating whether we're on the bound thread. + /// + public bool IsOnBoundThread => Thread.CurrentThread == this.BoundThread; + + /// + /// Runs queued tasks. + /// + public void Run() + { + foreach (var task in this.scheduledTasks.Keys) + { + if (!this.scheduledTasks.TryUpdate(task, Running, Scheduled)) + continue; + + _ = this.TryExecuteTask(task); + } + } + + /// + protected override IEnumerable GetScheduledTasks() + { + return this.scheduledTasks.Keys; + } + + /// + protected override void QueueTask(Task task) + { + this.scheduledTasks[task] = Scheduled; + } + + /// + protected override bool TryDequeue(Task task) + { + if (!this.scheduledTasks.TryRemove(task, out _)) + return false; + return true; + } + + /// + protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) + { + if (!this.IsOnBoundThread) + return false; + + if (taskWasPreviouslyQueued && !this.scheduledTasks.TryUpdate(task, Running, Scheduled)) + return false; + + _ = this.TryExecuteTask(task); + return true; + } + + private new bool TryExecuteTask(Task task) + { + var r = base.TryExecuteTask(task); + this.scheduledTasks.Remove(task, out _); + return r; + } +} From cf4a9e305597501d43722139dfba671ad0d06e77 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 14 Mar 2024 08:57:30 +0900 Subject: [PATCH 573/585] Easier SingleFontChooserDialog ctor, window pos/size/flags, and more docs (#1704) * Make SingleFontChooserDialog ctor less confusing The current constructor expects a new fresh instance of IFontAtlas, which can be easy to miss, resulting in wasted time troubleshooting without enough clues. New constructor is added that directly takes an instance of UiBuilder, and the old constructor has been obsoleted and should be changed to private on api 10. * Add position, size, and window flags conf to SFCD * Improve documentations * Add test for PopupPosition/Size --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../SingleFontChooserDialog.cs | 243 ++++++++++++++++-- .../Widgets/GamePrebakedFontsTestWidget.cs | 89 +++++-- .../Windows/Settings/Tabs/SettingsTabLook.cs | 5 +- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 20 +- .../Interface/ManagedFontAtlas/IFontHandle.cs | 21 +- .../ManagedFontAtlas/Internals/FontHandle.cs | 9 +- 6 files changed, 327 insertions(+), 60 deletions(-) diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs index ca75e5ce0..9420fe42c 100644 --- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -9,6 +9,7 @@ using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; @@ -84,11 +85,22 @@ public sealed class SingleFontChooserDialog : IDisposable private IFontHandle? fontHandle; private SingleFontSpec selectedFont; - /// - /// Initializes a new instance of the class. - /// + private bool popupPositionChanged; + private bool popupSizeChanged; + private Vector2 popupPosition = new(float.NaN); + private Vector2 popupSize = new(float.NaN); + + /// Initializes a new instance of the class. /// A new instance of created using /// as its auto-rebuild mode. + /// The passed instance of will be disposed after use. If you pass an atlas + /// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing + /// this font chooser. Consider using for automatic + /// handling of font atlas derived from a , or even for automatic + /// registration and unregistration of event handler in addition to automatic disposal of this + /// class and the temporary font atlas for this font chooser dialog. + [Obsolete("See remarks, and use the other constructor.", false)] + [Api10ToDo("Make private.")] public SingleFontChooserDialog(IFontAtlas newAsyncAtlas) { this.counter = Interlocked.Increment(ref counterStatic); @@ -99,6 +111,39 @@ public sealed class SingleFontChooserDialog : IDisposable Encoding.UTF8.GetBytes("Font preview.\n0123456789!", this.fontPreviewText); } +#pragma warning disable CS0618 // Type or member is obsolete + // TODO: Api10ToDo; Remove this pragma warning disable line + + /// Initializes a new instance of the class. + /// The relevant instance of UiBuilder. + /// Whether the fonts in the atlas is global scaled. + /// Atlas name for debugging purposes. + /// + /// The passed is only used for creating a temporary font atlas. It will not + /// automatically register a hander for . + /// Consider using for automatic registration and unregistration of + /// event handler in addition to automatic disposal of this class and the temporary font atlas + /// for this font chooser dialog. + /// + public SingleFontChooserDialog(UiBuilder uiBuilder, bool isGlobalScaled = true, string? debugAtlasName = null) + : this(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async, isGlobalScaled, debugAtlasName)) + { + } + + /// Initializes a new instance of the class. + /// An instance of . + /// The temporary atlas name. + internal SingleFontChooserDialog(FontAtlasFactory factory, string debugAtlasName) + : this(factory.CreateFontAtlas(debugAtlasName, FontAtlasAutoRebuildMode.Async)) + { + } + +#pragma warning restore CS0618 // Type or member is obsolete + // TODO: Api10ToDo; Remove this pragma warning restore line + + /// Called when the selected font spec has changed. + public event Action? SelectedFontSpecChanged; + /// /// Gets or sets the title of this font chooser dialog popup. /// @@ -153,6 +198,8 @@ public sealed class SingleFontChooserDialog : IDisposable this.useAdvancedOptions |= Math.Abs(value.LineHeight - 1f) > 0.000001; this.useAdvancedOptions |= value.GlyphOffset != default; this.useAdvancedOptions |= value.LetterSpacing != 0f; + + this.SelectedFontSpecChanged?.Invoke(this.selectedFont); } } @@ -166,15 +213,55 @@ public sealed class SingleFontChooserDialog : IDisposable /// public bool IgnorePreviewGlobalScale { get; set; } - /// - /// Creates a new instance of that will automatically draw and dispose itself as - /// needed. + /// Gets or sets a value indicating whether this popup should be modal, blocking everything behind from + /// being interacted. + /// If true, then will be + /// used. Otherwise, will be used. + public bool IsModal { get; set; } = true; + + /// Gets or sets the window flags. + public ImGuiWindowFlags WindowFlags { get; set; } + + /// Gets or sets the popup window position. + /// + /// Setting the position only works before the first call to . + /// If any of the coordinates are , default position will be used. + /// The position will be clamped into the work area of the selected monitor. + /// + public Vector2 PopupPosition + { + get => this.popupPosition; + set + { + this.popupPositionChanged = true; + this.popupPosition = value; + } + } + + /// Gets or sets the popup window size. + /// + /// Setting the size only works before the first call to . + /// If any of the coordinates are , default size will be used. + /// The size will be clamped into the work area of the selected monitor. + /// + public Vector2 PopupSize + { + get => this.popupSize; + set + { + this.popupSizeChanged = true; + this.popupSize = value; + } + } + + /// Creates a new instance of that will automatically draw and + /// dispose itself as needed; calling and are handled automatically. /// /// An instance of . /// The new instance of . public static SingleFontChooserDialog CreateAuto(UiBuilder uiBuilder) { - var fcd = new SingleFontChooserDialog(uiBuilder.CreateFontAtlas(FontAtlasAutoRebuildMode.Async)); + var fcd = new SingleFontChooserDialog(uiBuilder); uiBuilder.Draw += fcd.Draw; fcd.tcs.Task.ContinueWith( r => @@ -187,6 +274,14 @@ public sealed class SingleFontChooserDialog : IDisposable return fcd; } + /// Gets the default popup size before clamping to monitor work area. + /// The default popup size. + public static Vector2 GetDefaultPopupSizeNonClamped() + { + ThreadSafety.AssertMainThread(); + return new Vector2(40, 30) * ImGui.GetTextLineHeight(); + } + /// public void Dispose() { @@ -204,13 +299,28 @@ public sealed class SingleFontChooserDialog : IDisposable ImGui.GetIO().WantTextInput = false; } + /// Sets and to be at the center of the current window + /// being drawn. + /// The preferred popup size. + public void SetPopupPositionAndSizeToCurrentWindowCenter(Vector2 preferredPopupSize) + { + ThreadSafety.AssertMainThread(); + this.PopupSize = preferredPopupSize; + this.PopupPosition = ImGui.GetWindowPos() + ((ImGui.GetWindowSize() - preferredPopupSize) / 2); + } + + /// Sets and to be at the center of the current window + /// being drawn. + public void SetPopupPositionAndSizeToCurrentWindowCenter() => + this.SetPopupPositionAndSizeToCurrentWindowCenter(GetDefaultPopupSizeNonClamped()); + /// /// Draws this dialog. /// public void Draw() { - if (this.firstDraw) - ImGui.OpenPopup(this.popupImGuiName); + const float popupMinWidth = 320; + const float popupMinHeight = 240; ImGui.GetIO().WantCaptureKeyboard = true; ImGui.GetIO().WantTextInput = true; @@ -220,12 +330,70 @@ public sealed class SingleFontChooserDialog : IDisposable return; } - var open = true; - ImGui.SetNextWindowSize(new(640, 480), ImGuiCond.Appearing); - if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open) || !open) + if (this.firstDraw) { - this.Cancel(); - return; + if (this.IsModal) + ImGui.OpenPopup(this.popupImGuiName); + } + + if (this.firstDraw || this.popupPositionChanged || this.popupSizeChanged) + { + var preferProvidedSize = !float.IsNaN(this.popupSize.X) && !float.IsNaN(this.popupSize.Y); + var size = preferProvidedSize ? this.popupSize : GetDefaultPopupSizeNonClamped(); + size.X = Math.Max(size.X, popupMinWidth); + size.Y = Math.Max(size.Y, popupMinHeight); + + var preferProvidedPos = !float.IsNaN(this.popupPosition.X) && !float.IsNaN(this.popupPosition.Y); + var monitorLocatorPos = preferProvidedPos ? this.popupPosition + (size / 2) : ImGui.GetMousePos(); + + var monitors = ImGui.GetPlatformIO().Monitors; + var preferredMonitor = 0; + var preferredDistance = GetDistanceFromMonitor(monitorLocatorPos, monitors[0]); + for (var i = 1; i < monitors.Size; i++) + { + var distance = GetDistanceFromMonitor(monitorLocatorPos, monitors[i]); + if (distance < preferredDistance) + { + preferredMonitor = i; + preferredDistance = distance; + } + } + + var lt = monitors[preferredMonitor].WorkPos; + var workSize = monitors[preferredMonitor].WorkSize; + size.X = Math.Min(size.X, workSize.X); + size.Y = Math.Min(size.Y, workSize.Y); + var rb = (lt + workSize) - size; + + var pos = + preferProvidedPos + ? new(Math.Clamp(this.PopupPosition.X, lt.X, rb.X), Math.Clamp(this.PopupPosition.Y, lt.Y, rb.Y)) + : (lt + rb) / 2; + + ImGui.SetNextWindowSize(size, ImGuiCond.Always); + ImGui.SetNextWindowPos(pos, ImGuiCond.Always); + this.popupPositionChanged = this.popupSizeChanged = false; + } + + ImGui.SetNextWindowSizeConstraints(new(popupMinWidth, popupMinHeight), new(float.MaxValue)); + if (this.IsModal) + { + var open = true; + if (!ImGui.BeginPopupModal(this.popupImGuiName, ref open, this.WindowFlags) || !open) + { + this.Cancel(); + return; + } + } + else + { + var open = true; + if (!ImGui.Begin(this.popupImGuiName, ref open, this.WindowFlags) || !open) + { + ImGui.End(); + this.Cancel(); + return; + } } var framePad = ImGui.GetStyle().FramePadding; @@ -261,12 +429,36 @@ public sealed class SingleFontChooserDialog : IDisposable ImGui.EndChild(); - ImGui.EndPopup(); + this.popupPosition = ImGui.GetWindowPos(); + this.popupSize = ImGui.GetWindowSize(); + if (this.IsModal) + ImGui.EndPopup(); + else + ImGui.End(); this.firstDraw = false; this.firstDrawAfterRefresh = false; } + private static float GetDistanceFromMonitor(Vector2 point, ImGuiPlatformMonitorPtr monitor) + { + var lt = monitor.MainPos; + var rb = monitor.MainPos + monitor.MainSize; + var xoff = + point.X < lt.X + ? lt.X - point.X + : point.X > rb.X + ? point.X - rb.X + : 0; + var yoff = + point.Y < lt.Y + ? lt.Y - point.Y + : point.Y > rb.Y + ? point.Y - rb.Y + : 0; + return MathF.Sqrt((xoff * xoff) + (yoff * yoff)); + } + private void DrawChoices() { var lineHeight = ImGui.GetTextLineHeight(); @@ -338,15 +530,20 @@ public sealed class SingleFontChooserDialog : IDisposable } } - if (this.IgnorePreviewGlobalScale) + if (this.fontHandle is null) { - this.fontHandle ??= this.selectedFont.CreateFontHandle( - this.atlas, - tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale))); - } - else - { - this.fontHandle ??= this.selectedFont.CreateFontHandle(this.atlas); + if (this.IgnorePreviewGlobalScale) + { + this.fontHandle = this.selectedFont.CreateFontHandle( + this.atlas, + tk => tk.OnPreBuild(e => e.SetFontScaleMode(e.Font, FontScaleMode.UndoGlobalScale))); + } + else + { + this.fontHandle = this.selectedFont.CreateFontHandle(this.atlas); + } + + this.SelectedFontSpecChanged?.InvokeSafely(this.selectedFont); } if (this.fontHandle is null) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs index 8bb999557..469ef3dc3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/GamePrebakedFontsTestWidget.cs @@ -44,6 +44,8 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable private bool useBold; private bool useMinimumBuild; + private SingleFontChooserDialog? chooserDialog; + /// public string[]? CommandShortcuts { get; init; } @@ -126,32 +128,75 @@ internal class GamePrebakedFontsTestWidget : IDataWindowWidget, IDisposable if (ImGui.Button("Test Lock")) Task.Run(this.TestLock); - ImGui.SameLine(); if (ImGui.Button("Choose Editor Font")) { - var fcd = new SingleFontChooserDialog( - Service.Get().CreateFontAtlas( - $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont", - FontAtlasAutoRebuildMode.Async)); - fcd.SelectedFont = this.fontSpec; - fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode; - Service.Get().Draw += fcd.Draw; - fcd.ResultTask.ContinueWith( - r => Service.Get().RunOnFrameworkThread( - () => - { - Service.Get().Draw -= fcd.Draw; - fcd.Dispose(); + if (this.chooserDialog is null) + { + DoNext(); + } + else + { + this.chooserDialog.Cancel(); + this.chooserDialog.ResultTask.ContinueWith(_ => Service.Get().RunOnFrameworkThread(DoNext)); + this.chooserDialog = null; + } - _ = r.Exception; - if (!r.IsCompletedSuccessfully) - return; + void DoNext() + { + var fcd = new SingleFontChooserDialog( + Service.Get(), + $"{nameof(GamePrebakedFontsTestWidget)}:EditorFont"); + this.chooserDialog = fcd; + fcd.SelectedFont = this.fontSpec; + fcd.IgnorePreviewGlobalScale = !this.atlasScaleMode; + fcd.IsModal = false; + Service.Get().Draw += fcd.Draw; + var prevSpec = this.fontSpec; + fcd.SelectedFontSpecChanged += spec => + { + this.fontSpec = spec; + Log.Information("Selected font: {font}", this.fontSpec); + this.fontDialogHandle?.Dispose(); + this.fontDialogHandle = null; + }; + fcd.ResultTask.ContinueWith( + r => Service.Get().RunOnFrameworkThread( + () => + { + Service.Get().Draw -= fcd.Draw; + fcd.Dispose(); - this.fontSpec = r.Result; - Log.Information("Selected font: {font}", this.fontSpec); - this.fontDialogHandle?.Dispose(); - this.fontDialogHandle = null; - })); + _ = r.Exception; + var spec = r.IsCompletedSuccessfully ? r.Result : prevSpec; + if (this.fontSpec != spec) + { + this.fontSpec = spec; + this.fontDialogHandle?.Dispose(); + this.fontDialogHandle = null; + } + + this.chooserDialog = null; + })); + } + } + + if (this.chooserDialog is not null) + { + ImGui.SameLine(); + ImGui.TextUnformatted($"{this.chooserDialog.PopupPosition}, {this.chooserDialog.PopupSize}"); + + ImGui.SameLine(); + if (ImGui.Button("Random Location")) + { + var monitors = ImGui.GetPlatformIO().Monitors; + var monitor = monitors[Random.Shared.Next() % monitors.Size]; + this.chooserDialog.PopupPosition = monitor.WorkPos + (monitor.WorkSize * new Vector2( + Random.Shared.NextSingle(), + Random.Shared.NextSingle())); + this.chooserDialog.PopupSize = monitor.WorkSize * new Vector2( + Random.Shared.NextSingle(), + Random.Shared.NextSingle()); + } } this.privateAtlas ??= diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index ea6400121..5ccace850 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -12,7 +12,6 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.ImGuiFontChooserDialog; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; -using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; using Dalamud.Interface.Utility; using Dalamud.Utility; @@ -199,10 +198,10 @@ public class SettingsTabLook : SettingsTab if (ImGui.Button(Loc.Localize("DalamudSettingChooseDefaultFont", "Choose Default Font"))) { var faf = Service.Get(); - var fcd = new SingleFontChooserDialog( - faf.CreateFontAtlas($"{nameof(SettingsTabLook)}:Default", FontAtlasAutoRebuildMode.Async)); + var fcd = new SingleFontChooserDialog(faf, $"{nameof(SettingsTabLook)}:Default"); fcd.SelectedFont = (SingleFontSpec)this.defaultFontSpec; fcd.FontFamilyExcludeFilter = x => x is DalamudDefaultFontAndFamilyId; + fcd.SetPopupPositionAndSizeToCurrentWindowCenter(); interfaceManager.Draw += fcd.Draw; fcd.ResultTask.ContinueWith( r => Service.Get().RunOnFrameworkThread( diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index 0445499c8..a79ab099d 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -82,21 +82,25 @@ public interface IFontAtlas : IDisposable /// public IDisposable SuppressAutoRebuild(); - /// - /// Creates a new from game's built-in fonts. - /// + /// Creates a new from game's built-in fonts. /// Font to use. /// Handle to a font that may or may not be ready yet. + /// This function does not throw. will be populated instead, if + /// the build procedure has failed. can be used regardless of the state of the font + /// handle. public IFontHandle NewGameFontHandle(GameFontStyle style); - /// - /// Creates a new IFontHandle using your own callbacks. - /// + /// Creates a new IFontHandle using your own callbacks. /// Callback for . /// Handle to a font that may or may not be ready yet. /// - /// Consider calling to support - /// glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language users. + /// Consider calling to + /// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language + /// users. + /// This function does not throw, even if would throw exceptions. + /// Instead, if it fails, the returned handle will contain an property + /// containing the exception happened during the build process. can be used even if + /// the build process has not been completed yet or failed. /// /// /// On initialization: diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs index 70799bb9c..0a9e9072e 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontHandle.cs @@ -58,10 +58,27 @@ public interface IFontHandle : IDisposable /// A disposable object that will pop the font on dispose. /// If called outside of the main thread. /// - /// This function uses , and may do extra things. + /// This function uses , and may do extra things. /// Use or to undo this operation. - /// Do not use . + /// Do not use . /// + /// + /// Push a font with `using` clause. + /// + /// using (fontHandle.Push()) + /// ImGui.TextUnformatted("Test"); + /// + /// Push a font with a matching call to . + /// + /// fontHandle.Push(); + /// ImGui.TextUnformatted("Test 2"); + /// + /// Push a font between two choices. + /// + /// using ((someCondition ? myFontHandle : dalamudPluginInterface.UiBuilder.MonoFontHandle).Push()) + /// ImGui.TextUnformatted("Test 3"); + /// + /// IDisposable Push(); /// diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 47254a5c9..89d968158 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -136,13 +136,18 @@ internal abstract class FontHandle : IFontHandle /// An instance of that must be disposed after use on success; /// null with populated on failure. /// - /// Still may be thrown. public ILockedImFont? TryLock(out string? errorMessage) { IFontHandleSubstance? prevSubstance = default; while (true) { - var substance = this.Manager.Substance; + if (this.manager is not { } nonDisposedManager) + { + errorMessage = "The font handle has been disposed."; + return null; + } + + var substance = nonDisposedManager.Substance; // Does the associated IFontAtlas have a built substance? if (substance is null) From 76ca202f38cd1962511e5a20739e5bcb206924d1 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 14 Mar 2024 12:54:12 +0900 Subject: [PATCH 574/585] Comments on RemoveNonDalamudInvocations --- .../ImGuiNotification/Internal/ActiveNotification.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index c54a9c6fa..019d9e281 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -252,6 +252,14 @@ internal sealed partial class ActiveNotification : IActiveNotification } /// Removes non-Dalamud invocation targets from events. + /// + /// This is done to prevent references of plugins being unloaded from outliving the plugin itself. + /// Anything that can contain plugin-provided types and functions count, which effectively means that events and + /// interface/object-typed fields need to be scrubbed. + /// As a notification can be marked as non-user-dismissable, in which case after removing event handlers there will + /// be no way to remove the notification, we force the notification to become user-dismissable, and reset the expiry + /// to the default duration on unload. + /// internal void RemoveNonDalamudInvocations() { var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); From ecfbcfe1944bf87460ad975c1f6b6d542de6633f Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 14 Mar 2024 12:55:28 +0900 Subject: [PATCH 575/585] Draw DefaultIcon instead of installed/3pp icon if plugin is gone --- .../Interface/ImGuiNotification/NotificationUtilities.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs index 0ed552b42..631263f95 100644 --- a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -131,12 +131,7 @@ public static class NotificationUtilities plugin.IsThirdParty, out var texture) || texture is null) { - texture = plugin switch - { - { IsDev: true } => dam.GetDalamudTextureWrap(DalamudAsset.DevPluginIcon), - { IsThirdParty: true } => dam.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon), - _ => dam.GetDalamudTextureWrap(DalamudAsset.InstalledIcon), - }; + texture = dam.GetDalamudTextureWrap(DalamudAsset.DefaultIcon); } return DrawIconFrom(minCoord, maxCoord, texture); From 9724e511e95e98c52f1a7981e6a1e2a1989371ba Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 14 Mar 2024 13:05:46 +0900 Subject: [PATCH 576/585] Add INotification.RespectUiHidden --- Dalamud/Interface/ImGuiNotification/INotification.cs | 3 +++ .../ImGuiNotification/Internal/ActiveNotification.cs | 7 +++++++ .../ImGuiNotification/Internal/NotificationManager.cs | 9 +++++++++ Dalamud/Interface/ImGuiNotification/Notification.cs | 3 +++ Dalamud/Interface/Internal/InterfaceManager.cs | 2 +- .../Internal/Windows/Data/Widgets/ImGuiWidget.cs | 5 +++++ lib/FFXIVClientStructs | 2 +- 7 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs index 207722c56..f9a043c0b 100644 --- a/Dalamud/Interface/ImGuiNotification/INotification.cs +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -60,6 +60,9 @@ public interface INotification /// is set to . bool ShowIndeterminateIfNoExpiry { get; set; } + /// Gets or sets a value indicating whether to respect the current UI visibility state. + bool RespectUiHidden { get; set; } + /// Gets or sets a value indicating whether the notification has been minimized. bool Minimized { get; set; } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs index 019d9e281..3bc7c3837 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -90,6 +90,13 @@ internal sealed partial class ActiveNotification : IActiveNotification set => this.underlyingNotification.Title = value; } + /// + public bool RespectUiHidden + { + get => this.underlyingNotification.RespectUiHidden; + set => this.underlyingNotification.RespectUiHidden = value; + } + /// public string? MinimizedText { diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index 973e93c72..272407615 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; @@ -20,6 +21,9 @@ namespace Dalamud.Interface.ImGuiNotification.Internal; [ServiceManager.EarlyLoadedService] internal class NotificationManager : INotificationManager, IServiceType, IDisposable { + [ServiceManager.ServiceDependency] + private readonly GameGui gameGui = Service.Get(); + private readonly List notifications = new(); private readonly ConcurrentBag pendingNotifications = new(); @@ -98,6 +102,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos { var viewportSize = ImGuiHelpers.MainViewport.WorkSize; var height = 0f; + var uiHidden = this.gameGui.GameUiHidden; while (this.pendingNotifications.TryTake(out var newNotification)) this.notifications.Add(newNotification); @@ -109,7 +114,11 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); foreach (var tn in this.notifications) + { + if (uiHidden && tn.RespectUiHidden) + continue; height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap; + } } } diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs index 612533cb8..5175985c7 100644 --- a/Dalamud/Interface/ImGuiNotification/Notification.cs +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -38,6 +38,9 @@ public sealed record Notification : INotification /// public bool ShowIndeterminateIfNoExpiry { get; set; } = true; + /// + public bool RespectUiHidden { get; set; } = true; + /// public bool Minimized { get; set; } = true; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index c811e9287..67e444cbe 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -923,7 +923,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.IsDispatchingEvents) { this.Draw?.Invoke(); - Service.Get().Draw(); + Service.GetNullable()?.Draw(); } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 95119bb48..086b0c1ad 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -127,6 +127,8 @@ internal class ImGuiWidget : IDataWindowWidget NotificationTemplate.ProgressModeTitles, NotificationTemplate.ProgressModeTitles.Length); + ImGui.Checkbox("Respect UI Hidden", ref this.notificationTemplate.RespectUiHidden); + ImGui.Checkbox("Minimized", ref this.notificationTemplate.Minimized); ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry); @@ -160,6 +162,7 @@ internal class ImGuiWidget : IDataWindowWidget : null, Type = type, ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry, + RespectUiHidden = this.notificationTemplate.RespectUiHidden, Minimized = this.notificationTemplate.Minimized, UserDismissable = this.notificationTemplate.UserDismissable, InitialDuration = @@ -388,6 +391,7 @@ internal class ImGuiWidget : IDataWindowWidget public int InitialDurationInt; public int HoverExtendDurationInt; public bool ShowIndeterminateIfNoExpiry; + public bool RespectUiHidden; public bool Minimized; public bool UserDismissable; public bool ActionBar; @@ -413,6 +417,7 @@ internal class ImGuiWidget : IDataWindowWidget this.UserDismissable = true; this.ActionBar = true; this.ProgressMode = 0; + this.RespectUiHidden = true; } } } diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 722a2c512..ac2ced26f 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 722a2c512238ac4b5324e3d343b316d8c8633a02 +Subproject commit ac2ced26fc98153c65f5b8f0eaf0f464258ff683 From 27e96e12ead4bcc054e3eab944cb143fe838850a Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Thu, 14 Mar 2024 13:08:59 +0900 Subject: [PATCH 577/585] Postmerge --- .../Interface/Internal/Windows/ConsoleWindow.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 1957ab720..ff5113275 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -12,6 +12,8 @@ using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -76,6 +78,8 @@ internal class ConsoleWindow : Window, IDisposable private int historyPos; private int copyStart = -1; + private IActiveNotification? prevCopyNotification; + /// Initializes a new instance of the class. /// An instance of . public ConsoleWindow(DalamudConfiguration configuration) @@ -441,10 +445,14 @@ internal class ConsoleWindow : Window, IDisposable return; ImGui.SetClipboardText(sb.ToString()); - Service.Get().AddNotification( - $"{n:n0} line(s) copied.", - this.WindowName, - NotificationType.Success); + this.prevCopyNotification?.DismissNow(); + this.prevCopyNotification = Service.Get().AddNotification( + new() + { + Title = this.WindowName, + Content = $"{n:n0} line(s) copied.", + Type = NotificationType.Success, + }); } private void DrawOptionsToolbar() From 4c18f77f51a2af2de52f8dbb1d262d6c7798f474 Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 14 Mar 2024 13:35:14 +0900 Subject: [PATCH 578/585] Fix log window layout (#1706) * Fix log window layout Fixed custom line rendering from not advancing ImGui cursor, and move input boxes around as log window is resized to become narower. * Undo unused change --- .../Internal/Windows/ConsoleWindow.cs | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 1957ab720..0c9c90d0d 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -90,11 +90,6 @@ internal class ConsoleWindow : Window, IDisposable this.Size = new Vector2(500, 400); this.SizeCondition = ImGuiCond.FirstUseEver; - this.SizeConstraints = new WindowSizeConstraints - { - MinimumSize = new Vector2(600.0f, 200.0f), - }; - this.RespectCloseHotkey = false; this.logLinesLimit = configuration.LogLinesLimit; @@ -555,10 +550,24 @@ internal class ConsoleWindow : Window, IDisposable if (ImGui.IsItemHovered()) ImGui.SetTooltip("Kill game"); ImGui.SameLine(); - ImGui.SetCursorPosX( - ImGui.GetContentRegionMax().X - (2 * 200.0f * ImGuiHelpers.GlobalScale) - ImGui.GetStyle().ItemSpacing.X); - ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale); + var inputWidth = 200.0f * ImGuiHelpers.GlobalScale; + var nextCursorPosX = ImGui.GetContentRegionMax().X - (2 * inputWidth) - ImGui.GetStyle().ItemSpacing.X; + var breakInputLines = nextCursorPosX < 0; + if (ImGui.GetCursorPosX() > nextCursorPosX) + { + ImGui.NewLine(); + inputWidth = ImGui.GetWindowWidth() - (ImGui.GetStyle().WindowPadding.X * 2); + + if (!breakInputLines) + inputWidth = (inputWidth - ImGui.GetStyle().ItemSpacing.X) / 2; + } + else + { + ImGui.SetCursorPosX(nextCursorPosX); + } + + ImGui.PushItemWidth(inputWidth); if (ImGui.InputTextWithHint( "##textHighlight", "regex highlight", @@ -583,8 +592,10 @@ internal class ConsoleWindow : Window, IDisposable log.HighlightMatches = null; } - ImGui.SameLine(); - ImGui.PushItemWidth(200.0f * ImGuiHelpers.GlobalScale); + if (!breakInputLines) + ImGui.SameLine(); + + ImGui.PushItemWidth(inputWidth); if (ImGui.InputTextWithHint( "##textFilter", "regex global filter", @@ -1082,6 +1093,8 @@ internal class ConsoleWindow : Window, IDisposable lastc = currc; } } + + ImGui.Dummy(screenPos - ImGui.GetCursorScreenPos()); } private record LogEntry From 3f4a91b726fd929bdb3cbf9062d9250331681313 Mon Sep 17 00:00:00 2001 From: wolfcomp <4028289+wolfcomp@users.noreply.github.com> Date: Sat, 16 Mar 2024 16:42:24 +0100 Subject: [PATCH 579/585] Handle static declares in AddonLifecycleWidget (#1720) * Handle static declares old way: new way: * get full name instead of class name --- .../Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 5b2855298..26af2a8b2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -90,7 +90,7 @@ public class AddonLifecycleWidget : IDataWindowWidget ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); ImGui.TableNextColumn(); - ImGui.Text($"{listener.FunctionDelegate.Target}::{listener.FunctionDelegate.Method.Name}"); + ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType.FullName}::{listener.FunctionDelegate.Method.Name}"); } ImGui.EndTable(); From 0656a524b181a43af7b5616676597f945fd64761 Mon Sep 17 00:00:00 2001 From: Ridan Vandenbergh Date: Sat, 16 Mar 2024 16:45:19 +0100 Subject: [PATCH 580/585] Add missing space in cross-world PF links (#1717) --- Dalamud/Game/Text/SeStringHandling/SeString.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 47c38b227..91dceb5d1 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -381,7 +381,7 @@ public class SeString { new PartyFinderPayload(listingId, isCrossWorld ? PartyFinderPayload.PartyFinderLinkType.NotSpecified : PartyFinderPayload.PartyFinderLinkType.LimitedToHomeWorld), // -> - new TextPayload($"Looking for Party ({recruiterName})"), + new TextPayload($"Looking for Party ({recruiterName})" + (isCrossWorld ? " " : string.Empty)), }; payloads.InsertRange(1, TextArrowPayloads); From 87b9edb4486c66486fa4db805e33b0e18693aa0d Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 17 Mar 2024 00:58:05 +0900 Subject: [PATCH 581/585] Add IInternal/PublicDisposableService (#1696) * Add IInternal/PublicDisposableService Plugins are exposed interfaces that are not inherited from `IDisposable`, but services implementing plugin interfaces often implement `IDisposable`. Some plugins may try to call `IDisposable.Dispose` on everything provided, and it also is possible to use `using` clause too eagerly while working on Dalamud itself, such as writing `using var smth = await Service.GetAsync();`. Such behaviors often lead to a difficult-to-debug errors, and making those services either not an `IDisposable` or making `IDisposable.Dispose` do nothing if the object has been loaded would prevent such errors. As `ServiceManager` must be the only class dealing with construction and disposal of services, `IInternalDisposableService` has been added to limit who can dispose the object. `IPublicDisposableService` also has been added to classes that can be constructed and accessed directly by plugins; for those, `Dispose` will be ignored if the instance is a service instance, and only `DisposeService` will respond. In addition, `DalamudPluginInterface` and `UiBuilder` also have been changed so that their `IDisposable.Dispose` no longer respond, and instead, internal functions have been added to only allow disposal from Dalamud. * Cleanup * Postmerge fixes * More explanation on RunOnFrameworkThread(ClearHooks) * Mark ReliableFileStorage public ctor obsolete --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- Dalamud.CorePlugin/PluginImpl.cs | 2 - .../Internal/DalamudConfiguration.cs | 4 +- Dalamud/Dalamud.cs | 22 ------ Dalamud/Data/DataManager.cs | 4 +- Dalamud/EntryPoint.cs | 2 + .../Game/Addon/Events/AddonEventManager.cs | 8 +-- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 8 +-- Dalamud/Game/ClientState/ClientState.cs | 8 +-- .../Game/ClientState/Conditions/Condition.cs | 65 +++++++---------- .../Game/ClientState/GamePad/GamepadState.cs | 4 +- Dalamud/Game/Command/CommandManager.cs | 8 +-- Dalamud/Game/Config/GameConfig.cs | 8 +-- Dalamud/Game/DutyState/DutyState.cs | 8 +-- Dalamud/Game/Framework.cs | 8 +-- Dalamud/Game/Gui/ChatGui.cs | 8 +-- Dalamud/Game/Gui/ContextMenu/ContextMenu.cs | 8 +-- Dalamud/Game/Gui/Dtr/DtrBar.cs | 8 +-- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 8 +-- Dalamud/Game/Gui/GameGui.cs | 8 +-- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 8 +-- Dalamud/Game/Gui/Toast/ToastGui.cs | 8 +-- Dalamud/Game/Internal/AntiDebug.cs | 56 ++++----------- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 69 +++++++------------ Dalamud/Game/Inventory/GameInventory.cs | 8 +-- Dalamud/Game/Network/GameNetwork.cs | 8 +-- .../Game/Network/Internal/NetworkHandlers.cs | 4 +- .../Game/Network/Internal/WinSockHandlers.cs | 4 +- Dalamud/Game/SigScanner.cs | 21 ++++-- Dalamud/Game/TargetSigScanner.cs | 12 +++- .../GameInteropProviderPluginScoped.cs | 4 +- Dalamud/Hooking/Internal/HookManager.cs | 4 +- .../Hooking/WndProcHook/WndProcHookManager.cs | 4 +- Dalamud/IServiceType.cs | 17 +++++ Dalamud/Interface/DragDrop/DragDropManager.cs | 9 ++- .../SingleFontChooserDialog.cs | 2 +- Dalamud/Interface/Internal/DalamudIme.cs | 4 +- .../Interface/Internal/DalamudInterface.cs | 4 +- .../ImGuiClipboardFunctionProvider.cs | 4 +- .../Internal/ImGuiDrawListFixProvider.cs | 4 +- .../Interface/Internal/InterfaceManager.cs | 64 +++++++++++------ Dalamud/Interface/Internal/TextureManager.cs | 4 +- .../Windows/Data/GameInventoryTestWidget.cs | 6 +- .../Internal/Windows/PluginImageCache.cs | 4 +- .../FontAtlasFactory.Implementation.cs | 22 ++++-- .../Internals/FontAtlasFactory.cs | 4 +- .../ManagedFontAtlas/Internals/FontHandle.cs | 9 ++- .../TitleScreenMenu/TitleScreenMenu.cs | 4 +- Dalamud/Interface/UiBuilder.cs | 5 ++ Dalamud/IoC/Internal/ServiceScope.cs | 13 +++- Dalamud/Logging/Internal/TaskTracker.cs | 4 +- Dalamud/Logging/ScopedPluginLogService.cs | 8 +-- Dalamud/Networking/Http/HappyHttpClient.cs | 4 +- Dalamud/Plugin/DalamudPluginInterface.cs | 24 ++++--- Dalamud/Plugin/Internal/PluginManager.cs | 15 +++- .../Profiles/ProfileCommandHandler.cs | 4 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 6 +- Dalamud/ServiceManager.cs | 42 ++++++++++- Dalamud/Service{T}.cs | 43 +++++++----- Dalamud/Storage/Assets/DalamudAssetManager.cs | 12 +++- Dalamud/Storage/ReliableFileStorage.cs | 24 ++++++- Dalamud/Utility/DisposeSafety.cs | 11 ++- Dalamud/Utility/Util.cs | 37 ---------- 62 files changed, 441 insertions(+), 381 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index cb9b4368a..7c9adc6a8 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -97,8 +97,6 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw -= this.OnDraw; this.windowSystem.RemoveAllWindows(); - - this.Interface.ExplicitDispose(); } /// diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 85a9507c9..70ed5dfde 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -26,7 +26,7 @@ namespace Dalamud.Configuration.Internal; #pragma warning disable SA1015 [InherentDependency] // We must still have this when unloading #pragma warning restore SA1015 -internal sealed class DalamudConfiguration : IServiceType, IDisposable +internal sealed class DalamudConfiguration : IInternalDisposableService { private static readonly JsonSerializerSettings SerializerSettings = new() { @@ -502,7 +502,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { // Make sure that we save, if a save is queued while we are shutting down this.Update(); diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 8c858ce7c..f9d2aff3c 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; -using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Storage; using Dalamud.Utility; @@ -187,27 +186,6 @@ internal sealed class Dalamud : IServiceType this.unloadSignal.WaitOne(); } - /// - /// Dispose subsystems related to plugin handling. - /// - public void DisposePlugins() - { - // this must be done before unloading interface manager, in order to do rebuild - // the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game - // will not receive any windows messages - Service.GetNullable()?.Dispose(); - - // this must be done before unloading plugins, or it can cause a race condition - // due to rendering happening on another thread, where a plugin might receive - // a render call after it has been disposed, which can crash if it attempts to - // use any resources that it freed in its own Dispose method - Service.GetNullable()?.Dispose(); - - Service.GetNullable()?.Dispose(); - - Service.GetNullable()?.Dispose(); - } - /// /// Replace the current exception handler with the default one. /// diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index b08c6ffe7..da93f57c4 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Data; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal sealed class DataManager : IDisposable, IServiceType, IDataManager +internal sealed class DataManager : IInternalDisposableService, IDataManager { private readonly Thread luminaResourceThread; private readonly CancellationTokenSource luminaCancellationTokenSource; @@ -158,7 +158,7 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager #endregion /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.luminaCancellationTokenSource.Cancel(); } diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index d0f9e8845..1ad3ad8a9 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -138,7 +138,9 @@ public sealed class EntryPoint SerilogEventSink.Instance.LogLine += SerilogOnLogLine; // Load configuration first to get some early persistent state, like log level +#pragma warning disable CS0618 // Type or member is obsolete var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!); +#pragma warning restore CS0618 // Type or member is obsolete var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs); // Set the appropriate logging level from the configuration diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 8ee09bed8..a9b9ef5fa 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Addon.Events; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonEventManager : IDisposable, IServiceType +internal unsafe class AddonEventManager : IInternalDisposableService { /// /// PluginName for Dalamud Internal use. @@ -62,7 +62,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.onUpdateCursor.Dispose(); @@ -204,7 +204,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager +internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddonEventManager { [ServiceManager.ServiceDependency] private readonly AddonEventManager eventManagerService = Service.Get(); @@ -225,7 +225,7 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. if (this.isForcingCursor) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 37f12ce3a..eefb3b5e9 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonLifecycle : IDisposable, IServiceType +internal unsafe class AddonLifecycle : IInternalDisposableService { private static readonly ModuleLog Log = new("AddonLifecycle"); @@ -89,7 +89,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType internal List EventListeners { get; } = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.onAddonSetupHook.Dispose(); this.onAddonSetup2Hook.Dispose(); @@ -383,7 +383,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle +internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLifecycle { [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); @@ -391,7 +391,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif private readonly List eventListeners = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var listener in this.eventListeners) { diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index d387c2e2d..bd4259f5a 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game.ClientState; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class ClientState : IDisposable, IServiceType, IClientState +internal sealed class ClientState : IInternalDisposableService, IClientState { private static readonly ModuleLog Log = new("ClientState"); @@ -115,7 +115,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.setupTerritoryTypeHook.Dispose(); this.framework.Update -= this.FrameworkOnOnUpdateEvent; @@ -196,7 +196,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState +internal class ClientStatePluginScoped : IInternalDisposableService, IClientState { [ServiceManager.ServiceDependency] private readonly ClientState clientStateService = Service.Get(); @@ -257,7 +257,7 @@ internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState public bool IsGPosing => this.clientStateService.IsGPosing; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; this.clientStateService.Login -= this.LoginForward; diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index a298b1502..dc8b28494 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game.ClientState.Conditions; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed partial class Condition : IServiceType, ICondition +internal sealed partial 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. @@ -22,6 +22,8 @@ internal sealed partial class Condition : IServiceType, ICondition private readonly bool[] cache = new bool[MaxConditionEntries]; + private bool isDisposed; + [ServiceManager.ServiceConstructor] private Condition(ClientState clientState) { @@ -35,6 +37,9 @@ internal sealed partial class Condition : IServiceType, ICondition this.framework.Update += this.FrameworkUpdate; } + /// Finalizes an instance of the class. + ~Condition() => this.Dispose(false); + /// public event ICondition.ConditionChangeDelegate? ConditionChange; @@ -60,6 +65,9 @@ internal sealed partial class Condition : IServiceType, ICondition public bool this[ConditionFlag flag] => this[(int)flag]; + /// + void IInternalDisposableService.DisposeService() => this.Dispose(true); + /// public bool Any() { @@ -89,6 +97,19 @@ internal sealed partial class Condition : IServiceType, ICondition return false; } + private void Dispose(bool disposing) + { + if (this.isDisposed) + return; + + if (disposing) + { + this.framework.Update -= this.FrameworkUpdate; + } + + this.isDisposed = true; + } + private void FrameworkUpdate(IFramework unused) { for (var i = 0; i < MaxConditionEntries; i++) @@ -112,44 +133,6 @@ internal sealed partial class Condition : IServiceType, ICondition } } -/// -/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. -/// -internal sealed partial class Condition : IDisposable -{ - private bool isDisposed; - - /// - /// Finalizes an instance of the class. - /// - ~Condition() - { - this.Dispose(false); - } - - /// - /// Disposes this instance, alongside its hooks. - /// - void IDisposable.Dispose() - { - GC.SuppressFinalize(this); - this.Dispose(true); - } - - private void Dispose(bool disposing) - { - if (this.isDisposed) - return; - - if (disposing) - { - this.framework.Update -= this.FrameworkUpdate; - } - - this.isDisposed = true; - } -} - /// /// Plugin-scoped version of a Condition service. /// @@ -159,7 +142,7 @@ internal sealed partial class Condition : IDisposable #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition +internal class ConditionPluginScoped : IInternalDisposableService, ICondition { [ServiceManager.ServiceDependency] private readonly Condition conditionService = Service.Get(); @@ -185,7 +168,7 @@ internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition public bool this[int flag] => this.conditionService[flag]; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.conditionService.ConditionChange -= this.ConditionChangedForward; diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index 40e632113..a0e16f0e2 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.GamePad; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState +internal unsafe class GamepadState : IInternalDisposableService, IGamepadState { private readonly Hook? gamepadPoll; @@ -109,7 +109,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState /// /// Disposes this instance, alongside its hooks. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.Dispose(true); GC.SuppressFinalize(this); diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 6b67f1892..7dcca763b 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Command; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager +internal sealed class CommandManager : IInternalDisposableService, ICommandManager { private static readonly ModuleLog Log = new("Command"); @@ -130,7 +130,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage } /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled; } @@ -170,7 +170,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandManager +internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager { private static readonly ModuleLog Log = new("Command"); @@ -193,7 +193,7 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM public ReadOnlyDictionary Commands => this.commandManagerService.Commands; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var command in this.pluginRegisteredCommands) { diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 162df9417..a021025b1 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.Config; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable +internal sealed class GameConfig : IInternalDisposableService, IGameConfig { private readonly TaskCompletionSource tcsInitialization = new(); private readonly TaskCompletionSource tcsSystem = new(); @@ -195,7 +195,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value); /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { var ode = new ObjectDisposedException(nameof(GameConfig)); this.tcsInitialization.SetExceptionIfIncomplete(ode); @@ -248,7 +248,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig +internal class GameConfigPluginScoped : IInternalDisposableService, IGameConfig { [ServiceManager.ServiceDependency] private readonly GameConfig gameConfigService = Service.Get(); @@ -295,7 +295,7 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig public GameConfigSection UiControl => this.gameConfigService.UiControl; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameConfigService.Changed -= this.ConfigChangedForward; this.initializationTask.ContinueWith( diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index c4bda0d19..e2e4aef15 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -13,7 +13,7 @@ namespace Dalamud.Game.DutyState; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal unsafe class DutyState : IDisposable, IServiceType, IDutyState +internal unsafe class DutyState : IInternalDisposableService, IDutyState { private readonly DutyStateAddressResolver address; private readonly Hook contentDirectorNetworkMessageHook; @@ -62,7 +62,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState private bool CompletedThisTerritory { get; set; } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.contentDirectorNetworkMessageHook.Dispose(); this.framework.Update -= this.FrameworkOnUpdateEvent; @@ -168,7 +168,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState +internal class DutyStatePluginScoped : IInternalDisposableService, IDutyState { [ServiceManager.ServiceDependency] private readonly DutyState dutyStateService = Service.Get(); @@ -200,7 +200,7 @@ internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState public bool IsDutyStarted => this.dutyStateService.IsDutyStarted; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.dutyStateService.DutyStarted -= this.DutyStartedForward; this.dutyStateService.DutyWiped -= this.DutyWipedForward; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 6520ca5c8..252a02031 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class Framework : IDisposable, IServiceType, IFramework +internal sealed class Framework : IInternalDisposableService, IFramework { private static readonly ModuleLog Log = new("Framework"); @@ -274,7 +274,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.RunOnFrameworkThread(() => { @@ -469,7 +469,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework +internal class FrameworkPluginScoped : IInternalDisposableService, IFramework { [ServiceManager.ServiceDependency] private readonly Framework frameworkService = Service.Get(); @@ -504,7 +504,7 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.frameworkService.Update -= this.OnUpdateForward; diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 02b52ee56..e0b90b382 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -29,7 +29,7 @@ namespace Dalamud.Game.Gui; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui +internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui { private static readonly ModuleLog Log = new("ChatGui"); @@ -109,7 +109,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.printMessageHook.Dispose(); this.populateItemLinkHook.Dispose(); @@ -409,7 +409,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui +internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui { [ServiceManager.ServiceDependency] private readonly ChatGui chatGuiService = Service.Get(); @@ -447,7 +447,7 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers => this.chatGuiService.RegisteredLinkHandlers; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.chatGuiService.ChatMessage -= this.OnMessageForward; this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward; diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs index 65c9b2760..f136d017a 100644 --- a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -28,7 +28,7 @@ namespace Dalamud.Game.Gui.ContextMenu; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu +internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu { private static readonly ModuleLog Log = new("ContextMenu"); @@ -77,7 +77,7 @@ internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMe private IReadOnlyList? SubmenuItems { get; set; } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { var manager = RaptureAtkUnitManager.Instance(); var menu = manager->GetAddonByName("ContextMenu"); @@ -496,7 +496,7 @@ original: #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu +internal class ContextMenuPluginScoped : IInternalDisposableService, IContextMenu { [ServiceManager.ServiceDependency] private readonly ContextMenu parentService = Service.Get(); @@ -514,7 +514,7 @@ internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu private object MenuItemsLock { get; } = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.parentService.OnMenuOpened -= this.OnMenuOpenedForward; diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 993bb951f..dbf6fba3c 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -22,7 +22,7 @@ namespace Dalamud.Game.Gui.Dtr; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar +internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar { private const uint BaseNodeId = 1000; @@ -101,7 +101,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.addonLifecycle.UnregisterListener(this.dtrPostDrawListener); this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener); @@ -493,7 +493,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar +internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar { [ServiceManager.ServiceDependency] private readonly DtrBar dtrBarService = Service.Get(); @@ -501,7 +501,7 @@ internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar private readonly Dictionary pluginEntries = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var entry in this.pluginEntries) { diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 2383b4e53..9310529e4 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -16,7 +16,7 @@ namespace Dalamud.Game.Gui.FlyText; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui +internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui { /// /// The native function responsible for adding fly text to the UI. See . @@ -78,7 +78,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui /// /// Disposes of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.createFlyTextHook.Dispose(); } @@ -277,7 +277,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui +internal class FlyTextGuiPluginScoped : IInternalDisposableService, IFlyTextGui { [ServiceManager.ServiceDependency] private readonly FlyTextGui flyTextGuiService = Service.Get(); @@ -294,7 +294,7 @@ internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.flyTextGuiService.FlyTextCreated -= this.FlyTextCreatedForward; diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index a97e19a0a..9272aa824 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -27,7 +27,7 @@ namespace Dalamud.Game.Gui; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui +internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui { private static readonly ModuleLog Log = new("GameGui"); @@ -344,7 +344,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui /// /// Disables the hooks and submodules of this module. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.setGlobalBgmHook.Dispose(); this.handleItemHoverHook.Dispose(); @@ -520,7 +520,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui +internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui { [ServiceManager.ServiceDependency] private readonly GameGui gameGuiService = Service.Get(); @@ -558,7 +558,7 @@ internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui public HoveredAction HoveredAction => this.gameGuiService.HoveredAction; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 4a8332d24..f19fe3b0a 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.Gui.PartyFinder; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGui +internal sealed class PartyFinderGui : IInternalDisposableService, IPartyFinderGui { private readonly PartyFinderAddressResolver address; private readonly IntPtr memory; @@ -47,7 +47,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.receiveListingHook.Dispose(); @@ -131,7 +131,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFinderGui +internal class PartyFinderGuiPluginScoped : IInternalDisposableService, IPartyFinderGui { [ServiceManager.ServiceDependency] private readonly PartyFinderGui partyFinderGuiService = Service.Get(); @@ -148,7 +148,7 @@ internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFin public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.partyFinderGuiService.ReceiveListing -= this.ReceiveListingForward; diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 7491b7f13..2cf327007 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -14,7 +14,7 @@ namespace Dalamud.Game.Gui.Toast; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui +internal sealed partial class ToastGui : IInternalDisposableService, IToastGui { private const uint QuestToastCheckmarkMagic = 60081; @@ -73,7 +73,7 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui /// /// Disposes of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.showNormalToastHook.Dispose(); this.showQuestToastHook.Dispose(); @@ -383,7 +383,7 @@ internal sealed partial class ToastGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui +internal class ToastGuiPluginScoped : IInternalDisposableService, IToastGui { [ServiceManager.ServiceDependency] private readonly ToastGui toastGuiService = Service.Get(); @@ -408,7 +408,7 @@ internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui public event IToastGui.OnErrorToastDelegate? ErrorToast; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.toastGuiService.Toast -= this.ToastForward; this.toastGuiService.QuestToast -= this.QuestToastForward; diff --git a/Dalamud/Game/Internal/AntiDebug.cs b/Dalamud/Game/Internal/AntiDebug.cs index 2f4ec28c0..5ab024012 100644 --- a/Dalamud/Game/Internal/AntiDebug.cs +++ b/Dalamud/Game/Internal/AntiDebug.cs @@ -12,7 +12,7 @@ namespace Dalamud.Game.Internal; /// This class disables anti-debug functionality in the game client. /// [ServiceManager.EarlyLoadedService] -internal sealed partial class AntiDebug : IServiceType +internal sealed class AntiDebug : IInternalDisposableService { private readonly byte[] nop = new byte[] { 0x31, 0xC0, 0x90, 0x90, 0x90, 0x90 }; private byte[] original; @@ -43,16 +43,25 @@ internal sealed partial class AntiDebug : IServiceType } } + /// Finalizes an instance of the class. + ~AntiDebug() => this.Disable(); + /// /// Gets a value indicating whether the anti-debugging is enabled. /// public bool IsEnabled { get; private set; } = false; + /// + void IInternalDisposableService.DisposeService() => this.Disable(); + /// /// Enables the anti-debugging by overwriting code in memory. /// public void Enable() { + if (this.IsEnabled) + return; + this.original = new byte[this.nop.Length]; if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled) { @@ -73,6 +82,9 @@ internal sealed partial class AntiDebug : IServiceType /// public void Disable() { + if (!this.IsEnabled) + return; + if (this.debugCheckAddress != IntPtr.Zero && this.original != null) { Log.Information($"Reverting debug check at 0x{this.debugCheckAddress.ToInt64():X}"); @@ -86,45 +98,3 @@ internal sealed partial class AntiDebug : IServiceType this.IsEnabled = false; } } - -/// -/// Implementing IDisposable. -/// -internal sealed partial class AntiDebug : IDisposable -{ - private bool disposed = false; - - /// - /// Finalizes an instance of the class. - /// - ~AntiDebug() => this.Dispose(false); - - /// - /// Disposes of managed and unmanaged resources. - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes of managed and unmanaged resources. - /// - /// If this was disposed through calling Dispose() or from being finalized. - private void Dispose(bool disposing) - { - if (this.disposed) - return; - - if (disposing) - { - // If anti-debug is enabled and is being disposed, odds are either the game is exiting, or Dalamud is being reloaded. - // If it is the latter, there's half a chance a debugger is currently attached. There's no real need to disable the - // check in either situation anyways. However if Dalamud is being reloaded, the sig may fail so may as well undo it. - this.Disable(); - } - - this.disposed = true; - } -} diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 30fab6b1b..9f9328de1 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -20,7 +20,7 @@ namespace Dalamud.Game.Internal; /// This class implements in-game Dalamud options in the in-game System menu. /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe partial class DalamudAtkTweaks : IServiceType +internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService { private readonly AtkValueChangeType atkValueChangeType; private readonly AtkValueSetString atkValueSetString; @@ -40,6 +40,8 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private readonly string locDalamudPlugins; private readonly string locDalamudSettings; + private bool disposed = false; + [ServiceManager.ServiceConstructor] private DalamudAtkTweaks(TargetSigScanner sigScanner) { @@ -69,6 +71,9 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType this.hookAtkUnitBaseReceiveGlobalEvent.Enable(); } + /// Finalizes an instance of the class. + ~DalamudAtkTweaks() => this.Dispose(false); + private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize); private delegate void AtkValueChangeType(AtkValue* thisPtr, ValueType type); @@ -79,6 +84,26 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5); + /// + void IInternalDisposableService.DisposeService() => this.Dispose(true); + + private void Dispose(bool disposing) + { + if (this.disposed) + return; + + if (disposing) + { + this.hookAgentHudOpenSystemMenu.Dispose(); + this.hookUiModuleRequestMainCommand.Dispose(); + this.hookAtkUnitBaseReceiveGlobalEvent.Dispose(); + + // this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; + } + + this.disposed = true; + } + /* private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args) { @@ -229,45 +254,3 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType } } } - -/// -/// Implements IDisposable. -/// -internal sealed partial class DalamudAtkTweaks : IDisposable -{ - private bool disposed = false; - - /// - /// Finalizes an instance of the class. - /// - ~DalamudAtkTweaks() => this.Dispose(false); - - /// - /// Dispose of managed and unmanaged resources. - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Dispose of managed and unmanaged resources. - /// - private void Dispose(bool disposing) - { - if (this.disposed) - return; - - if (disposing) - { - this.hookAgentHudOpenSystemMenu.Dispose(); - this.hookUiModuleRequestMainCommand.Dispose(); - this.hookAtkUnitBaseReceiveGlobalEvent.Dispose(); - - // this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; - } - - this.disposed = true; - } -} diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 1c7f3e3bf..3e3dbc685 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Inventory; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal class GameInventory : IDisposable, IServiceType +internal class GameInventory : IInternalDisposableService { private readonly List subscribersPendingChange = new(); private readonly List subscribers = new(); @@ -61,7 +61,7 @@ internal class GameInventory : IDisposable, IServiceType private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { lock (this.subscribersPendingChange) { @@ -351,7 +351,7 @@ internal class GameInventory : IDisposable, IServiceType #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory +internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInventory { private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped)); @@ -406,7 +406,7 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public event IGameInventory.InventoryChangedDelegate? ItemMergedExplicit; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameInventoryService.Unsubscribe(this); diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 4099f228e..954612af7 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.Network; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork +internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork { private readonly GameNetworkAddressResolver address; private readonly Hook processZonePacketDownHook; @@ -59,7 +59,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.processZonePacketDownHook.Dispose(); this.processZonePacketUpHook.Dispose(); @@ -145,7 +145,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork +internal class GameNetworkPluginScoped : IInternalDisposableService, IGameNetwork { [ServiceManager.ServiceDependency] private readonly GameNetwork gameNetworkService = Service.Get(); @@ -162,7 +162,7 @@ internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward; diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 8d5ec1344..2a46af3d3 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -26,7 +26,7 @@ namespace Dalamud.Game.Network.Internal; /// This class handles network notifications and uploading market board data. /// [ServiceManager.BlockingEarlyLoadedService] -internal unsafe class NetworkHandlers : IDisposable, IServiceType +internal unsafe class NetworkHandlers : IInternalDisposableService { private readonly IMarketBoardUploader uploader; @@ -213,7 +213,7 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType /// /// Disposes of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.disposing = true; this.Dispose(this.disposing); diff --git a/Dalamud/Game/Network/Internal/WinSockHandlers.cs b/Dalamud/Game/Network/Internal/WinSockHandlers.cs index 8439389ff..619c458c4 100644 --- a/Dalamud/Game/Network/Internal/WinSockHandlers.cs +++ b/Dalamud/Game/Network/Internal/WinSockHandlers.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game.Network.Internal; /// This class enables TCP optimizations in the game socket for better performance. /// [ServiceManager.EarlyLoadedService] -internal sealed class WinSockHandlers : IDisposable, IServiceType +internal sealed class WinSockHandlers : IInternalDisposableService { private Hook ws2SocketHook; @@ -27,7 +27,7 @@ internal sealed class WinSockHandlers : IDisposable, IServiceType /// /// Disposes of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.ws2SocketHook?.Dispose(); } diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index fe2d9083e..5e49052ae 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -104,6 +104,10 @@ public class SigScanner : IDisposable, ISigScanner /// public ProcessModule Module { get; } + /// Gets or sets a value indicating whether this instance of is meant to be a + /// Dalamud service. + private protected bool IsService { get; set; } + private IntPtr TextSectionTop => this.TextSectionBase + this.TextSectionSize; /// @@ -309,13 +313,11 @@ public class SigScanner : IDisposable, ISigScanner } } - /// - /// Free the memory of the copied module search area on object disposal, if applicable. - /// + /// public void Dispose() { - this.Save(); - Marshal.FreeHGlobal(this.moduleCopyPtr); + if (!this.IsService) + this.DisposeCore(); } /// @@ -337,6 +339,15 @@ public class SigScanner : IDisposable, ISigScanner } } + /// + /// Free the memory of the copied module search area on object disposal, if applicable. + /// + private protected void DisposeCore() + { + this.Save(); + Marshal.FreeHGlobal(this.moduleCopyPtr); + } + /// /// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location. /// diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index 35c82562e..e169ea904 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class TargetSigScanner : SigScanner, IServiceType +internal class TargetSigScanner : SigScanner, IPublicDisposableService { /// /// Initializes a new instance of the class. @@ -26,4 +26,14 @@ internal class TargetSigScanner : SigScanner, IServiceType : base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile) { } + + /// + void IInternalDisposableService.DisposeService() + { + if (this.IsService) + this.DisposeCore(); + } + + /// + void IPublicDisposableService.MarkDisposeOnlyFromService() => this.IsService = true; } diff --git a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index 9958385b9..1138d4e07 100644 --- a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -21,7 +21,7 @@ namespace Dalamud.Hooking.Internal; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceType, IDisposable +internal class GameInteropProviderPluginScoped : IGameInteropProvider, IInternalDisposableService { private readonly LocalPlugin plugin; private readonly SigScanner scanner; @@ -83,7 +83,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT => this.HookFromAddress(this.scanner.ScanText(signature), detour, backend); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { var notDisposed = this.trackedHooks.Where(x => !x.IsDisposed).ToArray(); if (notDisposed.Length != 0) diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs index 9c288a276..c8cdf3a46 100644 --- a/Dalamud/Hooking/Internal/HookManager.cs +++ b/Dalamud/Hooking/Internal/HookManager.cs @@ -14,7 +14,7 @@ namespace Dalamud.Hooking.Internal; /// This class manages the final disposition of hooks, cleaning up any that have not reverted their changes. /// [ServiceManager.EarlyLoadedService] -internal class HookManager : IDisposable, IServiceType +internal class HookManager : IInternalDisposableService { /// /// Logger shared with . @@ -74,7 +74,7 @@ internal class HookManager : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { RevertHooks(); TrackedHooks.Clear(); diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs index 91020f898..a2253eb23 100644 --- a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -15,7 +15,7 @@ namespace Dalamud.Hooking.WndProcHook; /// Manages WndProc hooks for game main window and extra ImGui viewport windows. /// [ServiceManager.BlockingEarlyLoadedService] -internal sealed class WndProcHookManager : IServiceType, IDisposable +internal sealed class WndProcHookManager : IInternalDisposableService { private static readonly ModuleLog Log = new(nameof(WndProcHookManager)); @@ -56,7 +56,7 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable public event WndProcEventDelegate? PostWndProc; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { if (this.dispatchMessageWHook.IsDisposed) return; diff --git a/Dalamud/IServiceType.cs b/Dalamud/IServiceType.cs index 973795faf..3a5dde880 100644 --- a/Dalamud/IServiceType.cs +++ b/Dalamud/IServiceType.cs @@ -6,3 +6,20 @@ public interface IServiceType { } + +/// , but for . +/// Use this to prevent services from accidentally being disposed by plugins or using clauses. +internal interface IInternalDisposableService : IServiceType +{ + /// Disposes the service. + void DisposeService(); +} + +/// An which happens to be public and needs to expose +/// . +internal interface IPublicDisposableService : IInternalDisposableService, IDisposable +{ + /// Marks that only should respond, + /// while suppressing . + void MarkDisposeOnlyFromService(); +} diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 151ef28a0..adc0ebff7 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Interface.DragDrop; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType +internal partial class DragDropManager : IInternalDisposableService, IDragDropManager { private nint windowHandlePtr = nint.Zero; @@ -56,6 +56,9 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService /// Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. public IReadOnlyList Directories { get; private set; } = Array.Empty(); + /// + void IInternalDisposableService.DisposeService() => this.Disable(); + /// Enable external drag and drop. public void Enable() { @@ -99,10 +102,6 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService this.ServiceAvailable = false; } - /// - public void Dispose() - => this.Disable(); - /// public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder) { diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs index 9420fe42c..7636f22b6 100644 --- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -93,7 +93,7 @@ public sealed class SingleFontChooserDialog : IDisposable /// Initializes a new instance of the class. /// A new instance of created using /// as its auto-rebuild mode. - /// The passed instance of will be disposed after use. If you pass an atlas + /// The passed instance of will be disposed after use. If you pass an atlas /// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing /// this font chooser. Consider using for automatic /// handling of font atlas derived from a , or even for automatic diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index caf014885..64040011e 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -34,7 +34,7 @@ namespace Dalamud.Interface.Internal; /// This class handles CJK IME. /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class DalamudIme : IDisposable, IServiceType +internal sealed unsafe class DalamudIme : IInternalDisposableService { private const int CImGuiStbTextCreateUndoOffset = 0xB57A0; private const int CImGuiStbTextUndoOffset = 0xB59C0; @@ -200,7 +200,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.candidateStrings.Count != 0 || this.ShowPartialConversion || this.inputModeIcon != default; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.interfaceManager.Draw -= this.Draw; this.ReleaseUnmanagedResources(); diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 1a07cd6ae..ec18fbb69 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -46,7 +46,7 @@ namespace Dalamud.Interface.Internal; /// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping. /// [ServiceManager.EarlyLoadedService] -internal class DalamudInterface : IDisposable, IServiceType +internal class DalamudInterface : IInternalDisposableService { private const float CreditsDarkeningMaxAlpha = 0.8f; @@ -209,7 +209,7 @@ internal class DalamudInterface : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.interfaceManager.Draw -= this.OnDraw; diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index bbf665405..9fa21a31b 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -35,7 +35,7 @@ namespace Dalamud.Interface.Internal; /// /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable +internal sealed unsafe class ImGuiClipboardFunctionProvider : IInternalDisposableService { private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider)); private readonly nint clipboardUserDataOriginal; @@ -75,7 +75,7 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { if (!this.clipboardUserData.IsAllocated) return; diff --git a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs index f2d6ed244..139dd96e2 100644 --- a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs @@ -24,7 +24,7 @@ namespace Dalamud.Interface.Internal; /// Change push_texture_id to only have one condition. /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposable +internal sealed unsafe class ImGuiDrawListFixProvider : IInternalDisposableService { private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0; private const int CImGuiImDrawListAddRectFilled = 0x59FD0; @@ -69,7 +69,7 @@ internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposabl ImDrawFlags flags); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.hookImDrawListAddPolyline.Dispose(); this.hookImDrawListAddRectFilled.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 48ad653d2..be14b882b 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; @@ -51,7 +52,7 @@ namespace Dalamud.Interface.Internal; /// This class manages interaction with the ImGui interface. /// [ServiceManager.BlockingEarlyLoadedService] -internal class InterfaceManager : IDisposable, IServiceType +internal class InterfaceManager : IInternalDisposableService { /// /// The default font size, in points. @@ -69,10 +70,13 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private readonly SwapChainVtableResolver address = new(); - private readonly Hook setCursorHook; private RawDX11Scene? scene; + private Hook? setCursorHook; private Hook? presentHook; private Hook? resizeBuffersHook; @@ -87,8 +91,6 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceConstructor] private InterfaceManager() { - this.setCursorHook = Hook.FromImport( - null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -233,25 +235,45 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// Dispose of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { - if (Service.GetNullable() is { } framework) - framework.RunOnFrameworkThread(Disposer).Wait(); - else - Disposer(); + // Unload hooks from the framework thread if possible. + // We're currently off the framework thread, as this function can only be called from + // ServiceManager.UnloadAllServices, which is called from EntryPoint.RunThread. + // The functions being unhooked are mostly called from the main thread, so unhooking from the main thread when + // possible would avoid any chance of unhooking a function that currently is being called. + // If unloading is initiated from "Unload Dalamud" /xldev menu, then the framework would still be running, as + // Framework.Destroy has never been called and thus Framework.IsFrameworkUnloading cannot be true, and this + // function will actually run the destroy from the framework thread. + // Otherwise, as Framework.IsFrameworkUnloading should have been set, this code should run immediately. + this.framework.RunOnFrameworkThread(ClearHooks).Wait(); + + // Below this point, hooks are guaranteed to be no longer called. + + // A font resource lock outlives the parent handle and the owner atlas. It should be disposed. + Interlocked.Exchange(ref this.defaultFontResourceLock, null)?.Dispose(); + + // Font handles become invalid after disposing the atlas, but just to be safe. + this.DefaultFontHandle?.Dispose(); + this.DefaultFontHandle = null; + + this.MonoFontHandle?.Dispose(); + this.MonoFontHandle = null; + + this.IconFontHandle?.Dispose(); + this.IconFontHandle = null; + + Interlocked.Exchange(ref this.dalamudAtlas, null)?.Dispose(); + Interlocked.Exchange(ref this.scene, null)?.Dispose(); - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.defaultFontResourceLock?.Dispose(); // lock outlives handle and atlas - this.defaultFontResourceLock = null; - this.dalamudAtlas?.Dispose(); - this.scene?.Dispose(); return; - void Disposer() + void ClearHooks() { - this.setCursorHook.Dispose(); - this.presentHook?.Dispose(); - this.resizeBuffersHook?.Dispose(); + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose(); + Interlocked.Exchange(ref this.presentHook, null)?.Dispose(); + Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose(); } } @@ -693,7 +715,6 @@ internal class InterfaceManager : IDisposable, IServiceType "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] private void ContinueConstruction( TargetSigScanner sigScanner, - Framework framework, FontAtlasFactory fontAtlasFactory) { this.dalamudAtlas = fontAtlasFactory @@ -731,7 +752,7 @@ internal class InterfaceManager : IDisposable, IServiceType this.DefaultFontHandle.ImFontChanged += (_, font) => { var fontLocked = font.NewRef(); - Service.Get().RunOnFrameworkThread( + this.framework.RunOnFrameworkThread( () => { // Update the ImGui default font. @@ -765,6 +786,7 @@ internal class InterfaceManager : IDisposable, IServiceType Log.Error(ex, "Could not enable immersive mode"); } + this.setCursorHook = Hook.FromImport(null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); @@ -808,7 +830,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed + return this.setCursorHook?.IsDisposed is not false ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 9f90ea1ad..74ce91e5e 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Interface.Internal; [ResolveVia] [ResolveVia] #pragma warning restore SA1015 -internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITextureSubstitutionProvider +internal class TextureManager : IInternalDisposableService, ITextureProvider, ITextureSubstitutionProvider { private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; @@ -268,7 +268,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.fallbackTextureWrap?.Dispose(); this.framework.Update -= this.FrameworkOnUpdate; diff --git a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs index c19f56654..5cede00cf 100644 --- a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs @@ -74,7 +74,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget this.standardEnabled = false; if (!this.rawEnabled) { - this.scoped.Dispose(); + ((IInternalDisposableService)this.scoped).DisposeService(); this.scoped = null; } } @@ -105,7 +105,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget this.rawEnabled = false; if (!this.standardEnabled) { - this.scoped.Dispose(); + ((IInternalDisposableService)this.scoped).DisposeService(); this.scoped = null; } } @@ -135,7 +135,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget { if (ImGui.Button("Disable##all-disable")) { - this.scoped?.Dispose(); + ((IInternalDisposableService)this.scoped)?.DisposeService(); this.scoped = null; this.standardEnabled = this.rawEnabled = false; } diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 29adbb3e5..97744b1a7 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -21,7 +21,7 @@ namespace Dalamud.Interface.Internal.Windows; /// A cache for plugin icons and images. /// [ServiceManager.EarlyLoadedService] -internal class PluginImageCache : IDisposable, IServiceType +internal class PluginImageCache : IInternalDisposableService { /// /// Maximum plugin image width. @@ -136,7 +136,7 @@ internal class PluginImageCache : IDisposable, IServiceType this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall, this.EmptyTexture); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.cancelToken.Cancel(); this.downloadQueue.CompleteAdding(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 883fcbbfc..b3d330075 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -204,12 +204,12 @@ internal sealed partial class FontAtlasFactory { while (this.IsBuildInProgress) await Task.Delay(100); - this.Garbage.Dispose(); + this.Clear(); }); } else { - this.Garbage.Dispose(); + this.Clear(); } return newRefCount; @@ -227,6 +227,20 @@ internal sealed partial class FontAtlasFactory var axisSubstance = this.Substances.OfType().Single(); return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; } + + public void Clear() + { + try + { + this.Garbage.Dispose(); + } + catch (Exception e) + { + Log.Error( + e, + $"Disposing {nameof(FontAtlasBuiltData)} of {this.Owner?.Name ?? "???"}."); + } + } } private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback @@ -547,13 +561,13 @@ internal sealed partial class FontAtlasFactory { if (this.buildIndex != rebuildIndex) { - data.ExplicitDisposeIgnoreExceptions(); + data.Release(); return; } var prevBuiltData = this.builtData; this.builtData = data; - prevBuiltData.ExplicitDisposeIgnoreExceptions(); + prevBuiltData?.Release(); this.buildTask = EmptyTask; fontsAndLocks.EnsureCapacity(data.Substances.Sum(x => x.RelevantHandles.Count)); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 3e0fd1394..7fa41487a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -31,7 +31,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// [ServiceManager.BlockingEarlyLoadedService] internal sealed partial class FontAtlasFactory - : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable + : IInternalDisposableService, GamePrebakedFontHandle.IGameFontTextureProvider { private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); private readonly CancellationTokenSource cancellationTokenSource = new(); @@ -161,7 +161,7 @@ internal sealed partial class FontAtlasFactory this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.cancellationTokenSource.Cancel(); this.scopedFinalizer.Dispose(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 15e2803da..0e26145f0 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; @@ -291,11 +292,15 @@ internal abstract class FontHandle : IFontHandle { if (disposing) { + if (Interlocked.Exchange(ref this.manager, null) is not { } managerToDisassociate) + return; + if (this.pushedFonts.Count > 0) Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); - this.Manager.FreeFontHandle(this); - this.manager = null; + + managerToDisassociate.FreeFontHandle(this); this.Disposed?.InvokeSafely(); + this.Disposed = null; this.ImFontChanged = null; } } diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs index 1f9a5bc76..6fbc0b4f3 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs @@ -193,7 +193,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleScreenMenu +internal class TitleScreenMenuPluginScoped : IInternalDisposableService, ITitleScreenMenu { [ServiceManager.ServiceDependency] private readonly TitleScreenMenu titleScreenMenuService = Service.Get(); @@ -204,7 +204,7 @@ internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleSc public IReadOnlyList? Entries => this.titleScreenMenuService.Entries; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var entry in this.pluginEntries) { diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 2053d9354..03132a530 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -17,6 +17,7 @@ using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Plugin; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -605,6 +606,10 @@ public sealed class UiBuilder : IDisposable } } + /// Clean up resources allocated by this instance of . + /// Dalamud internal use only. + internal void DisposeInternal() => this.scopedFinalizer.Dispose(); + /// /// Open the registered configuration UI, if it exists. /// diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 01c18a8b2..9fcf1af3c 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -96,6 +96,17 @@ internal class ServiceScopeImpl : IServiceScope /// public void Dispose() { - foreach (var createdObject in this.scopeCreatedObjects.OfType()) createdObject.Dispose(); + foreach (var createdObject in this.scopeCreatedObjects) + { + switch (createdObject) + { + case IInternalDisposableService d: + d.DisposeService(); + break; + case IDisposable d: + d.Dispose(); + break; + } + } } } diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index b65f0efa7..9ecabe6c7 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -13,7 +13,7 @@ namespace Dalamud.Logging.Internal; /// Class responsible for tracking asynchronous tasks. /// [ServiceManager.EarlyLoadedService] -internal class TaskTracker : IDisposable, IServiceType +internal class TaskTracker : IInternalDisposableService { private static readonly ModuleLog Log = new("TT"); private static readonly List TrackedTasksInternal = new(); @@ -119,7 +119,7 @@ internal class TaskTracker : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.scheduleAndStartHook?.Dispose(); diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs index 924b4885d..0c044f2c2 100644 --- a/Dalamud/Logging/ScopedPluginLogService.cs +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -17,7 +17,7 @@ namespace Dalamud.Logging; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable +internal class ScopedPluginLogService : IServiceType, IPluginLog { private readonly LocalPlugin localPlugin; @@ -53,12 +53,6 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable /// public ILogger Logger { get; } - /// - public void Dispose() - { - GC.SuppressFinalize(this); - } - /// public void Fatal(string messageTemplate, params object[] values) => this.Write(LogEventLevel.Fatal, null, messageTemplate, values); diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index 4379a698f..23c6e3899 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -12,7 +12,7 @@ namespace Dalamud.Networking.Http; /// awareness. /// [ServiceManager.BlockingEarlyLoadedService] -internal class HappyHttpClient : IDisposable, IServiceType +internal class HappyHttpClient : IInternalDisposableService { /// /// Initializes a new instance of the class. @@ -58,7 +58,7 @@ internal class HappyHttpClient : IDisposable, IServiceType public HappyEyeballsCallback SharedHappyEyeballsCallback { get; } /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.SharedHttpClient.Dispose(); this.SharedHappyEyeballsCallback.Dispose(); diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 5e103ecbe..135cf89ea 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -452,26 +452,28 @@ public sealed class DalamudPluginInterface : IDisposable #endregion - /// - /// Unregister your plugin and dispose all references. - /// + /// void IDisposable.Dispose() { - this.UiBuilder.ExplicitDispose(); - Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); - Service.Get().LocalizationChanged -= this.OnLocalizationChanged; - Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; } - /// - /// Obsolete implicit dispose implementation. Should not be used. - /// - [Obsolete("Do not dispose \"DalamudPluginInterface\".", true)] + /// This function will do nothing. Dalamud will dispose this object on plugin unload. + [Obsolete("This function will do nothing. Dalamud will dispose this object on plugin unload.", true)] public void Dispose() { // ignored } + /// Unregister the plugin and dispose all references. + /// Dalamud internal use only. + internal void DisposeInternal() + { + Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); + Service.Get().LocalizationChanged -= this.OnLocalizationChanged; + Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; + this.UiBuilder.DisposeInternal(); + } + /// /// Dispatch the active plugins changed event. /// diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 6bdf73036..b815ac036 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -55,7 +55,7 @@ namespace Dalamud.Plugin.Internal; [InherentDependency] #pragma warning restore SA1015 -internal partial class PluginManager : IDisposable, IServiceType +internal partial class PluginManager : IInternalDisposableService { /// /// Default time to wait between plugin unload and plugin assembly unload. @@ -370,7 +370,7 @@ internal partial class PluginManager : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { var disposablePlugins = this.installedPluginsList.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError).ToArray(); @@ -410,7 +410,16 @@ internal partial class PluginManager : IDisposable, IServiceType // Now that we've waited enough, dispose the whole plugin. // Since plugins should have been unloaded above, this should be done quickly. foreach (var plugin in disposablePlugins) - plugin.ExplicitDisposeIgnoreExceptions($"Error disposing {plugin.Name}", Log); + { + try + { + plugin.Dispose(); + } + catch (Exception e) + { + Log.Error(e, $"Error disposing {plugin.Name}"); + } + } } this.assemblyLocationMonoHook?.Dispose(); diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs index 7001e4d7b..eebb87aaa 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs @@ -16,7 +16,7 @@ namespace Dalamud.Plugin.Internal.Profiles; /// Service responsible for profile-related chat commands. /// [ServiceManager.EarlyLoadedService] -internal class ProfileCommandHandler : IServiceType, IDisposable +internal class ProfileCommandHandler : IInternalDisposableService { private readonly CommandManager cmd; private readonly ProfileManager profileManager; @@ -69,7 +69,7 @@ internal class ProfileCommandHandler : IServiceType, IDisposable } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.cmd.RemoveHandler("/xlenablecollection"); this.cmd.RemoveHandler("/xldisablecollection"); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 0f65bafb2..911bc436d 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -240,7 +240,7 @@ internal class LocalPlugin : IDisposable this.instance = null; } - this.DalamudInterface?.ExplicitDispose(); + this.DalamudInterface?.DisposeInternal(); this.DalamudInterface = null; this.ServiceScope?.Dispose(); @@ -426,7 +426,7 @@ internal class LocalPlugin : IDisposable if (this.instance == null) { this.State = PluginState.LoadError; - this.DalamudInterface.ExplicitDispose(); + this.DalamudInterface.DisposeInternal(); Log.Error( $"Error while loading {this.Name}, failed to bind and call the plugin constructor"); return; @@ -499,7 +499,7 @@ internal class LocalPlugin : IDisposable this.instance = null; - this.DalamudInterface?.ExplicitDispose(); + this.DalamudInterface?.DisposeInternal(); this.DalamudInterface = null; this.ServiceScope?.Dispose(); diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index acd7c2b6f..845a65d6e 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -175,7 +176,8 @@ internal static class ServiceManager foreach (var serviceType in GetConcreteServiceTypes()) { var serviceKind = serviceType.GetServiceKind(); - Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind"); + + CheckServiceTypeContracts(serviceType); // Let IoC know about the interfaces this service implements serviceContainer.RegisterInterfaces(serviceType); @@ -514,6 +516,44 @@ internal static class ServiceManager return ServiceKind.ProvidedService; } + /// Validate service type contracts, and throws exceptions accordingly. + /// An instance of that is supposed to be a service type. + /// Does nothing on non-debug builds. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CheckServiceTypeContracts(Type serviceType) + { +#if DEBUG + try + { + if (!serviceType.IsAssignableTo(typeof(IServiceType))) + throw new InvalidOperationException($"Non-{nameof(IServiceType)} passed."); + if (serviceType.GetServiceKind() == ServiceKind.None) + throw new InvalidOperationException("Service type is not specified."); + + var isServiceDisposable = + serviceType.IsAssignableTo(typeof(IInternalDisposableService)); + var isAnyDisposable = + isServiceDisposable + || serviceType.IsAssignableTo(typeof(IDisposable)) + || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); + if (isAnyDisposable && !isServiceDisposable) + { + throw new InvalidOperationException( + $"A service must be an {nameof(IInternalDisposableService)} without specifying " + + $"{nameof(IDisposable)} nor {nameof(IAsyncDisposable)} if it is purely meant to be a service, " + + $"or an {nameof(IPublicDisposableService)} if it also is allowed to be constructed not as a " + + $"service to be used elsewhere and has to offer {nameof(IDisposable)} or " + + $"{nameof(IAsyncDisposable)}. See {nameof(ReliableFileStorage)} for an example of " + + $"{nameof(IPublicDisposableService)}."); + } + } + catch (Exception e) + { + throw new InvalidOperationException($"{serviceType.Name}: {e.Message}"); + } +#endif + } + /// /// Indicates that this constructor will be called for early initialization. /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 08f592826..ed03749d5 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -65,6 +65,12 @@ internal static class Service where T : IServiceType None, } + /// Does nothing. + /// Used to invoke the static ctor. + public static void Nop() + { + } + /// /// Sets the type in the service locator to the given object. /// @@ -72,6 +78,8 @@ internal static class Service where T : IServiceType public static void Provide(T obj) { ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name); + if (obj is IPublicDisposableService pds) + pds.MarkDisposeOnlyFromService(); instanceTcs.SetResult(obj); } @@ -297,23 +305,26 @@ internal static class Service where T : IServiceType if (!instanceTcs.Task.IsCompletedSuccessfully) return; - var instance = instanceTcs.Task.Result; - if (instance is IDisposable disposable) + switch (instanceTcs.Task.Result) { - ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name); - try - { - disposable.Dispose(); - ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name); - } - catch (Exception e) - { - ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name); - } - } - else - { - ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name); + case IInternalDisposableService d: + ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name); + try + { + d.DisposeService(); + ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name); + } + catch (Exception e) + { + ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name); + } + + break; + + default: + ServiceManager.CheckServiceTypeContracts(typeof(T)); + ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name); + break; } instanceTcs = new TaskCompletionSource(); diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 68be78352..4f53460fb 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Storage.Assets; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudAssetManager +internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamudAssetManager { private const int DownloadAttemptCount = 10; private const int RenameAttemptCount = 10; @@ -67,7 +67,13 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Where(x => x.GetAttribute()?.Required is true) .Select(this.CreateStreamAsync) .Select(x => x.ToContentDisposedTask())) - .ContinueWith(_ => loadTimings.Dispose()), + .ContinueWith( + r => + { + loadTimings.Dispose(); + return r; + }) + .Unwrap(), "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); Task.WhenAll( @@ -83,7 +89,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { lock (this.syncRoot) { diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index a013e95b5..eab93269e 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -22,17 +22,22 @@ namespace Dalamud.Storage; /// This is not an early-loaded service, as it is needed before they are initialized. /// [ServiceManager.ProvidedService] -public class ReliableFileStorage : IServiceType, IDisposable +[Api10ToDo("Make internal and IInternalDisposableService, and remove #pragma guard from the caller.")] +public class ReliableFileStorage : IPublicDisposableService { private static readonly ModuleLog Log = new("VFS"); private readonly object syncRoot = new(); + private SQLiteConnection? db; + private bool isService; /// /// Initializes a new instance of the class. /// /// Path to the VFS. + [Obsolete("Dalamud internal use only.", false)] + [Api10ToDo("Make internal, and remove #pragma guard from the caller.")] public ReliableFileStorage(string vfsDbPath) { var databasePath = Path.Combine(vfsDbPath, "dalamudVfs.db"); @@ -60,7 +65,7 @@ public class ReliableFileStorage : IServiceType, IDisposable } } } - + /// /// Check if a file exists. /// This will return true if the file does not exist on the filesystem, but in the transparent backup. @@ -288,9 +293,20 @@ public class ReliableFileStorage : IServiceType, IDisposable /// public void Dispose() { - this.db?.Dispose(); + if (!this.isService) + this.DisposeCore(); } + /// + void IInternalDisposableService.DisposeService() + { + if (this.isService) + this.DisposeCore(); + } + + /// + void IPublicDisposableService.MarkDisposeOnlyFromService() => this.isService = true; + /// /// Replace possible non-portable parts of a path with portable versions. /// @@ -312,6 +328,8 @@ public class ReliableFileStorage : IServiceType, IDisposable this.db.CreateTable(); } + private void DisposeCore() => this.db?.Dispose(); + private class DbFile { [PrimaryKey] diff --git a/Dalamud/Utility/DisposeSafety.cs b/Dalamud/Utility/DisposeSafety.cs index 8ac891e0a..64d31048f 100644 --- a/Dalamud/Utility/DisposeSafety.cs +++ b/Dalamud/Utility/DisposeSafety.cs @@ -70,7 +70,16 @@ public static class DisposeSafety r => { if (!r.IsCompletedSuccessfully) - return ignoreAllExceptions ? Task.CompletedTask : r; + { + if (ignoreAllExceptions) + { + _ = r.Exception; + return Task.CompletedTask; + } + + return r; + } + try { r.Result.Dispose(); diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 65196b3ee..43355ac2c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -19,7 +19,6 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Serilog; @@ -638,42 +637,6 @@ public static class Util if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) throw new Win32Exception(); } - - /// - /// Dispose this object. - /// - /// The object to dispose. - /// The type of object to dispose. - internal static void ExplicitDispose(this T obj) where T : IDisposable - { - obj.Dispose(); - } - - /// - /// Dispose this object. - /// - /// The object to dispose. - /// Log message to print, if specified and an error occurs. - /// Module logger, if any. - /// The type of object to dispose. - internal static void ExplicitDisposeIgnoreExceptions( - this T obj, string? logMessage = null, ModuleLog? moduleLog = null) where T : IDisposable - { - try - { - obj.Dispose(); - } - catch (Exception e) - { - if (logMessage == null) - return; - - if (moduleLog != null) - moduleLog.Error(e, logMessage); - else - Log.Error(e, logMessage); - } - } /// /// Gets a random, inoffensive, human-friendly string. From e52c2755cb6bb66eeea67718c249fa07afdf6144 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 17 Mar 2024 01:02:36 +0900 Subject: [PATCH 582/585] Fix CreateImGuiRangesFrom to omit null char (#1709) * Fix CreateImGuiRangesFrom to omit null char UnicodeRanges.BasicLatin is [0, 127], but ImGui stops reading the glyph range list on encountering a zero. Fixed that by ensuring that 0 never appears in the glyph range list. * Make problems explicit --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Interface/ManagedFontAtlas/IFontAtlas.cs | 11 +++++++ .../FontAtlasFactory.Implementation.cs | 29 +++++++++++++++++-- Dalamud/Interface/UiBuilder.cs | 13 +++++++-- Dalamud/Interface/Utility/ImGuiHelpers.cs | 7 +++-- 4 files changed, 51 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index a79ab099d..2feac8849 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -85,6 +85,10 @@ public interface IFontAtlas : IDisposable /// Creates a new from game's built-in fonts. /// Font to use. /// Handle to a font that may or may not be ready yet. + /// When called during , + /// , , and alike. Move the font handle + /// creating code outside those handlers, and only initialize them once. Call + /// on a previous font handle if you're replacing one. /// This function does not throw. will be populated instead, if /// the build procedure has failed. can be used regardless of the state of the font /// handle. @@ -93,6 +97,13 @@ public interface IFontAtlas : IDisposable /// Creates a new IFontHandle using your own callbacks. /// Callback for . /// Handle to a font that may or may not be ready yet. + /// When called during , + /// , , and alike. Move the font handle + /// creating code outside those handlers, and only initialize them once. Call + /// on a previous font handle if you're replacing one. + /// Consider calling to + /// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language + /// users. /// /// Consider calling to /// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index b3d330075..3c175ae3c 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -35,6 +35,9 @@ internal sealed partial class FontAtlasFactory /// public const string EllipsisCodepoints = "\u2026\u0085"; + /// Marker for tasks on whether it's being called inside a font build cycle. + public static readonly AsyncLocal IsBuildInProgressForTask = new(); + /// /// If set, disables concurrent font build operation. /// @@ -427,11 +430,28 @@ internal sealed partial class FontAtlasFactory } /// - public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); + public IFontHandle NewGameFontHandle(GameFontStyle style) + { + if (IsBuildInProgressForTask.Value) + { + throw new InvalidOperationException( + $"{nameof(this.NewGameFontHandle)} may not be called during {nameof(this.BuildStepChange)}, the callback of {nameof(this.NewDelegateFontHandle)}, {nameof(UiBuilder.BuildFonts)} or {nameof(UiBuilder.AfterBuildFonts)}."); + } + + return this.gameFontHandleManager.NewFontHandle(style); + } /// - public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => - this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) + { + if (IsBuildInProgressForTask.Value) + { + throw new InvalidOperationException( + $"{nameof(this.NewDelegateFontHandle)} may not be called during {nameof(this.BuildStepChange)} or the callback of {nameof(this.NewDelegateFontHandle)}, {nameof(UiBuilder.BuildFonts)} or {nameof(UiBuilder.AfterBuildFonts)}."); + } + + return this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); + } /// public void BuildFontsOnNextFrame() @@ -630,6 +650,8 @@ internal sealed partial class FontAtlasFactory FontAtlasBuiltData? res = null; nint atlasPtr = 0; BuildToolkit? toolkit = null; + + IsBuildInProgressForTask.Value = true; try { res = new(this, scale); @@ -754,6 +776,7 @@ internal sealed partial class FontAtlasFactory // ReSharper disable once ConstantConditionalAccessQualifier toolkit?.Dispose(); this.buildQueued = false; + IsBuildInProgressForTask.Value = false; } unsafe bool ValidateMergeFontReferences(ImFontPtr replacementDstFont) diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 03132a530..2c2ca9725 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -516,9 +516,16 @@ public sealed class UiBuilder : IDisposable /// Handle to the game font which may or may not be available for use yet. [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style), - Service.Get()); + public GameFontHandle GetGameFontHandle(GameFontStyle style) + { + var prevValue = FontAtlasFactory.IsBuildInProgressForTask.Value; + FontAtlasFactory.IsBuildInProgressForTask.Value = false; + var v = new GameFontHandle( + (GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style), + Service.Get()); + FontAtlasFactory.IsBuildInProgressForTask.Value = prevValue; + return v; + } /// /// Call this to queue a rebuild of the font atlas.
    diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index f02effe1d..639b0315d 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -493,12 +493,13 @@ public static class ImGuiHelpers /// The range array that can be used for . public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => ranges - .Where(x => x.FirstCodePoint <= ushort.MaxValue) + .Select(x => (First: Math.Max(x.FirstCodePoint, 1), Last: x.FirstCodePoint + x.Length)) + .Where(x => x.First <= ushort.MaxValue && x.First <= x.Last) .SelectMany( x => new[] { - (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), - (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), + (ushort)Math.Min(x.First, ushort.MaxValue), + (ushort)Math.Min(x.Last, ushort.MaxValue), }) .Append((ushort)0) .ToArray(); From c709ad1811d521ebfaa18e8693352de07401a6d7 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sun, 17 Mar 2024 04:44:10 +0900 Subject: [PATCH 583/585] Make NotificationManager IInternalDisposableService (#1723) Pull request merge order shenanigans. --- .../ImGuiNotification/Internal/NotificationManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index 272407615..42aad2c45 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Interface.ImGuiNotification.Internal; /// Class handling notifications/toasts in ImGui. [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal class NotificationManager : INotificationManager, IServiceType, IDisposable +internal class NotificationManager : INotificationManager, IInternalDisposableService { [ServiceManager.ServiceDependency] private readonly GameGui gameGui = Service.Get(); @@ -51,7 +51,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos private IFontAtlas PrivateAtlas { get; } /// - public void Dispose() + public void DisposeService() { this.PrivateAtlas.Dispose(); foreach (var n in this.pendingNotifications) @@ -129,7 +129,7 @@ internal class NotificationManager : INotificationManager, IServiceType, IDispos #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class NotificationManagerPluginScoped : INotificationManager, IServiceType, IDisposable +internal class NotificationManagerPluginScoped : INotificationManager, IInternalDisposableService { private readonly LocalPlugin localPlugin; private readonly ConcurrentDictionary notifications = new(); @@ -151,7 +151,7 @@ internal class NotificationManagerPluginScoped : INotificationManager, IServiceT } /// - public void Dispose() + public void DisposeService() { while (!this.notifications.IsEmpty) { From 5d473919a1da5e9f50bf1ccc4e5ee60f11afc929 Mon Sep 17 00:00:00 2001 From: srkizer Date: Tue, 19 Mar 2024 12:10:47 +0900 Subject: [PATCH 584/585] Hide scheduler from RunOnFrameworkThread (#1725) * Hide scheduler from RunOnFrameworkThread Creating new tasks via Task.Run and alike would fetch the current scheduler, which we do not want in case of running stuff from the framework thread. Change is to prevent the standard library from seeing the "current scheduler". If one wants to use `await` with an async function to be run in the framework thread, one can use `RunOnFrameworkThreadAwaitable` instead now. * TaskSchedulerWidget: test better stuff * TaskSchedulerWidget: add freeze tests * More comments * Make TaskFactory a getter method instead of property to avoid bad suggestions * Why are there stuff still not pushed --- Dalamud/Game/Framework.cs | 78 ++++++-- .../Data/Widgets/TaskSchedulerWidget.cs | 84 ++++++++- Dalamud/Plugin/Services/IFramework.cs | 170 +++++++++++++++++- 3 files changed, 310 insertions(+), 22 deletions(-) diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 252a02031..e03ea882e 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -103,9 +103,6 @@ internal sealed class Framework : IInternalDisposableService, IFramework /// public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue; - /// - public TaskFactory FrameworkThreadTaskFactory { get; } - /// public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; @@ -125,6 +122,11 @@ internal sealed class Framework : IInternalDisposableService, IFramework ///
    internal bool DispatchUpdateEvents { get; set; } = true; + private TaskFactory FrameworkThreadTaskFactory { get; } + + /// + public TaskFactory GetTaskFactory() => this.FrameworkThreadTaskFactory; + /// public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) { @@ -138,6 +140,38 @@ internal sealed class Framework : IInternalDisposableService, IFramework return tcs.Task; } + /// + public Task RunOnFrameworkThreadAwaitable(Action action, CancellationToken cancellationToken = default) + { + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken); + } + + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) + { + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken); + } + + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) + { + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap(); + } + + /// + public Task RunOnFrameworkThreadAwaitable(Func> action, CancellationToken cancellationToken = default) + { + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap(); + } + /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func); @@ -193,7 +227,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework this.DelayTicks(delayTicks, cancellationToken), }, _ => func(), - cancellationToken); + cancellationToken, + TaskContinuationOptions.HideScheduler, + this.frameworkThreadTaskScheduler); } /// @@ -218,7 +254,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework this.DelayTicks(delayTicks, cancellationToken), }, _ => action(), - cancellationToken); + cancellationToken, + TaskContinuationOptions.HideScheduler, + this.frameworkThreadTaskScheduler); } /// @@ -243,7 +281,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework this.DelayTicks(delayTicks, cancellationToken), }, _ => func(), - cancellationToken).Unwrap(); + cancellationToken, + TaskContinuationOptions.HideScheduler, + this.frameworkThreadTaskScheduler).Unwrap(); } /// @@ -268,7 +308,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework this.DelayTicks(delayTicks, cancellationToken), }, _ => func(), - cancellationToken).Unwrap(); + cancellationToken, + TaskContinuationOptions.HideScheduler, + this.frameworkThreadTaskScheduler).Unwrap(); } /// @@ -491,9 +533,6 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework /// public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC; - /// - public TaskFactory FrameworkThreadTaskFactory => this.frameworkService.FrameworkThreadTaskFactory; - /// public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta; @@ -511,10 +550,29 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework this.Update = null; } + /// + public TaskFactory GetTaskFactory() => this.frameworkService.GetTaskFactory(); + /// public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) => this.frameworkService.DelayTicks(numTicks, cancellationToken); + /// + public Task RunOnFrameworkThreadAwaitable(Action action, CancellationToken cancellationToken = default) => + this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) => + this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) => + this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + + /// + public Task RunOnFrameworkThreadAwaitable(Func> action, CancellationToken cancellationToken = default) => + this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + /// public Task RunOnFrameworkThread(Func func) => this.frameworkService.RunOnFrameworkThread(func); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index c6d8c4e8b..0c86466e3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -144,9 +144,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 2s+60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(2), delayTicks: 60); } - ImGui.SameLine(); - - if (ImGui.Button("Every 60 frames")) + if (ImGui.Button("Every 60f")) { _ = framework.RunOnTick( async () => @@ -154,6 +152,8 @@ internal class TaskSchedulerWidget : IDataWindowWidget for (var i = 0L; ; i++) { Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + var it = i; + _ = Task.Factory.StartNew(() => Log.Information($" => Sub #{it}; MainThread={ThreadSafety.IsMainThread}")); await framework.DelayTicks(60, this.taskSchedulerCancelSource.Token); } }, @@ -162,6 +162,68 @@ internal class TaskSchedulerWidget : IDataWindowWidget ImGui.SameLine(); + if (ImGui.Button("Every 1s")) + { + _ = framework.RunOnTick( + async () => + { + for (var i = 0L; ; i++) + { + Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + var it = i; + _ = Task.Factory.StartNew(() => Log.Information($" => Sub #{it}; MainThread={ThreadSafety.IsMainThread}")); + await Task.Delay(TimeSpan.FromSeconds(1), this.taskSchedulerCancelSource.Token); + } + }, + cancellationToken: this.taskSchedulerCancelSource.Token); + } + + ImGui.SameLine(); + + if (ImGui.Button("Every 60f (Await)")) + { + _ = framework.RunOnFrameworkThreadAwaitable( + async () => + { + for (var i = 0L; ; i++) + { + Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + var it = i; + _ = Task.Factory.StartNew(() => Log.Information($" => Sub #{it}; MainThread={ThreadSafety.IsMainThread}")); + await framework.DelayTicks(60, this.taskSchedulerCancelSource.Token); + } + }, + this.taskSchedulerCancelSource.Token); + } + + ImGui.SameLine(); + + if (ImGui.Button("Every 1s (Await)")) + { + _ = framework.RunOnFrameworkThreadAwaitable( + async () => + { + for (var i = 0L; ; i++) + { + Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + var it = i; + _ = Task.Factory.StartNew(() => Log.Information($" => Sub #{it}; MainThread={ThreadSafety.IsMainThread}")); + await Task.Delay(TimeSpan.FromSeconds(1), this.taskSchedulerCancelSource.Token); + } + }, + this.taskSchedulerCancelSource.Token); + } + + ImGui.SameLine(); + + if (ImGui.Button("As long as it's in Framework Thread")) + { + Task.Run(async () => await framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); + framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + } + + ImGui.SameLine(); + if (ImGui.Button("Error in 1s")) { _ = framework.RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)); @@ -169,10 +231,18 @@ internal class TaskSchedulerWidget : IDataWindowWidget ImGui.SameLine(); - if (ImGui.Button("As long as it's in Framework Thread")) + if (ImGui.Button("Freeze 1s")) { - Task.Run(async () => await framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); - framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + _ = framework.RunOnFrameworkThread(() => Helper().Wait()); + static async Task Helper() => await Task.Delay(1000); + } + + ImGui.SameLine(); + + if (ImGui.Button("Freeze Completely")) + { + _ = framework.RunOnFrameworkThreadAwaitable(() => Helper().Wait()); + static async Task Helper() => await Task.Delay(1000); } if (ImGui.CollapsingHeader("Download")) @@ -217,7 +287,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget this.downloadState = default; var factory = downloadUsingGlobalScheduler ? Task.Factory - : framework.FrameworkThreadTaskFactory; + : framework.GetTaskFactory(); this.downloadState = default; this.downloadTask = factory.StartNew( async () => diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs index a93abd252..4b04b633e 100644 --- a/Dalamud/Plugin/Services/IFramework.cs +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -1,11 +1,29 @@ using System.Threading; using System.Threading.Tasks; +using Dalamud.Interface.Internal.Windows.Data.Widgets; + namespace Dalamud.Plugin.Services; /// /// This class represents the Framework of the native game client and grants access to various subsystems. /// +/// +/// Choosing between RunOnFrameworkThread and RunOnFrameworkThreadAwaitable +///
      +///
    • If you do need to do use await and have your task keep executing on the main thread after waiting is +/// done, use RunOnFrameworkThreadAwaitable.
    • +///
    • If you need to call or , use +/// RunOnFrameworkThread.
    • +///
    +/// The game is likely to completely lock up if you call above synchronous function and getter, because starting +/// a new task by default runs on , which would make the task run on the framework +/// thread if invoked via RunOnFrameworkThreadAwaitable. This includes Task.Factory.StartNew and +/// Task.ContinueWith. Use Task.Run if you need to start a new task from the callback specified to +/// RunOnFrameworkThreadAwaitable, as it will force your task to be run in the default thread pool. +/// See to see the difference in behaviors, and how would a misuse of these +/// functions result in a deadlock. +///
    public interface IFramework { /// @@ -29,11 +47,6 @@ public interface IFramework /// public DateTime LastUpdateUTC { get; } - /// - /// Gets a that runs tasks during Framework Update event. - /// - public TaskFactory FrameworkThreadTaskFactory { get; } - /// /// Gets the delta between the last Framework Update and the currently executing one. /// @@ -49,20 +62,97 @@ public interface IFramework ///
    public bool IsFrameworkUnloading { get; } + /// Gets a that runs tasks during Framework Update event. + /// The task factory. + public TaskFactory GetTaskFactory(); + /// /// Returns a task that completes after the given number of ticks. /// /// Number of ticks to delay. /// The cancellation token. /// A new that gets resolved after specified number of ticks happen. + /// The continuation will run on the framework thread by default. public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default); + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// The cancellation token. + /// Task representing the pending or already completed function. + /// + /// Starting new tasks and waiting on them synchronously from this callback will completely lock up + /// the game. Use await if you need to wait on something from an async callback. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// + public Task RunOnFrameworkThreadAwaitable(Action action, CancellationToken cancellationToken = default); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// The cancellation token. + /// Task representing the pending or already completed function. + /// + /// Starting new tasks and waiting on them synchronously from this callback will completely lock up + /// the game. Use await if you need to wait on something from an async callback. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// The cancellation token. + /// Task representing the pending or already completed function. + /// + /// Starting new tasks and waiting on them synchronously from this callback will completely lock up + /// the game. Use await if you need to wait on something from an async callback. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// The cancellation token. + /// Task representing the pending or already completed function. + /// + /// Starting new tasks and waiting on them synchronously from this callback will completely lock up + /// the game. Use await if you need to wait on something from an async callback. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// + public Task RunOnFrameworkThreadAwaitable(Func> action, CancellationToken cancellationToken = default); + /// /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. /// /// Return type. /// Function to call. /// Task representing the pending or already completed function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnFrameworkThread(Func func); /// @@ -70,6 +160,16 @@ public interface IFramework /// /// Function to call. /// Task representing the pending or already completed function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnFrameworkThread(Action action); /// @@ -78,6 +178,16 @@ public interface IFramework /// Return type. /// Function to call. /// Task representing the pending or already completed function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// [Obsolete($"Use {nameof(RunOnTick)} instead.")] public Task RunOnFrameworkThread(Func> func); @@ -86,6 +196,16 @@ public interface IFramework /// /// Function to call. /// Task representing the pending or already completed function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// [Obsolete($"Use {nameof(RunOnTick)} instead.")] public Task RunOnFrameworkThread(Func func); @@ -98,6 +218,16 @@ public interface IFramework /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. /// Cancellation token which will prevent the execution of this function if wait conditions are not met. /// Task representing the pending function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); /// @@ -108,6 +238,16 @@ public interface IFramework /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. /// Cancellation token which will prevent the execution of this function if wait conditions are not met. /// Task representing the pending function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); /// @@ -119,6 +259,16 @@ public interface IFramework /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. /// Cancellation token which will prevent the execution of this function if wait conditions are not met. /// Task representing the pending function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); /// @@ -129,5 +279,15 @@ public interface IFramework /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. /// Cancellation token which will prevent the execution of this function if wait conditions are not met. /// Task representing the pending function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); } From be63276a85c378eed1d327e98995b3ece8f9f068 Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 20 Mar 2024 00:04:21 +0900 Subject: [PATCH 585/585] Rename to Framework.Run (#1728) --- Dalamud/Game/Framework.cs | 24 ++++---- .../Data/Widgets/TaskSchedulerWidget.cs | 6 +- Dalamud/Plugin/Services/IFramework.cs | 58 +++++++++---------- 3 files changed, 44 insertions(+), 44 deletions(-) diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index e03ea882e..9e520daab 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -141,7 +141,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework } /// - public Task RunOnFrameworkThreadAwaitable(Action action, CancellationToken cancellationToken = default) + public Task Run(Action action, CancellationToken cancellationToken = default) { if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; @@ -149,7 +149,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework } /// - public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) + public Task Run(Func action, CancellationToken cancellationToken = default) { if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; @@ -157,7 +157,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework } /// - public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) + public Task Run(Func action, CancellationToken cancellationToken = default) { if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; @@ -165,7 +165,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework } /// - public Task RunOnFrameworkThreadAwaitable(Func> action, CancellationToken cancellationToken = default) + public Task Run(Func> action, CancellationToken cancellationToken = default) { if (cancellationToken == default) cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; @@ -558,20 +558,20 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework this.frameworkService.DelayTicks(numTicks, cancellationToken); /// - public Task RunOnFrameworkThreadAwaitable(Action action, CancellationToken cancellationToken = default) => - this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + public Task Run(Action action, CancellationToken cancellationToken = default) => + this.frameworkService.Run(action, cancellationToken); /// - public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) => - this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + public Task Run(Func action, CancellationToken cancellationToken = default) => + this.frameworkService.Run(action, cancellationToken); /// - public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) => - this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + public Task Run(Func action, CancellationToken cancellationToken = default) => + this.frameworkService.Run(action, cancellationToken); /// - public Task RunOnFrameworkThreadAwaitable(Func> action, CancellationToken cancellationToken = default) => - this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + public Task Run(Func> action, CancellationToken cancellationToken = default) => + this.frameworkService.Run(action, cancellationToken); /// public Task RunOnFrameworkThread(Func func) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index 0c86466e3..f4086fe5a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -182,7 +182,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget if (ImGui.Button("Every 60f (Await)")) { - _ = framework.RunOnFrameworkThreadAwaitable( + _ = framework.Run( async () => { for (var i = 0L; ; i++) @@ -200,7 +200,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget if (ImGui.Button("Every 1s (Await)")) { - _ = framework.RunOnFrameworkThreadAwaitable( + _ = framework.Run( async () => { for (var i = 0L; ; i++) @@ -241,7 +241,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget if (ImGui.Button("Freeze Completely")) { - _ = framework.RunOnFrameworkThreadAwaitable(() => Helper().Wait()); + _ = framework.Run(() => Helper().Wait()); static async Task Helper() => await Task.Delay(1000); } diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs index 4b04b633e..f1a4b6906 100644 --- a/Dalamud/Plugin/Services/IFramework.cs +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -9,18 +9,18 @@ namespace Dalamud.Plugin.Services; /// This class represents the Framework of the native game client and grants access to various subsystems. /// /// -/// Choosing between RunOnFrameworkThread and RunOnFrameworkThreadAwaitable +/// Choosing between RunOnFrameworkThread and Run ///
      ///
    • If you do need to do use await and have your task keep executing on the main thread after waiting is -/// done, use RunOnFrameworkThreadAwaitable.
    • +/// done, use Run. ///
    • If you need to call or , use -/// RunOnFrameworkThread.
    • +/// RunOnFrameworkThread. It also skips the task scheduler if invoked already from the framework thread. ///
    /// The game is likely to completely lock up if you call above synchronous function and getter, because starting /// a new task by default runs on , which would make the task run on the framework -/// thread if invoked via RunOnFrameworkThreadAwaitable. This includes Task.Factory.StartNew and +/// thread if invoked via Run. This includes Task.Factory.StartNew and /// Task.ContinueWith. Use Task.Run if you need to start a new task from the callback specified to -/// RunOnFrameworkThreadAwaitable, as it will force your task to be run in the default thread pool. +/// Run, as it will force your task to be run in the default thread pool. /// See to see the difference in behaviors, and how would a misuse of these /// functions result in a deadlock. ///
    @@ -85,10 +85,10 @@ public interface IFramework /// Starting new tasks and waiting on them synchronously from this callback will completely lock up /// the game. Use await if you need to wait on something from an async callback. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// - public Task RunOnFrameworkThreadAwaitable(Action action, CancellationToken cancellationToken = default); + public Task Run(Action action, CancellationToken cancellationToken = default); /// /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. @@ -101,10 +101,10 @@ public interface IFramework /// Starting new tasks and waiting on them synchronously from this callback will completely lock up /// the game. Use await if you need to wait on something from an async callback. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// - public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default); + public Task Run(Func action, CancellationToken cancellationToken = default); /// /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. @@ -116,10 +116,10 @@ public interface IFramework /// Starting new tasks and waiting on them synchronously from this callback will completely lock up /// the game. Use await if you need to wait on something from an async callback. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// - public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default); + public Task Run(Func action, CancellationToken cancellationToken = default); /// /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. @@ -132,10 +132,10 @@ public interface IFramework /// Starting new tasks and waiting on them synchronously from this callback will completely lock up /// the game. Use await if you need to wait on something from an async callback. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// - public Task RunOnFrameworkThreadAwaitable(Func> action, CancellationToken cancellationToken = default); + public Task Run(Func> action, CancellationToken cancellationToken = default); /// /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. @@ -146,11 +146,11 @@ public interface IFramework /// /// await, Task.Factory.StartNew or alike will continue off the framework thread. /// Awaiting on the returned from RunOnFrameworkThread, - /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// Run, or RunOnTick right away inside the callback specified to this /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); /// directly or indirectly from the delegate passed to this function. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// public Task RunOnFrameworkThread(Func func); @@ -163,11 +163,11 @@ public interface IFramework /// /// await, Task.Factory.StartNew or alike will continue off the framework thread. /// Awaiting on the returned from RunOnFrameworkThread, - /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// Run, or RunOnTick right away inside the callback specified to this /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); /// directly or indirectly from the delegate passed to this function. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// public Task RunOnFrameworkThread(Action action); @@ -181,11 +181,11 @@ public interface IFramework /// /// await, Task.Factory.StartNew or alike will continue off the framework thread. /// Awaiting on the returned from RunOnFrameworkThread, - /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// Run, or RunOnTick right away inside the callback specified to this /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); /// directly or indirectly from the delegate passed to this function. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// [Obsolete($"Use {nameof(RunOnTick)} instead.")] @@ -199,11 +199,11 @@ public interface IFramework /// /// await, Task.Factory.StartNew or alike will continue off the framework thread. /// Awaiting on the returned from RunOnFrameworkThread, - /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// Run, or RunOnTick right away inside the callback specified to this /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); /// directly or indirectly from the delegate passed to this function. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// [Obsolete($"Use {nameof(RunOnTick)} instead.")] @@ -221,11 +221,11 @@ public interface IFramework /// /// await, Task.Factory.StartNew or alike will continue off the framework thread. /// Awaiting on the returned from RunOnFrameworkThread, - /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// Run, or RunOnTick right away inside the callback specified to this /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); /// directly or indirectly from the delegate passed to this function. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); @@ -241,11 +241,11 @@ public interface IFramework /// /// await, Task.Factory.StartNew or alike will continue off the framework thread. /// Awaiting on the returned from RunOnFrameworkThread, - /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// Run, or RunOnTick right away inside the callback specified to this /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); /// directly or indirectly from the delegate passed to this function. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); @@ -262,11 +262,11 @@ public interface IFramework /// /// await, Task.Factory.StartNew or alike will continue off the framework thread. /// Awaiting on the returned from RunOnFrameworkThread, - /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// Run, or RunOnTick right away inside the callback specified to this /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); /// directly or indirectly from the delegate passed to this function. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); @@ -282,11 +282,11 @@ public interface IFramework /// /// await, Task.Factory.StartNew or alike will continue off the framework thread. /// Awaiting on the returned from RunOnFrameworkThread, - /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// Run, or RunOnTick right away inside the callback specified to this /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); /// directly or indirectly from the delegate passed to this function. /// See the remarks on if you need to choose which one to use, between - /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// Run and RunOnFrameworkThread. Note that RunOnTick is a fancy /// version of RunOnFrameworkThread. /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default);