From 48968322c450164467bf610006d912bfe1506970 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 6 Apr 2025 23:30:45 +0200 Subject: [PATCH 001/106] Update ConditionFlag enum (#2234) --- .../ClientState/Conditions/ConditionFlag.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs index 5b7bd2145..b83d48bd6 100644 --- a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs +++ b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs @@ -5,6 +5,8 @@ namespace Dalamud.Game.ClientState.Conditions; /// /// These come from LogMessage (somewhere) and directly map to each state field managed by the client. As of 5.25, it maps to /// LogMessage row 7700 and onwards, which can be checked by looking at the Condition sheet and looking at what column 2 maps to. +/// +/// The first 24 conditions are the local players CharacterModes. /// public enum ConditionFlag { @@ -220,8 +222,14 @@ public enum ConditionFlag /// /// Unable to execute command while auto-run is active. /// + [Obsolete("To avoid confusion, renamed to UsingChocoboTaxi.")] AutorunActive = 49, + /// + /// Unable to execute command while auto-run is active. + /// + UsingChocoboTaxi = 49, + /// /// Unable to execute command while occupied. /// @@ -261,8 +269,14 @@ public enum ConditionFlag /// /// Unable to execute command at this time. /// + [Obsolete("Renamed to MountOrOrnamentTransition.")] Unknown57 = 57, + /// + /// Unable to execute command at this time. + /// + MountOrOrnamentTransition = 57, + /// /// Unable to execute command while watching a cutscene. /// @@ -430,7 +444,7 @@ public enum ConditionFlag /// [Obsolete("Use InDutyQueue")] BoundToDuty97 = 91, - + /// /// Unable to execute command while bound by duty. /// Specifically triggered when you are in a queue for a duty but not inside a duty. @@ -450,8 +464,14 @@ public enum ConditionFlag /// /// Unable to execute command while using a parasol. /// + [Obsolete("Renamed to UsingFashionAccessory.")] UsingParasol = 94, + /// + /// Unable to execute command while using a fashion accessory. + /// + UsingFashionAccessory = 94, + /// /// Unable to execute command while bound by duty. /// @@ -481,4 +501,8 @@ public enum ConditionFlag /// Unable to execute command while editing a portrait. /// EditingPortrait = 100, + + // Unknown101 = 101, + // Unknown102 = 102, + // Unknown103 = 103, } From f96e2ae37c773a4c38ac1b7e27ddf0fd92bf6ffb Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sun, 6 Apr 2025 23:36:26 +0200 Subject: [PATCH 002/106] Fix reading world name for PcName (#2235) --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 83f8e241a..7003893ff 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -24,6 +24,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Component.Text; +using FFXIVClientStructs.Interop; using Lumina.Data.Structs.Excel; using Lumina.Excel; @@ -445,7 +446,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (this.gameConfig.UiConfig.TryGetUInt("LogCrossWorldName", out var logCrossWorldName) && logCrossWorldName == 1) - context.Builder.Append((ReadOnlySeStringSpan)world.Name); + context.Builder.Append(new ReadOnlySeStringSpan(world.Name.GetPointer(0))); } return true; From 499952b3d2e36ae826894ada16419c3a00ce9222 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 9 Apr 2025 22:13:11 +0200 Subject: [PATCH 003/106] SeStringEvaluator: Fix HeadAll not capitalizing correctly (#2240) * Fix obsoletes * Fix HeadAll not capitalizing correctly * Fix incorrect denoun cases in SeString Creator * Implement Utf8String.ToUpper in C# * Handle characters with accents too * Add remarks to ToUpper functions --- Dalamud/Game/ClientState/ClientState.cs | 2 +- .../Game/Text/Evaluator/SeStringEvaluator.cs | 4 +- .../Data/Widgets/NounProcessorWidget.cs | 5 +- .../Data/Widgets/SeStringCreatorWidget.cs | 2 +- .../Internal/AutoUpdate/AutoUpdateManager.cs | 2 +- Dalamud/Utility/StringExtensions.cs | 132 +++++++++++++++++- 6 files changed, 138 insertions(+), 9 deletions(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index da9873d8d..5101657ba 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -158,7 +158,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, - ConditionFlag.UsingParasol]); + ConditionFlag.UsingFashionAccessory]); blockingFlag = blockingConditions.FirstOrDefault(); return blockingFlag == 0; diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 7003893ff..b9633d6e3 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -939,9 +939,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (p.Type == ReadOnlySePayloadType.Text) { - context.Builder.Append( - context.CultureInfo.TextInfo.ToTitleCase(Encoding.UTF8.GetString(p.Body.Span))); - + context.Builder.Append(Encoding.UTF8.GetString(p.Body.Span).ToUpper(true, true, false, context.Language)); continue; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs index bc0bd0ac9..3cb5d3242 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/NounProcessorWidget.cs @@ -9,6 +9,7 @@ using Dalamud.Game.Text.Noun.Enums; using Dalamud.Interface.Utility.Raii; using ImGuiNET; + using Lumina.Data; using Lumina.Excel; using Lumina.Excel.Sheets; @@ -21,7 +22,7 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; internal class NounProcessorWidget : IDataWindowWidget { /// A list of German grammatical cases. - internal static readonly string[] GermanCases = ["Nominative", "Genitive", "Dative", "Accusative"]; + internal static readonly string[] GermanCases = [string.Empty, "Nominative", "Genitive", "Dative", "Accusative"]; private static readonly Type[] NounSheets = [ typeof(Aetheryte), @@ -156,7 +157,7 @@ internal class NounProcessorWidget : IDataWindowWidget GrammaticalCase = grammaticalCase, }; var output = nounProcessor.ProcessNoun(nounParams).ExtractText().Replace("\"", "\\\""); - var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase]}" : "1"; + var caseParam = language == ClientLanguage.German ? $"(int)GermanCases.{GermanCases[grammaticalCase + 1]}" : "1"; sb.AppendLine($"new(nameof(LSheets.{sheetType.Name}), {this.rowId}, ClientLanguage.{language}, {this.amount}, (int){articleTypeEnumType.Name}.{Enum.GetName(articleTypeEnumType, articleType)}, {caseParam}, \"{output}\"),"); } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index 92e57ddac..2175b8be1 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -1014,7 +1014,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget ImGui.TextUnformatted(Enum.GetName(articleTypeEnumType, u32)); } - if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 3) + if (macroCode is MacroCode.DeNoun && exprIdx == 4 && u32 is >= 0 and <= 4) { ImGui.SameLine(); ImGui.TextUnformatted(NounProcessorWidget.GermanCases[u32]); diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index ce135b947..d40184a76 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -501,7 +501,7 @@ internal class AutoUpdateManager : IServiceType condition.OnlyAny(ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, - ConditionFlag.UsingParasol); + ConditionFlag.UsingFashionAccessory); } private bool IsPluginManagerReady() diff --git a/Dalamud/Utility/StringExtensions.cs b/Dalamud/Utility/StringExtensions.cs index 50973e338..c28aebab2 100644 --- a/Dalamud/Utility/StringExtensions.cs +++ b/Dalamud/Utility/StringExtensions.cs @@ -1,5 +1,8 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Text; + +using Dalamud.Game; using FFXIVClientStructs.FFXIV.Client.UI; @@ -10,6 +13,9 @@ namespace Dalamud.Utility; /// public static class StringExtensions { + private static readonly string[] CommonExcludedWords = ["sas", "zos", "van", "nan", "tol", "deus", "mal", "de", "rem", "out", "yae", "bas", "cen", "quo", "viator", "la"]; + private static readonly string[] EnglishExcludedWords = ["of", "the", "to", "and", "a", "an", "or", "at", "by", "for", "in", "on", "with", "from", .. CommonExcludedWords]; + /// /// An extension method to chain usage of string.Format. /// @@ -77,7 +83,7 @@ public static class StringExtensions public static string StripSoftHyphen(this string input) => input.Replace("\u00AD", string.Empty); /// - /// Truncates the given string to the specified maximum number of characters, + /// Truncates the given string to the specified maximum number of characters, /// appending an ellipsis if truncation occurs. /// /// The string to truncate. @@ -88,4 +94,128 @@ public static class StringExtensions { return string.IsNullOrEmpty(input) || input.Length <= maxChars ? input : input[..maxChars] + ellipses; } + + /// + /// Converts the input string to uppercase based on specified options like capitalizing the first character, + /// normalizing vowels, and excluding certain words based on the selected language. + /// + /// The input string to be converted to uppercase. + /// Whether to capitalize only the first character of the string. + /// Whether to capitalize the first letter of each word. + /// Whether to normalize vowels to uppercase if they appear at the beginning of a word. + /// The language context used to determine which words to exclude from capitalization. + /// A new string with the appropriate characters converted to uppercase. + /// This is a C# implementation of Client::System::String::Utf8String.ToUpper with word exclusion lists as used by the HeadAll macro. + public static string ToUpper(this string input, bool firstCharOnly, bool everyWord, bool normalizeVowels, ClientLanguage language) + { + return ToUpper(input, firstCharOnly, everyWord, normalizeVowels, language switch + { + ClientLanguage.Japanese => [], + ClientLanguage.English => EnglishExcludedWords, + ClientLanguage.German => CommonExcludedWords, + ClientLanguage.French => CommonExcludedWords, + _ => [], + }); + } + + /// + /// Converts the input string to uppercase based on specified options like capitalizing the first character, + /// normalizing vowels, and excluding certain words based on the selected language. + /// + /// The input string to be converted to uppercase. + /// Whether to capitalize only the first character of the string. + /// Whether to capitalize the first letter of each word. + /// Whether to normalize vowels to uppercase if they appear at the beginning of a word. + /// A list of words to exclude from being capitalized. Words in this list will remain lowercase. + /// A new string with the appropriate characters converted to uppercase. + /// This is a C# implementation of Client::System::String::Utf8String.ToUpper. + public static string ToUpper(this string input, bool firstCharOnly, bool everyWord, bool normalizeVowels, ReadOnlySpan excludedWords) + { + if (string.IsNullOrEmpty(input)) + return input; + + var builder = new StringBuilder(input); + var isWordBeginning = true; + var length = firstCharOnly && !everyWord ? 1 : builder.Length; + + for (var i = 0; i < length; i++) + { + var ch = builder[i]; + + if (ch == ' ') + { + isWordBeginning = true; + continue; + } + + if (firstCharOnly && !isWordBeginning) + continue; + + // Basic ASCII a-z + if (ch >= 'a' && ch <= 'z') + { + var substr = builder.ToString(i, builder.Length - i); + var isExcluded = false; + + // Do not exclude words at the beginning + if (i > 0) + { + foreach (var excludedWord in excludedWords) + { + if (substr.StartsWith(excludedWord + " ", StringComparison.OrdinalIgnoreCase)) + { + isExcluded = true; + break; + } + } + } + + if (!isExcluded) + { + builder[i] = char.ToUpperInvariant(ch); + } + } + + // Special œ → Œ + else if (ch == 'œ') + { + builder[i] = 'Œ'; + } + + // Characters with accents + else if (ch >= 'à' && ch <= 'ý' && ch != '÷') + { + builder[i] = char.ToUpperInvariant(ch); + } + + // Normalize vowels with accents + else if (normalizeVowels && isWordBeginning) + { + if ("àáâãäå".Contains(ch)) + { + builder[i] = 'A'; + } + else if ("èéêë".Contains(ch)) + { + builder[i] = 'E'; + } + else if ("ìíîï".Contains(ch)) + { + builder[i] = 'I'; + } + else if ("òóôõö".Contains(ch)) + { + builder[i] = 'O'; + } + else if ("ùúûü".Contains(ch)) + { + builder[i] = 'U'; + } + } + + isWordBeginning = false; + } + + return builder.ToString(); + } } From bc18198435ba5ea9d1d5d7dc9f4ddec52e1a9eb8 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 9 Apr 2025 22:13:27 +0200 Subject: [PATCH 004/106] Fix kilo macro not having thousands separators (#2237) --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index b9633d6e3..e5bf1d7e6 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -636,7 +636,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator { case false when digit == 0: continue; - case true when i % 3 == 0: + case true when MathF.Log10(i) % 3 == 2: this.ResolveStringExpression(in context, eSep); break; } From d3dd3ab7c71c5dded73abe773c26cdbc07295994 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Wed, 9 Apr 2025 22:14:47 +0200 Subject: [PATCH 005/106] Add configurable default sorting to FileDialog. (#2233) --- .../ImGuiFileDialog/FileDialog.Files.cs | 329 +++++++++++------- .../ImGuiFileDialog/FileDialog.UI.cs | 18 +- .../ImGuiFileDialog/FileDialogManager.cs | 39 ++- 3 files changed, 239 insertions(+), 147 deletions(-) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs index 705c0f100..e5b7fc15e 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.Files.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Dalamud.Utility; + namespace Dalamud.Interface.ImGuiFileDialog; /// @@ -13,11 +15,44 @@ public partial class FileDialog private readonly DriveListLoader driveListLoader = new(); - private List files = new(); - private List filteredFiles = new(); + private readonly List files = []; + private readonly List filteredFiles = []; private SortingField currentSortingField = SortingField.FileName; - private bool[] sortDescending = { false, false, false, false }; + + /// Fired whenever the sorting field changes. + public event Action? SortOrderChanged; + + /// The sorting type of the file selector. + public enum SortingField + { + /// No sorting specified. + None = 0, + + /// Sort for ascending file names in culture-specific order. + FileName = 1, + + /// Sort for ascending file types in culture-specific order. + Type = 2, + + /// Sort for ascending file sizes. + Size = 3, + + /// Sort for ascending last update dates. + Date = 4, + + /// Sort for descending file names in culture-specific order. + FileNameDescending = 5, + + /// Sort for descending file types in culture-specific order. + TypeDescending = 6, + + /// Sort for descending file sizes. + SizeDescending = 7, + + /// Sort for descending last update dates. + DateDescending = 8, + } private enum FileStructType { @@ -25,48 +60,64 @@ public partial class FileDialog Directory, } - private enum SortingField + /// Specify the current and subsequent sort order. + /// The new sort order. None is invalid and will not have any effect. + public void SortFields(SortingField sortingField) { - None, - FileName, - Type, - Size, - Date, + Comparison? sortFunc = sortingField switch + { + SortingField.FileName => SortByFileNameAsc, + SortingField.FileNameDescending => SortByFileNameDesc, + SortingField.Type => SortByTypeAsc, + SortingField.TypeDescending => SortByTypeDesc, + SortingField.Size => SortBySizeAsc, + SortingField.SizeDescending => SortBySizeDesc, + SortingField.Date => SortByDateAsc, + SortingField.DateDescending => SortByDateDesc, + _ => null, + }; + + if (sortFunc is null) + { + return; + } + + this.files.Sort(sortFunc); + this.currentSortingField = sortingField; + this.ApplyFilteringOnFileList(); + this.SortOrderChanged?.InvokeSafely(this.currentSortingField); } - private static string ComposeNewPath(List decomp) + private static string ComposeNewPath(List decomposition) { - // Handle UNC paths (network paths) - if (decomp.Count >= 2 && string.IsNullOrEmpty(decomp[0]) && string.IsNullOrEmpty(decomp[1])) + switch (decomposition.Count) { - var pathParts = new List(decomp); - pathParts.RemoveRange(0, 2); - // Can not access server level or UNC root - if (pathParts.Count <= 1) - { - return string.Empty; - } + // Handle UNC paths (network paths) + case >= 2 when string.IsNullOrEmpty(decomposition[0]) && string.IsNullOrEmpty(decomposition[1]): + var pathParts = new List(decomposition); + pathParts.RemoveRange(0, 2); - return $"\\\\{string.Join('\\', pathParts)}"; + // Can not access server level or UNC root + if (pathParts.Count <= 1) + { + return string.Empty; + } + + return $@"\\{string.Join('\\', pathParts)}"; + case 1: + var drivePath = decomposition[0]; + if (drivePath[^1] != Path.DirectorySeparatorChar) + { // turn C: into C:\ + drivePath += Path.DirectorySeparatorChar; + } + + return drivePath; + default: return Path.Combine(decomposition.ToArray()); } - - if (decomp.Count == 1) - { - var drivePath = decomp[0]; - if (drivePath[^1] != Path.DirectorySeparatorChar) - { // turn C: into C:\ - drivePath += Path.DirectorySeparatorChar; - } - - return drivePath; - } - - return Path.Combine(decomp.ToArray()); } private static FileStruct GetFile(FileInfo file, string path) - { - return new FileStruct + => new() { FileName = file.Name, FilePath = path, @@ -76,11 +127,9 @@ public partial class FileDialog Type = FileStructType.File, Ext = file.Extension.Trim('.'), }; - } private static FileStruct GetDir(DirectoryInfo dir, string path) - { - return new FileStruct + => new() { FileName = dir.Name, FilePath = path, @@ -90,136 +139,191 @@ public partial class FileDialog Type = FileStructType.Directory, Ext = string.Empty, }; - } private static int SortByFileNameDesc(FileStruct a, FileStruct b) { - if (a.FileName[0] == '.' && b.FileName[0] != '.') + switch (a.FileName, b.FileName) { - return 1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - if (a.FileName[0] != '.' && b.FileName[0] == '.') + if (a.FileName[0] is '.') { - return -1; - } - - if (a.FileName[0] == '.' && b.FileName[0] == '.') - { - if (a.FileName.Length == 1) - { - return -1; - } - - if (b.FileName.Length == 1) + if (b.FileName[0] is not '.') { return 1; } - return -1 * string.Compare(a.FileName[1..], b.FileName[1..]); + if (a.FileName.Length is 1) + { + return -1; + } + + if (b.FileName.Length is 1) + { + return 1; + } + + return -1 * string.Compare(a.FileName[1..], b.FileName[1..], StringComparison.CurrentCulture); + } + + if (b.FileName[0] is '.') + { + return -1; } if (a.Type != b.Type) { - return a.Type == FileStructType.Directory ? 1 : -1; + return a.Type is FileStructType.Directory ? 1 : -1; } - return -1 * string.Compare(a.FileName, b.FileName); + return -string.Compare(a.FileName, b.FileName, StringComparison.CurrentCulture); } private static int SortByFileNameAsc(FileStruct a, FileStruct b) { - if (a.FileName[0] == '.' && b.FileName[0] != '.') + switch (a.FileName, b.FileName) { - return -1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - if (a.FileName[0] != '.' && b.FileName[0] == '.') + if (a.FileName[0] is '.') { - return 1; - } - - if (a.FileName[0] == '.' && b.FileName[0] == '.') - { - if (a.FileName.Length == 1) - { - return 1; - } - - if (b.FileName.Length == 1) + if (b.FileName[0] is not '.') { return -1; } - return string.Compare(a.FileName[1..], b.FileName[1..]); + if (a.FileName.Length is 1) + { + return 1; + } + + if (b.FileName.Length is 1) + { + return -1; + } + + return string.Compare(a.FileName[1..], b.FileName[1..], StringComparison.CurrentCulture); + } + + if (b.FileName[0] is '.') + { + return 1; } if (a.Type != b.Type) { - return a.Type == FileStructType.Directory ? -1 : 1; + return a.Type is FileStructType.Directory ? -1 : 1; } - return string.Compare(a.FileName, b.FileName); + return string.Compare(a.FileName, b.FileName, StringComparison.CurrentCulture); } private static int SortByTypeDesc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? 1 : -1; } - return string.Compare(a.Ext, b.Ext); + return string.Compare(a.Ext, b.Ext, StringComparison.CurrentCulture); } private static int SortByTypeAsc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? -1 : 1; } - return -1 * string.Compare(a.Ext, b.Ext); + return -string.Compare(a.Ext, b.Ext, StringComparison.CurrentCulture); } private static int SortBySizeDesc(FileStruct a, FileStruct b) { - if (a.Type != b.Type) + switch (a.FileName, b.FileName) { - return (a.Type == FileStructType.Directory) ? 1 : -1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - return (a.FileSize > b.FileSize) ? 1 : -1; + if (a.Type != b.Type) + { + return (a.Type is FileStructType.Directory) ? 1 : -1; + } + + return a.FileSize.CompareTo(b.FileSize); } private static int SortBySizeAsc(FileStruct a, FileStruct b) { - if (a.Type != b.Type) + switch (a.FileName, b.FileName) { - return (a.Type == FileStructType.Directory) ? -1 : 1; + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; } - return (a.FileSize > b.FileSize) ? -1 : 1; + if (a.Type != b.Type) + { + return (a.Type is FileStructType.Directory) ? -1 : 1; + } + + return -a.FileSize.CompareTo(b.FileSize); } private static int SortByDateDesc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? 1 : -1; } - return string.Compare(a.FileModifiedDate, b.FileModifiedDate); + return string.Compare(a.FileModifiedDate, b.FileModifiedDate, StringComparison.CurrentCulture); } private static int SortByDateAsc(FileStruct a, FileStruct b) { + switch (a.FileName, b.FileName) + { + case ("..", ".."): return 0; + case ("..", _): return -1; + case (_, ".."): return 1; + } + if (a.Type != b.Type) { return (a.Type == FileStructType.Directory) ? -1 : 1; } - return -1 * string.Compare(a.FileModifiedDate, b.FileModifiedDate); + return -string.Compare(a.FileModifiedDate, b.FileModifiedDate, StringComparison.CurrentCulture); } private bool CreateDir(string dirPath) @@ -336,52 +440,17 @@ public partial class FileDialog this.quickAccess.Add(new SideBarItem("Videos", Environment.GetFolderPath(Environment.SpecialFolder.MyVideos), FontAwesomeIcon.Video)); } - private void SortFields(SortingField sortingField, bool canChangeOrder = false) - { - switch (sortingField) + private SortingField GetNewSorting(int column) + => column switch { - case SortingField.FileName: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[0] = !this.sortDescending[0]; - } - - this.files.Sort(this.sortDescending[0] ? SortByFileNameDesc : SortByFileNameAsc); - break; - - case SortingField.Type: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[1] = !this.sortDescending[1]; - } - - this.files.Sort(this.sortDescending[1] ? SortByTypeDesc : SortByTypeAsc); - break; - - case SortingField.Size: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[2] = !this.sortDescending[2]; - } - - this.files.Sort(this.sortDescending[2] ? SortBySizeDesc : SortBySizeAsc); - break; - - case SortingField.Date: - if (canChangeOrder && sortingField == this.currentSortingField) - { - this.sortDescending[3] = !this.sortDescending[3]; - } - - this.files.Sort(this.sortDescending[3] ? SortByDateDesc : SortByDateAsc); - break; - } - - if (sortingField != SortingField.None) - { - this.currentSortingField = sortingField; - } - - this.ApplyFilteringOnFileList(); - } + 0 when this.currentSortingField is SortingField.FileName => SortingField.FileNameDescending, + 0 => SortingField.FileName, + 1 when this.currentSortingField is SortingField.Type => SortingField.TypeDescending, + 1 => SortingField.Type, + 2 when this.currentSortingField is SortingField.Size => SortingField.SizeDescending, + 2 => SortingField.Size, + 3 when this.currentSortingField is SortingField.Date => SortingField.DateDescending, + 3 => SortingField.Date, + _ => SortingField.None, + }; } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs index e4747b1e6..608455650 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialog.UI.cs @@ -373,22 +373,8 @@ public partial class FileDialog ImGui.PopID(); if (ImGui.IsItemClicked()) { - if (column == 0) - { - this.SortFields(SortingField.FileName, true); - } - else if (column == 1) - { - this.SortFields(SortingField.Type, true); - } - else if (column == 2) - { - this.SortFields(SortingField.Size, true); - } - else - { - this.SortFields(SortingField.Date, true); - } + var sorting = this.GetNewSorting(column); + this.SortFields(sorting); } } diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index ae9c8ef38..12a391903 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -9,9 +9,15 @@ namespace Dalamud.Interface.ImGuiFileDialog; /// public class FileDialogManager { + /// Gets or sets a function that returns the desired default sort order in the file dialog. + public Func? GetDefaultSortOrder { get; set; } + + /// Gets or sets an action to invoke when a file dialog changes its sort order. + public Action? SetDefaultSortOrder { get; set; } + #pragma warning disable SA1401 /// Additional quick access items for the side bar. - public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = new(); + public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = []; /// Additional flags with which to draw the window. public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None; @@ -189,10 +195,41 @@ public class FileDialogManager this.callback = callback as Action; } + if (this.dialog is not null) + { + this.dialog.SortOrderChanged -= this.OnSortOrderChange; + } + this.dialog = new FileDialog(id, title, filters, path, defaultFileName, defaultExtension, selectionCountMax, isModal, flags); + if (this.GetDefaultSortOrder is not null) + { + try + { + var order = this.GetDefaultSortOrder(); + this.dialog.SortFields(order); + } + catch + { + // ignored. + } + } + + this.dialog.SortOrderChanged += this.OnSortOrderChange; this.dialog.WindowFlags |= this.AddedWindowFlags; foreach (var (name, location, icon, position) in this.CustomSideBarItems) this.dialog.SetQuickAccess(name, location, icon, position); this.dialog.Show(); } + + private void OnSortOrderChange(FileDialog.SortingField sortOrder) + { + try + { + this.SetDefaultSortOrder?.Invoke(sortOrder); + } + catch + { + // ignored. + } + } } From a555514de3cd72d92f1c02412c437a04bee974bb Mon Sep 17 00:00:00 2001 From: Blair Date: Thu, 10 Apr 2025 06:15:09 +1000 Subject: [PATCH 006/106] Show correct changelog in plugin installer window (#2236) --- .../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 dfd37431c..c2efd2d68 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2715,7 +2715,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushID($"installed{index}{plugin.Manifest.InternalName}"); - var applicableChangelog = plugin.IsTesting ? remoteManifest?.Changelog : remoteManifest?.TestingChangelog; + var applicableChangelog = plugin.IsTesting ? remoteManifest?.TestingChangelog : remoteManifest?.Changelog; var hasChangelog = !applicableChangelog.IsNullOrWhitespace(); var didDrawApplicableChangelogInsideCollapsible = false; From 98c5fbd6667367d29d2b5d8b9a50e9bac2fade05 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Wed, 9 Apr 2025 22:16:28 +0200 Subject: [PATCH 007/106] Update AddonEventType (#2238) * Update AddonEventType * Fix copy paste mistake --- Dalamud/Game/Addon/Events/AddonEventType.cs | 223 ++++++++++++++------ 1 file changed, 162 insertions(+), 61 deletions(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventType.cs b/Dalamud/Game/Addon/Events/AddonEventType.cs index 100168e22..ec46c368b 100644 --- a/Dalamud/Game/Addon/Events/AddonEventType.cs +++ b/Dalamud/Game/Addon/Events/AddonEventType.cs @@ -9,150 +9,251 @@ 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 Wheel. + /// + MouseWheel = 8, + /// /// Mouse Click. /// MouseClick = 9, - + + /// + /// Mouse Double Click. + /// + MouseDoubleClick = 10, + /// /// Input Received. /// InputReceived = 12, - + /// /// Focus Start. /// FocusStart = 18, - + /// /// Focus Stop. /// FocusStop = 19, - + /// - /// Button Press, sent on MouseDown on Button. + /// Resize (ChatLogPanel). + /// + Resize = 19, + + /// + /// AtkComponentButton Press, sent on MouseDown on Button. /// ButtonPress = 23, - + /// - /// Button Release, sent on MouseUp and MouseOut. + /// AtkComponentButton Release, sent on MouseUp and MouseOut. /// ButtonRelease = 24, - + /// - /// Button Click, sent on MouseUp and MouseClick on button. + /// AtkComponentButton Click, sent on MouseUp and MouseClick on button. /// ButtonClick = 25, - + /// - /// List Item RollOver. + /// Value Update (NumericInput, ScrollBar, etc.) + /// + ValueUpdate = 27, + + /// + /// AtkComponentSlider Value Update. + /// + SliderValueUpdate = 29, + + /// + /// AtkComponentSlider Released. + /// + SliderReleased = 30, + + /// + /// AtkComponentList RollOver. /// ListItemRollOver = 33, - + /// - /// List Item Roll Out. + /// AtkComponentList Roll Out. /// ListItemRollOut = 34, - + /// - /// List Item Toggle. + /// AtkComponentList Click. /// - ListItemToggle = 35, - + ListItemClick = 35, + /// - /// Drag Drop Begin. + /// AtkComponentList Toggle. + /// + [Obsolete("Use ListItemClick")] + ListItemToggle = 35, + + /// + /// AtkComponentList Double Click. + /// + ListItemDoubleClick = 36, + + /// + /// AtkComponentList Select. + /// + ListItemSelect = 38, + + /// + /// AtkComponentDragDrop Begin. /// Sent on MouseDown over a draggable icon (will NOT send for a locked icon). /// - DragDropBegin = 47, - + DragDropBegin = 50, + /// - /// Drag Drop Insert. + /// AtkComponentDragDrop End. + /// + DragDropEnd = 51, + + /// + /// AtkComponentDragDrop Insert. /// Sent when dropping an icon into a hotbar/inventory slot or similar. /// - DragDropInsert = 50, - + DragDropInsert = 53, + /// - /// Drag Drop Roll Over. + /// AtkComponentDragDrop Roll Over. /// - DragDropRollOver = 52, - + DragDropRollOver = 55, + /// - /// Drag Drop Roll Out. + /// AtkComponentDragDrop Roll Out. /// - DragDropRollOut = 53, - + DragDropRollOut = 56, + /// - /// Drag Drop Discard. + /// AtkComponentDragDrop Discard. /// Sent when dropping an icon into empty screenspace, eg to remove an action from a hotBar. /// - DragDropDiscard = 54, - + DragDropDiscard = 57, + /// /// Drag Drop Unknown. /// - [Obsolete("Use DragDropDiscard")] + [Obsolete("Use DragDropDiscard", true)] DragDropUnk54 = 54, - + /// - /// Drag Drop Cancel. + /// AtkComponentDragDrop Cancel. /// Sent on MouseUp if the cursor has not moved since DragDropBegin, OR on MouseDown over a locked icon. /// - DragDropCancel = 55, - + DragDropCancel = 58, + /// /// Drag Drop Unknown. /// - [Obsolete("Use DragDropCancel")] + [Obsolete("Use DragDropCancel", true)] DragDropUnk55 = 55, - + /// - /// Icon Text Roll Over. + /// AtkComponentIconText Roll Over. /// - IconTextRollOver = 56, - + IconTextRollOver = 59, + /// - /// Icon Text Roll Out. + /// AtkComponentIconText Roll Out. /// - IconTextRollOut = 57, - + IconTextRollOut = 60, + /// - /// Icon Text Click. + /// AtkComponentIconText Click. /// - IconTextClick = 58, - + IconTextClick = 61, + /// - /// Window Roll Over. + /// AtkDialogue Close. /// - WindowRollOver = 67, - + DialogueClose = 62, + /// - /// Window Roll Out. + /// AtkDialogue Submit. /// - WindowRollOut = 68, - + DialogueSubmit = 63, + /// - /// Window Change Scale. + /// AtkTimer Tick. /// - WindowChangeScale = 69, + TimerTick = 64, + + /// + /// AtkTimer End. + /// + TimerEnd = 65, + + /// + /// AtkSimpleTween Progress. + /// + TweenProgress = 67, + + /// + /// AtkSimpleTween Complete. + /// + TweenComplete = 68, + + /// + /// AtkAddonControl Child Addon Attached. + /// + ChildAddonAttached = 69, + + /// + /// AtkComponentWindow Roll Over. + /// + WindowRollOver = 70, + + /// + /// AtkComponentWindow Roll Out. + /// + WindowRollOut = 71, + + /// + /// AtkComponentWindow Change Scale. + /// + WindowChangeScale = 72, + + /// + /// AtkTextNode Link Mouse Click. + /// + LinkMouseClick = 75, + + /// + /// AtkTextNode Link Mouse Over. + /// + LinkMouseOver = 76, + + /// + /// AtkTextNode Link Mouse Out. + /// + LinkMouseOut = 77, } From 26aaf974bc9eb4d0aff236c62779d4f45851090f Mon Sep 17 00:00:00 2001 From: Blair Date: Thu, 10 Apr 2025 06:16:51 +1000 Subject: [PATCH 008/106] Add menu functions to ImRaii (#2227) --- Dalamud/Interface/Utility/Raii/EndObjects.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Dalamud/Interface/Utility/Raii/EndObjects.cs b/Dalamud/Interface/Utility/Raii/EndObjects.cs index 261c071c3..f2fa64349 100644 --- a/Dalamud/Interface/Utility/Raii/EndObjects.cs +++ b/Dalamud/Interface/Utility/Raii/EndObjects.cs @@ -65,6 +65,15 @@ public static partial class ImRaii public static IEndObject Combo(string label, string previewValue, ImGuiComboFlags flags) => new EndConditionally(ImGui.EndCombo, ImGui.BeginCombo(label, previewValue, flags)); + public static IEndObject Menu(string label) + => new EndConditionally(ImGui.EndMenu, ImGui.BeginMenu(label)); + + public static IEndObject MenuBar() + => new EndConditionally(ImGui.EndMenuBar, ImGui.BeginMenuBar()); + + public static IEndObject MainMenuBar() + => new EndConditionally(ImGui.EndMainMenuBar, ImGui.BeginMainMenuBar()); + public static IEndObject Group() { ImGui.BeginGroup(); From 2787e375484d375759d16e5f311098aa04aee2f2 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 12 Apr 2025 12:18:08 +0200 Subject: [PATCH 009/106] build: 12.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 55847cf46..9dc572e36 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.7 + 12.0.0.8 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 708f3d0ab21ae9439a5ecadd753f847c43bee13a Mon Sep 17 00:00:00 2001 From: nebel <9887+nebel@users.noreply.github.com> Date: Tue, 15 Apr 2025 02:54:53 +0900 Subject: [PATCH 010/106] Improve null checking for badly set properties in NamePlateGui (#2245) --- Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs | 6 +++--- Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs index a398bdb82..a721015bb 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs @@ -53,7 +53,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany return; var sb = new SeStringBuilder(); - if (this.OuterWrap is { Item1: var outerLeft }) + if (this.OuterWrap is { Item1: { } outerLeft }) { sb.Append(outerLeft); } @@ -67,7 +67,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany sb.Append(isFreeCompany ? " «" : "《"); } - if (this.TextWrap is { Item1: var left, Item2: var right }) + if (this.TextWrap is { Item1: { } left, Item2: { } right }) { sb.Append(left); sb.Append(this.Text ?? this.GetStrippedField(handler)); @@ -87,7 +87,7 @@ public class NamePlateQuotedParts(NamePlateStringField field, bool isFreeCompany sb.Append(isFreeCompany ? "»" : "》"); } - if (this.OuterWrap is { Item2: var outerRight }) + if (this.OuterWrap is { Item2: { } outerRight }) { sb.Append(outerRight); } diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs index 2906005da..7ec178795 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateSimpleParts.cs @@ -35,7 +35,7 @@ public class NamePlateSimpleParts(NamePlateStringField field) if ((nint)handler.GetFieldAsPointer(field) == NamePlateGui.EmptyStringPointer) return; - if (this.TextWrap is { Item1: var left, Item2: var right }) + if (this.TextWrap is { Item1: { } left, Item2: { } right }) { var sb = new SeStringBuilder(); sb.Append(left); From e52ac55d917ed00860b01ef1c04ed97b9ce16b2b Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Mon, 14 Apr 2025 10:57:56 -0700 Subject: [PATCH 011/106] deps: bump clientstructs (#2243) - fix fate HasExpBonus - remove deprecation from concrete class - kept on interface --- Dalamud/Game/ClientState/Fates/Fate.cs | 5 +++-- lib/FFXIVClientStructs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index f48e66ad6..b81373144 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -3,6 +3,7 @@ using System.Numerics; using Dalamud.Data; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Memory; +using Dalamud.Utility; using Lumina.Excel; @@ -222,8 +223,8 @@ internal unsafe partial class Fate : IFate public byte Progress => this.Struct->Progress; /// - [Obsolete($"Use {nameof(HasBonus)} instead")] - public bool HasExpBonus => this.Struct->IsExpBonus; + [Api13ToDo("Remove")] + public bool HasExpBonus => this.HasBonus; /// public bool HasBonus => this.Struct->IsBonus; diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 9e7f03ed6..f6e517720 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9e7f03ed6d3d5cb9e6952f00c4779ac64427bc81 +Subproject commit f6e51772078bc9ea5768e675c70026659a4525d3 From 184101415503aa3a6c9435bbb6dc3bcb41acef43 Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 14 Apr 2025 21:33:50 +0200 Subject: [PATCH 012/106] build: 12.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 9dc572e36..8198101fa 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.8 + 12.0.0.9 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 2f0c57d5ad591301bbb3c4310e3b45a38e3dfae3 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 15 Apr 2025 19:12:07 +0200 Subject: [PATCH 013/106] Reassign leftover Api12ToDo to Api13ToDo --- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 30 +++++++++---------- .../SeStringHandling/Payloads/ItemPayload.cs | 2 +- Dalamud/Utility/Api12ToDoAttribute.cs | 24 --------------- 3 files changed, 16 insertions(+), 40 deletions(-) delete mode 100644 Dalamud/Utility/Api12ToDoAttribute.cs diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index 49a2cbb73..dc5f5f048 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -17,32 +17,32 @@ public interface IReadOnlyDtrBarEntry /// Gets the title of this entry. /// public string Title { get; } - + /// /// Gets a value indicating whether this entry has a click action. /// public bool HasClickAction { get; } - + /// /// Gets the text of this entry. /// public SeString? Text { get; } - + /// /// Gets a tooltip to be shown when the user mouses over the dtr entry. /// public SeString? Tooltip { get; } - + /// /// Gets a value indicating whether this entry should be shown. /// public bool Shown { get; } - + /// /// Gets a value indicating whether or not the user has hidden this entry from view through the Dalamud settings. /// public bool UserHidden { get; } - + /// /// Triggers the click action of this entry. /// @@ -59,22 +59,22 @@ public interface IDtrBarEntry : IReadOnlyDtrBarEntry /// Gets or sets the text of this entry. /// public new SeString? Text { get; set; } - + /// /// Gets or sets a tooltip to be shown when the user mouses over the dtr entry. /// public new SeString? Tooltip { get; set; } - + /// /// Gets or sets a value indicating whether this entry is visible. /// public new bool Shown { get; set; } - + /// /// Gets or sets a action to be invoked when the user clicks on the dtr entry. /// public Action? OnClick { get; set; } - + /// /// Remove this entry from the bar. /// You will need to re-acquire it from DtrBar to reuse it. @@ -121,7 +121,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry /// public SeString? Tooltip { get; set; } - + /// /// Gets or sets a action to be invoked when the user clicks on the dtr entry. /// @@ -145,14 +145,14 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry } /// - [Api12ToDo("Maybe make this config scoped to internalname?")] + [Api13ToDo("Maybe make this config scoped to internalname?")] public bool UserHidden => this.configuration.DtrIgnore?.Contains(this.Title) ?? false; /// /// Gets or sets the internal text node of this entry. /// internal AtkTextNode* TextNode { get; set; } - + /// /// Gets or sets the storage for the text of this entry. /// @@ -171,7 +171,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry /// /// Gets or sets a value indicating whether this entry has just been added. /// - internal bool Added { get; set; } + internal bool Added { get; set; } /// /// Gets or sets the plugin that owns this entry. @@ -183,7 +183,7 @@ internal sealed unsafe class DtrBarEntry : IDisposable, IDtrBarEntry { if (this.OnClick == null) return false; - + this.OnClick.Invoke(); return true; } diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs index d6fd897b8..56372e0f9 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs @@ -75,7 +75,7 @@ public class ItemPayload : Payload /// /// Kinds of items that can be fetched from this payload. /// - [Api12ToDo("Move this out of ItemPayload. It's used in other classes too.")] + [Api13ToDo("Move this out of ItemPayload. It's used in other classes too.")] public enum ItemKind : uint { /// diff --git a/Dalamud/Utility/Api12ToDoAttribute.cs b/Dalamud/Utility/Api12ToDoAttribute.cs deleted file mode 100644 index 9f871274d..000000000 --- a/Dalamud/Utility/Api12ToDoAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Dalamud.Utility; - -/// -/// Utility class for marking something to be changed for API 11, for ease of lookup. -/// -[AttributeUsage(AttributeTargets.All, Inherited = false)] -internal sealed class Api12ToDoAttribute : Attribute -{ - /// - /// Marks that this should be made internal. - /// - public const string MakeInternal = "Make internal."; - - /// - /// Initializes a new instance of the class. - /// - /// The explanation. - /// The explanation 2. - public Api12ToDoAttribute(string what, string what2 = "") - { - _ = what; - _ = what2; - } -} From 3f724170b24f6dc0c811255f91324b4fded56436 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 15 Apr 2025 21:14:24 +0200 Subject: [PATCH 014/106] Fix a warning --- Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index 12a391903..db490315f 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -16,12 +16,15 @@ public class FileDialogManager public Action? SetDefaultSortOrder { get; set; } #pragma warning disable SA1401 - /// Additional quick access items for the side bar. +#pragma warning disable SA1201 + /// Additional quick access items for the sidebar. public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = []; + /// Additional flags with which to draw the window. public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None; #pragma warning restore SA1401 +#pragma warning restore SA1201 private FileDialog? dialog; private Action? callback; From b8101541256dd066dc10ccbfb7e8cbad38f77241 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 15 Apr 2025 21:29:18 +0200 Subject: [PATCH 015/106] Add MinimumDalamudVersion to manifest, validate at install, update and load --- .../ImGuiFileDialog/FileDialogManager.cs | 1 - .../PluginInstaller/PluginInstallerWindow.cs | 37 +++++++++++++++---- Dalamud/Plugin/Internal/PluginManager.cs | 1 + Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 4 ++ .../Types/Manifest/IPluginManifest.cs | 17 ++++++--- .../Plugin/Internal/Types/PluginManifest.cs | 4 ++ Dalamud/Utility/Util.cs | 11 +++++- 7 files changed, 58 insertions(+), 17 deletions(-) diff --git a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs index db490315f..5b466cba2 100644 --- a/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs +++ b/Dalamud/Interface/ImGuiFileDialog/FileDialogManager.cs @@ -20,7 +20,6 @@ public class FileDialogManager /// Additional quick access items for the sidebar. public readonly List<(string Name, string Path, FontAwesomeIcon Icon, int Position)> CustomSideBarItems = []; - /// Additional flags with which to draw the window. public ImGuiWindowFlags AddedWindowFlags = ImGuiWindowFlags.None; #pragma warning restore SA1401 diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index c2efd2d68..6de4473e6 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -228,6 +228,7 @@ internal class PluginInstallerWindow : Window, IDisposable IsInstallableOutdated = 1 << 5, IsOrphan = 1 << 6, IsTesting = 1 << 7, + IsIncompatible = 1 << 8, } private enum InstalledPluginListFilter @@ -2139,7 +2140,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.PushStyleVar(ImGuiStyleVar.Alpha, overlayAlpha); if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); - else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan)) + else if ((flags.HasFlag(PluginHeaderFlags.HasTrouble) && !pluginDisabled) || flags.HasFlag(PluginHeaderFlags.IsOrphan) || flags.HasFlag(PluginHeaderFlags.IsIncompatible)) ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize); else if (flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) ImGui.Image(this.imageCache.OutdatedInstallableIcon.ImGuiHandle, iconSize); @@ -2215,9 +2216,14 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SetCursorPos(cursor); // Outdated warning - if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) + if (flags.HasFlag(PluginHeaderFlags.IsIncompatible)) { - ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); + ImGui.TextWrapped(Locs.PluginBody_Incompatible); + } + else if (plugin is { IsOutdated: true, IsBanned: false } || flags.HasFlag(PluginHeaderFlags.IsInstallableOutdated)) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed); var bodyText = Locs.PluginBody_Outdated + " "; if (flags.HasFlag(PluginHeaderFlags.UpdateAvailable)) @@ -2226,7 +2232,6 @@ internal class PluginInstallerWindow : Window, IDisposable bodyText += Locs.PluginBody_Outdated_WaitForUpdate; ImGui.TextWrapped(bodyText); - ImGui.PopStyleColor(); } else if (plugin is { IsBanned: true }) { @@ -2395,6 +2400,14 @@ internal class PluginInstallerWindow : Window, IDisposable var effectiveApiLevel = useTesting && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel; var isOutdated = effectiveApiLevel < PluginManager.DalamudApiLevel; + var isIncompatible = manifest.MinimumDalamudVersion != null && + manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed; + + var enableInstallButton = this.updateStatus != OperationStatus.InProgress && + this.installStatus != OperationStatus.InProgress && + !isOutdated && + !isIncompatible; + // Check for valid versions if ((useTesting && manifest.TestingAssemblyVersion == null) || manifest.AssemblyVersion == null) { @@ -2419,6 +2432,11 @@ internal class PluginInstallerWindow : Window, IDisposable label += Locs.PluginTitleMod_TestingAvailable; } + if (isIncompatible) + { + label += Locs.PluginTitleMod_Incompatible; + } + var isThirdParty = manifest.SourceRepo.IsThirdParty; ImGui.PushID($"available{index}{manifest.InternalName}"); @@ -2432,6 +2450,8 @@ internal class PluginInstallerWindow : Window, IDisposable flags |= PluginHeaderFlags.IsInstallableOutdated; if (useTesting || manifest.IsTestingExclusive) flags |= PluginHeaderFlags.IsTesting; + if (isIncompatible) + flags |= PluginHeaderFlags.IsIncompatible; if (this.DrawPluginCollapsingHeader(label, null, manifest, flags, () => this.DrawAvailablePluginContextMenu(manifest), index)) { @@ -2459,9 +2479,6 @@ internal class PluginInstallerWindow : Window, IDisposable ImGuiHelpers.ScaledDummy(5); - // Controls - var disabled = this.updateStatus == OperationStatus.InProgress || this.installStatus == OperationStatus.InProgress || isOutdated; - var versionString = useTesting ? $"{manifest.TestingAssemblyVersion}" : $"{manifest.AssemblyVersion}"; @@ -2470,7 +2487,7 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGuiComponents.DisabledButton(Locs.PluginButton_SafeMode); } - else if (disabled) + else if (!enableInstallButton) { ImGuiComponents.DisabledButton(Locs.PluginButton_InstallVersion(versionString)); } @@ -4049,6 +4066,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginTitleMod_TestingAvailable => Loc.Localize("InstallerTestingAvailable", " (has testing version)"); + public static string PluginTitleMod_Incompatible => Loc.Localize("InstallerTitleModIncompatible", " (incompatible)"); + public static string PluginTitleMod_DevPlugin => Loc.Localize("InstallerDevPlugin", " (dev plugin)"); public static string PluginTitleMod_UpdateFailed => Loc.Localize("InstallerUpdateFailed", " (update failed)"); @@ -4105,6 +4124,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginBody_Outdated => Loc.Localize("InstallerOutdatedPluginBody ", "This plugin is outdated and incompatible."); + public static string PluginBody_Incompatible => Loc.Localize("InstallerIncompatiblePluginBody ", "This plugin is incompatible with your version of Dalamud. Please attempt to restart your game."); + 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."); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index a20f87241..a33910825 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1774,6 +1774,7 @@ internal class PluginManager : IInternalDisposableService var updates = this.AvailablePlugins .Where(remoteManifest => plugin.Manifest.InternalName == remoteManifest.InternalName) .Where(remoteManifest => plugin.Manifest.InstalledFromUrl == remoteManifest.SourceRepo.PluginMasterUrl || !remoteManifest.SourceRepo.IsThirdParty) + .Where(remoteManifest => remoteManifest.MinimumDalamudVersion == null || Util.AssemblyVersionParsed >= remoteManifest.MinimumDalamudVersion) .Where(remoteManifest => { var useTesting = this.UseTesting(remoteManifest); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 1b9025538..3c8875cf3 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -15,6 +15,7 @@ using Dalamud.Plugin.Internal.Exceptions; using Dalamud.Plugin.Internal.Loader; using Dalamud.Plugin.Internal.Profiles; using Dalamud.Plugin.Internal.Types.Manifest; +using Dalamud.Utility; namespace Dalamud.Plugin.Internal.Types; @@ -312,6 +313,9 @@ internal class LocalPlugin : IAsyncDisposable if (!this.CheckPolicy()) throw new PluginPreconditionFailedException($"Unable to load {this.Name} as a load policy forbids it"); + if (this.Manifest.MinimumDalamudVersion != null && this.Manifest.MinimumDalamudVersion > Util.AssemblyVersionParsed) + throw new PluginPreconditionFailedException($"Unable to load {this.Name}, Dalamud version is lower than minimum required version {this.Manifest.MinimumDalamudVersion}"); + this.State = PluginState.Loading; Log.Information($"Loading {this.DllFile.Name}"); diff --git a/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs index 4b0951397..5ab5abbc1 100644 --- a/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/Manifest/IPluginManifest.cs @@ -16,7 +16,7 @@ public interface IPluginManifest /// Gets the public name of the plugin. /// public string Name { get; } - + /// /// Gets a punchline of the plugins functions. /// @@ -26,7 +26,7 @@ public interface IPluginManifest /// Gets the author/s of the plugin. /// public string Author { get; } - + /// /// Gets a value indicating whether the plugin can be unloaded asynchronously. /// @@ -41,17 +41,22 @@ public interface IPluginManifest /// Gets the assembly version of the plugin's testing variant. /// public Version? TestingAssemblyVersion { get; } - + + /// + /// Gets the minimum Dalamud assembly version this plugin requires. + /// + public Version? MinimumDalamudVersion { get; } + /// /// Gets the DIP17 channel name. /// public string? Dip17Channel { get; } - + /// /// Gets the last time this plugin was updated. /// public long LastUpdate { get; } - + /// /// Gets a changelog, null if none exists. /// @@ -88,7 +93,7 @@ public interface IPluginManifest /// Gets an URL to the website or source code of the plugin. /// public string? RepoUrl { get; } - + /// /// Gets a description of the plugins functions. /// diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index 01951c8a6..26cb642f6 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -75,6 +75,10 @@ internal record PluginManifest : IPluginManifest [JsonConverter(typeof(GameVersionConverter))] public GameVersion? ApplicableVersion { get; init; } = GameVersion.Any; + /// + [JsonProperty] + public Version? MinimumDalamudVersion { get; init; } + /// [JsonProperty] public int DalamudApiLevel { get; init; } = PluginManager.DalamudApiLevel; diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 966fa1e11..99f83dd03 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -70,10 +70,17 @@ public static class Util private static ulong moduleEndAddr; /// - /// Gets the assembly version of Dalamud. + /// Gets the Dalamud version. /// + [Api13ToDo("Remove. Make both versions here internal. Add an API somewhere.")] public static string AssemblyVersion { get; } = - Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); + Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!.ToString(); + + /// + /// Gets the Dalamud version. + /// + internal static Version AssemblyVersionParsed { get; } = + Assembly.GetAssembly(typeof(ChatHandlers))!.GetName().Version!; /// /// Gets the SCM Version from the assembly, or null if it cannot be found. This method will generally return From af1eb275cf8255e0d1db6c9af0be1080c0481917 Mon Sep 17 00:00:00 2001 From: Cytraen <60638768+Cytraen@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:33:28 -0400 Subject: [PATCH 016/106] sort by search score in plugin installer if opened w/ search text (#2246) --- .../PluginInstaller/PluginInstallerWindow.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index c2efd2d68..eca3672c2 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -285,9 +285,13 @@ internal class PluginInstallerWindow : Window, IDisposable _ = pluginManager.ReloadPluginMastersAsync(); Service.Get().ScanDevPlugins(); - if (!this.isSearchTextPrefilled) this.searchText = string.Empty; - this.sortKind = PluginSortKind.Alphabetical; - this.filterText = Locs.SortBy_Alphabetical; + if (!this.isSearchTextPrefilled) + { + this.searchText = string.Empty; + this.sortKind = PluginSortKind.Alphabetical; + this.filterText = Locs.SortBy_Alphabetical; + } + this.adaptiveSort = true; if (this.updateStatus == OperationStatus.Complete || this.updateStatus == OperationStatus.Idle) @@ -363,11 +367,20 @@ internal class PluginInstallerWindow : Window, IDisposable { this.isSearchTextPrefilled = false; this.searchText = string.Empty; + if (this.sortKind == PluginSortKind.SearchScore) + { + this.sortKind = PluginSortKind.Alphabetical; + this.filterText = Locs.SortBy_Alphabetical; + this.ResortPlugins(); + } } else { this.isSearchTextPrefilled = true; this.searchText = text; + this.sortKind = PluginSortKind.SearchScore; + this.filterText = Locs.SortBy_SearchScore; + this.ResortPlugins(); } } From cb8d9cc3971928e1ce7dd204e2ea2f4481818ee9 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 15 Apr 2025 21:43:08 +0200 Subject: [PATCH 017/106] Replace update message with link to changelog ...instead of pointing to Discord --- Dalamud/Game/ChatHandlers.cs | 25 +++++++++++++++---- Dalamud/Game/Gui/ChatGui.cs | 1 + Dalamud/Interface/DalamudWindowOpenKinds.cs | 13 +++++++--- .../PluginInstaller/PluginInstallerWindow.cs | 6 +++++ 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index c40744ca4..064667596 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -8,6 +8,8 @@ using Dalamud.Configuration.Internal; using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; @@ -100,8 +102,6 @@ internal partial class ChatHandlers : IServiceType if (chatGui == null || pluginManager == null || dalamudInterface == null) return; - var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); - if (this.configuration.PrintDalamudWelcomeMsg) { chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud {0} loaded."), Util.GetScmVersion()) @@ -116,15 +116,30 @@ internal partial class ChatHandlers : IServiceType } } - if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion)) + if (string.IsNullOrEmpty(this.configuration.LastVersion) || !Util.AssemblyVersion.StartsWith(this.configuration.LastVersion)) { + var linkPayload = chatGui.AddChatLinkHandler( + "dalamud", + 8459324, + (_, _) => dalamudInterface.OpenPluginInstallerTo(PluginInstallerOpenKind.Changelogs)); + + var updateMessage = new SeStringBuilder() + .AddText(Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully!")) + .AddUiForeground(500) + .AddText(" [") + .Add(linkPayload) + .AddText(Loc.Localize("DalamudClickToViewChangelogs", " Click here to view the changelog.")) + .Add(RawPayload.LinkTerminator) + .AddText("]") + .AddUiForegroundOff(); + chatGui.Print(new XivChatEntry { - Message = Loc.Localize("DalamudUpdated", "Dalamud has been updated successfully! Please check the discord for a full changelog."), + Message = updateMessage.Build(), Type = XivChatType.Notice, }); - this.configuration.LastVersion = assemblyVersion; + this.configuration.LastVersion = Util.AssemblyVersion; this.configuration.QueueSave(); } diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 791cbb97a..721070d9b 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -220,6 +220,7 @@ internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui /// The ID of the command to run. /// The command action itself. /// A payload for handling. + [Api13ToDo("Plugins should not specify their own command IDs here. We should assign them ourselves.")] internal DalamudLinkPayload AddChatLinkHandler(string pluginName, uint commandId, Action commandAction) { var payload = new DalamudLinkPayload { Plugin = pluginName, CommandId = commandId }; diff --git a/Dalamud/Interface/DalamudWindowOpenKinds.cs b/Dalamud/Interface/DalamudWindowOpenKinds.cs index 588ff858b..35d2825f7 100644 --- a/Dalamud/Interface/DalamudWindowOpenKinds.cs +++ b/Dalamud/Interface/DalamudWindowOpenKinds.cs @@ -14,16 +14,21 @@ public enum PluginInstallerOpenKind /// Open to the "Installed Plugins" page. /// InstalledPlugins, - + /// /// Open to the "Can be updated" page. /// UpdateablePlugins, /// - /// Open to the "Changelogs" page. + /// Open to the "Plugin Changelogs" page. /// Changelogs, + + /// + /// Open to the "Dalamud Changelogs" page. + /// + DalamudChangelogs, } /// @@ -35,12 +40,12 @@ public enum SettingsOpenKind /// Open to the "General" page. /// General, - + /// /// Open to the "Look & Feel" page. /// LookAndFeel, - + /// /// Open to the "Auto Updates" page. /// diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index eca3672c2..317fa86d0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -501,6 +501,12 @@ internal class PluginInstallerWindow : Window, IDisposable // Plugins category this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.All; break; + case PluginInstallerOpenKind.DalamudChangelogs: + // Changelog group + this.categoryManager.CurrentGroupKind = PluginCategoryManager.GroupKind.Changelog; + // Dalamud category + this.categoryManager.CurrentCategoryKind = PluginCategoryManager.CategoryKind.DalamudChangelogs; + break; default: throw new ArgumentOutOfRangeException(nameof(kind), kind, null); } From 81ced564d6f964321d6d6a59544ad12c01fefdaf Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 15 Apr 2025 23:44:10 +0200 Subject: [PATCH 018/106] Better ImGui setup condition (#2249) * Better ImGui setup condition * Fix build --- Dalamud/Interface/Internal/InterfaceManager.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index ed27a1043..786bc4589 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -34,8 +34,6 @@ using Dalamud.Plugin.Services; using Dalamud.Utility; using Dalamud.Utility.Timing; -using FFXIVClientStructs.FFXIV.Client.Graphics.Environment; - using ImGuiNET; using ImGuiScene; @@ -47,6 +45,8 @@ using PInvoke; using TerraFX.Interop.DirectX; using TerraFX.Interop.Windows; +using CSFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework; + // general dev notes, here because it's easiest /* @@ -524,7 +524,9 @@ internal partial class InterfaceManager : IInternalDisposableService // Some graphics drivers seem to consider the game's shader cache as invalid if we hook too early. // The game loads shader packages on the file thread and then compiles them. It will show the logo once it is done. // This is a workaround, but it fixes an issue where the game would take a very long time to get to the title screen. - if (EnvManager.Instance() == null) + // NetworkModuleProxy is set up after lua scripts are loaded (EventFramework.LoadState >= 5), which can only happen + // after the shaders are compiled (if necessary) and loaded. AgentLobby.Update doesn't do much until this condition is met. + if (CSFramework.Instance()->GetNetworkModuleProxy() == null) return; this.SetupHooks(Service.Get(), Service.Get()); From 672793b6c0697f9bba4b79da920fbcab597525b9 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 17 Apr 2025 17:14:25 +0200 Subject: [PATCH 019/106] Add hook stress test --- Dalamud/Hooking/Internal/CallHook.cs | 24 ++- .../Windows/Data/Widgets/HookWidget.cs | 181 +++++++++++++++++- 2 files changed, 189 insertions(+), 16 deletions(-) diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs index c9b5562ba..5b438b5a8 100644 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -15,10 +15,10 @@ namespace Dalamud.Hooking.Internal; /// 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 +internal class CallHook : IDalamudHook where T : Delegate { private readonly Reloaded.Hooks.AsmHook asmHook; - + private T? detour; private bool activated; @@ -29,7 +29,10 @@ internal class CallHook : IDisposable where T : Delegate /// Delegate to invoke. internal CallHook(nint address, T detour) { + ArgumentNullException.ThrowIfNull(detour); + this.detour = detour; + this.Address = address; var detourPtr = Marshal.GetFunctionPointerForDelegate(this.detour); var code = new[] @@ -38,14 +41,14 @@ internal class CallHook : IDisposable where T : Delegate $"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); } @@ -53,7 +56,16 @@ internal class CallHook : IDisposable where T : Delegate /// Gets a value indicating whether or not the hook is enabled. /// public bool IsEnabled => this.asmHook.IsEnabled; - + + /// + public IntPtr Address { get; } + + /// + public string BackendName => "Reloaded AsmHook"; + + /// + public bool IsDisposed => this.detour == null; + /// /// Starts intercepting a call to the function. /// @@ -65,7 +77,7 @@ internal class CallHook : IDisposable where T : Delegate this.asmHook.Activate(); return; } - + this.asmHook.Enable(); } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs index b24587d6c..ec5f12d6e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/HookWidget.cs @@ -1,6 +1,15 @@ -using System.Runtime.InteropServices; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Dalamud.Game; +using Dalamud.Game.Addon.Lifecycle; using Dalamud.Hooking; +using Dalamud.Hooking.Internal; + +using FFXIVClientStructs.FFXIV.Component.GUI; + using ImGuiNET; using PInvoke; using Serilog; @@ -10,23 +19,46 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// /// Widget for displaying hook information. /// -internal class HookWidget : IDataWindowWidget +internal unsafe class HookWidget : IDataWindowWidget { + private readonly List hookStressTestList = []; + private Hook? messageBoxMinHook; private bool hookUseMinHook; - + + private int hookStressTestCount = 0; + private int hookStressTestMax = 1000; + private int hookStressTestWait = 100; + private int hookStressTestMaxDegreeOfParallelism = 10; + private StressTestHookTarget hookStressTestHookTarget = StressTestHookTarget.Random; + private bool hookStressTestRunning = false; + + private MessageBoxWDelegate? messageBoxWOriginal; + private AddonFinalizeDelegate? addonFinalizeOriginal; + + private AddonLifecycleAddressResolver? address; + private delegate int MessageBoxWDelegate( IntPtr hWnd, [MarshalAs(UnmanagedType.LPWStr)] string text, [MarshalAs(UnmanagedType.LPWStr)] string caption, NativeFunctions.MessageBoxType type); - + + private delegate void AddonFinalizeDelegate(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase); + + private enum StressTestHookTarget + { + MessageBoxW, + AddonFinalize, + Random, + } + /// - public string DisplayName { get; init; } = "Hook"; + public string DisplayName { get; init; } = "Hook"; /// public string[]? CommandShortcuts { get; init; } = { "hook" }; - + /// public bool Ready { get; set; } @@ -34,6 +66,9 @@ internal class HookWidget : IDataWindowWidget public void Load() { this.Ready = true; + + this.address = new AddonLifecycleAddressResolver(); + this.address.Setup(Service.Get()); } /// @@ -41,7 +76,9 @@ internal class HookWidget : IDataWindowWidget { try { - ImGui.Checkbox("Use MinHook", ref this.hookUseMinHook); + ImGui.Checkbox("Use MinHook (only for regular hooks, AsmHook is Reloaded-only)", ref this.hookUseMinHook); + + ImGui.Separator(); if (ImGui.Button("Create")) this.messageBoxMinHook = Hook.FromSymbol("User32", "MessageBoxW", this.MessageBoxWDetour, this.hookUseMinHook); @@ -66,18 +103,94 @@ internal class HookWidget : IDataWindowWidget if (this.messageBoxMinHook != null) ImGui.Text("Enabled: " + this.messageBoxMinHook?.IsEnabled); + + ImGui.Separator(); + + ImGui.BeginDisabled(this.hookStressTestRunning); + ImGui.Text("Stress Test"); + + if (ImGui.InputInt("Max", ref this.hookStressTestMax)) + this.hookStressTestCount = 0; + + ImGui.InputInt("Wait (ms)", ref this.hookStressTestWait); + ImGui.InputInt("Max Degree of Parallelism", ref this.hookStressTestMaxDegreeOfParallelism); + + if (ImGui.BeginCombo("Target", HookTargetToString(this.hookStressTestHookTarget))) + { + foreach (var target in Enum.GetValues()) + { + if (ImGui.Selectable(HookTargetToString(target), this.hookStressTestHookTarget == target)) + this.hookStressTestHookTarget = target; + } + + ImGui.EndCombo(); + } + + if (ImGui.Button("Stress Test")) + { + Task.Run(() => + { + this.hookStressTestRunning = true; + this.hookStressTestCount = 0; + Parallel.For( + 0, + this.hookStressTestMax, + new ParallelOptions + { + MaxDegreeOfParallelism = this.hookStressTestMaxDegreeOfParallelism, + }, + _ => + { + this.hookStressTestList.Add(this.HookTarget(this.hookStressTestHookTarget)); + this.hookStressTestCount++; + Thread.Sleep(this.hookStressTestWait); + }); + }).ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error(t.Exception, "Stress test failed"); + } + else + { + Log.Information("Stress test completed"); + } + + this.hookStressTestRunning = false; + this.hookStressTestList.ForEach(hook => + { + hook.Dispose(); + }); + this.hookStressTestList.Clear(); + }); + } + + ImGui.EndDisabled(); + + ImGui.TextUnformatted("Status: " + (this.hookStressTestRunning ? "Running" : "Idle")); + ImGui.ProgressBar(this.hookStressTestCount / (float)this.hookStressTestMax, new System.Numerics.Vector2(0, 0), $"{this.hookStressTestCount}/{this.hookStressTestMax}"); } catch (Exception ex) { - Log.Error(ex, "MinHook error caught"); + Log.Error(ex, "Hook error caught"); } } - + + private static string HookTargetToString(StressTestHookTarget target) + { + return target switch + { + StressTestHookTarget.MessageBoxW => "MessageBoxW (Hook)", + StressTestHookTarget.AddonFinalize => "AddonFinalize (Hook)", + _ => target.ToString(), + }; + } + private int MessageBoxWDetour(IntPtr hwnd, string text, string caption, NativeFunctions.MessageBoxType type) { Log.Information("[DATAHOOK] {Hwnd} {Text} {Caption} {Type}", hwnd, text, caption, type); - var result = this.messageBoxMinHook!.Original(hwnd, "Cause Access Violation?", caption, NativeFunctions.MessageBoxType.YesNo); + var result = this.messageBoxWOriginal!(hwnd, "Cause Access Violation?", caption, NativeFunctions.MessageBoxType.YesNo); if (result == (int)User32.MessageBoxResult.IDYES) { @@ -86,4 +199,52 @@ internal class HookWidget : IDataWindowWidget return result; } + + private void OnAddonFinalize(AtkUnitManager* unitManager, AtkUnitBase** atkUnitBase) + { + Log.Information("OnAddonFinalize"); + this.addonFinalizeOriginal!(unitManager, atkUnitBase); + } + + private void OnAddonUpdate(AtkUnitBase* thisPtr, float delta) + { + Log.Information("OnAddonUpdate"); + } + + private IDalamudHook HookMessageBoxW() + { + var hook = Hook.FromSymbol( + "User32", + "MessageBoxW", + this.MessageBoxWDetour, + this.hookUseMinHook); + + this.messageBoxWOriginal = hook.Original; + hook.Enable(); + return hook; + } + + private IDalamudHook HookAddonFinalize() + { + var hook = Hook.FromAddress(this.address!.AddonFinalize, this.OnAddonFinalize); + + this.addonFinalizeOriginal = hook.Original; + hook.Enable(); + return hook; + } + + private IDalamudHook HookTarget(StressTestHookTarget target) + { + if (target == StressTestHookTarget.Random) + { + target = (StressTestHookTarget)Random.Shared.Next(0, 2); + } + + return target switch + { + StressTestHookTarget.MessageBoxW => this.HookMessageBoxW(), + StressTestHookTarget.AddonFinalize => this.HookAddonFinalize(), + _ => throw new ArgumentOutOfRangeException(nameof(target), target, null), + }; + } } From c7cc771ee2b4c4a2d51daf7cdb03a83dc91016b9 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 20 Apr 2025 15:16:46 +0200 Subject: [PATCH 020/106] Upgrade Reloaded.Hooks/Reloaded.Assembler to address concurrency problems --- Dalamud/Dalamud.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 8198101fa..c99e2a860 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -60,8 +60,8 @@ - - + + From 39ac9f9dadea6ac391f96f7584f1221495982b8d Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 21 Apr 2025 14:01:59 +0200 Subject: [PATCH 021/106] Make ItemUtil public (#2252) --- Dalamud/Utility/ItemUtil.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Dalamud/Utility/ItemUtil.cs b/Dalamud/Utility/ItemUtil.cs index 32160aa15..0b37a6abb 100644 --- a/Dalamud/Utility/ItemUtil.cs +++ b/Dalamud/Utility/ItemUtil.cs @@ -14,14 +14,14 @@ namespace Dalamud.Utility; /// /// Utilities related to Items. /// -internal static class ItemUtil +public static class ItemUtil { private static int? eventItemRowCount; /// Converts raw item ID to item ID with its classification. /// Raw item ID. /// Item ID and its classification. - internal static (uint ItemId, ItemKind Kind) GetBaseId(uint rawItemId) + public static (uint ItemId, ItemKind Kind) GetBaseId(uint rawItemId) { if (IsEventItem(rawItemId)) return (rawItemId, ItemKind.EventItem); // EventItem IDs are NOT adjusted if (IsHighQuality(rawItemId)) return (rawItemId - 1_000_000, ItemKind.Hq); @@ -33,7 +33,7 @@ internal static class ItemUtil /// Item ID. /// Item classification. /// Raw Item ID. - internal static uint GetRawId(uint itemId, ItemKind kind) + public static uint GetRawId(uint itemId, ItemKind kind) { return kind switch { @@ -50,7 +50,7 @@ internal static class ItemUtil /// The item id to check. /// true when the item id belongs to a normal item. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsNormalItem(uint itemId) + public static bool IsNormalItem(uint itemId) { return itemId < 500_000; } @@ -61,7 +61,7 @@ internal static class ItemUtil /// The item id to check. /// true when the item id belongs to a collectible item. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsCollectible(uint itemId) + public static bool IsCollectible(uint itemId) { return itemId is >= 500_000 and < 1_000_000; } @@ -72,7 +72,7 @@ internal static class ItemUtil /// The item id to check. /// true when the item id belongs to a high quality item. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsHighQuality(uint itemId) + public static bool IsHighQuality(uint itemId) { return itemId is >= 1_000_000 and < 2_000_000; } @@ -83,7 +83,7 @@ internal static class ItemUtil /// The item id to check. /// true when the item id belongs to an event item. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool IsEventItem(uint itemId) + public static bool IsEventItem(uint itemId) { return itemId >= 2_000_000 && itemId - 2_000_000 < (eventItemRowCount ??= Service.Get().GetExcelSheet().Count); } @@ -95,7 +95,7 @@ internal static class ItemUtil /// Whether to include the High Quality or Collectible icon. /// An optional client language override. /// The item name. - internal static ReadOnlySeString GetItemName(uint itemId, bool includeIcon = true, ClientLanguage? language = null) + public static ReadOnlySeString GetItemName(uint itemId, bool includeIcon = true, ClientLanguage? language = null) { var dataManager = Service.Get(); @@ -145,7 +145,7 @@ internal static class ItemUtil /// The raw item Id. /// Wheather this color is used as edge color. /// The Color row id. - internal static uint GetItemRarityColorType(uint itemId, bool isEdgeColor = false) + public static uint GetItemRarityColorType(uint itemId, bool isEdgeColor = false) { var rarity = 1u; From 61a17dac28b42987fa8210a435598ca8bb136817 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 21 Apr 2025 14:02:26 +0200 Subject: [PATCH 022/106] SeString Creator and Evaluator fixes (#2250) * Fix SeString Creator example * SeStringEvaluator: Don't print auto translation brackets for categories * SeStringEvaluator: Fix map id mask MapId is a ushort, not a byte. --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 8 ++++---- .../Windows/Data/Widgets/SeStringCreatorWidget.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index e5bf1d7e6..05ad3c496 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -1066,8 +1066,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (!enu.MoveNext() || !this.TryResolveUInt(in context, enu.Current, out var placeNameIdInt)) return false; - var instance = packedIds >> 0x10; - var mapId = packedIds & 0xFF; + var instance = packedIds >> 16; + var mapId = packedIds & 0xFFFF; if (this.dataManager.GetExcelSheet(context.Language) .TryGetRow(territoryTypeId, out var territoryTypeRow)) @@ -1355,8 +1355,6 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator var group = (uint)(e0Val + 1); var rowId = (uint)e1Val; - using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55); - if (!this.dataManager.GetExcelSheet(context.Language).TryGetFirst( row => row.Group == group && !row.LookupTable.IsEmpty, out var groupRow)) @@ -1381,6 +1379,8 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator return true; } + using var icons = new SeStringBuilderIconWrap(context.Builder, 54, 55); + // CategoryDataCache if (lookupTable.Equals("#")) { diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index 2175b8be1..71d4277a7 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -135,8 +135,8 @@ internal class SeStringCreatorWidget : IDataWindowWidget new TextEntry(TextEntryType.Macro, ""), new TextEntry(TextEntryType.Macro, ""), new TextEntry(TextEntryType.String, "Dalamud"), - new TextEntry(TextEntryType.Macro, ""), - new TextEntry(TextEntryType.Macro, ""), + new TextEntry(TextEntryType.Macro, ""), + new TextEntry(TextEntryType.Macro, ""), new TextEntry(TextEntryType.Macro, " "), ]; From aea62732e5ce9add17dbe7da5864b2c54753b1b4 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 22 Apr 2025 16:34:19 +0200 Subject: [PATCH 023/106] Safer AddonEventManager node removal (#2232) * Remove events on Framework thread * Make sure the addon is not unloaded * Cleanup node finding loop * Wait for PluginEventController to be disposed --- Dalamud/Game/Addon/Events/AddonEventManager.cs | 9 ++++++--- .../Game/Addon/Events/PluginEventController.cs | 15 ++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index dce2a7e73..a7241dd58 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Collections.Concurrent; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; @@ -230,8 +230,11 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo { this.eventManagerService.ResetCursor(); } - - this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId); + + Service.Get().RunOnFrameworkThread(() => + { + this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId); + }).Wait(); } /// diff --git a/Dalamud/Game/Addon/Events/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs index 403a812db..f32c7ad8f 100644 --- a/Dalamud/Game/Addon/Events/PluginEventController.cs +++ b/Dalamud/Game/Addon/Events/PluginEventController.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Dalamud.Game.Gui; using Dalamud.Logging.Internal; -using Dalamud.Memory; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -139,17 +138,19 @@ internal unsafe class PluginEventController : IDisposable // 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) + // Make sure the addon is not unloaded var atkUnitBase = (AtkUnitBase*)currentAddonPointer; + if (atkUnitBase->UldManager.LoadedState == AtkLoadState.Unloaded) return; + + // Does this addon contain the node this event is for? (by address) var nodeFound = false; - foreach (var index in Enumerable.Range(0, atkUnitBase->UldManager.NodeListCount)) + foreach (var node in atkUnitBase->UldManager.Nodes) { - 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) + if ((nint)node.Value == eventEntry.Node) { nodeFound = true; + break; } } From cf5a6f2635489e57ca649020624d68e7ddb00a96 Mon Sep 17 00:00:00 2001 From: Tupae <71929744+Etupa@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:34:36 +0200 Subject: [PATCH 024/106] Add a command line for multimonitor toogle (#2254) * Update DalamudCommands.cs Added a /xlmulti command for toggling multimonitor. Using Dalamud local build encounters Assertion Failure with Umbra plugin enabled. Otherwise works fine. * Update DalamudCommands.cs replaced /xlmulti with /xltogglemultimonitor to be more explicit --------- Co-authored-by: Fractal --- Dalamud/Interface/Internal/DalamudCommands.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Dalamud/Interface/Internal/DalamudCommands.cs b/Dalamud/Interface/Internal/DalamudCommands.cs index 636f71bfa..a3b28d4a8 100644 --- a/Dalamud/Interface/Internal/DalamudCommands.cs +++ b/Dalamud/Interface/Internal/DalamudCommands.cs @@ -152,6 +152,11 @@ internal class DalamudCommands : IServiceType "DalamudCopyLogHelp", "Copy the dalamud.log file to your clipboard."), }); + // Add the new command handler for toggling multi-monitor option + commandManager.AddHandler("/xltogglemultimonitor", new CommandInfo(this.OnToggleMultiMonitorCommand) + { + HelpMessage = Loc.Localize("DalamudToggleMultiMonitorHelp", "Toggle multi-monitor windows."), + }); } private void OnUnloadCommand(string command, string arguments) @@ -416,4 +421,19 @@ internal class DalamudCommands : IServiceType : Loc.Localize("DalamudLogCopyFailure", "Could not copy log file to clipboard."); chatGui.Print(message); } + + private void OnToggleMultiMonitorCommand(string command, string arguments) + { + var configuration = Service.Get(); + var chatGui = Service.Get(); + + configuration.IsDisableViewport = !configuration.IsDisableViewport; + configuration.QueueSave(); + + var message = configuration.IsDisableViewport + ? Loc.Localize("DalamudMultiMonitorDisabled", "Multi-monitor windows disabled.") + : Loc.Localize("DalamudMultiMonitorEnabled", "Multi-monitor windows enabled."); + + chatGui.Print(message); + } } From 4995383fcc5d435df2c7bf00ae579bf37731fb45 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 22 Apr 2025 16:44:43 +0200 Subject: [PATCH 025/106] Update ClientStructs (#2244) 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 f6e517720..aae44a712 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit f6e51772078bc9ea5768e675c70026659a4525d3 +Subproject commit aae44a7126bfa583529cb94cd7377b5c7617a5c1 From a9299b4aeaeda7fc770b4940d0e9d6b64757d5fb Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Tue, 22 Apr 2025 21:03:06 +0200 Subject: [PATCH 026/106] Add Dataset to FlyTextKind enum (#2255) --- Dalamud/Game/Gui/FlyText/FlyTextKind.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs index 0edbd09ee..407410e85 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs @@ -94,10 +94,15 @@ public enum FlyTextKind : int /// /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. - /// Added in 7.2, usage currently unknown. /// + [Obsolete("Use Dataset instead", true)] Unknown16 = 16, + /// + /// Val1 in serif font next to all caps condensed font Text1 with Text2 in sans-serif as subtitle. + /// + Dataset = 16, + /// /// Val1 in serif font, Text2 in sans-serif as subtitle. /// Added in 7.2, usage currently unknown. From d881fea12b599c622f35651c2f24fbccb1bb3f4e Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 22 Apr 2025 21:53:23 +0200 Subject: [PATCH 027/106] build: 12.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 c99e2a860..562206764 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.9 + 12.0.0.10 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 096f9c3e52544c602d60cdfb594df0d5f1ebf3d1 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 24 Apr 2025 19:51:53 +0200 Subject: [PATCH 028/106] Update ClientStructs (#2256) 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 aae44a712..b1e0fc768 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit aae44a7126bfa583529cb94cd7377b5c7617a5c1 +Subproject commit b1e0fc7685f13bdd33f202ff83beedd3eba82325 From e29171cc99e428f550aa6517d975342481f53be8 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 24 Apr 2025 21:58:54 +0200 Subject: [PATCH 029/106] Fix race condition in plugin load When opening the installer while boot plugins are still loaded, it may have been possible for plugins to be added to the installed plugins list twice, causing various statekeeping issues --- .../Interface/Internal/DalamudInterface.cs | 4 +- .../PluginInstaller/PluginInstallerWindow.cs | 4 +- .../Widgets/DevPluginsSettingsEntry.cs | 2 +- Dalamud/Plugin/Internal/PluginManager.cs | 84 ++++++++----------- .../Plugin/Internal/Types/LocalDevPlugin.cs | 8 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 63 ++++++++------ 6 files changed, 83 insertions(+), 82 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 5d21e954a..42907016f 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -46,6 +46,8 @@ using ImPlotNET; using PInvoke; using Serilog.Events; +using Task = System.Threading.Tasks.Task; + namespace Dalamud.Interface.Internal; /// @@ -1013,7 +1015,7 @@ internal class DalamudInterface : IInternalDisposableService if (ImGui.MenuItem("Scan dev plugins")) { - pluginManager.ScanDevPlugins(); + Task.Run(pluginManager.ScanDevPluginsAsync); } ImGui.Separator(); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 317fa86d0..6868827b4 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -283,7 +283,7 @@ internal class PluginInstallerWindow : Window, IDisposable var pluginManager = Service.Get(); _ = pluginManager.ReloadPluginMastersAsync(); - Service.Get().ScanDevPlugins(); + _ = pluginManager.ScanDevPluginsAsync(); if (!this.isSearchTextPrefilled) { @@ -782,7 +782,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.SameLine(); if (ImGui.Button(Locs.FooterButton_ScanDevPlugins)) { - pluginManager.ScanDevPlugins(); + _ = pluginManager.ScanDevPluginsAsync(); } } diff --git a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs index 4c5dc8b83..d7f8b1ca1 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Widgets/DevPluginsSettingsEntry.cs @@ -52,7 +52,7 @@ public class DevPluginsSettingsEntry : SettingsEntry if (this.devPluginLocationsChanged) { - Service.Get().ScanDevPlugins(); + _ = Service.Get().ScanDevPluginsAsync(); this.devPluginLocationsChanged = false; } } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index a20f87241..28fc1fcb1 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -776,7 +776,8 @@ internal class PluginManager : IInternalDisposableService /// only shown as disabled in the installed plugins window. This is a modified version of LoadAllPlugins that works /// a little differently. /// - public void ScanDevPlugins() + /// A representing the asynchronous operation. This function generally will not block as new plugins aren't loaded. + public async Task ScanDevPluginsAsync() { // devPlugins are more freeform. Look for any dll and hope to get lucky. var devDllFiles = new List(); @@ -823,8 +824,7 @@ internal class PluginManager : IInternalDisposableService try { // Add them to the list and let the user decide, nothing is auto-loaded. - this.LoadPluginAsync(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true) - .Wait(); + await this.LoadPluginAsync(dllFile, manifest, PluginLoadReason.Installer, isDev: true, doNotLoad: true); listChanged = true; } catch (InvalidPluginException) @@ -1188,32 +1188,20 @@ internal class PluginManager : IInternalDisposableService { // Testing exclusive if (manifest.IsTestingExclusive && !this.configuration.DoPluginTest) - { - Log.Verbose($"Testing exclusivity: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}"); return false; - } // Applicable version if (manifest.ApplicableVersion < this.dalamud.StartInfo.GameVersion) - { - Log.Verbose($"Game version: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}"); return false; - } // API level - we keep the API before this in the installer to show as "outdated" var effectiveApiLevel = this.UseTesting(manifest) && manifest.TestingDalamudApiLevel != null ? manifest.TestingDalamudApiLevel.Value : manifest.DalamudApiLevel; if (effectiveApiLevel < DalamudApiLevel - 1 && !this.LoadAllApiLevels) - { - Log.Verbose($"API Level: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}"); return false; - } // Banned if (this.IsManifestBanned(manifest)) - { - Log.Verbose($"Banned: {manifest.InternalName} - {manifest.AssemblyVersion} - {manifest.TestingAssemblyVersion}"); return false; - } return true; } @@ -1572,6 +1560,8 @@ internal class PluginManager : IInternalDisposableService /// The loaded plugin. private async Task LoadPluginAsync(FileInfo dllFile, LocalPluginManifest manifest, PluginLoadReason reason, bool isDev = false, bool isBoot = false, bool doNotLoad = false) { + // TODO: Split this function - it should only take care of adding the plugin to the list, not loading itself, that should be done through the plugin instance + var loadPlugin = !doNotLoad; LocalPlugin? plugin; @@ -1582,21 +1572,34 @@ internal class PluginManager : IInternalDisposableService throw new Exception("No internal name"); } - if (isDev) + // Track the plugin as soon as it is instantiated to prevent it from being loaded twice, + // if the installer or DevPlugin scanner is attempting to add plugins while we are still loading boot plugins + lock (this.pluginListLock) { - Log.Information("Loading dev plugin {Name}", manifest.InternalName); - plugin = new LocalDevPlugin(dllFile, manifest); + // Check if this plugin is already loaded + if (this.installedPluginsList.Any(lp => lp.DllFile.FullName == dllFile.FullName)) + throw new InvalidOperationException("Plugin at the provided path is already loaded"); - // This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet - // TODO(goat): Re-enable this when we have better tracing for what was rendering when - // this.configuration.ImGuiAssertsEnabledAtStartup ??= true; - } - else - { - Log.Information("Loading plugin {Name}", manifest.InternalName); - plugin = new LocalPlugin(dllFile, manifest); + if (isDev) + { + Log.Information("Loading dev plugin {Name}", manifest.InternalName); + plugin = new LocalDevPlugin(dllFile, manifest); + + // This is a dev plugin - turn ImGui asserts on by default if we haven't chosen yet + // TODO(goat): Re-enable this when we have better tracing for what was rendering when + // this.configuration.ImGuiAssertsEnabledAtStartup ??= true; + } + else + { + Log.Information("Loading plugin {Name}", manifest.InternalName); + plugin = new LocalPlugin(dllFile, manifest); + } + + this.installedPluginsList.Add(plugin); } + Log.Verbose("Starting to load plugin {Name} at {FileLocation}", manifest.InternalName, dllFile.FullName); + // 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 @@ -1697,43 +1700,34 @@ internal class PluginManager : IInternalDisposableService catch (BannedPluginException) { // Out of date plugins get added so they can be updated. - Log.Information($"Plugin was banned, adding anyways: {dllFile.Name}"); + Log.Information("{InternalName}: Plugin was banned, adding anyways", plugin.Manifest.InternalName); } catch (Exception ex) { if (plugin.IsDev) { // Dev plugins always get added to the list so they can be fiddled with in the UI - Log.Information(ex, $"Dev plugin failed to load, adding anyways: {dllFile.Name}"); - - // NOTE(goat): This can't work - plugins don't "unload" if they fail to load. - // plugin.Disable(); // Disable here, otherwise you can't enable+load later + Log.Information(ex, "{InternalName}: Dev plugin failed to load", plugin.Manifest.InternalName); } else if (plugin.IsOutdated) { // Out of date plugins get added, so they can be updated. - Log.Information(ex, $"Plugin was outdated, adding anyways: {dllFile.Name}"); + Log.Information(ex, "{InternalName}: Plugin was outdated", plugin.Manifest.InternalName); } else if (plugin.IsOrphaned) { // Orphaned plugins get added, so that users aren't confused. - Log.Information(ex, $"Plugin was orphaned, adding anyways: {dllFile.Name}"); + Log.Information(ex, "{InternalName}: Plugin was orphaned", plugin.Manifest.InternalName); } else if (isBoot) { // During boot load, plugins always get added to the list so they can be fiddled with in the UI - Log.Information(ex, $"Regular plugin failed to load, adding anyways: {dllFile.Name}"); - - // NOTE(goat): This can't work - plugins don't "unload" if they fail to load. - // plugin.Disable(); // Disable here, otherwise you can't enable+load later + Log.Information(ex, "{InternalName}: Regular plugin failed to load", plugin.Manifest.InternalName); } else if (!plugin.CheckPolicy()) { // During boot load, plugins always get added to the list so they can be fiddled with in the UI - Log.Information(ex, $"Plugin not loaded due to policy, adding anyways: {dllFile.Name}"); - - // NOTE(goat): This can't work - plugins don't "unload" if they fail to load. - // plugin.Disable(); // Disable here, otherwise you can't enable+load later + Log.Information(ex, "{InternalName}: Plugin not loaded due to policy", plugin.Manifest.InternalName); } else { @@ -1742,14 +1736,6 @@ internal class PluginManager : IInternalDisposableService } } - if (plugin == null) - throw new Exception("Plugin was null when adding to list"); - - lock (this.pluginListLock) - { - this.installedPluginsList.Add(plugin); - } - // Mark as finished loading if (manifest.LoadSync) this.StartupLoadTracking?.Finish(manifest.InternalName); diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 581bfd724..b8f2b2708 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -15,7 +15,7 @@ namespace Dalamud.Plugin.Internal.Types; /// This class represents a dev plugin and all facets of its lifecycle. /// The DLL on disk, dependencies, loaded assembly, etc. /// -internal class LocalDevPlugin : LocalPlugin +internal sealed class LocalDevPlugin : LocalPlugin { private static readonly ModuleLog Log = new("PLUGIN"); @@ -41,7 +41,7 @@ internal class LocalDevPlugin : LocalPlugin configuration.DevPluginSettings[dllFile.FullName] = this.devSettings = new DevPluginSettings(); configuration.QueueSave(); } - + // Legacy dev plugins might not have this! if (this.devSettings.WorkingPluginId == Guid.Empty) { @@ -85,7 +85,7 @@ internal class LocalDevPlugin : LocalPlugin } } } - + /// /// Gets an ID uniquely identifying this specific instance of a devPlugin. /// @@ -152,7 +152,7 @@ internal class LocalDevPlugin : LocalPlugin if (manifestPath.Exists) this.manifest = LocalPluginManifest.Load(manifestPath) ?? throw new Exception("Could not reload manifest."); } - + /// protected override void OnPreReload() { diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 1b9025538..144e09b47 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -62,12 +62,13 @@ internal class LocalPlugin : IAsyncDisposable } this.DllFile = dllFile; - this.State = PluginState.Unloaded; // Although it is conditionally used here, we need to set the initial value regardless. this.manifestFile = LocalPluginManifest.GetManifestFile(this.DllFile); this.manifest = manifest; + this.State = PluginState.Unloaded; + var needsSaveDueToLegacyFiles = false; // This converts from the ".disabled" file feature to the manifest instead. @@ -352,19 +353,13 @@ internal class LocalPlugin : IAsyncDisposable } this.loader.Reload(); + this.RefreshAssemblyInformation(); } - // Load the assembly - this.pluginAssembly ??= this.loader.LoadDefaultAssembly(); - - this.AssemblyName = this.pluginAssembly.GetName(); - - // Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor. - this.pluginType ??= this.pluginAssembly.GetTypes() - .First(type => type.IsAssignableTo(typeof(IDalamudPlugin))); + Log.Verbose("{Name} ({Guid}): Have type", this.InternalName, this.EffectiveWorkingPluginId); // Check for any loaded plugins with the same assembly name - var assemblyName = this.pluginAssembly.GetName().Name; + var assemblyName = this.pluginAssembly!.GetName().Name; foreach (var otherPlugin in pluginManager.InstalledPlugins) { // During hot-reloading, this plugin will be in the plugin list, and the instance will have been disposed @@ -376,7 +371,7 @@ internal class LocalPlugin : IAsyncDisposable if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null) { this.State = PluginState.Unloaded; - Log.Debug($"Duplicate assembly: {this.Name}"); + Log.Debug("Duplicate assembly: {Name}", this.InternalName); throw new DuplicatePluginException(assemblyName); } @@ -392,7 +387,7 @@ internal class LocalPlugin : IAsyncDisposable this.instance = await CreatePluginInstance( this.manifest, this.serviceScope, - this.pluginType, + this.pluginType!, this.dalamudInterface); this.State = PluginState.Loaded; Log.Information("Finished loading {PluginName}", this.InternalName); @@ -620,42 +615,60 @@ internal class LocalPlugin : IAsyncDisposable throw; } + this.RefreshAssemblyInformation(); + } + + private void RefreshAssemblyInformation() + { + if (this.loader == null) + throw new InvalidOperationException("No loader available"); + try { this.pluginAssembly = this.loader.LoadDefaultAssembly(); + this.AssemblyName = this.pluginAssembly.GetName(); } catch (Exception ex) { - this.pluginAssembly = null; - this.pluginType = null; - this.loader.Dispose(); - + this.ResetLoader(); Log.Error(ex, $"Not a plugin: {this.DllFile.FullName}"); throw new InvalidPluginException(this.DllFile); } + if (this.pluginAssembly == null) + { + this.ResetLoader(); + Log.Error("Plugin assembly is null: {DllFileFullName}", 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))); + this.ResetLoader(); + Log.Error(ex, "Could not load one or more types when searching for IDalamudPlugin: {DllFileFullName}", this.DllFile.FullName); + throw; } - if (this.pluginType == default) + if (this.pluginType == null) { - this.pluginAssembly = null; - this.pluginType = null; - this.loader.Dispose(); - - Log.Error($"Nothing inherits from IDalamudPlugin: {this.DllFile.FullName}"); + this.ResetLoader(); + Log.Error("Nothing inherits from IDalamudPlugin: {DllFileFullName}", this.DllFile.FullName); throw new InvalidPluginException(this.DllFile); } } + private void ResetLoader() + { + this.pluginAssembly = null; + this.pluginType = null; + this.loader?.Dispose(); + this.loader = null; + } + /// Clears and disposes all resources associated with the plugin instance. /// Whether to clear and dispose . /// Exceptions, if any occurred. From f482badd8ee009cabb2568195669249aefd9d011 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 25 Apr 2025 18:53:30 +0200 Subject: [PATCH 030/106] Add progress bar to boot plugin loads, show which are pending --- .../Interface/Internal/DalamudInterface.cs | 7 +- .../PluginInstaller/PluginInstallerWindow.cs | 69 ++++++++++++------- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 42907016f..f010e3496 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -9,7 +9,6 @@ using System.Runtime.InteropServices; using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Console; -using Dalamud.Data; using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; @@ -46,12 +45,10 @@ using ImPlotNET; using PInvoke; using Serilog.Events; -using Task = System.Threading.Tasks.Task; - namespace Dalamud.Interface.Internal; /// -/// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping. +/// This plugin implements all the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping. /// [ServiceManager.EarlyLoadedService] internal class DalamudInterface : IInternalDisposableService @@ -1015,7 +1012,7 @@ internal class DalamudInterface : IInternalDisposableService if (ImGui.MenuItem("Scan dev plugins")) { - Task.Run(pluginManager.ScanDevPluginsAsync); + _ = pluginManager.ScanDevPluginsAsync(); } ImGui.Separator(); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 6868827b4..091142bc3 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -473,6 +473,36 @@ internal class PluginInstallerWindow : Window, IDisposable configuration.QueueSave(); } + private static void DrawProgressBar(IEnumerable items, Func pendingFunc, Func totalFunc, Action renderPending) + { + var windowSize = ImGui.GetWindowSize(); + + var numLoaded = 0; + var total = 0; + + var itemsArray = items as T[] ?? items.ToArray(); + var allPending = itemsArray.Where(pendingFunc) + .ToArray(); + var allLoadedOrLoading = itemsArray.Count(totalFunc); + + // Cap number of items we show to avoid clutter + const int maxShown = 3; + foreach (var repo in allPending.Take(maxShown)) + { + renderPending(repo); + } + + ImGuiHelpers.ScaledDummy(10); + + numLoaded += allLoadedOrLoading - allPending.Length; + total += allLoadedOrLoading; + if (numLoaded != total) + { + ImGui.SetCursorPosX(windowSize.X / 3); + ImGui.ProgressBar(numLoaded / (float)total, new Vector2(windowSize.X / 3, 50), $"{numLoaded}/{total}"); + } + } + private void SetOpenPage(PluginInstallerOpenKind kind) { switch (kind) @@ -576,40 +606,29 @@ internal class PluginInstallerWindow : Window, IDisposable if (pluginManager.PluginsReady && !pluginManager.ReposReady) { ImGuiHelpers.CenteredText("Loading repositories..."); + ImGuiHelpers.ScaledDummy(10); + + DrawProgressBar(pluginManager.Repos, x => x.State != PluginRepositoryState.Success && + x.State != PluginRepositoryState.Fail && + x.IsEnabled, + x => x.IsEnabled, + x => ImGuiHelpers.CenteredText($"Loading {x.PluginMasterUrl}")); } else if (!pluginManager.PluginsReady && pluginManager.ReposReady) { ImGuiHelpers.CenteredText("Loading installed plugins..."); + ImGuiHelpers.ScaledDummy(10); + + DrawProgressBar(pluginManager.InstalledPlugins, x => x.State == PluginState.Loading, + x => x.State is PluginState.Loaded or + PluginState.LoadError or + PluginState.Loading, + x => ImGuiHelpers.CenteredText($"Loading {x.Name}")); } else { ImGuiHelpers.CenteredText("Loading repositories and plugins..."); } - - var currentProgress = 0; - var total = 0; - - var pendingRepos = pluginManager.Repos.ToArray() - .Where(x => (x.State != PluginRepositoryState.Success && - x.State != PluginRepositoryState.Fail) && - x.IsEnabled) - .ToArray(); - var allRepoCount = - pluginManager.Repos.Count(x => x.State != PluginRepositoryState.Fail && x.IsEnabled); - - foreach (var repo in pendingRepos) - { - ImGuiHelpers.CenteredText($"{repo.PluginMasterUrl}: {repo.State}"); - } - - currentProgress += allRepoCount - pendingRepos.Length; - total += allRepoCount; - - if (currentProgress != total) - { - ImGui.SetCursorPosX(windowSize.X / 3); - ImGui.ProgressBar(currentProgress / (float)total, new Vector2(windowSize.X / 3, 50)); - } } break; From b996ff5e10aeb3554545c53599da2ec394138255 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 25 Apr 2025 19:01:56 +0200 Subject: [PATCH 031/106] Theme progress bars and plots by default --- Dalamud/Interface/Style/StyleModelV1.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Dalamud/Interface/Style/StyleModelV1.cs b/Dalamud/Interface/Style/StyleModelV1.cs index 43341126e..ca3f74942 100644 --- a/Dalamud/Interface/Style/StyleModelV1.cs +++ b/Dalamud/Interface/Style/StyleModelV1.cs @@ -101,8 +101,8 @@ public class StyleModelV1 : StyleModel { "DockingEmptyBg", new Vector4(0.2f, 0.2f, 0.2f, 1) }, { "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1) }, { "PlotLinesHovered", new Vector4(1, 0.43f, 0.35f, 1) }, - { "PlotHistogram", new Vector4(0.9f, 0.7f, 0, 1) }, - { "PlotHistogramHovered", new Vector4(1, 0.6f, 0, 1) }, + { "PlotHistogram", new Vector4(0.578199f, 0.16989735f, 0.16989735f, 0.78431374f) }, + { "PlotHistogramHovered", new Vector4(0.7819905f, 0.12230185f, 0.12230185f, 0.78431374f) }, { "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1) }, { "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 1) }, { "TableBorderLight", new Vector4(0.23f, 0.23f, 0.25f, 1) }, @@ -220,8 +220,8 @@ public class StyleModelV1 : StyleModel { "DockingEmptyBg", new Vector4(0.2f, 0.2f, 0.2f, 1f) }, { "PlotLines", new Vector4(0.61f, 0.61f, 0.61f, 1f) }, { "PlotLinesHovered", new Vector4(1f, 0.43f, 0.35f, 1f) }, - { "PlotHistogram", new Vector4(0.9f, 0.7f, 0f, 1f) }, - { "PlotHistogramHovered", new Vector4(1f, 0.6f, 0f, 1f) }, + { "PlotHistogram", new Vector4(0.578199f, 0.16989735f, 0.16989735f, 0.78431374f) }, + { "PlotHistogramHovered", new Vector4(0.7819905f, 0.12230185f, 0.12230185f, 0.78431374f) }, { "TableHeaderBg", new Vector4(0.19f, 0.19f, 0.2f, 1f) }, { "TableBorderStrong", new Vector4(0.31f, 0.31f, 0.35f, 1f) }, { "TableBorderLight", new Vector4(0.23f, 0.23f, 0.25f, 1f) }, From ce49b0d51fffc61257285c2108ca3a1e2d358eff Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 25 Apr 2025 20:59:44 +0200 Subject: [PATCH 032/106] SeStringEvaluator: fallback to games ClientLanguage (#2261) --- .../Game/Text/Evaluator/SeStringEvaluator.cs | 27 ++++++++++++++----- .../Data/Widgets/SeStringCreatorWidget.cs | 3 ++- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 05ad3c496..2ccb3ff47 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -51,6 +51,9 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator { private static readonly ModuleLog Log = new("SeStringEvaluator"); + [ServiceManager.ServiceDependency] + private readonly ClientState.ClientState clientState = Service.Get(); + [ServiceManager.ServiceDependency] private readonly DataManager dataManager = Service.Get(); @@ -92,7 +95,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator if (str.IsTextOnly()) return new(str); - var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + var lang = language ?? this.GetEffectiveClientLanguage(); // TODO: remove culture info toggling after supporting CultureInfo for SeStringBuilder.Append, // and then remove try...finally block (discard builder from the pool on exception) @@ -116,7 +119,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator Span localParameters = default, ClientLanguage? language = null) { - var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + var lang = language ?? this.GetEffectiveClientLanguage(); if (!this.dataManager.GetExcelSheet(lang).TryGetRow(addonId, out var addonRow)) return default; @@ -130,7 +133,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator Span localParameters = default, ClientLanguage? language = null) { - var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + var lang = language ?? this.GetEffectiveClientLanguage(); if (!this.dataManager.GetExcelSheet(lang).TryGetRow(lobbyId, out var lobbyRow)) return default; @@ -144,7 +147,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator Span localParameters = default, ClientLanguage? language = null) { - var lang = language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage(); + var lang = language ?? this.GetEffectiveClientLanguage(); if (!this.dataManager.GetExcelSheet(lang).TryGetRow(logMessageId, out var logMessageRow)) return default; @@ -155,7 +158,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator /// public string EvaluateActStr(ActionKind actionKind, uint id, ClientLanguage? language = null) => this.actStrCache.GetOrAdd( - new(actionKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()), + new(actionKind, id, language ?? this.GetEffectiveClientLanguage()), static (key, t) => t.EvaluateFromAddon(2026, [key.Kind.GetActStrId(key.Id)], key.Language) .ExtractText() .StripSoftHyphen(), @@ -164,7 +167,7 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator /// public string EvaluateObjStr(ObjectKind objectKind, uint id, ClientLanguage? language = null) => this.objStrCache.GetOrAdd( - new(objectKind, id, language ?? this.dalamudConfiguration.EffectiveLanguage.ToClientLanguage()), + new(objectKind, id, language ?? this.GetEffectiveClientLanguage()), static (key, t) => t.EvaluateFromAddon(2025, [key.Kind.GetObjStrId(key.Id)], key.Language) .ExtractText() .StripSoftHyphen(), @@ -183,6 +186,18 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator private static uint ConvertRawToMapPosY(Lumina.Excel.Sheets.Map map, float y) => ConvertRawToMapPos(map, map.OffsetY, y); + private ClientLanguage GetEffectiveClientLanguage() + { + return this.dalamudConfiguration.EffectiveLanguage switch + { + "ja" => ClientLanguage.Japanese, + "en" => ClientLanguage.English, + "de" => ClientLanguage.German, + "fr" => ClientLanguage.French, + _ => this.clientState.ClientLanguage, + }; + } + private SeStringBuilder EvaluateAndAppendTo( SeStringBuilder builder, ReadOnlySeStringSpan str, diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index 71d4277a7..c45e0cdfb 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -6,6 +6,7 @@ using System.Text; using Dalamud.Configuration.Internal; using Dalamud.Data; using Dalamud.Game; +using Dalamud.Game.ClientState; using Dalamud.Game.Text.Evaluator; using Dalamud.Game.Text.Noun.Enums; using Dalamud.Game.Text.SeStringHandling; @@ -168,7 +169,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget /// public void Load() { - this.language = Service.Get().EffectiveLanguage.ToClientLanguage(); + this.language = Service.Get().ClientLanguage; this.UpdateInputString(false); this.Ready = true; } From 0db49a5642ebad67673958cf96a0678513b12691 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 25 Apr 2025 21:43:10 +0200 Subject: [PATCH 033/106] DalamudConfiguration.ForceSave should wait for the save task to exit --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 515556b7e..b22580b73 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -561,6 +561,8 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public void ForceSave() { this.Save(); + this.isSaveQueued = false; + this.writeTask?.GetAwaiter().GetResult(); } /// From 999e3ea57acbf4b652fe7ac3f885311c7da33c6a Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 25 Apr 2025 21:43:27 +0200 Subject: [PATCH 034/106] ForceSave() when enabling safe mode from the exception window --- Dalamud/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index f6ba990e6..47591f821 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -316,7 +316,7 @@ public sealed class EntryPoint Log.Information("User chose to disable plugins on next launch..."); var config = Service.Get(); config.PluginSafeMode = true; - config.QueueSave(); + config.ForceSave(); } Log.CloseAndFlush(); From 08f959444b04384d02f3016c21f424542913ea4f Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 25 Apr 2025 22:12:39 +0200 Subject: [PATCH 035/106] Add "restart in safe mode" button to blocked message --- .../PluginInstaller/PluginInstallerWindow.cs | 26 ++- Dalamud/Interface/Utility/ImGuiHelpers.cs | 174 +++++++++++++++++- 2 files changed, 192 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 091142bc3..83adfa524 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -639,11 +639,27 @@ internal class PluginInstallerWindow : Window, IDisposable throw new ArgumentOutOfRangeException(); } - if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(90) && !pluginManager.PluginsReady) + if (DateTime.Now - this.timeLoaded > TimeSpan.FromSeconds(30) && !pluginManager.PluginsReady) { + ImGuiHelpers.ScaledDummy(10); ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed); ImGuiHelpers.CenteredText("One of your plugins may be blocking the installer."); + ImGuiHelpers.CenteredText("You can try restarting in safe mode, and deleting the plugin."); ImGui.PopStyleColor(); + + ImGuiHelpers.BeginHorizontalButtonGroup() + .Add( + "Restart in Safe Mode", + () => + { + var config = Service.Get(); + config.PluginSafeMode = true; + config.ForceSave(); + Dalamud.RestartGame(); + }) + .SetCentered(true) + .WithHeight(30) + .Draw(); } } } @@ -3136,11 +3152,13 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGuiComponents.DisabledToggleButton(toggleId, this.loadingIndicatorKind == LoadingIndicatorKind.EnablingSingle); } - else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled) + else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled || pluginManager.SafeMode) { ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable); - if (inMultipleProfiles && ImGui.IsItemHovered()) + if (pluginManager.SafeMode && ImGui.IsItemHovered()) + ImGui.SetTooltip(Locs.PluginButtonToolTip_SafeMode); + else if (inMultipleProfiles && ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInSingleProfile); else if (inSingleNonDefaultProfileWhichIsDisabled && ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.PluginButtonToolTip_SingleProfileDisabled(profilesThatWantThisPlugin.First().Name)); @@ -4220,6 +4238,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButtonToolTip_NeedsToBeInSingleProfile => Loc.Localize("InstallerUnloadNeedsToBeInSingleProfile", "This plugin is in more than one collection. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it here, make sure it is only in a single collection."); + public static string PluginButtonToolTip_SafeMode => Loc.Localize("InstallerButtonSafeModeTooltip", "Cannot enable plugins in safe mode."); + public static string PluginButtonToolTip_SingleProfileDisabled(string name) => Loc.Localize("InstallerSingleProfileDisabled", "The collection '{0}' which contains this plugin is disabled.\nPlease enable it in the collections manager to toggle the plugin individually.").Format(name); #endregion diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index d9056fec4..8e9bf2c0f 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -111,7 +111,7 @@ public static class ImGuiHelpers /// /// The size of the indent. public static void ScaledIndent(float size) => ImGui.Indent(size * GlobalScale); - + /// /// Use a relative ImGui.SameLine() from your current cursor position, scaled by the Dalamud global scale. /// @@ -286,7 +286,7 @@ public static class ImGuiHelpers 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); } @@ -450,7 +450,7 @@ public static class ImGuiHelpers /// /// Center the ImGui cursor for a certain text. - /// + /// /// The text to center for. public static void CenterCursorForText(string text) => CenterCursorFor(ImGui.CalcTextSize(text).X); @@ -461,6 +461,12 @@ public static class ImGuiHelpers public static void CenterCursorFor(float itemWidth) => ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + /// + /// Starts a new horizontal button group. + /// + /// The group. + public static HorizontalButtonGroup BeginHorizontalButtonGroup() => new(); + /// /// Allocates memory on the heap using
/// Memory must be freed using . @@ -535,7 +541,7 @@ public static class ImGuiHelpers builder.BuildRanges(out var vec); return new ReadOnlySpan((void*)vec.Data, vec.Size).ToArray(); } - + /// public static ushort[] CreateImGuiRangesFrom(params UnicodeRange[] ranges) => CreateImGuiRangesFrom((IEnumerable)ranges); @@ -618,7 +624,7 @@ public static class ImGuiHelpers ImGuiNative.ImGuiInputTextCallbackData_InsertChars(data, 0, pBuf, pBuf + len); ImGuiNative.ImGuiInputTextCallbackData_SelectAll(data); } - + /// /// Finds the corresponding ImGui viewport ID for the given window handle. /// @@ -891,4 +897,162 @@ public static class ImGuiHelpers set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~GlyphIdMask) | ((uint)value << GlyphIdShift); } } + + /// + /// Class helper for creating a horizontal button group. + /// + public class HorizontalButtonGroup + { + private readonly List buttons = []; + + /// + /// Gets or sets a value indicating whether the buttons should be centered horizontally. + /// + public bool IsCentered { get; set; } = false; + + /// + /// Gets or sets the height of the buttons. If null, the default frame height is used. + /// + public float? Height { get; set; } + + /// + /// Gets or sets the extra margin to add to the inside of each button, before and after the text. + /// If null, the default margin is used. + /// + public float? ExtraMargin { get; set; } + + /// + /// Gets or sets the padding between buttons. If null, the default item spacing is used. + /// + public float? PaddingBetweenButtons { get; set; } + + /// + /// Add a button to the group. + /// + /// The text of the button. + /// The action to perform when the button is pressed. + /// The group. + public HorizontalButtonGroup Add(string text, Action action) + { + this.buttons.Add(new ButtonDef(text, action)); + return this; + } + + /// + /// Sets whether the buttons should be centered horizontally. + /// + /// The value. + /// The group. + public HorizontalButtonGroup SetCentered(bool centered) + { + this.IsCentered = centered; + return this; + } + + /// + /// Sets the height of the buttons. + /// + /// The height. + /// The group. + public HorizontalButtonGroup WithHeight(float height) + { + this.Height = height; + return this; + } + + /// + /// Sets the extra margin to add to the inside of each button, before and after the text. + /// + /// The margin. + /// The group. + public HorizontalButtonGroup WithExtraMargin(float extraMargin) + { + this.ExtraMargin = extraMargin; + return this; + } + + /// + /// Sets the padding between buttons. + /// + /// The padding. + /// The group. + public HorizontalButtonGroup WithPaddingBetweenButtons(float padding) + { + this.PaddingBetweenButtons = padding; + return this; + } + + /// + /// Draw the button group at the current location. + /// + public void Draw() + { + var buttonHeight = this.Height * GlobalScale ?? ImGui.GetFrameHeight(); + var buttonCount = this.buttons.Count; + + if (buttonCount == 0) + return; + + var buttonWidths = new float[buttonCount]; + var totalContentWidth = 0f; + var extraMargin = this.ExtraMargin ?? 0f; + + for (var i = 0; i < buttonCount; i++) + { + var buttonText = this.buttons[i].Text; + buttonWidths[i] = ImGui.CalcTextSize(buttonText).X + (2 * extraMargin) + (2 * ImGui.GetStyle().FramePadding.X); + totalContentWidth += buttonWidths[i]; + } + + var buttonPadding = this.PaddingBetweenButtons ?? ImGui.GetStyle().ItemSpacing.X; + if (buttonCount > 1) + totalContentWidth += buttonPadding * (buttonCount - 1); + + var startX = ImGui.GetCursorPosX(); + if (this.IsCentered) + { + var availWidth = ImGui.GetContentRegionAvail().X; + startX += (availWidth - totalContentWidth) * 0.5f; + ImGui.SetCursorPosX(startX); + } + + var originalSpacing = ImGui.GetStyle().ItemSpacing; + if (this.PaddingBetweenButtons.HasValue) + { + var spacing = originalSpacing; + spacing.X = this.PaddingBetweenButtons.Value; + ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, spacing); + } + + for (var i = 0; i < buttonCount; i++) + { + var buttonDef = this.buttons[i]; + + if (this.ExtraMargin.HasValue) + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(ImGui.GetStyle().FramePadding.X + extraMargin, ImGui.GetStyle().FramePadding.Y)); + + if (this.Height.HasValue) + { + if (ImGui.Button(buttonDef.Text, new Vector2(buttonWidths[i], buttonHeight))) + buttonDef.Action?.Invoke(); + } + else + { + if (ImGui.Button(buttonDef.Text, new Vector2(buttonWidths[i], -1))) + buttonDef.Action?.Invoke(); + } + + if (this.ExtraMargin.HasValue) + ImGui.PopStyleVar(); + + if (i < buttonCount - 1) + ImGui.SameLine(); + } + + if (this.PaddingBetweenButtons.HasValue) + ImGui.PopStyleVar(); + } + + private record ButtonDef(string Text, Action Action); + } } From f4102db488f1c3531e85738146502bd352083d63 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 25 Apr 2025 22:46:30 +0200 Subject: [PATCH 036/106] Add "startup behavior" to profiles Choose between remember, always enable, always disable --- .../PluginInstaller/ProfileManagerWidget.cs | 80 ++++++++++++------- Dalamud/Plugin/Internal/Profiles/Profile.cs | 46 ++++++++--- .../Internal/Profiles/ProfileModelV1.cs | 30 ++++++- 3 files changed, 115 insertions(+), 41 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index ddb89d38c..4c1348a61 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -56,11 +56,11 @@ internal class ProfileManagerWidget this.DrawChoice(); return; } - + var tutorialTitle = Locs.TutorialTitle + "###collectionsTutorWindow"; var tutorialId = ImGui.GetID(tutorialTitle); this.DrawTutorial(tutorialTitle); - + switch (this.mode) { case Mode.Overview: @@ -120,22 +120,22 @@ internal class ProfileManagerWidget ImGuiHelpers.SafeTextWrapped(Locs.TutorialParagraphFour); ImGuiHelpers.ScaledDummy(5); ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommands); - + ImGui.Bullet(); ImGui.SameLine(); ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnable); - + ImGui.Bullet(); ImGui.SameLine(); ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsDisable); - + ImGui.Bullet(); ImGui.SameLine(); ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsToggle); ImGuiHelpers.SafeTextWrapped(Locs.TutorialCommandsEnd); ImGuiHelpers.ScaledDummy(5); - + var buttonWidth = 120f; ImGui.SetCursorPosX((ImGui.GetWindowWidth() - buttonWidth) / 2); if (ImGui.Button("OK", new Vector2(buttonWidth, 40))) @@ -186,14 +186,14 @@ internal class ProfileManagerWidget if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.ImportProfileHint); - + ImGui.SameLine(); ImGuiHelpers.ScaledDummy(5); ImGui.SameLine(); - + if (ImGuiComponents.IconButton(FontAwesomeIcon.Question)) ImGui.OpenPopup(tutorialId); - + if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.TutorialHint); @@ -386,10 +386,19 @@ internal class ProfileManagerWidget ImGuiHelpers.ScaledDummy(5); - var enableAtBoot = profile.AlwaysEnableAtBoot; - if (ImGui.Checkbox(Locs.AlwaysEnableAtBoot, ref enableAtBoot)) + ImGui.TextUnformatted(Locs.StartupBehavior); + if (ImGui.BeginCombo("##startupBehaviorPicker", Locs.PolicyToLocalisedName(profile.StartupPolicy))) { - profile.AlwaysEnableAtBoot = enableAtBoot; + foreach (var policy in Enum.GetValues(typeof(ProfileModelV1.ProfileStartupPolicy)).Cast()) + { + var name = Locs.PolicyToLocalisedName(policy); + if (ImGui.Selectable(name, profile.StartupPolicy == policy)) + { + profile.StartupPolicy = policy; + } + } + + ImGui.EndCombo(); } ImGuiHelpers.ScaledDummy(5); @@ -425,7 +434,7 @@ internal class ProfileManagerWidget ImGui.Image(pic.DevPluginIcon.ImGuiHandle, new Vector2(pluginLineHeight)); ImGui.PopStyleVar(); } - + ImGui.SameLine(); var text = $"{pmPlugin.Name}{(pmPlugin.IsDev ? " (dev plugin" : string.Empty)}"; @@ -448,12 +457,12 @@ internal class ProfileManagerWidget ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (pluginLineHeight / 2) - (textHeight.Y / 2)); ImGui.TextUnformatted(text); - + var firstAvailableInstalled = pm.InstalledPlugins.FirstOrDefault(x => x.InternalName == profileEntry.InternalName); var installable = pm.AvailablePlugins.FirstOrDefault( x => x.InternalName == profileEntry.InternalName && !x.SourceRepo.IsThirdParty); - + if (firstAvailableInstalled != null) { ImGui.Text($"Match to plugin '{firstAvailableInstalled.Name}'?"); @@ -488,7 +497,7 @@ internal class ProfileManagerWidget if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.InstallPlugin); } - + ImGui.SetCursorPos(before); } @@ -554,6 +563,9 @@ internal class ProfileManagerWidget private static class Locs { + public static string StartupBehavior => + Loc.Localize("ProfileManagerStartupBehavior", "Startup behavior"); + public static string TooltipEnableDisable => Loc.Localize("ProfileManagerEnableDisableHint", "Enable/Disable this collection"); @@ -567,9 +579,6 @@ internal class ProfileManagerWidget public static string NoPluginsInProfile => Loc.Localize("ProfileManagerNoPluginsInProfile", "Collection has no plugins!"); - public static string AlwaysEnableAtBoot => - Loc.Localize("ProfileManagerAlwaysEnableAtBoot", "Always enable when game starts"); - public static string DeleteProfileHint => Loc.Localize("ProfileManagerDeleteProfile", "Delete this collection"); public static string CopyToClipboardHint => @@ -608,13 +617,13 @@ internal class ProfileManagerWidget public static string TutorialTitle => Loc.Localize("ProfileManagerTutorial", "About Collections"); - + public static string TutorialParagraphOne => Loc.Localize("ProfileManagerTutorialParagraphOne", "Collections are shareable lists of plugins that can be enabled or disabled in the plugin installer or via chat commands.\nWhen a plugin is part of a collection, it will be enabled if the collection is enabled. If a plugin is part of multiple collections, it will be enabled if one or more collections it is a part of are enabled."); - + public static string TutorialParagraphTwo => Loc.Localize("ProfileManagerTutorialParagraphTwo", "You can add plugins to collections by clicking the plus button when editing a collection on this screen, or by using the button with the toolbox icon on the \"Installed Plugins\" screen."); - + public static string TutorialParagraphThree => Loc.Localize("ProfileManagerTutorialParagraphThree", "If a collection's \"Start on boot\" checkbox is ticked, the collection and the plugins within will be enabled every time the game starts up, even if it has been manually disabled in a prior session."); @@ -623,29 +632,46 @@ internal class ProfileManagerWidget public static string TutorialCommands => Loc.Localize("ProfileManagerTutorialCommands", "You can use the following commands in chat or in macros to manage active collections:"); - + public static string TutorialCommandsEnable => Loc.Localize("ProfileManagerTutorialCommandsEnable", "{0} \"Collection Name\" - Enable a collection").Format(PluginManagementCommandHandler.CommandEnableProfile); public static string TutorialCommandsDisable => Loc.Localize("ProfileManagerTutorialCommandsDisable", "{0} \"Collection Name\" - Disable a collection").Format(PluginManagementCommandHandler.CommandDisableProfile); - + public static string TutorialCommandsToggle => Loc.Localize("ProfileManagerTutorialCommandsToggle", "{0} \"Collection Name\" - Toggle a collection's state").Format(PluginManagementCommandHandler.CommandToggleProfile); - + 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); + + public static string PolicyToLocalisedName(ProfileModelV1.ProfileStartupPolicy policy) + { + return policy switch + { + ProfileModelV1.ProfileStartupPolicy.RememberState => Loc.Localize( + "ProfileManagerRememberState", + "Remember state"), + ProfileModelV1.ProfileStartupPolicy.AlwaysEnable => Loc.Localize( + "ProfileManagerAlwaysEnableAtBoot", + "Always enable at boot"), + ProfileModelV1.ProfileStartupPolicy.AlwaysDisable => Loc.Localize( + "ProfileManagerAlwaysDisableAtBoot", + "Always disable at boot"), + _ => throw new ArgumentOutOfRangeException(nameof(policy), policy, null), + }; + } } } diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 2c254167e..9039004ab 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -33,6 +33,18 @@ internal class Profile this.modelV1 = model as ProfileModelV1 ?? throw new ArgumentException("Model was null or unhandled version"); + // Migrate "policy" + if (this.modelV1.StartupPolicy == null) + { +#pragma warning disable CS0618 + this.modelV1.StartupPolicy = this.modelV1.AlwaysEnableOnBoot + ? ProfileModelV1.ProfileStartupPolicy.AlwaysEnable + : ProfileModelV1.ProfileStartupPolicy.RememberState; +#pragma warning restore CS0618 + + Service.Get().QueueSave(); + } + // We don't actually enable plugins here, PM will do it on bootup if (isDefaultProfile) { @@ -40,10 +52,18 @@ internal class Profile this.IsEnabled = this.modelV1.IsEnabled = true; this.Name = this.modelV1.Name = "DEFAULT"; } - else if (this.modelV1.AlwaysEnableOnBoot && isBoot) + else if (isBoot) { - this.IsEnabled = true; - Log.Verbose("{Guid} set enabled because bootup", this.modelV1.Guid); + if (this.modelV1.StartupPolicy == ProfileModelV1.ProfileStartupPolicy.AlwaysEnable) + { + this.IsEnabled = true; + Log.Verbose("{Guid} set enabled because always enable", this.modelV1.Guid); + } + else if (this.modelV1.StartupPolicy == ProfileModelV1.ProfileStartupPolicy.AlwaysDisable) + { + this.IsEnabled = false; + Log.Verbose("{Guid} set disabled because always disable", this.modelV1.Guid); + } } else if (this.modelV1.IsEnabled) { @@ -72,12 +92,12 @@ internal class Profile /// /// Gets or sets a value indicating whether this profile shall always be enabled at boot. /// - public bool AlwaysEnableAtBoot + public ProfileModelV1.ProfileStartupPolicy StartupPolicy { - get => this.modelV1.AlwaysEnableOnBoot; + get => this.modelV1.StartupPolicy ?? ProfileModelV1.ProfileStartupPolicy.RememberState; set { - this.modelV1.AlwaysEnableOnBoot = value; + this.modelV1.StartupPolicy = value; Service.Get().QueueSave(); } } @@ -164,7 +184,7 @@ internal class Profile 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"); - + lock (this) { var existing = this.modelV1.Plugins.FirstOrDefault(x => x.WorkingPluginId == workingPluginId); @@ -182,9 +202,9 @@ internal class Profile }); } } - + Log.Information("Adding plugin {Plugin}({Guid}) to profile {Profile} with state {State}", internalName, workingPluginId, this.Guid, state); - + // We need to remove this plugin from the default profile, if it declares it. if (!this.IsDefaultProfile && this.manager.DefaultProfile.WantsPlugin(workingPluginId) != null) { @@ -221,7 +241,7 @@ internal class Profile if (!this.modelV1.Plugins.Remove(entry)) throw new Exception("Couldn't remove plugin from model collection"); } - + Log.Information("Removing plugin {Plugin}({Guid}) from profile {Profile}", entry.InternalName, entry.WorkingPluginId, this.Guid); // We need to add this plugin back to the default profile, if we were the last profile to have it. @@ -260,7 +280,7 @@ internal class Profile // 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; @@ -268,7 +288,7 @@ internal class Profile } } } - + Service.Get().QueueSave(); } @@ -319,7 +339,7 @@ internal sealed class PluginNotFoundException : ProfileOperationException : base($"The plugin '{internalName}' was not found in the profile") { } - + /// /// Initializes a new instance of the class. /// diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index 99da4263b..62d0de70c 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -9,6 +9,27 @@ namespace Dalamud.Plugin.Internal.Profiles; ///
public class ProfileModelV1 : ProfileModel { + /// + /// Enum representing the startup policy of a profile. + /// + public enum ProfileStartupPolicy + { + /// + /// Remember the last state of the profile. + /// + RememberState, + + /// + /// Always enable the profile. + /// + AlwaysEnable, + + /// + /// Always disable the profile. + /// + AlwaysDisable, + } + /// /// Gets the prefix of this version. /// @@ -18,8 +39,15 @@ public class ProfileModelV1 : ProfileModel /// Gets or sets a value indicating whether or not this profile should always be enabled at boot. /// [JsonProperty("b")] + [Obsolete("Superseded by StartupPolicy")] public bool AlwaysEnableOnBoot { get; set; } = false; + /// + /// Gets or sets the policy to use when Dalamud is loading. + /// + [JsonProperty("p")] + public ProfileStartupPolicy? StartupPolicy { get; set; } + /// /// Gets or sets a value indicating whether or not this profile is currently enabled. /// @@ -46,7 +74,7 @@ public class ProfileModelV1 : ProfileModel /// Gets or sets the internal name of the plugin. /// public string? InternalName { get; set; } - + /// /// Gets or sets an ID uniquely identifying this specific instance of a plugin. /// From 731d7e0f6e8206a0137bcd1ffc4e6c93766f62b5 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 25 Apr 2025 22:48:26 +0200 Subject: [PATCH 037/106] Un-whether-or-not the codebase --- .../Internal/DalamudConfiguration.cs | 54 ++++----- .../Internal/EnvironmentConfiguration.cs | 2 +- Dalamud/Console/ConsoleEntry.cs | 6 +- Dalamud/Console/ConsoleManager.cs | 108 +++++++++--------- .../Game/Addon/Lifecycle/AddonSetupHook.cs | 12 +- Dalamud/Game/ChatHandlers.cs | 2 +- Dalamud/Game/ClientState/Fates/Fate.cs | 4 +- .../ClientState/JobGauge/Types/BLMGauge.cs | 6 +- .../ClientState/JobGauge/Types/PCTGauge.cs | 16 +-- .../Objects/Enums/CustomizeIndex.cs | 2 +- Dalamud/Game/Gui/Dtr/DtrBar.cs | 2 +- Dalamud/Game/Gui/Dtr/DtrBarEntry.cs | 2 +- Dalamud/Game/Gui/GameGui.cs | 4 +- .../Gui/NamePlate/NamePlateQuotedParts.cs | 2 +- .../MarketBoardItemRequest.cs | 2 +- .../Game/Network/Internal/NetworkHandlers.cs | 2 +- Dalamud/Game/SigScanner.cs | 4 +- Dalamud/Game/TargetSigScanner.cs | 2 +- .../SeStringHandling/Payloads/ItemPayload.cs | 4 +- .../Payloads/UIForegroundPayload.cs | 2 +- .../Payloads/UIGlowPayload.cs | 2 +- .../Text/SeStringHandling/SeStringBuilder.cs | 2 +- Dalamud/Hooking/AsmHook.cs | 6 +- Dalamud/Hooking/Hook.cs | 8 +- Dalamud/Hooking/IDalamudHook.cs | 4 +- Dalamud/Hooking/Internal/CallHook.cs | 2 +- Dalamud/Interface/Animation/Easing.cs | 4 +- .../ImGuiFileDialog/DriveListLoader.cs | 2 +- .../Interface/Internal/DalamudInterface.cs | 2 +- .../Interface/Internal/InterfaceManager.cs | 4 +- .../SelfTest/Steps/LuminaSelfTestStep.cs | 2 +- .../Windows/Settings/SettingsEntry.cs | 6 +- .../TitleScreenMenu/TitleScreenMenuEntry.cs | 2 +- Dalamud/Interface/UiBuilder.cs | 12 +- Dalamud/Interface/Windowing/Window.cs | 14 +-- Dalamud/Logging/Internal/TaskTracker.cs | 8 +- Dalamud/NativeFunctions.cs | 26 ++--- Dalamud/Plugin/DalamudPluginInterface.cs | 2 +- Dalamud/Plugin/IDalamudPluginInterface.cs | 4 +- Dalamud/Plugin/InstalledPluginState.cs | 4 +- .../Internal/AutoUpdate/AutoUpdateManager.cs | 2 +- Dalamud/Plugin/Internal/PluginManager.cs | 4 +- Dalamud/Plugin/Internal/Profiles/Profile.cs | 20 ++-- .../Internal/Profiles/ProfileManager.cs | 10 +- .../Internal/Profiles/ProfileModelV1.cs | 6 +- .../Internal/Profiles/ProfilePluginEntry.cs | 6 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 6 +- .../Plugin/Internal/Types/PluginManifest.cs | 2 +- Dalamud/Plugin/Services/IClientState.cs | 10 +- Dalamud/Plugin/Services/ISigScanner.cs | 20 ++-- Dalamud/Plugin/Services/ITextureProvider.cs | 6 +- Dalamud/SafeMemory.cs | 14 +-- Dalamud/Storage/ReliableFileStorage.cs | 6 +- Dalamud/Support/BugBait.cs | 6 +- Dalamud/Utility/Signatures/NullabilityUtil.cs | 6 +- .../Wrappers/IFieldOrPropertyInfo.cs | 2 +- Dalamud/Utility/Timing/TimingHandle.cs | 2 +- Dalamud/Utility/Util.cs | 8 +- DalamudCrashHandler/miniz.h | 8 +- 59 files changed, 249 insertions(+), 249 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index b22580b73..2766ba681 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -66,12 +66,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public List? BadWords { get; set; } /// - /// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found. + /// Gets or sets a value indicating whether the taskbar should flash once a duty is found. /// public bool DutyFinderTaskbarFlash { get; set; } = true; /// - /// Gets or sets a value indicating whether or not a message should be sent in chat once a duty is found. + /// Gets or sets a value indicating whether a message should be sent in chat once a duty is found. /// public bool DutyFinderChatMessage { get; set; } = true; @@ -101,7 +101,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public XivChatType GeneralChatType { get; set; } = XivChatType.Debug; /// - /// Gets or sets a value indicating whether or not plugin testing builds should be shown. + /// Gets or sets a value indicating whether plugin testing builds should be shown. /// public bool DoPluginTest { get; set; } = false; @@ -116,7 +116,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public List ThirdRepoList { get; set; } = new(); /// - /// Gets or sets a value indicating whether or not a disclaimer regarding third-party repos has been dismissed. + /// Gets or sets a value indicating whether a disclaimer regarding third-party repos has been dismissed. /// public bool? ThirdRepoSpeedbumpDismissed { get; set; } = null; @@ -174,38 +174,38 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public float ImeStateIndicatorOpacity { get; set; } = 1f; /// - /// Gets or sets a value indicating whether or not plugin UI should be hidden. + /// Gets or sets a value indicating whether plugin UI should be hidden. /// public bool ToggleUiHide { get; set; } = true; /// - /// Gets or sets a value indicating whether or not plugin UI should be hidden during cutscenes. + /// Gets or sets a value indicating whether plugin UI should be hidden during cutscenes. /// public bool ToggleUiHideDuringCutscenes { get; set; } = true; /// - /// Gets or sets a value indicating whether or not plugin UI should be hidden during GPose. + /// Gets or sets a value indicating whether plugin UI should be hidden during GPose. /// public bool ToggleUiHideDuringGpose { get; set; } = true; /// - /// Gets or sets a value indicating whether or not a message containing Dalamud's current version and the number of loaded plugins should be sent at login. + /// Gets or sets a value indicating whether a message containing Dalamud's current version and the number of loaded plugins should be sent at login. /// public bool PrintDalamudWelcomeMsg { get; set; } = true; /// - /// Gets or sets a value indicating whether or not a message containing detailed plugin information should be sent at login. + /// Gets or sets a value indicating whether a message containing detailed plugin information should be sent at login. /// public bool PrintPluginsWelcomeMsg { get; set; } = true; /// - /// Gets or sets a value indicating whether or not plugins should be auto-updated. + /// Gets or sets a value indicating whether plugins should be auto-updated. /// [Obsolete("Use AutoUpdateBehavior instead.")] public bool AutoUpdatePlugins { get; set; } /// - /// Gets or sets a value indicating whether or not Dalamud should add buttons to the system menu. + /// Gets or sets a value indicating whether Dalamud should add buttons to the system menu. /// public bool DoButtonsSystemMenu { get; set; } = true; @@ -220,12 +220,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public bool LogSynchronously { get; set; } = false; /// - /// Gets or sets a value indicating whether or not the debug log should scroll automatically. + /// Gets or sets a value indicating whether the debug log should scroll automatically. /// public bool LogAutoScroll { get; set; } = true; /// - /// Gets or sets a value indicating whether or not the debug log should open at startup. + /// Gets or sets a value indicating whether the debug log should open at startup. /// public bool LogOpenAtStartup { get; set; } @@ -240,29 +240,29 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public List LogCommandHistory { get; set; } = new(); /// - /// Gets or sets a value indicating whether or not the dev bar should open at startup. + /// Gets or sets a value indicating whether the dev bar should open at startup. /// public bool DevBarOpenAtStartup { get; set; } /// - /// Gets or sets a value indicating whether or not ImGui asserts should be enabled at startup. + /// Gets or sets a value indicating whether ImGui asserts should be enabled at startup. /// public bool? ImGuiAssertsEnabledAtStartup { get; set; } /// - /// Gets or sets a value indicating whether or not docking should be globally enabled in ImGui. + /// Gets or sets a value indicating whether docking should be globally enabled in ImGui. /// public bool IsDocking { get; set; } /// - /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. + /// Gets or sets a value indicating whether 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; } /// - /// Gets or sets a value indicating whether or not an additional button allowing pinning and clickthrough options should be shown + /// Gets or sets a value indicating whether 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; @@ -273,17 +273,17 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public bool IsDisableViewport { get; set; } = true; /// - /// Gets or sets a value indicating whether or not navigation via a gamepad should be globally enabled in ImGui. + /// Gets or sets a value indicating whether navigation via a gamepad should be globally enabled in ImGui. /// public bool IsGamepadNavigationEnabled { get; set; } = true; /// - /// Gets or sets a value indicating whether or not focus management is enabled. + /// Gets or sets a value indicating whether focus management is enabled. /// public bool IsFocusManagementEnabled { get; set; } = true; /// - /// Gets or sets a value indicating whether or not the anti-anti-debug check is enabled on startup. + /// Gets or sets a value indicating whether the anti-anti-debug check is enabled on startup. /// public bool IsAntiAntiDebugEnabled { get; set; } = false; @@ -298,7 +298,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public string? DalamudBetaKind { get; set; } /// - /// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started. + /// Gets or sets a value indicating whether any plugin should be loaded when the game is started. /// It is reset immediately when read. /// public bool PluginSafeMode { get; set; } @@ -310,7 +310,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public int? PluginWaitBeforeFree { get; set; } /// - /// Gets or sets a value indicating whether or not crashes during shutdown should be reported. + /// Gets or sets a value indicating whether crashes during shutdown should be reported. /// public bool ReportShutdownCrashes { get; set; } @@ -342,12 +342,12 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public ProfileModel? DefaultProfile { get; set; } /// - /// Gets or sets a value indicating whether or not profiles are enabled. + /// Gets or sets a value indicating whether profiles are enabled. /// public bool ProfilesEnabled { get; set; } = false; /// - /// Gets or sets a value indicating whether or not the user has seen the profiles tutorial. + /// Gets or sets a value indicating whether the user has seen the profiles tutorial. /// public bool ProfilesHasSeenTutorial { get; set; } = false; @@ -391,7 +391,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService public bool? ReduceMotions { get; set; } /// - /// Gets or sets a value indicating whether or not market board data should be uploaded. + /// Gets or sets a value indicating whether market board data should be uploaded. /// public bool IsMbCollect { get; set; } = true; @@ -427,7 +427,7 @@ internal sealed class DalamudConfiguration : IInternalDisposableService } /// - /// Gets or sets a value indicating whether or not to show info on dev bar. + /// Gets or sets a value indicating whether to show info on dev bar. /// public bool ShowDevBarInfo { get; set; } = true; diff --git a/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs b/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs index 11a8d3567..4b7e6dd3d 100644 --- a/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs +++ b/Dalamud/Configuration/Internal/EnvironmentConfiguration.cs @@ -21,7 +21,7 @@ internal class EnvironmentConfiguration public static bool DalamudForceMinHook { get; } = GetEnvironmentVariable("DALAMUD_FORCE_MINHOOK"); /// - /// Gets a value indicating whether or not Dalamud context menus should be disabled. + /// Gets a value indicating whether Dalamud context menus should be disabled. /// public static bool DalamudDoContextMenu { get; } = GetEnvironmentVariable("DALAMUD_ENABLE_CONTEXTMENU"); diff --git a/Dalamud/Console/ConsoleEntry.cs b/Dalamud/Console/ConsoleEntry.cs index 93f250228..407411c6b 100644 --- a/Dalamud/Console/ConsoleEntry.cs +++ b/Dalamud/Console/ConsoleEntry.cs @@ -11,13 +11,13 @@ public interface IConsoleEntry /// Gets the name of the entry. /// public string Name { get; } - + /// /// Gets the description of the entry. /// public string Description { get; } } - + /// /// Interface representing a command in the console. /// @@ -27,7 +27,7 @@ public interface IConsoleCommand : IConsoleEntry /// Execute this command. /// /// Arguments to invoke the entry with. - /// Whether or not execution succeeded. + /// Whether execution succeeded. public bool Invoke(IEnumerable arguments); } diff --git a/Dalamud/Console/ConsoleManager.cs b/Dalamud/Console/ConsoleManager.cs index 4112cde2a..c79a104e1 100644 --- a/Dalamud/Console/ConsoleManager.cs +++ b/Dalamud/Console/ConsoleManager.cs @@ -18,9 +18,9 @@ namespace Dalamud.Console; internal partial class ConsoleManager : IServiceType { private static readonly ModuleLog Log = new("CON"); - + private Dictionary entries = new(); - + /// /// Initializes a new instance of the class. /// @@ -29,17 +29,17 @@ internal partial class ConsoleManager : IServiceType { this.AddCommand("toggle", "Toggle a boolean variable.", this.OnToggleVariable); } - + /// /// Event that is triggered when a command is processed. Return true to stop the command from being processed any further. /// public event Func? Invoke; - + /// /// Gets a read-only dictionary of console entries. /// public IReadOnlyDictionary Entries => this.entries; - + /// /// Add a command to the console. /// @@ -53,13 +53,13 @@ internal partial class ConsoleManager : IServiceType ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(description); ArgumentNullException.ThrowIfNull(func); - + if (this.FindEntry(name) != null) throw new InvalidOperationException($"Entry '{name}' already exists."); var command = new ConsoleCommand(name, description, func); this.entries.Add(name, command); - + return command; } @@ -77,14 +77,14 @@ internal partial class ConsoleManager : IServiceType ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(description); Traits.ThrowIfTIsNullableAndNull(defaultValue); - + if (this.FindEntry(name) != null) throw new InvalidOperationException($"Entry '{name}' already exists."); var variable = new ConsoleVariable(name, description); variable.Value = defaultValue; this.entries.Add(name, variable); - + return variable; } @@ -98,11 +98,11 @@ internal partial class ConsoleManager : IServiceType { ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(alias); - + var target = this.FindEntry(name); if (target == null) throw new EntryNotFoundException(name); - + if (this.FindEntry(alias) != null) throw new InvalidOperationException($"Entry '{alias}' already exists."); @@ -135,21 +135,21 @@ internal partial class ConsoleManager : IServiceType public T GetVariable(string name) { ArgumentNullException.ThrowIfNull(name); - + var entry = this.FindEntry(name); - + if (entry is ConsoleVariable variable) return variable.Value; - + if (entry is ConsoleVariable) throw new InvalidOperationException($"Variable '{name}' is not of type {typeof(T).Name}."); - + if (entry is null) throw new EntryNotFoundException(name); - + throw new InvalidOperationException($"Command '{name}' is not a variable."); } - + /// /// Set the value of a variable. /// @@ -162,18 +162,18 @@ internal partial class ConsoleManager : IServiceType { ArgumentNullException.ThrowIfNull(name); Traits.ThrowIfTIsNullableAndNull(value); - + var entry = this.FindEntry(name); - + if (entry is ConsoleVariable variable) variable.Value = value; - + if (entry is ConsoleVariable) throw new InvalidOperationException($"Variable '{name}' is not of type {typeof(T).Name}."); if (entry is null) - throw new EntryNotFoundException(name); - + throw new EntryNotFoundException(name); + throw new InvalidOperationException($"Command '{name}' is not a variable."); } @@ -181,16 +181,16 @@ internal partial class ConsoleManager : IServiceType /// Process a console command. /// /// The command to process. - /// Whether or not the command was successfully processed. + /// Whether the command was successfully processed. public bool ProcessCommand(string command) { if (this.Invoke?.Invoke(command) == true) return true; - + var matches = GetCommandParsingRegex().Matches(command); if (matches.Count == 0) return false; - + var entryName = matches[0].Value; if (string.IsNullOrEmpty(entryName) || entryName.Any(char.IsWhiteSpace)) { @@ -204,7 +204,7 @@ internal partial class ConsoleManager : IServiceType Log.Error("Command {CommandName} not found", entryName); return false; } - + var parsedArguments = new List(); if (entry.ValidArguments != null) @@ -217,13 +217,13 @@ internal partial class ConsoleManager : IServiceType PrintUsage(entry); return false; } - + var argumentToMatch = entry.ValidArguments[i - 1]; - + var group = matches[i]; if (!group.Success) continue; - + var value = group.Value; if (string.IsNullOrEmpty(value)) continue; @@ -262,15 +262,15 @@ internal partial class ConsoleManager : IServiceType throw new Exception("Unhandled argument type."); } } - + if (parsedArguments.Count != entry.ValidArguments.Count) { // Either fill in the default values or error out - + for (var i = parsedArguments.Count; i < entry.ValidArguments.Count; i++) { var argument = entry.ValidArguments[i]; - + // If the default value is DBNull, we need to error out as that means it was not specified if (argument.DefaultValue == DBNull.Value) { @@ -281,7 +281,7 @@ internal partial class ConsoleManager : IServiceType parsedArguments.Add(argument.DefaultValue); } - + if (parsedArguments.Count != entry.ValidArguments.Count) { Log.Error("Too many arguments for command {CommandName}", entryName); @@ -302,20 +302,20 @@ internal partial class ConsoleManager : IServiceType return entry.Invoke(parsedArguments); } - + [GeneratedRegex("""("[^"]+"|[^\s"]+)""", RegexOptions.Compiled)] private static partial Regex GetCommandParsingRegex(); - + private static void PrintUsage(ConsoleEntry entry, bool error = true) { Log.WriteLog( - error ? LogEventLevel.Error : LogEventLevel.Information, + error ? LogEventLevel.Error : LogEventLevel.Information, "Usage: {CommandName} {Arguments}", null, entry.Name, string.Join(" ", entry.ValidArguments?.Select(x => $"<{x.Type.ToString().ToLowerInvariant()}>") ?? Enumerable.Empty())); } - + private ConsoleEntry? FindEntry(string name) { return this.entries.TryGetValue(name, out var entry) ? entry as ConsoleEntry : null; @@ -333,7 +333,7 @@ internal partial class ConsoleManager : IServiceType return true; } - + private static class Traits { public static void ThrowIfTIsNullableAndNull(T? argument, [CallerArgumentExpression("argument")] string? paramName = null) @@ -364,17 +364,17 @@ internal partial class ConsoleManager : IServiceType /// public string Description { get; } - + /// /// Gets or sets a list of valid argument types for this console entry. /// public IReadOnlyList? ValidArguments { get; protected set; } - + /// /// Execute this command. /// /// Arguments to invoke the entry with. - /// Whether or not execution succeeded. + /// Whether execution succeeded. public abstract bool Invoke(IEnumerable arguments); /// @@ -388,19 +388,19 @@ internal partial class ConsoleManager : IServiceType { if (type == typeof(string)) return new ArgumentInfo(ConsoleArgumentType.String, defaultValue); - + if (type == typeof(int)) return new ArgumentInfo(ConsoleArgumentType.Integer, defaultValue); - + if (type == typeof(float)) return new ArgumentInfo(ConsoleArgumentType.Float, defaultValue); - + if (type == typeof(bool)) return new ArgumentInfo(ConsoleArgumentType.Bool, defaultValue); - + throw new ArgumentException($"Invalid argument type: {type.Name}"); } - + public record ArgumentInfo(ConsoleArgumentType Type, object? DefaultValue); } @@ -436,7 +436,7 @@ internal partial class ConsoleManager : IServiceType private class ConsoleCommand : ConsoleEntry, IConsoleCommand { private readonly Delegate func; - + /// /// Initializes a new instance of the class. /// @@ -447,17 +447,17 @@ internal partial class ConsoleManager : IServiceType : base(name, description) { this.func = func; - + if (func.Method.ReturnType != typeof(bool)) throw new ArgumentException("Console command functions must return a boolean indicating success."); - + var validArguments = new List(); foreach (var parameterInfo in func.Method.GetParameters()) { var paraT = parameterInfo.ParameterType; validArguments.Add(TypeToArgument(paraT, parameterInfo.DefaultValue)); } - + this.ValidArguments = validArguments; } @@ -491,7 +491,7 @@ internal partial class ConsoleManager : IServiceType { this.ValidArguments = new List { TypeToArgument(typeof(T), null) }; } - + /// public T Value { get; set; } @@ -507,16 +507,16 @@ internal partial class ConsoleManager : IServiceType { this.Value = (T)(object)!boolValue; } - + Log.WriteLog(LogEventLevel.Information, "{VariableName} = {VariableValue}", null, this.Name, this.Value); return true; } - + if (first.GetType() != typeof(T)) throw new ArgumentException($"Console variable must be set with an argument of type {typeof(T).Name}."); this.Value = (T)first; - + return true; } } diff --git a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs index aa684a644..297323b8f 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonSetupHook.cs @@ -11,7 +11,7 @@ namespace Dalamud.Game.Addon.Lifecycle; internal class AddonSetupHook : IDisposable where T : Delegate { private readonly Reloaded.Hooks.AsmHook asmHook; - + private T? detour; private bool activated; @@ -30,22 +30,22 @@ internal class AddonSetupHook : IDisposable where T : Delegate "use64", $"mov r9, 0x{detourPtr:X8}", }; - + var opt = new AsmHookOptions { PreferRelativeJump = true, Behaviour = Reloaded.Hooks.Definitions.Enums.AsmHookBehaviour.DoNotExecuteOriginal, MaxOpcodeSize = 5, }; - + this.asmHook = new Reloaded.Hooks.AsmHook(code, (nuint)address, opt); } /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public bool IsEnabled => this.asmHook.IsEnabled; - + /// /// Starts intercepting a call to the function. /// @@ -57,7 +57,7 @@ internal class AddonSetupHook : IDisposable where T : Delegate this.asmHook.Activate(); return; } - + this.asmHook.Enable(); } diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 064667596..61cd88a05 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -43,7 +43,7 @@ internal partial class ChatHandlers : IServiceType public string? LastLink { get; private set; } /// - /// Gets a value indicating whether or not auto-updates have already completed this session. + /// Gets a value indicating whether auto-updates have already completed this session. /// public bool IsAutoUpdateComplete { get; private set; } diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index b81373144..de17478a0 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -70,13 +70,13 @@ public interface IFate : IEquatable byte Progress { get; } /// - /// Gets a value indicating whether or not this has a EXP bonus. + /// Gets a value indicating whether this has a EXP bonus. /// [Obsolete($"Use {nameof(HasBonus)} instead")] bool HasExpBonus { get; } /// - /// Gets a value indicating whether or not this has a bonus. + /// Gets a value indicating whether this has a bonus. /// bool HasBonus { get; } diff --git a/Dalamud/Game/ClientState/JobGauge/Types/BLMGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/BLMGauge.cs index c4058132a..ad0cdd9e1 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/BLMGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/BLMGauge.cs @@ -45,19 +45,19 @@ public unsafe class BLMGauge : JobGaugeBase this.Struct->AstralSoulStacks; /// - /// Gets a value indicating whether or not the player is in Umbral Ice. + /// Gets a value indicating whether the player is in Umbral Ice. /// /// true or false. public bool InUmbralIce => this.Struct->ElementStance < 0; /// - /// Gets a value indicating whether or not the player is in Astral fire. + /// Gets a value indicating whether the player is in Astral fire. /// /// true or false. public bool InAstralFire => this.Struct->ElementStance > 0; /// - /// Gets a value indicating whether or not Enochian is active. + /// Gets a value indicating whether Enochian is active. /// /// true or false. public bool IsEnochianActive => this.Struct->EnochianActive; diff --git a/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs b/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs index db9d51dc4..d31a22702 100644 --- a/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs +++ b/Dalamud/Game/ClientState/JobGauge/Types/PCTGauge.cs @@ -14,7 +14,7 @@ public unsafe class PCTGauge : JobGaugeBase /// Initializes a new instance of the class. /// /// Address of the job gauge. - internal PCTGauge(IntPtr address) + internal PCTGauge(IntPtr address) : base(address) { } @@ -28,29 +28,29 @@ public unsafe class PCTGauge : JobGaugeBase /// Gets the amount of paint the player has. /// public byte Paint => Struct->Paint; - + /// - /// Gets a value indicating whether or not a creature motif is drawn. + /// Gets a value indicating whether a creature motif is drawn. /// public bool CreatureMotifDrawn => Struct->CreatureMotifDrawn; /// - /// Gets a value indicating whether or not a weapon motif is drawn. + /// Gets a value indicating whether a weapon motif is drawn. /// public bool WeaponMotifDrawn => Struct->WeaponMotifDrawn; /// - /// Gets a value indicating whether or not a landscape motif is drawn. + /// Gets a value indicating whether a landscape motif is drawn. /// public bool LandscapeMotifDrawn => Struct->LandscapeMotifDrawn; /// - /// Gets a value indicating whether or not a moogle portrait is ready. + /// Gets a value indicating whether a moogle portrait is ready. /// public bool MooglePortraitReady => Struct->MooglePortraitReady; - + /// - /// Gets a value indicating whether or not a madeen portrait is ready. + /// Gets a value indicating whether a madeen portrait is ready. /// public bool MadeenPortraitReady => Struct->MadeenPortraitReady; diff --git a/Dalamud/Game/ClientState/Objects/Enums/CustomizeIndex.cs b/Dalamud/Game/ClientState/Objects/Enums/CustomizeIndex.cs index 299583fd3..534ee6347 100644 --- a/Dalamud/Game/ClientState/Objects/Enums/CustomizeIndex.cs +++ b/Dalamud/Game/ClientState/Objects/Enums/CustomizeIndex.cs @@ -42,7 +42,7 @@ public enum CustomizeIndex HairStyle = 0x06, /// - /// Whether or not the character has hair highlights. + /// Whether the character has hair highlights. /// HasHighlights = 0x07, // negative to enable, positive to disable diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index c6208fb2f..6f3f9a8dd 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -269,7 +269,7 @@ internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar /// Check whether an entry with the specified title exists. /// /// The title to check for. - /// Whether or not an entry with that title is registered. + /// Whether an entry with that title is registered. internal bool HasEntry(string title) { var found = false; diff --git a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs index dc5f5f048..26708eb4c 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBarEntry.cs @@ -39,7 +39,7 @@ public interface IReadOnlyDtrBarEntry public bool Shown { get; } /// - /// Gets a value indicating whether or not the user has hidden this entry from view through the Dalamud settings. + /// Gets a value indicating whether the user has hidden this entry from view through the Dalamud settings. /// public bool UserHidden { get; } diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index d3fe444ea..ada9021c4 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -75,7 +75,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui } // Hooked delegates - + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate char HandleImmDelegate(IntPtr framework, char a2, byte a3); @@ -259,7 +259,7 @@ internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui /// /// Indicates if the game is in the lobby scene (title screen, chara select, chara make, aesthetician etc.). /// - /// A value indicating whether or not the game is in the lobby scene. + /// A value indicating whether the game is in the lobby scene. internal bool IsInLobby() => RaptureAtkModule.Instance()->CurrentUIScene.StartsWith("LobbyMain"u8); /// diff --git a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs index a721015bb..0093a1b40 100644 --- a/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs +++ b/Dalamud/Game/Gui/NamePlate/NamePlateQuotedParts.cs @@ -6,7 +6,7 @@ namespace Dalamud.Game.Gui.NamePlate; /// A part builder for constructing and setting quoted nameplate fields (i.e. free company tag and title). /// /// The field type which should be set. -/// Whether or not this is a Free Company part. +/// Whether this is a Free Company part. /// /// This class works as a lazy writer initialized with empty parts, where an empty part signifies no change should be /// performed. Only after all handler processing is complete does it write out any parts which were set to the diff --git a/Dalamud/Game/Network/Internal/MarketBoardUploaders/MarketBoardItemRequest.cs b/Dalamud/Game/Network/Internal/MarketBoardUploaders/MarketBoardItemRequest.cs index a32a92b13..9c566687a 100644 --- a/Dalamud/Game/Network/Internal/MarketBoardUploaders/MarketBoardItemRequest.cs +++ b/Dalamud/Game/Network/Internal/MarketBoardUploaders/MarketBoardItemRequest.cs @@ -21,7 +21,7 @@ internal class MarketBoardItemRequest public uint Status { get; private set; } /// - /// Gets a value indicating whether or not this request was successful. + /// Gets a value indicating whether this request was successful. /// public bool Ok => this.Status == 0; diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index c0929fa84..7d6304655 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -248,7 +248,7 @@ internal unsafe class NetworkHandlers : IInternalDisposableService /// /// Disposes of managed and unmanaged resources. /// - /// Whether or not to execute the disposal. + /// Whether to execute the disposal. protected void Dispose(bool shouldDispose) { if (!shouldDispose) diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index 3422848f3..5aaf17f12 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -31,7 +31,7 @@ public class SigScanner : IDisposable, ISigScanner /// /// Initializes a new instance of the class using the main module of the current process. /// - /// Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. + /// Whether 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 SigScanner(bool doCopy = false, FileInfo? cacheFile = null) : this(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile) @@ -42,7 +42,7 @@ public class SigScanner : IDisposable, ISigScanner /// Initializes a new instance of the class. /// /// The ProcessModule to be used for scanning. - /// Whether or not to copy the module upon initialization for search operations to use, as to not get disturbed by possible hooks. + /// Whether 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 SigScanner(ProcessModule module, bool doCopy = false, FileInfo? cacheFile = null) { diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index 5c93fb4d4..f60c32d9a 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -19,7 +19,7 @@ internal class TargetSigScanner : SigScanner, IPublicDisposableService /// /// 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. + /// Whether 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/Game/Text/SeStringHandling/Payloads/ItemPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs index 56372e0f9..25cdf7f9f 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/ItemPayload.cs @@ -31,7 +31,7 @@ public class ItemPayload : Payload /// Creates a payload representing an interactable item link for the specified item. /// /// The id of the item. - /// Whether or not the link should be for the high-quality variant of the item. + /// Whether the link should be for the high-quality variant of the item. /// An optional name to include in the item link. Typically this should /// be left as null, or set to the normal item name. Actual overrides are better done with the subsequent /// TextPayload that is a part of a full item link in chat. @@ -142,7 +142,7 @@ public class ItemPayload : Payload : (RowRef)LuminaUtils.CreateRef(this.ItemId); /// - /// Gets a value indicating whether or not this item link is for a high-quality version of the item. + /// Gets a value indicating whether this item link is for a high-quality version of the item. /// [JsonProperty] public bool IsHQ => this.Kind == ItemKind.Hq; diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs index 8443e06ce..bf360ce34 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIForegroundPayload.cs @@ -46,7 +46,7 @@ public class UIForegroundPayload : Payload public override PayloadType Type => PayloadType.UIForeground; /// - /// Gets a value indicating whether or not this payload represents applying a foreground color, or disabling one. + /// Gets a value indicating whether this payload represents applying a foreground color, or disabling one. /// public bool IsEnabled => this.ColorKey != 0; diff --git a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs index d22318378..e54427073 100644 --- a/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs +++ b/Dalamud/Game/Text/SeStringHandling/Payloads/UIGlowPayload.cs @@ -64,7 +64,7 @@ public class UIGlowPayload : Payload } /// - /// Gets a value indicating whether or not this payload represents applying a glow color, or disabling one. + /// Gets a value indicating whether this payload represents applying a glow color, or disabling one. /// public bool IsEnabled => this.ColorKey != 0; diff --git a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs index e78ac2de8..d5080e6e8 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeStringBuilder.cs @@ -113,7 +113,7 @@ public class SeStringBuilder /// Add an item link to the builder. /// /// The item ID. - /// Whether or not the item is high quality. + /// Whether the item is high quality. /// Override for the item's name. /// The current builder. public SeStringBuilder AddItemLink(uint itemId, bool isHq, string? itemNameOverride = null) => diff --git a/Dalamud/Hooking/AsmHook.cs b/Dalamud/Hooking/AsmHook.cs index f1ed7fd11..09ae336dc 100644 --- a/Dalamud/Hooking/AsmHook.cs +++ b/Dalamud/Hooking/AsmHook.cs @@ -20,7 +20,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook private bool isEnabled = false; private DynamicMethod statsMethod; - + private Guid hookId = Guid.NewGuid(); /// @@ -89,7 +89,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook } /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public bool IsEnabled { @@ -101,7 +101,7 @@ public sealed class AsmHook : IDisposable, IDalamudHook } /// - /// Gets a value indicating whether or not the hook has been disposed. + /// Gets a value indicating whether the hook has been disposed. /// public bool IsDisposed { get; private set; } diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 20796bbf0..1a492146f 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -59,23 +59,23 @@ public abstract class Hook : IDalamudHook where T : Delegate => this.IsDisposed ? Marshal.GetDelegateForFunctionPointer(this.address) : this.Original; /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public virtual bool IsEnabled => throw new NotImplementedException(); /// - /// Gets a value indicating whether or not the hook has been disposed. + /// Gets a value indicating whether the hook has been disposed. /// public bool IsDisposed { get; private set; } /// public virtual string BackendName => throw new NotImplementedException(); - + /// /// Gets the unique GUID for this hook. /// protected Guid HookId { get; } = Guid.NewGuid(); - + /// /// Remove a hook from the current process. /// diff --git a/Dalamud/Hooking/IDalamudHook.cs b/Dalamud/Hooking/IDalamudHook.cs index bffca242e..5ced572c0 100644 --- a/Dalamud/Hooking/IDalamudHook.cs +++ b/Dalamud/Hooking/IDalamudHook.cs @@ -11,12 +11,12 @@ public interface IDalamudHook : IDisposable public IntPtr Address { get; } /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public bool IsEnabled { get; } /// - /// Gets a value indicating whether or not the hook is disposed. + /// Gets a value indicating whether the hook is disposed. /// public bool IsDisposed { get; } diff --git a/Dalamud/Hooking/Internal/CallHook.cs b/Dalamud/Hooking/Internal/CallHook.cs index 5b438b5a8..92bc6e31a 100644 --- a/Dalamud/Hooking/Internal/CallHook.cs +++ b/Dalamud/Hooking/Internal/CallHook.cs @@ -53,7 +53,7 @@ internal class CallHook : IDalamudHook where T : Delegate } /// - /// Gets a value indicating whether or not the hook is enabled. + /// Gets a value indicating whether the hook is enabled. /// public bool IsEnabled => this.asmHook.IsEnabled; diff --git a/Dalamud/Interface/Animation/Easing.cs b/Dalamud/Interface/Animation/Easing.cs index 8191487f4..0d2057b3b 100644 --- a/Dalamud/Interface/Animation/Easing.cs +++ b/Dalamud/Interface/Animation/Easing.cs @@ -85,12 +85,12 @@ public abstract class Easing public TimeSpan Duration { get; set; } /// - /// Gets a value indicating whether or not the animation is running. + /// Gets a value indicating whether the animation is running. /// public bool IsRunning => this.animationTimer.IsRunning; /// - /// Gets a value indicating whether or not the animation is done. + /// Gets a value indicating whether the animation is done. /// public bool IsDone => this.animationTimer.ElapsedMilliseconds > this.Duration.TotalMilliseconds; diff --git a/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs b/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs index 487a08132..6998e2ef0 100644 --- a/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs +++ b/Dalamud/Interface/ImGuiFileDialog/DriveListLoader.cs @@ -23,7 +23,7 @@ internal class DriveListLoader public IReadOnlyList Drives { get; private set; } /// - /// Gets a value indicating whether or not the loader is loading. + /// Gets a value indicating whether the loader is loading. /// public bool Loading { get; private set; } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index f010e3496..8ba579d17 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -521,7 +521,7 @@ internal class DalamudInterface : IInternalDisposableService /// /// Toggle the screen darkening effect used for the credits. /// - /// Whether or not to turn the effect on. + /// Whether to turn the effect on. public void SetCreditsDarkeningAnimation(bool status) { this.isCreditsDarkening = status; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 786bc4589..ec175c5aa 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -212,7 +212,7 @@ internal partial class InterfaceManager : IInternalDisposableService public IntPtr WindowHandlePtr => this.scene?.WindowHandlePtr ?? IntPtr.Zero; /// - /// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. + /// Gets or sets a value indicating whether the game's cursor should be overridden with the ImGui cursor. /// public bool OverrideGameCursor { @@ -231,7 +231,7 @@ internal partial class InterfaceManager : IInternalDisposableService public bool IsReady => this.scene != null; /// - /// Gets or sets a value indicating whether or not Draw events should be dispatched. + /// Gets or sets a value indicating whether Draw events should be dispatched. /// public bool IsDispatchingEvents { get; set; } = true; diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs index 3fbc4361f..ff9649a14 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/LuminaSelfTestStep.cs @@ -8,7 +8,7 @@ namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; /// Test setup for Lumina. /// /// ExcelRow to run test on. -/// Whether or not the sheet is large. If it is large, the self test will iterate through the full sheet in one frame and benchmark the time taken. +/// Whether the sheet is large. If it is large, the self test will iterate through the full sheet in one frame and benchmark the time taken. internal class LuminaSelfTestStep(bool isLargeSheet) : ISelfTestStep where T : struct, IExcelRow { diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs index 46013b72c..4cb239da3 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsEntry.cs @@ -11,12 +11,12 @@ public abstract class SettingsEntry public string? Name { get; protected set; } /// - /// Gets or sets a value indicating whether or not this entry is valid. + /// Gets or sets a value indicating whether this entry is valid. /// public virtual bool IsValid { get; protected set; } = true; /// - /// Gets or sets a value indicating whether or not this entry is visible. + /// Gets or sets a value indicating whether this entry is visible. /// public virtual bool IsVisible { get; protected set; } = true; @@ -54,7 +54,7 @@ public abstract class SettingsEntry { // ignored } - + /// /// Function to be called when the tab is closed. /// diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs index a98d32770..1596db447 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs @@ -14,7 +14,7 @@ namespace Dalamud.Interface; public interface ITitleScreenMenuEntry : IReadOnlyTitleScreenMenuEntry, IComparable { /// - /// Gets or sets a value indicating whether or not this entry is internal. + /// Gets or sets a value indicating whether this entry is internal. /// bool IsInternal { get; set; } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 2fc8c83b1..abe6bd87b 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -147,7 +147,7 @@ public interface IUiBuilder bool DisableGposeUiHide { get; set; } /// - /// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. + /// Gets or sets a value indicating whether the game's cursor should be overridden with the ImGui cursor. /// bool OverrideGameCursor { get; set; } @@ -157,7 +157,7 @@ public interface IUiBuilder ulong FrameCount { get; } /// - /// Gets a value indicating whether or not a cutscene is playing. + /// Gets a value indicating whether a cutscene is playing. /// bool CutsceneActive { get; } @@ -177,7 +177,7 @@ public interface IUiBuilder IFontAtlas FontAtlas { get; } /// - /// Gets a value indicating whether or not to use "reduced motion". This usually means that you should use less + /// Gets a value indicating whether to use "reduced motion". This usually means that you should use less /// intrusive animations, or disable them entirely. /// bool ShouldUseReducedMotion { get; } @@ -445,7 +445,7 @@ public sealed class UiBuilder : IDisposable, IUiBuilder public bool DisableGposeUiHide { get; set; } = false; /// - /// Gets or sets a value indicating whether or not the game's cursor should be overridden with the ImGui cursor. + /// Gets or sets a value indicating whether the game's cursor should be overridden with the ImGui cursor. /// public bool OverrideGameCursor { @@ -459,7 +459,7 @@ public sealed class UiBuilder : IDisposable, IUiBuilder public ulong FrameCount { get; private set; } = 0; /// - /// Gets a value indicating whether or not a cutscene is playing. + /// Gets a value indicating whether a cutscene is playing. /// public bool CutsceneActive { @@ -489,7 +489,7 @@ public sealed class UiBuilder : IDisposable, IUiBuilder public IFontAtlas FontAtlas { get; } /// - /// Gets a value indicating whether or not to use "reduced motion". This usually means that you should use less + /// Gets a value indicating whether to use "reduced motion". This usually means that you should use less /// intrusive animations, or disable them entirely. /// public bool ShouldUseReducedMotion => Service.Get().ReduceMotions ?? false; diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index b3a505c1d..7779100b0 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -150,7 +150,7 @@ public abstract class Window public WindowSizeConstraints? SizeConstraints { get; set; } /// - /// Gets or sets a value indicating whether or not this window is collapsed. + /// Gets or sets a value indicating whether this window is collapsed. /// public bool? Collapsed { get; set; } @@ -165,7 +165,7 @@ public abstract class Window public ImGuiWindowFlags Flags { get; set; } /// - /// Gets or sets a value indicating whether or not this ImGui window will be forced to stay inside the main game window. + /// Gets or sets a value indicating whether this ImGui window will be forced to stay inside the main game window. /// public bool ForceMainWindow { get; set; } @@ -175,17 +175,17 @@ public abstract class Window public float? BgAlpha { get; set; } /// - /// Gets or sets a value indicating whether or not this ImGui window should display a close button in the title bar. + /// Gets or sets a value indicating whether this ImGui window should display a close button in the title bar. /// 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. + /// Gets or sets a value indicating whether 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. + /// Gets or sets a value indicating whether this window should offer to be made click-through via the window's titlebar context menu. /// public bool AllowClickthrough { get; set; } = true; @@ -199,7 +199,7 @@ public abstract class Window public List TitleBarButtons { get; set; } = new(); /// - /// Gets or sets a value indicating whether or not this window will stay open. + /// Gets or sets a value indicating whether this window will stay open. /// public bool IsOpen { @@ -804,7 +804,7 @@ public abstract class Window public int Priority { get; set; } /// - /// Gets or sets a value indicating whether or not the button shall be clickable + /// Gets or sets a value indicating whether the button shall be clickable /// when the respective window is set to clickthrough. /// public bool AvailableClickthrough { get; set; } diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index b45ed82d6..cb9a0db6d 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -200,22 +200,22 @@ internal class TaskTracker : IInternalDisposableService public StackTrace? StackTrace { get; set; } /// - /// Gets or sets a value indicating whether or not the task was completed. + /// Gets or sets a value indicating whether the task was completed. /// public bool IsCompleted { get; set; } /// - /// Gets or sets a value indicating whether or not the task faulted. + /// Gets or sets a value indicating whether the task faulted. /// public bool IsFaulted { get; set; } /// - /// Gets or sets a value indicating whether or not the task was canceled. + /// Gets or sets a value indicating whether the task was canceled. /// public bool IsCanceled { get; set; } /// - /// Gets or sets a value indicating whether or not the task was completed successfully. + /// Gets or sets a value indicating whether the task was completed successfully. /// public bool IsCompletedSuccessfully { get; set; } diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs index 32ab99372..c9382bd17 100644 --- a/Dalamud/NativeFunctions.cs +++ b/Dalamud/NativeFunctions.cs @@ -835,7 +835,7 @@ internal static partial class NativeFunctions SPI_GETCLIENTAREAANIMATION = 0x1042, #pragma warning restore SA1602 } - + /// /// Retrieves or sets the value of one of the system-wide parameters. This function can also update the user profile while setting a parameter. /// @@ -1428,18 +1428,18 @@ internal static partial class NativeFunctions /// created with this option cannot be locked. /// NoSerialize = 0x00000001, - + /// /// The system raises an exception to indicate failure (for example, an out-of-memory condition) for calls to /// HeapAlloc and HeapReAlloc instead of returning NULL. /// GenerateExceptions = 0x00000004, - + /// /// The allocated memory will be initialized to zero. Otherwise, the memory is not initialized to zero. /// ZeroMemory = 0x00000008, - + /// /// All memory blocks that are allocated from this heap allow code execution, if the hardware enforces data /// execution prevention. Use this flag heap in applications that run code from the heap. If @@ -1701,7 +1701,7 @@ internal static partial class NativeFunctions /// /// This value determines the initial amount of memory that is committed for the heap. /// The value is rounded up to a multiple of the system page size. The value must be smaller than dwMaximumSize. - /// + /// /// If this parameter is 0, the function commits one page. To determine the size of a page on the host computer, /// use the GetSystemInfo function. /// @@ -1710,12 +1710,12 @@ internal static partial class NativeFunctions /// system page size and then reserves a block of that size in the process's virtual address space for the heap. /// If allocation requests made by the HeapAlloc or HeapReAlloc functions exceed the size specified by /// dwInitialSize, the system commits additional pages of memory for the heap, up to the heap's maximum size. - /// + /// /// If dwMaximumSize is not zero, the heap size is fixed and cannot grow beyond the maximum size. Also, the largest /// memory block that can be allocated from the heap is slightly less than 512 KB for a 32-bit process and slightly /// less than 1,024 KB for a 64-bit process. Requests to allocate larger blocks fail, even if the maximum size of /// the heap is large enough to contain the block. - /// + /// /// If dwMaximumSize is 0, the heap can grow in size. The heap's size is limited only by the available memory. /// Requests to allocate memory blocks larger than the limit for a fixed-size heap do not automatically fail; /// instead, the system calls the VirtualAlloc function to obtain the memory that is needed for large blocks. @@ -1723,12 +1723,12 @@ internal static partial class NativeFunctions /// /// /// If the function succeeds, the return value is a handle to the newly created heap. - /// + /// /// If the function fails, the return value is NULL. To get extended error information, call GetLastError. /// [DllImport("kernel32.dll", SetLastError = true)] public static extern nint HeapCreate(HeapOptions flOptions, nuint dwInitialSize, nuint dwMaximumSize); - + /// /// Allocates a block of memory from a heap. The allocated memory is not movable. /// @@ -1756,7 +1756,7 @@ internal static partial class NativeFunctions /// [DllImport("kernel32.dll", SetLastError=false)] public static extern nint HeapAlloc(nint hHeap, HeapOptions dwFlags, nuint dwBytes); - + /// /// See https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc. /// Reserves, commits, or changes the state of a region of pages in the virtual address space of the calling process. @@ -1987,7 +1987,7 @@ internal static partial class NativeFunctions /// /// If this value is , enumerates the loaded modules for the process and effectively calls the SymLoadModule64 function for each module. /// - /// Whether or not the function succeeded. + /// Whether the function succeeded. [DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool SymInitialize(IntPtr hProcess, string userSearchPath, bool fInvadeProcess); @@ -1995,7 +1995,7 @@ internal static partial class NativeFunctions /// Deallocates all resources associated with the process handle. /// /// A handle to the process that was originally passed to the function. - /// Whether or not the function succeeded. + /// Whether the function succeeded. [DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern bool SymCleanup(IntPtr hProcess); @@ -2009,7 +2009,7 @@ internal static partial class NativeFunctions /// Exception information. /// User information. /// Callback. - /// Whether or not the minidump succeeded. + /// Whether the minidump succeeded. [DllImport("dbghelp.dll")] public static extern bool MiniDumpWriteDump(IntPtr hProcess, uint processId, IntPtr hFile, int dumpType, ref MinidumpExceptionInformation exceptionInfo, IntPtr userStreamParam, IntPtr callback); diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index d5cf360af..5dac85164 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -100,7 +100,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa public PluginLoadReason Reason { get; } /// - /// Gets a value indicating whether or not auto-updates have already completed this session. + /// Gets a value indicating whether auto-updates have already completed this session. /// public bool IsAutoUpdateComplete => Service.Get().IsAutoUpdateComplete; diff --git a/Dalamud/Plugin/IDalamudPluginInterface.cs b/Dalamud/Plugin/IDalamudPluginInterface.cs index 100d4570e..5b7c3836e 100644 --- a/Dalamud/Plugin/IDalamudPluginInterface.cs +++ b/Dalamud/Plugin/IDalamudPluginInterface.cs @@ -35,7 +35,7 @@ public interface IDalamudPluginInterface /// What action caused this event to be fired. /// If this plugin was affected by the change. public delegate void ActivePluginsChangedDelegate(PluginListInvalidationKind kind, bool affectedThisPlugin); - + /// /// Event that gets fired when loc is changed /// @@ -52,7 +52,7 @@ public interface IDalamudPluginInterface PluginLoadReason Reason { get; } /// - /// Gets a value indicating whether or not auto-updates have already completed this session. + /// Gets a value indicating whether auto-updates have already completed this session. /// bool IsAutoUpdateComplete { get; } diff --git a/Dalamud/Plugin/InstalledPluginState.cs b/Dalamud/Plugin/InstalledPluginState.cs index 64c8e40a5..1c79e33f0 100644 --- a/Dalamud/Plugin/InstalledPluginState.cs +++ b/Dalamud/Plugin/InstalledPluginState.cs @@ -34,12 +34,12 @@ public interface IExposedPlugin bool IsTesting { get; } /// - /// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not. + /// Gets a value indicating whether this plugin is orphaned(belongs to a repo) or not. /// bool IsOrphaned { get; } /// - /// Gets a value indicating whether or not this plugin is serviced(repo still exists, but plugin no longer does). + /// Gets a value indicating whether this plugin is serviced(repo still exists, but plugin no longer does). /// bool IsDecommissioned { get; } diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index d40184a76..adec4f73d 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -131,7 +131,7 @@ internal class AutoUpdateManager : IServiceType } /// - /// Gets a value indicating whether or not auto-updates have already completed this session. + /// Gets a value indicating whether auto-updates have already completed this session. /// public bool IsAutoUpdateComplete { get; private set; } diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 28fc1fcb1..e8283dd8f 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -284,7 +284,7 @@ internal class PluginManager : IInternalDisposableService /// Check if a manifest even has an available testing version. /// /// The manifest to test. - /// Whether or not a testing version is available. + /// Whether a testing version is available. public static bool HasTestingVersion(IPluginManifest manifest) { var av = manifest.AssemblyVersion; @@ -1037,7 +1037,7 @@ internal class PluginManager : IInternalDisposableService /// /// The available plugin update. /// Whether to notify that installed plugins have changed afterwards. - /// Whether or not to actually perform the update, or just indicate success. + /// Whether to actually perform the update, or just indicate success. /// The status of the update. public async Task UpdateSinglePluginAsync(AvailablePluginUpdate metadata, bool notify, bool dryRun) { diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 9039004ab..edbeabfac 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -24,8 +24,8 @@ internal class Profile /// /// The manager this profile belongs to. /// The model this profile is tied to. - /// Whether or not this profile is the default profile. - /// Whether or not this profile was initialized during bootup. + /// Whether this profile is the default profile. + /// Whether this profile was initialized during bootup. public Profile(ProfileManager manager, ProfileModel model, bool isDefaultProfile, bool isBoot) { this.manager = manager; @@ -108,12 +108,12 @@ internal class Profile public Guid Guid => this.modelV1.Guid; /// - /// Gets a value indicating whether or not this profile is currently enabled. + /// Gets a value indicating whether this profile is currently enabled. /// public bool IsEnabled { get; private set; } /// - /// Gets a value indicating whether or not this profile is the default profile. + /// Gets a value indicating whether this profile is the default profile. /// public bool IsDefaultProfile { get; } @@ -139,8 +139,8 @@ internal class Profile /// Set this profile's state. This cannot be called for the default profile. /// This will block until all states have been applied. /// - /// Whether or not the profile is enabled. - /// Whether or not the current state should immediately be applied. + /// Whether the profile is enabled. + /// Whether the current state should immediately be applied. /// Thrown when an untoggleable profile is toggled. /// A representing the asynchronous operation. public async Task SetStateAsync(bool enabled, bool apply = true) @@ -178,8 +178,8 @@ internal class Profile /// /// 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. + /// Whether the plugin should be enabled. + /// Whether the current state should immediately be applied. /// A representing the asynchronous operation. public async Task AddOrUpdateAsync(Guid workingPluginId, string? internalName, bool state, bool apply = true) { @@ -223,9 +223,9 @@ internal class Profile /// This will block until all states have been applied. /// /// The ID of the plugin. - /// Whether or not the current state should immediately be applied. + /// Whether the current state should immediately be applied. /// - /// Whether or not to throw when a plugin is removed from the default profile, without being in another profile. + /// Whether to throw when a plugin is removed from the default profile, without being in another profile. /// Used to prevent orphan plugins, but can be ignored when cleaning up old entries. /// /// A representing the asynchronous operation. diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 8a1711b0d..82c72ebef 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -54,7 +54,7 @@ internal class ProfileManager : IServiceType public IEnumerable Profiles => this.profiles; /// - /// Gets a value indicating whether or not the profile manager is busy enabling/disabling plugins. + /// Gets a value indicating whether the profile manager is busy enabling/disabling plugins. /// public bool IsBusy => this.isBusy; @@ -71,8 +71,8 @@ internal class ProfileManager : IServiceType /// 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. + /// Whether the plugin should be added to the default preset, if it's not present in any preset. + /// Whether the plugin shall be enabled. public async Task GetWantStateAsync(Guid workingPluginId, string? internalName, bool defaultState, bool addIfNotDeclared = true) { var want = false; @@ -106,7 +106,7 @@ internal class ProfileManager : IServiceType /// Check whether a plugin is declared in any profile. /// /// The ID of the plugin. - /// Whether or not the plugin is in any profile. + /// Whether the plugin is in any profile. public bool IsInAnyProfile(Guid workingPluginId) { lock (this.profiles) @@ -118,7 +118,7 @@ internal class ProfileManager : IServiceType /// A plugin can never be in the default profile if it is in any other profile. /// /// The ID of the plugin. - /// Whether or not the plugin is in the default profile. + /// Whether the plugin is in the default profile. public bool IsInDefaultProfile(Guid workingPluginId) => this.DefaultProfile.WantsPlugin(workingPluginId) != null; diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs index 62d0de70c..a1a327c1d 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileModelV1.cs @@ -36,7 +36,7 @@ public class ProfileModelV1 : ProfileModel public static string SerializedPrefix => "DP1"; /// - /// Gets or sets a value indicating whether or not this profile should always be enabled at boot. + /// Gets or sets a value indicating whether this profile should always be enabled at boot. /// [JsonProperty("b")] [Obsolete("Superseded by StartupPolicy")] @@ -49,7 +49,7 @@ public class ProfileModelV1 : ProfileModel public ProfileStartupPolicy? StartupPolicy { get; set; } /// - /// Gets or sets a value indicating whether or not this profile is currently enabled. + /// Gets or sets a value indicating whether this profile is currently enabled. /// [JsonProperty("e")] public bool IsEnabled { get; set; } = false; @@ -81,7 +81,7 @@ public class ProfileModelV1 : ProfileModel public Guid WorkingPluginId { get; set; } /// - /// Gets or sets a value indicating whether or not this entry is enabled. + /// Gets or sets a value indicating whether this entry is enabled. /// public bool IsEnabled { get; set; } } diff --git a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs index 7909981bc..a2805ff6e 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfilePluginEntry.cs @@ -10,7 +10,7 @@ internal class ProfilePluginEntry /// /// The internal name of the plugin. /// The ID of the plugin. - /// A value indicating whether or not this entry is enabled. + /// A value indicating whether this entry is enabled. public ProfilePluginEntry(string internalName, Guid workingPluginId, bool state) { this.InternalName = internalName; @@ -22,14 +22,14 @@ internal class ProfilePluginEntry /// Gets the internal name of the plugin. /// public string InternalName { get; } - + /// /// Gets or sets an ID uniquely identifying this specific instance of a plugin. /// public Guid WorkingPluginId { get; set; } /// - /// Gets a value indicating whether or not this entry is enabled. + /// Gets a value indicating whether this entry is enabled. /// public bool IsEnabled { get; } } diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 144e09b47..6478a5bb5 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -178,13 +178,13 @@ internal class LocalPlugin : IAsyncDisposable public bool IsTesting => this.manifest.IsTestingExclusive || this.manifest.Testing; /// - /// Gets a value indicating whether or not this plugin is orphaned(belongs to a repo) or not. + /// Gets a value indicating whether this plugin is orphaned(belongs to a repo) or not. /// public bool IsOrphaned => !this.IsDev && this.GetSourceRepository() == null; /// - /// Gets a value indicating whether or not this plugin is serviced(repo still exists, but plugin no longer does). + /// Gets a value indicating whether this plugin is serviced(repo still exists, but plugin no longer does). /// public bool IsDecommissioned => !this.IsDev && this.GetSourceRepository()?.State == PluginRepositoryState.Success && @@ -499,7 +499,7 @@ internal class LocalPlugin : IAsyncDisposable /// /// Check if anything forbids this plugin from loading. /// - /// Whether or not this plugin shouldn't load. + /// Whether this plugin shouldn't load. public bool CheckPolicy() { var startInfo = Service.Get().StartInfo; diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index 01951c8a6..c9c231b4d 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -42,7 +42,7 @@ internal record PluginManifest : IPluginManifest public List? CategoryTags { get; init; } /// - /// Gets or sets a value indicating whether or not the plugin is hidden in the plugin installer. + /// Gets or sets a value indicating whether 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] diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index bac2b3e3f..60d8a17e2 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -79,7 +79,7 @@ public interface IClientState /// Gets the current Territory the player resides in. /// public ushort TerritoryType { get; } - + /// /// Gets the current Map the player resides in. /// @@ -101,17 +101,17 @@ public interface IClientState public bool IsLoggedIn { get; } /// - /// Gets a value indicating whether or not the user is playing PvP. + /// Gets a value indicating whether the user is playing PvP. /// public bool IsPvP { get; } /// - /// Gets a value indicating whether or not the user is playing PvP, excluding the Wolves' Den. + /// Gets a value indicating whether 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. + /// Gets a value indicating whether the client is currently in Group Pose (GPose) mode. /// public bool IsGPosing { get; } diff --git a/Dalamud/Plugin/Services/ISigScanner.cs b/Dalamud/Plugin/Services/ISigScanner.cs index c0ebd9310..ac0f2c55f 100644 --- a/Dalamud/Plugin/Services/ISigScanner.cs +++ b/Dalamud/Plugin/Services/ISigScanner.cs @@ -10,12 +10,12 @@ namespace Dalamud.Game; public interface ISigScanner { /// - /// Gets a value indicating whether or not the search on this module is performed on a copy. + /// Gets a value indicating whether the search on this module is performed on a copy. /// public bool IsCopy { get; } /// - /// Gets a value indicating whether or not the ProcessModule is 32-bit. + /// Gets a value indicating whether the ProcessModule is 32-bit. /// public bool Is32BitProcess { get; } @@ -84,7 +84,7 @@ public interface ISigScanner /// The offset from function start of the instruction using the data. /// An IntPtr to the static memory location. public nint GetStaticAddressFromSig(string signature, int offset = 0); - + /// /// Try scanning for a .data address using a .text function. /// This is intended to be used with IDA sigs. @@ -95,14 +95,14 @@ public interface ISigScanner /// The offset from function start of the instruction using the data. /// true if the signature was found. public bool TryGetStaticAddressFromSig(string signature, out nint result, int offset = 0); - + /// /// Scan for a byte signature in the .data section. /// /// The signature. /// The real offset of the found signature. public nint ScanData(string signature); - + /// /// Try scanning for a byte signature in the .data section. /// @@ -110,14 +110,14 @@ public interface ISigScanner /// The real offset of the signature, if found. /// true if the signature was found. public bool TryScanData(string signature, out nint result); - + /// /// Scan for a byte signature in the whole module search area. /// /// The signature. /// The real offset of the found signature. public nint ScanModule(string signature); - + /// /// Try scanning for a byte signature in the whole module search area. /// @@ -125,7 +125,7 @@ public interface ISigScanner /// The real offset of the signature, if found. /// true if the signature was found. public bool TryScanModule(string signature, out nint result); - + /// /// Resolve a RVA address. /// @@ -133,14 +133,14 @@ public interface ISigScanner /// The relative offset. /// The calculated offset. public nint ResolveRelativeAddress(nint nextInstAddr, int relOffset); - + /// /// Scan for a byte signature in the .text section. /// /// The signature. /// The real offset of the found signature. public nint ScanText(string signature); - + /// /// Try scanning for a byte signature in the .text section. /// diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index ff13f11f1..201e2b803 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -191,7 +191,7 @@ public interface ITextureProvider /// Caching the returned object is not recommended. Performance benefit will be minimal. /// ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); - + /// Gets a shared texture corresponding to the given game resource icon specifier. /// /// This function does not throw exceptions. @@ -200,7 +200,7 @@ public interface ITextureProvider /// /// A game icon specifier. /// The resulting . - /// Whether or not the lookup succeeded. + /// Whether the lookup succeeded. bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture); /// Gets a shared texture corresponding to the given path to a game resource. @@ -221,7 +221,7 @@ public interface ITextureProvider /// Caching the returned object is not recommended. Performance benefit will be minimal. /// ISharedImmediateTexture GetFromFile(string path); - + /// Gets a shared texture corresponding to the given file on the filesystem. /// The file on the filesystem to load. /// The shared texture that you may use to obtain the loaded texture wrap and load states. diff --git a/Dalamud/SafeMemory.cs b/Dalamud/SafeMemory.cs index 3365ff118..8d9c62c30 100644 --- a/Dalamud/SafeMemory.cs +++ b/Dalamud/SafeMemory.cs @@ -25,7 +25,7 @@ public static class SafeMemory /// The address to read from. /// The amount of bytes to read. /// The result buffer. - /// Whether or not the read succeeded. + /// Whether the read succeeded. public static bool ReadBytes(IntPtr address, int count, out byte[] buffer) { buffer = new byte[count <= 0 ? 0 : count]; @@ -37,7 +37,7 @@ public static class SafeMemory /// /// The address to write to. /// The buffer to write. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static bool WriteBytes(IntPtr address, byte[] buffer) { return Imports.WriteProcessMemory(Handle, address, buffer, buffer.Length, out _); @@ -49,7 +49,7 @@ public static class SafeMemory /// The type of the struct. /// The address to read from. /// The resulting object. - /// Whether or not the read succeeded. + /// Whether the read succeeded. public static bool Read(IntPtr address, out T result) where T : struct { if (!ReadBytes(address, SizeCache.Size, out var buffer)) @@ -91,7 +91,7 @@ public static class SafeMemory /// The type of the struct. /// The address to write to. /// The object to write. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static bool Write(IntPtr address, T obj) where T : struct { using var mem = new LocalMemory(SizeCache.Size); @@ -105,7 +105,7 @@ public static class SafeMemory /// The type of the structs. /// The address to write to. /// The array to write. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static bool Write(IntPtr address, T[] objArray) where T : struct { if (objArray == null || objArray.Length == 0) @@ -164,7 +164,7 @@ public static class SafeMemory /// /// The address to write to. /// The string to write. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static bool WriteString(IntPtr address, string str) { return WriteString(address, str, Encoding.UTF8); @@ -181,7 +181,7 @@ public static class SafeMemory /// The address to write to. /// The string to write. /// The encoding to use. - /// Whether or not the write succeeded. + /// Whether the write succeeded. public static bool WriteString(IntPtr address, string str, Encoding encoding) { if (string.IsNullOrEmpty(str)) diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 9b87a71a0..9791b9e45 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -157,7 +157,7 @@ internal class ReliableFileStorage : IInternalDisposableService /// automatically written back to disk, however. /// /// The path to read from. - /// Whether or not the backup of the file should take priority. + /// Whether 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. @@ -171,7 +171,7 @@ internal class ReliableFileStorage : IInternalDisposableService /// /// The path to read from. /// The encoding to read with. - /// Whether or not the backup of the file should take priority. + /// Whether 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. @@ -249,7 +249,7 @@ internal class ReliableFileStorage : IInternalDisposableService /// automatically written back to disk, however. /// /// The path to read from. - /// Whether or not the backup of the file should take priority. + /// Whether 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. diff --git a/Dalamud/Support/BugBait.cs b/Dalamud/Support/BugBait.cs index c82e5e652..8dbf2e429 100644 --- a/Dalamud/Support/BugBait.cs +++ b/Dalamud/Support/BugBait.cs @@ -20,10 +20,10 @@ internal static class BugBait /// Send feedback to Discord. /// /// The plugin to send feedback about. - /// Whether or not the plugin is a testing plugin. + /// Whether the plugin is a testing plugin. /// The content of the feedback. /// The reporter name. - /// Whether or not the most recent exception to occur should be included in the report. + /// Whether the most recent exception to occur should be included in the report. /// A representing the asynchronous operation. public static async Task SendFeedback(IPluginManifest plugin, bool isTesting, string content, string reporter, bool includeException) { @@ -43,7 +43,7 @@ internal static class BugBait { model.Exception = Troubleshooting.LastException == null ? "Was included, but none happened" : Troubleshooting.LastException?.ToString(); } - + var httpClient = Service.Get().SharedHttpClient; var postContent = new StringContent(JsonConvert.SerializeObject(model), Encoding.UTF8, "application/json"); diff --git a/Dalamud/Utility/Signatures/NullabilityUtil.cs b/Dalamud/Utility/Signatures/NullabilityUtil.cs index da4b24db6..da45db197 100755 --- a/Dalamud/Utility/Signatures/NullabilityUtil.cs +++ b/Dalamud/Utility/Signatures/NullabilityUtil.cs @@ -14,21 +14,21 @@ internal static class NullabilityUtil /// Check if the provided property is nullable. /// /// The property to check. - /// Whether or not the property is nullable. + /// Whether the property is nullable. internal static bool IsNullable(PropertyInfo property) => IsNullableHelper(property.PropertyType, property.DeclaringType, property.CustomAttributes); /// /// Check if the provided field is nullable. /// /// The field to check. - /// Whether or not the field is nullable. + /// Whether the field is nullable. internal static bool IsNullable(FieldInfo field) => IsNullableHelper(field.FieldType, field.DeclaringType, field.CustomAttributes); /// /// Check if the provided parameter is nullable. /// /// The parameter to check. - /// Whether or not the parameter is nullable. + /// Whether the parameter is nullable. internal static bool IsNullable(ParameterInfo parameter) => IsNullableHelper(parameter.ParameterType, parameter.Member, parameter.CustomAttributes); private static bool IsNullableHelper(Type memberType, MemberInfo? declaringType, IEnumerable customAttributes) diff --git a/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs b/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs index 76e32da28..a660164c1 100755 --- a/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs +++ b/Dalamud/Utility/Signatures/Wrappers/IFieldOrPropertyInfo.cs @@ -16,7 +16,7 @@ internal interface IFieldOrPropertyInfo Type ActualType { get; } /// - /// Gets a value indicating whether or not the field or property is nullable. + /// Gets a value indicating whether the field or property is nullable. /// bool IsNullable { get; } diff --git a/Dalamud/Utility/Timing/TimingHandle.cs b/Dalamud/Utility/Timing/TimingHandle.cs index d73a9c2d3..e334908b1 100644 --- a/Dalamud/Utility/Timing/TimingHandle.cs +++ b/Dalamud/Utility/Timing/TimingHandle.cs @@ -67,7 +67,7 @@ public sealed class TimingHandle : TimingEvent, IDisposable, IComparable - /// Gets a value indicating whether or not this timing was started on the main thread. + /// Gets a value indicating whether this timing was started on the main thread. /// public bool IsMainThread { get; private set; } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 966fa1e11..4959fafb2 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -308,7 +308,7 @@ public static class Util /// /// The structure to show. /// The address to the structure. - /// Whether or not this structure should start out expanded. + /// Whether this structure should start out expanded. /// The already followed path. public static void ShowStruct(object obj, ulong addr, bool autoExpand = false, IEnumerable? path = null) => ShowStructInternal(obj, addr, autoExpand, path); @@ -318,7 +318,7 @@ public static class Util /// /// The type of the structure. /// The pointer to the structure. - /// Whether or not this structure should start out expanded. + /// Whether this structure should start out expanded. public static unsafe void ShowStruct(T* obj, bool autoExpand = false) where T : unmanaged { ShowStruct(*obj, (ulong)&obj, autoExpand); @@ -328,7 +328,7 @@ public static class Util /// Show a GameObject's internal data in an ImGui-context. /// /// The GameObject to show. - /// Whether or not the struct should start as expanded. + /// Whether the struct should start as expanded. public static unsafe void ShowGameObjectStruct(IGameObject go, bool autoExpand = true) { switch (go) @@ -1031,7 +1031,7 @@ public static class Util /// /// The structure to show. /// The address to the structure. - /// Whether or not this structure should start out expanded. + /// Whether 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) diff --git a/DalamudCrashHandler/miniz.h b/DalamudCrashHandler/miniz.h index 6cc398c92..0c12a3ccb 100644 --- a/DalamudCrashHandler/miniz.h +++ b/DalamudCrashHandler/miniz.h @@ -115,7 +115,7 @@ -/* Defines to completely disable specific portions of miniz.c: +/* Defines to completely disable specific portions of miniz.c: If all macros here are defined the only functionality remaining will be CRC-32, adler-32, tinfl, and tdefl. */ /* Define MINIZ_NO_STDIO to disable all usage and any functions which rely on stdio for file I/O. */ @@ -138,7 +138,7 @@ /* Define MINIZ_NO_ZLIB_COMPATIBLE_NAME to disable zlib names, to prevent conflicts against stock zlib. */ /*#define MINIZ_NO_ZLIB_COMPATIBLE_NAMES */ -/* Define MINIZ_NO_MALLOC to disable all calls to malloc, free, and realloc. +/* Define MINIZ_NO_MALLOC to disable all calls to malloc, free, and realloc. Note if MINIZ_NO_MALLOC is defined then the user must always provide custom user alloc/free/realloc callbacks to the zlib and archive API's, and a few stand-alone helper API's which don't provide custom user functions (such as tdefl_compress_mem_to_heap() and tinfl_decompress_mem_to_heap()) won't work. */ @@ -360,7 +360,7 @@ MINIZ_EXPORT mz_ulong mz_compressBound(mz_ulong source_len); /* Initializes a decompressor. */ MINIZ_EXPORT int mz_inflateInit(mz_streamp pStream); -/* mz_inflateInit2() is like mz_inflateInit() with an additional option that controls the window size and whether or not the stream has been wrapped with a zlib header/footer: */ +/* mz_inflateInit2() is like mz_inflateInit() with an additional option that controls the window size and whether the stream has been wrapped with a zlib header/footer: */ /* window_bits must be MZ_DEFAULT_WINDOW_BITS (to parse zlib header/footer) or -MZ_DEFAULT_WINDOW_BITS (raw deflate). */ MINIZ_EXPORT int mz_inflateInit2(mz_streamp pStream, int window_bits); @@ -908,7 +908,7 @@ struct tinfl_decompressor_tag #ifdef __cplusplus } #endif - + #pragma once From 06851ab14a886ce2f34027937517f19cec5f97c1 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 25 Apr 2025 22:59:04 +0200 Subject: [PATCH 038/106] Add separators between profiles and entries for better visual clarity --- .../PluginInstaller/ProfileManagerWidget.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index 4c1348a61..114124738 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -224,6 +224,9 @@ internal class ProfileManagerWidget ImGuiHelpers.ScaledDummy(3); ImGui.SameLine(); + // Center text in frame height + var textHeight = ImGui.CalcTextSize(profile.Name); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (ImGui.GetFrameHeight() / 2) - (textHeight.Y / 2)); ImGui.Text(profile.Name); ImGui.SameLine(); @@ -263,6 +266,17 @@ internal class ProfileManagerWidget didAny = true; ImGuiHelpers.ScaledDummy(2); + + // Separator if not the last item + if (profile != profman.Profiles.Last()) + { + // Very light grey + ImGui.PushStyleColor(ImGuiCol.Border, ImGuiColors.DalamudGrey.WithAlpha(0.2f)); + ImGui.Separator(); + ImGui.PopStyleColor(); + + ImGuiHelpers.ScaledDummy(2); + } } if (toCloneGuid != null) @@ -523,6 +537,15 @@ internal class ProfileManagerWidget if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.RemovePlugin); + + // Separator if not the last item + if (profileEntry != profile.Plugins.Last()) + { + // Very light grey + ImGui.PushStyleColor(ImGuiCol.Border, ImGuiColors.DalamudGrey.WithAlpha(0.2f)); + ImGui.Separator(); + ImGui.PopStyleColor(); + } } if (wantRemovePluginGuid != null) From b25fceb90055a534938937f5b18536d03cd61742 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 25 Apr 2025 23:01:01 +0200 Subject: [PATCH 039/106] build: 12.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 562206764..4f2012aea 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.10 + 12.0.0.11 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 0bddf305771e6e46a8023dd212de7aecec2b8824 Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Sat, 26 Apr 2025 01:28:12 +0200 Subject: [PATCH 040/106] Fix fly text combo in Dalamud Data (#2262) --- .../Windows/Data/Widgets/FlyTextWidget.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FlyTextWidget.cs index 40275645f..41dac5b43 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 System.Linq; +using System.Numerics; using Dalamud.Game.Gui.FlyText; @@ -39,13 +40,15 @@ internal class FlyTextWidget : IDataWindowWidget /// public void Draw() { - if (ImGui.BeginCombo("Kind", this.flyKind.ToString())) + if (ImGui.BeginCombo("Kind", $"{this.flyKind} ({(int)this.flyKind})")) { - var names = Enum.GetNames(typeof(FlyTextKind)); - for (var i = 0; i < names.Length; i++) + var values = Enum.GetValues().Distinct(); + foreach (var value in values) { - if (ImGui.Selectable($"{names[i]} ({i})")) - this.flyKind = (FlyTextKind)i; + if (ImGui.Selectable($"{value} ({(int)value})")) + { + this.flyKind = value; + } } ImGui.EndCombo(); From a925e37ceb7bc3f55d92a6c243825489d1639396 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 26 Apr 2025 12:17:27 +0200 Subject: [PATCH 041/106] Actually respect remember state flag --- Dalamud/Plugin/Internal/Profiles/Profile.cs | 22 ++++++++++++++----- .../Internal/Profiles/ProfileManager.cs | 4 ++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index edbeabfac..d899b0cca 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -64,16 +64,28 @@ internal class Profile this.IsEnabled = false; Log.Verbose("{Guid} set disabled because always disable", this.modelV1.Guid); } - } - else if (this.modelV1.IsEnabled) - { - this.IsEnabled = true; - Log.Verbose("{Guid} set enabled because remember", this.modelV1.Guid); + else if (this.modelV1.StartupPolicy == ProfileModelV1.ProfileStartupPolicy.RememberState) + { + this.IsEnabled = this.modelV1.IsEnabled; + Log.Verbose("{Guid} set enabled because remember", this.modelV1.Guid); + } + else + { + throw new ArgumentOutOfRangeException(nameof(this.modelV1.StartupPolicy)); + } } else { Log.Verbose("{Guid} not enabled", this.modelV1.Guid); } + + Log.Verbose("Init profile {Guid} ({Name}) enabled:{Enabled} policy:{Policy} plugins:{NumPlugins} will be enabled:{Status}", + this.modelV1.Guid, + this.modelV1.Name, + this.modelV1.IsEnabled, + this.modelV1.StartupPolicy, + this.modelV1.Plugins.Count, + this.IsEnabled); } /// diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs index 82c72ebef..775ff7a72 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileManager.cs @@ -193,6 +193,10 @@ internal class ProfileManager : IServiceType } } } + else + { + throw new InvalidOperationException("Unsupported profile model version"); + } this.config.SavedProfiles!.Add(newModel); this.config.QueueSave(); From 4633820e4836e635279e0188f671f7b099aac480 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 26 Apr 2025 12:17:52 +0200 Subject: [PATCH 042/106] build: 12.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 4f2012aea..5c8783eaf 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.11 + 12.0.0.12 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From ced601e2f5842a0dcf5ffb99f268553f01e3fa03 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 26 Apr 2025 12:48:07 +0200 Subject: [PATCH 043/106] build: 12.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 5c8783eaf..daa677227 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.12 + 12.0.0.13 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 82cdcf0a0cda174eedfd6c584020f6bd98b78f2b Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 26 Apr 2025 19:19:51 +0200 Subject: [PATCH 044/106] Use GameInventoryType in InventoryWidget (#2257) --- .../Windows/Data/Widgets/InventoryWidget.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs index f8aa4e500..dce6341a2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/InventoryWidget.cs @@ -26,7 +26,7 @@ internal class InventoryWidget : IDataWindowWidget { private DataManager dataManager; private TextureManager textureManager; - private InventoryType? selectedInventoryType = InventoryType.Inventory1; + private GameInventoryType? selectedInventoryType = GameInventoryType.Inventory1; /// public string[]? CommandShortcuts { get; init; } = ["inv", "inventory"]; @@ -56,7 +56,7 @@ internal class InventoryWidget : IDataWindowWidget ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X); - this.DrawInventoryType((InventoryType)this.selectedInventoryType); + this.DrawInventoryType(this.selectedInventoryType.Value); } private static string StripSoftHypen(string input) @@ -74,9 +74,9 @@ internal class InventoryWidget : IDataWindowWidget ImGui.TableSetupScrollFreeze(2, 1); ImGui.TableHeadersRow(); - foreach (var inventoryType in Enum.GetValues()) + foreach (var inventoryType in Enum.GetValues()) { - var items = GameInventoryItem.GetReadOnlySpanOfInventory((GameInventoryType)inventoryType); + var items = GameInventoryItem.GetReadOnlySpanOfInventory(inventoryType); using var itemDisabled = ImRaii.Disabled(items.IsEmpty); @@ -98,7 +98,7 @@ internal class InventoryWidget : IDataWindowWidget if (ImGui.MenuItem("Copy Address")) { - var container = InventoryManager.Instance()->GetInventoryContainer(inventoryType); + var container = InventoryManager.Instance()->GetInventoryContainer((InventoryType)inventoryType); ImGui.SetClipboardText($"0x{(nint)container:X}"); } } @@ -109,9 +109,9 @@ internal class InventoryWidget : IDataWindowWidget } } - private unsafe void DrawInventoryType(InventoryType inventoryType) + private unsafe void DrawInventoryType(GameInventoryType inventoryType) { - var items = GameInventoryItem.GetReadOnlySpanOfInventory((GameInventoryType)inventoryType); + var items = GameInventoryItem.GetReadOnlySpanOfInventory(inventoryType); if (items.IsEmpty) { ImGui.TextUnformatted($"{inventoryType} is empty."); From fd1eafddfbb6b14ff0d5fedb4e163b95e68feda4 Mon Sep 17 00:00:00 2001 From: Asriel Date: Mon, 28 Apr 2025 12:13:48 -0700 Subject: [PATCH 045/106] Update Lumina to 5.7.0 and Lumina.Excel to 7.2.2 (#2258) --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index ef07620a4..92902b0c0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -10,8 +10,8 @@ - 5.6.1 - 7.2.1 + 5.7.0 + 7.2.2 13.0.3 From febf4e55a49a93f7d1bae0810112896637973693 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:38:30 +0200 Subject: [PATCH 046/106] Update ClientStructs (#2260) 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 b1e0fc768..e14220b15 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit b1e0fc7685f13bdd33f202ff83beedd3eba82325 +Subproject commit e14220b157a3f9db8d218351f6e513aa9e169e2e From c82bb8191d3b26982f01c1b54769f4bb6af82a03 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 26 Apr 2025 21:52:45 +0200 Subject: [PATCH 047/106] Show exception in service init error message --- Dalamud/Dalamud.cs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 93de4c64d..e406c3502 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -35,7 +35,7 @@ internal sealed class Dalamud : IServiceType private static int shownServiceError = 0; private readonly ManualResetEvent unloadSignal; - + #endregion /// @@ -48,15 +48,15 @@ internal sealed class Dalamud : IServiceType 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")) @@ -71,10 +71,10 @@ internal sealed class Dalamud : IServiceType configuration, scanner, Localization.FromAssets(info.AssetDirectory!, configuration.LanguageOverride)); - + // Set up FFXIVClientStructs this.SetupClientStructsResolver(cacheDir); - + void KickoffGameThread() { Log.Verbose("=============== GAME THREAD KICKOFF ==============="); @@ -85,12 +85,12 @@ internal sealed class Dalamud : IServiceType void HandleServiceInitFailure(Task t) { Log.Error(t.Exception!, "Service initialization failure"); - + if (Interlocked.CompareExchange(ref shownServiceError, 1, 0) != 0) return; Util.Fatal( - "Dalamud failed to load all necessary services.\n\nThe game will continue, but you may not be able to use plugins.", + $"Dalamud failed to load all necessary services.\nThe game will continue, but you may not be able to use plugins.\n\n{t.Exception}", "Dalamud", false); } @@ -124,7 +124,7 @@ internal sealed class Dalamud : IServiceType this.DebugExceptionFilter = Service.Get().ScanText(debugSig); Log.Debug($"SE debug exception filter at {this.DebugExceptionFilter.ToInt64():X}"); } - + /// /// Gets the start information for this Dalamud instance. /// @@ -188,7 +188,7 @@ internal sealed class Dalamud : IServiceType /// /// Replace the current exception handler with the default one. /// - internal void UseDefaultExceptionHandler() => + internal void UseDefaultExceptionHandler() => this.SetExceptionHandler(this.DefaultExceptionFilter); /// From 69d8968dca3caa4d299e5d96606fa57cb7826c7f Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 1 May 2025 14:45:25 +0200 Subject: [PATCH 048/106] IoC: Allow private scoped objects to resolve singleton services --- .../Windows/Data/Widgets/ServicesWidget.cs | 55 ++++++++++++------- Dalamud/IoC/Internal/ObjectInstance.cs | 9 ++- .../IoC/Internal/ObjectInstanceVisibility.cs | 17 ++++++ Dalamud/IoC/Internal/ServiceContainer.cs | 45 ++++++++------- Dalamud/IoC/Internal/ServiceScope.cs | 11 ++-- Dalamud/Plugin/DalamudPluginInterface.cs | 3 +- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 2 +- Dalamud/Service/ServiceManager.cs | 51 +++++++++-------- Dalamud/Service/Service{T}.cs | 19 ++++--- 9 files changed, 134 insertions(+), 78 deletions(-) create mode 100644 Dalamud/IoC/Internal/ObjectInstanceVisibility.cs diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs index d1e6bc58a..fe89a2d1e 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ServicesWidget.cs @@ -26,9 +26,9 @@ internal class ServicesWidget : IDataWindowWidget /// public string[]? CommandShortcuts { get; init; } = { "services" }; - + /// - public string DisplayName { get; init; } = "Service Container"; + public string DisplayName { get; init; } = "Service Container"; /// public bool Ready { get; set; } @@ -48,7 +48,7 @@ internal class ServicesWidget : IDataWindowWidget { if (ImGui.Button("Clear selection")) this.selectedNodes.Clear(); - + ImGui.SameLine(); switch (this.includeUnloadDependencies) { @@ -90,12 +90,12 @@ internal class ServicesWidget : IDataWindowWidget 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 += node.DisplayedNameSize.X + cellPad.X + margin.X; @@ -139,7 +139,7 @@ internal class ServicesWidget : IDataWindowWidget { 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]; @@ -149,7 +149,7 @@ internal class ServicesWidget : IDataWindowWidget dl.AddLine(point1, point2, lineInvalidColor, 2f * ImGuiHelpers.GlobalScale); } - + foreach (var parent in node.Parents) { rect = this.nodeRects[parent]; @@ -170,7 +170,7 @@ internal class ServicesWidget : IDataWindowWidget } } } - + // 3. Draw boxes foreach (var levelNodes in this.dependencyNodes) { @@ -231,36 +231,49 @@ internal class ServicesWidget : IDataWindowWidget } } } - + ImGui.SetCursorPos(default); ImGui.Dummy(new(maxRowWidth, this.dependencyNodes.Count * rowHeight)); ImGui.EndChild(); } } - if (ImGui.CollapsingHeader("Plugin-facing Services")) + if (ImGui.CollapsingHeader("Singleton 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!!!"); } + switch (instance.Value.Visibility) + { + case ObjectInstanceVisibility.Internal: + ImGui.Text("\t => Internally resolved"); + break; + + case ObjectInstanceVisibility.ExposedToPlugins: + var hasInterface = container.InterfaceToTypeMap.Values.Any(x => x == instance.Key); + using (ImRaii.PushColor(ImGuiCol.Text, ImGuiColors.DalamudRed, !hasInterface)) + { + ImGui.Text("\t => Exposed to plugins!"); + ImGui.Text( + hasInterface + ? $"\t => Provided via interface: {container.InterfaceToTypeMap.First(x => x.Value == instance.Key).Key.FullName}" + : "\t => NO INTERFACE!!!"); + } + + break; + default: + throw new ArgumentOutOfRangeException(); + } + ImGuiHelpers.ScaledDummy(2); } } @@ -301,7 +314,7 @@ internal class ServicesWidget : IDataWindowWidget public string DisplayedName { get; } public string TypeSuffix { get; } - + public uint TypeSuffixColor { get; } public Vector2 DisplayedNameSize => @@ -319,7 +332,7 @@ internal class ServicesWidget : IDataWindowWidget public IEnumerable Relatives => this.parents.Concat(this.children).Concat(this.invalidParents); - + public int Level { get; private set; } public static List CreateTree(bool includeUnloadDependencies) diff --git a/Dalamud/IoC/Internal/ObjectInstance.cs b/Dalamud/IoC/Internal/ObjectInstance.cs index 3fd626a05..3a963f6bd 100644 --- a/Dalamud/IoC/Internal/ObjectInstance.cs +++ b/Dalamud/IoC/Internal/ObjectInstance.cs @@ -13,9 +13,11 @@ internal class ObjectInstance /// /// Weak reference to the underlying instance. /// Type of the underlying instance. - public ObjectInstance(Task instanceTask, Type type) + /// The visibility of this instance. + public ObjectInstance(Task instanceTask, Type type, ObjectInstanceVisibility visibility) { this.InstanceTask = instanceTask; + this.Visibility = visibility; } /// @@ -23,4 +25,9 @@ internal class ObjectInstance /// /// The underlying instance. public Task InstanceTask { get; } + + /// + /// Gets or sets the visibility of the object instance. + /// + public ObjectInstanceVisibility Visibility { get; set; } } diff --git a/Dalamud/IoC/Internal/ObjectInstanceVisibility.cs b/Dalamud/IoC/Internal/ObjectInstanceVisibility.cs new file mode 100644 index 000000000..7ab564603 --- /dev/null +++ b/Dalamud/IoC/Internal/ObjectInstanceVisibility.cs @@ -0,0 +1,17 @@ +namespace Dalamud.IoC.Internal; + +/// +/// Enum that declares the visibility of an object instance in the service container. +/// +internal enum ObjectInstanceVisibility +{ + /// + /// The object instance is only visible to other internal services. + /// + Internal, + + /// + /// The object instance is visible to all services and plugins. + /// + ExposedToPlugins, +} diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index a8eacb02d..6745155f6 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel.Design; using System.Diagnostics; using System.Linq; using System.Reflection; @@ -12,7 +13,7 @@ namespace Dalamud.IoC.Internal; /// /// A simple singleton-only IOC container that provides (optional) version-based dependency resolution. -/// +/// /// This is only used to resolve dependencies for plugins. /// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// @@ -29,13 +30,18 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// public ServiceContainer() { + // Register the service container itself as a singleton. + // For all other services, this is done through the static constructor of Service{T}. + this.instances.Add( + typeof(IServiceContainer), + new(new Task(() => new WeakReference(this), TaskCreationOptions.RunContinuationsAsynchronously), typeof(ServiceContainer), ObjectInstanceVisibility.Internal)); } - + /// /// Gets a dictionary of all registered instances. /// public IReadOnlyDictionary Instances => this.instances; - + /// /// Gets a dictionary mapping interfaces to their implementations. /// @@ -45,15 +51,13 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// Register a singleton object of any type into the current IOC container. /// /// The existing instance to register in the container. + /// The visibility of this singleton. /// The type to register. - public void RegisterSingleton(Task instance) + public void RegisterSingleton(Task instance, ObjectInstanceVisibility visibility) { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } + ArgumentNullException.ThrowIfNull(instance); - this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T)); + this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T), visibility); } /// @@ -69,7 +73,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"); @@ -81,10 +85,11 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// Create an object. /// /// The type of object to create. + /// Defines which services are allowed to be directly resolved into this type. /// Scoped objects to be included in the constructor. /// The scope to be used to create scoped services. /// The created object. - public async Task CreateAsync(Type objectType, object[] scopedObjects, IServiceScope? scope = null) + public async Task CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, object[] scopedObjects, IServiceScope? scope = null) { var errorStep = "constructor lookup"; @@ -174,7 +179,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType { if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; - + if (serviceType.GetCustomAttribute() != null) { if (scope == null) @@ -211,7 +216,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType private ConstructorInfo? FindApplicableCtor(Type type, object[] scopedObjects) { // get a list of all the available types: scoped and singleton - var types = scopedObjects + var allValidServiceTypes = scopedObjects .Select(o => o.GetType()) .Union(this.instances.Keys) .ToArray(); @@ -224,7 +229,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType var ctors = type.GetConstructors(ctorFlags); foreach (var ctor in ctors) { - if (this.ValidateCtor(ctor, types)) + if (this.ValidateCtor(ctor, allValidServiceTypes)) { return ctor; } @@ -233,28 +238,30 @@ internal class ServiceContainer : IServiceProvider, IServiceType return null; } - private bool ValidateCtor(ConstructorInfo ctor, Type[] types) + private bool ValidateCtor(ConstructorInfo ctor, Type[] validTypes) { bool IsTypeValid(Type type) { - var contains = types.Any(x => x.IsAssignableTo(type)); + var contains = validTypes.Any(x => x.IsAssignableTo(type)); // Scoped services are created on-demand return contains || type.GetCustomAttribute() != null; } - + var parameters = ctor.GetParameters(); foreach (var parameter in parameters) { var valid = IsTypeValid(parameter.ParameterType); - + // If this service is provided by an interface if (!valid && this.interfaceToTypeMap.TryGetValue(parameter.ParameterType, out var implementationType)) valid = IsTypeValid(implementationType); if (!valid) { - Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!); + Log.Error("Ctor from {DeclaringType}: Failed to validate {TypeName}, unable to find any services that satisfy the type", + ctor.DeclaringType?.FullName ?? ctor.DeclaringType?.Name ?? "null", + parameter.ParameterType.FullName!); return false; } } diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 5ce8bc7d0..98209eeb7 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -25,9 +25,10 @@ internal interface IServiceScope : IAsyncDisposable /// Create an object. /// /// The type of object to create. + /// Defines which services are allowed to be directly resolved into this type. /// Scoped objects to be included in the constructor. /// The created object. - Task CreateAsync(Type objectType, params object[] scopedObjects); + Task CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, params object[] scopedObjects); /// /// Inject interfaces into public or static properties on the provided object. @@ -72,13 +73,13 @@ internal class ServiceScopeImpl : IServiceScope } /// - public Task CreateAsync(Type objectType, params object[] scopedObjects) + public Task CreateAsync(Type objectType, ObjectInstanceVisibility allowedVisibility, params object[] scopedObjects) { this.disposeLock.EnterReadLock(); try { ObjectDisposedException.ThrowIf(this.disposed, this); - return this.container.CreateAsync(objectType, scopedObjects, this); + return this.container.CreateAsync(objectType, allowedVisibility, scopedObjects, this); } finally { @@ -117,7 +118,9 @@ internal class ServiceScopeImpl : IServiceScope objectType, static (objectType, p) => p.Scope.container.CreateAsync( objectType, - p.Objects.Concat(p.Scope.privateScopedObjects).ToArray()), + ObjectInstanceVisibility.Internal, // We are allowed to resolve internal services here since this is a private scoped object. + p.Objects.Concat(p.Scope.privateScopedObjects).ToArray(), + p.Scope), (Scope: this, Objects: scopedObjects)); } finally diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 5dac85164..f82d241d4 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -19,6 +19,7 @@ using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings; +using Dalamud.IoC.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.AutoUpdate; using Dalamud.Plugin.Internal.Types; @@ -482,7 +483,7 @@ internal sealed class DalamudPluginInterface : IDalamudPluginInterface, IDisposa /// public async Task CreateAsync(params object[] scopedObjects) where T : class => - (T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), this.GetPublicIocScopes(scopedObjects)); + (T)await this.plugin.ServiceScope!.CreateAsync(typeof(T), ObjectInstanceVisibility.ExposedToPlugins, this.GetPublicIocScopes(scopedObjects)); /// public bool Inject(object instance, params object[] scopedObjects) diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index e05cbc190..4b2b62669 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -577,7 +577,7 @@ internal class LocalPlugin : IAsyncDisposable var newInstanceTask = forceFrameworkThread ? framework.RunOnFrameworkThread(Create) : Create(); return await newInstanceTask.ConfigureAwait(false); - async Task Create() => (IDalamudPlugin)await scope.CreateAsync(type, dalamudInterface); + async Task Create() => (IDalamudPlugin)await scope.CreateAsync(type, ObjectInstanceVisibility.ExposedToPlugins, dalamudInterface); } private static void SetupLoaderConfig(LoaderConfig config) diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 206b24736..1ae03a80d 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -72,7 +72,7 @@ internal static class ServiceManager /// The justification for using this feature. [InjectableType] public delegate void RegisterUnloadAfterDelegate(IEnumerable unloadAfter, string justification); - + /// /// Kinds of services. /// @@ -83,27 +83,27 @@ internal static class ServiceManager /// Not a service. /// None = 0, - + /// /// Service that is loaded manually. /// ProvidedService = 1 << 0, - + /// /// Service that is loaded asynchronously while the game starts. /// EarlyLoadedService = 1 << 1, - + /// /// Service that is loaded before the game starts. /// BlockingEarlyLoadedService = 1 << 2, - + /// /// Service that is only instantiable via scopes. /// ScopedService = 1 << 3, - + /// /// Service that is loaded automatically when the game starts, synchronously or asynchronously. /// @@ -114,7 +114,7 @@ internal static class ServiceManager /// Gets task that gets completed when all blocking early loading services are done loading. /// public static Task BlockingResolved { get; } = BlockingServicesLoadedTaskCompletionSource.Task; - + /// /// Gets a cancellation token that will be cancelled once Dalamud needs to unload, be it due to a failure state /// during initialization or during regular operation. @@ -139,10 +139,13 @@ internal static class ServiceManager #if DEBUG lock (LoadedServices) { + // ServiceContainer MUST be first. The static ctor of Service will call Service.Get() + // which causes a deadlock otherwise. + ProvideService(new ServiceContainer()); + ProvideService(dalamud); ProvideService(fs); ProvideService(configuration); - ProvideService(new ServiceContainer()); ProvideService(scanner); ProvideService(localization); } @@ -193,7 +196,7 @@ internal static class ServiceManager var getAsyncTaskMap = new Dictionary(); var serviceContainer = Service.Get(); - + foreach (var serviceType in GetConcreteServiceTypes()) { var serviceKind = serviceType.GetServiceKind(); @@ -202,13 +205,13 @@ internal static class ServiceManager // 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; var genericWrappedServiceType = typeof(Service<>).MakeGenericType(serviceType); - + var getTask = (Task)genericWrappedServiceType .InvokeMember( nameof(Service.GetAsync), @@ -290,7 +293,7 @@ internal static class ServiceManager var tasks = tasksEnumerable.AsReadOnlyCollection(); if (tasks.Count == 0) return; - + // Time we wait until showing the loading dialog const int loadingDialogTimeout = 10000; @@ -330,7 +333,7 @@ internal static class ServiceManager hasDeps = false; } } - + if (!hasDeps) continue; @@ -437,7 +440,7 @@ internal static class ServiceManager public static void UnloadAllServices() { UnloadCancellationTokenSource.Cancel(); - + var framework = Service.GetNullable(Service.ExceptionPropagationMode.None); if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false }) { @@ -450,14 +453,14 @@ internal static class ServiceManager var dependencyServicesMap = new Dictionary>(); var allToUnload = new HashSet(); var unloadOrder = new List(); - + Log.Information("==== COLLECTING SERVICES TO UNLOAD ===="); - + foreach (var serviceType in GetConcreteServiceTypes()) { if (!serviceType.IsAssignableTo(typeof(IServiceType))) continue; - + // Scoped services shall never be unloaded here. // Their lifetime must be managed by the IServiceScope that owns them. If it leaks, it's their fault. if (serviceType.GetServiceKind() == ServiceKind.ScopedService) @@ -485,12 +488,12 @@ internal static class ServiceManager unloadOrder.Add(serviceType); Log.Information("Queue for unload {Type}", serviceType.FullName!); } - + foreach (var serviceType in allToUnload) { UnloadService(serviceType); } - + Log.Information("==== UNLOADING ALL SERVICES ===="); unloadOrder.Reverse(); @@ -507,7 +510,7 @@ internal static class ServiceManager null, null); } - + #if DEBUG lock (LoadedServices) { @@ -536,17 +539,17 @@ internal static class ServiceManager var attr = type.GetCustomAttribute(true)?.GetType(); if (attr == null) return ServiceKind.None; - + Debug.Assert( type.IsAssignableTo(typeof(IServiceType)), "Service did not inherit from IServiceType"); if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedServiceAttribute))) return ServiceKind.BlockingEarlyLoadedService; - + if (attr.IsAssignableTo(typeof(EarlyLoadedServiceAttribute))) return ServiceKind.EarlyLoadedService; - + if (attr.IsAssignableTo(typeof(ScopedServiceAttribute))) return ServiceKind.ScopedService; @@ -572,7 +575,7 @@ internal static class ServiceManager var isAnyDisposable = isServiceDisposable || serviceType.IsAssignableTo(typeof(IDisposable)) - || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); + || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); if (isAnyDisposable && !isServiceDisposable) { throw new InvalidOperationException( diff --git a/Dalamud/Service/Service{T}.cs b/Dalamud/Service/Service{T}.cs index b4bfff917..c92c8baff 100644 --- a/Dalamud/Service/Service{T}.cs +++ b/Dalamud/Service/Service{T}.cs @@ -42,8 +42,13 @@ internal static class Service where T : IServiceType else ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name); - if (exposeToPlugins) - Service.Get().RegisterSingleton(instanceTcs.Task); + // We can't use the service container to register itself. It does so in its constructor. + if (typeof(T) != typeof(ServiceContainer)) + { + Service.Get().RegisterSingleton( + instanceTcs.Task, + exposeToPlugins ? ObjectInstanceVisibility.ExposedToPlugins : ObjectInstanceVisibility.Internal); + } } /// @@ -163,7 +168,7 @@ internal static class Service where T : IServiceType return dependencyServices; var res = new List(); - + ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name); var ctor = GetServiceConstructor(); @@ -174,12 +179,12 @@ internal static class Service where T : IServiceType .Select(x => x.ParameterType) .Where(x => x.GetServiceKind() != ServiceManager.ServiceKind.None)); } - + res.AddRange(typeof(T) .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(x => x.GetCustomAttribute(true) != null) .Select(x => x.FieldType)); - + res.AddRange(typeof(T) .GetCustomAttributes() .OfType() @@ -351,7 +356,7 @@ internal static class Service where T : IServiceType var ctor = GetServiceConstructor(); if (ctor == null) throw new Exception($"Service \"{typeof(T).FullName}\" had no applicable constructor"); - + var args = await ResolveInjectedParameters(ctor.GetParameters(), additionalProvidedTypedObjects) .ConfigureAwait(false); using (Timings.Start($"{typeof(T).Name} Construct")) @@ -387,7 +392,7 @@ internal static class Service where T : IServiceType argTask = Task.FromResult(additionalProvidedTypedObjects.Single(x => x.GetType() == argType)); continue; } - + argTask = (Task)typeof(Service<>) .MakeGenericType(argType) .InvokeMember( From 22430ce054f9cc4aa0f865b1ce8d5a28b6562fca Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 1 May 2025 14:45:51 +0200 Subject: [PATCH 049/106] Make ConsoleManagerPluginScoped internal as it's supposed to be --- Dalamud/Console/ConsoleManagerPluginScoped.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs index e1eddcf7a..eb1f6fffc 100644 --- a/Dalamud/Console/ConsoleManagerPluginScoped.cs +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -19,11 +19,11 @@ namespace Dalamud.Console; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService +internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService { [ServiceManager.ServiceDependency] private readonly ConsoleManager console = Service.Get(); - + private readonly List trackedEntries = new(); /// @@ -38,7 +38,7 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService /// public string Prefix { get; private set; } - + /// void IInternalDisposableService.DisposeService() { @@ -46,7 +46,7 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService { this.console.RemoveEntry(trackedEntry); } - + this.trackedEntries.Clear(); } @@ -108,21 +108,21 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService this.console.RemoveEntry(entry); this.trackedEntries.Remove(entry); } - + private string GetPrefixedName(string name) { ArgumentNullException.ThrowIfNull(name); - + // If the name is empty, return the prefix to allow for a single command or variable to be top-level. if (name.Length == 0) return this.Prefix; - + if (name.Any(char.IsWhiteSpace)) throw new ArgumentException("Name cannot contain whitespace.", nameof(name)); - + return $"{this.Prefix}.{name}"; } - + private IConsoleCommand InternalAddCommand(string name, string description, Delegate func) { var command = this.console.AddCommand(this.GetPrefixedName(name), description, func); @@ -137,7 +137,7 @@ public class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService internal static partial class ConsoleManagerPluginUtil { private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"]; - + /// /// Get a sanitized namespace name from a plugin's internal name. /// @@ -147,10 +147,10 @@ internal static partial class ConsoleManagerPluginUtil { // Must be lowercase pluginInternalName = pluginInternalName.ToLowerInvariant(); - + // Remove all non-alphabetic characters pluginInternalName = NonAlphaRegex().Replace(pluginInternalName, string.Empty); - + // Remove reserved namespaces from the start or end foreach (var reservedNamespace in ReservedNamespaces) { @@ -158,13 +158,13 @@ internal static partial class ConsoleManagerPluginUtil { pluginInternalName = pluginInternalName[reservedNamespace.Length..]; } - + if (pluginInternalName.EndsWith(reservedNamespace)) { pluginInternalName = pluginInternalName[..^reservedNamespace.Length]; } } - + return pluginInternalName; } From 1913a4cd2c3568dc5f9b42c38add7047210314a0 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 1 May 2025 16:38:57 +0200 Subject: [PATCH 050/106] Provide services in the same order in Debug and Release --- Dalamud/Service/ServiceManager.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 1ae03a80d..92fe5ae41 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -136,8 +136,7 @@ internal static class ServiceManager TargetSigScanner scanner, Localization localization) { -#if DEBUG - lock (LoadedServices) + void ProvideAllServices() { // ServiceContainer MUST be first. The static ctor of Service will call Service.Get() // which causes a deadlock otherwise. @@ -150,6 +149,12 @@ internal static class ServiceManager ProvideService(localization); } +#if DEBUG + lock (LoadedServices) + { + ProvideAllServices() + } + return; void ProvideService(T service) where T : IServiceType @@ -159,12 +164,8 @@ internal static class ServiceManager LoadedServices.Add(typeof(T)); } #else - ProvideService(dalamud); - ProvideService(fs); - ProvideService(configuration); - ProvideService(new ServiceContainer()); - ProvideService(scanner); - ProvideService(localization); + + ProvideAllServices(); return; void ProvideService(T service) where T : IServiceType => Service.Provide(service); From ddf0a97c83f5d585ec54e82f43f974442a203f5a Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 1 May 2025 20:47:03 +0200 Subject: [PATCH 051/106] Add plugin error notifications, per-plugin event invocation wrappers --- .../Internal/DevPluginSettings.cs | 10 +- Dalamud/Console/ConsoleManagerPluginScoped.cs | 82 ++++---- Dalamud/Game/Framework.cs | 11 +- .../Interface/Internal/DalamudInterface.cs | 8 +- .../Internal/Windows/ConsoleWindow.cs | 76 ++++--- .../PluginInstaller/PluginInstallerWindow.cs | 20 ++ Dalamud/Logging/ScopedPluginLogService.cs | 23 +- Dalamud/Plugin/Internal/PluginErrorHandler.cs | 198 ++++++++++++++++++ .../Plugin/Internal/Types/LocalDevPlugin.cs | 10 + Dalamud/Service/ServiceManager.cs | 2 +- Dalamud/Service/Service{T}.cs | 3 + 11 files changed, 358 insertions(+), 85 deletions(-) create mode 100644 Dalamud/Plugin/Internal/PluginErrorHandler.cs diff --git a/Dalamud/Configuration/Internal/DevPluginSettings.cs b/Dalamud/Configuration/Internal/DevPluginSettings.cs index 361632a14..64327e658 100644 --- a/Dalamud/Configuration/Internal/DevPluginSettings.cs +++ b/Dalamud/Configuration/Internal/DevPluginSettings.cs @@ -12,16 +12,22 @@ internal sealed class DevPluginSettings /// public bool StartOnBoot { get; set; } = true; + /// + /// Gets or sets a value indicating whether we should show notifications for errors this plugin + /// is creating. + /// + public bool NotifyForErrors { get; set; } = true; + /// /// 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; - + /// /// Gets or sets a list of validation problems that have been dismissed by the user. /// diff --git a/Dalamud/Console/ConsoleManagerPluginScoped.cs b/Dalamud/Console/ConsoleManagerPluginScoped.cs index eb1f6fffc..41949c7d7 100644 --- a/Dalamud/Console/ConsoleManagerPluginScoped.cs +++ b/Dalamud/Console/ConsoleManagerPluginScoped.cs @@ -11,6 +11,47 @@ namespace Dalamud.Console; #pragma warning disable Dalamud001 +/// +/// Utility functions for the console manager. +/// +internal static partial class ConsoleManagerPluginUtil +{ + private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"]; + + /// + /// Get a sanitized namespace name from a plugin's internal name. + /// + /// The plugin's internal name. + /// A sanitized namespace. + public static string GetSanitizedNamespaceName(string pluginInternalName) + { + // Must be lowercase + pluginInternalName = pluginInternalName.ToLowerInvariant(); + + // Remove all non-alphabetic characters + pluginInternalName = NonAlphaRegex().Replace(pluginInternalName, string.Empty); + + // Remove reserved namespaces from the start or end + foreach (var reservedNamespace in ReservedNamespaces) + { + if (pluginInternalName.StartsWith(reservedNamespace)) + { + pluginInternalName = pluginInternalName[reservedNamespace.Length..]; + } + + if (pluginInternalName.EndsWith(reservedNamespace)) + { + pluginInternalName = pluginInternalName[..^reservedNamespace.Length]; + } + } + + return pluginInternalName; + } + + [GeneratedRegex(@"[^a-z]")] + private static partial Regex NonAlphaRegex(); +} + /// /// Plugin-scoped version of the console service. /// @@ -130,44 +171,3 @@ internal class ConsoleManagerPluginScoped : IConsole, IInternalDisposableService return command; } } - -/// -/// Utility functions for the console manager. -/// -internal static partial class ConsoleManagerPluginUtil -{ - private static readonly string[] ReservedNamespaces = ["dalamud", "xl", "plugin"]; - - /// - /// Get a sanitized namespace name from a plugin's internal name. - /// - /// The plugin's internal name. - /// A sanitized namespace. - public static string GetSanitizedNamespaceName(string pluginInternalName) - { - // Must be lowercase - pluginInternalName = pluginInternalName.ToLowerInvariant(); - - // Remove all non-alphabetic characters - pluginInternalName = NonAlphaRegex().Replace(pluginInternalName, string.Empty); - - // Remove reserved namespaces from the start or end - foreach (var reservedNamespace in ReservedNamespaces) - { - if (pluginInternalName.StartsWith(reservedNamespace)) - { - pluginInternalName = pluginInternalName[reservedNamespace.Length..]; - } - - if (pluginInternalName.EndsWith(reservedNamespace)) - { - pluginInternalName = pluginInternalName[..^reservedNamespace.Length]; - } - } - - return pluginInternalName; - } - - [GeneratedRegex(@"[^a-z]")] - private static partial Regex NonAlphaRegex(); -} diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 82c7f5f6c..88f9d0bb6 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -12,6 +12,7 @@ using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; @@ -47,7 +48,7 @@ internal sealed class Framework : IInternalDisposableService, IFramework private readonly ConcurrentDictionary tickDelayedTaskCompletionSources = new(); - private ulong tickCounter; + private ulong tickCounter; [ServiceManager.ServiceConstructor] private unsafe Framework() @@ -504,14 +505,18 @@ internal sealed class Framework : IInternalDisposableService, IFramework #pragma warning restore SA1015 internal class FrameworkPluginScoped : IInternalDisposableService, IFramework { + private readonly PluginErrorHandler pluginErrorHandler; + [ServiceManager.ServiceDependency] private readonly Framework frameworkService = Service.Get(); /// /// Initializes a new instance of the class. /// - internal FrameworkPluginScoped() + /// Error handler instance. + internal FrameworkPluginScoped(PluginErrorHandler pluginErrorHandler) { + this.pluginErrorHandler = pluginErrorHandler; this.frameworkService.Update += this.OnUpdateForward; } @@ -604,7 +609,7 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework } else { - this.Update?.Invoke(framework); + this.pluginErrorHandler.InvokeAndCatch(this.Update, $"{nameof(IFramework)}::{nameof(IFramework.Update)}", framework); } } } diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 8ba579d17..9760b601d 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -308,8 +308,14 @@ internal class DalamudInterface : IInternalDisposableService /// /// Opens the . /// - public void OpenLogWindow() + /// The filter to set, if not null. + public void OpenLogWindow(string? textFilter = "") { + if (textFilter != null) + { + this.consoleWindow.TextFilter = textFilter; + } + this.consoleWindow.IsOpen = true; this.consoleWindow.BringToFront(); } diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index f7ce5d145..8ef49fffc 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -41,9 +41,9 @@ internal class ConsoleWindow : Window, IDisposable // Fields below should be touched only from the main thread. private readonly RollingList logText; private readonly RollingList filteredLogEntries; - + private readonly List pluginFilters = new(); - + private readonly DalamudConfiguration configuration; private int newRolledLines; @@ -87,14 +87,14 @@ internal class ConsoleWindow : Window, IDisposable : base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { this.configuration = configuration; - + this.autoScroll = configuration.LogAutoScroll; this.autoOpen = configuration.LogOpenAtStartup; Service.GetAsync().ContinueWith(r => r.Result.Update += this.FrameworkOnUpdate); - + var cm = Service.Get(); - cm.AddCommand("clear", "Clear the console log", () => + cm.AddCommand("clear", "Clear the console log", () => { this.QueueClear(); return true; @@ -123,6 +123,19 @@ internal class ConsoleWindow : Window, IDisposable /// Gets the queue where log entries that are not processed yet are stored. public static ConcurrentQueue<(string Line, LogEvent LogEvent)> NewLogEntries { get; } = new(); + /// + /// Gets or sets the current text filter. + /// + public string TextFilter + { + get => this.textFilter; + set + { + this.textFilter = value; + this.RecompileLogFilter(); + } + } + /// public override void OnOpen() { @@ -578,7 +591,7 @@ internal class ConsoleWindow : Window, IDisposable inputWidth = ImGui.GetWindowWidth() - (ImGui.GetStyle().WindowPadding.X * 2); if (!breakInputLines) - inputWidth = (inputWidth - ImGui.GetStyle().ItemSpacing.X) / 2; + inputWidth = (inputWidth - ImGui.GetStyle().ItemSpacing.X) / 2; } else { @@ -622,24 +635,29 @@ internal class ConsoleWindow : Window, IDisposable ImGuiInputTextFlags.EnterReturnsTrue | ImGuiInputTextFlags.AutoSelectAll) || ImGui.IsItemDeactivatedAfterEdit()) { - 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; + this.RecompileLogFilter(); } } + private void RecompileLogFilter() + { + 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; + } + private void DrawSettingsPopup() { if (ImGui.Checkbox("Open at startup", ref this.autoOpen)) @@ -799,15 +817,15 @@ internal class ConsoleWindow : Window, IDisposable { if (string.IsNullOrEmpty(this.commandText)) return; - + this.historyPos = -1; - + if (this.commandText != this.configuration.LogCommandHistory.LastOrDefault()) this.configuration.LogCommandHistory.Add(this.commandText); - + if (this.configuration.LogCommandHistory.Count > HistorySize) this.configuration.LogCommandHistory.RemoveAt(0); - + this.configuration.QueueSave(); this.lastCmdSuccess = Service.Get().ProcessCommand(this.commandText); @@ -832,7 +850,7 @@ internal class ConsoleWindow : Window, IDisposable this.completionZipText = null; this.completionTabIdx = 0; break; - + case ImGuiInputTextFlags.CallbackCompletion: var textBytes = new byte[data->BufTextLen]; Marshal.Copy((IntPtr)data->Buf, textBytes, 0, data->BufTextLen); @@ -843,11 +861,11 @@ internal class ConsoleWindow : Window, IDisposable // We can't do any completion for parameters at the moment since it just calls into CommandHandler if (words.Length > 1) return 0; - + var wordToComplete = words[0]; if (wordToComplete.IsNullOrWhitespace()) return 0; - + if (this.completionZipText is not null) wordToComplete = this.completionZipText; @@ -878,7 +896,7 @@ internal class ConsoleWindow : Window, IDisposable toComplete = candidates.ElementAt(this.completionTabIdx); this.completionTabIdx = (this.completionTabIdx + 1) % candidates.Count(); } - + if (toComplete != null) { ptr.DeleteChars(0, ptr.BufTextLen); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index cfcad2ff4..c1bd64447 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -3569,6 +3569,24 @@ internal class PluginInstallerWindow : Window, IDisposable { ImGui.SetTooltip(Locs.PluginButtonToolTip_AutomaticReloading); } + + // Error Notifications + ImGui.PushStyleColor(ImGuiCol.Button, plugin.NotifyForErrors ? greenColor : redColor); + ImGui.PushStyleColor(ImGuiCol.ButtonHovered, plugin.NotifyForErrors ? greenColor : redColor); + + ImGui.SameLine(); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Bolt)) + { + plugin.NotifyForErrors ^= true; + configuration.QueueSave(); + } + + ImGui.PopStyleColor(2); + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip(Locs.PluginButtonToolTip_NotifyForErrors); + } } } @@ -4239,6 +4257,8 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButtonToolTip_AutomaticReloading => Loc.Localize("InstallerAutomaticReloading", "Automatic reloading"); + public static string PluginButtonToolTip_NotifyForErrors => Loc.Localize("InstallerNotifyForErrors", "Show Dalamud notifications when this plugin is creating errors"); + public static string PluginButtonToolTip_DeletePlugin => Loc.Localize("InstallerDeletePlugin ", "Delete plugin"); public static string PluginButtonToolTip_DeletePluginRestricted => Loc.Localize("InstallerDeletePluginRestricted", "Cannot delete right now - please restart the game."); diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs index 7305aa87b..5b0ca15e5 100644 --- a/Dalamud/Logging/ScopedPluginLogService.cs +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -1,5 +1,6 @@ using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; @@ -20,6 +21,7 @@ namespace Dalamud.Logging; internal class ScopedPluginLogService : IServiceType, IPluginLog { private readonly LocalPlugin localPlugin; + private readonly PluginErrorHandler errorHandler; private readonly LoggingLevelSwitch levelSwitch; @@ -27,10 +29,12 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog /// Initializes a new instance of the class. /// /// The plugin that owns this service. - internal ScopedPluginLogService(LocalPlugin localPlugin) + /// Error notifier service. + internal ScopedPluginLogService(LocalPlugin localPlugin, PluginErrorHandler errorHandler) { this.localPlugin = localPlugin; - + this.errorHandler = errorHandler; + this.levelSwitch = new LoggingLevelSwitch(this.GetDefaultLevel()); var loggerConfiguration = new LoggerConfiguration() @@ -40,7 +44,7 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog this.Logger = loggerConfiguration.CreateLogger(); } - + /// public ILogger Logger { get; } @@ -50,7 +54,7 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog get => this.levelSwitch.MinimumLevel; set => this.levelSwitch.MinimumLevel = value; } - + /// public void Fatal(string messageTemplate, params object[] values) => this.Write(LogEventLevel.Fatal, null, messageTemplate, values); @@ -82,11 +86,11 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog /// 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); @@ -106,10 +110,13 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog /// 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) { + if (level == LogEventLevel.Error) + this.errorHandler.NotifyError(); + this.Logger.Write( level, exception: exception, @@ -124,7 +131,7 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog 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/Internal/PluginErrorHandler.cs b/Dalamud/Plugin/Internal/PluginErrorHandler.cs new file mode 100644 index 000000000..54589595c --- /dev/null +++ b/Dalamud/Plugin/Internal/PluginErrorHandler.cs @@ -0,0 +1,198 @@ +using System.Collections.Generic; +using System.Linq.Expressions; + +using Dalamud.Interface; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Internal.Types; + +using ImGuiNET; + +using Serilog; + +namespace Dalamud.Plugin.Internal; + +/// +/// Service responsible for notifying the user when a plugin is creating errors. +/// +[ServiceManager.ScopedService] +internal class PluginErrorHandler : IServiceType +{ + private readonly LocalPlugin plugin; + private readonly NotificationManager notificationManager; + private readonly DalamudInterface di; + + private readonly Dictionary invokerCache = new(); + + private DateTime lastErrorTime = DateTime.MinValue; + private IActiveNotification? activeNotification; + + /// + /// Initializes a new instance of the class. + /// + /// The plugin we are notifying for. + /// The notification manager. + /// The dalamud interface class. + [ServiceManager.ServiceConstructor] + public PluginErrorHandler(LocalPlugin plugin, NotificationManager notificationManager, DalamudInterface di) + { + this.plugin = plugin; + this.notificationManager = notificationManager; + this.di = di; + } + + /// + /// Invoke the specified delegate and catch any exceptions that occur. + /// Writes an error message to the log if an exception occurs and shows + /// a notification if the plugin is a dev plugin and the user has enabled error notifications. + /// + /// The delegate to invoke. + /// A hint to show about the origin of the exception if an error occurs. + /// Arguments to the event handler. + /// The type of the delegate. + /// Whether invocation was successful/did not throw an exception. + public bool InvokeAndCatch( + TDelegate? eventHandler, + string hint, + params object[] args) + where TDelegate : Delegate + { + if (eventHandler == null) + return true; + + try + { + var invoker = this.GetInvoker(); + invoker(eventHandler, args); + return true; + } + catch (Exception ex) + { + Log.Error(ex, $"[{this.plugin.InternalName}] Exception in event handler {{EventHandlerName}}", hint); + this.NotifyError(); + return false; + } + } + + /// + /// Show a notification, if the plugin is a dev plugin and the user has enabled error notifications. + /// This function has a cooldown built-in. + /// + public void NotifyError() + { + if (this.plugin is not LocalDevPlugin devPlugin) + return; + + if (!devPlugin.NotifyForErrors) + return; + + // If the notification is already active, we don't need to show it again. + if (this.activeNotification is { DismissReason: null }) + return; + + var now = DateTime.UtcNow; + if (now - this.lastErrorTime < TimeSpan.FromMinutes(2)) + return; + + this.lastErrorTime = now; + + var creatingErrorsText = $"{devPlugin.Name} is creating errors"; + var notification = new Notification() + { + Title = creatingErrorsText, + Icon = INotificationIcon.From(FontAwesomeIcon.Bolt), + Type = NotificationType.Error, + InitialDuration = TimeSpan.FromSeconds(15), + MinimizedText = creatingErrorsText, + Content = $"The plugin '{devPlugin.Name}' is creating errors. Click 'Show console' to learn more.", + RespectUiHidden = false, + }; + + this.activeNotification = this.notificationManager.AddNotification(notification); + this.activeNotification.DrawActions += _ => + { + if (ImGui.Button("Show console")) + { + this.di.OpenLogWindow(this.plugin.InternalName); + this.activeNotification.DismissNow(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Show the console filtered to this plugin"); + } + + ImGui.SameLine(); + + if (ImGui.Button("Disable notifications")) + { + devPlugin.NotifyForErrors = false; + this.activeNotification.DismissNow(); + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Disable error notifications for this plugin"); + } + }; + } + + private static Action CreateInvoker() where TDelegate : Delegate + { + var delegateType = typeof(TDelegate); + var method = delegateType.GetMethod("Invoke"); + if (method == null) + throw new InvalidOperationException($"Delegate {delegateType} does not have an Invoke method."); + + var parameters = method.GetParameters(); + + // Create parameters for the lambda + var delegateParam = Expression.Parameter(delegateType, "d"); + var argsParam = Expression.Parameter(typeof(object[]), "args"); + + // Create expressions to convert array elements to parameter types + var callArgs = new Expression[parameters.Length]; + for (int i = 0; i < parameters.Length; i++) + { + var paramType = parameters[i].ParameterType; + var arrayAccess = Expression.ArrayIndex(argsParam, Expression.Constant(i)); + callArgs[i] = Expression.Convert(arrayAccess, paramType); + } + + // Create the delegate invocation expression + var callExpr = Expression.Call(delegateParam, method, callArgs); + + // If return type is not void, discard the result + Expression bodyExpr; + if (method.ReturnType != typeof(void)) + { + // Create a block that executes the call and then returns void + bodyExpr = Expression.Block( + Expression.Call(delegateParam, method, callArgs), + Expression.Empty()); + } + else + { + bodyExpr = callExpr; + } + + // Compile and return the lambda + var lambda = Expression.Lambda>( + bodyExpr, delegateParam, argsParam); + return lambda.Compile(); + } + + private Action GetInvoker() where TDelegate : Delegate + { + var delegateType = typeof(TDelegate); + + if (!this.invokerCache.TryGetValue(delegateType, out var cachedInvoker)) + { + cachedInvoker = CreateInvoker(); + this.invokerCache[delegateType] = cachedInvoker; + } + + return (Action)cachedInvoker; + } +} diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index b8f2b2708..34b54163a 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -86,6 +86,16 @@ internal sealed class LocalDevPlugin : LocalPlugin } } + /// + /// Gets or sets a value indicating whether users should be notified when this plugin + /// is causing errors. + /// + public bool NotifyForErrors + { + get => this.devSettings.NotifyForErrors; + set => this.devSettings.NotifyForErrors = value; + } + /// /// Gets an ID uniquely identifying this specific instance of a devPlugin. /// diff --git a/Dalamud/Service/ServiceManager.cs b/Dalamud/Service/ServiceManager.cs index 92fe5ae41..9847f7147 100644 --- a/Dalamud/Service/ServiceManager.cs +++ b/Dalamud/Service/ServiceManager.cs @@ -152,7 +152,7 @@ internal static class ServiceManager #if DEBUG lock (LoadedServices) { - ProvideAllServices() + ProvideAllServices(); } return; diff --git a/Dalamud/Service/Service{T}.cs b/Dalamud/Service/Service{T}.cs index c92c8baff..1f5558893 100644 --- a/Dalamud/Service/Service{T}.cs +++ b/Dalamud/Service/Service{T}.cs @@ -23,6 +23,9 @@ namespace Dalamud; [SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")] internal static class Service where T : IServiceType { + // TODO: Service should only work with singleton services. Trying to call Service.Get() on a scoped service should + // be a compile-time error. + private static readonly ServiceManager.ServiceAttribute ServiceAttribute; private static TaskCompletionSource instanceTcs = new(TaskCreationOptions.RunContinuationsAsynchronously); private static List? dependencyServices; From 30b5c0be11671cf383019e7cf8364bf6f0bb4289 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 1 May 2025 20:49:30 +0200 Subject: [PATCH 052/106] Update ConditionFlag (#2265) --- .../ClientState/Conditions/ConditionFlag.cs | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs index b83d48bd6..ef6649d7d 100644 --- a/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs +++ b/Dalamud/Game/ClientState/Conditions/ConditionFlag.cs @@ -178,11 +178,20 @@ public enum ConditionFlag /// /// Unable to execute command while occupied. /// + /// + /// Observed during Materialize (Desynthesis, Materia Extraction, Aetherial Reduction) and Repair. + /// Occupied39 = 39, /// /// Unable to execute command while crafting. /// + ExecutingCraftingAction = 40, + + /// + /// Unable to execute command while crafting. + /// + [Obsolete("Renamed to ExecutingCraftingAction.")] Crafting40 = 40, /// @@ -193,6 +202,13 @@ public enum ConditionFlag /// /// Unable to execute command while gathering. /// + /// Includes fishing. + ExecutingGatheringAction = 42, + + /// + /// Unable to execute command while gathering. + /// + [Obsolete("Renamed to ExecutingGatheringAction.")] Gathering42 = 42, /// @@ -345,6 +361,9 @@ public enum ConditionFlag /// /// Unable to execute command while mounting. /// + /// + /// Observed in Cosmic Exploration while using the actions Astrodrill (only briefly) and Solar Flarethrower. + /// Mounting71 = 71, /// @@ -412,7 +431,10 @@ public enum ConditionFlag /// ParticipatingInCrossWorldPartyOrAlliance = 84, - // Unknown85 = 85, + /// + /// Observed in Cosmic Exploration while gathering during a stellar mission. + /// + Unknown85 = 85, /// /// Unable to execute command while playing duty record. @@ -480,6 +502,9 @@ public enum ConditionFlag /// /// Cannot execute at this time. /// + /// + /// Observed in Cosmic Exploration while participating in MechaEvent. + /// Unknown96 = 96, /// @@ -502,7 +527,21 @@ public enum ConditionFlag /// EditingPortrait = 100, - // Unknown101 = 101, - // Unknown102 = 102, + /// + /// Cannot execute at this time. + /// + /// + /// Observed in Cosmic Exploration, in mech flying to FATE or during Cosmoliner use. Maybe ClientPath related. + /// + Unknown101 = 101, + + /// + /// Unable to execute command while undertaking a duty. + /// + /// + /// Used in Cosmic Exploration. + /// + PilotingMech = 102, + // Unknown103 = 103, } From 987227492ec5c04574b85c327b0eee46e18ade19 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 1 May 2025 20:59:03 +0200 Subject: [PATCH 053/106] Update ClientStructs (#2263) 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 e14220b15..ba0a66024 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit e14220b157a3f9db8d218351f6e513aa9e169e2e +Subproject commit ba0a66024d53a05ddc4d51e3bfaafc583e61e50e From 60ca710fa76c8f5f62cf7fcd5be989fefeb26f2c Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 2 May 2025 00:10:15 +0200 Subject: [PATCH 054/106] build: 12.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 daa677227..d1de48476 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.13 + 12.0.0.14 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 5304c9abc3586d2662ebea0600076bd49820af8d Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 2 May 2025 02:13:24 +0200 Subject: [PATCH 055/106] Fix CS obsoletion --- Dalamud/Game/ClientState/Fates/Fate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index de17478a0..2da2dde9d 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -250,5 +250,5 @@ internal unsafe partial class Fate : IFate /// /// Gets the territory this is located in. /// - public RowRef TerritoryType => LuminaUtils.CreateRef(this.Struct->MapMarkers[0].TerritoryId); + public RowRef TerritoryType => LuminaUtils.CreateRef(this.Struct->MapMarkers[0].MapMarkerData.TerritoryTypeId); } From 85b77226e9ee7fd75a6abf85bf921f37b846fbbb Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 2 May 2025 02:21:34 +0200 Subject: [PATCH 056/106] Clarify wording in settings and error notifications --- .../Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs | 3 ++- Dalamud/Plugin/Internal/PluginErrorHandler.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs index 1aae0dfb3..fae7e5e8f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabExperimental.cs @@ -60,7 +60,8 @@ public class SettingsTabExperimental : SettingsTab "Enable ImGui asserts"), Loc.Localize( "DalamudSettingEnableImGuiAssertsHint", - "If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins."), + "If this setting is enabled, a window containing further details will be shown when an internal assertion in ImGui fails.\nWe recommend enabling this when developing plugins. " + + "This setting does not persist and will reset when the game restarts.\nUse the setting below to enable it at startup."), c => Service.Get().ShowAsserts, (v, _) => Service.Get().ShowAsserts = v), diff --git a/Dalamud/Plugin/Internal/PluginErrorHandler.cs b/Dalamud/Plugin/Internal/PluginErrorHandler.cs index 54589595c..f9d1f73f6 100644 --- a/Dalamud/Plugin/Internal/PluginErrorHandler.cs +++ b/Dalamud/Plugin/Internal/PluginErrorHandler.cs @@ -105,7 +105,8 @@ internal class PluginErrorHandler : IServiceType Type = NotificationType.Error, InitialDuration = TimeSpan.FromSeconds(15), MinimizedText = creatingErrorsText, - Content = $"The plugin '{devPlugin.Name}' is creating errors. Click 'Show console' to learn more.", + Content = $"The plugin '{devPlugin.Name}' is creating errors. Click 'Show console' to learn more.\n\n" + + $"You are seeing this because '{devPlugin.Name}' is a Dev Plugin.", RespectUiHidden = false, }; From 09f519ce6f1fa98ece499d70c6014de234d70b74 Mon Sep 17 00:00:00 2001 From: foophoof Date: Fri, 2 May 2025 05:05:42 +0100 Subject: [PATCH 057/106] Convert PluginStatWindow to ImRaii (#2268) Currently the Hooks tab asserts because of a missing ImGui.EndTabItem. Instead of just adding that, I took the opportunity to convert everything to use ImRaii instead. --- .../Internal/Windows/PluginStatWindow.cs | 435 +++++++++--------- 1 file changed, 219 insertions(+), 216 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index eeafa98e7..397262ed0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -8,6 +8,7 @@ using Dalamud.Hooking.Internal; using Dalamud.Interface.Components; using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; @@ -44,51 +45,54 @@ internal class PluginStatWindow : Window { var pluginManager = Service.Get(); - if (!ImGui.BeginTabBar("Stat Tabs")) + using var tabBar = ImRaii.TabBar("Stat Tabs"); + if (!tabBar) return; - if (ImGui.BeginTabItem("Draw times")) + using (var tabItem = ImRaii.TabItem("Draw times")) { - var doStats = UiBuilder.DoStats; - - if (ImGui.Checkbox("Enable Draw Time Tracking", ref doStats)) + if (tabItem) { - UiBuilder.DoStats = doStats; - } + var doStats = UiBuilder.DoStats; - if (doStats) - { - ImGui.SameLine(); - if (ImGui.Button("Reset")) + if (ImGui.Checkbox("Enable Draw Time Tracking", ref doStats)) { - foreach (var plugin in pluginManager.InstalledPlugins) - { - if (plugin.DalamudInterface != null) - { - plugin.DalamudInterface.LocalUiBuilder.LastDrawTime = -1; - plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime = -1; - plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Clear(); - } - } + UiBuilder.DoStats = doStats; } - var loadedPlugins = pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded); - var totalLast = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.LastDrawTime ?? 0); - var totalAverage = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0); + if (doStats) + { + ImGui.SameLine(); + if (ImGui.Button("Reset")) + { + foreach (var plugin in pluginManager.InstalledPlugins) + { + if (plugin.DalamudInterface != null) + { + plugin.DalamudInterface.LocalUiBuilder.LastDrawTime = -1; + plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime = -1; + plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Clear(); + } + } + } - ImGuiComponents.TextWithLabel("Total Last", $"{totalLast / 10000f:F4}ms", "All last draw times added together"); - ImGui.SameLine(); - ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage / 10000f:F4}ms", "All average draw times added together"); - ImGui.SameLine(); - ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times"); + var loadedPlugins = pluginManager.InstalledPlugins.Where(plugin => plugin.State == PluginState.Loaded); + var totalLast = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.LastDrawTime ?? 0); + var totalAverage = loadedPlugins.Sum(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0); - ImGui.InputTextWithHint( - "###PluginStatWindow_DrawSearch", - "Search", - ref this.drawSearchText, - 500); + ImGuiComponents.TextWithLabel("Total Last", $"{totalLast / 10000f:F4}ms", "All last draw times added together"); + ImGui.SameLine(); + ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage / 10000f:F4}ms", "All average draw times added together"); + ImGui.SameLine(); + ImGuiComponents.TextWithLabel("Collective Average", $"{(loadedPlugins.Any() ? totalAverage / loadedPlugins.Count() / 10000f : 0):F4}ms", "Average of all average draw times"); - if (ImGui.BeginTable( + ImGui.InputTextWithHint( + "###PluginStatWindow_DrawSearch", + "Search", + ref this.drawSearchText, + 500); + + using var table = ImRaii.Table( "##PluginStatsDrawTimes", 4, ImGuiTableFlags.RowBg @@ -97,99 +101,100 @@ internal class PluginStatWindow : Window | ImGuiTableFlags.Resizable | ImGuiTableFlags.ScrollY | ImGuiTableFlags.Reorderable - | ImGuiTableFlags.Hideable)) - { - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn("Plugin"); - ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort); // Changes too fast to sort - ImGui.TableSetupColumn("Longest"); - ImGui.TableSetupColumn("Average"); - ImGui.TableHeadersRow(); + | ImGuiTableFlags.Hideable); - var sortSpecs = ImGui.TableGetSortSpecs(); - loadedPlugins = sortSpecs.Specs.ColumnIndex switch + if (table) { - 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? loadedPlugins.OrderBy(plugin => plugin.Name) - : loadedPlugins.OrderByDescending(plugin => plugin.Name), - 2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0) - : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0), - 3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0) - : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0), - _ => loadedPlugins, - }; + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Plugin"); + ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort); // Changes too fast to sort + ImGui.TableSetupColumn("Longest"); + ImGui.TableSetupColumn("Average"); + ImGui.TableHeadersRow(); - foreach (var plugin in loadedPlugins) - { - if (!this.drawSearchText.IsNullOrEmpty() - && !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase)) + var sortSpecs = ImGui.TableGetSortSpecs(); + loadedPlugins = sortSpecs.Specs.ColumnIndex switch { - continue; - } + 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? loadedPlugins.OrderBy(plugin => plugin.Name) + : loadedPlugins.OrderByDescending(plugin => plugin.Name), + 2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0) + : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.MaxDrawTime ?? 0), + 3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? loadedPlugins.OrderBy(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0) + : loadedPlugins.OrderByDescending(plugin => plugin.DalamudInterface?.LocalUiBuilder.DrawTimeHistory.DefaultIfEmpty().Average() ?? 0), + _ => loadedPlugins, + }; - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - ImGui.Text(plugin.Manifest.Name); - - if (plugin.DalamudInterface != null) + foreach (var plugin in loadedPlugins) { - ImGui.TableNextColumn(); - ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.LastDrawTime / 10000f:F4}ms"); + if (!this.drawSearchText.IsNullOrEmpty() + && !plugin.Manifest.Name.Contains(this.drawSearchText, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + ImGui.TableNextRow(); ImGui.TableNextColumn(); - ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime / 10000f:F4}ms"); + ImGui.Text(plugin.Manifest.Name); - ImGui.TableNextColumn(); - ImGui.Text(plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Count > 0 - ? $"{plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms" - : "-"); + if (plugin.DalamudInterface != null) + { + ImGui.TableNextColumn(); + ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.LastDrawTime / 10000f:F4}ms"); + + ImGui.TableNextColumn(); + ImGui.Text($"{plugin.DalamudInterface.LocalUiBuilder.MaxDrawTime / 10000f:F4}ms"); + + ImGui.TableNextColumn(); + ImGui.Text(plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Count > 0 + ? $"{plugin.DalamudInterface.LocalUiBuilder.DrawTimeHistory.Average() / 10000f:F4}ms" + : "-"); + } } } - - ImGui.EndTable(); } } - - ImGui.EndTabItem(); } - if (ImGui.BeginTabItem("Framework times")) + using (var tabItem = ImRaii.TabItem("Framework times")) { - var doStats = Framework.StatsEnabled; - - if (ImGui.Checkbox("Enable Framework Update Tracking", ref doStats)) + if (tabItem) { - Framework.StatsEnabled = doStats; - } + var doStats = Framework.StatsEnabled; - if (doStats) - { - ImGui.SameLine(); - if (ImGui.Button("Reset")) + if (ImGui.Checkbox("Enable Framework Update Tracking", ref doStats)) { - Framework.StatsHistory.Clear(); + Framework.StatsEnabled = doStats; } - var statsHistory = Framework.StatsHistory.ToArray(); - var totalLast = statsHistory.Sum(stats => stats.Value.LastOrDefault()); - var totalAverage = statsHistory.Sum(stats => stats.Value.DefaultIfEmpty().Average()); + if (doStats) + { + ImGui.SameLine(); + if (ImGui.Button("Reset")) + { + Framework.StatsHistory.Clear(); + } - ImGuiComponents.TextWithLabel("Total Last", $"{totalLast:F4}ms", "All last update times added together"); - ImGui.SameLine(); - ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage:F4}ms", "All average update times added together"); - ImGui.SameLine(); - ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times"); + var statsHistory = Framework.StatsHistory.ToArray(); + var totalLast = statsHistory.Sum(stats => stats.Value.LastOrDefault()); + var totalAverage = statsHistory.Sum(stats => stats.Value.DefaultIfEmpty().Average()); - ImGui.InputTextWithHint( - "###PluginStatWindow_FrameworkSearch", - "Search", - ref this.frameworkSearchText, - 500); + ImGuiComponents.TextWithLabel("Total Last", $"{totalLast:F4}ms", "All last update times added together"); + ImGui.SameLine(); + ImGuiComponents.TextWithLabel("Total Average", $"{totalAverage:F4}ms", "All average update times added together"); + ImGui.SameLine(); + ImGuiComponents.TextWithLabel("Collective Average", $"{(statsHistory.Any() ? totalAverage / statsHistory.Length : 0):F4}ms", "Average of all average update times"); - if (ImGui.BeginTable( + ImGui.InputTextWithHint( + "###PluginStatWindow_FrameworkSearch", + "Search", + ref this.frameworkSearchText, + 500); + + using var table = ImRaii.Table( "##PluginStatsFrameworkTimes", 4, ImGuiTableFlags.RowBg @@ -198,77 +203,77 @@ internal class PluginStatWindow : Window | ImGuiTableFlags.Resizable | ImGuiTableFlags.ScrollY | ImGuiTableFlags.Reorderable - | ImGuiTableFlags.Hideable)) - { - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250); - ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort, 50); // Changes too fast to sort - ImGui.TableSetupColumn("Longest", ImGuiTableColumnFlags.None, 50); - ImGui.TableSetupColumn("Average", ImGuiTableColumnFlags.None, 50); - ImGui.TableHeadersRow(); - - var sortSpecs = ImGui.TableGetSortSpecs(); - statsHistory = sortSpecs.Specs.ColumnIndex switch + | ImGuiTableFlags.Hideable); + if (table) { - 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? statsHistory.OrderBy(handler => handler.Key).ToArray() - : statsHistory.OrderByDescending(handler => handler.Key).ToArray(), - 2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Max()).ToArray() - : statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Max()).ToArray(), - 3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending - ? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Average()).ToArray() - : statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Average()).ToArray(), - _ => statsHistory, - }; + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Method", ImGuiTableColumnFlags.None, 250); + ImGui.TableSetupColumn("Last", ImGuiTableColumnFlags.NoSort, 50); // Changes too fast to sort + ImGui.TableSetupColumn("Longest", ImGuiTableColumnFlags.None, 50); + ImGui.TableSetupColumn("Average", ImGuiTableColumnFlags.None, 50); + ImGui.TableHeadersRow(); - foreach (var handlerHistory in statsHistory) - { - if (!handlerHistory.Value.Any()) + var sortSpecs = ImGui.TableGetSortSpecs(); + statsHistory = sortSpecs.Specs.ColumnIndex switch { - continue; - } + 0 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? statsHistory.OrderBy(handler => handler.Key).ToArray() + : statsHistory.OrderByDescending(handler => handler.Key).ToArray(), + 2 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Max()).ToArray() + : statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Max()).ToArray(), + 3 => sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending + ? statsHistory.OrderBy(handler => handler.Value.DefaultIfEmpty().Average()).ToArray() + : statsHistory.OrderByDescending(handler => handler.Value.DefaultIfEmpty().Average()).ToArray(), + _ => statsHistory, + }; - if (!this.frameworkSearchText.IsNullOrEmpty() - && handlerHistory.Key != null - && !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase)) + foreach (var handlerHistory in statsHistory) { - continue; + if (!handlerHistory.Value.Any()) + { + continue; + } + + if (!this.frameworkSearchText.IsNullOrEmpty() + && handlerHistory.Key != null + && !handlerHistory.Key.Contains(this.frameworkSearchText, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + ImGui.Text($"{handlerHistory.Key}"); + + ImGui.TableNextColumn(); + ImGui.Text($"{handlerHistory.Value.Last():F4}ms"); + + ImGui.TableNextColumn(); + ImGui.Text($"{handlerHistory.Value.Max():F4}ms"); + + ImGui.TableNextColumn(); + ImGui.Text($"{handlerHistory.Value.Average():F4}ms"); } - - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - ImGui.Text($"{handlerHistory.Key}"); - - ImGui.TableNextColumn(); - ImGui.Text($"{handlerHistory.Value.Last():F4}ms"); - - ImGui.TableNextColumn(); - ImGui.Text($"{handlerHistory.Value.Max():F4}ms"); - - ImGui.TableNextColumn(); - ImGui.Text($"{handlerHistory.Value.Average():F4}ms"); } - - ImGui.EndTable(); } } - - ImGui.EndTabItem(); } - if (ImGui.BeginTabItem("Hooks")) + using (var tabItem = ImRaii.TabItem("Hooks")) { - ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks); + if (tabItem) + { + ImGui.Checkbox("Show Dalamud Hooks", ref this.showDalamudHooks); - ImGui.InputTextWithHint( - "###PluginStatWindow_HookSearch", - "Search", - ref this.hookSearchText, - 500); + ImGui.InputTextWithHint( + "###PluginStatWindow_HookSearch", + "Search", + ref this.hookSearchText, + 500); - if (ImGui.BeginTable( + using var table = ImRaii.Table( "##PluginStatsHooks", 4, ImGuiTableFlags.RowBg @@ -276,80 +281,78 @@ internal class PluginStatWindow : Window | ImGuiTableFlags.Resizable | ImGuiTableFlags.ScrollY | ImGuiTableFlags.Reorderable - | ImGuiTableFlags.Hideable)) - { - ImGui.TableSetupScrollFreeze(0, 1); - ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250); - ImGui.TableSetupColumn("Address", ImGuiTableColumnFlags.None, 100); - ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.None, 40); - ImGui.TableSetupColumn("Backend", ImGuiTableColumnFlags.None, 40); - ImGui.TableHeadersRow(); - - foreach (var (guid, trackedHook) in HookManager.TrackedHooks) + | ImGuiTableFlags.Hideable); + if (table) { - try + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableSetupColumn("Detour Method", ImGuiTableColumnFlags.None, 250); + ImGui.TableSetupColumn("Address", ImGuiTableColumnFlags.None, 100); + ImGui.TableSetupColumn("Status", ImGuiTableColumnFlags.None, 40); + ImGui.TableSetupColumn("Backend", ImGuiTableColumnFlags.None, 40); + ImGui.TableHeadersRow(); + + foreach (var (guid, trackedHook) in HookManager.TrackedHooks) { - if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) - continue; - - if (!this.hookSearchText.IsNullOrEmpty()) + try { - if ((trackedHook.Delegate.Target == null || !trackedHook.Delegate.Target.ToString().Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase)) - && !trackedHook.Delegate.Method.Name.Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase)) + if (!this.showDalamudHooks && trackedHook.Assembly == Assembly.GetExecutingAssembly()) continue; - } - ImGui.TableNextRow(); - - ImGui.TableNextColumn(); - - ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}"); - ImGui.TextDisabled(trackedHook.Assembly.FullName); - ImGui.TableNextColumn(); - if (!trackedHook.Hook.IsDisposed) - { - if (ImGui.Selectable($"{trackedHook.Hook.Address.ToInt64():X}")) + if (!this.hookSearchText.IsNullOrEmpty()) { - ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}"); - Service.Get().AddNotification($"{trackedHook.Hook.Address.ToInt64():X}", "Copied to clipboard", NotificationType.Success); + if ((trackedHook.Delegate.Target == null || !trackedHook.Delegate.Target.ToString().Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase)) + && !trackedHook.Delegate.Method.Name.Contains(this.hookSearchText, StringComparison.OrdinalIgnoreCase)) + continue; } - var processMemoryOffset = trackedHook.InProcessMemory; - if (processMemoryOffset.HasValue) + ImGui.TableNextRow(); + + ImGui.TableNextColumn(); + + ImGui.Text($"{trackedHook.Delegate.Target} :: {trackedHook.Delegate.Method.Name}"); + ImGui.TextDisabled(trackedHook.Assembly.FullName); + ImGui.TableNextColumn(); + if (!trackedHook.Hook.IsDisposed) { - if (ImGui.Selectable($"ffxiv_dx11.exe+{processMemoryOffset:X}")) + if (ImGui.Selectable($"{trackedHook.Hook.Address.ToInt64():X}")) { - ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}"); - Service.Get().AddNotification($"ffxiv_dx11.exe+{processMemoryOffset:X}", "Copied to clipboard", NotificationType.Success); + ImGui.SetClipboardText($"{trackedHook.Hook.Address.ToInt64():X}"); + Service.Get().AddNotification($"{trackedHook.Hook.Address.ToInt64():X}", "Copied to clipboard", NotificationType.Success); + } + + var processMemoryOffset = trackedHook.InProcessMemory; + if (processMemoryOffset.HasValue) + { + if (ImGui.Selectable($"ffxiv_dx11.exe+{processMemoryOffset:X}")) + { + ImGui.SetClipboardText($"ffxiv_dx11.exe+{processMemoryOffset:X}"); + Service.Get().AddNotification($"ffxiv_dx11.exe+{processMemoryOffset:X}", "Copied to clipboard", NotificationType.Success); + } } } + + ImGui.TableNextColumn(); + + if (trackedHook.Hook.IsDisposed) + { + ImGui.Text("Disposed"); + } + else + { + ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled"); + } + + ImGui.TableNextColumn(); + + ImGui.Text(trackedHook.Hook.BackendName); } - - ImGui.TableNextColumn(); - - if (trackedHook.Hook.IsDisposed) + catch (Exception ex) { - ImGui.Text("Disposed"); + Log.Error(ex, "Error drawing hooks in plugin stats"); } - else - { - ImGui.Text(trackedHook.Hook.IsEnabled ? "Enabled" : "Disabled"); - } - - ImGui.TableNextColumn(); - - ImGui.Text(trackedHook.Hook.BackendName); - } - catch (Exception ex) - { - Log.Error(ex, "Error drawing hooks in plugin stats"); } } - - ImGui.EndTable(); } } - - ImGui.EndTabBar(); } } From 20ef5fb919d12121913484ba8c19f9da610ed2f9 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 3 May 2025 17:47:44 +0200 Subject: [PATCH 058/106] build: 12.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 d1de48476..934d050a2 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.14 + 12.0.0.15 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 2b49170f6ab811ecee0fef152f2eb00c04259c17 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 7 May 2025 00:00:32 +0200 Subject: [PATCH 059/106] Add configurable "anchor position" for notifications --- .../Internal/DalamudConfiguration.cs | 7 + .../Internal/ActiveNotification.ImGui.cs | 79 ++++++- .../Internal/NotificationConstants.cs | 5 + .../Internal/NotificationManager.cs | 74 +++++- .../Internal/NotificationPositionChooser.cs | 213 ++++++++++++++++++ .../Internal/NotificationSnapDirection.cs | 27 +++ .../Windows/Settings/Tabs/SettingsTabLook.cs | 12 +- 7 files changed, 401 insertions(+), 16 deletions(-) create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs create mode 100644 Dalamud/Interface/ImGuiNotification/Internal/NotificationSnapDirection.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 2766ba681..6816b166f 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -3,12 +3,14 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; +using System.Numerics; using System.Runtime.InteropServices; using System.Threading.Tasks; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.FontIdentifier; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ReShadeHandling; using Dalamud.Interface.Style; @@ -501,6 +503,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public bool SendUpdateNotificationToChat { get; set; } = false; + /// + /// Gets or sets a value indicating where notifications are anchored to on the screen. + /// + public Vector2 NotificationAnchorPosition { get; set; } = new(1f, 1f); + /// /// Load a configuration from the provided path. /// diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index c672dd3b3..ce70ab180 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -15,8 +15,9 @@ internal sealed partial class ActiveNotification /// Draws this notification. /// The maximum width of the notification window. /// The offset from the bottom. + /// Where notifications are anchored to on the screen. /// The height of the notification. - public float Draw(float width, float offsetY) + public float Draw(float width, float offsetY, Vector2 anchorPosition, NotificationSnapDirection snapDirection) { var opacity = Math.Clamp( @@ -35,7 +36,6 @@ internal sealed partial class ActiveNotification (NotificationConstants.ScaledWindowPadding * 2); var viewport = ImGuiHelpers.MainViewport; - var viewportPos = viewport.WorkPos; var viewportSize = viewport.WorkSize; ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); @@ -52,13 +52,78 @@ internal sealed partial class ActiveNotification NotificationConstants.BackgroundOpacity)); } + Vector2 topLeft; + Vector2 pivot; + if (snapDirection is NotificationSnapDirection.Top or NotificationSnapDirection.Bottom) + { + // Top or bottom + var xPos = (viewportSize.X - width) * anchorPosition.X; + xPos = Math.Max(NotificationConstants.ScaledViewportEdgeMargin, Math.Min(viewportSize.X - width - NotificationConstants.ScaledViewportEdgeMargin, xPos)); + + if (snapDirection == NotificationSnapDirection.Top) + { + // Top + var yPos = NotificationConstants.ScaledViewportEdgeMargin - offsetY; + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 0); + } + else + { + // Bottom + var yPos = viewportSize.Y - offsetY - NotificationConstants.ScaledViewportEdgeMargin; + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 1); + } + } + else + { + // Left or Right + var yPos = (viewportSize.Y * anchorPosition.Y) - offsetY; + yPos = Math.Max( + NotificationConstants.ScaledViewportEdgeMargin, + Math.Min(viewportSize.Y - offsetY - NotificationConstants.ScaledViewportEdgeMargin, yPos)); + + if (snapDirection == NotificationSnapDirection.Left) + { + // Left + var xPos = NotificationConstants.ScaledViewportEdgeMargin; + + if (anchorPosition.Y > 0.5f) + { + // Bottom + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 1); + } + else + { + // Top + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 0); + } + } + else + { + // Right + var xPos = viewportSize.X - width - NotificationConstants.ScaledViewportEdgeMargin; + + if (anchorPosition.Y > 0.5f) + { + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 1); + } + else + { + topLeft = new Vector2(xPos, yPos); + pivot = new(0, 0); + } + } + } + ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowPos( - (viewportPos + viewportSize) - - new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - - new Vector2(0, offsetY), + topLeft, ImGuiCond.Always, - Vector2.One); + pivot); ImGui.SetNextWindowSizeConstraints( new(width, actionWindowHeight), new( @@ -142,7 +207,7 @@ internal sealed partial class ActiveNotification ImGui.PopStyleColor(); ImGui.PopStyleVar(3); - return windowSize.Y; + return NotificationManager.ShouldScrollDownwards(anchorPosition) ? -windowSize.Y : windowSize.Y; } /// Calculates the effective expiry, taking ImGui window state into account. diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs index 8b7ce7bfa..b79855a6b 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -54,6 +54,11 @@ internal static class NotificationConstants /// public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + /// + /// The ratio of the screen at which the notification window will snap to the top or bottom of the screen. + /// + public const float NotificationTopBottomSnapMargin = 0.08f; + /// Default duration of the notification. public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(7); diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs index 4157d1356..b8759fc2a 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -1,6 +1,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; +using System.Numerics; +using Dalamud.Configuration.Internal; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ManagedFontAtlas; @@ -22,9 +24,14 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe [ServiceManager.ServiceDependency] private readonly GameGui gameGui = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration configuration = Service.Get(); + private readonly List notifications = new(); private readonly ConcurrentBag pendingNotifications = new(); + private NotificationPositionChooser? positionChooser; + [ServiceManager.ServiceConstructor] private NotificationManager(FontAtlasFactory fontAtlasFactory) { @@ -48,6 +55,48 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe /// Gets the private atlas for use with notification windows. private IFontAtlas PrivateAtlas { get; } + /// + /// Calculate the width to be used to draw notifications. + /// + /// The width. + public static float CalculateNotificationWidth() + { + var viewportSize = ImGuiHelpers.MainViewport.WorkSize; + var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; + width += NotificationConstants.ScaledWindowPadding * 3; + width += NotificationConstants.ScaledIconSize; + return Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth); + } + + /// + /// Check if notifications should scroll downwards on the screen, based on the anchor position. + /// + /// Where notifications are anchored to. + /// A value indicating wether notifications should scroll downwards. + public static bool ShouldScrollDownwards(Vector2 anchorPosition) + { + return anchorPosition.Y < 0.5f; + } + + /// + /// Choose the snap position for a notification based on the anchor position. + /// + /// Where notifications are anchored to. + /// The snap position. + public static NotificationSnapDirection ChooseSnapDirection(Vector2 anchorPosition) + { + if (anchorPosition.Y <= NotificationConstants.NotificationTopBottomSnapMargin) + return NotificationSnapDirection.Top; + + if (anchorPosition.Y >= 1f - NotificationConstants.NotificationTopBottomSnapMargin) + return NotificationSnapDirection.Bottom; + + if (anchorPosition.X <= 0.5f) + return NotificationSnapDirection.Left; + + return NotificationSnapDirection.Right; + } + /// public void DisposeService() { @@ -98,25 +147,38 @@ internal class NotificationManager : INotificationManager, IInternalDisposableSe /// Draw all currently queued notifications. public void Draw() { - var viewportSize = ImGuiHelpers.MainViewport.WorkSize; var height = 0f; var uiHidden = this.gameGui.GameUiHidden; while (this.pendingNotifications.TryTake(out var newNotification)) this.notifications.Add(newNotification); - var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; - width += NotificationConstants.ScaledWindowPadding * 3; - width += NotificationConstants.ScaledIconSize; - width = Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth); + var width = CalculateNotificationWidth(); this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); + + var scrollsDownwards = ShouldScrollDownwards(this.configuration.NotificationAnchorPosition); + var snapDirection = ChooseSnapDirection(this.configuration.NotificationAnchorPosition); + foreach (var tn in this.notifications) { if (uiHidden && tn.RespectUiHidden) continue; - height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap; + + height += tn.Draw(width, height, this.configuration.NotificationAnchorPosition, snapDirection); + height += scrollsDownwards ? -NotificationConstants.ScaledWindowGap : NotificationConstants.ScaledWindowGap; } + + this.positionChooser?.Draw(); + } + + /// + /// Starts the position chooser for notifications. Will block the UI until the user makes a selection. + /// + public void StartPositionChooser() + { + this.positionChooser = new NotificationPositionChooser(this.configuration); + this.positionChooser.SelectionMade += () => { this.positionChooser = null; }; } } diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs new file mode 100644 index 000000000..6ad42ad80 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs @@ -0,0 +1,213 @@ +using System.Numerics; + +using Dalamud.Configuration.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Raii; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// +/// Class responsible for drawing UI that lets users choose the position of notifications. +/// +internal class NotificationPositionChooser +{ + private readonly DalamudConfiguration configuration; + private readonly Vector2 previousAnchorPosition; + + private Vector2 currentAnchorPosition; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration we are reading or writing from. + public NotificationPositionChooser(DalamudConfiguration configuration) + { + this.configuration = configuration; + this.previousAnchorPosition = configuration.NotificationAnchorPosition; + } + + /// + /// Gets or sets an action that is invoked when the user makes a selection. + /// + public event Action? SelectionMade; + + /// + /// Draw the chooser UI. + /// + public void Draw() + { + 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.SetNextWindowFocus(); + ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos); + ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size); + ImGuiHelpers.ForceNextWindowMainViewport(); + + ImGui.SetNextWindowBgAlpha(0.6f); + + ImGui.Begin( + "###NotificationPositionChooser", + ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNav); + + var mousePosUnit = ImGui.GetMousePos() / ImGuiHelpers.MainViewport.Size; + + // Store the offset as a Vector2 + this.currentAnchorPosition = mousePosUnit; + + DrawPreview(this.previousAnchorPosition, 0.3f); + DrawPreview(this.currentAnchorPosition, 1f); + + if (ImGui.IsMouseClicked(ImGuiMouseButton.Right)) + { + this.SelectionMade?.Invoke(); + } + else if (ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + { + this.configuration.NotificationAnchorPosition = this.currentAnchorPosition; + this.configuration.QueueSave(); + + this.SelectionMade?.Invoke(); + } + + // In the middle of the screen, draw some instructions + string[] instructions = ["Drag to move the notifications to where you would like them to appear.", + "Click to select the position.", + "Right-click to close without making changes."]; + + var dl = ImGui.GetWindowDrawList(); + for (var i = 0; i < instructions.Length; i++) + { + var instruction = instructions[i]; + var instructionSize = ImGui.CalcTextSize(instruction); + var instructionPos = new Vector2( + ImGuiHelpers.MainViewport.Size.X / 2 - instructionSize.X / 2, + ImGuiHelpers.MainViewport.Size.Y / 2 - instructionSize.Y / 2 + i * instructionSize.Y); + dl.AddText(instructionPos, 0xFFFFFFFF, instruction); + } + + ImGui.End(); + } + + private static void DrawPreview(Vector2 anchorPosition, float borderAlpha) + { + var dl = ImGui.GetWindowDrawList(); + var width = NotificationManager.CalculateNotificationWidth(); + var height = 100f * ImGuiHelpers.GlobalScale; + var smallBoxHeight = height * 0.4f; + var edgeMargin = NotificationConstants.ScaledViewportEdgeMargin; + var spacing = 10f * ImGuiHelpers.GlobalScale; + + var viewportSize = ImGuiHelpers.MainViewport.Size; + var borderColor = ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, borderAlpha)); + var borderThickness = 4.0f * ImGuiHelpers.GlobalScale; + var borderRounding = 4.0f * ImGuiHelpers.GlobalScale; + var backgroundColor = new Vector4(0, 0, 0, 0.5f); // Semi-transparent black + + // Calculate positions based on the snap position + Vector2 topLeft, bottomRight, smallTopLeft, smallBottomRight; + + var snapPos = NotificationManager.ChooseSnapDirection(anchorPosition); + if (snapPos is NotificationSnapDirection.Top or NotificationSnapDirection.Bottom) + { + // Calculate X position - same logic for top and bottom + var xPos = (viewportSize.X - width) * anchorPosition.X; + xPos = Math.Max(edgeMargin, Math.Min(viewportSize.X - width - edgeMargin, xPos)); + + if (snapPos == NotificationSnapDirection.Top) + { + // For top position: big box at top, small box below it + var yPos = edgeMargin; + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + + smallTopLeft = new Vector2(xPos, yPos + height + spacing); + smallBottomRight = new Vector2(xPos + width, yPos + height + spacing + smallBoxHeight); + } + else + { + // For bottom position: big box at bottom, small box above it + var yPos = viewportSize.Y - height - edgeMargin; + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + + smallTopLeft = new Vector2(xPos, yPos - smallBoxHeight - spacing); + smallBottomRight = new Vector2(xPos + width, yPos - spacing); + } + } + else + { + // For left and right positions, boxes are still stacked vertically (one above the other) + // Only the horizontal position changes + + // Calculate Y position based on unit offset - used for both left and right positions + var yPos = (viewportSize.Y - height) * anchorPosition.Y; + yPos = Math.Max(edgeMargin, Math.Min(viewportSize.Y - height - edgeMargin, yPos)); + + if (snapPos == NotificationSnapDirection.Left) + { + // For left position: boxes are at the left edge of the screen + var xPos = edgeMargin; + + if (anchorPosition.Y > 0.5f) + { + // Small box on top + smallTopLeft = new Vector2(xPos, yPos - smallBoxHeight - spacing); + smallBottomRight = new Vector2(xPos + width, yPos - spacing); + + // Big box below + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + } + else + { + // Big box on top + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + + // Small box below + smallTopLeft = new Vector2(xPos, yPos + height + spacing); + smallBottomRight = new Vector2(xPos + width, yPos + height + spacing + smallBoxHeight); + } + } + else + { + // For right position: boxes are at the right edge of the screen + var xPos = viewportSize.X - width - edgeMargin; + + if (anchorPosition.Y > 0.5f) + { + // Small box on top + smallTopLeft = new Vector2(xPos, yPos - smallBoxHeight - spacing); + smallBottomRight = new Vector2(xPos + width, yPos - spacing); + + // Big box below + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + } + else + { + // Big box on top + topLeft = new Vector2(xPos, yPos); + bottomRight = new Vector2(xPos + width, yPos + height); + + // Small box below + smallTopLeft = new Vector2(xPos, yPos + height + spacing); + smallBottomRight = new Vector2(xPos + width, yPos + height + spacing + smallBoxHeight); + } + } + } + + // Draw the big box + dl.AddRectFilled(topLeft, bottomRight, ImGui.ColorConvertFloat4ToU32(backgroundColor), borderRounding, ImDrawFlags.RoundCornersAll); + dl.AddRect(topLeft, bottomRight, borderColor, borderRounding, ImDrawFlags.RoundCornersAll, borderThickness); + + // Draw the small box + dl.AddRectFilled(smallTopLeft, smallBottomRight, ImGui.ColorConvertFloat4ToU32(backgroundColor), borderRounding, ImDrawFlags.RoundCornersAll); + dl.AddRect(smallTopLeft, smallBottomRight, borderColor, borderRounding, ImDrawFlags.RoundCornersAll, borderThickness); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationSnapDirection.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationSnapDirection.cs new file mode 100644 index 000000000..1666e7a8c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationSnapDirection.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// +/// Where notifications should snap to on the screen when they are shown. +/// +public enum NotificationSnapDirection +{ + /// + /// Snap to the top of the screen. + /// + Top, + + /// + /// Snap to the bottom of the screen. + /// + Bottom, + + /// + /// Snap to the left of the screen. + /// + Left, + + /// + /// Snap to the right of the screen. + /// + Right, +} diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 7f75dbf29..112fa3a3f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; using Dalamud.Interface.ImGuiFontChooserDialog; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Windows.PluginInstaller; using Dalamud.Interface.Internal.Windows.Settings.Widgets; using Dalamud.Interface.ManagedFontAtlas.Internals; @@ -38,7 +39,7 @@ public class SettingsTabLook : SettingsTab private IFontSpec defaultFontSpec = null!; public override SettingsEntry[] Entries { get; } = - { + [ new GapSettingsEntry(5, true), new ButtonSettingsEntry( @@ -46,6 +47,11 @@ public class SettingsTabLook : SettingsTab Loc.Localize("DalamudSettingsStyleEditorHint", "Modify the look & feel of Dalamud windows."), () => Service.Get().OpenStyleEditor()), + new ButtonSettingsEntry( + Loc.Localize("DalamudSettingsOpenNotificationEditor", "Modify Notification Position"), + Loc.Localize("DalamudSettingsNotificationEditorHint", "Choose where Dalamud notifications appear on the screen."), + () => Service.Get().StartPositionChooser()), + new SettingsEntry( Loc.Localize("DalamudSettingsUseDarkMode", "Use Windows immersive/dark mode"), Loc.Localize("DalamudSettingsUseDarkModeHint", "This will cause the FFXIV window title bar to follow your preferred Windows color settings, and switch to dark mode if enabled."), @@ -167,8 +173,8 @@ public class SettingsTabLook : SettingsTab ImGui.TextUnformatted("\uE020\uE021\uE022\uE023\uE024\uE025\uE026\uE027"); ImGui.PopStyleVar(1); }, - }, - }; + } + ]; public override string Title => Loc.Localize("DalamudSettingsVisual", "Look & Feel"); From 50fdc2e5c505b4e915875c8cd916d8ed1ca9fe4f Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 7 May 2025 00:00:48 +0200 Subject: [PATCH 060/106] TSM should force itself to front if it is expanded --- Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index cbb6998ac..2b8973dc5 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -152,6 +152,10 @@ internal class TitleScreenMenuWindow : Window, IDisposable { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(0, 0)); ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(0, 0)); + + if (this.state == State.Show) + ImGui.SetNextWindowFocus(); + base.PreDraw(); } From 5c5ce37a7098bad4ee59896bd203437bdcbbee9b Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 7 May 2025 22:38:10 +0200 Subject: [PATCH 061/106] Fix docs warning --- .../ImGuiNotification/Internal/ActiveNotification.ImGui.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index ce70ab180..d72a41781 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -16,6 +16,7 @@ internal sealed partial class ActiveNotification /// The maximum width of the notification window. /// The offset from the bottom. /// Where notifications are anchored to on the screen. + /// Direction of the screen which we are snapping to. /// The height of the notification. public float Draw(float width, float offsetY, Vector2 anchorPosition, NotificationSnapDirection snapDirection) { From ac14d61a86a80f91a47a32906b87789e36df70dd Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 7 May 2025 22:40:05 +0200 Subject: [PATCH 062/106] Add some more logging to boot plugin loads --- Dalamud/Plugin/Internal/PluginManager.cs | 37 +++++++++++++++++++----- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index fdcba0162..b60414b0d 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -663,6 +663,8 @@ internal class PluginManager : IInternalDisposableService _ = Task.Run( async () => { + Log.Verbose("Starting async boot load"); + // Load plugins that want to be loaded during Framework.Tick var framework = await Service.GetAsync().ConfigureAwait(false); await framework.RunOnTick( @@ -671,10 +673,13 @@ internal class PluginManager : IInternalDisposableService syncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1), tokenSource.Token), cancellationToken: tokenSource.Token).ConfigureAwait(false); + Log.Verbose("Loaded FrameworkTickSync plugins (LoadRequiredState == 1)"); + loadTasks.Add(LoadPluginsAsync( "FrameworkTickAsync", asyncPlugins.Where(def => def.Manifest?.LoadRequiredState == 1), tokenSource.Token)); + Log.Verbose("Kicked off FrameworkTickAsync plugins (LoadRequiredState == 1)"); // Load plugins that want to be loaded during Framework.Tick, when drawing facilities are available _ = await Service.GetAsync().ConfigureAwait(false); @@ -684,14 +689,18 @@ internal class PluginManager : IInternalDisposableService syncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null), tokenSource.Token), cancellationToken: tokenSource.Token); + Log.Verbose("Loaded DrawAvailableSync plugins (LoadRequiredState == 0 or null)"); + loadTasks.Add(LoadPluginsAsync( "DrawAvailableAsync", asyncPlugins.Where(def => def.Manifest?.LoadRequiredState is 0 or null), tokenSource.Token)); + Log.Verbose("Kicked off DrawAvailableAsync plugins (LoadRequiredState == 0 or null)"); // Save signatures when all plugins are done loading, successful or not. try { + Log.Verbose("Now waiting for {NumTasks} async load tasks", loadTasks.Count); await Task.WhenAll(loadTasks).ConfigureAwait(false); Log.Information("Loaded plugins on boot"); } @@ -715,8 +724,13 @@ internal class PluginManager : IInternalDisposableService } this.StartupLoadTracking = null; - }, - tokenSource.Token); + }, tokenSource.Token).ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error(t.Exception, "Failed to load FrameworkTickAsync/DrawAvailableAsync plugins"); + } + }, TaskContinuationOptions.OnlyOnFaulted); } /// @@ -1831,18 +1845,27 @@ internal class PluginManager : IInternalDisposableService _ = this.SetPluginReposFromConfigAsync(false); this.OnInstalledPluginsChanged += () => Task.Run(Troubleshooting.LogTroubleshooting); - Log.Information("[T3] PM repos OK!"); + Log.Information("Repos loaded!"); } using (Timings.Start("PM Cleanup Plugins")) { this.CleanupPlugins(); - Log.Information("[T3] PMC OK!"); + Log.Information("Plugin cleanup OK!"); } using (Timings.Start("PM Load Sync Plugins")) { - var loadAllPlugins = Task.Run(this.LoadAllPlugins); + var loadAllPlugins = Task.Run(this.LoadAllPlugins) + .ContinueWith(t => + { + if (t.IsFaulted) + { + Log.Error(t.Exception, "Error in LoadAllPlugins()"); + } + + _ = Task.Run(Troubleshooting.LogTroubleshooting); + }); // We wait for all blocking services and tasks to finish before kicking off the main thread in any mode. // This means that we don't want to block here if this stupid thing isn't enabled. @@ -1852,10 +1875,8 @@ internal class PluginManager : IInternalDisposableService loadAllPlugins.Wait(); } - Log.Information("[T3] PML OK!"); + Log.Information("Boot load started"); } - - _ = Task.Run(Troubleshooting.LogTroubleshooting); } catch (Exception ex) { From df8de39098d206d67f0d25bcf4eff378e9af9cb8 Mon Sep 17 00:00:00 2001 From: goaaats Date: Wed, 7 May 2025 23:49:32 +0200 Subject: [PATCH 063/106] Even more boot load logging --- Dalamud/Plugin/Internal/PluginManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index b60414b0d..bfb1b3430 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -683,6 +683,7 @@ internal class PluginManager : IInternalDisposableService // Load plugins that want to be loaded during Framework.Tick, when drawing facilities are available _ = await Service.GetAsync().ConfigureAwait(false); + Log.Verbose(" InterfaceManager is ready, starting to load DrawAvailableSync plugins"); await framework.RunOnTick( () => LoadPluginsSync( "DrawAvailableSync", From a12c63d6a265cc7f13f4ed42b5dc6aeeef4055ce Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 9 May 2025 22:08:09 +0200 Subject: [PATCH 064/106] Fix notification positioning when multi-monitor is enabled --- .../Internal/ActiveNotification.ImGui.cs | 3 ++- .../Internal/NotificationPositionChooser.cs | 21 +++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs index d72a41781..ed39332bd 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -38,6 +38,7 @@ internal sealed partial class ActiveNotification var viewport = ImGuiHelpers.MainViewport; var viewportSize = viewport.WorkSize; + var viewportPos = viewport.Pos; ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); @@ -122,7 +123,7 @@ internal sealed partial class ActiveNotification ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowPos( - topLeft, + topLeft + viewportPos, ImGuiCond.Always, pivot); ImGui.SetNextWindowSizeConstraints( diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs index 6ad42ad80..2a50c5ae8 100644 --- a/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationPositionChooser.cs @@ -42,9 +42,13 @@ internal class NotificationPositionChooser using var style2 = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 0f); using var color = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 0)); + var viewport = ImGuiHelpers.MainViewport; + var viewportSize = viewport.Size; + var viewportPos = viewport.Pos; + ImGui.SetNextWindowFocus(); - ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos); - ImGui.SetNextWindowSize(ImGuiHelpers.MainViewport.Size); + ImGui.SetNextWindowPos(viewportPos); + ImGui.SetNextWindowSize(viewportSize); ImGuiHelpers.ForceNextWindowMainViewport(); ImGui.SetNextWindowBgAlpha(0.6f); @@ -54,7 +58,8 @@ internal class NotificationPositionChooser ImGuiWindowFlags.NoDocking | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNav); - var mousePosUnit = ImGui.GetMousePos() / ImGuiHelpers.MainViewport.Size; + var adjustedMousePos = ImGui.GetMousePos() - viewportPos; + var mousePosUnit = adjustedMousePos / viewportSize; // Store the offset as a Vector2 this.currentAnchorPosition = mousePosUnit; @@ -87,6 +92,7 @@ internal class NotificationPositionChooser var instructionPos = new Vector2( ImGuiHelpers.MainViewport.Size.X / 2 - instructionSize.X / 2, ImGuiHelpers.MainViewport.Size.Y / 2 - instructionSize.Y / 2 + i * instructionSize.Y); + instructionPos += viewportPos; dl.AddText(instructionPos, 0xFFFFFFFF, instruction); } @@ -102,7 +108,9 @@ internal class NotificationPositionChooser var edgeMargin = NotificationConstants.ScaledViewportEdgeMargin; var spacing = 10f * ImGuiHelpers.GlobalScale; - var viewportSize = ImGuiHelpers.MainViewport.Size; + var viewport = ImGuiHelpers.MainViewport; + var viewportSize = viewport.Size; + var viewportPos = viewport.Pos; var borderColor = ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, borderAlpha)); var borderThickness = 4.0f * ImGuiHelpers.GlobalScale; var borderRounding = 4.0f * ImGuiHelpers.GlobalScale; @@ -202,6 +210,11 @@ internal class NotificationPositionChooser } } + topLeft += viewportPos; + bottomRight += viewportPos; + smallTopLeft += viewportPos; + smallBottomRight += viewportPos; + // Draw the big box dl.AddRectFilled(topLeft, bottomRight, ImGui.ColorConvertFloat4ToU32(backgroundColor), borderRounding, ImDrawFlags.RoundCornersAll); dl.AddRect(topLeft, bottomRight, borderColor, borderRounding, ImDrawFlags.RoundCornersAll, borderThickness); From 4dce0c00e8ce568fa88c60860310fdf6690c4167 Mon Sep 17 00:00:00 2001 From: srkizer Date: Sat, 10 May 2025 05:47:42 +0900 Subject: [PATCH 065/106] Implement DrawListTextureWrap (#2036) * Implement DrawListTextureWrap * Fix unloading * minor fixes * Add CreateFromClipboardAsync --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- Dalamud/Dalamud.csproj | 7 + .../Interface/Internal/StaThreadService.cs | 282 ++++++++ .../Windows/Data/Widgets/TexWidget.cs | 9 + .../Internal/TextureManager.Clipboard.cs | 498 +++++++++++++ .../Textures/Internal/TextureManager.Wic.cs | 16 +- .../Textures/Internal/TextureManager.cs | 28 +- .../Internal/TextureManagerPluginScoped.cs | 29 + .../TextureWraps/IDrawListTextureWrap.cs | 61 ++ .../Internal/DrawListTextureWrap.cs | 283 ++++++++ .../DeviceContextStateBackup.cs | 669 ++++++++++++++++++ .../DrawListTextureWrap/Renderer.Common.hlsl | 4 + .../Renderer.DrawToPremul.hlsl | 40 ++ .../Renderer.DrawToPremul.ps.bin | Bin 0 -> 16620 bytes .../Renderer.DrawToPremul.vs.bin | Bin 0 -> 16972 bytes .../Renderer.MakeStraight.hlsl | 22 + .../Renderer.MakeStraight.ps.bin | Bin 0 -> 14596 bytes .../Renderer.MakeStraight.vs.bin | Bin 0 -> 14360 bytes .../Internal/DrawListTextureWrap/Renderer.cs | 595 ++++++++++++++++ .../DrawListTextureWrap/WindowPrinter.cs | 136 ++++ .../Utility/Internal/DevTextureSaveMenu.cs | 75 +- Dalamud/Interface/Windowing/Window.cs | 19 + Dalamud/Plugin/Services/ITextureProvider.cs | 23 + .../Services/ITextureReadbackProvider.cs | 13 + Dalamud/Utility/ClipboardFormats.cs | 40 ++ Dalamud/Utility/ThreadBoundTaskScheduler.cs | 7 + 25 files changed, 2821 insertions(+), 35 deletions(-) create mode 100644 Dalamud/Interface/Internal/StaThreadService.cs create mode 100644 Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.vs.bin create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs create mode 100644 Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs create mode 100644 Dalamud/Utility/ClipboardFormats.cs diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 934d050a2..fd1c3ef64 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -114,6 +114,13 @@ + + + + + + + diff --git a/Dalamud/Interface/Internal/StaThreadService.cs b/Dalamud/Interface/Internal/StaThreadService.cs new file mode 100644 index 000000000..87e003288 --- /dev/null +++ b/Dalamud/Interface/Internal/StaThreadService.cs @@ -0,0 +1,282 @@ +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Utility; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Internal; + +/// Dedicated thread for OLE operations, and possibly more native thread-serialized operations. +[ServiceManager.EarlyLoadedService] +internal partial class StaThreadService : IInternalDisposableService +{ + private readonly CancellationTokenSource cancellationTokenSource = new(); + private readonly Thread thread; + private readonly ThreadBoundTaskScheduler taskScheduler; + private readonly TaskFactory taskFactory; + + private readonly TaskCompletionSource messageReceiverHwndTask = + new(TaskCreationOptions.RunContinuationsAsynchronously); + + [ServiceManager.ServiceConstructor] + private StaThreadService() + { + try + { + this.thread = new(this.OleThreadBody); + this.thread.SetApartmentState(ApartmentState.STA); + + this.taskScheduler = new(this.thread); + this.taskScheduler.TaskQueued += this.TaskSchedulerOnTaskQueued; + this.taskFactory = new( + this.cancellationTokenSource.Token, + TaskCreationOptions.None, + TaskContinuationOptions.None, + this.taskScheduler); + + this.thread.Start(); + this.messageReceiverHwndTask.Task.Wait(); + } + catch (Exception e) + { + this.cancellationTokenSource.Cancel(); + this.messageReceiverHwndTask.SetException(e); + throw; + } + } + + /// Gets all the available clipboard formats. + public IReadOnlySet AvailableClipboardFormats { get; private set; } = ImmutableSortedSet.Empty; + + /// Places a pointer to a specific data object onto the clipboard. This makes the data object accessible + /// to the function. + /// Pointer to the interface on the data object from which the data to + /// be placed on the clipboard can be obtained. This parameter can be NULL; in which case the clipboard is emptied. + /// + /// This function returns on success. + [LibraryImport("ole32.dll")] + public static unsafe partial int OleSetClipboard(IDataObject* pdo); + + /// + public static unsafe void OleSetClipboard(ComPtr pdo) => + Marshal.ThrowExceptionForHR(OleSetClipboard(pdo.Get())); + + /// Retrieves a data object that you can use to access the contents of the clipboard. + /// Address of pointer variable that receives the interface pointer to + /// the clipboard data object. + /// This function returns on success. + [LibraryImport("ole32.dll")] + public static unsafe partial int OleGetClipboard(IDataObject** pdo); + + /// + public static unsafe ComPtr OleGetClipboard() + { + var pdo = default(ComPtr); + Marshal.ThrowExceptionForHR(OleGetClipboard(pdo.GetAddressOf())); + return pdo; + } + + /// Calls the appropriate method or function to release the specified storage medium. + /// Address of to release. + [LibraryImport("ole32.dll")] + public static unsafe partial void ReleaseStgMedium(STGMEDIUM* stgm); + + /// + public static unsafe void ReleaseStgMedium(ref STGMEDIUM stgm) + { + fixed (STGMEDIUM* pstgm = &stgm) + ReleaseStgMedium(pstgm); + } + + /// + void IInternalDisposableService.DisposeService() + { + this.cancellationTokenSource.Cancel(); + if (this.messageReceiverHwndTask.Task.IsCompletedSuccessfully) + SendMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_CLOSE, 0, 0); + + this.thread.Join(); + } + + /// Runs a given delegate in the messaging thread. + /// Delegate to run. + /// Optional cancellation token. + /// A representating the state of the operation. + public async Task Run(Action action, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this.cancellationTokenSource.Token, + cancellationToken); + await this.taskFactory.StartNew(action, cancellationToken).ConfigureAwait(true); + } + + /// Runs a given delegate in the messaging thread. + /// Type of the return value. + /// Delegate to run. + /// Optional cancellation token. + /// A representating the state of the operation. + public async Task Run(Func func, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this.cancellationTokenSource.Token, + cancellationToken); + return await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + } + + /// Runs a given delegate in the messaging thread. + /// Delegate to run. + /// Optional cancellation token. + /// A representating the state of the operation. + public async Task Run(Func func, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this.cancellationTokenSource.Token, + cancellationToken); + await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + } + + /// Runs a given delegate in the messaging thread. + /// Type of the return value. + /// Delegate to run. + /// Optional cancellation token. + /// A representating the state of the operation. + public async Task Run(Func> func, CancellationToken cancellationToken = default) + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource( + this.cancellationTokenSource.Token, + cancellationToken); + return await await this.taskFactory.StartNew(func, cancellationToken).ConfigureAwait(true); + } + + [LibraryImport("ole32.dll")] + private static partial int OleInitialize(nint reserved); + + [LibraryImport("ole32.dll")] + private static partial void OleUninitialize(); + + [LibraryImport("ole32.dll")] + private static partial int OleFlushClipboard(); + + private void TaskSchedulerOnTaskQueued() => + PostMessageW(this.messageReceiverHwndTask.Task.Result, WM.WM_NULL, 0, 0); + + private void UpdateAvailableClipboardFormats(HWND hWnd) + { + if (!OpenClipboard(hWnd)) + { + this.AvailableClipboardFormats = ImmutableSortedSet.Empty; + return; + } + + var formats = new SortedSet(); + for (var cf = EnumClipboardFormats(0); cf != 0; cf = EnumClipboardFormats(cf)) + formats.Add(cf); + this.AvailableClipboardFormats = formats; + CloseClipboard(); + } + + private LRESULT MessageReceiverWndProc(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + this.taskScheduler.Run(); + + switch (uMsg) + { + case WM.WM_CLIPBOARDUPDATE: + this.UpdateAvailableClipboardFormats(hWnd); + break; + + case WM.WM_DESTROY: + PostQuitMessage(0); + return 0; + } + + return DefWindowProcW(hWnd, uMsg, wParam, lParam); + } + + private unsafe void OleThreadBody() + { + var hInstance = (HINSTANCE)Marshal.GetHINSTANCE(typeof(StaThreadService).Module); + ushort wndClassAtom = 0; + var gch = GCHandle.Alloc(this); + try + { + ((HRESULT)OleInitialize(0)).ThrowOnError(); + + fixed (char* name = typeof(StaThreadService).FullName!) + { + var wndClass = new WNDCLASSEXW + { + cbSize = (uint)sizeof(WNDCLASSEXW), + lpfnWndProc = &MessageReceiverWndProcStatic, + hInstance = hInstance, + hbrBackground = (HBRUSH)(COLOR.COLOR_BACKGROUND + 1), + lpszClassName = (ushort*)name, + }; + + wndClassAtom = RegisterClassExW(&wndClass); + if (wndClassAtom == 0) + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + + this.messageReceiverHwndTask.SetResult( + CreateWindowExW( + 0, + (ushort*)wndClassAtom, + (ushort*)name, + 0, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + CW_USEDEFAULT, + default, + default, + hInstance, + (void*)GCHandle.ToIntPtr(gch))); + + [UnmanagedCallersOnly] + static LRESULT MessageReceiverWndProcStatic(HWND hWnd, uint uMsg, WPARAM wParam, LPARAM lParam) + { + nint gchn; + if (uMsg == WM.WM_NCCREATE) + { + gchn = (nint)((CREATESTRUCTW*)lParam)->lpCreateParams; + SetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA, gchn); + } + else + { + gchn = GetWindowLongPtrW(hWnd, GWLP.GWLP_USERDATA); + } + + if (gchn == 0) + return DefWindowProcW(hWnd, uMsg, wParam, lParam); + + return ((StaThreadService)GCHandle.FromIntPtr(gchn).Target!) + .MessageReceiverWndProc(hWnd, uMsg, wParam, lParam); + } + } + + AddClipboardFormatListener(this.messageReceiverHwndTask.Task.Result); + this.UpdateAvailableClipboardFormats(this.messageReceiverHwndTask.Task.Result); + + for (MSG msg; GetMessageW(&msg, default, 0, 0);) + { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + catch (Exception e) + { + gch.Free(); + _ = OleFlushClipboard(); + OleUninitialize(); + if (wndClassAtom != 0) + UnregisterClassW((ushort*)wndClassAtom, hInstance); + this.messageReceiverHwndTask.TrySetException(e); + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 07b2d01ff..ac48668fb 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -182,6 +182,15 @@ internal class TexWidget : IDataWindowWidget ImGui.Dummy(new(ImGui.GetTextLineHeightWithSpacing())); + if (!this.textureManager.HasClipboardImage()) + { + ImGuiComponents.DisabledButton("Paste from Clipboard"); + } + else if (ImGui.Button("Paste from Clipboard")) + { + this.addedTextures.Add(new(Api10: this.textureManager.CreateFromClipboardAsync())); + } + if (ImGui.CollapsingHeader(nameof(ITextureProvider.GetFromGameIcon))) { ImGui.PushID(nameof(this.DrawGetFromGameIcon)); diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs new file mode 100644 index 000000000..8a510e967 --- /dev/null +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Clipboard.cs @@ -0,0 +1,498 @@ +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.TextureWraps; +using Dalamud.Memory; +using Dalamud.Utility; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Interface.Textures.Internal; + +/// Service responsible for loading and disposing ImGui texture wraps. +internal sealed partial class TextureManager +{ + /// + public async Task CopyToClipboardAsync( + IDalamudTextureWrap wrap, + string? preferredFileNameWithoutExtension = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); + + using var wrapAux = new WrapAux(wrap, leaveWrapOpen); + bool hasAlphaChannel; + switch (wrapAux.Desc.Format) + { + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8X8_UNORM_SRGB: + hasAlphaChannel = false; + break; + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM: + case DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: + hasAlphaChannel = true; + break; + default: + await this.CopyToClipboardAsync( + await this.CreateFromExistingTextureAsync( + wrap, + new() { Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM }, + cancellationToken: cancellationToken), + preferredFileNameWithoutExtension, + false, + cancellationToken); + return; + } + + // https://stackoverflow.com/questions/15689541/win32-clipboard-and-alpha-channel-images + // https://learn.microsoft.com/en-us/windows/win32/shell/clipboard + using var pdo = default(ComPtr); + unsafe + { + fixed (Guid* piid = &IID.IID_IDataObject) + SHCreateDataObject(null, 1, null, null, piid, (void**)pdo.GetAddressOf()).ThrowOnError(); + } + + var ms = new MemoryStream(); + { + ms.SetLength(ms.Position = 0); + await this.SaveToStreamAsync( + wrap, + GUID.GUID_ContainerFormatPng, + ms, + new Dictionary { ["InterlaceOption"] = true }, + true, + true, + cancellationToken); + + unsafe + { + using var ims = default(ComPtr); + fixed (byte* p = ms.GetBuffer()) + ims.Attach(SHCreateMemStream(p, (uint)ms.Length)); + if (ims.IsEmpty()) + throw new OutOfMemoryException(); + + AddToDataObject( + pdo, + ClipboardFormats.Png, + new() + { + tymed = (uint)TYMED.TYMED_ISTREAM, + pstm = ims.Get(), + }); + AddToDataObject( + pdo, + ClipboardFormats.FileContents, + new() + { + tymed = (uint)TYMED.TYMED_ISTREAM, + pstm = ims.Get(), + }); + ims.Get()->AddRef(); + ims.Detach(); + } + + if (preferredFileNameWithoutExtension is not null) + { + unsafe + { + preferredFileNameWithoutExtension += ".png"; + if (preferredFileNameWithoutExtension.Length >= 260) + preferredFileNameWithoutExtension = preferredFileNameWithoutExtension[..^4] + ".png"; + var namea = (CodePagesEncodingProvider.Instance.GetEncoding(0) ?? Encoding.UTF8) + .GetBytes(preferredFileNameWithoutExtension); + if (namea.Length > 260) + { + namea.AsSpan()[^4..].CopyTo(namea.AsSpan(256, 4)); + Array.Resize(ref namea, 260); + } + + var fgda = new FILEGROUPDESCRIPTORA + { + cItems = 1, + fgd = new() + { + e0 = new() + { + dwFlags = unchecked((uint)FD_FLAGS.FD_FILESIZE | (uint)FD_FLAGS.FD_UNICODE), + nFileSizeHigh = (uint)(ms.Length >> 32), + nFileSizeLow = (uint)ms.Length, + }, + }, + }; + namea.AsSpan().CopyTo(new(fgda.fgd.e0.cFileName, 260)); + + AddToDataObject( + pdo, + ClipboardFormats.FileDescriptorA, + new() + { + tymed = (uint)TYMED.TYMED_HGLOBAL, + hGlobal = CreateHGlobalFromMemory(new(ref fgda)), + }); + + var fgdw = new FILEGROUPDESCRIPTORW + { + cItems = 1, + fgd = new() + { + e0 = new() + { + dwFlags = unchecked((uint)FD_FLAGS.FD_FILESIZE | (uint)FD_FLAGS.FD_UNICODE), + nFileSizeHigh = (uint)(ms.Length >> 32), + nFileSizeLow = (uint)ms.Length, + }, + }, + }; + preferredFileNameWithoutExtension.AsSpan().CopyTo(new(fgdw.fgd.e0.cFileName, 260)); + + AddToDataObject( + pdo, + ClipboardFormats.FileDescriptorW, + new() + { + tymed = (uint)TYMED.TYMED_HGLOBAL, + hGlobal = CreateHGlobalFromMemory(new(ref fgdw)), + }); + } + } + } + + { + ms.SetLength(ms.Position = 0); + await this.SaveToStreamAsync( + wrap, + GUID.GUID_ContainerFormatBmp, + ms, + new Dictionary { ["EnableV5Header32bppBGRA"] = false }, + true, + true, + cancellationToken); + AddToDataObject( + pdo, + CF.CF_DIB, + new() + { + tymed = (uint)TYMED.TYMED_HGLOBAL, + hGlobal = CreateHGlobalFromMemory( + ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf()..]), + }); + } + + if (hasAlphaChannel) + { + ms.SetLength(ms.Position = 0); + await this.SaveToStreamAsync( + wrap, + GUID.GUID_ContainerFormatBmp, + ms, + new Dictionary { ["EnableV5Header32bppBGRA"] = true }, + true, + true, + cancellationToken); + AddToDataObject( + pdo, + CF.CF_DIBV5, + new() + { + tymed = (uint)TYMED.TYMED_HGLOBAL, + hGlobal = CreateHGlobalFromMemory( + ms.GetBuffer().AsSpan(0, (int)ms.Length)[Unsafe.SizeOf()..]), + }); + } + + var omts = await Service.GetAsync(); + await omts.Run(() => StaThreadService.OleSetClipboard(pdo), cancellationToken); + + return; + + static unsafe void AddToDataObject(ComPtr pdo, uint clipboardFormat, STGMEDIUM stg) + { + var fec = new FORMATETC + { + cfFormat = (ushort)clipboardFormat, + ptd = null, + dwAspect = (uint)DVASPECT.DVASPECT_CONTENT, + lindex = 0, + tymed = stg.tymed, + }; + pdo.Get()->SetData(&fec, &stg, true).ThrowOnError(); + } + + static unsafe HGLOBAL CreateHGlobalFromMemory(ReadOnlySpan data) where T : unmanaged + { + var h = GlobalAlloc(GMEM.GMEM_MOVEABLE, (nuint)(data.Length * sizeof(T))); + if (h == 0) + throw new OutOfMemoryException("Failed to allocate."); + + var p = GlobalLock(h); + data.CopyTo(new(p, data.Length)); + GlobalUnlock(h); + return h; + } + } + + /// + public bool HasClipboardImage() + { + var acf = Service.Get().AvailableClipboardFormats; + return acf.Contains(CF.CF_DIBV5) + || acf.Contains(CF.CF_DIB) + || acf.Contains(ClipboardFormats.Png) + || acf.Contains(ClipboardFormats.FileContents); + } + + /// + public async Task CreateFromClipboardAsync( + string? debugName = null, + CancellationToken cancellationToken = default) + { + var omts = await Service.GetAsync(); + var (stgm, clipboardFormat) = await omts.Run(GetSupportedClipboardData, cancellationToken); + + try + { + return this.BlameSetName( + await this.DynamicPriorityTextureLoader.LoadAsync( + null, + ct => + clipboardFormat is CF.CF_DIB or CF.CF_DIBV5 + ? CreateTextureFromStorageMediumDib(this, stgm, ct) + : CreateTextureFromStorageMedium(this, stgm, ct), + cancellationToken), + debugName ?? $"{nameof(this.CreateFromClipboardAsync)}({(TYMED)stgm.tymed})"); + } + finally + { + StaThreadService.ReleaseStgMedium(ref stgm); + } + + // Converts a CF_DIB/V5 format to a full BMP format, for WIC consumption. + static unsafe Task CreateTextureFromStorageMediumDib( + TextureManager textureManager, + scoped in STGMEDIUM stgm, + CancellationToken ct) + { + var ms = new MemoryStream(); + switch ((TYMED)stgm.tymed) + { + case TYMED.TYMED_HGLOBAL when stgm.hGlobal != default: + { + var pMem = GlobalLock(stgm.hGlobal); + if (pMem is null) + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + try + { + var size = (int)GlobalSize(stgm.hGlobal); + ms.SetLength(sizeof(BITMAPFILEHEADER) + size); + new ReadOnlySpan(pMem, size).CopyTo(ms.GetBuffer().AsSpan(sizeof(BITMAPFILEHEADER))); + } + finally + { + GlobalUnlock(stgm.hGlobal); + } + + break; + } + + case TYMED.TYMED_ISTREAM when stgm.pstm is not null: + { + STATSTG stat; + if (stgm.pstm->Stat(&stat, (uint)STATFLAG.STATFLAG_NONAME).SUCCEEDED && stat.cbSize.QuadPart > 0) + ms.SetLength(sizeof(BITMAPFILEHEADER) + (int)stat.cbSize.QuadPart); + else + ms.SetLength(8192); + + var offset = (uint)sizeof(BITMAPFILEHEADER); + for (var read = 1u; read != 0;) + { + if (offset == ms.Length) + ms.SetLength(ms.Length * 2); + fixed (byte* pMem = ms.GetBuffer().AsSpan((int)offset)) + { + stgm.pstm->Read(pMem, (uint)(ms.Length - offset), &read).ThrowOnError(); + offset += read; + } + } + + ms.SetLength(offset); + break; + } + + default: + return Task.FromException(new NotSupportedException()); + } + + ref var bfh = ref Unsafe.As(ref ms.GetBuffer()[0]); + bfh.bfType = 0x4D42; + bfh.bfSize = (uint)ms.Length; + + ref var bih = ref Unsafe.As(ref ms.GetBuffer()[sizeof(BITMAPFILEHEADER)]); + bfh.bfOffBits = (uint)(sizeof(BITMAPFILEHEADER) + bih.biSize); + + if (bih.biSize >= sizeof(BITMAPINFOHEADER)) + { + if (bih.biBitCount > 8) + { + if (bih.biCompression == BI.BI_BITFIELDS) + bfh.bfOffBits += (uint)(3 * sizeof(RGBQUAD)); + else if (bih.biCompression == 6 /* BI_ALPHABITFIELDS */) + bfh.bfOffBits += (uint)(4 * sizeof(RGBQUAD)); + } + } + + if (bih.biClrUsed > 0) + bfh.bfOffBits += (uint)(bih.biClrUsed * sizeof(RGBQUAD)); + else if (bih.biBitCount <= 8) + bfh.bfOffBits += (uint)(sizeof(RGBQUAD) << bih.biBitCount); + + using var pinned = ms.GetBuffer().AsMemory().Pin(); + using var strm = textureManager.Wic.CreateIStreamViewOfMemory(pinned, (int)ms.Length); + return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct)); + } + + // Interprets a data as an image file using WIC. + static unsafe Task CreateTextureFromStorageMedium( + TextureManager textureManager, + scoped in STGMEDIUM stgm, + CancellationToken ct) + { + switch ((TYMED)stgm.tymed) + { + case TYMED.TYMED_HGLOBAL when stgm.hGlobal != default: + { + var pMem = GlobalLock(stgm.hGlobal); + if (pMem is null) + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + try + { + var size = (int)GlobalSize(stgm.hGlobal); + using var strm = textureManager.Wic.CreateIStreamViewOfMemory(pMem, size); + return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct)); + } + finally + { + GlobalUnlock(stgm.hGlobal); + } + } + + case TYMED.TYMED_FILE when stgm.lpszFileName is not null: + { + var fileName = MemoryHelper.ReadString((nint)stgm.lpszFileName, Encoding.Unicode, short.MaxValue); + return textureManager.NoThrottleCreateFromFileAsync(fileName, ct); + } + + case TYMED.TYMED_ISTREAM when stgm.pstm is not null: + { + using var strm = new ComPtr(stgm.pstm); + return Task.FromResult(textureManager.Wic.NoThrottleCreateFromWicStream(strm, ct)); + } + + default: + return Task.FromException(new NotSupportedException()); + } + } + + static unsafe bool TryGetClipboardDataAs( + ComPtr pdo, + uint clipboardFormat, + uint tymed, + out STGMEDIUM stgm) + { + var fec = new FORMATETC + { + cfFormat = (ushort)clipboardFormat, + ptd = null, + dwAspect = (uint)DVASPECT.DVASPECT_CONTENT, + lindex = -1, + tymed = tymed, + }; + fixed (STGMEDIUM* pstgm = &stgm) + return pdo.Get()->GetData(&fec, pstgm).SUCCEEDED; + } + + // Takes a data from clipboard for use with WIC. + static unsafe (STGMEDIUM Stgm, uint ClipboardFormat) GetSupportedClipboardData() + { + using var pdo = StaThreadService.OleGetClipboard(); + const uint tymeds = (uint)TYMED.TYMED_HGLOBAL | + (uint)TYMED.TYMED_FILE | + (uint)TYMED.TYMED_ISTREAM; + const uint sharedRead = STGM.STGM_READ | STGM.STGM_SHARE_DENY_WRITE; + + // Try taking data from clipboard as-is. + if (TryGetClipboardDataAs(pdo, CF.CF_DIBV5, tymeds, out var stgm)) + return (stgm, CF.CF_DIBV5); + if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileContents, tymeds, out stgm)) + return (stgm, ClipboardFormats.FileContents); + if (TryGetClipboardDataAs(pdo, ClipboardFormats.Png, tymeds, out stgm)) + return (stgm, ClipboardFormats.Png); + if (TryGetClipboardDataAs(pdo, CF.CF_DIB, tymeds, out stgm)) + return (stgm, CF.CF_DIB); + + // Try reading file from the path stored in clipboard. + if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileNameW, (uint)TYMED.TYMED_HGLOBAL, out stgm)) + { + var pPath = GlobalLock(stgm.hGlobal); + try + { + IStream* pfs; + SHCreateStreamOnFileW((ushort*)pPath, sharedRead, &pfs).ThrowOnError(); + + var stgm2 = new STGMEDIUM + { + tymed = (uint)TYMED.TYMED_ISTREAM, + pstm = pfs, + pUnkForRelease = (IUnknown*)pfs, + }; + return (stgm2, ClipboardFormats.FileContents); + } + finally + { + if (pPath is not null) + GlobalUnlock(stgm.hGlobal); + StaThreadService.ReleaseStgMedium(ref stgm); + } + } + + if (TryGetClipboardDataAs(pdo, ClipboardFormats.FileNameA, (uint)TYMED.TYMED_HGLOBAL, out stgm)) + { + var pPath = GlobalLock(stgm.hGlobal); + try + { + IStream* pfs; + SHCreateStreamOnFileA((sbyte*)pPath, sharedRead, &pfs).ThrowOnError(); + + var stgm2 = new STGMEDIUM + { + tymed = (uint)TYMED.TYMED_ISTREAM, + pstm = pfs, + pUnkForRelease = (IUnknown*)pfs, + }; + return (stgm2, ClipboardFormats.FileContents); + } + finally + { + if (pPath is not null) + GlobalUnlock(stgm.hGlobal); + StaThreadService.ReleaseStgMedium(ref stgm); + } + } + + throw new InvalidOperationException("No compatible clipboard format found."); + } + } +} diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs index df84f9545..e7357a625 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.Wic.cs @@ -6,7 +6,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using Dalamud.Interface.Internal; using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Plugin.Services; @@ -173,7 +172,7 @@ internal sealed partial class TextureManager ReadOnlyMemory bytes, CancellationToken cancellationToken = default) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); cancellationToken.ThrowIfCancellationRequested(); try @@ -204,7 +203,7 @@ internal sealed partial class TextureManager string path, CancellationToken cancellationToken = default) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); cancellationToken.ThrowIfCancellationRequested(); try @@ -359,11 +358,18 @@ internal sealed partial class TextureManager /// An instance of . /// The number of bytes in the memory. /// The new instance of . - public unsafe ComPtr CreateIStreamViewOfMemory(MemoryHandle handle, int length) + public unsafe ComPtr CreateIStreamViewOfMemory(MemoryHandle handle, int length) => + this.CreateIStreamViewOfMemory((byte*)handle.Pointer, length); + + /// Creates a new instance of from a fixed memory allocation. + /// Address of the data. + /// The number of bytes in the memory. + /// The new instance of . + public unsafe ComPtr CreateIStreamViewOfMemory(void* address, int length) { using var wicStream = default(ComPtr); this.wicFactory.Get()->CreateStream(wicStream.GetAddressOf()).ThrowOnError(); - wicStream.Get()->InitializeFromMemory((byte*)handle.Pointer, checked((uint)length)).ThrowOnError(); + wicStream.Get()->InitializeFromMemory((byte*)address, checked((uint)length)).ThrowOnError(); var res = default(ComPtr); wicStream.As(ref res).ThrowOnError(); diff --git a/Dalamud/Interface/Textures/Internal/TextureManager.cs b/Dalamud/Interface/Textures/Internal/TextureManager.cs index c9ee5d20e..8ac8e60ec 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManager.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManager.cs @@ -11,7 +11,9 @@ using Dalamud.Interface.Textures.Internal.SharedImmediateTextures; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Textures.TextureWraps.Internal; using Dalamud.Logging.Internal; +using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Services; +using Dalamud.Storage.Assets; using Dalamud.Utility; using Dalamud.Utility.TerraFxCom; @@ -48,10 +50,11 @@ internal sealed partial class TextureManager [ServiceManager.ServiceDependency] private readonly InterfaceManager interfaceManager = Service.Get(); + private readonly CancellationTokenSource disposeCts = new(); + private DynamicPriorityQueueLoader? dynamicPriorityTextureLoader; private SharedTextureManager? sharedTextureManager; private WicManager? wicManager; - private bool disposing; private ComPtr device; [ServiceManager.ServiceConstructor] @@ -104,10 +107,10 @@ internal sealed partial class TextureManager /// void IInternalDisposableService.DisposeService() { - if (this.disposing) + if (this.disposeCts.IsCancellationRequested) return; - this.disposing = true; + this.disposeCts.Cancel(); Interlocked.Exchange(ref this.dynamicPriorityTextureLoader, null)?.Dispose(); Interlocked.Exchange(ref this.simpleDrawer, null)?.Dispose(); @@ -269,6 +272,21 @@ internal sealed partial class TextureManager return wrap; } + /// + public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) => this.CreateDrawListTexture(null, debugName); + + /// + /// Plugin that created the draw list. + /// + /// + public IDrawListTextureWrap CreateDrawListTexture(LocalPlugin? plugin, string? debugName = null) => + new DrawListTextureWrap( + new(this.device), + this, + Service.Get().Empty4X4, + plugin, + debugName ?? $"{nameof(this.CreateDrawListTexture)}"); + /// bool ITextureProvider.IsDxgiFormatSupported(int dxgiFormat) => this.IsDxgiFormatSupported((DXGI_FORMAT)dxgiFormat); @@ -330,7 +348,7 @@ internal sealed partial class TextureManager /// The loaded texture. internal IDalamudTextureWrap NoThrottleCreateFromTexFile(TexFile file) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); var buffer = file.TextureBuffer; var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(file.Header.Format, false); @@ -354,7 +372,7 @@ internal sealed partial class TextureManager /// The loaded texture. internal IDalamudTextureWrap NoThrottleCreateFromTexFile(ReadOnlySpan fileBytes) { - ObjectDisposedException.ThrowIf(this.disposing, this); + ObjectDisposedException.ThrowIf(this.disposeCts.IsCancellationRequested, this); if (!TexFileExtensions.IsPossiblyTexFile2D(fileBytes)) throw new InvalidDataException("The file is not a TexFile."); diff --git a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs index c62ad61b4..93600a263 100644 --- a/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs +++ b/Dalamud/Interface/Textures/Internal/TextureManagerPluginScoped.cs @@ -151,6 +151,10 @@ internal sealed class TextureManagerPluginScoped return textureWrap; } + /// + public IDrawListTextureWrap CreateDrawListTexture(string? debugName = null) => + this.ManagerOrThrow.CreateDrawListTexture(this.plugin, debugName); + /// public async Task CreateFromExistingTextureAsync( IDalamudTextureWrap wrap, @@ -267,6 +271,17 @@ internal sealed class TextureManagerPluginScoped return textureWrap; } + /// + public async Task CreateFromClipboardAsync( + string? debugName = null, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + var textureWrap = await manager.CreateFromClipboardAsync(debugName, cancellationToken); + manager.Blame(textureWrap, this.plugin); + return textureWrap; + } + /// public IEnumerable GetSupportedImageDecoderInfos() => this.ManagerOrThrow.Wic.GetSupportedDecoderInfos(); @@ -279,6 +294,9 @@ internal sealed class TextureManagerPluginScoped return shared; } + /// + public bool HasClipboardImage() => this.ManagerOrThrow.HasClipboardImage(); + /// public bool TryGetFromGameIcon(in GameIconLookup lookup, [NotNullWhen(true)] out ISharedImmediateTexture? texture) { @@ -411,6 +429,17 @@ internal sealed class TextureManagerPluginScoped cancellationToken); } + /// + public async Task CopyToClipboardAsync( + IDalamudTextureWrap wrap, + string? preferredFileNameWithoutExtension = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default) + { + var manager = await this.ManagerTask; + await manager.CopyToClipboardAsync(wrap, preferredFileNameWithoutExtension, leaveWrapOpen, cancellationToken); + } + private void ResultOnInterceptTexDataLoad(string path, ref string? replacementPath) => this.InterceptTexDataLoad?.Invoke(path, ref replacementPath); } diff --git a/Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs new file mode 100644 index 000000000..4fb5d1aca --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/IDrawListTextureWrap.cs @@ -0,0 +1,61 @@ +using System.Numerics; + +using Dalamud.Interface.Textures.TextureWraps.Internal; + +using ImGuiNET; + +namespace Dalamud.Interface.Textures.TextureWraps; + +/// A texture wrap that can be drawn using ImGui draw data. +public interface IDrawListTextureWrap : IDalamudTextureWrap +{ + /// Gets or sets the width of the texture. + /// If is to be set together, set use instead. + new int Width { get; set; } + + /// Gets or sets the width of the texture. + /// If is to be set together, set use instead. + new int Height { get; set; } + + /// Gets or sets the size of the texture. + /// Components will be rounded up. + new Vector2 Size { get; set; } + + /// + int IDalamudTextureWrap.Width => this.Width; + + /// + int IDalamudTextureWrap.Height => this.Height; + + /// + Vector2 IDalamudTextureWrap.Size => this.Size; + + /// Gets or sets the color to use when clearing this texture. + /// Color in RGBA. Defaults to , which is full transparency. + Vector4 ClearColor { get; set; } + + /// Draws a draw list to this texture. + /// Draw list to draw from. + /// Left-top coordinates of the draw commands in the draw list. + /// Scale to apply to all draw commands in the draw list. + /// This function can be called only from the main thread. + void Draw(ImDrawListPtr drawListPtr, Vector2 displayPos, Vector2 scale); + + /// + void Draw(scoped in ImDrawData drawData); + + /// Draws from a draw data to this texture. + /// Draw data to draw. + ///
    + ///
  • Texture size will be kept as specified in . will be + /// used only as shader parameters.
  • + ///
  • This function can be called only from the main thread.
  • + ///
+ void Draw(ImDrawDataPtr drawData); + + /// Resizes this texture and draws an ImGui window. + /// Name and ID of the window to draw. Use the value that goes into + /// . + /// Scale to apply to all draw commands in the draw list. + void ResizeAndDrawWindow(ReadOnlySpan windowName, Vector2 scale); +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs new file mode 100644 index 000000000..4e82479b0 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs @@ -0,0 +1,283 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.TextureWraps.Internal; + +/// +internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, IDeferredDisposable +{ + private readonly TextureManager textureManager; + private readonly IDalamudTextureWrap emptyTexture; + private readonly LocalPlugin? plugin; + private readonly string debugName; + + private ComPtr device; + private ComPtr deviceContext; + private ComPtr tex; + private ComPtr srv; + private ComPtr rtv; + private ComPtr uav; + + private int width; + private int height; + private DXGI_FORMAT format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM; + + /// Initializes a new instance of the class. + /// Pointer to a D3D11 device. Ownership is taken. + /// Instance of the class. + /// Texture to use, if or is 0. + /// Plugin that holds responsible for this texture. + /// Name for debug display purposes. + public DrawListTextureWrap( + ComPtr device, + TextureManager textureManager, + IDalamudTextureWrap emptyTexture, + LocalPlugin? plugin, + string debugName) + { + this.textureManager = textureManager; + this.emptyTexture = emptyTexture; + this.plugin = plugin; + this.debugName = debugName; + + if (device.IsEmpty()) + throw new ArgumentNullException(nameof(device)); + + this.device.Swap(ref device); + fixed (ID3D11DeviceContext** pdc = &this.deviceContext.GetPinnableReference()) + this.device.Get()->GetImmediateContext(pdc); + + this.emptyTexture = emptyTexture; + this.srv = new((ID3D11ShaderResourceView*)emptyTexture.ImGuiHandle); + } + + /// Finalizes an instance of the class. + ~DrawListTextureWrap() => this.RealDispose(); + + /// + public nint ImGuiHandle => (nint)this.srv.Get(); + + /// + public int Width + { + get => this.width; + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value, nameof(value)); + this.Resize(value, this.height, this.format); + } + } + + /// + public int Height + { + get => this.height; + set + { + ArgumentOutOfRangeException.ThrowIfNegative(value, nameof(value)); + this.Resize(this.width, value, this.format).ThrowOnError(); + } + } + + /// + public Vector2 Size + { + get => new(this.width, this.height); + set + { + if (value.X is <= 0 or float.NaN) + throw new ArgumentOutOfRangeException(nameof(value), value, "X component is invalid."); + if (value.Y is <= 0 or float.NaN) + throw new ArgumentOutOfRangeException(nameof(value), value, "Y component is invalid."); + this.Resize((int)MathF.Ceiling(value.X), (int)MathF.Ceiling(value.Y), this.format).ThrowOnError(); + } + } + + /// + public Vector4 ClearColor { get; set; } + + /// Gets or sets the . + public int DxgiFormat + { + get => (int)this.format; + set + { + if (!this.textureManager.IsDxgiFormatSupportedForCreateFromExistingTextureAsync((DXGI_FORMAT)value)) + { + throw new ArgumentException( + "Specified format is not a supported rendering target format.", + nameof(value)); + } + + this.Resize(this.width, this.Height, (DXGI_FORMAT)value).ThrowOnError(); + } + } + + /// + public void Dispose() + { + if (Service.GetNullable() is { } im) + im.EnqueueDeferredDispose(this); + else + this.RealDispose(); + } + + /// + public void RealDispose() + { + this.srv.Reset(); + this.tex.Reset(); + this.rtv.Reset(); + this.uav.Reset(); + this.device.Reset(); + this.deviceContext.Reset(); + +#pragma warning disable CA1816 + GC.SuppressFinalize(this); +#pragma warning restore CA1816 + } + + /// + public void Draw(ImDrawListPtr drawListPtr, Vector2 displayPos, Vector2 scale) => + this.Draw( + new ImDrawData + { + Valid = 1, + CmdListsCount = 1, + TotalIdxCount = drawListPtr.IdxBuffer.Size, + TotalVtxCount = drawListPtr.VtxBuffer.Size, + CmdLists = (ImDrawList**)(&drawListPtr), + DisplayPos = displayPos, + DisplaySize = this.Size, + FramebufferScale = scale, + }); + + /// + public void Draw(scoped in ImDrawData drawData) + { + fixed (ImDrawData* pDrawData = &drawData) + this.Draw(new(pDrawData)); + } + + /// + public void Draw(ImDrawDataPtr drawData) + { + ThreadSafety.AssertMainThread(); + + // Do nothing if the render target is empty. + if (this.rtv.IsEmpty()) + return; + + // Clear the texture first, as the texture exists. + var clearColor = this.ClearColor; + this.deviceContext.Get()->ClearRenderTargetView(this.rtv.Get(), (float*)&clearColor); + + // If there is nothing to draw, then stop. + if (!drawData.Valid + || drawData.CmdListsCount < 1 + || drawData.TotalIdxCount < 1 + || drawData.TotalVtxCount < 1 + || drawData.CmdLists == 0 + || drawData.DisplaySize.X <= 0 + || drawData.DisplaySize.Y <= 0 + || drawData.FramebufferScale.X == 0 + || drawData.FramebufferScale.Y == 0) + return; + + using (new DeviceContextStateBackup(this.device.Get()->GetFeatureLevel(), this.deviceContext)) + { + Service.Get().RenderDrawData(this.rtv.Get(), drawData); + Service.Get().MakeStraight(this.uav.Get()); + } + } + + /// Resizes the texture. + /// New texture width. + /// New texture height. + /// New format. + /// if the texture has been resized, if the texture has not + /// been resized, or a value with that evaluates to . + private HRESULT Resize(int newWidth, int newHeight, DXGI_FORMAT newFormat) + { + if (newWidth < 0 || newHeight < 0) + return E.E_INVALIDARG; + + if (newWidth == 0 || newHeight == 0) + { + this.tex.Reset(); + this.srv.Reset(); + this.rtv.Reset(); + this.uav.Reset(); + this.width = newWidth; + this.Height = newHeight; + this.srv = new((ID3D11ShaderResourceView*)this.emptyTexture.ImGuiHandle); + return S.S_FALSE; + } + + if (this.width == newWidth && this.height == newHeight) + return S.S_FALSE; + + // These new resources will take replace the existing resources, only once all allocations are completed. + using var tmptex = default(ComPtr); + using var tmpsrv = default(ComPtr); + using var tmprtv = default(ComPtr); + using var tmpuav = default(ComPtr); + + var tmpTexDesc = new D3D11_TEXTURE2D_DESC + { + Width = (uint)newWidth, + Height = (uint)newHeight, + MipLevels = 1, + ArraySize = 1, + Format = newFormat, + SampleDesc = new(1, 0), + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | + D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET | + D3D11_BIND_FLAG.D3D11_BIND_UNORDERED_ACCESS), + CPUAccessFlags = 0u, + MiscFlags = 0u, + }; + var hr = this.device.Get()->CreateTexture2D(&tmpTexDesc, null, tmptex.GetAddressOf()); + if (hr.FAILED) + return hr; + + var tmpres = (ID3D11Resource*)tmptex.Get(); + var srvDesc = new D3D11_SHADER_RESOURCE_VIEW_DESC(tmptex, D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + hr = this.device.Get()->CreateShaderResourceView(tmpres, &srvDesc, tmpsrv.GetAddressOf()); + if (hr.FAILED) + return hr; + + var rtvDesc = new D3D11_RENDER_TARGET_VIEW_DESC(tmptex, D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + hr = this.device.Get()->CreateRenderTargetView(tmpres, &rtvDesc, tmprtv.GetAddressOf()); + if (hr.FAILED) + return hr; + + var uavDesc = new D3D11_UNORDERED_ACCESS_VIEW_DESC(tmptex, D3D11_UAV_DIMENSION.D3D11_UAV_DIMENSION_TEXTURE2D); + hr = this.device.Get()->CreateUnorderedAccessView(tmpres, &uavDesc, tmpuav.GetAddressOf()); + if (hr.FAILED) + return hr; + + tmptex.Swap(ref this.tex); + tmpsrv.Swap(ref this.srv); + tmprtv.Swap(ref this.rtv); + tmpuav.Swap(ref this.uav); + this.width = newWidth; + this.height = newHeight; + this.format = newFormat; + + this.textureManager.BlameSetName(this, this.debugName); + this.textureManager.Blame(this, this.plugin); + return S.S_OK; + } +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs new file mode 100644 index 000000000..55cf13881 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/DeviceContextStateBackup.cs @@ -0,0 +1,669 @@ +using System.Runtime.InteropServices; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.TextureWraps.Internal; + +/// +internal sealed unsafe partial class DrawListTextureWrap +{ + /// Captures states of a . + // TODO: Use the one in https://github.com/goatcorp/Dalamud/pull/1923 once the PR goes in + internal struct DeviceContextStateBackup : IDisposable + { + private InputAssemblerState inputAssemblerState; + private RasterizerState rasterizerState; + private OutputMergerState outputMergerState; + private VertexShaderState vertexShaderState; + private HullShaderState hullShaderState; + private DomainShaderState domainShaderState; + private GeometryShaderState geometryShaderState; + private PixelShaderState pixelShaderState; + private ComputeShaderState computeShaderState; + + /// + /// Initializes a new instance of the struct, + /// by capturing all states of a . + /// + /// The feature level. + /// The device context. + public DeviceContextStateBackup(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx) + { + this.inputAssemblerState = InputAssemblerState.From(ctx); + this.rasterizerState = RasterizerState.From(ctx); + this.outputMergerState = OutputMergerState.From(featureLevel, ctx); + this.vertexShaderState = VertexShaderState.From(ctx); + this.hullShaderState = HullShaderState.From(ctx); + this.domainShaderState = DomainShaderState.From(ctx); + this.geometryShaderState = GeometryShaderState.From(ctx); + this.pixelShaderState = PixelShaderState.From(ctx); + this.computeShaderState = ComputeShaderState.From(featureLevel, ctx); + } + + /// + public void Dispose() + { + this.inputAssemblerState.Dispose(); + this.rasterizerState.Dispose(); + this.outputMergerState.Dispose(); + this.vertexShaderState.Dispose(); + this.hullShaderState.Dispose(); + this.domainShaderState.Dispose(); + this.geometryShaderState.Dispose(); + this.pixelShaderState.Dispose(); + this.computeShaderState.Dispose(); + } + + /// + /// Captures Input Assembler states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct InputAssemblerState : IDisposable + { + private const int BufferCount = D3D11.D3D11_IA_VERTEX_INPUT_RESOURCE_SLOT_COUNT; + + private ComPtr context; + private ComPtr layout; + private ComPtr indexBuffer; + private DXGI_FORMAT indexFormat; + private uint indexOffset; + private D3D_PRIMITIVE_TOPOLOGY topology; + private fixed ulong buffers[BufferCount]; + private fixed uint strides[BufferCount]; + private fixed uint offsets[BufferCount]; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static InputAssemblerState From(ID3D11DeviceContext* ctx) + { + var state = default(InputAssemblerState); + state.context.Attach(ctx); + ctx->AddRef(); + ctx->IAGetInputLayout(state.layout.GetAddressOf()); + ctx->IAGetPrimitiveTopology(&state.topology); + ctx->IAGetIndexBuffer(state.indexBuffer.GetAddressOf(), &state.indexFormat, &state.indexOffset); + ctx->IAGetVertexBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers, state.strides, state.offsets); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (InputAssemblerState* pThis = &this) + { + ctx->IASetInputLayout(pThis->layout); + ctx->IASetPrimitiveTopology(pThis->topology); + ctx->IASetIndexBuffer(pThis->indexBuffer, pThis->indexFormat, pThis->indexOffset); + ctx->IASetVertexBuffers( + 0, + BufferCount, + (ID3D11Buffer**)pThis->buffers, + pThis->strides, + pThis->offsets); + + pThis->context.Dispose(); + pThis->layout.Dispose(); + pThis->indexBuffer.Dispose(); + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + } + } + } + + /// + /// Captures Rasterizer states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct RasterizerState : IDisposable + { + private const int Count = D3D11.D3D11_VIEWPORT_AND_SCISSORRECT_MAX_INDEX; + + private ComPtr context; + private ComPtr state; + private fixed byte viewports[24 * Count]; + private fixed ulong scissorRects[16 * Count]; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static RasterizerState From(ID3D11DeviceContext* ctx) + { + var state = default(RasterizerState); + state.context.Attach(ctx); + ctx->AddRef(); + ctx->RSGetState(state.state.GetAddressOf()); + uint n = Count; + ctx->RSGetViewports(&n, (D3D11_VIEWPORT*)state.viewports); + n = Count; + ctx->RSGetScissorRects(&n, (RECT*)state.scissorRects); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (RasterizerState* pThis = &this) + { + ctx->RSSetState(pThis->state); + ctx->RSSetViewports(Count, (D3D11_VIEWPORT*)pThis->viewports); + ctx->RSSetScissorRects(Count, (RECT*)pThis->scissorRects); + + pThis->context.Dispose(); + pThis->state.Dispose(); + } + } + } + + /// + /// Captures Output Merger states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct OutputMergerState : IDisposable + { + private const int RtvCount = D3D11.D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT; + private const int UavCountMax = D3D11.D3D11_1_UAV_SLOT_COUNT; + + private ComPtr context; + private ComPtr blendState; + private fixed float blendFactor[4]; + private uint sampleMask; + private uint stencilRef; + private ComPtr depthStencilState; + private fixed ulong rtvs[RtvCount]; // ID3D11RenderTargetView*[RtvCount] + private ComPtr dsv; + private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCount] + private int uavCount; + + /// + /// Creates a new instance of from . + /// + /// The feature level. + /// The device context. + /// The captured state. + public static OutputMergerState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx) + { + var state = default(OutputMergerState); + state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1 + ? D3D11.D3D11_1_UAV_SLOT_COUNT + : D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT; + state.context.Attach(ctx); + ctx->AddRef(); + ctx->OMGetBlendState(state.blendState.GetAddressOf(), state.blendFactor, &state.sampleMask); + ctx->OMGetDepthStencilState(state.depthStencilState.GetAddressOf(), &state.stencilRef); + ctx->OMGetRenderTargetsAndUnorderedAccessViews( + RtvCount, + (ID3D11RenderTargetView**)state.rtvs, + state.dsv.GetAddressOf(), + 0, + (uint)state.uavCount, + (ID3D11UnorderedAccessView**)state.uavs); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (OutputMergerState* pThis = &this) + { + ctx->OMSetBlendState(pThis->blendState, pThis->blendFactor, pThis->sampleMask); + ctx->OMSetDepthStencilState(pThis->depthStencilState, pThis->stencilRef); + var rtvc = (uint)RtvCount; + while (rtvc > 0 && pThis->rtvs[rtvc - 1] == 0) + rtvc--; + + var uavlb = rtvc; + while (uavlb < this.uavCount && pThis->uavs[uavlb] == 0) + uavlb++; + + var uavc = (uint)this.uavCount; + while (uavc > uavlb && pThis->uavs[uavc - 1] == 0) + uavlb--; + uavc -= uavlb; + + ctx->OMSetRenderTargetsAndUnorderedAccessViews( + rtvc, + (ID3D11RenderTargetView**)pThis->rtvs, + pThis->dsv, + uavc == 0 ? 0 : uavlb, + uavc, + uavc == 0 ? null : (ID3D11UnorderedAccessView**)pThis->uavs, + null); + + this.context.Reset(); + this.blendState.Reset(); + this.depthStencilState.Reset(); + this.dsv.Reset(); + foreach (ref var b in new Span>(pThis->rtvs, RtvCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->uavs, this.uavCount)) + b.Dispose(); + } + } + } + + /// + /// Captures Vertex Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct VertexShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static VertexShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(VertexShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->VSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->VSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->VSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->VSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (VertexShaderState* pThis = &this) + { + ctx->VSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->VSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->VSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->VSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Hull Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct HullShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static HullShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(HullShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->HSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->HSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->HSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->HSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (HullShaderState* pThis = &this) + { + ctx->HSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->HSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->HSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->HSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Domain Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct DomainShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static DomainShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(DomainShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->DSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->DSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->DSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->DSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (DomainShaderState* pThis = &this) + { + ctx->DSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->DSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->DSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->DSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Geometry Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct GeometryShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static GeometryShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(GeometryShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->GSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->GSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->GSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->GSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (GeometryShaderState* pThis = &this) + { + ctx->GSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->GSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->GSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->GSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Pixel Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct PixelShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int ClassInstanceCount = 256; // According to msdn + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[ClassInstanceCount]; + private fixed ulong buffers[BufferCount]; + private fixed ulong samplers[SamplerCount]; + private fixed ulong resources[ResourceCount]; + private uint instCount; + + /// + /// Creates a new instance of from . + /// + /// The device context. + /// The captured state. + public static PixelShaderState From(ID3D11DeviceContext* ctx) + { + var state = default(PixelShaderState); + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = ClassInstanceCount; + ctx->PSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->PSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->PSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->PSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (PixelShaderState* pThis = &this) + { + ctx->PSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->PSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->PSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->PSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + + /// + /// Captures Compute Shader states of a . + /// + [StructLayout(LayoutKind.Sequential)] + public struct ComputeShaderState : IDisposable + { + private const int BufferCount = D3D11.D3D11_COMMONSHADER_CONSTANT_BUFFER_API_SLOT_COUNT; + private const int SamplerCount = D3D11.D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT; + private const int ResourceCount = D3D11.D3D11_COMMONSHADER_INPUT_RESOURCE_SLOT_COUNT; + private const int InstanceCount = 256; // According to msdn + private const int UavCountMax = D3D11.D3D11_1_UAV_SLOT_COUNT; + + private ComPtr context; + private ComPtr shader; + private fixed ulong insts[InstanceCount]; // ID3D11ClassInstance*[BufferCount] + private fixed ulong buffers[BufferCount]; // ID3D11Buffer*[BufferCount] + private fixed ulong samplers[SamplerCount]; // ID3D11SamplerState*[SamplerCount] + private fixed ulong resources[ResourceCount]; // ID3D11ShaderResourceView*[ResourceCount] + private fixed ulong uavs[UavCountMax]; // ID3D11UnorderedAccessView*[UavCountMax] + private uint instCount; + private int uavCount; + + /// + /// Creates a new instance of from . + /// + /// The feature level. + /// The device context. + /// The captured state. + public static ComputeShaderState From(D3D_FEATURE_LEVEL featureLevel, ID3D11DeviceContext* ctx) + { + var state = default(ComputeShaderState); + state.uavCount = featureLevel >= D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1 + ? D3D11.D3D11_1_UAV_SLOT_COUNT + : D3D11.D3D11_PS_CS_UAV_REGISTER_COUNT; + state.context.Attach(ctx); + ctx->AddRef(); + state.instCount = InstanceCount; + ctx->CSGetShader(state.shader.GetAddressOf(), (ID3D11ClassInstance**)state.insts, &state.instCount); + ctx->CSGetConstantBuffers(0, BufferCount, (ID3D11Buffer**)state.buffers); + ctx->CSGetSamplers(0, SamplerCount, (ID3D11SamplerState**)state.samplers); + ctx->CSGetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)state.resources); + ctx->CSGetUnorderedAccessViews(0, (uint)state.uavCount, (ID3D11UnorderedAccessView**)state.uavs); + return state; + } + + /// + public void Dispose() + { + var ctx = this.context.Get(); + if (ctx is null) + return; + + fixed (ComputeShaderState* pThis = &this) + { + ctx->CSSetShader(pThis->shader, (ID3D11ClassInstance**)pThis->insts, pThis->instCount); + ctx->CSSetConstantBuffers(0, BufferCount, (ID3D11Buffer**)pThis->buffers); + ctx->CSSetSamplers(0, SamplerCount, (ID3D11SamplerState**)pThis->samplers); + ctx->CSSetShaderResources(0, ResourceCount, (ID3D11ShaderResourceView**)pThis->resources); + ctx->CSSetUnorderedAccessViews( + 0, + (uint)this.uavCount, + (ID3D11UnorderedAccessView**)pThis->uavs, + null); + + foreach (ref var b in new Span>(pThis->buffers, BufferCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->samplers, SamplerCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->resources, ResourceCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->insts, (int)pThis->instCount)) + b.Dispose(); + foreach (ref var b in new Span>(pThis->uavs, this.uavCount)) + b.Dispose(); + pThis->context.Dispose(); + pThis->shader.Dispose(); + } + } + } + } +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl new file mode 100644 index 000000000..17b53ba6c --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.Common.hlsl @@ -0,0 +1,4 @@ +cbuffer TransformationBuffer : register(b0) { + float4x4 g_view; + float4 g_colorMultiplier; +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl new file mode 100644 index 000000000..171d3e73b --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl @@ -0,0 +1,40 @@ +#include "DrawListTexture.Renderer.Common.hlsl" + +struct ImDrawVert { + float2 position : POSITION; + float2 uv : TEXCOORD0; + float4 color : COLOR0; +}; + +struct VsData { + float4 position : SV_POSITION; + float2 uv : TEXCOORD0; + float4 color : COLOR0; +}; + +struct PsData { + float4 color : COLOR0; +}; + +Texture2D s_texture : register(t0); +SamplerState s_sampler : register(s0); +RWTexture2D s_output : register(u1); + +VsData vs_main(const ImDrawVert idv) { + VsData result; + result.position = mul(g_view, float4(idv.position, 0, 1)); + result.uv = idv.uv; + result.color = idv.color; + return result; +} + +float4 ps_main(const VsData vd) : SV_TARGET { + return s_texture.Sample(s_sampler, vd.uv) * vd.color; +} + +/* + +fxc /Zi /T vs_5_0 /E vs_main /Fo DrawListTexture.Renderer.DrawToPremul.vs.bin DrawListTexture.Renderer.DrawToPremul.hlsl +fxc /Zi /T ps_5_0 /E ps_main /Fo DrawListTexture.Renderer.DrawToPremul.ps.bin DrawListTexture.Renderer.DrawToPremul.hlsl + +*/ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin new file mode 100644 index 0000000000000000000000000000000000000000..e3c68edf33f9766c259aa99b4ca284228754bec2 GIT binary patch literal 16620 zcmeHOU2Igx6+U;3gIR1?6Z2C*$v|ji6K8krxC!A;V6P2U4L05dT%H51 z?-~fDL5&(kX;rmq9xA1(ckoO9>QoS8XuX69~YsIzPM+8_65Z@>4?kKQ}~=9iy)zg82G>jy+0 zLAne0Utkf$%fQ1ik-q{5Gu=Hu2R#6gWG^3Tr>8Z@eHYomOj{din}7<~4BP>%2NJ#p ztZ{R*fAG2xUj?$Pg z7B6H^j12T=dvm@0Mepy>qAZ>_DbdP|2CG(Ut0Y>M3<3+oPUKrqd1a&6z91R(EmZL7j6=1w*>Q1{Vn|8q|m?gDa1-rXF zVSZ%_oM5|`4zW5^-Yl9Hxb7Yhtc!c9+D_OR-OMSlTggy~?dr2B8<aw6dlhtUOrjvsC{a(Qf!_WnJO4( zTfJ`V_zgDBl~*+R12)o6{y^lL(>1d7(U=_Ep~>~1YSK~CA^M?T^pME6g~v`8`&3A}>F$&++{2P^~5GdX1^NAbd)33D9{D@I#=#eS>rLUH-76~}j`;`2=SIi&n(Wqce5 zz&T4V+2f`)sdG*5^oXG~ZZzEzzYv-c=Q@UoKs2=lG2Wg?I+MZEux$!GiyKJf+ z!=x=@F>}1gE$N9M#3jQq%ie6}1YNX7hl1-xYi)iepqKeq`pDDI1UekAOV-nWMn}Ho zFKO*9O#R$%r@#88mp{<%c2m#0A>Kpsz2Ll%cbL4FW7&V3KK1<%amYme1t580coB?1 zHgoXSn|Uz*4Wul25!_n~u~CBF z`YM?k^F^6Zal=m->d`T)aSKEnOyewwKt7Rk;Q)}ZLAIUTYo475xM0)J6IlsY`HPY(%~m=^EMS@@iso9wW;w zqt2J*d#l{M#9TR)hvToR-0F~Ay)VZ)Ema8jzt=Y$_p|$dskSYu_y4y*$K7n?SEIae zaCS9__(1=VGtUQR8R)GH2vPc=S|IaGCVMzDSY1!&tEZY-dgo(Dv?Xi-e($k)RqDnj zv>|~-`*TP+eFJ>n&m)N-umINmSr7z_vxtlFu=t7@6LYLYuskfVK|b&9zb~OV+RU~b zard#tIL7%(|BEpCiR#S@WXW#;{}GqZZ^pgXJFL8I5g1obc64{nI|WLhs*-E`~LGujG0tlp>(FVG;T_&q)!^Qq--mn zI&JXpW1~egf5sz><6mTU>i%8gy0u!|N`&uLc@Dzw95~4){1NnHq^p1ouY!XR%54Yo zB$e{KbSK=uuW4C31$-S<&j44$UGl7#prd;9*}T8YBW*=`04ag@5W67D^3o@QJGj_*xht%S)4uAK8zo1}ZBx~kP>sUE&=Zg6p40fy2 zZVmOdMe6T>9mjH6%Pg7WwjLd7(U110`t{Vg%$Q;6-R5L5mq(&45o?pa)RE)opbx_# zA)wz692L`(9S@rFWonAT+%(Ky9Wk_lF~iUKQJT%(3fXj zpkJTUp4S$t&l^+9I^`H}!;>K)-_Zw}IyrJLmDf^7drsG(fkCu#7_u*fL_>e-df(KM zLjpL0=Gr6fNUAh-GCc!6^-Mr?Mt5ac23u;|kymM!Q4apBArH@dXvTXwPHu4PDV;2D z7YVC!#RpqfdUo`#b?bl?QhF6n$72c!m*T>>NLp55+v_p{IH$5OreS7V99n(&rnii)6+I==|K3M4eU zB=}th9{7NPGk~Yo1Bjb{dTKi#bbNLNVweu>^kNxZ!X)2QEDK8PjdRNT@ zS7L42@z-m{xeh(`+os)#P`vBX>A$}ClUvu5N}gp4*QvKoJhzag!~^;Sd7x|=u_2>? z&8lI=uw9)r3$b(gQhrA`T(Yfl*4FzblUC-!NyD=Bi(wt$Y>wOYqFKsukFW33)oyUV zzq~w!615{u_=-AoXS`G=_4Kvau75sdgH@DOvn_2* z+i+BSQZJ3zZW`8G#zd}U8&=d#wCoS3GLuE@^iy`mHXv4VvwpD>#fDB*8tZgV84g0) zEZfDh?H4Y$K{)JrW2!VVnaLHRS+h`@Hz2vOsTR*yo~UJ%aE#&l#!bg619d>3Ea#){ z2}!Fvj2-1~Y4Yo{B4$+N=7%ERo~@PUXKLiFtR}m^C-TQVHM09W_TxX&q_0Vn@87^1 zqq0?gf*$n1)T$oXnJZ-T^!I`a0`38wQst?6d^dc$MNV3kB7hdW$_V)3up9jJ;=ItN;@wyaVj*IE`tZ<^ zp0pR?EW>G|5`trH$cZY@L{A^B-r*%ZruZ$%0cn?#I zt9rcG)bIKDvmk8_S_Xn=Y^oF+MTPql=6lqy7=85=d%b=D#l;6!9NrTTSDo_nnAuV1 zh+eODZts5|jV4;c7kx8gxdEm~ccRuApemgh(XZH;s z$=Rn&>wIZAWm<+&G|a-pa59t6OqRzgw6B0w&v+(l45x~jLLoDnH%i0qA~FYYr?`;G z58wYR5qWvv@I=nO;HWOmsUGuGE1IjQF3qX7ob@YJJH=EzhDn>p%gkG?#at0AaamxS zMRzuHf-YI7eT(ZQ>uq*!K`+(c^g*PbTj=n4ePSd1=M3aq|D4d?!_+^c{pZCquYIIF z=%$``L%h>tJ#}9A3ewn42;2VK?Ah;rj4cDpF9KQShZj(wWRr*Q{2+_Me*!7XvH%{g z2RL?J(%H!Sbep=DF+rhPd9y!=p2mWfo;*)}lz(yHt)SRcX z&TGeQUGe!{fZP5R=NVS~kdF^lqWBFyev^+M@$q#Zzsbkn<>PPm@%ikl_M3hDR`3s^ zuOS$4Svu89)msBkW%6YMx3`M}+pR(W{rYrNQGCBXWgq2rpB>6R@^|_8v`@9;@4xaF z<^BCv{vw|dL-`$Imw z>wnXx?PZOK$QmHO0jkGWHEeqpXF-Gx52>e<48TU&y5SHgr)|jnUZ!0>o4Kv{tRGR| z(SZNF)UIE%L7g3`BPqG9cp)iIu|D$}?1+f7f0G@ug0-XuJO#SUok_AzST=8{MLEX} zk!wANjxj>%WP2Dv@4RwOXJmoS&7RI}p3ap%ol$Z3rb^!`Pv2SzML0({LH7#KZPM)c zh5dJZfW#rs5%?a7wuZgBOxHVU$Bi^{$-zJV>Rhj3!T2w^QGvoG^8mh)yZ?=RJw)o_r7Rdyg%vo^o7fO~m>PFsFZwO3pR2eAA+uu;C`?7uIeYuenlT(-!G#s)kI?%sk? zVs_U1wto3J;VJG>)yeMpgJaHj!&B^@W#4s<@rBiG&bjYeX?k;=G@a*vH4LKXIFxd#hKtAo9QSul{5FaIKTd-3*a zylsVsU8?)7D)oju9g0V)IojNUm(lj4yWUzV&Hg(k5p}oc_)|HRPNs8QSmR5xB!H*P z1B-qCxf{<+ygyes-&YtnC0@u(8YPLBtZe+8!GCWX&70ZtE@K@3&N*#nyZU>O7D$4WHo$%6{0H zmw|cjf%5(?3wje&o&QFWZ-p%14?G`cK;Hu7dV=TBd`GZt9vQKh+&`=bt^u<5^b9?) r7RdiWq=%jXHUT#PHv##s3E<&+VC%zeK4?AgK;VJE1Azw~QxE(Xn~Wqp literal 0 HcmV?d00001 diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl new file mode 100644 index 000000000..b8423697a --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl @@ -0,0 +1,22 @@ +RWTexture2D s_output : register(u1); + +float4 vs_main(const float2 position : POSITION) : SV_POSITION { + return float4(position, 0, 1); +} + +float4 ps_main(const float4 position : SV_POSITION) : SV_TARGET { + const float4 src = s_output[position.xy]; + s_output[position.xy] = + src.a > 0 + ? float4(src.rgb / src.a, src.a) + : float4(0, 0, 0, 0); + + return float4(0, 0, 0, 0); // unused +} + +/* + +fxc /Zi /T vs_5_0 /E vs_main /Fo DrawListTexture.Renderer.MakeStraight.vs.bin DrawListTexture.Renderer.MakeStraight.hlsl +fxc /Zi /T ps_5_0 /E ps_main /Fo DrawListTexture.Renderer.MakeStraight.ps.bin DrawListTexture.Renderer.MakeStraight.hlsl + +*/ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin new file mode 100644 index 0000000000000000000000000000000000000000..0b979f6b61f10856c82f9938e9bcf7c579d55b11 GIT binary patch literal 14596 zcmeHOU2GKB6+W{z#$Jr^QX5=o$xTI7L!A8~q@|8=3|^`&YnHA*9)G=1qy)CZIY9$F$*X%$6nAENZ7Qlui2D0zu+zi(!) zJtnM08i^l!4xByro^$Ux=bn3K=FYj}?8$@u4e8fT6t*|dz4`nrv3LIc+!aGa>c1(n z9<&GecVHu!J;33Z$ajIC07tR|gD*g)f&JKC)-5a;s8}eDWHTA^I)Dn;3ET;61`!95+z50}m#dYTYDJC~$DFcTo~WqKkzMNWk=zlLn>5EQNA;JdXNn~hGU;SS zE?v4blsoi9SD>R2Q~?RpS?85RHldI^aeDYzZYV!=>c{>*DhU z+kN_5k!Rl!nd{Hi3uW~E%BG;re|`NKOwne@Fco6-;Rnziv~7XR3Ce>pzyC+*Pd&i( zTIXPk&ua+uyhK^xKV<~n=IdAn+V@x=CCi}~s&j)wX~tZ~&JY!e1u*EB4<-7@>;hO-9+gIZcz{i*(k9#ThhgX$Z}bb4caTQ*09aF+gV2mYLO z0M~N_t0MxJdDDqt%|>9K$c;5S_-Nn@>u?)WKyU8?%8-Ca1CX)^R(AySzGQWeG8$d2 z2wY5NjN?CxO>j^6-sKaGfBp4OkNv7)^U#+sy#5Q_hcaRq{&oD3n^|FTRK8ClkbNYZ zJDeR6zRT4Dh1cF%vWS0wN8rjSkpnvod7xM)*Ge(@%e!^*t(lm-`W=y`dA#}gufg~C zAS3Top}q@IZ@fMda`9a!ts-MUn|OVP*=7tPFk z+RWatnULzhlAojs?1a4At6XPH?Oz<(x3$&e?D?l3^Ssi7UM5oeJsG`*f~2W>ReF&_ z?&EqG6g!18D&;rq^2x4cjk|SYj5PSqpiRRKBOlmixGdreIr-~|- z=R`buI;~OzdP-Dku&lCMyS%ntNi~EREjY3gn^^`F{6k zp;$TRnRb^gOw;37{3R^@ihpxU`c1I-r?L25z4$%TYx@qk z{k^m?hpB)2t^Jj!e)JDxg>4{OeHz%F`C(Gs_Xj9<0;&7`j~^|(h6a>91Eeg1dnW=s zcRDCNiB@j{$8!QLr~z*%fb$e%7yEX3^G=ectw;K9WygG`|gU&8IJ#-x1=|7oVT! zIUnv54e}u<+iNgx6~ew3BWHA4jr~ zb$I9l_^}KzRPW ziyitl+%*56(?rz$9*zLdd$u*D=5a~kO7yNPUR&}-AZY|EM1VJ}C140wEpV?B!Rm{^ z7Wt~T|DMCpjHPWkb}Yve>#?GDcqhWVOBhSvqpiL$tl1H*bkow$hBX_UH860_@wujq zm-pEyZO^wz+teo935GFt1?&P|0BYER0`~@C(E9|wtErpL3S)dz4|eL5vu*m1U-Mrq zP}VQ@*z@D_P?3jUPtLOq;nwK3)=O5cC{4xLg*iuTly zJyDjFZBARRq>x8SOx?nnp zZ3z0)wl)753!ncASck~{3G_u!FCT>R+qg^5fii!80cD;0A?*T>&jz;T&798#=EvHA zn}7^BdmsY; E14oeP;{X5v literal 0 HcmV?d00001 diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin new file mode 100644 index 0000000000000000000000000000000000000000..1baeecdaef844bc9a92a43d68fae190205dc126d GIT binary patch literal 14360 zcmeHNPiz!b7=N=ZxZRer#kN#P@TH<@q3-OKBBYc;>9)|MK-t|0!jg8{ooH*V_q`WCb*S_EWh}Ho)umQLoSOFwBstuWjWenTAA<@W>^d@_G zazx3fChs)H#7X2~O6rv^~g2T7#!xgeAJ!QLDWBga^lvC=!-O;bNFVYJO&zT4wLS@4Ee z7B~o|c`$XL9m7bbI#QQWF7zN4m~hgT%Yk_En8qOW7BJS{A+#e>#&S*pz}=!C3}E4 zdjhKS;mNi@;ojuQ9;rXN`&r;}Bqp7BJ1|W9(EcwoXUhM*a`1!iuGQ;*y?0meQ{BRS z>Tf4@Ei4l3K3zo?i0_IgkHiyfpoq?#6|FAP(yvihaVqy-WuQ|hFhH`V5NTV6NJF+Q9#6uYIJTm44f9?#pnm78i-vuxFD z$j2J@g+eX|pRoGJ6isg!GIYy!n>F)s!_sWc(9uf7TN;rr2*2=)Ar3HANYy=Q4Wd~S zk2Y}<^7^cp@&AiyDHK!CwI?ankvQCy^5hg~*TOAxi0_{r*%=`p<vwEZiB~7<$N!u&70pyLe&v+Q_aXxWmHYtrlMs> z?8t-_8PxPeGLPh}T%ll0jVIWfq$LO*U$WrP4mtFY-tISFXo2Zl)3<0N_c0v=PPe8H zYxYUQ9JSI(!&KFA)zGu)xRO)G@|juMqhl=?R)*Aca$M1MWiY2&Y1h3pushYdl1nen z1Ieivd$l#4)$H?*XlqV%#uF{5HY?hi6Ky)4O`@G@5}kqb=ivOdySPPM7C8SHoZk}8 z?}%R8E8y1OqtX#zqUKa$u-1^n$*63WY%_;|bg5Bgin`l=*G zk*rd$FIR#nFD)o1TeP$Gs#5fSiM4q$#aDSk$>mjC>l6p-H}C*?RK8H2;13q)L3dxG zyrQ6-?$ZK@%-sX61Cq(Y(!HC%*8TYH^}W}AsIEB>`{o8?cThe6B#`iLz)86=y%f@9 zQ;c=WqfYFP@)QFk8>GE@k3#GE`{LgoKZw(gs1K{l0_9)FBZM+z~l!r zI13VxuMv`8av(u1+gfuFl-B+rb4~=!?Czc0Ay!vIXGiiotRkD7Wd(V`PyL2n{uaSs z;_+9qV3>Y;o6$A^>cgYAK;it0Gk^Jl`G53U_!5@O0(9TArA)p}zwMy6PxC&-%cauF zm+P-hD}RoB4HspBTK2rN|DFQ3G`B5x+KRPdQ62eAtI`6+mNI|-SLKv0Noktn^JOt7 z?z4eazgx+wM>k?6=tD0YNbfN(0R_||@r=lLRO}Z9kmqwpPp3A3llm7P1HFZGlAlhv z2MgrI!Cp6Xl^VSGRpfT@{sQbNVnk1*JEaDIw09v8Wkl%=;z?Z2&?XZxR%cvda6(y9 zbY`Z)QQS>a&0_=ay2if&S$wd1nQuT`?{}lA!c8a=Lykq!RXSlW9*VvH_ypa^y_!DS zqYoQQ)|D~UVlwWycoSP{t2t}6ez{-`+yv2f(9T*p1utf7ox?Sz6+JW zY9PJ0Cu7$D%YkJ4G9bk-Hvnl;Xd*~9P14iAO+X(WzXdiwev!UJ-vYh`d<*y%SOFIJ E4`unISO5S3 literal 0 HcmV?d00001 diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs new file mode 100644 index 000000000..cc6cfd000 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs @@ -0,0 +1,595 @@ +using System.Buffers; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Reflection; +using System.Runtime.InteropServices; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +using TerraFX.Interop.DirectX; +using TerraFX.Interop.Windows; + +namespace Dalamud.Interface.Textures.TextureWraps.Internal; + +/// +internal sealed unsafe partial class DrawListTextureWrap +{ + /// The renderer. + [ServiceManager.EarlyLoadedService] + internal sealed class Renderer : IInternalDisposableService + { + private ComPtr device; + private ComPtr deviceContext; + + private ComPtr drawToPremulVertexShader; + private ComPtr drawToPremulPixelShader; + private ComPtr drawToPremulInputLayout; + private ComPtr drawToPremulVertexBuffer; + private ComPtr drawToPremulVertexConstantBuffer; + private ComPtr drawToPremulIndexBuffer; + + private ComPtr makeStraightVertexShader; + private ComPtr makeStraightPixelShader; + private ComPtr makeStraightInputLayout; + private ComPtr makeStraightVertexBuffer; + private ComPtr makeStraightIndexBuffer; + + private ComPtr samplerState; + private ComPtr blendState; + private ComPtr rasterizerState; + private ComPtr depthStencilState; + private int vertexBufferSize; + private int indexBufferSize; + + [ServiceManager.ServiceConstructor] + private Renderer(InterfaceManager.InterfaceManagerWithScene iwms) + { + try + { + this.device = new((ID3D11Device*)iwms.Manager.Device!.NativePointer); + fixed (ID3D11DeviceContext** p = &this.deviceContext.GetPinnableReference()) + this.device.Get()->GetImmediateContext(p); + this.deviceContext.Get()->AddRef(); + + this.Setup(); + } + catch + { + this.ReleaseUnmanagedResources(); + throw; + } + } + + /// Finalizes an instance of the class. + ~Renderer() => this.ReleaseUnmanagedResources(); + + /// + public void DisposeService() => this.ReleaseUnmanagedResources(); + + /// Renders draw data. + /// The render target. + /// Pointer to the draw data. + public void RenderDrawData(ID3D11RenderTargetView* prtv, ImDrawDataPtr drawData) + { + ThreadSafety.AssertMainThread(); + + if (drawData.DisplaySize.X <= 0 || drawData.DisplaySize.Y <= 0 + || !drawData.Valid || drawData.CmdListsCount < 1) + return; + var cmdLists = new Span(drawData.NativePtr->CmdLists, drawData.NativePtr->CmdListsCount); + + // Create and grow vertex/index buffers if needed + if (this.vertexBufferSize < drawData.TotalVtxCount) + this.drawToPremulVertexBuffer.Dispose(); + if (this.drawToPremulVertexBuffer.Get() is null) + { + this.vertexBufferSize = drawData.TotalVtxCount + 5000; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ImDrawVert) * this.vertexBufferSize), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + this.device.Get()->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.drawToPremulVertexBuffer.Attach(buffer); + } + + if (this.indexBufferSize < drawData.TotalIdxCount) + this.drawToPremulIndexBuffer.Dispose(); + if (this.drawToPremulIndexBuffer.Get() is null) + { + this.indexBufferSize = drawData.TotalIdxCount + 5000; + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(ushort) * this.indexBufferSize), + (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + var buffer = default(ID3D11Buffer*); + this.device.Get()->CreateBuffer(&desc, null, &buffer).ThrowOnError(); + this.drawToPremulIndexBuffer.Attach(buffer); + } + + // Upload vertex/index data into a single contiguous GPU buffer + try + { + var vertexData = default(D3D11_MAPPED_SUBRESOURCE); + var indexData = default(D3D11_MAPPED_SUBRESOURCE); + this.deviceContext.Get()->Map( + (ID3D11Resource*)this.drawToPremulVertexBuffer.Get(), + 0, + D3D11_MAP.D3D11_MAP_WRITE_DISCARD, + 0, + &vertexData).ThrowOnError(); + this.deviceContext.Get()->Map( + (ID3D11Resource*)this.drawToPremulIndexBuffer.Get(), + 0, + D3D11_MAP.D3D11_MAP_WRITE_DISCARD, + 0, + &indexData).ThrowOnError(); + + var targetVertices = new Span(vertexData.pData, this.vertexBufferSize); + var targetIndices = new Span(indexData.pData, this.indexBufferSize); + foreach (ref var cmdList in cmdLists) + { + var vertices = new ImVectorWrapper(&cmdList.NativePtr->VtxBuffer); + var indices = new ImVectorWrapper(&cmdList.NativePtr->IdxBuffer); + + vertices.DataSpan.CopyTo(targetVertices); + indices.DataSpan.CopyTo(targetIndices); + + targetVertices = targetVertices[vertices.Length..]; + targetIndices = targetIndices[indices.Length..]; + } + } + finally + { + this.deviceContext.Get()->Unmap((ID3D11Resource*)this.drawToPremulVertexBuffer.Get(), 0); + this.deviceContext.Get()->Unmap((ID3D11Resource*)this.drawToPremulIndexBuffer.Get(), 0); + } + + // Setup orthographic projection matrix into our constant buffer. + // Our visible imgui space lies from DisplayPos (LT) to DisplayPos+DisplaySize (RB). + // DisplayPos is (0,0) for single viewport apps. + try + { + var data = default(D3D11_MAPPED_SUBRESOURCE); + this.deviceContext.Get()->Map( + (ID3D11Resource*)this.drawToPremulVertexConstantBuffer.Get(), + 0, + D3D11_MAP.D3D11_MAP_WRITE_DISCARD, + 0, + &data).ThrowOnError(); + ref var xform = ref *(TransformationBuffer*)data.pData; + xform.View = + Matrix4x4.CreateOrthographicOffCenter( + drawData.DisplayPos.X, + drawData.DisplayPos.X + drawData.DisplaySize.X, + drawData.DisplayPos.Y + drawData.DisplaySize.Y, + drawData.DisplayPos.Y, + 1f, + 0f); + } + finally + { + this.deviceContext.Get()->Unmap((ID3D11Resource*)this.drawToPremulVertexConstantBuffer.Get(), 0); + } + + // Set up render state + { + this.deviceContext.Get()->IASetInputLayout(this.drawToPremulInputLayout); + var buffer = this.drawToPremulVertexBuffer.Get(); + var stride = (uint)sizeof(ImDrawVert); + var offset = 0u; + this.deviceContext.Get()->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + this.deviceContext.Get()->IASetIndexBuffer( + this.drawToPremulIndexBuffer, + DXGI_FORMAT.DXGI_FORMAT_R16_UINT, + 0); + this.deviceContext.Get()->IASetPrimitiveTopology( + D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var viewport = new D3D11_VIEWPORT( + 0, + 0, + drawData.DisplaySize.X * drawData.FramebufferScale.X, + drawData.DisplaySize.Y * drawData.FramebufferScale.Y); + this.deviceContext.Get()->RSSetState(this.rasterizerState); + this.deviceContext.Get()->RSSetViewports(1, &viewport); + + var blendColor = default(Vector4); + this.deviceContext.Get()->OMSetBlendState(this.blendState, (float*)&blendColor, 0xffffffff); + this.deviceContext.Get()->OMSetDepthStencilState(this.depthStencilState, 0); + this.deviceContext.Get()->OMSetRenderTargets(1, &prtv, null); + + this.deviceContext.Get()->VSSetShader(this.drawToPremulVertexShader.Get(), null, 0); + buffer = this.drawToPremulVertexConstantBuffer.Get(); + this.deviceContext.Get()->VSSetConstantBuffers(0, 1, &buffer); + + // PS handled later + + this.deviceContext.Get()->GSSetShader(null, null, 0); + this.deviceContext.Get()->HSSetShader(null, null, 0); + this.deviceContext.Get()->DSSetShader(null, null, 0); + this.deviceContext.Get()->CSSetShader(null, null, 0); + } + + // Render command lists + // (Because we merged all buffers into a single one, we maintain our own offset into them) + var vertexOffset = 0; + var indexOffset = 0; + var clipOff = new Vector4(drawData.DisplayPos, drawData.DisplayPos.X, drawData.DisplayPos.Y); + var frameBufferScaleV4 = + new Vector4(drawData.FramebufferScale, drawData.FramebufferScale.X, drawData.FramebufferScale.Y); + foreach (ref var cmdList in cmdLists) + { + var cmds = new ImVectorWrapper(&cmdList.NativePtr->CmdBuffer); + foreach (ref var cmd in cmds.DataSpan) + { + var clipV4 = (cmd.ClipRect - clipOff) * frameBufferScaleV4; + var clipRect = new RECT((int)clipV4.X, (int)clipV4.Y, (int)clipV4.Z, (int)clipV4.W); + + // Skip the draw if nothing would be visible + if (clipRect.left >= clipRect.right || clipRect.top >= clipRect.bottom || cmd.ElemCount == 0) + continue; + + this.deviceContext.Get()->RSSetScissorRects(1, &clipRect); + + if (cmd.UserCallback == nint.Zero) + { + // Bind texture and draw + var samplerp = this.samplerState.Get(); + var srvp = (ID3D11ShaderResourceView*)cmd.TextureId; + this.deviceContext.Get()->PSSetShader(this.drawToPremulPixelShader, null, 0); + this.deviceContext.Get()->PSSetSamplers(0, 1, &samplerp); + this.deviceContext.Get()->PSSetShaderResources(0, 1, &srvp); + this.deviceContext.Get()->DrawIndexed( + cmd.ElemCount, + (uint)(cmd.IdxOffset + indexOffset), + (int)(cmd.VtxOffset + vertexOffset)); + } + } + + indexOffset += cmdList.IdxBuffer.Size; + vertexOffset += cmdList.VtxBuffer.Size; + } + } + + /// Renders draw data. + /// The pointer to a Texture2D UAV to make straight. + public void MakeStraight(ID3D11UnorderedAccessView* puav) + { + ThreadSafety.AssertMainThread(); + + D3D11_TEXTURE2D_DESC texDesc; + using (var texRes = default(ComPtr)) + { + puav->GetResource(texRes.GetAddressOf()); + + using var tex = default(ComPtr); + texRes.As(&tex).ThrowOnError(); + tex.Get()->GetDesc(&texDesc); + } + + this.deviceContext.Get()->IASetInputLayout(this.makeStraightInputLayout); + var buffer = this.makeStraightVertexBuffer.Get(); + var stride = (uint)sizeof(Vector2); + var offset = 0u; + this.deviceContext.Get()->IASetVertexBuffers(0, 1, &buffer, &stride, &offset); + this.deviceContext.Get()->IASetIndexBuffer( + this.makeStraightIndexBuffer, + DXGI_FORMAT.DXGI_FORMAT_R16_UINT, + 0); + this.deviceContext.Get()->IASetPrimitiveTopology( + D3D_PRIMITIVE_TOPOLOGY.D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); + + var scissorRect = new RECT(0, 0, (int)texDesc.Width, (int)texDesc.Height); + this.deviceContext.Get()->RSSetScissorRects(1, &scissorRect); + this.deviceContext.Get()->RSSetState(this.rasterizerState); + var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height); + this.deviceContext.Get()->RSSetViewports(1, &viewport); + + this.deviceContext.Get()->OMSetBlendState(null, null, 0xFFFFFFFF); + this.deviceContext.Get()->OMSetDepthStencilState(this.depthStencilState, 0); + var nullrtv = default(ID3D11RenderTargetView*); + this.deviceContext.Get()->OMSetRenderTargetsAndUnorderedAccessViews(1, &nullrtv, null, 1, 1, &puav, null); + + this.deviceContext.Get()->VSSetShader(this.makeStraightVertexShader.Get(), null, 0); + this.deviceContext.Get()->PSSetShader(this.makeStraightPixelShader.Get(), null, 0); + this.deviceContext.Get()->GSSetShader(null, null, 0); + this.deviceContext.Get()->HSSetShader(null, null, 0); + this.deviceContext.Get()->DSSetShader(null, null, 0); + this.deviceContext.Get()->CSSetShader(null, null, 0); + + this.deviceContext.Get()->DrawIndexed(6, 0, 0); + } + + [SuppressMessage( + "StyleCop.CSharp.LayoutRules", + "SA1519:Braces should not be omitted from multi-line child statement", + Justification = "Multiple fixed")] + private void Setup() + { + var assembly = Assembly.GetExecutingAssembly(); + var rendererName = typeof(Renderer).FullName!.Replace('+', '.'); + + if (this.drawToPremulVertexShader.IsEmpty() || this.drawToPremulInputLayout.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream($"{rendererName}.DrawToPremul.vs.bin")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + + using var tempShader = default(ComPtr); + using var tempInputLayout = default(ComPtr); + + fixed (byte* pArray = array) + fixed (void* pszPosition = "POSITION"u8) + fixed (void* pszTexCoord = "TEXCOORD"u8) + fixed (void* pszColor = "COLOR"u8) + { + this.device.Get()->CreateVertexShader( + pArray, + (nuint)stream.Length, + null, + tempShader.GetAddressOf()).ThrowOnError(); + + var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszTexCoord, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + new() + { + SemanticName = (sbyte*)pszColor, + Format = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM, + AlignedByteOffset = uint.MaxValue, + }, + }; + this.device.Get()->CreateInputLayout( + ied, + 3, + pArray, + (nuint)stream.Length, + tempInputLayout.GetAddressOf()) + .ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + + tempShader.Swap(ref this.drawToPremulVertexShader); + tempInputLayout.Swap(ref this.drawToPremulInputLayout); + } + + if (this.drawToPremulPixelShader.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream($"{rendererName}.DrawToPremul.ps.bin")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + + using var tmp = default(ComPtr); + fixed (byte* pArray = array) + { + this.device.Get()->CreatePixelShader(pArray, (nuint)stream.Length, null, tmp.GetAddressOf()) + .ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + + tmp.Swap(ref this.drawToPremulPixelShader); + } + + if (this.makeStraightVertexShader.IsEmpty() || this.makeStraightInputLayout.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream($"{rendererName}.MakeStraight.vs.bin")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + + using var tempShader = default(ComPtr); + using var tempInputLayout = default(ComPtr); + + fixed (byte* pArray = array) + fixed (void* pszPosition = "POSITION"u8) + { + this.device.Get()->CreateVertexShader( + pArray, + (nuint)stream.Length, + null, + tempShader.GetAddressOf()).ThrowOnError(); + + var ied = stackalloc D3D11_INPUT_ELEMENT_DESC[] + { + new() + { + SemanticName = (sbyte*)pszPosition, + Format = DXGI_FORMAT.DXGI_FORMAT_R32G32_FLOAT, + AlignedByteOffset = uint.MaxValue, + }, + }; + this.device.Get()->CreateInputLayout( + ied, + 1, + pArray, + (nuint)stream.Length, + tempInputLayout.GetAddressOf()) + .ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + + tempShader.Swap(ref this.makeStraightVertexShader); + tempInputLayout.Swap(ref this.makeStraightInputLayout); + } + + if (this.makeStraightPixelShader.IsEmpty()) + { + using var stream = assembly.GetManifestResourceStream($"{rendererName}.MakeStraight.ps.bin")!; + var array = ArrayPool.Shared.Rent((int)stream.Length); + stream.ReadExactly(array, 0, (int)stream.Length); + + using var tmp = default(ComPtr); + fixed (byte* pArray = array) + { + this.device.Get()->CreatePixelShader(pArray, (nuint)stream.Length, null, tmp.GetAddressOf()) + .ThrowOnError(); + } + + ArrayPool.Shared.Return(array); + + tmp.Swap(ref this.makeStraightPixelShader); + } + + if (this.makeStraightVertexBuffer.IsEmpty()) + { + using var tmp = default(ComPtr); + var desc = new D3D11_BUFFER_DESC( + (uint)(sizeof(Vector2) * 4), + (uint)D3D11_BIND_FLAG.D3D11_BIND_VERTEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var data = stackalloc Vector2[] { new(-1, 1), new(-1, -1), new(1, 1), new(1, -1) }; + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + this.device.Get()->CreateBuffer(&desc, &subr, tmp.GetAddressOf()).ThrowOnError(); + tmp.Swap(ref this.makeStraightVertexBuffer); + } + + if (this.makeStraightIndexBuffer.IsEmpty()) + { + using var tmp = default(ComPtr); + var desc = new D3D11_BUFFER_DESC( + sizeof(ushort) * 6, + (uint)D3D11_BIND_FLAG.D3D11_BIND_INDEX_BUFFER, + D3D11_USAGE.D3D11_USAGE_IMMUTABLE); + var data = stackalloc ushort[] { 0, 1, 2, 1, 2, 3 }; + var subr = new D3D11_SUBRESOURCE_DATA { pSysMem = data }; + this.device.Get()->CreateBuffer(&desc, &subr, tmp.GetAddressOf()).ThrowOnError(); + tmp.Swap(ref this.makeStraightIndexBuffer); + } + + if (this.drawToPremulVertexConstantBuffer.IsEmpty()) + { + using var tmp = default(ComPtr); + var bufferDesc = new D3D11_BUFFER_DESC( + (uint)sizeof(TransformationBuffer), + (uint)D3D11_BIND_FLAG.D3D11_BIND_CONSTANT_BUFFER, + D3D11_USAGE.D3D11_USAGE_DYNAMIC, + (uint)D3D11_CPU_ACCESS_FLAG.D3D11_CPU_ACCESS_WRITE); + this.device.Get()->CreateBuffer(&bufferDesc, null, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.drawToPremulVertexConstantBuffer); + } + + if (this.samplerState.IsEmpty()) + { + using var tmp = default(ComPtr); + var samplerDesc = new D3D11_SAMPLER_DESC + { + Filter = D3D11_FILTER.D3D11_FILTER_MIN_MAG_MIP_LINEAR, + AddressU = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + AddressV = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + AddressW = D3D11_TEXTURE_ADDRESS_MODE.D3D11_TEXTURE_ADDRESS_WRAP, + MipLODBias = 0, + MaxAnisotropy = 0, + ComparisonFunc = D3D11_COMPARISON_FUNC.D3D11_COMPARISON_ALWAYS, + MinLOD = 0, + MaxLOD = 0, + }; + this.device.Get()->CreateSamplerState(&samplerDesc, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.samplerState); + } + + // Create the blending setup + if (this.blendState.IsEmpty()) + { + using var tmp = default(ComPtr); + var blendStateDesc = new D3D11_BLEND_DESC + { + RenderTarget = + { + e0 = + { + BlendEnable = true, + SrcBlend = D3D11_BLEND.D3D11_BLEND_SRC_ALPHA, + DestBlend = D3D11_BLEND.D3D11_BLEND_INV_SRC_ALPHA, + BlendOp = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + SrcBlendAlpha = D3D11_BLEND.D3D11_BLEND_INV_DEST_ALPHA, + DestBlendAlpha = D3D11_BLEND.D3D11_BLEND_ONE, + BlendOpAlpha = D3D11_BLEND_OP.D3D11_BLEND_OP_ADD, + RenderTargetWriteMask = (byte)D3D11_COLOR_WRITE_ENABLE.D3D11_COLOR_WRITE_ENABLE_ALL, + }, + }, + }; + this.device.Get()->CreateBlendState(&blendStateDesc, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.blendState); + } + + // Create the rasterizer state + if (this.rasterizerState.IsEmpty()) + { + using var tmp = default(ComPtr); + var rasterizerDesc = new D3D11_RASTERIZER_DESC + { + FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID, + CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE, + ScissorEnable = true, + DepthClipEnable = true, + }; + this.device.Get()->CreateRasterizerState(&rasterizerDesc, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.rasterizerState); + } + + // Create the depth-stencil State + if (this.depthStencilState.IsEmpty()) + { + using var tmp = default(ComPtr); + var dsDesc = new D3D11_DEPTH_STENCIL_DESC + { + DepthEnable = false, + StencilEnable = false, + }; + this.device.Get()->CreateDepthStencilState(&dsDesc, tmp.GetAddressOf()).ThrowOnError(); + + tmp.Swap(ref this.depthStencilState); + } + } + + private void ReleaseUnmanagedResources() + { + this.device.Reset(); + this.deviceContext.Reset(); + this.drawToPremulVertexShader.Reset(); + this.drawToPremulPixelShader.Reset(); + this.drawToPremulInputLayout.Reset(); + this.makeStraightVertexShader.Reset(); + this.makeStraightPixelShader.Reset(); + this.makeStraightInputLayout.Reset(); + this.samplerState.Reset(); + this.drawToPremulVertexConstantBuffer.Reset(); + this.blendState.Reset(); + this.rasterizerState.Reset(); + this.depthStencilState.Reset(); + this.drawToPremulVertexBuffer.Reset(); + this.drawToPremulIndexBuffer.Reset(); + } + + [StructLayout(LayoutKind.Sequential)] + private struct TransformationBuffer + { + public Matrix4x4 View; + public Vector4 ColorMultiplier; + } + } +} diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs new file mode 100644 index 000000000..342bfaa93 --- /dev/null +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs @@ -0,0 +1,136 @@ +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +using ImGuiNET; + +namespace Dalamud.Interface.Textures.TextureWraps.Internal; + +/// +internal sealed unsafe partial class DrawListTextureWrap +{ + /// + public void ResizeAndDrawWindow(ReadOnlySpan windowName, Vector2 scale) + { + ref var window = ref ImGuiWindow.FindWindowByName(windowName); + if (Unsafe.IsNullRef(ref window)) + throw new ArgumentException("Window not found", nameof(windowName)); + + this.Size = window.Size; + + var numDrawList = CountDrawList(ref window); + var drawLists = stackalloc ImDrawList*[numDrawList]; + var drawData = new ImDrawData + { + Valid = 1, + CmdListsCount = numDrawList, + TotalIdxCount = 0, + TotalVtxCount = 0, + CmdLists = drawLists, + DisplayPos = window.Pos, + DisplaySize = window.Size, + FramebufferScale = scale, + }; + AddWindowToDrawData(ref window, ref drawLists); + for (var i = 0; i < numDrawList; i++) + { + drawData.TotalVtxCount += drawData.CmdLists[i]->VtxBuffer.Size; + drawData.TotalIdxCount += drawData.CmdLists[i]->IdxBuffer.Size; + } + + this.Draw(drawData); + + return; + + static bool IsWindowActiveAndVisible(scoped in ImGuiWindow window) => + window.Active != 0 && window.Hidden == 0; + + static void AddWindowToDrawData(scoped ref ImGuiWindow window, ref ImDrawList** wptr) + { + switch (window.DrawList.CmdBuffer.Size) + { + case 0: + case 1 when window.DrawList.CmdBuffer[0].ElemCount == 0 && + window.DrawList.CmdBuffer[0].UserCallback == 0: + break; + default: + *wptr++ = window.DrawList; + break; + } + + for (var i = 0; i < window.DC.ChildWindows.Size; i++) + { + ref var child = ref *(ImGuiWindow*)window.DC.ChildWindows[i]; + if (IsWindowActiveAndVisible(in child)) // Clipped children may have been marked not active + AddWindowToDrawData(ref child, ref wptr); + } + } + + static int CountDrawList(scoped ref ImGuiWindow window) + { + var res = window.DrawList.CmdBuffer.Size switch + { + 0 => 0, + 1 when window.DrawList.CmdBuffer[0].ElemCount == 0 && + window.DrawList.CmdBuffer[0].UserCallback == 0 => 0, + _ => 1, + }; + for (var i = 0; i < window.DC.ChildWindows.Size; i++) + res += CountDrawList(ref *(ImGuiWindow*)window.DC.ChildWindows[i]); + return res; + } + } + + [StructLayout(LayoutKind.Explicit, Size = 0x448)] + private struct ImGuiWindow + { + [FieldOffset(0x048)] + public Vector2 Pos; + + [FieldOffset(0x050)] + public Vector2 Size; + + [FieldOffset(0x0CB)] + public byte Active; + + [FieldOffset(0x0D2)] + public byte Hidden; + + [FieldOffset(0x118)] + public ImGuiWindowTempData DC; + + [FieldOffset(0x2C0)] + public ImDrawListPtr DrawList; + + private static nint pfnImGuiFindWindowByName; + + public static ref ImGuiWindow FindWindowByName(ReadOnlySpan name) + { + var nb = Encoding.UTF8.GetByteCount(name); + var buf = stackalloc byte[nb + 1]; + buf[Encoding.UTF8.GetBytes(name, new(buf, nb))] = 0; + if (pfnImGuiFindWindowByName == 0) + { + pfnImGuiFindWindowByName = + Process + .GetCurrentProcess() + .Modules + .Cast() + .First(x => x.ModuleName == "cimgui.dll") + .BaseAddress + 0x357F0; + } + + return ref *((delegate* unmanaged)pfnImGuiFindWindowByName)(buf); + } + + [StructLayout(LayoutKind.Explicit, Size = 0xF0)] + public struct ImGuiWindowTempData + { + [FieldOffset(0x98)] + public ImVector ChildWindows; + } + } +} diff --git a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs index a6584f9aa..5fa242abe 100644 --- a/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs +++ b/Dalamud/Interface/Utility/Internal/DevTextureSaveMenu.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text; using System.Threading.Tasks; using Dalamud.Interface.ImGuiFileDialog; @@ -48,6 +49,19 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService string name, Task texture) { + name = new StringBuilder(name) + .Replace('<', '_') + .Replace('>', '_') + .Replace(':', '_') + .Replace('"', '_') + .Replace('/', '_') + .Replace('\\', '_') + .Replace('|', '_') + .Replace('?', '_') + .Replace('*', '_') + .ToString(); + + var isCopy = false; try { var initiatorScreenOffset = ImGui.GetMousePos(); @@ -55,11 +69,12 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService var textureManager = await Service.GetAsync(); var popupName = $"{nameof(this.ShowTextureSaveMenuAsync)}_{textureWrap.ImGuiHandle:X}"; - BitmapCodecInfo encoder; + BitmapCodecInfo? encoder; { var first = true; var encoders = textureManager.Wic.GetSupportedEncoderInfos().ToList(); - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var tcs = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); Service.Get().Draw += DrawChoices; encoder = await tcs.Task; @@ -85,6 +100,8 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService return; } + if (ImGui.Selectable("Copy")) + tcs.TrySetResult(null); foreach (var encoder2 in encoders) { if (ImGui.Selectable(encoder2.Name)) @@ -106,8 +123,21 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService } } - string path; + if (encoder is null) { + isCopy = true; + await textureManager.CopyToClipboardAsync(textureWrap, name, true); + } + else + { + var props = new Dictionary(); + if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) + props["CompressionQuality"] = 1.0f; + else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || + encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || + encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) + props["ImageQuality"] = 1.0f; + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); this.fileDialogManager.SaveFileDialog( "Save texture...", @@ -121,30 +151,23 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService else tcs.SetResult(path2); }); - path = await tcs.Task.ConfigureAwait(false); - } + var path = await tcs.Task.ConfigureAwait(false); - var props = new Dictionary(); - if (encoder.ContainerGuid == GUID.GUID_ContainerFormatTiff) - props["CompressionQuality"] = 1.0f; - else if (encoder.ContainerGuid == GUID.GUID_ContainerFormatJpeg || - encoder.ContainerGuid == GUID.GUID_ContainerFormatHeif || - encoder.ContainerGuid == GUID.GUID_ContainerFormatWmp) - props["ImageQuality"] = 1.0f; - await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); + await textureManager.SaveToFileAsync(textureWrap, encoder.ContainerGuid, path, props: props); - var notif = Service.Get().AddNotification( - new() + var notif = Service.Get().AddNotification( + new() + { + Content = $"File saved to: {path}", + Title = initiatorName, + Type = NotificationType.Success, + }); + notif.Click += n => { - Content = $"File saved to: {path}", - Title = initiatorName, - Type = NotificationType.Success, - }); - notif.Click += n => - { - Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); - n.Notification.DismissNow(); - }; + Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); + n.Notification.DismissNow(); + }; + } } catch (Exception e) { @@ -155,7 +178,9 @@ internal sealed class DevTextureSaveMenu : IInternalDisposableService e, $"{nameof(DalamudInterface)}.{nameof(this.ShowTextureSaveMenuAsync)}({initiatorName}, {name})"); Service.Get().AddNotification( - $"Failed to save file: {e}", + isCopy + ? $"Failed to copy file: {e}" + : $"Failed to save file: {e}", initiatorName, NotificationType.Error); } diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 7779100b0..8dc517cb2 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -4,6 +4,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; +using System.Threading.Tasks; using CheapLoc; @@ -11,7 +12,10 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; using Dalamud.Interface.Internal; +using Dalamud.Interface.Textures.Internal; +using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; +using Dalamud.Interface.Utility.Internal; using Dalamud.Interface.Windowing.Persistence; using Dalamud.Logging.Internal; @@ -408,6 +412,7 @@ public abstract class Window var showAdditions = (this.AllowPinning || this.AllowClickthrough) && internalDrawFlags.HasFlag(WindowDrawFlags.UseAdditionalOptions) && flagsApplicableForTitleBarIcons; + var printWindow = false; if (showAdditions) { ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 1f); @@ -482,6 +487,9 @@ public abstract class Window if (!isAvailable) ImGui.EndDisabled(); + if (ImGui.Button(Loc.Localize("WindowSystemContextActionPrintWindow", "Print window"))) + printWindow = true; + ImGui.EndPopup(); } @@ -540,6 +548,17 @@ public abstract class Window ImGui.End(); + if (printWindow) + { + var tex = Service.Get().CreateDrawListTexture( + Loc.Localize("WindowSystemContextActionPrintWindow", "Print window")); + tex.ResizeAndDrawWindow(this.WindowName, Vector2.One); + _ = Service.Get().ShowTextureSaveMenuAsync( + this.WindowName, + this.WindowName, + Task.FromResult(tex)); + } + this.PostDraw(); this.PostHandlePreset(persistence); diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 201e2b803..3f9ae99df 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -9,6 +9,8 @@ using Dalamud.Interface.Internal.Windows.Data.Widgets; using Dalamud.Interface.Textures; using Dalamud.Interface.Textures.TextureWraps; +using ImGuiNET; + using Lumina.Data.Files; namespace Dalamud.Plugin.Services; @@ -45,6 +47,14 @@ public interface ITextureProvider bool cpuWrite, string? debugName = null); + /// Creates a texture that can be drawn from an or an . + /// + /// Name for debug display purposes. + /// A new draw list texture. + /// No new resource is allocated upfront; it will be done when is + /// set with positive values for both components. + IDrawListTextureWrap CreateDrawListTexture(string? debugName = null); + /// Creates a texture from the given existing texture, cropping and converting pixel format as needed. /// /// The source texture wrap. The passed value may be disposed once this function returns, @@ -169,6 +179,14 @@ public interface ITextureProvider string? debugName = null, CancellationToken cancellationToken = default); + /// Creates a texture from clipboard. + /// Name for debug display purposes. + /// The cancellation token. + /// A representing the status of the operation. + Task CreateFromClipboardAsync( + string? debugName = null, + CancellationToken cancellationToken = default); + /// Gets the supported bitmap decoders. /// The supported bitmap decoders. /// @@ -192,6 +210,11 @@ public interface ITextureProvider /// ISharedImmediateTexture GetFromGameIcon(in GameIconLookup lookup); + /// Gets a value indicating whether the current desktop clipboard contains an image that can be attempted + /// to read using . + /// true if it is the case. + bool HasClipboardImage(); + /// Gets a shared texture corresponding to the given game resource icon specifier. /// /// This function does not throw exceptions. diff --git a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs index b41ded41f..3d2894355 100644 --- a/Dalamud/Plugin/Services/ITextureReadbackProvider.cs +++ b/Dalamud/Plugin/Services/ITextureReadbackProvider.cs @@ -106,4 +106,17 @@ public interface ITextureReadbackProvider IReadOnlyDictionary? props = null, bool leaveWrapOpen = false, CancellationToken cancellationToken = default); + + /// Copies the texture to clipboard. + /// Texture wrap to copy. + /// Preferred file name. + /// Whether to leave non-disposed when the returned + /// completes. + /// The cancellation token. + /// A representing the status of the operation. + Task CopyToClipboardAsync( + IDalamudTextureWrap wrap, + string? preferredFileNameWithoutExtension = null, + bool leaveWrapOpen = false, + CancellationToken cancellationToken = default); } diff --git a/Dalamud/Utility/ClipboardFormats.cs b/Dalamud/Utility/ClipboardFormats.cs new file mode 100644 index 000000000..07b6c00d6 --- /dev/null +++ b/Dalamud/Utility/ClipboardFormats.cs @@ -0,0 +1,40 @@ +using System.Runtime.InteropServices; + +using TerraFX.Interop.Windows; + +using static TerraFX.Interop.Windows.Windows; + +namespace Dalamud.Utility; + +/// Clipboard formats, looked up by their names. +internal static class ClipboardFormats +{ + /// + public static uint FileContents { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILECONTENTS); + + /// Gets the clipboard format corresponding to the PNG file format. + public static uint Png { get; } = ClipboardFormatFromName("PNG"); + + /// + public static uint FileDescriptorW { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILEDESCRIPTORW); + + /// + public static uint FileDescriptorA { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILEDESCRIPTORA); + + /// + public static uint FileNameW { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILENAMEW); + + /// + public static uint FileNameA { get; } = ClipboardFormatFromName(CFSTR.CFSTR_FILENAMEA); + + private static unsafe uint ClipboardFormatFromName(ReadOnlySpan name) + { + uint cf; + fixed (void* p = name) + cf = RegisterClipboardFormatW((ushort*)p); + if (cf != 0) + return cf; + throw Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error()) ?? + new InvalidOperationException($"RegisterClipboardFormatW({name}) failed."); + } +} diff --git a/Dalamud/Utility/ThreadBoundTaskScheduler.cs b/Dalamud/Utility/ThreadBoundTaskScheduler.cs index 4b6de29ff..2930bd27f 100644 --- a/Dalamud/Utility/ThreadBoundTaskScheduler.cs +++ b/Dalamud/Utility/ThreadBoundTaskScheduler.cs @@ -22,8 +22,14 @@ internal class ThreadBoundTaskScheduler : TaskScheduler public ThreadBoundTaskScheduler(Thread? boundThread = null) { this.BoundThread = boundThread; + this.TaskQueued += static () => { }; } + /// + /// Event fired when a task has been posted. + /// + public event Action TaskQueued; + /// /// Gets or sets the thread this task scheduler is bound to. /// @@ -57,6 +63,7 @@ internal class ThreadBoundTaskScheduler : TaskScheduler /// protected override void QueueTask(Task task) { + this.TaskQueued.Invoke(); this.scheduledTasks[task] = Scheduled; } From 33605e3ace52b839685ebcd99c15dc4ac05b7284 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 9 May 2025 22:53:44 +0200 Subject: [PATCH 066/106] Move AntiDebug to xivfixes (#2264) * Move AntiDebug to xivfixes * Update BootEnabledGameFixes * Check BootEnabledGameFixes * Apply suggestions from code review Co-authored-by: KazWolfe --------- Co-authored-by: KazWolfe --- Dalamud.Boot/xivfixes.cpp | 43 ++++++++ Dalamud.Boot/xivfixes.h | 1 + Dalamud.Injector/EntryPoint.cs | 1 + .../Internal/DalamudConfiguration.cs | 5 - Dalamud/Game/Internal/AntiDebug.cs | 102 ------------------ .../Interface/Internal/DalamudInterface.cs | 14 --- 6 files changed, 45 insertions(+), 121 deletions(-) delete mode 100644 Dalamud/Game/Internal/AntiDebug.cs diff --git a/Dalamud.Boot/xivfixes.cpp b/Dalamud.Boot/xivfixes.cpp index f3b6aaa2c..eb0f7df56 100644 --- a/Dalamud.Boot/xivfixes.cpp +++ b/Dalamud.Boot/xivfixes.cpp @@ -648,6 +648,48 @@ void xivfixes::symbol_load_patches(bool bApply) { } } +void xivfixes::disable_game_debugging_protection(bool bApply) { + static const char* LogTag = "[xivfixes:disable_game_debugging_protection]"; + static const std::vector patchBytes = { + 0x31, 0xC0, // XOR EAX, EAX + 0x90, // NOP + 0x90, // NOP + 0x90, // NOP + 0x90 // NOP + }; + + if (!bApply) + return; + + if (!g_startInfo.BootEnabledGameFixes.contains("disable_game_debugging_protection")) { + logging::I("{} Turned off via environment variable.", LogTag); + return; + } + + // Find IsDebuggerPresent in Framework.Tick() + const char* matchPtr = utils::signature_finder() + .look_in(utils::loaded_module(g_hGameInstance), ".text") + .look_for_hex("FF 15 ?? ?? ?? ?? 85 C0 74 13 41") + .find_one() + .Match.data(); + + if (!matchPtr) { + logging::E("{} Failed to find signature.", LogTag); + return; + } + + void* address = const_cast(static_cast(matchPtr)); + + DWORD oldProtect; + if (VirtualProtect(address, patchBytes.size(), PAGE_EXECUTE_READWRITE, &oldProtect)) { + memcpy(address, patchBytes.data(), patchBytes.size()); + VirtualProtect(address, patchBytes.size(), oldProtect, &oldProtect); + logging::I("{} Patch applied at address 0x{:X}.", LogTag, reinterpret_cast(address)); + } else { + logging::E("{} Failed to change memory protection.", LogTag); + } +} + void xivfixes::apply_all(bool bApply) { for (const auto& [taskName, taskFunction] : std::initializer_list> { @@ -658,6 +700,7 @@ void xivfixes::apply_all(bool bApply) { { "backup_userdata_save", &backup_userdata_save }, { "prevent_icmphandle_crashes", &prevent_icmphandle_crashes }, { "symbol_load_patches", &symbol_load_patches }, + { "disable_game_debugging_protection", &disable_game_debugging_protection }, } ) { try { diff --git a/Dalamud.Boot/xivfixes.h b/Dalamud.Boot/xivfixes.h index afe2edb45..1cab3afae 100644 --- a/Dalamud.Boot/xivfixes.h +++ b/Dalamud.Boot/xivfixes.h @@ -8,6 +8,7 @@ namespace xivfixes { void backup_userdata_save(bool bApply); void prevent_icmphandle_crashes(bool bApply); void symbol_load_patches(bool bApply); + void disable_game_debugging_protection(bool bApply); void apply_all(bool bApply); } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index b526beb1c..cd2127355 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -476,6 +476,7 @@ namespace Dalamud.Injector "backup_userdata_save", "prevent_icmphandle_crashes", "symbol_load_patches", + "disable_game_debugging_protection", }; startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 6816b166f..3bc30fad0 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -284,11 +284,6 @@ internal sealed class DalamudConfiguration : IInternalDisposableService ///
public bool IsFocusManagementEnabled { get; set; } = true; - /// - /// Gets or sets a value indicating whether the anti-anti-debug check is enabled on startup. - /// - public bool IsAntiAntiDebugEnabled { get; set; } = false; - /// /// Gets or sets a value indicating whether to resume game main thread after plugins load. /// diff --git a/Dalamud/Game/Internal/AntiDebug.cs b/Dalamud/Game/Internal/AntiDebug.cs deleted file mode 100644 index 48b8688a1..000000000 --- a/Dalamud/Game/Internal/AntiDebug.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Collections.Generic; - -using Dalamud.Utility; - -#if !DEBUG -using Dalamud.Configuration.Internal; -#endif -using Serilog; - -namespace Dalamud.Game.Internal; - -/// -/// This class disables anti-debug functionality in the game client. -/// -[ServiceManager.EarlyLoadedService] -internal sealed class AntiDebug : IInternalDisposableService -{ - private readonly byte[] nop = [0x31, 0xC0, 0x90, 0x90, 0x90, 0x90]; - private byte[]? original; - private IntPtr debugCheckAddress; - - [ServiceManager.ServiceConstructor] - private AntiDebug(TargetSigScanner sigScanner) - { - try - { - // This sig has to be the call site in Framework_Tick - this.debugCheckAddress = sigScanner.ScanText("FF 15 ?? ?? ?? ?? 85 C0 74 13 41"); - } - catch (KeyNotFoundException) - { - this.debugCheckAddress = IntPtr.Zero; - } - - Log.Verbose($"Debug check address {Util.DescribeAddress(this.debugCheckAddress)}"); - - if (!this.IsEnabled) - { -#if DEBUG - this.Enable(); -#else - if (Service.Get().IsAntiAntiDebugEnabled) - this.Enable(); -#endif - } - } - - /// Finalizes an instance of the class. - ~AntiDebug() => ((IInternalDisposableService)this).DisposeService(); - - /// - /// 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) - { - Log.Information($"Overwriting debug check at {Util.DescribeAddress(this.debugCheckAddress)}"); - SafeMemory.ReadBytes(this.debugCheckAddress, this.nop.Length, out this.original); - SafeMemory.WriteBytes(this.debugCheckAddress, this.nop); - } - else - { - Log.Information("Debug check already overwritten?"); - } - - this.IsEnabled = true; - } - - /// - /// Disable the anti-debugging by reverting the overwritten code in memory. - /// - public void Disable() - { - if (!this.IsEnabled) - return; - - if (this.debugCheckAddress != IntPtr.Zero && this.original != null) - { - Log.Information($"Reverting debug check at {Util.DescribeAddress(this.debugCheckAddress)}"); - SafeMemory.WriteBytes(this.debugCheckAddress, this.original); - } - else - { - Log.Information("Debug check was not overwritten?"); - } - - this.IsEnabled = false; - } -} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 9760b601d..97edb8716 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -14,7 +14,6 @@ using Dalamud.Game.ClientState; 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; @@ -719,19 +718,6 @@ internal class DalamudInterface : IInternalDisposableService this.dalamud.StartInfo.LogName); } - var antiDebug = Service.Get(); - if (ImGui.MenuItem("Disable Debugging Protections", null, antiDebug.IsEnabled)) - { - var newEnabled = !antiDebug.IsEnabled; - if (newEnabled) - antiDebug.Enable(); - else - antiDebug.Disable(); - - this.configuration.IsAntiAntiDebugEnabled = newEnabled; - this.configuration.QueueSave(); - } - ImGui.Separator(); if (ImGui.MenuItem("Open Data window")) From bf491525f605b92b79922b5137fc7ad4e7fae897 Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 9 May 2025 23:05:38 +0200 Subject: [PATCH 067/106] Upgrade cimgui, use custom FindWindowByName instead of hardcoded offset --- .../DrawListTextureWrap/WindowPrinter.cs | 17 +++++------------ lib/cimgui | 2 +- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs index 342bfaa93..532da3e03 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/WindowPrinter.cs @@ -105,25 +105,18 @@ internal sealed unsafe partial class DrawListTextureWrap [FieldOffset(0x2C0)] public ImDrawListPtr DrawList; - private static nint pfnImGuiFindWindowByName; + [DllImport("cimgui", CallingConvention = CallingConvention.Cdecl)] +#pragma warning disable SA1300 + public static extern ImGuiWindow* igCustom_FindWindowByName(byte* inherit); +#pragma warning restore SA1300 public static ref ImGuiWindow FindWindowByName(ReadOnlySpan name) { var nb = Encoding.UTF8.GetByteCount(name); var buf = stackalloc byte[nb + 1]; buf[Encoding.UTF8.GetBytes(name, new(buf, nb))] = 0; - if (pfnImGuiFindWindowByName == 0) - { - pfnImGuiFindWindowByName = - Process - .GetCurrentProcess() - .Modules - .Cast() - .First(x => x.ModuleName == "cimgui.dll") - .BaseAddress + 0x357F0; - } - return ref *((delegate* unmanaged)pfnImGuiFindWindowByName)(buf); + return ref *igCustom_FindWindowByName(buf); } [StructLayout(LayoutKind.Explicit, Size = 0xF0)] diff --git a/lib/cimgui b/lib/cimgui index 122ee1681..27c8565f6 160000 --- a/lib/cimgui +++ b/lib/cimgui @@ -1 +1 @@ -Subproject commit 122ee16819437eea7eefe0c04398b44174106d86 +Subproject commit 27c8565f631b004c3266373890e41ecc627f775b From 2c735e9ec3de0c8019a60cb593631d5142ff8db4 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Fri, 9 May 2025 23:10:35 +0200 Subject: [PATCH 068/106] Do not throw ObjectDisposedException in IsEnabled and Disable (#2266) --- Dalamud/Hooking/Hook.cs | 1 + .../Internal/FunctionPointerVariableHook.cs | 36 ++++++------------ Dalamud/Hooking/Internal/MinHookHook.cs | 38 ++++++++----------- Dalamud/Hooking/Internal/ReloadedHook.cs | 18 +++------ 4 files changed, 35 insertions(+), 58 deletions(-) diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 1a492146f..972a2730d 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -90,6 +90,7 @@ public abstract class Hook : IDalamudHook where T : Delegate /// /// Starts intercepting a call to the function. /// + /// Hook is already disposed. public virtual void Enable() => throw new NotImplementedException(); /// diff --git a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs index 40a33fc1b..5f27d9a37 100644 --- a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs +++ b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs @@ -115,14 +115,7 @@ internal class FunctionPointerVariableHook : Hook } /// - public override bool IsEnabled - { - get - { - this.CheckDisposed(); - return this.enabled; - } - } + public override bool IsEnabled => !this.IsDisposed && this.enabled; /// public override string BackendName => "MinHook"; @@ -131,9 +124,7 @@ internal class FunctionPointerVariableHook : Hook public override void Dispose() { if (this.IsDisposed) - { return; - } this.Disable(); @@ -148,15 +139,13 @@ internal class FunctionPointerVariableHook : Hook /// public override void Enable() { - this.CheckDisposed(); - - if (this.enabled) - { - return; - } - lock (HookManager.HookEnableSyncRoot) { + this.CheckDisposed(); + + if (this.enabled) + return; + Marshal.WriteIntPtr(this.ppfnThunkJumpTarget, this.pfnDetour); this.enabled = true; } @@ -165,15 +154,14 @@ internal class FunctionPointerVariableHook : Hook /// public override void Disable() { - this.CheckDisposed(); - - if (!this.enabled) - { - return; - } - lock (HookManager.HookEnableSyncRoot) { + if (this.IsDisposed) + return; + + if (!this.enabled) + return; + Marshal.WriteIntPtr(this.ppfnThunkJumpTarget, this.pfnOriginal); this.enabled = false; } diff --git a/Dalamud/Hooking/Internal/MinHookHook.cs b/Dalamud/Hooking/Internal/MinHookHook.cs index 0305f3c84..d4889ba11 100644 --- a/Dalamud/Hooking/Internal/MinHookHook.cs +++ b/Dalamud/Hooking/Internal/MinHookHook.cs @@ -50,14 +50,7 @@ internal class MinHookHook : Hook where T : Delegate } /// - public override bool IsEnabled - { - get - { - this.CheckDisposed(); - return this.minHookImpl.Enabled; - } - } + public override bool IsEnabled => !this.IsDisposed && this.minHookImpl.Enabled; /// public override string BackendName => "MinHook"; @@ -84,28 +77,29 @@ internal class MinHookHook : Hook where T : Delegate /// public override void Enable() { - this.CheckDisposed(); - - if (!this.minHookImpl.Enabled) + lock (HookManager.HookEnableSyncRoot) { - lock (HookManager.HookEnableSyncRoot) - { - this.minHookImpl.Enable(); - } + this.CheckDisposed(); + + if (!this.minHookImpl.Enabled) + return; + + this.minHookImpl.Enable(); } } /// public override void Disable() { - this.CheckDisposed(); - - if (this.minHookImpl.Enabled) + lock (HookManager.HookEnableSyncRoot) { - lock (HookManager.HookEnableSyncRoot) - { - this.minHookImpl.Disable(); - } + if (this.IsDisposed) + return; + + if (!this.minHookImpl.Enabled) + return; + + this.minHookImpl.Disable(); } } } diff --git a/Dalamud/Hooking/Internal/ReloadedHook.cs b/Dalamud/Hooking/Internal/ReloadedHook.cs index 2b0a4e9ce..cdd939d19 100644 --- a/Dalamud/Hooking/Internal/ReloadedHook.cs +++ b/Dalamud/Hooking/Internal/ReloadedHook.cs @@ -45,14 +45,7 @@ internal class ReloadedHook : Hook where T : Delegate } /// - public override bool IsEnabled - { - get - { - this.CheckDisposed(); - return this.hookImpl.IsHookEnabled; - } - } + public override bool IsEnabled => !this.IsDisposed && this.hookImpl.IsHookEnabled; /// public override string BackendName => "Reloaded"; @@ -73,10 +66,10 @@ internal class ReloadedHook : Hook where T : Delegate /// public override void Enable() { - this.CheckDisposed(); - lock (HookManager.HookEnableSyncRoot) { + this.CheckDisposed(); + if (!this.hookImpl.IsHookEnabled) this.hookImpl.Enable(); } @@ -85,10 +78,11 @@ internal class ReloadedHook : Hook where T : Delegate /// public override void Disable() { - this.CheckDisposed(); - lock (HookManager.HookEnableSyncRoot) { + if (this.IsDisposed) + return; + if (!this.hookImpl.IsHookActivated) return; From b1c874c12387927fdc5ffc338b7c184717706b3e Mon Sep 17 00:00:00 2001 From: goaaats Date: Fri, 9 May 2025 23:40:49 +0200 Subject: [PATCH 069/106] Fade in/out window system windows --- Dalamud/Interface/Windowing/Window.cs | 67 +++++++++++++++++++-- Dalamud/Interface/Windowing/WindowSystem.cs | 3 + 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 8dc517cb2..e4ff699ed 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -32,6 +32,9 @@ namespace Dalamud.Interface.Windowing; /// public abstract class Window { + private const float FadeInOutTime = 1f; + private const float FadeInOutStep = 0.09f; + private static readonly ModuleLog Log = new("WindowSystem"); private static bool wasEscPressedLastFrame = false; @@ -48,6 +51,12 @@ public abstract class Window private PresetModel.PresetWindow? presetWindow; private bool presetDirty = false; + private float fadeInTimer = 1f; + private float fadeOutTimer = 0f; + private IDrawListTextureWrap? fadeOutTexture = null; + private Vector2 fadeOutSize = Vector2.Zero; + private Vector2 fadeOutOrigin = Vector2.Zero; + /// /// Initializes a new instance of the class. /// @@ -89,6 +98,11 @@ public abstract class Window /// Enable the built-in "additional options" menu on the title bar. ///
UseAdditionalOptions = 1 << 2, + + /// + /// Do not draw non-critical animations. + /// + IsReducedMotion = 1 << 3, } /// @@ -316,6 +330,7 @@ public abstract class Window internal void DrawInternal(WindowDrawFlags internalDrawFlags, WindowSystemPersistence? persistence) { this.PreOpenCheck(); + var isReducedMotion = internalDrawFlags.HasFlag(WindowDrawFlags.IsReducedMotion); if (!this.IsOpen) { @@ -330,9 +345,35 @@ public abstract class Window UIGlobals.PlaySoundEffect(this.OnCloseSfxId); } + if (this.fadeOutTexture != null) + { + this.fadeOutTimer -= FadeInOutStep; + if (this.fadeOutTimer <= 0f) + { + this.fadeOutTexture.Dispose(); + this.fadeOutTexture = null; + } + else + { + var dl = ImGui.GetBackgroundDrawList(); + dl.AddImage( + this.fadeOutTexture.ImGuiHandle, + this.fadeOutOrigin, + this.fadeOutOrigin + this.fadeOutSize, + Vector2.Zero, + Vector2.One, + ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, Math.Clamp(this.fadeOutTimer / FadeInOutTime, 0f, 1f)))); + } + } + + this.fadeInTimer = !isReducedMotion ? 0f : FadeInOutTime; return; } + this.fadeInTimer += FadeInOutStep; + if (this.fadeInTimer > FadeInOutTime) + this.fadeInTimer = FadeInOutTime; + this.Update(); if (!this.DrawConditions()) return; @@ -546,8 +587,19 @@ public abstract class Window } } + this.fadeOutSize = ImGui.GetWindowSize(); + this.fadeOutOrigin = ImGui.GetWindowPos(); + ImGui.End(); + if (!this.internalIsOpen && this.fadeOutTexture == null && !isReducedMotion) + { + this.fadeOutTexture = Service.Get().CreateDrawListTexture( + "WindowFadeOutTexture"); + this.fadeOutTexture.ResizeAndDrawWindow(this.WindowName, Vector2.One); + this.fadeOutTimer = FadeInOutTime; + } + if (printWindow) { var tex = Service.Get().CreateDrawListTexture( @@ -567,7 +619,7 @@ public abstract class Window ImGui.PopID(); } - private void ApplyConditionals() + private unsafe void ApplyConditionals() { if (this.Position.HasValue) { @@ -594,15 +646,18 @@ public abstract class Window ImGui.SetNextWindowSizeConstraints(this.SizeConstraints.Value.MinimumSize * ImGuiHelpers.GlobalScale, this.SizeConstraints.Value.MaximumSize * ImGuiHelpers.GlobalScale); } - if (this.BgAlpha.HasValue) + var maxBgAlpha = this.internalAlpha ?? this.BgAlpha; + var fadeInAlpha = this.fadeInTimer / FadeInOutTime; + if (fadeInAlpha < 1f) { - ImGui.SetNextWindowBgAlpha(this.BgAlpha.Value); + maxBgAlpha = maxBgAlpha.HasValue ? + Math.Clamp(maxBgAlpha.Value * fadeInAlpha, 0f, 1f) : + (*ImGui.GetStyleColorVec4(ImGuiCol.WindowBg)).W * fadeInAlpha; } - // Manually set alpha takes precedence, if devs don't want that, they should turn it off - if (this.internalAlpha.HasValue) + if (maxBgAlpha.HasValue) { - ImGui.SetNextWindowBgAlpha(this.internalAlpha.Value); + ImGui.SetNextWindowBgAlpha(maxBgAlpha.Value); } } diff --git a/Dalamud/Interface/Windowing/WindowSystem.cs b/Dalamud/Interface/Windowing/WindowSystem.cs index f79eea025..ccdc2b44c 100644 --- a/Dalamud/Interface/Windowing/WindowSystem.cs +++ b/Dalamud/Interface/Windowing/WindowSystem.cs @@ -119,6 +119,9 @@ public class WindowSystem if (config?.IsFocusManagementEnabled ?? false) flags |= Window.WindowDrawFlags.UseFocusManagement; + if (config?.ReduceMotions ?? false) + flags |= Window.WindowDrawFlags.IsReducedMotion; + // Shallow clone the list of windows so that we can edit it without modifying it while the loop is iterating foreach (var window in this.windows.ToArray()) { From b4e62571a61329fabacc9100ef66fba1479d04ae Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 May 2025 01:10:53 +0200 Subject: [PATCH 070/106] Add option to auto-update disabled plugins --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 5 +++++ .../Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs | 4 ++++ Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 3bc30fad0..168382fab 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -498,6 +498,11 @@ internal sealed class DalamudConfiguration : IInternalDisposableService /// public bool SendUpdateNotificationToChat { get; set; } = false; + /// + /// Gets or sets a value indicating whether disabled plugins should be auto-updated. + /// + public bool UpdateDisabledPlugins { get; set; } = false; + /// /// Gets or sets a value indicating where notifications are anchored to on the screen. /// diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs index 9356131ad..3815c4425 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAutoUpdate.cs @@ -22,6 +22,7 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; public class SettingsTabAutoUpdates : SettingsTab { private AutoUpdateBehavior behavior; + private bool updateDisabledPlugins; private bool checkPeriodically; private bool chatNotification; private string pickerSearch = string.Empty; @@ -66,6 +67,7 @@ public class SettingsTabAutoUpdates : SettingsTab ImGuiHelpers.ScaledDummy(8); + ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateDisabledPlugins", "Auto-Update plugins that are currently disabled"), ref this.updateDisabledPlugins); ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdateChatMessage", "Show notification about updates available in chat"), ref this.chatNotification); ImGui.Checkbox(Loc.Localize("DalamudSettingsAutoUpdatePeriodically", "Periodically check for new updates while playing"), ref this.checkPeriodically); ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsAutoUpdatePeriodicallyHint", @@ -237,6 +239,7 @@ public class SettingsTabAutoUpdates : SettingsTab var configuration = Service.Get(); this.behavior = configuration.AutoUpdateBehavior ?? AutoUpdateBehavior.None; + this.updateDisabledPlugins = configuration.UpdateDisabledPlugins; this.chatNotification = configuration.SendUpdateNotificationToChat; this.checkPeriodically = configuration.CheckPeriodicallyForUpdates; this.autoUpdatePreferences = configuration.PluginAutoUpdatePreferences; @@ -249,6 +252,7 @@ public class SettingsTabAutoUpdates : SettingsTab var configuration = Service.Get(); configuration.AutoUpdateBehavior = this.behavior; + configuration.UpdateDisabledPlugins = this.updateDisabledPlugins; configuration.SendUpdateNotificationToChat = this.chatNotification; configuration.CheckPeriodicallyForUpdates = this.checkPeriodically; configuration.PluginAutoUpdatePreferences = this.autoUpdatePreferences; diff --git a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs index adec4f73d..1a57790f8 100644 --- a/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs +++ b/Dalamud/Plugin/Internal/AutoUpdate/AutoUpdateManager.cs @@ -460,7 +460,7 @@ internal class AutoUpdateManager : IServiceType .Where( p => !p.InstalledPlugin.IsDev && // Never update dev-plugins - p.InstalledPlugin.IsWantedByAnyProfile && // Never update plugins that are not wanted by any profile(not enabled) + (p.InstalledPlugin.IsWantedByAnyProfile || this.config.UpdateDisabledPlugins) && // Never update plugins that are not wanted by any profile(not enabled) !p.InstalledPlugin.Manifest.ScheduledForDeletion); // Never update plugins that we want to get rid of return updateablePlugins.Where(FilterPlugin).ToList(); From 9afece8679c615baf8058b6b531120b7525350d9 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 May 2025 13:08:24 +0200 Subject: [PATCH 071/106] Improve fade-in, use delta time, draw fade-out texture on fake window to preserve focus order --- Dalamud/Interface/Windowing/Window.cs | 54 +++++++++++++++++++++------ 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index e4ff699ed..3ff39f42f 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -16,6 +16,7 @@ using Dalamud.Interface.Textures.Internal; using Dalamud.Interface.Textures.TextureWraps; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Internal; +using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing.Persistence; using Dalamud.Logging.Internal; @@ -32,8 +33,7 @@ namespace Dalamud.Interface.Windowing; /// public abstract class Window { - private const float FadeInOutTime = 1f; - private const float FadeInOutStep = 0.09f; + private const float FadeInOutTime = 0.08f; private static readonly ModuleLog Log = new("WindowSystem"); @@ -51,6 +51,7 @@ public abstract class Window private PresetModel.PresetWindow? presetWindow; private bool presetDirty = false; + private bool pushedFadeInAlpha = false; private float fadeInTimer = 1f; private float fadeOutTimer = 0f; private IDrawListTextureWrap? fadeOutTexture = null; @@ -347,7 +348,7 @@ public abstract class Window if (this.fadeOutTexture != null) { - this.fadeOutTimer -= FadeInOutStep; + this.fadeOutTimer -= ImGui.GetIO().DeltaTime; if (this.fadeOutTimer <= 0f) { this.fadeOutTexture.Dispose(); @@ -355,14 +356,7 @@ public abstract class Window } else { - var dl = ImGui.GetBackgroundDrawList(); - dl.AddImage( - this.fadeOutTexture.ImGuiHandle, - this.fadeOutOrigin, - this.fadeOutOrigin + this.fadeOutSize, - Vector2.Zero, - Vector2.One, - ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, Math.Clamp(this.fadeOutTimer / FadeInOutTime, 0f, 1f)))); + this.DrawFakeFadeOutWindow(); } } @@ -370,7 +364,7 @@ public abstract class Window return; } - this.fadeInTimer += FadeInOutStep; + this.fadeInTimer += ImGui.GetIO().DeltaTime; if (this.fadeInTimer > FadeInOutTime) this.fadeInTimer = FadeInOutTime; @@ -592,6 +586,12 @@ public abstract class Window ImGui.End(); + if (this.pushedFadeInAlpha) + { + ImGui.PopStyleVar(); + this.pushedFadeInAlpha = false; + } + if (!this.internalIsOpen && this.fadeOutTexture == null && !isReducedMotion) { this.fadeOutTexture = Service.Get().CreateDrawListTexture( @@ -653,6 +653,8 @@ public abstract class Window maxBgAlpha = maxBgAlpha.HasValue ? Math.Clamp(maxBgAlpha.Value * fadeInAlpha, 0f, 1f) : (*ImGui.GetStyleColorVec4(ImGuiCol.WindowBg)).W * fadeInAlpha; + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * fadeInAlpha); + this.pushedFadeInAlpha = true; } if (maxBgAlpha.HasValue) @@ -805,6 +807,34 @@ public abstract class Window ImGui.PopClipRect(); } + private void DrawFakeFadeOutWindow() + { + // Draw a fake window to fade out, so that the fade out texture stays in the right place in the + // focus order + ImGui.SetNextWindowPos(this.fadeOutOrigin); + ImGui.SetNextWindowSize(this.fadeOutSize); + + using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, Vector2.Zero); + style.Push(ImGuiStyleVar.WindowBorderSize, 0); + style.Push(ImGuiStyleVar.FrameBorderSize, 0); + + var fakeFlags = ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs | + ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground; + if (ImGui.Begin(this.WindowName, fakeFlags)) + { + var dl = ImGui.GetWindowDrawList(); + dl.AddImage( + this.fadeOutTexture!.ImGuiHandle, + this.fadeOutOrigin, + this.fadeOutOrigin + this.fadeOutSize, + Vector2.Zero, + Vector2.One, + ImGui.ColorConvertFloat4ToU32(new(1f, 1f, 1f, Math.Clamp(this.fadeOutTimer / FadeInOutTime, 0f, 1f)))); + } + + ImGui.End(); + } + /// /// Structure detailing the size constraints of a window. /// From 271c258e4038e71228b3f8080a407988fd3c2432 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 May 2025 13:16:12 +0200 Subject: [PATCH 072/106] Add per-window opt-out for fades --- Dalamud/Interface/Windowing/Window.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 3ff39f42f..51a6549e7 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -143,6 +143,12 @@ public abstract class Window /// public uint OnCloseSfxId { get; set; } = 24u; + /// + /// Gets or sets a value indicating whether this window should not fade in and out, regardless of the users' + /// preference. + /// + public bool DisableFadeInFadeOut { get; set; } = false; + /// /// Gets or sets the position of this window. /// @@ -331,7 +337,7 @@ public abstract class Window internal void DrawInternal(WindowDrawFlags internalDrawFlags, WindowSystemPersistence? persistence) { this.PreOpenCheck(); - var isReducedMotion = internalDrawFlags.HasFlag(WindowDrawFlags.IsReducedMotion); + var doFades = !internalDrawFlags.HasFlag(WindowDrawFlags.IsReducedMotion) && !this.DisableFadeInFadeOut; if (!this.IsOpen) { @@ -360,7 +366,7 @@ public abstract class Window } } - this.fadeInTimer = !isReducedMotion ? 0f : FadeInOutTime; + this.fadeInTimer = doFades ? 0f : FadeInOutTime; return; } @@ -592,7 +598,7 @@ public abstract class Window this.pushedFadeInAlpha = false; } - if (!this.internalIsOpen && this.fadeOutTexture == null && !isReducedMotion) + if (!this.internalIsOpen && this.fadeOutTexture == null && doFades) { this.fadeOutTexture = Service.Get().CreateDrawListTexture( "WindowFadeOutTexture"); @@ -818,9 +824,10 @@ public abstract class Window style.Push(ImGuiStyleVar.WindowBorderSize, 0); style.Push(ImGuiStyleVar.FrameBorderSize, 0); - var fakeFlags = ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs | - ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground; - if (ImGui.Begin(this.WindowName, fakeFlags)) + const ImGuiWindowFlags flags = ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | + ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs | + ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground; + if (ImGui.Begin(this.WindowName, flags)) { var dl = ImGui.GetWindowDrawList(); dl.AddImage( From e9aa2e2ac395285a396d07daef431ff59e2c1c52 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 May 2025 13:23:32 +0200 Subject: [PATCH 073/106] Respect alpha when drawing installer loading overlay --- .../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 c1bd64447..db0109ee0 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -575,7 +575,7 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.GetWindowDrawList().AddRectFilled( ImGui.GetWindowPos() + new Vector2(0, titleHeight), ImGui.GetWindowPos() + windowSize, - 0xCC000000, + ImGui.ColorConvertFloat4ToU32(new(0f, 0f, 0f, 0.8f * ImGui.GetStyle().Alpha)), ImGui.GetStyle().WindowRounding, ImDrawFlags.RoundCornersBottom); ImGui.PopClipRect(); From 5c33d150cdd34084fbe461e0080594cac68b959e Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 May 2025 14:21:30 +0200 Subject: [PATCH 074/106] Add Window.OnSafeToRemove(), tune timings a bit more --- Dalamud/Interface/Windowing/Window.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 51a6549e7..032f6e93e 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -33,7 +33,7 @@ namespace Dalamud.Interface.Windowing; /// public abstract class Window { - private const float FadeInOutTime = 0.08f; + private const float FadeInOutTime = 0.072f; private static readonly ModuleLog Log = new("WindowSystem"); @@ -52,7 +52,7 @@ public abstract class Window private bool presetDirty = false; private bool pushedFadeInAlpha = false; - private float fadeInTimer = 1f; + private float fadeInTimer = 0f; private float fadeOutTimer = 0f; private IDrawListTextureWrap? fadeOutTexture = null; private Vector2 fadeOutSize = Vector2.Zero; @@ -322,6 +322,14 @@ public abstract class Window { } + /// + /// Code to be executed when the window is safe to be disposed or removed from the window system. + /// Doing so in may result in animations not playing correctly. + /// + public virtual void OnSafeToRemove() + { + } + /// /// Code to be executed every frame, even when the window is collapsed. /// @@ -359,6 +367,7 @@ public abstract class Window { this.fadeOutTexture.Dispose(); this.fadeOutTexture = null; + this.OnSafeToRemove(); } else { From c33a5346b1ef9b81af54c938e8a022692968a708 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 May 2025 18:37:34 +0200 Subject: [PATCH 075/106] Respect fadein/out for Window.DrawConditions() and Window.IsOpen --- Dalamud/Interface/Windowing/Window.cs | 94 ++++++++++++++++++++------- 1 file changed, 71 insertions(+), 23 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 032f6e93e..876f9ec63 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -41,6 +41,10 @@ public abstract class Window private bool internalLastIsOpen = false; private bool internalIsOpen = false; + private bool internalWantsToClose = false; + private bool internalLastDrawConditions = true; + private bool internalDrawConditions = true; + private bool internalIsPinned = false; private bool internalIsClickthrough = false; private bool didPushInternalAlpha = false; @@ -229,7 +233,18 @@ public abstract class Window public bool IsOpen { get => this.internalIsOpen; - set => this.internalIsOpen = value; + set + { + if (!value && this.internalIsOpen) + { + this.internalWantsToClose = true; + } + else if (value && !this.internalIsOpen) + { + this.internalWantsToClose = false; + this.internalIsOpen = true; + } + } } private bool CanShowCloseButton => this.ShowCloseButton && !this.internalIsClickthrough; @@ -360,22 +375,7 @@ public abstract class Window UIGlobals.PlaySoundEffect(this.OnCloseSfxId); } - if (this.fadeOutTexture != null) - { - this.fadeOutTimer -= ImGui.GetIO().DeltaTime; - if (this.fadeOutTimer <= 0f) - { - this.fadeOutTexture.Dispose(); - this.fadeOutTexture = null; - this.OnSafeToRemove(); - } - else - { - this.DrawFakeFadeOutWindow(); - } - } - - this.fadeInTimer = doFades ? 0f : FadeInOutTime; + DrawFadeOut(); return; } @@ -384,8 +384,15 @@ public abstract class Window this.fadeInTimer = FadeInOutTime; this.Update(); - if (!this.DrawConditions()) + this.internalDrawConditions = this.DrawConditions(); + if (this.internalDrawConditions == this.internalLastDrawConditions && !this.internalDrawConditions) + { + DrawFadeOut(); return; + } + + if (this.internalDrawConditions) + this.internalLastDrawConditions = this.internalDrawConditions; var hasNamespace = !string.IsNullOrEmpty(this.Namespace); @@ -607,12 +614,21 @@ public abstract class Window this.pushedFadeInAlpha = false; } - if (!this.internalIsOpen && this.fadeOutTexture == null && doFades) + if (this.internalDrawConditions != this.internalLastDrawConditions && !this.internalDrawConditions) { - this.fadeOutTexture = Service.Get().CreateDrawListTexture( - "WindowFadeOutTexture"); - this.fadeOutTexture.ResizeAndDrawWindow(this.WindowName, Vector2.One); - this.fadeOutTimer = FadeInOutTime; + this.internalLastDrawConditions = this.internalDrawConditions; + SetupFadeOut(); + } + + if (this.internalWantsToClose) + { + this.internalIsOpen = false; + this.internalWantsToClose = false; + } + + if (!this.internalIsOpen) + { + SetupFadeOut(); } if (printWindow) @@ -632,6 +648,38 @@ public abstract class Window if (hasNamespace) ImGui.PopID(); + return; + + void SetupFadeOut() + { + if (this.fadeOutTexture == null && doFades) + { + this.fadeOutTexture = Service.Get().CreateDrawListTexture( + "WindowFadeOutTexture"); + this.fadeOutTexture.ResizeAndDrawWindow(this.WindowName, Vector2.One); + this.fadeOutTimer = FadeInOutTime; + } + } + + void DrawFadeOut() + { + if (this.fadeOutTexture != null) + { + this.fadeOutTimer -= ImGui.GetIO().DeltaTime; + if (this.fadeOutTimer <= 0f) + { + this.fadeOutTexture.Dispose(); + this.fadeOutTexture = null; + this.OnSafeToRemove(); + } + else + { + this.DrawFakeFadeOutWindow(); + } + } + + this.fadeInTimer = doFades ? 0f : FadeInOutTime; + } } private unsafe void ApplyConditionals() From 421d8cee3b258b342428fd7447e8b86c468c686b Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 10 May 2025 19:31:06 +0200 Subject: [PATCH 076/106] Revert "Respect fadein/out for Window.DrawConditions() and Window.IsOpen" This reverts commit c33a5346b1ef9b81af54c938e8a022692968a708. --- Dalamud/Interface/Windowing/Window.cs | 94 +++++++-------------------- 1 file changed, 23 insertions(+), 71 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 876f9ec63..032f6e93e 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -41,10 +41,6 @@ public abstract class Window private bool internalLastIsOpen = false; private bool internalIsOpen = false; - private bool internalWantsToClose = false; - private bool internalLastDrawConditions = true; - private bool internalDrawConditions = true; - private bool internalIsPinned = false; private bool internalIsClickthrough = false; private bool didPushInternalAlpha = false; @@ -233,18 +229,7 @@ public abstract class Window public bool IsOpen { get => this.internalIsOpen; - set - { - if (!value && this.internalIsOpen) - { - this.internalWantsToClose = true; - } - else if (value && !this.internalIsOpen) - { - this.internalWantsToClose = false; - this.internalIsOpen = true; - } - } + set => this.internalIsOpen = value; } private bool CanShowCloseButton => this.ShowCloseButton && !this.internalIsClickthrough; @@ -375,7 +360,22 @@ public abstract class Window UIGlobals.PlaySoundEffect(this.OnCloseSfxId); } - DrawFadeOut(); + if (this.fadeOutTexture != null) + { + this.fadeOutTimer -= ImGui.GetIO().DeltaTime; + if (this.fadeOutTimer <= 0f) + { + this.fadeOutTexture.Dispose(); + this.fadeOutTexture = null; + this.OnSafeToRemove(); + } + else + { + this.DrawFakeFadeOutWindow(); + } + } + + this.fadeInTimer = doFades ? 0f : FadeInOutTime; return; } @@ -384,15 +384,8 @@ public abstract class Window this.fadeInTimer = FadeInOutTime; this.Update(); - this.internalDrawConditions = this.DrawConditions(); - if (this.internalDrawConditions == this.internalLastDrawConditions && !this.internalDrawConditions) - { - DrawFadeOut(); + if (!this.DrawConditions()) return; - } - - if (this.internalDrawConditions) - this.internalLastDrawConditions = this.internalDrawConditions; var hasNamespace = !string.IsNullOrEmpty(this.Namespace); @@ -614,21 +607,12 @@ public abstract class Window this.pushedFadeInAlpha = false; } - if (this.internalDrawConditions != this.internalLastDrawConditions && !this.internalDrawConditions) + if (!this.internalIsOpen && this.fadeOutTexture == null && doFades) { - this.internalLastDrawConditions = this.internalDrawConditions; - SetupFadeOut(); - } - - if (this.internalWantsToClose) - { - this.internalIsOpen = false; - this.internalWantsToClose = false; - } - - if (!this.internalIsOpen) - { - SetupFadeOut(); + this.fadeOutTexture = Service.Get().CreateDrawListTexture( + "WindowFadeOutTexture"); + this.fadeOutTexture.ResizeAndDrawWindow(this.WindowName, Vector2.One); + this.fadeOutTimer = FadeInOutTime; } if (printWindow) @@ -648,38 +632,6 @@ public abstract class Window if (hasNamespace) ImGui.PopID(); - return; - - void SetupFadeOut() - { - if (this.fadeOutTexture == null && doFades) - { - this.fadeOutTexture = Service.Get().CreateDrawListTexture( - "WindowFadeOutTexture"); - this.fadeOutTexture.ResizeAndDrawWindow(this.WindowName, Vector2.One); - this.fadeOutTimer = FadeInOutTime; - } - } - - void DrawFadeOut() - { - if (this.fadeOutTexture != null) - { - this.fadeOutTimer -= ImGui.GetIO().DeltaTime; - if (this.fadeOutTimer <= 0f) - { - this.fadeOutTexture.Dispose(); - this.fadeOutTexture = null; - this.OnSafeToRemove(); - } - else - { - this.DrawFakeFadeOutWindow(); - } - } - - this.fadeInTimer = doFades ? 0f : FadeInOutTime; - } } private unsafe void ApplyConditionals() From 15352a3e235a893e097e8f3e998818124a278416 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 17 May 2025 12:57:55 +0200 Subject: [PATCH 077/106] Upgrade to goatcorp.Reloaded.Hooks 4.2.0-goatcorp5 Possibly fixes an issue with Wine 10 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index fd1c3ef64..9364d5f72 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -60,7 +60,7 @@ - + From 43ab6f6f638ff2406c7c0eb8dc371acf0dac6a0c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Fri, 23 May 2025 15:44:24 -0700 Subject: [PATCH 078/106] UiDebug2 Misc Fixes (#2273) * Fix incorrect type check * Fix address calculation --- Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs | 2 +- Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs index 6c12d3b4c..4440f7d34 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Res.cs @@ -86,7 +86,7 @@ internal unsafe partial class ResNodeTree : IDisposable /// An existing or newly-created instance of . internal static ResNodeTree GetOrCreate(AtkResNode* node, AddonTree addonTree) => addonTree.NodeTrees.TryGetValue((nint)node, out var nodeTree) ? nodeTree - : (int)node->Type > 1000 + : (int)node->Type >= 1000 ? new ComponentNodeTree(node, addonTree) : node->Type switch { diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs index 57e5eff99..259d1f827 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs @@ -78,7 +78,7 @@ public readonly unsafe partial struct TimelineTree { var animation = this.Resource->Animations[a]; var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation; - this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + (a * sizeof(AtkTimelineAnimation)))); + this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + a)); } } } From fe8efc9dde597e398b68baf859581cd103e08283 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Mon, 26 May 2025 12:09:50 -0700 Subject: [PATCH 079/106] UIDebug2 Timeline Labelset support (#2275) * Add LabelSet Display * Update TimelineTree.cs --- .../UiDebug2/Browsing/TimelineTree.cs | 101 +++++++++++++++--- 1 file changed, 87 insertions(+), 14 deletions(-) diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs index 259d1f827..30ff795f3 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/TimelineTree.cs @@ -52,9 +52,10 @@ public readonly unsafe partial struct TimelineTree return; } - var count = this.Resource->AnimationCount; + var animationCount = this.Resource->AnimationCount; + var labelSetCount = this.Resource->LabelSetCount; - if (count > 0) + if (animationCount > 0) { using var tree = ImRaii.TreeNode($"Timeline##{(nint)this.node:X}timeline", SpanFullWidth); @@ -66,22 +67,35 @@ public readonly unsafe partial struct TimelineTree ShowStruct(this.NodeTimeline); - PrintFieldValuePairs( - ("Id", $"{this.NodeTimeline->Resource->Id}"), - ("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"), - ("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})")); - - PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}")); - ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List"); - - for (var a = 0; a < count; a++) + if (this.Resource->Animations is not null) { - var animation = this.Resource->Animations[a]; - var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation; - this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + a)); + PrintFieldValuePairs( + ("Id", $"{this.NodeTimeline->Resource->Id}"), + ("Parent Time", $"{this.NodeTimeline->ParentFrameTime:F2} ({this.NodeTimeline->ParentFrameTime * 30:F0})"), + ("Frame Time", $"{this.NodeTimeline->FrameTime:F2} ({this.NodeTimeline->FrameTime * 30:F0})")); + + PrintFieldValuePairs(("Active Label Id", $"{this.NodeTimeline->ActiveLabelId}"), ("Duration", $"{this.NodeTimeline->LabelFrameIdxDuration}"), ("End Frame", $"{this.NodeTimeline->LabelEndFrameIdx}")); + ImGui.TextColored(new(0.6f, 0.6f, 0.6f, 1), "Animation List"); + + for (var a = 0; a < animationCount; a++) + { + var animation = this.Resource->Animations[a]; + var isActive = this.ActiveAnimation != null && &animation == this.ActiveAnimation; + this.PrintAnimation(animation, a, isActive, (nint)(this.NodeTimeline->Resource->Animations + a)); + } } } } + + if (labelSetCount > 0 && this.Resource->LabelSets is not null) + { + using var tree = ImRaii.TreeNode($"Timeline Label Sets##{(nint)this.node:X}LabelSets", SpanFullWidth); + + if (tree.Success) + { + this.DrawLabelSets(); + } + } } private static void GetFrameColumn(Span keyGroups, List columns, ushort endFrame) @@ -381,4 +395,63 @@ public readonly unsafe partial struct TimelineTree return columns; } + + private void DrawLabelSets() + { + PrintFieldValuePair("LabelSet", $"{(nint)this.NodeTimeline->Resource->LabelSets:X}"); + + ImGui.SameLine(); + + ShowStruct(this.NodeTimeline->Resource->LabelSets); + + PrintFieldValuePairs( + ("StartFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->StartFrameIdx}"), + ("EndFrameIdx", $"{this.NodeTimeline->Resource->LabelSets->EndFrameIdx}")); + + using var labelSetTable = ImRaii.TreeNode("Entries"); + if (labelSetTable.Success) + { + var keyFrameGroup = this.Resource->LabelSets->LabelKeyGroup; + + using var table = ImRaii.Table($"##{(nint)this.node}labelSetKeyFrameTable", 7, Borders | SizingFixedFit | RowBg | NoHostExtendX); + if (table.Success) + { + ImGui.TableSetupColumn("Frame ID", WidthFixed); + ImGui.TableSetupColumn("Speed Start", WidthFixed); + ImGui.TableSetupColumn("Speed End", WidthFixed); + ImGui.TableSetupColumn("Interpolation", WidthFixed); + ImGui.TableSetupColumn("Label ID", WidthFixed); + ImGui.TableSetupColumn("Jump Behavior", WidthFixed); + ImGui.TableSetupColumn("Target Label ID", WidthFixed); + + ImGui.TableHeadersRow(); + + for (var l = 0; l < keyFrameGroup.KeyFrameCount; l++) + { + var keyFrame = keyFrameGroup.KeyFrames[l]; + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.FrameIdx}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.SpeedCoefficient1:F2}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.SpeedCoefficient2:F2}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.Interpolation}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.Value.Label.LabelId}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.Value.Label.JumpBehavior}"); + + ImGui.TableNextColumn(); + ImGui.TextUnformatted($"{keyFrame.Value.Label.JumpLabelId}"); + } + } + } + } } From 452ff3adb3db69d3efccb03863f5494280e084da Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Mon, 26 May 2025 21:16:58 +0200 Subject: [PATCH 080/106] Update ClientStructs (#2269) 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 ba0a66024..a01acc1d2 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit ba0a66024d53a05ddc4d51e3bfaafc583e61e50e +Subproject commit a01acc1d29f540a2c363e2519ce5e1dfb515de84 From a12147c94506c8a88d3b5639fba1c6c81938edda Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 27 May 2025 17:56:34 +0200 Subject: [PATCH 081/106] Update ClientStructs (#2276) 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 a01acc1d2..cc8ace372 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit a01acc1d29f540a2c363e2519ce5e1dfb515de84 +Subproject commit cc8ace37200f84b435afd2fded279ec347c49b1e From 544eb39753419d351c29e37982a558027f0186aa Mon Sep 17 00:00:00 2001 From: Aireil <33433913+Aireil@users.noreply.github.com> Date: Tue, 27 May 2025 20:58:29 +0200 Subject: [PATCH 082/106] Add new kinds to FlyTextKind enum (#2278) --- Dalamud/Game/Gui/FlyText/FlyTextKind.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs index 407410e85..2b8325927 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextKind.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextKind.cs @@ -105,16 +105,26 @@ public enum FlyTextKind : int /// /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// Added in 7.2, usage currently unknown. /// + [Obsolete("Use Knowledge instead", true)] Unknown17 = 17, /// /// Val1 in serif font, Text2 in sans-serif as subtitle. - /// Added in 7.2, usage currently unknown. /// + Knowledge = 17, + + /// + /// Val1 in serif font, Text2 in sans-serif as subtitle. + /// + [Obsolete("Use PhantomExp instead", true)] Unknown18 = 18, + /// + /// Val1 in serif font, Text2 in sans-serif as subtitle. + /// + PhantomExp = 18, + /// /// Sans-serif Text1 next to serif Val1 with all caps condensed font MP with Text2 in sans-serif as subtitle. /// From d80202a75555941dfd8692d3c730b641a153cf77 Mon Sep 17 00:00:00 2001 From: goaaats Date: Tue, 27 May 2025 22:50:02 +0200 Subject: [PATCH 083/106] build: 12.0.1.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 9364d5f72..fa855490d 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.0.15 + 12.0.1.0 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From e415699bb353d0ff3995d617a7f9cb9f234f5ae0 Mon Sep 17 00:00:00 2001 From: srkizer Date: Fri, 30 May 2025 00:06:48 +0900 Subject: [PATCH 084/106] DrawListTextureWrap: use two textures (#2285) Making premultiplied pixel data into straight alpha in-place using UAV seems to be not working on older graphics cards. Now every instance of DrawListTextureWrap keeps two GPU textures, where one keeps a premultiplied data which will be written to using ImGui draw data and read from to calculate straight alpha pixel data. --- .../Internal/DrawListTextureWrap.cs | 44 +++++++++++++----- .../Renderer.DrawToPremul.hlsl | 7 ++- .../Renderer.DrawToPremul.ps.bin | Bin 16620 -> 4759 bytes .../Renderer.DrawToPremul.vs.bin | Bin 16972 -> 7328 bytes .../Renderer.MakeStraight.hlsl | 13 ++---- .../Renderer.MakeStraight.ps.bin | Bin 14596 -> 5372 bytes .../Renderer.MakeStraight.vs.bin | Bin 14360 -> 3094 bytes .../Internal/DrawListTextureWrap/Renderer.cs | 13 +++--- 8 files changed, 47 insertions(+), 30 deletions(-) diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs index 4e82479b0..4901ca2e3 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap.cs @@ -26,7 +26,10 @@ internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, private ComPtr tex; private ComPtr srv; private ComPtr rtv; - private ComPtr uav; + + private ComPtr texPremultiplied; + private ComPtr srvPremultiplied; + private ComPtr rtvPremultiplied; private int width; private int height; @@ -138,7 +141,9 @@ internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, this.srv.Reset(); this.tex.Reset(); this.rtv.Reset(); - this.uav.Reset(); + this.srvPremultiplied.Reset(); + this.texPremultiplied.Reset(); + this.rtvPremultiplied.Reset(); this.device.Reset(); this.deviceContext.Reset(); @@ -180,7 +185,7 @@ internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, // Clear the texture first, as the texture exists. var clearColor = this.ClearColor; - this.deviceContext.Get()->ClearRenderTargetView(this.rtv.Get(), (float*)&clearColor); + this.deviceContext.Get()->ClearRenderTargetView(this.rtvPremultiplied.Get(), (float*)&clearColor); // If there is nothing to draw, then stop. if (!drawData.Valid @@ -196,8 +201,8 @@ internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, using (new DeviceContextStateBackup(this.device.Get()->GetFeatureLevel(), this.deviceContext)) { - Service.Get().RenderDrawData(this.rtv.Get(), drawData); - Service.Get().MakeStraight(this.uav.Get()); + Service.Get().RenderDrawData(this.rtvPremultiplied.Get(), drawData); + Service.Get().MakeStraight(this.srvPremultiplied.Get(), this.rtv.Get()); } } @@ -217,7 +222,9 @@ internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, this.tex.Reset(); this.srv.Reset(); this.rtv.Reset(); - this.uav.Reset(); + this.texPremultiplied.Reset(); + this.srvPremultiplied.Reset(); + this.rtvPremultiplied.Reset(); this.width = newWidth; this.Height = newHeight; this.srv = new((ID3D11ShaderResourceView*)this.emptyTexture.ImGuiHandle); @@ -231,7 +238,9 @@ internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, using var tmptex = default(ComPtr); using var tmpsrv = default(ComPtr); using var tmprtv = default(ComPtr); - using var tmpuav = default(ComPtr); + using var tmptexPremultiplied = default(ComPtr); + using var tmpsrvPremultiplied = default(ComPtr); + using var tmprtvPremultiplied = default(ComPtr); var tmpTexDesc = new D3D11_TEXTURE2D_DESC { @@ -243,8 +252,7 @@ internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, SampleDesc = new(1, 0), Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, BindFlags = (uint)(D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE | - D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET | - D3D11_BIND_FLAG.D3D11_BIND_UNORDERED_ACCESS), + D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET), CPUAccessFlags = 0u, MiscFlags = 0u, }; @@ -263,15 +271,27 @@ internal sealed unsafe partial class DrawListTextureWrap : IDrawListTextureWrap, if (hr.FAILED) return hr; - var uavDesc = new D3D11_UNORDERED_ACCESS_VIEW_DESC(tmptex, D3D11_UAV_DIMENSION.D3D11_UAV_DIMENSION_TEXTURE2D); - hr = this.device.Get()->CreateUnorderedAccessView(tmpres, &uavDesc, tmpuav.GetAddressOf()); + hr = this.device.Get()->CreateTexture2D(&tmpTexDesc, null, tmptexPremultiplied.GetAddressOf()); + if (hr.FAILED) + return hr; + + tmpres = (ID3D11Resource*)tmptexPremultiplied.Get(); + srvDesc = new(tmptexPremultiplied, D3D_SRV_DIMENSION.D3D11_SRV_DIMENSION_TEXTURE2D); + hr = this.device.Get()->CreateShaderResourceView(tmpres, &srvDesc, tmpsrvPremultiplied.GetAddressOf()); + if (hr.FAILED) + return hr; + + rtvDesc = new(tmptexPremultiplied, D3D11_RTV_DIMENSION.D3D11_RTV_DIMENSION_TEXTURE2D); + hr = this.device.Get()->CreateRenderTargetView(tmpres, &rtvDesc, tmprtvPremultiplied.GetAddressOf()); if (hr.FAILED) return hr; tmptex.Swap(ref this.tex); tmpsrv.Swap(ref this.srv); tmprtv.Swap(ref this.rtv); - tmpuav.Swap(ref this.uav); + tmptexPremultiplied.Swap(ref this.texPremultiplied); + tmpsrvPremultiplied.Swap(ref this.srvPremultiplied); + tmprtvPremultiplied.Swap(ref this.rtvPremultiplied); this.width = newWidth; this.height = newHeight; this.format = newFormat; diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl index 171d3e73b..b1cdf2fd0 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.hlsl @@ -1,4 +1,4 @@ -#include "DrawListTexture.Renderer.Common.hlsl" +#include "Renderer.Common.hlsl" struct ImDrawVert { float2 position : POSITION; @@ -18,7 +18,6 @@ struct PsData { Texture2D s_texture : register(t0); SamplerState s_sampler : register(s0); -RWTexture2D s_output : register(u1); VsData vs_main(const ImDrawVert idv) { VsData result; @@ -34,7 +33,7 @@ float4 ps_main(const VsData vd) : SV_TARGET { /* -fxc /Zi /T vs_5_0 /E vs_main /Fo DrawListTexture.Renderer.DrawToPremul.vs.bin DrawListTexture.Renderer.DrawToPremul.hlsl -fxc /Zi /T ps_5_0 /E ps_main /Fo DrawListTexture.Renderer.DrawToPremul.ps.bin DrawListTexture.Renderer.DrawToPremul.hlsl +fxc /Zi /T vs_5_0 /E vs_main /Fo Renderer.DrawToPremul.vs.bin Renderer.DrawToPremul.hlsl +fxc /Zi /T ps_5_0 /E ps_main /Fo Renderer.DrawToPremul.ps.bin Renderer.DrawToPremul.hlsl */ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.DrawToPremul.ps.bin index e3c68edf33f9766c259aa99b4ca284228754bec2..80c297ce6f1d8c1fbe3b09e71ddb27012a59a973 100644 GIT binary patch literal 4759 zcmcgwOK%%h6uzZT3~p0i>Y^%Fr3lY8!W-sj$#WKvqb zHuL!X@BjMY!RcS#T6pX(PdCd4lu}>6q|_0tm%xVrY=VCT*Pm1B4{)_qo_zwH5Fqs~ zH;11+I{>>cAy!KlE)Y8f*5DlY9QZ{r>0|K0P#XLJcSZgT%(#cahrrK3SVCPEtH0t~ z?XF{ckOF%%=y|rf(Q10G@2&?rR~^^a7itT-cF$U~Jw4;LyDb>&%lYEl`O6oJ`8Ny2 zqT1fxp0CZ_bP$iV90Mt&=B!Z55X1?D{N>y!FkaGfdjKQ25ER@WN81<{wxpEza)~=VAzk?C$<19ZorViX;%DZFO zDYbkGW9mloty@a1j;Vviv9T}*DkxXtSR-lKQw2INJi4 z^F0UV9pk%=E14wqaW7{f?<%7kI1BORyNI-#jnuK54eu9eHyi3m3A@lI`9t(cY*^k; z8_6v*kh2h+d%QPd8dcjUJa&YzpEi5RyPx&!C70AZ>MVHQ@rn!Q$asg11MfIral}ac z+kiw&-dV(-57Bg-&r6y;&p!=k+%%l;&NQ60rs1qL4QHQeIN$eaID2~moa~pjq%@rU zrs3>24QE`LD`!c65@ps(5}Z29lAki;k|@hL2rl;QizGOGD6<}t;L;;8))0Q@OrWWOtosWsKQLz0HmY_9A5XTIr?T!YdUm_#aOdzf|lFS(^@{1 z-Wf3H^`Vkar$SOUrJJtfdQgWSRMb5H$2ph$k`-73`KFTj65nRYutojTP5G1yk(1i(oVVqU+JdHnQcSX~z z*B?|wQ;2k-9INrs-ZAdL9`FRi%YnlhXhu3*8%O`~N3rze*OOu-G7gq&77aiW}2(L^>zGPqwZN9f8F)k79Xi=5tz7BtA+RqL=)be($NW< y!!UE-SP92-1Gl5qby~I;oy(l#u3C;Cox}9BTou{(`XWW%|MlN6W*{mWk@_2|BFY*7 literal 16620 zcmeHOU2Igx6+U;3gIR1?6Z2C*$v|ji6K8krxC!A;V6P2U4L05dT%H51 z?-~fDL5&(kX;rmq9xA1(ckoO9>QoS8XuX69~YsIzPM+8_65Z@>4?kKQ}~=9iy)zg82G>jy+0 zLAne0Utkf$%fQ1ik-q{5Gu=Hu2R#6gWG^3Tr>8Z@eHYomOj{din}7<~4BP>%2NJ#p ztZ{R*fAG2xUj?$Pg z7B6H^j12T=dvm@0Mepy>qAZ>_DbdP|2CG(Ut0Y>M3<3+oPUKrqd1a&6z91R(EmZL7j6=1w*>Q1{Vn|8q|m?gDa1-rXF zVSZ%_oM5|`4zW5^-Yl9Hxb7Yhtc!c9+D_OR-OMSlTggy~?dr2B8<aw6dlhtUOrjvsC{a(Qf!_WnJO4( zTfJ`V_zgDBl~*+R12)o6{y^lL(>1d7(U=_Ep~>~1YSK~CA^M?T^pME6g~v`8`&3A}>F$&++{2P^~5GdX1^NAbd)33D9{D@I#=#eS>rLUH-76~}j`;`2=SIi&n(Wqce5 zz&T4V+2f`)sdG*5^oXG~ZZzEzzYv-c=Q@UoKs2=lG2Wg?I+MZEux$!GiyKJf+ z!=x=@F>}1gE$N9M#3jQq%ie6}1YNX7hl1-xYi)iepqKeq`pDDI1UekAOV-nWMn}Ho zFKO*9O#R$%r@#88mp{<%c2m#0A>Kpsz2Ll%cbL4FW7&V3KK1<%amYme1t580coB?1 zHgoXSn|Uz*4Wul25!_n~u~CBF z`YM?k^F^6Zal=m->d`T)aSKEnOyewwKt7Rk;Q)}ZLAIUTYo475xM0)J6IlsY`HPY(%~m=^EMS@@iso9wW;w zqt2J*d#l{M#9TR)hvToR-0F~Ay)VZ)Ema8jzt=Y$_p|$dskSYu_y4y*$K7n?SEIae zaCS9__(1=VGtUQR8R)GH2vPc=S|IaGCVMzDSY1!&tEZY-dgo(Dv?Xi-e($k)RqDnj zv>|~-`*TP+eFJ>n&m)N-umINmSr7z_vxtlFu=t7@6LYLYuskfVK|b&9zb~OV+RU~b zard#tIL7%(|BEpCiR#S@WXW#;{}GqZZ^pgXJFL8I5g1obc64{nI|WLhs*-E`~LGujG0tlp>(FVG;T_&q)!^Qq--mn zI&JXpW1~egf5sz><6mTU>i%8gy0u!|N`&uLc@Dzw95~4){1NnHq^p1ouY!XR%54Yo zB$e{KbSK=uuW4C31$-S<&j44$UGl7#prd;9*}T8YBW*=`04ag@5W67D^KrpuwcQmQpJuf5(^MZHY{0Gf@RtC2T;E6&YasDcib8fs5#Mi&V2LDnKS2{ zaqR2-xf3UUfA6zH`}X%93GRIV^PBe$)OwUsU++fFZi`a$ z`H9KTfs-4du|I;Qw*Gmq2ZK4Z`TYL<#9lm^bR z&|iu4!2F;-HB*?;g$w0nFVH9b`dYOH<#0ClR`&40T=u|dE=Rl5g{dqoorq*BkJ{}Ofa4Ejsk%~@`v zfch{#hw$CN#BEMOKDF%{f_4o$*KWf;$ZITozv9{Dmdafw#{C&~e+2%=Cb{4}kh7)l#a_F>&N*|j zuM3Xug7aDz9Qr{aReHPZvF|$N0v{0+KHDU+(auH3H=56#a>38;@;NDeCKt|o;=*~C zTsZBzaNb21&bn~n{0!;BpXh+|dy0#XkDpFF(*b{~1CEccPCNVz>)IC|bDcQ3T$7GJ z_oTRR#@~fA{w|z$T{z?K!Wn-T&iK1<#-C?P`_EWXTsZxA;q>2y)2<7r|1O;VyKwsN z!aMoT`f>4DKk~mmeOIW<{SusKrEb5<#5yqw@n?;i;KJv3911y%3x)b_qb9iUpKrst zhWr=WaLEy2v1=(YpD|72VxO@{<6@t8AdQQCt`nR(h5j%%XMLu1!b6ZQqY}1Y>+CVpnj3npeRXCyH>zH-nm2!5BY^aK(l}&8uL{Cxv73 z1t%s#oUviOQiLnGLCvdRtOe?6X&-ZMsgE?zrLzv0XYSoDtc|qLF8yUZq;42%#xNZl z+GNd;FZWhzm}li)Tt_?S$4gVy=&T=n6qX8p;CXAF-&if>%e8X7xt#RVjmQgD$`!9v z^e#oszzan=8b%U-C3wb!eS zO0Bu<>HdU+N6vM>5zqGbW%@E<6f`T5p00E8f)_;klfI0G#Lu{Mdd&~3d{nQ;wLHju zXC0y0!jwEs9!<}U>G-)hrsHSKoV$#c?70x;%TamVeq-tVQqPXlW^?+VZ#n1MvV)g4 z#JRi<7bBAu93}8p5q>Wih(-tDRiRv8!vp+6RE|9GLNiSB!kE{Wv8Qc?i}iA~F;MXv z;kvL_ms^7p3(G|r;5P?*b~DH(`8cYPMj z(weRH6DA5~F4mK2)MLXg2X>hmG(UjowRMM)6D|+p{77E$`25sFQO*>2v7IEASrfW} zBxu9BwG6KY^$-M9eDBko^MUR}X>XD*G6b~+r>X8X*jHe!%^mS6?>n=ho%r1f8 zp5!gTaqe5jk*sBVxQAF?d$i@g%+N?*rm__O(4}3!ex++=X*4MT`GrcG4BKSbxIgfV zl*!9f&0i|l!WqAU*ZVhmd|ZkqjWt{8Q5m(md%keSyO)j?m=-U literal 16972 zcmeHOU2GKB6+UAdSk?yC#QYR2I{Xx^iMzXY+!TR9V6P2U4L05te{$un*JHC`y|bB} zHBLwr)T$DSq9|2)Xw{_Utx<`Twoj-PYM%Pg=7CCmXd@I=t<*j=QiM~reW-B1Z)Wa# zY!(-yV!+I?&YpYEJ@=k-&z*bb{*05SySjf`f9ah=|NfP^{hjxB{_g$ONlirhyF^xk z?gd^4BU&T!Ht^kAkz}37P_n0Y8hpyyK+;a2thh6)Au|XZO18C;w;8AdqrhFj1|VVB zC3pR@En?cr6iVZ!HJPz3o@QJGj_*xht%S)4uAK8zo1}ZBx~kP>sUE&=Zg6p40fy2 zZVmOdMe6T>9mjH6%Pg7WwjLd7(U110`t{Vg%$Q;6-R5L5mq(&45o?pa)RE)opbx_# zA)wz692L`(9S@rFWonAT+%(Ky9Wk_lF~iUKQJT%(3fXj zpkJTUp4S$t&l^+9I^`H}!;>K)-_Zw}IyrJLmDf^7drsG(fkCu#7_u*fL_>e-df(KM zLjpL0=Gr6fNUAh-GCc!6^-Mr?Mt5ac23u;|kymM!Q4apBArH@dXvTXwPHu4PDV;2D z7YVC!#RpqfdUo`#b?bl?QhF6n$72c!m*T>>NLp55+v_p{IH$5OreS7V99n(&rnii)6+I==|K3M4eU zB=}th9{7NPGk~Yo1Bjb{dTKi#bbNLNVweu>^kNxZ!X)2QEDK8PjdRNT@ zS7L42@z-m{xeh(`+os)#P`vBX>A$}ClUvu5N}gp4*QvKoJhzag!~^;Sd7x|=u_2>? z&8lI=uw9)r3$b(gQhrA`T(Yfl*4FzblUC-!NyD=Bi(wt$Y>wOYqFKsukFW33)oyUV zzq~w!615{u_=-AoXS`G=_4Kvau75sdgH@DOvn_2* z+i+BSQZJ3zZW`8G#zd}U8&=d#wCoS3GLuE@^iy`mHXv4VvwpD>#fDB*8tZgV84g0) zEZfDh?H4Y$K{)JrW2!VVnaLHRS+h`@Hz2vOsTR*yo~UJ%aE#&l#!bg619d>3Ea#){ z2}!Fvj2-1~Y4Yo{B4$+N=7%ERo~@PUXKLiFtR}m^C-TQVHM09W_TxX&q_0Vn@87^1 zqq0?gf*$n1)T$oXnJZ-T^!I`a0`38wQst?6d^dc$MNV3kB7hdW$_V)3up9jJ;=ItN;@wyaVj*IE`tZ<^ zp0pR?EW>G|5`trH$cZY@L{A^B-r*%ZruZ$%0cn?#I zt9rcG)bIKDvmk8_S_Xn=Y^oF+MTPql=6lqy7=85=d%b=D#l;6!9NrTTSDo_nnAuV1 zh+eODZts5|jV4;c7kx8gxdEm~ccRuApemgh(XZH;s z$=Rn&>wIZAWm<+&G|a-pa59t6OqRzgw6B0w&v+(l45x~jLLoDnH%i0qA~FYYr?`;G z58wYR5qWvv@I=nO;HWOmsUGuGE1IjQF3qX7ob@YJJH=EzhDn>p%gkG?#at0AaamxS zMRzuHf-YI7eT(ZQ>uq*!K`+(c^g*PbTj=n4ePSd1=M3aq|D4d?!_+^c{pZCquYIIF z=%$``L%h>tJ#}9A3ewn42;2VK?Ah;rj4cDpF9KQShZj(wWRr*Q{2+_Me*!7XvH%{g z2RL?J(%H!Sbep=DF+rhPd9y!=p2mWfo;*)}lz(yHt)SRcX z&TGeQUGe!{fZP5R=NVS~kdF^lqWBFyev^+M@$q#Zzsbkn<>PPm@%ikl_M3hDR`3s^ zuOS$4Svu89)msBkW%6YMx3`M}+pR(W{rYrNQGCBXWgq2rpB>6R@^|_8v`@9;@4xaF z<^BCv{vw|dL-`$Imw z>wnXx?PZOK$QmHO0jkGWHEeqpXF-Gx52>e<48TU&y5SHgr)|jnUZ!0>o4Kv{tRGR| z(SZNF)UIE%L7g3`BPqG9cp)iIu|D$}?1+f7f0G@ug0-XuJO#SUok_AzST=8{MLEX} zk!wANjxj>%WP2Dv@4RwOXJmoS&7RI}p3ap%ol$Z3rb^!`Pv2SzML0({LH7#KZPM)c zh5dJZfW#rs5%?a7wuZgBOxHVU$Bi^{$-zJV>Rhj3!T2w^QGvoG^8mh)yZ?=RJw)o_r7Rdyg%vo^o7fO~m>PFsFZwO3pR2eAA+uu;C`?7uIeYuenlT(-!G#s)kI?%sk? zVs_U1wto3J;VJG>)yeMpgJaHj!&B^@W#4s<@rBiG&bjYeX?k;=G@a*vH4LKXIFxd#hKtAo9QSul{5FaIKTd-3*a zylsVsU8?)7D)oju9g0V)IojNUm(lj4yWUzV&Hg(k5p}oc_)|HRPNs8QSmR5xB!H*P z1B-qCxf{<+ygyes-&YtnC0@u(8YPLBtZe+8!GCWX&70ZtE@K@3&N*#nyZU>O7D$4WHo$%6{0H zmw|cjf%5(?3wje&o&QFWZ-p%14?G`cK;Hu7dV=TBd`GZt9vQKh+&`=bt^u<5^b9?) r7RdiWq=%jXHUT#PHv##s3E<&+VC%zeK4?AgK;VJE1Azw~QxE(Xn~Wqp diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl index b8423697a..bce235d7f 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.hlsl @@ -1,22 +1,19 @@ -RWTexture2D s_output : register(u1); +Texture2D s_texture : register(t0); float4 vs_main(const float2 position : POSITION) : SV_POSITION { return float4(position, 0, 1); } float4 ps_main(const float4 position : SV_POSITION) : SV_TARGET { - const float4 src = s_output[position.xy]; - s_output[position.xy] = - src.a > 0 + const float4 src = s_texture[position.xy]; + return src.a > 0 ? float4(src.rgb / src.a, src.a) : float4(0, 0, 0, 0); - - return float4(0, 0, 0, 0); // unused } /* -fxc /Zi /T vs_5_0 /E vs_main /Fo DrawListTexture.Renderer.MakeStraight.vs.bin DrawListTexture.Renderer.MakeStraight.hlsl -fxc /Zi /T ps_5_0 /E ps_main /Fo DrawListTexture.Renderer.MakeStraight.ps.bin DrawListTexture.Renderer.MakeStraight.hlsl +fxc /Zi /T vs_5_0 /E vs_main /Fo Renderer.MakeStraight.vs.bin Renderer.MakeStraight.hlsl +fxc /Zi /T ps_5_0 /E ps_main /Fo Renderer.MakeStraight.ps.bin Renderer.MakeStraight.hlsl */ diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.ps.bin index 0b979f6b61f10856c82f9938e9bcf7c579d55b11..2892c7361ce6d9d0bf5dc81c9ad404ca6917386b 100644 GIT binary patch literal 5372 zcmdT|&u<%55Pk`56HMKf3PcY*yaR`%3btcXXakhAaehf@Q(}ik#VFc1>%`RYTD#j4 zglKbwdxelVa6#e_Bu+>ixX{0VLl1E0hLBJtda7i;Z}-i1lk6HJB$SLYo_RChn>RD> z?T>6}`O4(_?8AQd*ROx?yz}{2^)J7v_bH{Gyr9%E$P3_Kz{g>n0AJ`=>LGZsG&TJN z>_VLLCI?45ojwfeP!~(1qqLm?Yw!^GW$#@d72n?SZ@ad- z+NimX=hS^Yv^cD1=gafDykV``uAX!@+l?lai@D z_1OcZwkMZfRF7ot2@~UPC`XkVI1L}y?VAV#j2r9;_*|LSAL1XM3?=XblH;_AdCbJQ zgiz(m<;s188vrwZ5~b+DdV#g~pZMI~XYEIpOIK#}X{9P~{fMA{fd2yXTrw{b^Jb+y zXTN2_{XL6=$+NkJAD+nyoRmL6-bb^329xd@9yJML?}E5I!=(5*rY;os!FCT@hx5LD zPkKdMuNdOeNaCdVX(xsxdN=Gr^F3=4;x)3-xyg> zWNKmu%#pSi>^1IB`}TtAbDv}{WWK3;8#1~s1dGcvpl+Dwdcg47CUuKLA1Q_l<{X9} z>7`s3>@lCY9OF3zH^YeVfd~}O2W|2Jn18GERyp`r zv&wo%tc}F$fY{L5=};#n*?CTr?7Y7v*%^yxAReDsQj(qVlkA+6WM^E~6pt_W-y}QZ zC)pW4$gBV^be5QX2CP@uk|J%S4=E5=gC6fSkz87;J ze*%-JKM!V%wJtb`6_1xRHz_~CVTp)%#>~G>k&QJ;?`fpY+_|PnEs)=#aqLf=zK4xC z*1>ti3ywPfr;Ep9@9`-kBAG)ey%uMDZR)~FSg|p_;3&l|F)3-2GtaT$)36e!7c6_p zUXVDC`7mz0KNv>;m&bj?-W;#aH2fQmd&{eq9oM$ow$oa#maL|=d3!CAb1mO?>sHOK zR>Ds*o+vk5tL;?-f6HoCORn|de8ck%jNa;^-NL6D+s$3IZrNquwHoUier}`bH4Um* z($TjbJ+57Q9T9AI$S(}PkL_WBQJ_vS~G1Lp7)6`8qHbb=&b8e#2>Do_xp| zh93K_qd(4OG|08_EvXgG8yk{&Bf2o6N4d@?v8CGkml}&N6$@`x3%>}-TCo(zwCC3J zTTd%|PnOPYee^!ojpYVp&eE53A+*v%dRvmF-(6qTdB)6HBcT}HjUE>?3NwFEdHHie zky|yLzuD0F3VV8frJ(au(p{aOc69QN-SKj(jaG`2+life8W}A#kxetZL~j3c^`&YnHA*9)G=1qy)CZIY9$F$*X%$6nAENZ7Qlui2D0zu+zi(!) zJtnM08i^l!4xByro^$Ux=bn3K=FYj}?8$@u4e8fT6t*|dz4`nrv3LIc+!aGa>c1(n z9<&GecVHu!J;33Z$ajIC07tR|gD*g)f&JKC)-5a;s8}eDWHTA^I)Dn;3ET;61`!95+z50}m#dYTYDJC~$DFcTo~WqKkzMNWk=zlLn>5EQNA;JdXNn~hGU;SS zE?v4blsoi9SD>R2Q~?RpS?85RHldI^aeDYzZYV!=>c{>*DhU z+kN_5k!Rl!nd{Hi3uW~E%BG;re|`NKOwne@Fco6-;Rnziv~7XR3Ce>pzyC+*Pd&i( zTIXPk&ua+uyhK^xKV<~n=IdAn+V@x=CCi}~s&j)wX~tZ~&JY!e1u*EB4<-7@>;hO-9+gIZcz{i*(k9#ThhgX$Z}bb4caTQ*09aF+gV2mYLO z0M~N_t0MxJdDDqt%|>9K$c;5S_-Nn@>u?)WKyU8?%8-Ca1CX)^R(AySzGQWeG8$d2 z2wY5NjN?CxO>j^6-sKaGfBp4OkNv7)^U#+sy#5Q_hcaRq{&oD3n^|FTRK8ClkbNYZ zJDeR6zRT4Dh1cF%vWS0wN8rjSkpnvod7xM)*Ge(@%e!^*t(lm-`W=y`dA#}gufg~C zAS3Top}q@IZ@fMda`9a!ts-MUn|OVP*=7tPFk z+RWatnULzhlAojs?1a4At6XPH?Oz<(x3$&e?D?l3^Ssi7UM5oeJsG`*f~2W>ReF&_ z?&EqG6g!18D&;rq^2x4cjk|SYj5PSqpiRRKBOlmixGdreIr-~|- z=R`buI;~OzdP-Dku&lCMyS%ntNi~EREjY3gn^^`F{6k zp;$TRnRb^gOw;37{3R^@ihpxU`c1I-r?L25z4$%TYx@qk z{k^m?hpB)2t^Jj!e)JDxg>4{OeHz%F`C(Gs_Xj9<0;&7`j~^|(h6a>91Eeg1dnW=s zcRDCNiB@j{$8!QLr~z*%fb$e%7yEX3^G=ectw;K9WygG`|gU&8IJ#-x1=|7oVT! zIUnv54e}u<+iNgx6~ew3BWHA4jr~ zb$I9l_^}KzRPW ziyitl+%*56(?rz$9*zLdd$u*D=5a~kO7yNPUR&}-AZY|EM1VJ}C140wEpV?B!Rm{^ z7Wt~T|DMCpjHPWkb}Yve>#?GDcqhWVOBhSvqpiL$tl1H*bkow$hBX_UH860_@wujq zm-pEyZO^wz+teo935GFt1?&P|0BYER0`~@C(E9|wtErpL3S)dz4|eL5vu*m1U-Mrq zP}VQ@*z@D_P?3jUPtLOq;nwK3)=O5cC{4xLg*iuTly zJyDjFZBARRq>x8SOx?nnp zZ3z0)wl)753!ncASck~{3G_u!FCT>R+qg^5fii!80cD;0A?*T>&jz;T&798#=EvHA zn}7^BdmsY; E14oeP;{X5v diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.MakeStraight.vs.bin index 1baeecdaef844bc9a92a43d68fae190205dc126d..1bbe7e592793fcd9f271e3ba408563cd7629532f 100644 GIT binary patch literal 3094 zcmcguOK;Rx6u#lnrlXFEM1@$exC?|PDDq6E;T0(1G06}q5Hub&luk8u24`ZJ*dyCO z5NeZU#kz}1Y*|zd5?vJb@LMW%Q>iH3H+-M%GdQVTl1)C%nS1WJ-+A0~uPxVZUAg+~ z?cwuZKiPQt>xs874?p>5en=^Gd>%Tj!hT5Q44;{ixsjgB7Hgv2r&S~ttn#`vY4s64365GB@AZ@|g-UMjpoH~(EqdClLFs(+v&DB+M3F#&I zm}YX4wTPehw^NSrWZWJv&z|P(B}ek&SR$~ee3k_4ch*_3MuL3yn7a7njXUAs+I$dk zt!tAw&J2m&nTy~|#_fUg>}lR!awKowS+Li5PYjS(Tb_O32xTuhJITA3_3R{<)SPz~ zyz_GJ8Bg9tS#~}{yw_>Iyw|eqd3@%`;xn!wy*KKTN9>%7PQPOiKJTv~1lJ1?yz14D z5Q4mq6M0SpkmZ}jW4)9#USK1PC$({%amB`YdW|}5Qa98HwA04EP#4MK(k5pFb-<-< zd)zcnV$#o&n)qLyWtP_L@jjOGO?c$@y_KoY-J=+X@%jqtK#!@fnC9G;q2kkHptS7W@ zcM-w%OJQZ?e6dh0EcQGr9M=zm_Oj);rH9)|MK-t|0!jg8{ooH*V_q`WCb*S_EWh}Ho)umQLoSOFwBstuWjWenTAA<@W>^d@_G zazx3fChs)H#7X2~O6rv^~g2T7#!xgeAJ!QLDWBga^lvC=!-O;bNFVYJO&zT4wLS@4Ee z7B~o|c`$XL9m7bbI#QQWF7zN4m~hgT%Yk_En8qOW7BJS{A+#e>#&S*pz}=!C3}E4 zdjhKS;mNi@;ojuQ9;rXN`&r;}Bqp7BJ1|W9(EcwoXUhM*a`1!iuGQ;*y?0meQ{BRS z>Tf4@Ei4l3K3zo?i0_IgkHiyfpoq?#6|FAP(yvihaVqy-WuQ|hFhH`V5NTV6NJF+Q9#6uYIJTm44f9?#pnm78i-vuxFD z$j2J@g+eX|pRoGJ6isg!GIYy!n>F)s!_sWc(9uf7TN;rr2*2=)Ar3HANYy=Q4Wd~S zk2Y}<^7^cp@&AiyDHK!CwI?ankvQCy^5hg~*TOAxi0_{r*%=`p<vwEZiB~7<$N!u&70pyLe&v+Q_aXxWmHYtrlMs> z?8t-_8PxPeGLPh}T%ll0jVIWfq$LO*U$WrP4mtFY-tISFXo2Zl)3<0N_c0v=PPe8H zYxYUQ9JSI(!&KFA)zGu)xRO)G@|juMqhl=?R)*Aca$M1MWiY2&Y1h3pushYdl1nen z1Ieivd$l#4)$H?*XlqV%#uF{5HY?hi6Ky)4O`@G@5}kqb=ivOdySPPM7C8SHoZk}8 z?}%R8E8y1OqtX#zqUKa$u-1^n$*63WY%_;|bg5Bgin`l=*G zk*rd$FIR#nFD)o1TeP$Gs#5fSiM4q$#aDSk$>mjC>l6p-H}C*?RK8H2;13q)L3dxG zyrQ6-?$ZK@%-sX61Cq(Y(!HC%*8TYH^}W}AsIEB>`{o8?cThe6B#`iLz)86=y%f@9 zQ;c=WqfYFP@)QFk8>GE@k3#GE`{LgoKZw(gs1K{l0_9)FBZM+z~l!r zI13VxuMv`8av(u1+gfuFl-B+rb4~=!?Czc0Ay!vIXGiiotRkD7Wd(V`PyL2n{uaSs z;_+9qV3>Y;o6$A^>cgYAK;it0Gk^Jl`G53U_!5@O0(9TArA)p}zwMy6PxC&-%cauF zm+P-hD}RoB4HspBTK2rN|DFQ3G`B5x+KRPdQ62eAtI`6+mNI|-SLKv0Noktn^JOt7 z?z4eazgx+wM>k?6=tD0YNbfN(0R_||@r=lLRO}Z9kmqwpPp3A3llm7P1HFZGlAlhv z2MgrI!Cp6Xl^VSGRpfT@{sQbNVnk1*JEaDIw09v8Wkl%=;z?Z2&?XZxR%cvda6(y9 zbY`Z)QQS>a&0_=ay2if&S$wd1nQuT`?{}lA!c8a=Lykq!RXSlW9*VvH_ypa^y_!DS zqYoQQ)|D~UVlwWycoSP{t2t}6ez{-`+yv2f(9T*p1utf7ox?Sz6+JW zY9PJ0Cu7$D%YkJ4G9bk-Hvnl;Xd*~9P14iAO+X(WzXdiwev!UJ-vYh`d<*y%SOFIJ E4`unISO5S3 diff --git a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs index cc6cfd000..1243c8754 100644 --- a/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs +++ b/Dalamud/Interface/Textures/TextureWraps/Internal/DrawListTextureWrap/Renderer.cs @@ -259,15 +259,16 @@ internal sealed unsafe partial class DrawListTextureWrap } /// Renders draw data. - /// The pointer to a Texture2D UAV to make straight. - public void MakeStraight(ID3D11UnorderedAccessView* puav) + /// The pointer to a Texture2D SRV to read premultiplied data from. + /// The pointer to a Texture2D RTV to write straightened data. + public void MakeStraight(ID3D11ShaderResourceView* psrv, ID3D11RenderTargetView* prtv) { ThreadSafety.AssertMainThread(); D3D11_TEXTURE2D_DESC texDesc; using (var texRes = default(ComPtr)) { - puav->GetResource(texRes.GetAddressOf()); + prtv->GetResource(texRes.GetAddressOf()); using var tex = default(ComPtr); texRes.As(&tex).ThrowOnError(); @@ -292,10 +293,9 @@ internal sealed unsafe partial class DrawListTextureWrap var viewport = new D3D11_VIEWPORT(0, 0, texDesc.Width, texDesc.Height); this.deviceContext.Get()->RSSetViewports(1, &viewport); - this.deviceContext.Get()->OMSetBlendState(null, null, 0xFFFFFFFF); + this.deviceContext.Get()->OMSetBlendState(null, null, 0xffffffff); this.deviceContext.Get()->OMSetDepthStencilState(this.depthStencilState, 0); - var nullrtv = default(ID3D11RenderTargetView*); - this.deviceContext.Get()->OMSetRenderTargetsAndUnorderedAccessViews(1, &nullrtv, null, 1, 1, &puav, null); + this.deviceContext.Get()->OMSetRenderTargets(1, &prtv, null); this.deviceContext.Get()->VSSetShader(this.makeStraightVertexShader.Get(), null, 0); this.deviceContext.Get()->PSSetShader(this.makeStraightPixelShader.Get(), null, 0); @@ -304,6 +304,7 @@ internal sealed unsafe partial class DrawListTextureWrap this.deviceContext.Get()->DSSetShader(null, null, 0); this.deviceContext.Get()->CSSetShader(null, null, 0); + this.deviceContext.Get()->PSSetShaderResources(0, 1, &psrv); this.deviceContext.Get()->DrawIndexed(6, 0, 0); } From e52e0b6df9a0b7282c2d4100dbdeef5db91b3699 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 29 May 2025 19:35:38 +0200 Subject: [PATCH 085/106] Scrollable Self-Test table (#2284) --- .../Windows/SelfTest/SelfTestWindow.cs | 186 ++++++++++-------- 1 file changed, 108 insertions(+), 78 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index b6f08edf6..da2aaff2d 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Numerics; @@ -9,7 +10,9 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; + using ImGuiNET; + using Lumina.Excel.Sheets; namespace Dalamud.Interface.Internal.Windows.SelfTest; @@ -61,7 +64,7 @@ internal class SelfTestWindow : Window private bool selfTestRunning = false; private int currentStep = 0; - + private int scrollToStep = -1; private DateTimeOffset lastTestStart; /// @@ -90,9 +93,10 @@ internal class SelfTestWindow : Window if (ImGuiComponents.IconButton(FontAwesomeIcon.StepForward)) { - this.testIndexToResult.Add(this.currentStep, (SelfTestStepResult.NotRan, null)); + this.testIndexToResult[this.currentStep] = (SelfTestStepResult.NotRan, null); this.steps[this.currentStep].CleanUp(); this.currentStep++; + this.scrollToStep = this.currentStep; this.lastTestStart = DateTimeOffset.Now; if (this.currentStep >= this.steps.Count) @@ -107,6 +111,7 @@ internal class SelfTestWindow : Window { this.selfTestRunning = true; this.currentStep = 0; + this.scrollToStep = this.currentStep; this.testIndexToResult.Clear(); this.lastTestStart = DateTimeOffset.Now; } @@ -116,11 +121,11 @@ internal class SelfTestWindow : Window ImGui.TextUnformatted($"Step: {this.currentStep} / {this.steps.Count}"); - ImGuiHelpers.ScaledDummy(10); + ImGui.Spacing(); this.DrawResultTable(); - ImGuiHelpers.ScaledDummy(10); + ImGui.Spacing(); if (this.currentStep >= this.steps.Count) { @@ -131,11 +136,11 @@ internal class SelfTestWindow : Window if (this.testIndexToResult.Any(x => x.Value.Result == SelfTestStepResult.Fail)) { - ImGui.TextColored(ImGuiColors.DalamudRed, "One or more checks failed!"); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, "One or more checks failed!"); } else { - ImGui.TextColored(ImGuiColors.HealerGreen, "All checks passed!"); + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "All checks passed!"); } return; @@ -146,8 +151,6 @@ internal class SelfTestWindow : Window return; } - ImGui.Separator(); - var step = this.steps[this.currentStep]; ImGui.TextUnformatted($"Current: {step.Name}"); @@ -164,13 +167,12 @@ internal class SelfTestWindow : Window result = SelfTestStepResult.Fail; } - ImGui.Separator(); - if (result != SelfTestStepResult.Waiting) { var duration = DateTimeOffset.Now - this.lastTestStart; - this.testIndexToResult.Add(this.currentStep, (result, duration)); + this.testIndexToResult[this.currentStep] = (result, duration); this.currentStep++; + this.scrollToStep = this.currentStep; this.lastTestStart = DateTimeOffset.Now; } @@ -178,90 +180,111 @@ internal class SelfTestWindow : Window private void DrawResultTable() { - if (ImGui.BeginTable("agingResultTable", 5, ImGuiTableFlags.Borders)) + var tableSize = ImGui.GetContentRegionAvail(); + + if (this.selfTestRunning) + tableSize -= new Vector2(0, 150); + + tableSize.Y = Math.Min(tableSize.Y, ImGui.GetWindowViewport().Size.Y * 0.5f); + + using var table = ImRaii.Table("agingResultTable", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.ScrollY, tableSize); + if (!table) + return; + + ImGui.TableSetupColumn("###index", ImGuiTableColumnFlags.WidthFixed, 12f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Name"); + ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale); + ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale); + + ImGui.TableSetupScrollFreeze(0, 1); + ImGui.TableHeadersRow(); + + for (var i = 0; i < this.steps.Count; i++) { - ImGui.TableSetupColumn("###index", ImGuiTableColumnFlags.WidthFixed, 12f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Name"); - ImGui.TableSetupColumn("Result", ImGuiTableColumnFlags.WidthFixed, 40f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn("Duration", ImGuiTableColumnFlags.WidthFixed, 90f * ImGuiHelpers.GlobalScale); - ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, 30f * ImGuiHelpers.GlobalScale); + var step = this.steps[i]; + ImGui.TableNextRow(); - ImGui.TableHeadersRow(); - - for (var i = 0; i < this.steps.Count; i++) + if (this.selfTestRunning && this.currentStep == i) { - var step = this.steps[i]; - ImGui.TableNextRow(); + ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, ImGui.GetColorU32(ImGuiCol.TableRowBgAlt)); + } - ImGui.TableSetColumnIndex(0); - ImGui.Text(i.ToString()); + ImGui.TableSetColumnIndex(0); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(i.ToString()); - ImGui.TableSetColumnIndex(1); - ImGui.Text(step.Name); + if (this.selfTestRunning && this.scrollToStep == i) + { + ImGui.SetScrollHereY(); + this.scrollToStep = -1; + } - if (this.testIndexToResult.TryGetValue(i, out var result)) + ImGui.TableSetColumnIndex(1); + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(step.Name); + + if (this.testIndexToResult.TryGetValue(i, out var result)) + { + ImGui.TableSetColumnIndex(2); + ImGui.AlignTextToFramePadding(); + + switch (result.Result) { - ImGui.TableSetColumnIndex(2); - ImGui.PushFont(InterfaceManager.MonoFont); + case SelfTestStepResult.Pass: + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.HealerGreen, "PASS"); + break; + case SelfTestStepResult.Fail: + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudRed, "FAIL"); + break; + default: + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR"); + break; + } - switch (result.Result) - { - case SelfTestStepResult.Pass: - ImGui.TextColored(ImGuiColors.HealerGreen, "PASS"); - break; - case SelfTestStepResult.Fail: - ImGui.TextColored(ImGuiColors.DalamudRed, "FAIL"); - break; - default: - ImGui.TextColored(ImGuiColors.DalamudGrey, "NR"); - break; - } - - ImGui.PopFont(); - - ImGui.TableSetColumnIndex(3); - if (result.Duration.HasValue) - { - ImGui.TextUnformatted(result.Duration.Value.ToString("g")); - } + ImGui.TableSetColumnIndex(3); + if (result.Duration.HasValue) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted(this.FormatTimeSpan(result.Duration.Value)); + } + } + else + { + ImGui.TableSetColumnIndex(2); + ImGui.AlignTextToFramePadding(); + if (this.selfTestRunning && this.currentStep == i) + { + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "WAIT"); } else { - ImGui.TableSetColumnIndex(2); - if (this.selfTestRunning && this.currentStep == i) - { - ImGui.TextColored(ImGuiColors.DalamudGrey, "WAIT"); - } - else - { - ImGui.TextColored(ImGuiColors.DalamudGrey, "NR"); - } - - ImGui.TableSetColumnIndex(3); - if (this.selfTestRunning && this.currentStep == i) - { - ImGui.TextUnformatted((DateTimeOffset.Now - this.lastTestStart).ToString("g")); - } + ImGuiHelpers.SafeTextColoredWrapped(ImGuiColors.DalamudGrey, "NR"); } - ImGui.TableSetColumnIndex(4); - using var id = ImRaii.PushId($"selfTest{i}"); - if (ImGuiComponents.IconButton(FontAwesomeIcon.FastForward)) + ImGui.TableSetColumnIndex(3); + ImGui.AlignTextToFramePadding(); + if (this.selfTestRunning && this.currentStep == i) { - this.StopTests(); - this.testIndexToResult.Remove(i); - this.currentStep = i; - this.selfTestRunning = true; - this.lastTestStart = DateTimeOffset.Now; - } - - if (ImGui.IsItemHovered()) - { - ImGui.SetTooltip("Jump to this test"); + ImGui.TextUnformatted(this.FormatTimeSpan(DateTimeOffset.Now - this.lastTestStart)); } } - ImGui.EndTable(); + ImGui.TableSetColumnIndex(4); + using var id = ImRaii.PushId($"selfTest{i}"); + if (ImGuiComponents.IconButton(FontAwesomeIcon.FastForward)) + { + this.StopTests(); + this.testIndexToResult.Remove(i); + this.currentStep = i; + this.selfTestRunning = true; + this.lastTestStart = DateTimeOffset.Now; + } + + if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Jump to this test"); + } } } @@ -281,4 +304,11 @@ internal class SelfTestWindow : Window } } } + + private string FormatTimeSpan(TimeSpan ts) + { + var str = ts.ToString("g", CultureInfo.InvariantCulture); + var commaPos = str.LastIndexOf('.'); + return commaPos > -1 && commaPos + 5 < str.Length ? str[..(commaPos + 5)] : str; + } } From 3f3a1f2be120e2b41d999c319d6e0bfbe35a0a80 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 29 May 2025 19:37:48 +0200 Subject: [PATCH 086/106] Add support for sheet payload links (#2282) * Fix "Print Evaluated" not evaluating with context * Add support for sheet payload links --- .../Game/Text/Evaluator/SeStringEvaluator.cs | 241 +++++++++++++----- .../Data/Widgets/SeStringCreatorWidget.cs | 8 +- .../SheetRedirectResolverSelfTestStep.cs | 4 + 3 files changed, 183 insertions(+), 70 deletions(-) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 2ccb3ff47..85c342e22 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -18,7 +18,6 @@ using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Info; @@ -36,6 +35,8 @@ using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; using AddonSheet = Lumina.Excel.Sheets.Addon; +using PlayerState = FFXIVClientStructs.FFXIV.Client.Game.UI.PlayerState; +using StatusSheet = Lumina.Excel.Sheets.Status; namespace Dalamud.Game.Text.Evaluator; @@ -727,84 +728,186 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator this.TryResolveUInt(in context, enu.Current, out eColParamValue); var resolvedSheetName = this.Evaluate(eSheetNameStr, context.LocalParameters, context.Language).ExtractText(); - - this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue); + var originalRowIdValue = eRowIdValue; + var flags = this.sheetRedirectResolver.Resolve(ref resolvedSheetName, ref eRowIdValue, ref eColIndexValue); if (string.IsNullOrEmpty(resolvedSheetName)) return false; - if (!this.dataManager.Excel.SheetNames.Contains(resolvedSheetName)) + var text = this.FormatSheetValue(context.Language, resolvedSheetName, eRowIdValue, eColIndexValue, eColParamValue); + if (text.IsEmpty) return false; - if (!this.dataManager.GetExcelSheet(context.Language, resolvedSheetName) - .TryGetRow(eRowIdValue, out var row)) - return false; + this.AddSheetRedirectItemDecoration(context, ref text, flags, eRowIdValue); - if (eColIndexValue >= row.Columns.Count) - return false; + if (resolvedSheetName != "DescriptionString") + eColParamValue = originalRowIdValue; - var column = row.Columns[(int)eColIndexValue]; - switch (column.Type) + // Note: The link marker symbol is added by RaptureLogMessage, probably somewhere in it's Update function. + // It is not part of this generated link. + this.CreateSheetLink(context, resolvedSheetName, text, eRowIdValue, eColParamValue); + + return true; + } + + private ReadOnlySeString FormatSheetValue(ClientLanguage language, string sheetName, uint rowId, uint colIndex, uint colParam) + { + if (!this.dataManager.Excel.SheetNames.Contains(sheetName)) + return default; + + if (!this.dataManager.GetExcelSheet(language, sheetName) + .TryGetRow(rowId, out var row)) + return default; + + if (colIndex >= row.Columns.Count) + return default; + + var column = row.Columns[(int)colIndex]; + return column.Type switch { - case ExcelColumnDataType.String: - context.Builder.Append(this.Evaluate(row.ReadString(column.Offset), [eColParamValue], context.Language)); - return true; - case ExcelColumnDataType.Bool: - context.Builder.Append((row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Int8: - context.Builder.Append(row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.UInt8: - context.Builder.Append(row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Int16: - context.Builder.Append(row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.UInt16: - context.Builder.Append(row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Int32: - context.Builder.Append(row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.UInt32: - context.Builder.Append(row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Float32: - context.Builder.Append(row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.Int64: - context.Builder.Append(row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.UInt64: - context.Builder.Append(row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool0: - context.Builder.Append((row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool1: - context.Builder.Append((row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool2: - context.Builder.Append((row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool3: - context.Builder.Append((row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool4: - context.Builder.Append((row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool5: - context.Builder.Append((row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool6: - context.Builder.Append((row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; - case ExcelColumnDataType.PackedBool7: - context.Builder.Append((row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture)); - return true; + ExcelColumnDataType.String => this.Evaluate(row.ReadString(column.Offset), [colParam], language), + ExcelColumnDataType.Bool => (row.ReadBool(column.Offset) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Int8 => row.ReadInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.UInt8 => row.ReadUInt8(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Int16 => row.ReadInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.UInt16 => row.ReadUInt16(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Int32 => row.ReadInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.UInt32 => row.ReadUInt32(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Float32 => row.ReadFloat32(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.Int64 => row.ReadInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.UInt64 => row.ReadUInt64(column.Offset).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool0 => (row.ReadPackedBool(column.Offset, 0) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool1 => (row.ReadPackedBool(column.Offset, 1) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool2 => (row.ReadPackedBool(column.Offset, 2) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool3 => (row.ReadPackedBool(column.Offset, 3) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool4 => (row.ReadPackedBool(column.Offset, 4) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool5 => (row.ReadPackedBool(column.Offset, 5) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool6 => (row.ReadPackedBool(column.Offset, 6) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + ExcelColumnDataType.PackedBool7 => (row.ReadPackedBool(column.Offset, 7) ? 1u : 0).ToString("D", CultureInfo.InvariantCulture), + _ => default, + }; + } + + private void AddSheetRedirectItemDecoration(in SeStringContext context, ref ReadOnlySeString text, SheetRedirectFlags flags, uint eRowIdValue) + { + if (!flags.HasFlag(SheetRedirectFlags.Item)) + return; + + var rarity = 1u; + var skipLink = false; + + if (flags.HasFlag(SheetRedirectFlags.EventItem)) + { + rarity = 8; + skipLink = true; + } + + var itemId = eRowIdValue; + + if (this.dataManager.GetExcelSheet(context.Language).TryGetRow(itemId, out var itemRow)) + { + rarity = itemRow.Rarity; + if (rarity == 0) + rarity = 1; + + if (itemRow.FilterGroup is 38 or 50) + skipLink = true; + } + + if (flags.HasFlag(SheetRedirectFlags.Collectible)) + { + itemId += 500000; + } + else if (flags.HasFlag(SheetRedirectFlags.HighQuality)) + { + itemId += 1000000; + } + + var sb = SeStringBuilder.SharedPool.Get(); + + sb.Append(this.EvaluateFromAddon(6, [rarity], context.Language)); + + if (!skipLink) + sb.PushLink(LinkMacroPayloadType.Item, itemId, rarity, 0u); // arg3 = some LogMessage flag based on LogKind RowId? => "89 5C 24 20 E8 ?? ?? ?? ?? 48 8B 1F" + + // there is code here for handling noun link markers (//), but i don't know why + + sb.Append(text); + + if (flags.HasFlag(SheetRedirectFlags.HighQuality) + && this.dataManager.GetExcelSheet(context.Language).TryGetRow(9, out var hqSymbol)) + { + sb.Append(hqSymbol.Text); + } + else if (flags.HasFlag(SheetRedirectFlags.Collectible) + && this.dataManager.GetExcelSheet(context.Language).TryGetRow(150, out var collectibleSymbol)) + { + sb.Append(collectibleSymbol.Text); + } + + if (!skipLink) + sb.PopLink(); + + text = sb.ToReadOnlySeString(); + SeStringBuilder.SharedPool.Return(sb); + } + + private void CreateSheetLink(in SeStringContext context, string resolvedSheetName, ReadOnlySeString text, uint eRowIdValue, uint eColParamValue) + { + switch (resolvedSheetName) + { + case "Achievement": + context.Builder.PushLink(LinkMacroPayloadType.Achievement, eRowIdValue, 0u, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "HowTo": + context.Builder.PushLink(LinkMacroPayloadType.HowTo, eRowIdValue, 0u, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "Status" when this.dataManager.GetExcelSheet(context.Language).TryGetRow(eRowIdValue, out var statusRow): + context.Builder.PushLink(LinkMacroPayloadType.Status, eRowIdValue, 0u, 0u, []); + + switch (statusRow.StatusCategory) + { + case 1: context.Builder.Append(this.EvaluateFromAddon(376)); break; // buff symbol + case 2: context.Builder.Append(this.EvaluateFromAddon(377)); break; // debuff symbol + } + + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "AkatsukiNoteString": + context.Builder.PushLink(LinkMacroPayloadType.AkatsukiNote, eColParamValue, 0u, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "DescriptionString" when eColParamValue > 0: + context.Builder.PushLink((LinkMacroPayloadType)11, eRowIdValue, eColParamValue, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "WKSPioneeringTrailString": + context.Builder.PushLink((LinkMacroPayloadType)12, eRowIdValue, eColParamValue, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + + case "MKDLore": + context.Builder.PushLink((LinkMacroPayloadType)13, eRowIdValue, 0u, 0u, text.AsSpan()); + context.Builder.Append(text); + context.Builder.PopLink(); + return; + default: - return false; + context.Builder.Append(text); + return; } } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs index c45e0cdfb..bff023ff4 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/SeStringCreatorWidget.cs @@ -94,6 +94,7 @@ internal class SeStringCreatorWidget : IDataWindowWidget { MacroCode.LowerHead, ["String"] }, { MacroCode.ColorType, ["ColorType"] }, { MacroCode.EdgeColorType, ["ColorType"] }, + { MacroCode.Ruby, ["StandardText", "RubyText"] }, { MacroCode.Digit, ["Value", "TargetLength"] }, { MacroCode.Ordinal, ["Value"] }, { MacroCode.Sound, ["IsJingle", "SoundId"] }, @@ -477,7 +478,12 @@ internal class SeStringCreatorWidget : IDataWindowWidget } } - RaptureLogModule.Instance()->PrintString(Service.Get().Evaluate(sb.ToReadOnlySeString())); + var evaluated = Service.Get().Evaluate( + sb.ToReadOnlySeString(), + this.localParameters, + this.language); + + RaptureLogModule.Instance()->PrintString(evaluated); } if (this.entries.Count != 0) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs index 6ab08cd91..b43c045f4 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/SheetRedirectResolverSelfTestStep.cs @@ -63,6 +63,10 @@ internal class SheetRedirectResolverSelfTestStep : ISelfTestStep new("WeatherPlaceName", 40), new("WeatherPlaceName", 52), new("WeatherPlaceName", 2300), + new("InstanceContent", 1), + new("PartyContent", 2), + new("PublicContent", 1), + new("AkatsukiNote", 1), ]; private unsafe delegate SheetRedirectFlags ResolveSheetRedirect(RaptureTextModule* thisPtr, Utf8String* sheetName, uint* rowId, uint* flags); From e20f132abe441aabd252a240e189219a294f1b3a Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 29 May 2025 19:38:10 +0200 Subject: [PATCH 087/106] Add ISeStringEvaluator.EvaluateMacroString (#2281) --- Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs | 9 +++++++++ Dalamud/Plugin/Services/ISeStringEvaluator.cs | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs index 85c342e22..57040701c 100644 --- a/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs +++ b/Dalamud/Game/Text/Evaluator/SeStringEvaluator.cs @@ -114,6 +114,15 @@ internal class SeStringEvaluator : IServiceType, ISeStringEvaluator } } + /// + public ReadOnlySeString EvaluateMacroString( + string macroString, + Span localParameters = default, + ClientLanguage? language = null) + { + return this.Evaluate(ReadOnlySeString.FromMacroString(macroString).AsSpan(), localParameters, language); + } + /// public ReadOnlySeString EvaluateFromAddon( uint addonId, diff --git a/Dalamud/Plugin/Services/ISeStringEvaluator.cs b/Dalamud/Plugin/Services/ISeStringEvaluator.cs index 2bd423b7c..846dcd53e 100644 --- a/Dalamud/Plugin/Services/ISeStringEvaluator.cs +++ b/Dalamud/Plugin/Services/ISeStringEvaluator.cs @@ -32,6 +32,15 @@ public interface ISeStringEvaluator /// An evaluated . ReadOnlySeString Evaluate(ReadOnlySeStringSpan str, Span localParameters = default, ClientLanguage? language = null); + /// + /// Evaluates macros in a macro string. + /// + /// The macro string. + /// An optional list of local parameters. + /// An optional language override. + /// An evaluated . + ReadOnlySeString EvaluateMacroString(string macroString, Span localParameters = default, ClientLanguage? language = null); + /// /// Evaluates macros in text from the Addon sheet. /// From 911999e98c3d9ae926bd62dc0f94d2ba861f5195 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Thu, 29 May 2025 19:38:29 +0200 Subject: [PATCH 088/106] Update AddonEventType (#2279) --- Dalamud/Game/Addon/Events/AddonEventType.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Dalamud/Game/Addon/Events/AddonEventType.cs b/Dalamud/Game/Addon/Events/AddonEventType.cs index ec46c368b..cd04152ca 100644 --- a/Dalamud/Game/Addon/Events/AddonEventType.cs +++ b/Dalamud/Game/Addon/Events/AddonEventType.cs @@ -1,4 +1,4 @@ -namespace Dalamud.Game.Addon.Events; +namespace Dalamud.Game.Addon.Events; /// /// Reimplementation of AtkEventType. @@ -50,6 +50,17 @@ public enum AddonEventType : byte /// InputReceived = 12, + /// + /// Input Navigation (LEFT, RIGHT, UP, DOWN, TAB_NEXT, TAB_PREV, TAB_BOTH_NEXT, TAB_BOTH_PREV, PAGEUP, PAGEDOWN). + /// + InputNavigation = 13, + + /// + /// InputBase Input Received (AtkComponentTextInput and AtkComponentNumericInput).
+ /// For example, this is fired for moving the text cursor, deletion of a character and inserting a new line. + ///
+ InputBaseInputReceived = 15, + /// /// Focus Start. /// From ed8a455dad5bc44c39de90343b76784f820ee439 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 29 May 2025 19:39:03 +0200 Subject: [PATCH 089/106] Update ClientStructs (#2280) 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 cc8ace372..35335b7de 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit cc8ace37200f84b435afd2fded279ec347c49b1e +Subproject commit 35335b7de1a2020958803b6c84a6711214a6ade4 From 944c3700db51a0ecfa0293592709c9217a263f6f Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 29 May 2025 19:40:32 +0200 Subject: [PATCH 090/106] build: 12.0.1.1 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index fa855490d..9798a43c4 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.1.0 + 12.0.1.1 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 84d121c7bc109199f351f7024b8f932ca9e337fb Mon Sep 17 00:00:00 2001 From: salanth357 Date: Thu, 29 May 2025 14:24:21 -0400 Subject: [PATCH 091/106] Add Completion module (#2274) * Add Completion module Dalamud and plugin commands will now be tab-completable in the ChatLog * PR feedback --------- Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- Dalamud/Game/Command/CommandManager.cs | 52 +++- Dalamud/Game/Internal/Completion.cs | 293 ++++++++++++++++++ .../Windows/SelfTest/SelfTestWindow.cs | 3 +- .../SelfTest/Steps/CompletionSelfTestStep.cs | 94 ++++++ 4 files changed, 438 insertions(+), 4 deletions(-) create mode 100644 Dalamud/Game/Internal/Completion.cs create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index fdaa5833b..09cd7877d 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -45,6 +45,16 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma this.console.Invoke += this.ConsoleOnInvoke; } + /// + /// Published whenever a command is registered + /// + public event EventHandler? CommandAdded; + + /// + /// Published whenever a command is unregistered + /// + public event EventHandler? CommandRemoved; + /// public ReadOnlyDictionary Commands => new(this.commandMap); @@ -122,6 +132,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma return false; } + this.CommandAdded?.Invoke(this, new CommandEventArgs + { + Command = command, + CommandInfo = info, + }); + if (!this.commandAssemblyNameMap.TryAdd((command, info), loaderAssemblyName)) { this.commandMap.Remove(command, out _); @@ -144,6 +160,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma return false; } + this.CommandAdded?.Invoke(this, new CommandEventArgs + { + Command = command, + CommandInfo = info, + }); + return true; } @@ -155,7 +177,17 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma this.commandAssemblyNameMap.TryRemove(assemblyKeyValuePair.Key, out _); } - return this.commandMap.Remove(command, out _); + var removed = this.commandMap.Remove(command, out var info); + if (removed) + { + this.CommandRemoved?.Invoke(this, new CommandEventArgs + { + Command = command, + CommandInfo = info, + }); + } + + return removed; } /// @@ -204,6 +236,20 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma return this.ProcessCommand(command->ToString()) ? 0 : result; } + + /// + public class CommandEventArgs : EventArgs + { + /// + /// Gets the command string + /// + public string Command { get; init; } + + /// + /// Gets the command info + /// + public IReadOnlyCommandInfo CommandInfo { get; init; } + } } /// @@ -268,7 +314,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand } else { - Log.Error($"Command {command} is already registered."); + Log.Error("Command {Command} is already registered.", command); } return false; @@ -287,7 +333,7 @@ internal class CommandManagerPluginScoped : IInternalDisposableService, ICommand } else { - Log.Error($"Command {command} not found."); + Log.Error("Command {Command} not found.", command); } return false; diff --git a/Dalamud/Game/Internal/Completion.cs b/Dalamud/Game/Internal/Completion.cs new file mode 100644 index 000000000..05fae3514 --- /dev/null +++ b/Dalamud/Game/Internal/Completion.cs @@ -0,0 +1,293 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Game.Command; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Hooking; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + +using FFXIVClientStructs.FFXIV.Client.System.String; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Component.Completion; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Dalamud.Game.Internal; + +/// +/// This class adds dalamud and plugin commands to the chat box's autocompletion. +/// +[ServiceManager.EarlyLoadedService] +internal sealed unsafe class Completion : IInternalDisposableService +{ + // 0xFF is a magic group number that causes CompletionModule's internals to treat entries + // as raw strings instead of as lookups into an EXD sheet + private const int GroupNumber = 0xFF; + + [ServiceManager.ServiceDependency] + private readonly CommandManager commandManager = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + private readonly EntryStrings dalamudCategory = new("【Dalamud】"); + private readonly Dictionary cachedCommands = []; + private readonly ConcurrentQueue addedCommands = []; + + private Hook? getSelection; + + // This is marked volatile since we set and check it from different threads. Instead of using a synchronization + // primitive, a volatile is sufficient since the absolute worst case is that we delay one extra frame to reset + // the list, which is fine + private volatile bool needsClear; + private bool disposed; + private nint wantedVtblPtr; + + /// + /// Initializes a new instance of the class. + /// + [ServiceManager.ServiceConstructor] + internal Completion() + { + this.commandManager.CommandAdded += this.OnCommandAdded; + this.commandManager.CommandRemoved += this.OnCommandRemoved; + + this.framework.Update += this.OnUpdate; + } + + /// Finalizes an instance of the class. + ~Completion() => this.Dispose(false); + + /// + void IInternalDisposableService.DisposeService() => this.Dispose(true); + + private static AtkUnitBase* FindOwningAddon(AtkComponentTextInput* component) + { + if (component == null) return null; + + var node = (AtkResNode*)component->OwnerNode; + if (node == null) return null; + + while (node->ParentNode != null) + node = node->ParentNode; + + foreach (var addon in RaptureAtkUnitManager.Instance()->AllLoadedUnitsList.Entries) + { + if (addon.Value->RootNode == node) + return addon; + } + + return null; + } + + private AtkComponentTextInput* GetActiveTextInput() + { + var mod = RaptureAtkModule.Instance(); + if (mod == null) return null; + + var basePtr = mod->TextInput.TargetTextInputEventInterface; + if (basePtr == null) return null; + + // Once CS has an implementation for multiple inheritance, we can remove this sig from dalamud + // as well as the nasty pointer arithmetic below. But for now, we need to do this manually. + // The AtkTextInputEventInterface* is the secondary base class for AtkComponentTextInput* + // so the pointer is sizeof(AtkComponentInputBase) into the object. We verify that we're looking + // at the object we think we are by confirming the pointed-to vtbl matches the known secondary vtbl for + // AtkComponentTextInput, and if it does, we can shift the pointer back to get the start of our text input + if (this.wantedVtblPtr == 0) + { + this.wantedVtblPtr = + Service.Get().GetStaticAddressFromSig( + "48 89 01 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8D 05 ?? ?? ?? ?? 48 89 81 ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 8B 48 68", + 4); + } + + var vtblPtr = *(nint*)basePtr; + if (vtblPtr != this.wantedVtblPtr) return null; + + // This needs to be updated if the layout/base order of AtkComponentTextInput changes + return (AtkComponentTextInput*)((AtkComponentInputBase*)basePtr - 1); + } + + private bool AllowCompletion(string cmd) + { + // this is one of our commands, let's see if we should allow this to be completed + var component = this.GetActiveTextInput(); + + // ContainingAddon or ContainingAddon2 aren't always populated, but they + // seem to be in any case where this is actually a completable AtkComponentTextInput + // In the worst case, we can walk the AtkNode tree- but let's try the easy pointers first + var addon = component->ContainingAddon; + if (addon == null) addon = component->ContainingAddon2; + if (addon == null) addon = FindOwningAddon(component); + + if (addon == null || addon->NameString != "ChatLog") + { + // we don't know what addon is completing, or we know it isn't ChatLog + // either way, we should just reject this completion + return false; + } + + // We're in ChatLog, so check if this is the start of the text input + // AtkComponentTextInput->UnkText1 is the evaluated version of the current text + // so if the command starts with that, then either it's empty or a prefix completion. + // In either case, we're happy to allow completion. + return cmd.StartsWith(component->UnkText1.StringPtr.ExtractText()); + } + + private void Dispose(bool disposing) + { + if (this.disposed) + return; + + if (disposing) + { + this.getSelection?.Disable(); + this.getSelection?.Dispose(); + this.framework.Update -= this.OnUpdate; + this.commandManager.CommandAdded -= this.OnCommandAdded; + this.commandManager.CommandRemoved -= this.OnCommandRemoved; + } + + this.disposed = true; + } + + private void OnCommandAdded(object? sender, CommandManager.CommandEventArgs e) + { + if (e.CommandInfo.ShowInHelp) + this.addedCommands.Enqueue(e.Command); + } + + private void OnCommandRemoved(object? sender, CommandManager.CommandEventArgs e) => this.needsClear = true; + + private void OnUpdate(IFramework fw) + { + var atkModule = RaptureAtkModule.Instance(); + if (atkModule == null) return; + + var textInput = &atkModule->TextInput; + + if (textInput->CompletionModule == null) return; + + // Before we change _anything_ we need to check the state of the UI- if the completion list is open + // changes to the underlying data are extremely unsafe, so we'll just wait until the next frame + // worst case, someone tries to complete a command that _just_ got unloaded so it won't do anything + // but that's the same as making a typo, really + if (textInput->CompletionDepth > 0) return; + + this.LoadCommands(textInput->CompletionModule); + } + + private CategoryData* EnsureCategoryData(CompletionModule* module) + { + if (module == null) return null; + + if (this.getSelection == null) + { + this.getSelection = Hook.FromAddress( + (IntPtr)module->VirtualTable->GetSelection, + this.GetSelectionDetour); + this.getSelection.Enable(); + } + + for (var i = 0; i < module->CategoryNames.Count; i++) + { + if (module->CategoryNames[i].ExtractText() == "【Dalamud】") + { + return module->CategoryData[i]; + } + } + + // Create the category since we don't have one + var categoryData = (CategoryData*)Memory.MemoryHelper.GameAllocateDefault((ulong)sizeof(CategoryData)); + categoryData->Ctor(GroupNumber, 0xFF); + module->AddCategoryData(GroupNumber, this.dalamudCategory.Display->StringPtr, + this.dalamudCategory.Match->StringPtr, categoryData); + + return categoryData; + } + + private void LoadCommands(CompletionModule* completionModule) + { + if (completionModule == null) return; + if (completionModule->CategoryNames.Count == 0) return; // We want this data populated first + + if (this.needsClear && this.cachedCommands.Count > 0) + { + this.needsClear = false; + completionModule->ClearCompletionData(); + this.cachedCommands.Clear(); + return; + } + + var catData = this.EnsureCategoryData(completionModule); + if (catData == null) return; + + if (catData->CompletionData.Count == 0) + { + var inputCommands = this.commandManager.Commands.Where(pair => pair.Value.ShowInHelp).OrderBy(pair => pair.Key); + foreach (var (cmd, _) in inputCommands) + AddEntry(cmd); + + return; + } + + while (this.addedCommands.TryDequeue(out var cmd)) + AddEntry(cmd); + + catData->SortEntries(); + return; + + void AddEntry(string cmd) + { + if (this.cachedCommands.ContainsKey(cmd)) return; + + var cmdStr = new EntryStrings(cmd); + this.cachedCommands.Add(cmd, cmdStr); + completionModule->AddCompletionEntry( + GroupNumber, + 0xFF, + cmdStr.Display->StringPtr, + cmdStr.Match->StringPtr, + 0xFF); + } + } + + private int GetSelectionDetour(CompletionModule* thisPtr, CategoryData.CompletionDataStruct* dataStructs, int index, Utf8String* outputString, Utf8String* outputDisplayString) + { + var ret = this.getSelection!.Original.Invoke(thisPtr, dataStructs, index, outputString, outputDisplayString); + if (ret != -2 || outputString == null) return ret; + + // -2 means it was a plain text final selection, so it might be ours + // Unfortunately, the code that uses this string mangles the color macro for some reason... + // We'll just strip those out since we don't need the color in the chatbox + var txt = outputString->StringPtr.ExtractText(); + if (!this.cachedCommands.ContainsKey(txt)) + return ret; + + if (!this.AllowCompletion(txt)) + { + outputString->Clear(); + if (outputDisplayString != null) outputDisplayString->Clear(); + return ret; + } + + outputString->SetString(txt + " "); + return ret; + } + + private class EntryStrings(string command) + { + ~EntryStrings() + { + this.Display->Dtor(true); + this.Match->Dtor(true); + } + + public Utf8String* Display { get; } = + Utf8String.FromSequence(new SeStringBuilder().AddUiForeground(command, 539).Encode()); + + public Utf8String* Match { get; } = Utf8String.FromString(command); + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index da2aaff2d..e19aafbab 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -57,7 +57,8 @@ internal class SelfTestWindow : Window new SheetRedirectResolverSelfTestStep(), new NounProcessorSelfTestStep(), new SeStringEvaluatorSelfTestStep(), - new LogoutEventSelfTestStep() + new LogoutEventSelfTestStep(), + new CompletionSelfTestStep() ]; private readonly Dictionary testIndexToResult = new(); diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs new file mode 100644 index 000000000..442131e14 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs @@ -0,0 +1,94 @@ +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Game.Text; +using Dalamud.Game.Text.SeStringHandling; + +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.Steps; + +/// +/// Test setup for Chat. +/// +internal class CompletionSelfTestStep : ISelfTestStep +{ + private int step = 0; + private bool registered; + private bool commandRun; + + /// + public string Name => "Test Completion"; + + /// + public SelfTestStepResult RunStep() + { + var cmdManager = Service.Get(); + switch (this.step) + { + case 0: + this.step++; + + break; + + case 1: + ImGui.Text("[Chat Log]"); + ImGui.Text("Use the category menus to navigate to [Dalamud], then complete a command from the list. Did it work?"); + if (ImGui.Button("Yes")) + this.step++; + ImGui.SameLine(); + + if (ImGui.Button("No")) + return SelfTestStepResult.Fail; + break; + case 2: + ImGui.Text("[Chat Log]"); + ImGui.Text("Type /xl into the chat log and tab-complete a dalamud command. Did it work?"); + + if (ImGui.Button("Yes")) + this.step++; + ImGui.SameLine(); + + if (ImGui.Button("No")) + return SelfTestStepResult.Fail; + + break; + + case 3: + ImGui.Text("[Chat Log]"); + if (!this.registered) + { + cmdManager.AddHandler("/xlselftestcompletion", new CommandInfo((_, _) => this.commandRun = true)); + this.registered = true; + } + + ImGui.Text("Tab-complete /xlselftestcompletion in the chat log and send the command"); + + if (this.commandRun) + this.step++; + + break; + + case 4: + ImGui.Text("[Other text inputs]"); + ImGui.Text("Open the party finder recruitment criteria dialog and try to tab-complete /xldev in the text box."); + ImGui.Text("Did the command appear in the text box? (It should not have)"); + if (ImGui.Button("Yes")) + return SelfTestStepResult.Fail; + ImGui.SameLine(); + + if (ImGui.Button("No")) + this.step++; + break; + case 5: + return SelfTestStepResult.Pass; + } + + return SelfTestStepResult.Waiting; + } + + /// + public void CleanUp() + { + Service.Get().RemoveHandler("/completionselftest"); + } +} From bf0dbde55f6672cf9696855284cbf3d5afe2d841 Mon Sep 17 00:00:00 2001 From: goaaats Date: Thu, 29 May 2025 21:08:03 +0200 Subject: [PATCH 092/106] Completion: Don't create Utf8String before the game has initialized --- Dalamud/Game/Command/CommandManager.cs | 4 +-- Dalamud/Game/Internal/Completion.cs | 42 ++++++++++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 09cd7877d..b72238abe 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -241,12 +241,12 @@ internal sealed unsafe class CommandManager : IInternalDisposableService, IComma public class CommandEventArgs : EventArgs { /// - /// Gets the command string + /// Gets the command string. /// public string Command { get; init; } /// - /// Gets the command info + /// Gets the command info. /// public IReadOnlyCommandInfo CommandInfo { get; init; } } diff --git a/Dalamud/Game/Internal/Completion.cs b/Dalamud/Game/Internal/Completion.cs index 05fae3514..56ff0d854 100644 --- a/Dalamud/Game/Internal/Completion.cs +++ b/Dalamud/Game/Internal/Completion.cs @@ -31,10 +31,11 @@ internal sealed unsafe class Completion : IInternalDisposableService [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); - private readonly EntryStrings dalamudCategory = new("【Dalamud】"); private readonly Dictionary cachedCommands = []; private readonly ConcurrentQueue addedCommands = []; + private EntryStrings? dalamudCategory; + private Hook? getSelection; // This is marked volatile since we set and check it from different threads. Instead of using a synchronization @@ -148,6 +149,9 @@ internal sealed unsafe class Completion : IInternalDisposableService this.framework.Update -= this.OnUpdate; this.commandManager.CommandAdded -= this.OnCommandAdded; this.commandManager.CommandRemoved -= this.OnCommandRemoved; + + this.dalamudCategory?.Dispose(); + this.ClearCachedCommands(); } this.disposed = true; @@ -176,6 +180,11 @@ internal sealed unsafe class Completion : IInternalDisposableService // but that's the same as making a typo, really if (textInput->CompletionDepth > 0) return; + // Create the category for Dalamud commands. + // This needs to be done here, since we cannot create Utf8Strings before the game + // has initialized (no allocator set up yet). + this.dalamudCategory ??= new EntryStrings("【Dalamud】"); + this.LoadCommands(textInput->CompletionModule); } @@ -202,12 +211,25 @@ internal sealed unsafe class Completion : IInternalDisposableService // Create the category since we don't have one var categoryData = (CategoryData*)Memory.MemoryHelper.GameAllocateDefault((ulong)sizeof(CategoryData)); categoryData->Ctor(GroupNumber, 0xFF); - module->AddCategoryData(GroupNumber, this.dalamudCategory.Display->StringPtr, + module->AddCategoryData(GroupNumber, this.dalamudCategory!.Display->StringPtr, this.dalamudCategory.Match->StringPtr, categoryData); return categoryData; } + private void ClearCachedCommands() + { + if (this.cachedCommands.Count == 0) + return; + + foreach (var entry in this.cachedCommands.Values) + { + entry.Dispose(); + } + + this.cachedCommands.Clear(); + } + private void LoadCommands(CompletionModule* completionModule) { if (completionModule == null) return; @@ -217,7 +239,7 @@ internal sealed unsafe class Completion : IInternalDisposableService { this.needsClear = false; completionModule->ClearCompletionData(); - this.cachedCommands.Clear(); + this.ClearCachedCommands(); return; } @@ -277,17 +299,17 @@ internal sealed unsafe class Completion : IInternalDisposableService return ret; } - private class EntryStrings(string command) + private class EntryStrings(string command) : IDisposable { - ~EntryStrings() - { - this.Display->Dtor(true); - this.Match->Dtor(true); - } - public Utf8String* Display { get; } = Utf8String.FromSequence(new SeStringBuilder().AddUiForeground(command, 539).Encode()); public Utf8String* Match { get; } = Utf8String.FromString(command); + + public void Dispose() + { + this.Display->Dtor(true); + this.Match->Dtor(true); + } } } From eb34eb10230036479ce683e1209f4a923c2d6517 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 31 May 2025 12:00:20 +0200 Subject: [PATCH 093/106] Fix some warnings --- Dalamud/Interface/Internal/UiDebug.cs | 12 ++++++------ .../Internal/UiDebug2/Browsing/NodeTree.Component.cs | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/Internal/UiDebug.cs b/Dalamud/Interface/Internal/UiDebug.cs index 9dfff75ec..d0ebc8fac 100644 --- a/Dalamud/Interface/Internal/UiDebug.cs +++ b/Dalamud/Interface/Internal/UiDebug.cs @@ -431,17 +431,17 @@ internal unsafe class UiDebug ImGui.SameLine(); Service.Get().Draw(textInputComponent->UnkText02); - ImGui.Text("Text3: "); + ImGui.Text("AvailableLines: "); ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText03); + Service.Get().Draw(textInputComponent->AvailableLines); - ImGui.Text("Text4: "); + ImGui.Text("HighlightedAutoTranslateOptionColorPrefix: "); ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText04); + Service.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorPrefix); - ImGui.Text("Text5: "); + ImGui.Text("HighlightedAutoTranslateOptionColorSuffix: "); ImGui.SameLine(); - Service.Get().Draw(textInputComponent->UnkText05); + Service.Get().Draw(textInputComponent->HighlightedAutoTranslateOptionColorSuffix); break; } diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs index 4a1989441..2383e19e0 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs @@ -92,11 +92,11 @@ internal unsafe class ComponentNodeTree : ResNodeTree ImGui.TextUnformatted( $"Text2: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText02.StringPtr))}"); ImGui.TextUnformatted( - $"Text3: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText03.StringPtr))}"); + $"AvailableLines: {Marshal.PtrToStringAnsi(new(textInputComponent->AvailableLines.StringPtr))}"); ImGui.TextUnformatted( - $"Text4: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText04.StringPtr))}"); + $"HighlightedAutoTranslateOptionColorPrefix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorPrefix.StringPtr))}"); ImGui.TextUnformatted( - $"Text5: {Marshal.PtrToStringAnsi(new(textInputComponent->UnkText05.StringPtr))}"); + $"HighlightedAutoTranslateOptionColorSuffix: {Marshal.PtrToStringAnsi(new(textInputComponent->HighlightedAutoTranslateOptionColorSuffix.StringPtr))}"); break; case List: case TreeList: From abde79dbc80e358130921c271905938c8aede815 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 31 May 2025 12:00:40 +0200 Subject: [PATCH 094/106] Re-add toggle window commands to CorePlugin --- Dalamud.CorePlugin/PluginImpl.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index 951050b33..1942e271b 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -46,6 +46,8 @@ namespace Dalamud.CorePlugin #else private readonly WindowSystem windowSystem = new("Dalamud.CorePlugin"); + private readonly PluginWindow window; + private Localization localization; private IPluginLog pluginLog; @@ -63,7 +65,8 @@ namespace Dalamud.CorePlugin this.Interface = pluginInterface; this.pluginLog = log; - this.windowSystem.AddWindow(new PluginWindow()); + this.window = new PluginWindow(); + this.windowSystem.AddWindow(this.window); this.Interface.UiBuilder.Draw += this.OnDraw; this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi; @@ -136,12 +139,12 @@ namespace Dalamud.CorePlugin { this.pluginLog.Information("Command called!"); - // this.window.IsOpen = true; + this.window.IsOpen ^= true; } private void OnOpenConfigUi() { - // this.window.IsOpen = true; + this.window.IsOpen = true; } private void OnOpenMainUi() From a2f6fb85e51dbbf56bcc78825b625276063efc89 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sat, 31 May 2025 13:20:54 +0200 Subject: [PATCH 095/106] Don't fade windows if we are docked or collapsed, for now The proper fix would be not to use the "fake window" approach and insert a drawlist, but not with the current bindings --- Dalamud/Interface/Windowing/Window.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Windowing/Window.cs b/Dalamud/Interface/Windowing/Window.cs index 032f6e93e..9b972256a 100644 --- a/Dalamud/Interface/Windowing/Window.cs +++ b/Dalamud/Interface/Windowing/Window.cs @@ -598,6 +598,8 @@ public abstract class Window this.fadeOutSize = ImGui.GetWindowSize(); this.fadeOutOrigin = ImGui.GetWindowPos(); + var isCollapsed = ImGui.IsWindowCollapsed(); + var isDocked = ImGui.IsWindowDocked(); ImGui.End(); @@ -607,7 +609,12 @@ public abstract class Window this.pushedFadeInAlpha = false; } - if (!this.internalIsOpen && this.fadeOutTexture == null && doFades) + // TODO: No fade-out if the window is collapsed. We could do this if we knew the "FullSize" of the window + // from the internal ImGuiWindow, but I don't want to mess with that here for now. We can do this a lot + // easier with the new bindings. + // TODO: No fade-out if docking is enabled and the window is docked, since this makes them "unsnap". + // Ideally we should get rid of this "fake window" thing and just insert a new drawlist at the correct spot. + if (!this.internalIsOpen && this.fadeOutTexture == null && doFades && !isCollapsed && !isDocked) { this.fadeOutTexture = Service.Get().CreateDrawListTexture( "WindowFadeOutTexture"); @@ -833,7 +840,7 @@ public abstract class Window style.Push(ImGuiStyleVar.WindowBorderSize, 0); style.Push(ImGuiStyleVar.FrameBorderSize, 0); - const ImGuiWindowFlags flags = ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse | + const ImGuiWindowFlags flags = ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoScrollWithMouse | ImGuiWindowFlags.NoMouseInputs | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoBackground; if (ImGui.Begin(this.WindowName, flags)) From e2609bbe0cbf63bb54fc49c85e0865509a7ba7a4 Mon Sep 17 00:00:00 2001 From: salanth357 Date: Sat, 31 May 2025 07:24:28 -0400 Subject: [PATCH 096/106] Improve performance in the Completion module (#2288) Only resort the data entries when we modify the lists. Also use EncodeWithNullTerminator to ensure the safety of strings. Also avoid parsing the category names when we're only looking for the presence of the Dalamud category --- Dalamud/Game/Internal/Completion.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Dalamud/Game/Internal/Completion.cs b/Dalamud/Game/Internal/Completion.cs index 56ff0d854..01c9c99c5 100644 --- a/Dalamud/Game/Internal/Completion.cs +++ b/Dalamud/Game/Internal/Completion.cs @@ -202,7 +202,7 @@ internal sealed unsafe class Completion : IInternalDisposableService for (var i = 0; i < module->CategoryNames.Count; i++) { - if (module->CategoryNames[i].ExtractText() == "【Dalamud】") + if (module->CategoryNames[i].AsReadOnlySeStringSpan().ContainsText("【Dalamud】"u8)) { return module->CategoryData[i]; } @@ -248,17 +248,24 @@ internal sealed unsafe class Completion : IInternalDisposableService if (catData->CompletionData.Count == 0) { - var inputCommands = this.commandManager.Commands.Where(pair => pair.Value.ShowInHelp).OrderBy(pair => pair.Key); + var inputCommands = this.commandManager.Commands.Where(pair => pair.Value.ShowInHelp); foreach (var (cmd, _) in inputCommands) AddEntry(cmd); + catData->SortEntries(); return; } + var needsSort = false; while (this.addedCommands.TryDequeue(out var cmd)) + { + needsSort = true; AddEntry(cmd); + } + + if (needsSort) + catData->SortEntries(); - catData->SortEntries(); return; void AddEntry(string cmd) @@ -302,7 +309,7 @@ internal sealed unsafe class Completion : IInternalDisposableService private class EntryStrings(string command) : IDisposable { public Utf8String* Display { get; } = - Utf8String.FromSequence(new SeStringBuilder().AddUiForeground(command, 539).Encode()); + Utf8String.FromSequence(new SeStringBuilder().AddUiForeground(command, 539).BuiltString.EncodeWithNullTerminator()); public Utf8String* Match { get; } = Utf8String.FromString(command); From 8ce676f2f10c13582106918fe00ed17aed254da4 Mon Sep 17 00:00:00 2001 From: goaaats Date: Sun, 1 Jun 2025 19:13:11 +0200 Subject: [PATCH 097/106] build: 12.0.1.2 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 9798a43c4..8d6029fd8 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.1.1 + 12.0.1.2 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 878d892631d645552d90a1276e4278be0a521296 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 3 Jun 2025 03:07:19 +0200 Subject: [PATCH 098/106] Update ClientStructs (#2286) 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 35335b7de..9f36ff10d 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 35335b7de1a2020958803b6c84a6711214a6ade4 +Subproject commit 9f36ff10dc7180488e2247155fdaaa72f851724e From 7c4e1c44a60f4ea58094f751b4c8fdadb6a85c30 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sat, 7 Jun 2025 09:55:13 -0700 Subject: [PATCH 099/106] Update NodeTree.Component.cs (#2291) --- .../Internal/UiDebug2/Browsing/NodeTree.Component.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs index 2383e19e0..dc9d377bb 100644 --- a/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs +++ b/Dalamud/Interface/Internal/UiDebug2/Browsing/NodeTree.Component.cs @@ -58,7 +58,13 @@ internal unsafe class ComponentNodeTree : ResNodeTree /// private protected override void PrintChildNodes() { - base.PrintChildNodes(); + var prevNode = this.CompNode->Component->UldManager.RootNode; + while (prevNode != null) + { + GetOrCreate(prevNode, this.AddonTree).Print(null); + prevNode = prevNode->PrevSiblingNode; + } + var count = this.UldManager->NodeListCount; PrintNodeListAsTree(this.UldManager->NodeList, count, $"Node List [{count}]:", this.AddonTree, new(0f, 0.5f, 0.8f, 1f)); } From db989591583136415171c553f852418fd4002a23 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:57:07 +0200 Subject: [PATCH 100/106] Update ClientStructs (#2290) 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 9f36ff10d..41b801bc0 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 9f36ff10dc7180488e2247155fdaaa72f851724e +Subproject commit 41b801bc0e1b17445b581fa17ad5318fb2961dad From adec94831922891a843e8b0d7a173600ba0a119e Mon Sep 17 00:00:00 2001 From: goaaats Date: Mon, 9 Jun 2025 21:19:04 +0200 Subject: [PATCH 101/106] build: 12.0.1.3 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 8d6029fd8..5a27b3893 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.1.2 + 12.0.1.3 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From 0691241240602c12bd5809fa7f48007091c380c1 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Sun, 15 Jun 2025 21:28:02 +0200 Subject: [PATCH 102/106] Update ClientStructs (#2293) 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 41b801bc0..4d1e9a5c8 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 41b801bc0e1b17445b581fa17ad5318fb2961dad +Subproject commit 4d1e9a5c873083470d80ea025c5a492b055637c8 From 12ee9343d4104907849b255a95073ff7745cd310 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 17 Jun 2025 17:36:26 +0200 Subject: [PATCH 103/106] Update ClientStructs (#2296) 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 4d1e9a5c8..a93b68f50 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 4d1e9a5c873083470d80ea025c5a492b055637c8 +Subproject commit a93b68f501556f644f97da2d0a54dba83b5ffda4 From 6a42568073d8e5df8c6a2dbb2777627e5e6e6366 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 17 Jun 2025 17:41:36 +0200 Subject: [PATCH 104/106] build: 12.0.1.4 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 5a27b3893..178a97a7c 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -6,7 +6,7 @@ XIV Launcher addon framework - 12.0.1.3 + 12.0.1.4 $(DalamudVersion) $(DalamudVersion) $(DalamudVersion) From b1986bd3d17e8e295c85d8bc2dba557c98cd6edc Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Tue, 17 Jun 2025 19:48:40 +0200 Subject: [PATCH 105/106] Self-Test Window improvements (#2298) * Move Logout self-test to the bottom of the list * Increase self-test result and make it scrollable * Allow text wrapping for some strings in self-tests * Fix context menu self-test not working properly on HQ items --- .../Internal/Windows/SelfTest/SelfTestWindow.cs | 9 ++++++--- .../Windows/SelfTest/Steps/CompletionSelfTestStep.cs | 2 +- .../Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs | 2 +- .../Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs | 8 ++++---- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index e19aafbab..e587fcdcf 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -57,8 +57,8 @@ internal class SelfTestWindow : Window new SheetRedirectResolverSelfTestStep(), new NounProcessorSelfTestStep(), new SeStringEvaluatorSelfTestStep(), - new LogoutEventSelfTestStep(), - new CompletionSelfTestStep() + new CompletionSelfTestStep(), + new LogoutEventSelfTestStep() ]; private readonly Dictionary testIndexToResult = new(); @@ -152,6 +152,9 @@ internal class SelfTestWindow : Window return; } + using var resultChild = ImRaii.Child("SelfTestResultChild", ImGui.GetContentRegionAvail()); + if (!resultChild) return; + var step = this.steps[this.currentStep]; ImGui.TextUnformatted($"Current: {step.Name}"); @@ -184,7 +187,7 @@ internal class SelfTestWindow : Window var tableSize = ImGui.GetContentRegionAvail(); if (this.selfTestRunning) - tableSize -= new Vector2(0, 150); + tableSize -= new Vector2(0, 200); tableSize.Y = Math.Min(tableSize.Y, ImGui.GetWindowViewport().Size.Y * 0.5f); diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs index 442131e14..40f8f3bce 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/CompletionSelfTestStep.cs @@ -32,7 +32,7 @@ internal class CompletionSelfTestStep : ISelfTestStep case 1: ImGui.Text("[Chat Log]"); - ImGui.Text("Use the category menus to navigate to [Dalamud], then complete a command from the list. Did it work?"); + ImGui.TextWrapped("Use the category menus to navigate to [Dalamud], then complete a command from the list. Did it work?"); if (ImGui.Button("Yes")) this.step++; ImGui.SameLine(); diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs index 0e2f61aba..3e414647e 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/ContextMenuSelfTestStep.cs @@ -145,7 +145,7 @@ internal class ContextMenuSelfTestStep : ISelfTestStep var targetItem = (a.Target as MenuTargetInventory)!.TargetItem; if (targetItem is { } item) { - name = (this.itemSheet.GetRowOrDefault(item.ItemId)?.Name.ExtractText() ?? $"Unknown ({item.ItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty); + name = (this.itemSheet.GetRowOrDefault(item.BaseItemId)?.Name.ExtractText() ?? $"Unknown ({item.BaseItemId})") + (item.IsHq ? $" {SeIconChar.HighQuality.ToIconString()}" : string.Empty); count = item.Quantity; } else diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs index 4a6dd185f..32ac685a8 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/Steps/MarketBoardSelfTestStep.cs @@ -1,4 +1,4 @@ -using System.Globalization; +using System.Globalization; using System.Linq; using Dalamud.Game.MarketBoard; @@ -109,7 +109,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep } else { - ImGui.Text("Does this information match the purchase you made? This is testing the request to the server."); + ImGui.TextWrapped("Does this information match the purchase you made? This is testing the request to the server."); ImGui.Separator(); ImGui.Text($"Quantity: {this.marketBoardPurchaseRequest.ItemQuantity.ToString()}"); ImGui.Text($"Item ID: {this.marketBoardPurchaseRequest.CatalogId}"); @@ -135,7 +135,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep } else { - ImGui.Text("Does this information match the purchase you made? This is testing the response from the server."); + ImGui.TextWrapped("Does this information match the purchase you made? This is testing the response from the server."); ImGui.Separator(); ImGui.Text($"Quantity: {this.marketBoardPurchase.ItemQuantity.ToString()}"); ImGui.Text($"Item ID: {this.marketBoardPurchase.CatalogId}"); @@ -156,7 +156,7 @@ internal class MarketBoardSelfTestStep : ISelfTestStep case SubStep.Taxes: if (this.marketTaxRate == null) { - ImGui.Text("Goto a Retainer Vocate and talk to then. Click the 'View market tax rates' menu item."); + ImGui.TextWrapped("Goto a Retainer Vocate and talk to then. Click the 'View market tax rates' menu item."); } else { From 13306e24ba39e20e6ecebd3c65796a4d4a013f41 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:51:00 -0700 Subject: [PATCH 106/106] Refactor IAddonEventManager (#2299) --- Dalamud/Game/Addon/Events/AddonEventData.cs | 46 ++++++++++ Dalamud/Game/Addon/Events/AddonEventEntry.cs | 24 ++++-- .../Game/Addon/Events/AddonEventManager.cs | 67 ++++++++++----- .../Addon/Events/PluginEventController.cs | 85 ++++++++++++++++--- Dalamud/Plugin/Services/IAddonEventManager.cs | 19 +++++ 5 files changed, 199 insertions(+), 42 deletions(-) create mode 100644 Dalamud/Game/Addon/Events/AddonEventData.cs diff --git a/Dalamud/Game/Addon/Events/AddonEventData.cs b/Dalamud/Game/Addon/Events/AddonEventData.cs new file mode 100644 index 000000000..3a5c05660 --- /dev/null +++ b/Dalamud/Game/Addon/Events/AddonEventData.cs @@ -0,0 +1,46 @@ +namespace Dalamud.Game.Addon.Events; + +/// +/// Object representing data that is relevant in handling native events. +/// +public class AddonEventData +{ + /// + /// Gets the AtkEventType for this event. + /// + public AddonEventType AtkEventType { get; internal set; } + + /// + /// Gets the param field for this event. + /// + public uint Param { get; internal set; } + + /// + /// Gets the pointer to the AtkEvent object for this event. + /// + /// Note: This is not a pointer to the AtkEventData object.

+ /// Warning: AtkEvent->Node has been modified to be the AtkUnitBase*, and AtkEvent->Target has been modified to be the AtkResNode* that triggered this event. + public nint AtkEventPointer { get; internal set; } + + /// + /// Gets the pointer to the AtkEventData object for this event. + /// + /// This field will contain relevant data such as left vs right click, scroll up vs scroll down. + public nint AtkEventDataPointer { get; internal set; } + + /// + /// Gets the pointer to the AtkUnitBase that is handling this event. + /// + public nint AddonPointer { get; internal set; } + + /// + /// Gets the pointer to the AtkResNode that triggered this event. + /// + public nint NodeTargetPointer { get; internal set; } + + /// + /// Gets or sets a pointer to the AtkEventListener responsible for handling this event. + /// Note: As the event listener is dalamud allocated, there's no reason to expose this field. + /// + internal nint AtkEventListener { get; set; } +} diff --git a/Dalamud/Game/Addon/Events/AddonEventEntry.cs b/Dalamud/Game/Addon/Events/AddonEventEntry.cs index 8b5808087..50b9c7ec4 100644 --- a/Dalamud/Game/Addon/Events/AddonEventEntry.cs +++ b/Dalamud/Game/Addon/Events/AddonEventEntry.cs @@ -1,5 +1,6 @@ -using Dalamud.Memory; -using Dalamud.Plugin.Services; +using Dalamud.Plugin.Services; +using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Component.GUI; namespace Dalamud.Game.Addon.Events; @@ -14,9 +15,9 @@ internal unsafe class AddonEventEntry /// Name of an invalid addon. ///
public const string InvalidAddonName = "NullAddon"; - + private string? addonName; - + /// /// Gets the pointer to the addons AtkUnitBase. /// @@ -35,18 +36,25 @@ internal unsafe class AddonEventEntry /// /// Gets the handler that gets called when this event is triggered. /// - public required IAddonEventManager.AddonEventHandler Handler { get; init; } - + [Obsolete("Use AddonEventDelegate Delegate instead")] + public IAddonEventManager.AddonEventHandler Handler { get; init; } + + /// + /// Gets the delegate that gets called when this event is triggered. + /// + [Api13ToDo("Make this field required")] + public IAddonEventManager.AddonEventDelegate Delegate { get; init; } + /// /// Gets the unique id for this event. /// public required uint ParamKey { get; init; } - + /// /// Gets the event type for this event. /// public required AddonEventType EventType { get; init; } - + /// /// Gets the event handle for this event. /// diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index a7241dd58..0990c1f5f 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -24,21 +24,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService /// PluginName for Dalamud Internal use. ///
public static readonly Guid DalamudInternalKey = Guid.NewGuid(); - + private static readonly ModuleLog Log = new("AddonEventManager"); - + [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycle = Service.Get(); private readonly AddonLifecycleEventListener finalizeEventListener; - + private readonly AddonEventManagerAddressResolver address; private readonly Hook onUpdateCursor; private readonly ConcurrentDictionary pluginEventControllers; - + private AddonCursorType? cursorOverride; - + [ServiceManager.ServiceConstructor] private AddonEventManager(TargetSigScanner sigScanner) { @@ -47,7 +47,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService this.pluginEventControllers = new ConcurrentDictionary(); this.pluginEventControllers.TryAdd(DalamudInternalKey, new PluginEventController()); - + this.cursorOverride = null; this.onUpdateCursor = Hook.FromAddress(this.address.UpdateCursor, this.UpdateCursorDetour); @@ -69,7 +69,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService { pluginEventController.Dispose(); } - + this.addonLifecycle.UnregisterListener(this.finalizeEventListener); } @@ -92,7 +92,30 @@ internal unsafe class AddonEventManager : IInternalDisposableService { Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); } - + + return null; + } + + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// Unique ID for this plugin. + /// The parent addon for this event. + /// The node that will trigger this event. + /// The event type for this event. + /// The delegate to call when event is triggered. + /// IAddonEventHandle used to remove the event. + internal IAddonEventHandle? AddEvent(Guid pluginId, nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate) + { + if (this.pluginEventControllers.TryGetValue(pluginId, out var controller)) + { + return controller.AddEvent(atkUnitBase, atkResNode, eventType, eventDelegate); + } + else + { + Log.Verbose($"Unable to locate controller for {pluginId}. No event was added."); + } + return null; } @@ -112,7 +135,7 @@ internal unsafe class AddonEventManager : IInternalDisposableService Log.Verbose($"Unable to locate controller for {pluginId}. No event was removed."); } } - + /// /// Force the game cursor to be the specified cursor. /// @@ -167,21 +190,21 @@ internal unsafe class AddonEventManager : IInternalDisposableService pluginList.Value.RemoveForAddon(addonInfo.AddonName); } } - + private nint UpdateCursorDetour(RaptureAtkModule* module) { try { var atkStage = AtkStage.Instance(); - + if (this.cursorOverride is not null && atkStage is not null) { var cursor = (AddonCursorType)atkStage->AtkCursor.Type; - if (cursor != this.cursorOverride) + if (cursor != this.cursorOverride) { AtkStage.Instance()->AtkCursor.SetCursorType((AtkCursor.CursorType)this.cursorOverride, 1); } - + return nint.Zero; } } @@ -218,7 +241,7 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo public AddonEventManagerPluginScoped(LocalPlugin plugin) { this.plugin = plugin; - + this.eventManagerService.AddPluginEventController(plugin.EffectiveWorkingPluginId); } @@ -236,28 +259,32 @@ internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddo this.eventManagerService.RemovePluginEventController(this.plugin.EffectiveWorkingPluginId); }).Wait(); } - + /// - public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) + public IAddonEventHandle? AddEvent(IntPtr atkUnitBase, IntPtr atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventHandler eventHandler) => this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventHandler); + /// + public IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, IAddonEventManager.AddonEventDelegate eventDelegate) + => this.eventManagerService.AddEvent(this.plugin.EffectiveWorkingPluginId, atkUnitBase, atkResNode, eventType, eventDelegate); + /// public void RemoveEvent(IAddonEventHandle eventHandle) => this.eventManagerService.RemoveEvent(this.plugin.EffectiveWorkingPluginId, eventHandle); - + /// public void SetCursor(AddonCursorType cursor) { this.isForcingCursor = true; - + this.eventManagerService.SetCursor(cursor); } - + /// public void ResetCursor() { this.isForcingCursor = false; - + this.eventManagerService.ResetCursor(); } } diff --git a/Dalamud/Game/Addon/Events/PluginEventController.cs b/Dalamud/Game/Addon/Events/PluginEventController.cs index f32c7ad8f..0b1491e77 100644 --- a/Dalamud/Game/Addon/Events/PluginEventController.cs +++ b/Dalamud/Game/Addon/Events/PluginEventController.cs @@ -4,6 +4,7 @@ using System.Linq; using Dalamud.Game.Gui; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; +using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -25,7 +26,7 @@ internal unsafe class PluginEventController : IDisposable } private AddonEventListener EventListener { get; init; } - + private List Events { get; } = new(); /// @@ -36,6 +37,7 @@ internal unsafe class PluginEventController : IDisposable /// The Event Type. /// The delegate to call when invoking this event. /// IAddonEventHandle used to remove the event. + [Obsolete("Use AddEvent that uses AddonEventDelegate instead of AddonEventHandler")] public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventHandler handler) { var node = (AtkResNode*)atkResNode; @@ -43,7 +45,7 @@ internal unsafe class PluginEventController : IDisposable var eventType = (AtkEventType)atkEventType; var eventId = this.GetNextParamKey(); var eventGuid = Guid.NewGuid(); - + var eventHandle = new AddonEventHandle { AddonName = addon->NameString, @@ -51,11 +53,54 @@ internal unsafe class PluginEventController : IDisposable EventType = atkEventType, EventGuid = eventGuid, }; - + var eventEntry = new AddonEventEntry { Addon = atkUnitBase, Handler = handler, + Delegate = null, + Node = atkResNode, + EventType = atkEventType, + ParamKey = eventId, + Handle = eventHandle, + }; + + Log.Verbose($"Adding Event. {eventEntry.LogString}"); + this.EventListener.RegisterEvent(addon, node, eventType, eventId); + this.Events.Add(eventEntry); + + return eventHandle; + } + + /// + /// Adds a tracked event. + /// + /// The Parent addon for the event. + /// The Node for the event. + /// The Event Type. + /// The delegate to call when invoking this event. + /// IAddonEventHandle used to remove the event. + public IAddonEventHandle AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType atkEventType, IAddonEventManager.AddonEventDelegate eventDelegate) + { + 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 = addon->NameString, + ParamKey = eventId, + EventType = atkEventType, + EventGuid = eventGuid, + }; + + var eventEntry = new AddonEventEntry + { + Addon = atkUnitBase, + Delegate = eventDelegate, + Handler = null, Node = atkResNode, EventType = atkEventType, ParamKey = eventId, @@ -91,14 +136,14 @@ internal unsafe class PluginEventController : IDisposable if (this.Events.Where(entry => entry.AddonName == addonName).ToList() is { Count: not 0 } events) { Log.Verbose($"Addon: {addonName} is Finalizing, removing {events.Count} events."); - + foreach (var registeredEvent in events) { this.RemoveEvent(registeredEvent.Handle); } } } - + /// public void Dispose() { @@ -106,7 +151,7 @@ internal unsafe class PluginEventController : IDisposable { this.RemoveEvent(registeredEvent.Handle); } - + this.EventListener.Dispose(); } @@ -119,7 +164,7 @@ internal unsafe class PluginEventController : IDisposable throw new OverflowException($"uint.MaxValue number of ParamKeys used for this event controller."); } - + /// /// Attempts to remove a tracked event from native UI. /// This method performs several safety checks to only remove events from a still active addon. @@ -153,7 +198,7 @@ internal unsafe class PluginEventController : IDisposable break; } } - + // If we didn't find the node, we can't remove the event. if (!nodeFound) return; @@ -167,33 +212,45 @@ internal unsafe class PluginEventController : IDisposable var paramKeyMatches = currentEvent->Param == eventEntry.ParamKey; var eventListenerAddressMatches = (nint)currentEvent->Listener == this.EventListener.Address; var eventTypeMatches = currentEvent->State.EventType == 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); } - + + [Api13ToDo("Remove invoke from eventInfo.Handler, and remove nullability from eventInfo.Delegate?.Invoke")] private void PluginEventListHandler(AtkEventListener* self, AtkEventType eventType, uint eventParam, AtkEvent* eventPtr, AtkEventData* eventDataPtr) { try { if (eventPtr 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)eventPtr->Node, (nint)eventPtr->Target); + eventInfo.Handler?.Invoke((AddonEventType)eventType, (nint)eventPtr->Node, (nint)eventPtr->Target); + + eventInfo.Delegate?.Invoke((AddonEventType)eventType, new AddonEventData + { + AddonPointer = (nint)eventPtr->Node, + NodeTargetPointer = (nint)eventPtr->Target, + AtkEventDataPointer = (nint)eventDataPtr, + AtkEventListener = (nint)self, + AtkEventType = (AddonEventType)eventType, + Param = eventParam, + AtkEventPointer = (nint)eventPtr, + }); } catch (Exception exception) { diff --git a/Dalamud/Plugin/Services/IAddonEventManager.cs b/Dalamud/Plugin/Services/IAddonEventManager.cs index c6ec5a941..e534eafb4 100644 --- a/Dalamud/Plugin/Services/IAddonEventManager.cs +++ b/Dalamud/Plugin/Services/IAddonEventManager.cs @@ -13,8 +13,16 @@ public interface IAddonEventManager /// Event type for this event handler. /// The parent addon for this event handler. /// The specific node that will trigger this event handler. + [Obsolete("Use AddonEventDelegate instead")] public delegate void AddonEventHandler(AddonEventType atkEventType, nint atkUnitBase, nint atkResNode); + /// + /// Delegate to be called when an event is received. + /// + /// The AtkEventType that triggered this event. + /// The event data object for use in handling this event. + public delegate void AddonEventDelegate(AddonEventType atkEventType, AddonEventData data); + /// /// Registers an event handler for the specified addon, node, and type. /// @@ -23,8 +31,19 @@ public interface IAddonEventManager /// The event type for this event. /// The handler to call when event is triggered. /// IAddonEventHandle used to remove the event. Null if no event was added. + [Obsolete("Use AddEvent with AddonEventDelegate instead of AddonEventHandler")] IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventHandler eventHandler); + /// + /// Registers an event handler for the specified addon, node, and type. + /// + /// 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. + /// IAddonEventHandle used to remove the event. Null if no event was added. + IAddonEventHandle? AddEvent(nint atkUnitBase, nint atkResNode, AddonEventType eventType, AddonEventDelegate eventDelegate); + /// /// Unregisters an event handler with the specified event id and event type. ///