From 40e90a39c8250528eeb94191a026a821c8a1493c Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 22 Nov 2023 15:30:38 +0900 Subject: [PATCH 01/13] Add Dalamud.CorePlugin.json (#1533) --- Dalamud.CorePlugin/Dalamud.CorePlugin.csproj | 6 ++++++ Dalamud.CorePlugin/Dalamud.CorePlugin.json | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 Dalamud.CorePlugin/Dalamud.CorePlugin.json diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj index 67ca26dee..d7eb8499c 100644 --- a/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.csproj @@ -50,4 +50,10 @@ false + + + + Always + + diff --git a/Dalamud.CorePlugin/Dalamud.CorePlugin.json b/Dalamud.CorePlugin/Dalamud.CorePlugin.json new file mode 100644 index 000000000..7db669a73 --- /dev/null +++ b/Dalamud.CorePlugin/Dalamud.CorePlugin.json @@ -0,0 +1,9 @@ +{ + "Author": "Dalamud Maintainers", + "Name": "CorePlugin", + "Punchline": "Testbed for developing Dalamud features.", + "Description": "Develop and debug internal Dalamud features using CorePlugin. You have full access to all types in Dalamud assembly.", + "InternalName": "CorePlugin", + "ApplicableVersion": "any", + "Tags": [] +} From a80ab30b4cb83e48350f38d91850529a49fa1905 Mon Sep 17 00:00:00 2001 From: Ottermandias <70807659+Ottermandias@users.noreply.github.com> Date: Sat, 25 Nov 2023 21:24:08 +0100 Subject: [PATCH 02/13] Add a log message for each leaked hook with its address. (#1543) --- Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index 59f2d2684..9958385b9 100644 --- a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -91,6 +91,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT foreach (var hook in notDisposed) { + Log.Warning("\t\t\tLeaked hook at +0x{Address:X}", hook.Address.ToInt64() - this.scanner.Module.BaseAddress.ToInt64()); hook.Dispose(); } From a6ea4aa56a9a4ae528f2ff8f4e6dc38cc1e5c955 Mon Sep 17 00:00:00 2001 From: srkizer Date: Mon, 27 Nov 2023 06:55:43 +0900 Subject: [PATCH 03/13] Update .editorconfig for Resharper/Rider to conform with StyleCop rules (#1534) --- .editorconfig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 0ae30cf95..66e123f53 100644 --- a/.editorconfig +++ b/.editorconfig @@ -57,6 +57,7 @@ dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static,readonly +dotnet_separate_import_directive_groups = true dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:suggestion dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:suggestion dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:suggestion @@ -97,6 +98,7 @@ resharper_apply_on_completion = true resharper_auto_property_can_be_made_get_only_global_highlighting = none resharper_auto_property_can_be_made_get_only_local_highlighting = none resharper_autodetect_indent_settings = true +resharper_blank_lines_around_single_line_auto_property = 1 resharper_braces_for_ifelse = required_for_multiline resharper_can_use_global_alias = false resharper_csharp_align_multiline_parameter = true @@ -105,14 +107,22 @@ resharper_csharp_empty_block_style = multiline resharper_csharp_int_align_comments = true resharper_csharp_new_line_before_while = true resharper_csharp_wrap_after_declaration_lpar = true +resharper_csharp_wrap_after_invocation_lpar = true +resharper_csharp_wrap_arguments_style = chop_if_long resharper_enforce_line_ending_style = true +resharper_instance_members_qualify_declared_in = this_class, base_class resharper_member_can_be_private_global_highlighting = none resharper_member_can_be_private_local_highlighting = none -resharper_new_line_before_finally = false +resharper_new_line_before_finally = true +resharper_parentheses_non_obvious_operations = none, multiplicative, additive, arithmetic, shift, bitwise_and, bitwise_exclusive_or, bitwise_inclusive_or, bitwise +resharper_parentheses_redundancy_style = remove_if_not_clarifies_precedence resharper_place_accessorholder_attribute_on_same_line = false resharper_place_field_attribute_on_same_line = false +resharper_place_simple_initializer_on_single_line = true resharper_show_autodetect_configure_formatting_tip = false +resharper_space_within_single_line_array_initializer_braces = true resharper_use_indent_from_vs = false +resharper_wrap_array_initializer_style = chop_if_long # ReSharper inspection severities resharper_arrange_missing_parentheses_highlighting = hint From 7a0de45f872a1347ab64c3d64cafdadc423d6271 Mon Sep 17 00:00:00 2001 From: srkizer Date: Mon, 27 Nov 2023 06:58:26 +0900 Subject: [PATCH 04/13] Miscellaneous improvements (#1537) --- .../Internal/DalamudConfiguration.cs | 18 +- Dalamud/Interface/GameFonts/FdtReader.cs | 44 +- .../Interface/GameFonts/GameFontManager.cs | 4 +- Dalamud/Interface/GameFonts/GameFontStyle.cs | 152 ++-- .../Interface/Internal/InterfaceManager.cs | 12 +- .../Internal/Windows/Data/DataWindow.cs | 54 +- .../Windows/Data/IDataWindowWidget.cs | 5 +- .../Data/Widgets/AddonLifecycleWidget.cs | 10 +- .../Data/Widgets/FontAwesomeTestWidget.cs | 24 +- .../Internal/Windows/ProfilerWindow.cs | 23 +- .../Windows/Settings/Tabs/SettingsTabLook.cs | 77 +- Dalamud/Interface/Utility/ImGuiHelpers.cs | 288 ++++++-- Dalamud/Interface/Utility/ImVectorWrapper.cs | 687 ++++++++++++++++++ Dalamud/Logging/Internal/ModuleLog.cs | 16 +- Dalamud/NativeFunctions.cs | 1 + 15 files changed, 1136 insertions(+), 279 deletions(-) create mode 100644 Dalamud/Interface/Utility/ImVectorWrapper.cs diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 1274744c6..35d5261da 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -34,7 +35,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable }; [JsonIgnore] - private string configPath; + private string? configPath; [JsonIgnore] private bool isSaveQueued; @@ -48,12 +49,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Event that occurs when dalamud configuration is saved. /// - public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved; + public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved; /// /// Gets or sets a list of muted works. /// - public List BadWords { get; set; } + public List? BadWords { get; set; } /// /// Gets or sets a value indicating whether or not the taskbar should flash once a duty is found. @@ -68,12 +69,12 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets the language code to load Dalamud localization with. /// - public string LanguageOverride { get; set; } = null; + public string? LanguageOverride { get; set; } = null; /// /// Gets or sets the last loaded Dalamud version. /// - public string LastVersion { get; set; } = null; + public string? LastVersion { get; set; } = null; /// /// Gets or sets a value indicating the last seen FTUE version. @@ -84,7 +85,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets the last loaded Dalamud version. /// - public string LastChangelogMajorMinor { get; set; } = null; + public string? LastChangelogMajorMinor { get; set; } = null; /// /// Gets or sets the chat type used by default for plugin messages. @@ -229,6 +230,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// Gets or sets a value indicating whether or not plugin user interfaces should trigger sound effects. /// This setting is effected by the in-game "System Sounds" option and volume. /// + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")] public bool EnablePluginUISoundEffects { get; set; } /// @@ -266,7 +268,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable /// /// Gets or sets the kind of beta to download when matches the server value. /// - public string DalamudBetaKind { get; set; } + public string? DalamudBetaKind { get; set; } /// /// Gets or sets a value indicating whether or not any plugin should be loaded when the game is started. @@ -514,6 +516,8 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable private void Save() { ThreadSafety.AssertMainThread(); + if (this.configPath is null) + throw new InvalidOperationException("configPath is not set."); Service.Get().WriteAllText( this.configPath, JsonConvert.SerializeObject(this, SerializerSettings)); diff --git a/Dalamud/Interface/GameFonts/FdtReader.cs b/Dalamud/Interface/GameFonts/FdtReader.cs index a68caba94..0e8f3fb59 100644 --- a/Dalamud/Interface/GameFonts/FdtReader.cs +++ b/Dalamud/Interface/GameFonts/FdtReader.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Runtime.InteropServices; @@ -22,7 +21,7 @@ public class FdtReader for (var i = 0; i < this.FontHeader.FontTableEntryCount; i++) this.Glyphs.Add(StructureFromByteArray(data, this.FileHeader.FontTableHeaderOffset + Marshal.SizeOf() + (Marshal.SizeOf() * i))); - for (int i = 0, i_ = Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); i < i_; i++) + for (int i = 0, to = Math.Min(this.FontHeader.KerningTableEntryCount, this.KerningHeader.Count); i < to; i++) this.Distances.Add(StructureFromByteArray(data, this.FileHeader.KerningTableHeaderOffset + Marshal.SizeOf() + (Marshal.SizeOf() * i))); } @@ -51,6 +50,14 @@ public class FdtReader /// public List Distances { get; init; } = new(); + /// + /// Finds the glyph index for the corresponding codepoint. + /// + /// Unicode codepoint (UTF-32 value). + /// Corresponding index, or a negative number according to . + public int FindGlyphIndex(int codepoint) => + this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8Int32(codepoint) }); + /// /// Finds glyph definition for corresponding codepoint. /// @@ -58,7 +65,7 @@ public class FdtReader /// Corresponding FontTableEntry, or null if not found. public FontTableEntry? FindGlyph(int codepoint) { - var i = this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8Int32(codepoint) }); + var i = this.FindGlyphIndex(codepoint); if (i < 0 || i == this.Glyphs.Count) return null; return this.Glyphs[i]; @@ -91,17 +98,12 @@ public class FdtReader return this.Distances[i].RightOffset; } - private static unsafe T StructureFromByteArray(byte[] data, int offset) - { - var len = Marshal.SizeOf(); - if (offset + len > data.Length) - throw new Exception("Data too short"); - - fixed (byte* ptr = data) - return Marshal.PtrToStructure(new(ptr + offset)); - } - - private static int CodePointToUtf8Int32(int codepoint) + /// + /// Translates a UTF-32 codepoint to a containing a UTF-8 character. + /// + /// The codepoint. + /// The uint. + internal static int CodePointToUtf8Int32(int codepoint) { if (codepoint <= 0x7F) { @@ -131,6 +133,16 @@ public class FdtReader } } + private static unsafe T StructureFromByteArray(byte[] data, int offset) + { + var len = Marshal.SizeOf(); + if (offset + len > data.Length) + throw new Exception("Data too short"); + + fixed (byte* ptr = data) + return Marshal.PtrToStructure(new(ptr + offset)); + } + private static int Utf8Uint32ToCodePoint(int n) { if ((n & 0xFFFFFF80) == 0) @@ -252,7 +264,7 @@ public class FdtReader /// Glyph table entry. /// [StructLayout(LayoutKind.Sequential)] - public unsafe struct FontTableEntry : IComparable + public struct FontTableEntry : IComparable { /// /// Mapping of texture channel index to byte index. @@ -367,7 +379,7 @@ public class FdtReader /// Kerning table entry. /// [StructLayout(LayoutKind.Sequential)] - public unsafe struct KerningTableEntry : IComparable + public struct KerningTableEntry : IComparable { /// /// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the left character. diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index a7cd27b83..71661682d 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -257,7 +257,7 @@ internal class GameFontManager : IServiceType /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(ImFontPtr? source, GameFontStyle target, bool missingOnly, bool rebuildLookupTable) { - ImGuiHelpers.CopyGlyphsAcrossFonts(source, this.fonts[target], missingOnly, rebuildLookupTable); + ImGuiHelpers.CopyGlyphsAcrossFonts(source ?? default, this.fonts[target], missingOnly, rebuildLookupTable); } /// @@ -269,7 +269,7 @@ internal class GameFontManager : IServiceType /// Whether to call target.BuildLookupTable(). public void CopyGlyphsAcrossFonts(GameFontStyle source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable) { - ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target, missingOnly, rebuildLookupTable); + ImGuiHelpers.CopyGlyphsAcrossFonts(this.fonts[source], target ?? default, missingOnly, rebuildLookupTable); } /// diff --git a/Dalamud/Interface/GameFonts/GameFontStyle.cs b/Dalamud/Interface/GameFonts/GameFontStyle.cs index 40b810161..946473df4 100644 --- a/Dalamud/Interface/GameFonts/GameFontStyle.cs +++ b/Dalamud/Interface/GameFonts/GameFontStyle.cs @@ -1,5 +1,3 @@ -using System; - namespace Dalamud.Interface.GameFonts; /// @@ -153,7 +151,7 @@ public struct GameFontStyle GameFontFamilyAndSize.TrumpGothic184 => 18.4f, GameFontFamilyAndSize.TrumpGothic23 => 23, GameFontFamilyAndSize.TrumpGothic34 => 34, - GameFontFamilyAndSize.TrumpGothic68 => 8, + GameFontFamilyAndSize.TrumpGothic68 => 68, _ => throw new InvalidOperationException(), }; @@ -186,77 +184,77 @@ public struct GameFontStyle /// Font family. /// Font size in points. /// Recommended GameFontFamilyAndSize. - public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size) - { - if (size <= 0) - return GameFontFamilyAndSize.Undefined; - - switch (family) + public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size) => + family switch { - case GameFontFamily.Undefined: - return GameFontFamilyAndSize.Undefined; + _ when size <= 0 => GameFontFamilyAndSize.Undefined, + GameFontFamily.Undefined => GameFontFamilyAndSize.Undefined, + GameFontFamily.Axis => size switch + { + <= ((int)((9.6f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis96, + <= ((int)((12f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis12, + <= ((int)((14f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis14, + <= ((int)((18f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Axis18, + _ => GameFontFamilyAndSize.Axis36, + }, + GameFontFamily.Jupiter => size switch + { + <= ((int)((16f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter16, + <= ((int)((20f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter20, + <= ((int)((23f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter23, + _ => GameFontFamilyAndSize.Jupiter46, + }, + GameFontFamily.JupiterNumeric => size switch + { + <= ((int)((45f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Jupiter45, + _ => GameFontFamilyAndSize.Jupiter90, + }, + GameFontFamily.Meidinger => size switch + { + <= ((int)((16f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Meidinger16, + <= ((int)((20f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.Meidinger20, + _ => GameFontFamilyAndSize.Meidinger40, + }, + GameFontFamily.MiedingerMid => size switch + { + <= ((int)((10f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid10, + <= ((int)((12f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid12, + <= ((int)((14f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid14, + <= ((int)((18f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.MiedingerMid18, + _ => GameFontFamilyAndSize.MiedingerMid36, + }, + GameFontFamily.TrumpGothic => size switch + { + <= ((int)((18.4f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.TrumpGothic184, + <= ((int)((23f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.TrumpGothic23, + <= ((int)((34f * 4f / 3f) + 0.5f) * 3f / 4f) + 0.001f => GameFontFamilyAndSize.TrumpGothic34, + _ => GameFontFamilyAndSize.TrumpGothic68, + }, + _ => GameFontFamilyAndSize.Undefined, + }; - case GameFontFamily.Axis: - if (size <= 9.601) - return GameFontFamilyAndSize.Axis96; - else if (size <= 12.001) - return GameFontFamilyAndSize.Axis12; - else if (size <= 14.001) - return GameFontFamilyAndSize.Axis14; - else if (size <= 18.001) - return GameFontFamilyAndSize.Axis18; - else - return GameFontFamilyAndSize.Axis36; - - case GameFontFamily.Jupiter: - if (size <= 16.001) - return GameFontFamilyAndSize.Jupiter16; - else if (size <= 20.001) - return GameFontFamilyAndSize.Jupiter20; - else if (size <= 23.001) - return GameFontFamilyAndSize.Jupiter23; - else - return GameFontFamilyAndSize.Jupiter46; - - case GameFontFamily.JupiterNumeric: - if (size <= 45.001) - return GameFontFamilyAndSize.Jupiter45; - else - return GameFontFamilyAndSize.Jupiter90; - - case GameFontFamily.Meidinger: - if (size <= 16.001) - return GameFontFamilyAndSize.Meidinger16; - else if (size <= 20.001) - return GameFontFamilyAndSize.Meidinger20; - else - return GameFontFamilyAndSize.Meidinger40; - - case GameFontFamily.MiedingerMid: - if (size <= 10.001) - return GameFontFamilyAndSize.MiedingerMid10; - else if (size <= 12.001) - return GameFontFamilyAndSize.MiedingerMid12; - else if (size <= 14.001) - return GameFontFamilyAndSize.MiedingerMid14; - else if (size <= 18.001) - return GameFontFamilyAndSize.MiedingerMid18; - else - return GameFontFamilyAndSize.MiedingerMid36; - - case GameFontFamily.TrumpGothic: - if (size <= 18.401) - return GameFontFamilyAndSize.TrumpGothic184; - else if (size <= 23.001) - return GameFontFamilyAndSize.TrumpGothic23; - else if (size <= 34.001) - return GameFontFamilyAndSize.TrumpGothic34; - else - return GameFontFamilyAndSize.TrumpGothic68; - - default: - return GameFontFamilyAndSize.Undefined; + /// + /// Calculates the adjustment to width resulting fron Weight and SkewStrength. + /// + /// Font header. + /// Glyph. + /// Width adjustment in pixel unit. + public int CalculateBaseWidthAdjustment(in FdtReader.FontTableHeader header, in FdtReader.FontTableEntry glyph) + { + var widthDelta = this.Weight; + switch (this.BaseSkewStrength) + { + case > 0: + widthDelta += (1f * this.BaseSkewStrength * (header.LineHeight - glyph.CurrentOffsetY)) + / header.LineHeight; + break; + case < 0: + widthDelta -= (1f * this.BaseSkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight)) + / header.LineHeight; + break; } + + return (int)MathF.Ceiling(widthDelta); } /// @@ -265,16 +263,8 @@ public struct GameFontStyle /// Font information. /// Glyph. /// Width adjustment in pixel unit. - public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) - { - var widthDelta = this.Weight; - if (this.BaseSkewStrength > 0) - widthDelta += 1f * this.BaseSkewStrength * (reader.FontHeader.LineHeight - glyph.CurrentOffsetY) / reader.FontHeader.LineHeight; - else if (this.BaseSkewStrength < 0) - widthDelta -= 1f * this.BaseSkewStrength * (glyph.CurrentOffsetY + glyph.BoundingHeight) / reader.FontHeader.LineHeight; - - return (int)Math.Ceiling(widthDelta); - } + public int CalculateBaseWidthAdjustment(FdtReader reader, FdtReader.FontTableEntry glyph) => + this.CalculateBaseWidthAdjustment(reader.FontHeader, glyph); /// public override string ToString() diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index d5394fe8d..9de87c6e3 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -52,8 +52,16 @@ namespace Dalamud.Interface.Internal; [ServiceManager.BlockingEarlyLoadedService] internal class InterfaceManager : IDisposable, IServiceType { - private const float DefaultFontSizePt = 12.0f; - private const float DefaultFontSizePx = DefaultFontSizePt * 4.0f / 3.0f; + /// + /// The default font size, in points. + /// + public const float DefaultFontSizePt = 12.0f; + + /// + /// The default font size, in pixels. + /// + public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; + private const ushort Fallback1Codepoint = 0x3013; // Geta mark; FFXIV uses this to indicate that a glyph is missing. private const ushort Fallback2Codepoint = '-'; // FFXIV uses dash if Geta mark is unavailable. diff --git a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs index d59b50e58..e9d4152a5 100644 --- a/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Data/DataWindow.cs @@ -18,39 +18,39 @@ internal class DataWindow : Window { private readonly IDataWindowWidget[] modules = { - new ServicesWidget(), - new AddressesWidget(), - new ObjectTableWidget(), - new FateTableWidget(), - new SeFontTestWidget(), - new FontAwesomeTestWidget(), - new PartyListWidget(), - new BuddyListWidget(), - new PluginIpcWidget(), - new ConditionWidget(), - new GaugeWidget(), - new CommandWidget(), - new AddonWidget(), new AddonInspectorWidget(), + new AddonLifecycleWidget(), + new AddonWidget(), + new AddressesWidget(), + new AetherytesWidget(), new AtkArrayDataBrowserWidget(), + new BuddyListWidget(), + new CommandWidget(), + new ConditionWidget(), + new ConfigurationWidget(), + new DataShareWidget(), + new DtrBarWidget(), + new FateTableWidget(), + new FlyTextWidget(), + new FontAwesomeTestWidget(), + new GamepadWidget(), + new GaugeWidget(), + new HookWidget(), + new IconBrowserWidget(), + new ImGuiWidget(), + new KeyStateWidget(), + new NetworkMonitorWidget(), + new ObjectTableWidget(), + new PartyListWidget(), + new PluginIpcWidget(), + new SeFontTestWidget(), + new ServicesWidget(), new StartInfoWidget(), new TargetWidget(), - new ToastWidget(), - new FlyTextWidget(), - new ImGuiWidget(), - new TexWidget(), - new KeyStateWidget(), - new GamepadWidget(), - new ConfigurationWidget(), new TaskSchedulerWidget(), - new HookWidget(), - new AetherytesWidget(), - new DtrBarWidget(), + new TexWidget(), + new ToastWidget(), new UIColorWidget(), - new DataShareWidget(), - new NetworkMonitorWidget(), - new IconBrowserWidget(), - new AddonLifecycleWidget(), }; private readonly IOrderedEnumerable orderedModules; diff --git a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs index 0e12e4c51..78df015ed 100644 --- a/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/IDataWindowWidget.cs @@ -1,7 +1,6 @@ -using System; -using System.Linq; +using System.Linq; -namespace Dalamud.Interface.Internal.Windows; +namespace Dalamud.Interface.Internal.Windows.Data; /// /// Class representing a date window entry. diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 7dc8e2f3c..0e654d316 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -28,8 +28,14 @@ public class AddonLifecycleWidget : IDataWindowWidget /// public void Load() { - this.AddonLifecycle = Service.GetNullable(); - if (this.AddonLifecycle is not null) this.Ready = true; + Service + .GetAsync() + .ContinueWith( + r => + { + this.AddonLifecycle = r.Result; + this.Ready = true; + }); } /// diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs index 26bd2e623..22f615e8a 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/FontAwesomeTestWidget.cs @@ -38,12 +38,30 @@ internal class FontAwesomeTestWidget : IDataWindowWidget public void Draw() { ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, Vector2.Zero); - - this.iconCategories ??= FontAwesomeHelpers.GetCategories(); + + this.iconCategories ??= new[] { "(Show All)", "(Undefined)" } + .Concat(FontAwesomeHelpers.GetCategories().Skip(1)) + .ToArray(); if (this.iconSearchChanged) { - this.icons = FontAwesomeHelpers.SearchIcons(this.iconSearchInput, this.iconCategories[this.selectedIconCategory]); + if (this.iconSearchInput == string.Empty && this.selectedIconCategory <= 1) + { + var en = InterfaceManager.IconFont.GlyphsWrapped() + .Select(x => (FontAwesomeIcon)x.Codepoint) + .Where(x => (ushort)x is >= 0xE000 and < 0xF000); + en = this.selectedIconCategory == 0 + ? en.Concat(FontAwesomeHelpers.SearchIcons(string.Empty, string.Empty)) + : en.Except(FontAwesomeHelpers.SearchIcons(string.Empty, string.Empty)); + this.icons = en.Distinct().Order().ToList(); + } + else + { + this.icons = FontAwesomeHelpers.SearchIcons( + this.iconSearchInput, + this.selectedIconCategory <= 1 ? string.Empty : this.iconCategories[this.selectedIconCategory]); + } + this.iconNames = this.icons.Select(icon => Enum.GetName(icon)!).ToList(); this.iconSearchChanged = false; } diff --git a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs index 16f253da9..cd653143b 100644 --- a/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ProfilerWindow.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -46,7 +45,7 @@ public class ProfilerWindow : Window ImGui.Text("Timings"); - var childHeight = Math.Max(300, 20 * (2 + this.occupied.Count)); + var childHeight = Math.Max(300, 20 * (2.5f + this.occupied.Count)); if (ImGui.BeginChild("Timings", new Vector2(0, childHeight), true)) { @@ -115,7 +114,7 @@ public class ProfilerWindow : Window parentDepthDict[timingHandle.Id] = depth; startX = Math.Max(startX, 0); - endX = Math.Max(endX, 0); + endX = Math.Max(endX, startX + (ImGuiHelpers.GlobalScale * 16)); Vector4 rectColor; if (this.occupied[depth].Count % 2 == 0) @@ -129,11 +128,6 @@ public class ProfilerWindow : Window if (maxRectDept < depth) maxRectDept = (uint)depth; - if (startX == endX) - { - continue; - } - var minPos = pos + new Vector2((uint)startX, 20 * depth); var maxPos = pos + new Vector2((uint)endX, 20 * (depth + 1)); @@ -231,22 +225,22 @@ public class ProfilerWindow : Window ImGui.EndChild(); var sliderMin = (float)this.min / 1000f; - if (ImGui.SliderFloat("Start", ref sliderMin, (float)actualMin / 1000f, (float)this.max / 1000f, "%.1fs")) + if (ImGui.SliderFloat("Start", ref sliderMin, (float)actualMin / 1000f, (float)this.max / 1000f, "%.2fs")) { this.min = sliderMin * 1000f; } var sliderMax = (float)this.max / 1000f; - if (ImGui.SliderFloat("End", ref sliderMax, (float)this.min / 1000f, (float)actualMax / 1000f, "%.1fs")) + if (ImGui.SliderFloat("End", ref sliderMax, (float)this.min / 1000f, (float)actualMax / 1000f, "%.2fs")) { this.max = sliderMax * 1000f; } - var sizeShown = (float)(this.max - this.min); - var sizeActual = (float)(actualMax - actualMin); - if (ImGui.SliderFloat("Size", ref sizeShown, sizeActual / 10f, sizeActual, "%.1fs")) + var sizeShown = (float)(this.max - this.min) / 1000f; + var sizeActual = (float)(actualMax - actualMin) / 1000f; + if (ImGui.SliderFloat("Size", ref sizeShown, sizeActual / 10f, sizeActual, "%.2fs")) { - this.max = this.min + sizeShown; + this.max = this.min + (sizeShown * 1000f); } ImGui.Text("Min: " + actualMin.ToString("0.000")); @@ -257,6 +251,7 @@ public class ProfilerWindow : Window [SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1401:Fields should be private", Justification = "Internals")] private class RectInfo { + // ReSharper disable once NotNullOrRequiredMemberIsNotInitialized <- well you're wrong internal TimingHandle Timing; internal Vector2 MinPos; internal Vector2 MaxPos; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs index 7a6f894c1..02e8ce789 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabLook.cs @@ -1,5 +1,6 @@ -using System; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Numerics; using CheapLoc; using Dalamud.Configuration.Internal; @@ -16,6 +17,16 @@ namespace Dalamud.Interface.Internal.Windows.Settings.Tabs; [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "Internals")] public class SettingsTabLook : SettingsTab { + private static readonly (string, float)[] GlobalUiScalePresets = + { + ("9.6pt##DalamudSettingsGlobalUiScaleReset96", 9.6f / InterfaceManager.DefaultFontSizePt), + ("12pt##DalamudSettingsGlobalUiScaleReset12", 12f / InterfaceManager.DefaultFontSizePt), + ("14pt##DalamudSettingsGlobalUiScaleReset14", 14f / InterfaceManager.DefaultFontSizePt), + ("18pt##DalamudSettingsGlobalUiScaleReset18", 18f / InterfaceManager.DefaultFontSizePt), + ("24pt##DalamudSettingsGlobalUiScaleReset24", 24f / InterfaceManager.DefaultFontSizePt), + ("36pt##DalamudSettingsGlobalUiScaleReset36", 36f / InterfaceManager.DefaultFontSizePt), + }; + private float globalUiScale; private float fontGamma; @@ -135,55 +146,22 @@ public class SettingsTabLook : SettingsTab { var interfaceManager = Service.Get(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); + ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsGlobalUiScale", "Global Font Scale")); - ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3); - if (ImGui.Button("9.6pt##DalamudSettingsGlobalUiScaleReset96")) - { - this.globalUiScale = 9.6f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - ImGui.SameLine(); - if (ImGui.Button("12pt##DalamudSettingsGlobalUiScaleReset12")) + var buttonSize = + GlobalUiScalePresets + .Select(x => ImGui.CalcTextSize(x.Item1, 0, x.Item1.IndexOf('#'))) + .Aggregate(Vector2.Zero, Vector2.Max) + + (ImGui.GetStyle().FramePadding * 2); + foreach (var (buttonLabel, scale) in GlobalUiScalePresets) { - this.globalUiScale = 1.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("14pt##DalamudSettingsGlobalUiScaleReset14")) - { - this.globalUiScale = 14.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("18pt##DalamudSettingsGlobalUiScaleReset18")) - { - this.globalUiScale = 18.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("24pt##DalamudSettingsGlobalUiScaleReset24")) - { - this.globalUiScale = 24.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); - } - - ImGui.SameLine(); - if (ImGui.Button("36pt##DalamudSettingsGlobalUiScaleReset36")) - { - this.globalUiScale = 36.0f / 12.0f; - ImGui.GetIO().FontGlobalScale = this.globalUiScale; - interfaceManager.RebuildFonts(); + ImGui.SameLine(); + if (ImGui.Button(buttonLabel, buttonSize) && Math.Abs(this.globalUiScale - scale) > float.Epsilon) + { + ImGui.GetIO().FontGlobalScale = this.globalUiScale = scale; + interfaceManager.RebuildFonts(); + } } var globalUiScaleInPt = 12f * this.globalUiScale; @@ -198,10 +176,9 @@ public class SettingsTabLook : SettingsTab ImGuiHelpers.ScaledDummy(5); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); + ImGui.AlignTextToFramePadding(); ImGui.Text(Loc.Localize("DalamudSettingsFontGamma", "Font Gamma")); ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 3); if (ImGui.Button(Loc.Localize("DalamudSettingsIndividualConfigResetToDefaultValue", "Reset") + "##DalamudSettingsFontGammaReset")) { this.fontGamma = 1.4f; diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index d11e1ce1c..579d93f86 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -1,8 +1,9 @@ using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Utility.Raii; using ImGuiNET; @@ -25,6 +26,20 @@ public static class ImGuiHelpers /// public static float GlobalScale { get; private set; } + /// + /// Gets a value indicating whether ImGui is initialized and ready for use.
+ /// This does not necessarily mean you can call drawing functions. + ///
+ public static unsafe bool IsImGuiInitialized => + ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null; + + /// + /// Gets the global Dalamud scale; even available before drawing is ready.
+ /// If you are sure that drawing is ready, at the point of using this, use instead. + ///
+ public static float GlobalScaleSafe => + IsImGuiInitialized ? ImGui.GetIO().FontGlobalScale : Service.Get().GlobalUiScale; + /// /// Check if the current ImGui window is on the main viewport. /// Only valid within a window. @@ -174,6 +189,47 @@ public static class ImGuiHelpers } } + /// + /// Unscales fonts after they have been rendered onto atlas. + /// + /// Font to scale. + /// Scale. + /// If a positive number is given, numbers will be rounded to this. + public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f) + { + Func rounder = round > 0 ? x => MathF.Round(x * round) / round : x => x; + + var font = fontPtr.NativePtr; + font->FontSize = rounder(font->FontSize * scale); + font->Ascent = rounder(font->Ascent * scale); + font->Descent = font->FontSize - font->Ascent; + if (font->ConfigData != null) + font->ConfigData->SizePixels = rounder(font->ConfigData->SizePixels * scale); + + foreach (ref var glyphHotDataReal in new Span( + (void*)font->IndexedHotData.Data, + font->IndexedHotData.Size)) + { + glyphHotDataReal.AdvanceX = rounder(glyphHotDataReal.AdvanceX * scale); + glyphHotDataReal.OccupiedWidth = rounder(glyphHotDataReal.OccupiedWidth * scale); + } + + foreach (ref var glyphReal in new Span((void*)font->Glyphs.Data, font->Glyphs.Size)) + { + glyphReal.X0 *= scale; + glyphReal.X1 *= scale; + glyphReal.Y0 *= scale; + glyphReal.Y1 *= scale; + glyphReal.AdvanceX = rounder(glyphReal.AdvanceX * scale); + } + + foreach (ref var kp in new Span((void*)font->KerningPairs.Data, font->KerningPairs.Size)) + kp.AdvanceXAdjustment = rounder(kp.AdvanceXAdjustment * scale); + + foreach (ref var fkp in new Span((void*)font->FrequentKerningPairs.Data, font->FrequentKerningPairs.Size)) + fkp = rounder(fkp * scale); + } + /// /// Fills missing glyphs in target font from source font, if both are not null. /// @@ -183,71 +239,110 @@ public static class ImGuiHelpers /// Whether to call target.BuildLookupTable(). /// Low codepoint range to copy. /// High codepoing range to copy. - public static void CopyGlyphsAcrossFonts(ImFontPtr? source, ImFontPtr? target, bool missingOnly, bool rebuildLookupTable, int rangeLow = 32, int rangeHigh = 0xFFFE) + [Obsolete("Use the non-nullable variant.", true)] + public static void CopyGlyphsAcrossFonts( + ImFontPtr? source, + ImFontPtr? target, + bool missingOnly, + bool rebuildLookupTable = true, + int rangeLow = 32, + int rangeHigh = 0xFFFE) => + CopyGlyphsAcrossFonts( + source ?? default, + target ?? default, + missingOnly, + rebuildLookupTable, + rangeLow, + rangeHigh); + + /// + /// Fills missing glyphs in target font from source font, if both are not null. + /// + /// Source font. + /// Target font. + /// Whether to copy missing glyphs only. + /// Whether to call target.BuildLookupTable(). + /// Low codepoint range to copy. + /// High codepoing range to copy. + public static unsafe void CopyGlyphsAcrossFonts( + ImFontPtr source, + ImFontPtr target, + bool missingOnly, + bool rebuildLookupTable = true, + int rangeLow = 32, + int rangeHigh = 0xFFFE) { - if (!source.HasValue || !target.HasValue) + if (!source.IsNotNullAndLoaded() || !target.IsNotNullAndLoaded()) return; - var scale = target.Value!.FontSize / source.Value!.FontSize; + var changed = false; + var scale = target.FontSize / source.FontSize; var addedCodepoints = new HashSet(); - unsafe + + if (source.Glyphs.Size == 0) + return; + + var glyphs = (ImFontGlyphReal*)source.Glyphs.Data; + if (glyphs is null) + throw new InvalidOperationException("Glyphs data is empty but size is >0?"); + + for (int j = 0, k = source.Glyphs.Size; j < k; j++) { - var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data; - for (int j = 0, k = source.Value!.Glyphs.Size; j < k; j++) + var glyph = &glyphs![j]; + if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) + continue; + + var prevGlyphPtr = (ImFontGlyphReal*)target.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; + if ((IntPtr)prevGlyphPtr == IntPtr.Zero) { - Debug.Assert(glyphs != null, nameof(glyphs) + " != null"); - - var glyph = &glyphs[j]; - if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh) - continue; - - var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr; - if ((IntPtr)prevGlyphPtr == IntPtr.Zero) - { - addedCodepoints.Add(glyph->Codepoint); - target.Value!.AddGlyph( - target.Value!.ConfigData, - (ushort)glyph->Codepoint, - glyph->TextureIndex, - glyph->X0 * scale, - ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->X1 * scale, - ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent, - glyph->U0, - glyph->V0, - glyph->U1, - glyph->V1, - glyph->AdvanceX * scale); - } - else if (!missingOnly) - { - addedCodepoints.Add(glyph->Codepoint); - prevGlyphPtr->TextureIndex = glyph->TextureIndex; - prevGlyphPtr->X0 = glyph->X0 * scale; - prevGlyphPtr->Y0 = ((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->X1 = glyph->X1 * scale; - prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent; - prevGlyphPtr->U0 = glyph->U0; - prevGlyphPtr->V0 = glyph->V0; - prevGlyphPtr->U1 = glyph->U1; - prevGlyphPtr->V1 = glyph->V1; - prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; - } + addedCodepoints.Add(glyph->Codepoint); + target.AddGlyph( + target.ConfigData, + (ushort)glyph->Codepoint, + glyph->TextureIndex, + glyph->X0 * scale, + ((glyph->Y0 - source.Ascent) * scale) + target.Ascent, + glyph->X1 * scale, + ((glyph->Y1 - source.Ascent) * scale) + target.Ascent, + glyph->U0, + glyph->V0, + glyph->U1, + glyph->V1, + glyph->AdvanceX * scale); + changed = true; } - - var kernPairs = source.Value!.KerningPairs; - for (int j = 0, k = kernPairs.Size; j < k; j++) + else if (!missingOnly) { - if (!addedCodepoints.Contains(kernPairs[j].Left)) - continue; - if (!addedCodepoints.Contains(kernPairs[j].Right)) - continue; - target.Value.AddKerningPair(kernPairs[j].Left, kernPairs[j].Right, kernPairs[j].AdvanceXAdjustment); + addedCodepoints.Add(glyph->Codepoint); + prevGlyphPtr->TextureIndex = glyph->TextureIndex; + prevGlyphPtr->X0 = glyph->X0 * scale; + prevGlyphPtr->Y0 = ((glyph->Y0 - source.Ascent) * scale) + target.Ascent; + prevGlyphPtr->X1 = glyph->X1 * scale; + prevGlyphPtr->Y1 = ((glyph->Y1 - source.Ascent) * scale) + target.Ascent; + prevGlyphPtr->U0 = glyph->U0; + prevGlyphPtr->V0 = glyph->V0; + prevGlyphPtr->U1 = glyph->U1; + prevGlyphPtr->V1 = glyph->V1; + prevGlyphPtr->AdvanceX = glyph->AdvanceX * scale; } } - if (rebuildLookupTable && target.Value!.Glyphs.Size > 0) - target.Value!.BuildLookupTableNonstandard(); + if (target.Glyphs.Size == 0) + return; + + var kernPairs = source.KerningPairs; + for (int j = 0, k = kernPairs.Size; j < k; j++) + { + if (!addedCodepoints.Contains(kernPairs[j].Left)) + continue; + if (!addedCodepoints.Contains(kernPairs[j].Right)) + continue; + target.AddKerningPair(kernPairs[j].Left, kernPairs[j].Right, kernPairs[j].AdvanceXAdjustment); + changed = true; + } + + if (changed && rebuildLookupTable) + target.BuildLookupTableNonstandard(); } /// @@ -302,21 +397,35 @@ public static class ImGuiHelpers /// Center the ImGui cursor for a certain text. /// /// The text to center for. - public static void CenterCursorForText(string text) - { - var textWidth = ImGui.CalcTextSize(text).X; - CenterCursorFor((int)textWidth); - } + public static void CenterCursorForText(string text) => CenterCursorFor(ImGui.CalcTextSize(text).X); /// /// Center the ImGui cursor for an item with a certain width. /// /// The width to center for. - public static void CenterCursorFor(int itemWidth) - { - var window = (int)ImGui.GetWindowWidth(); - ImGui.SetCursorPosX((window / 2) - (itemWidth / 2)); - } + public static void CenterCursorFor(float itemWidth) => + ImGui.SetCursorPosX((int)((ImGui.GetWindowWidth() - itemWidth) / 2)); + + /// + /// Determines whether is empty. + /// + /// The pointer. + /// Whether it is empty. + public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null; + + /// + /// Determines whether is not null and loaded. + /// + /// The pointer. + /// Whether it is empty. + public static unsafe bool IsNotNullAndLoaded(this ImFontPtr ptr) => ptr.NativePtr != null && ptr.IsLoaded(); + + /// + /// Determines whether is empty. + /// + /// The pointer. + /// Whether it is empty. + public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null; /// /// Get data needed for each new frame. @@ -330,19 +439,57 @@ public static class ImGuiHelpers /// ImFontGlyph the correct version. /// [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")] + [StructLayout(LayoutKind.Explicit, Size = 40)] public struct ImFontGlyphReal { + [FieldOffset(0)] public uint ColoredVisibleTextureIndexCodepoint; + + [FieldOffset(4)] public float AdvanceX; + + [FieldOffset(8)] public float X0; + + [FieldOffset(12)] public float Y0; + + [FieldOffset(16)] public float X1; + + [FieldOffset(20)] public float Y1; + + [FieldOffset(24)] public float U0; + + [FieldOffset(28)] public float V0; + + [FieldOffset(32)] public float U1; + + [FieldOffset(36)] public float V1; + [FieldOffset(8)] + public Vector2 XY0; + + [FieldOffset(16)] + public Vector2 XY1; + + [FieldOffset(24)] + public Vector2 UV0; + + [FieldOffset(32)] + public Vector2 UV1; + + [FieldOffset(8)] + public Vector4 XY; + + [FieldOffset(24)] + public Vector4 UV; + private const uint ColoredMask /*****/ = 0b_00000000_00000000_00000000_00000001u; private const uint VisibleMask /*****/ = 0b_00000000_00000000_00000000_00000010u; private const uint TextureMask /*****/ = 0b_00000000_00000000_00000111_11111100u; @@ -390,7 +537,7 @@ public static class ImGuiHelpers private const uint UseBisectMask /***/ = 0b_00000000_00000000_00000000_00000001u; private const uint OffsetMask /******/ = 0b_00000000_00001111_11111111_11111110u; - private const uint CountMask /*******/ = 0b_11111111_11110000_00000111_11111100u; + private const uint CountMask /*******/ = 0b_11111111_11110000_00000000_00000000u; private const int UseBisectShift = 0; private const int OffsetShift = 1; @@ -419,6 +566,7 @@ public static class ImGuiHelpers /// ImFontAtlasCustomRect the correct version. /// [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "ImGui internals")] + [StructLayout(LayoutKind.Sequential)] public unsafe struct ImFontAtlasCustomRectReal { public ushort Width; @@ -431,10 +579,10 @@ public static class ImGuiHelpers public ImFont* Font; private const uint TextureIndexMask /***/ = 0b_00000000_00000000_00000111_11111100u; - private const uint GlyphIDMask /********/ = 0b_11111111_11111111_11111000_00000000u; + private const uint GlyphIdMask /********/ = 0b_11111111_11111111_11111000_00000000u; private const int TextureIndexShift = 2; - private const int GlyphIDShift = 11; + private const int GlyphIdShift = 11; public int TextureIndex { @@ -444,8 +592,8 @@ public static class ImGuiHelpers public int GlyphId { - get => (int)(this.TextureIndexAndGlyphId & GlyphIDMask) >> GlyphIDShift; - set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~GlyphIDMask) | ((uint)value << GlyphIDShift); + get => (int)(this.TextureIndexAndGlyphId & GlyphIdMask) >> GlyphIdShift; + set => this.TextureIndexAndGlyphId = (this.TextureIndexAndGlyphId & ~GlyphIdMask) | ((uint)value << GlyphIdShift); } } } diff --git a/Dalamud/Interface/Utility/ImVectorWrapper.cs b/Dalamud/Interface/Utility/ImVectorWrapper.cs new file mode 100644 index 000000000..67b002179 --- /dev/null +++ b/Dalamud/Interface/Utility/ImVectorWrapper.cs @@ -0,0 +1,687 @@ +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Numerics; + +using ImGuiNET; + +using JetBrains.Annotations; + +namespace Dalamud.Interface.Utility; + +/// +/// Utility methods for . +/// +public static class ImVectorWrapper +{ + /// + /// Creates a new instance of the struct, initialized with + /// .
+ /// You must call after use. + ///
+ /// The item type. + /// The initial data. + /// The destroyer function to call on item removal. + /// The minimum capacity of the new vector. + /// The new wrapped vector, that has to be disposed after use. + public static ImVectorWrapper CreateFromEnumerable( + IEnumerable sourceEnumerable, + ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, + int minCapacity = 0) + where T : unmanaged + { + var res = new ImVectorWrapper(0, destroyer); + try + { + switch (sourceEnumerable) + { + case T[] c: + res.SetCapacity(Math.Max(minCapacity, c.Length + 1)); + res.LengthUnsafe = c.Length; + c.AsSpan().CopyTo(res.DataSpan); + break; + case ICollection c: + res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); + res.AddRange(sourceEnumerable); + break; + case ICollection c: + res.SetCapacity(Math.Max(minCapacity, c.Count + 1)); + res.AddRange(sourceEnumerable); + break; + default: + res.SetCapacity(minCapacity); + res.AddRange(sourceEnumerable); + res.EnsureCapacity(res.LengthUnsafe + 1); + break; + } + + // Null termination + Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); + res.StorageSpan[res.LengthUnsafe] = default; + + return res; + } + catch + { + res.Dispose(); + throw; + } + } + + /// + /// Creates a new instance of the struct, initialized with + /// .
+ /// You must call after use. + ///
+ /// The item type. + /// The initial data. + /// The destroyer function to call on item removal. + /// The minimum capacity of the new vector. + /// The new wrapped vector, that has to be disposed after use. + public static ImVectorWrapper CreateFromSpan( + ReadOnlySpan sourceSpan, + ImVectorWrapper.ImGuiNativeDestroyDelegate? destroyer = null, + int minCapacity = 0) + where T : unmanaged + { + var res = new ImVectorWrapper(Math.Max(minCapacity, sourceSpan.Length + 1), destroyer); + try + { + res.LengthUnsafe = sourceSpan.Length; + sourceSpan.CopyTo(res.DataSpan); + + // Null termination + Debug.Assert(res.LengthUnsafe < res.CapacityUnsafe, "Capacity must be more than source length + 1"); + res.StorageSpan[res.LengthUnsafe] = default; + return res; + } + catch + { + res.Dispose(); + throw; + } + } + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper ConfigDataWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->ConfigData, ImGuiNative.ImFontConfig_destroy); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper FontsWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Fonts, x => ImGuiNative.ImFont_destroy(x->NativePtr)); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper TexturesWrapped(this ImFontAtlasPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Textures); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper GlyphsWrapped(this ImFontPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->Glyphs); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper IndexedHotDataWrapped(this ImFontPtr obj) + => obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->IndexedHotData); + + /// + /// Wraps into a .
+ /// This does not need to be disposed. + ///
+ /// The owner object. + /// The wrapped vector. + public static unsafe ImVectorWrapper IndexLookupWrapped(this ImFontPtr obj) => + obj.NativePtr is null + ? throw new NullReferenceException() + : new(&obj.NativePtr->IndexLookup); +} + +/// +/// Wrapper for ImVector. +/// +/// Contained type. +public unsafe struct ImVectorWrapper : IList, IList, IReadOnlyList, IDisposable + where T : unmanaged +{ + private ImVector* vector; + private ImGuiNativeDestroyDelegate? destroyer; + + /// + /// Initializes a new instance of the struct.
+ /// If is set to true, you must call after use, + /// and the underlying memory for must have been allocated using + /// . Otherwise, it will crash. + ///
+ /// The underlying vector. + /// The destroyer function to call on item removal. + /// Whether this wrapper owns the vector. + public ImVectorWrapper( + [NotNull] ImVector* vector, + ImGuiNativeDestroyDelegate? destroyer = null, + bool ownership = false) + { + if (vector is null) + throw new ArgumentException($"{nameof(vector)} cannot be null.", nameof(this.vector)); + + this.vector = vector; + this.destroyer = destroyer; + this.HasOwnership = ownership; + } + + /// + /// Initializes a new instance of the struct.
+ /// You must call after use. + ///
+ /// The initial capacity. + /// The destroyer function to call on item removal. + public ImVectorWrapper(int initialCapacity = 0, ImGuiNativeDestroyDelegate? destroyer = null) + { + if (initialCapacity < 0) + { + throw new ArgumentOutOfRangeException( + nameof(initialCapacity), + initialCapacity, + $"{nameof(initialCapacity)} cannot be a negative number."); + } + + this.vector = (ImVector*)ImGuiNative.igMemAlloc((uint)sizeof(ImVector)); + if (this.vector is null) + throw new OutOfMemoryException(); + *this.vector = default; + this.HasOwnership = true; + this.destroyer = destroyer; + + try + { + this.EnsureCapacity(initialCapacity); + } + catch + { + ImGuiNative.igMemFree(this.vector); + this.vector = null; + this.HasOwnership = false; + this.destroyer = null; + throw; + } + } + + /// + /// Destroy callback for items. + /// + /// Pointer to self. + public delegate void ImGuiNativeDestroyDelegate(T* self); + + /// + /// Gets the raw vector. + /// + public ImVector* RawVector => this.vector; + + /// + /// Gets a view of the underlying ImVector{T}, for the range of . + /// + public Span DataSpan => new(this.DataUnsafe, this.LengthUnsafe); + + /// + /// Gets a view of the underlying ImVector{T}, for the range of . + /// + public Span StorageSpan => new(this.DataUnsafe, this.CapacityUnsafe); + + /// + /// Gets a value indicating whether this is disposed. + /// + public bool IsDisposed => this.vector is null; + + /// + /// Gets a value indicating whether this has the ownership of the underlying + /// . + /// + public bool HasOwnership { get; private set; } + + /// + /// Gets the underlying . + /// + public ImVector* Vector => + this.vector is null ? throw new ObjectDisposedException(nameof(ImVectorWrapper)) : this.vector; + + /// + /// Gets the number of items contained inside the underlying ImVector{T}. + /// + public int Length => this.LengthUnsafe; + + /// + /// Gets the number of items that can be contained inside the underlying ImVector{T}. + /// + public int Capacity => this.CapacityUnsafe; + + /// + /// Gets the pointer to the first item in the data inside underlying ImVector{T}. + /// + public T* Data => this.DataUnsafe; + + /// + /// Gets the reference to the number of items contained inside the underlying ImVector{T}. + /// + public ref int LengthUnsafe => ref *&this.Vector->Size; + + /// + /// Gets the reference to the number of items that can be contained inside the underlying ImVector{T}. + /// + public ref int CapacityUnsafe => ref *&this.Vector->Capacity; + + /// + /// Gets the reference to the pointer to the first item in the data inside underlying ImVector{T}. + /// + /// This may be null, if is zero. + public ref T* DataUnsafe => ref *(T**)&this.Vector->Data; + + /// + public bool IsReadOnly => false; + + /// + int ICollection.Count => this.LengthUnsafe; + + /// + bool ICollection.IsSynchronized => false; + + /// + object ICollection.SyncRoot { get; } = new(); + + /// + int ICollection.Count => this.LengthUnsafe; + + /// + int IReadOnlyCollection.Count => this.LengthUnsafe; + + /// + bool IList.IsFixedSize => false; + + /// + /// Gets the element at the specified index as a reference. + /// + /// Index of the item. + /// If is out of range. + public ref T this[int index] => ref this.DataUnsafe[this.EnsureIndex(index)]; + + /// + T IReadOnlyList.this[int index] => this[index]; + + /// + object? IList.this[int index] + { + get => this[index]; + set => this[index] = value is null ? default : (T)value; + } + + /// + T IList.this[int index] + { + get => this[index]; + set => this[index] = value; + } + + /// + public void Dispose() + { + if (this.HasOwnership) + { + this.Clear(); + this.SetCapacity(0); + Debug.Assert(this.vector->Data == 0, "SetCapacity(0) did not free the data"); + ImGuiNative.igMemFree(this.vector); + } + + this.vector = null; + this.HasOwnership = false; + this.destroyer = null; + } + + /// + public IEnumerator GetEnumerator() + { + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + yield return this[i]; + } + + /// + public void Add(in T item) + { + this.EnsureCapacityExponential(this.LengthUnsafe + 1); + this.DataUnsafe[this.LengthUnsafe++] = item; + } + + /// + public void AddRange(IEnumerable items) + { + if (items is ICollection { Count: var count }) + this.EnsureCapacityExponential(this.LengthUnsafe + count); + + foreach (var item in items) + this.Add(item); + } + + /// + public void AddRange(Span items) + { + this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); + foreach (var item in items) + this.Add(item); + } + + /// + public void Clear() => this.Clear(false); + + /// + /// Clears this vector, optionally skipping destroyer invocation. + /// + /// Whether to skip destroyer invocation. + public void Clear(bool skipDestroyer) + { + if (this.destroyer != null && !skipDestroyer) + { + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + this.destroyer(&this.DataUnsafe[i]); + } + + this.LengthUnsafe = 0; + } + + /// + public bool Contains(in T item) => this.IndexOf(in item) != -1; + + /// + /// Size down the underlying ImVector{T}. + /// + /// Capacity to reserve. + /// Whether the capacity has been changed. + public bool Compact(int reservation) => this.SetCapacity(Math.Max(reservation, this.LengthUnsafe)); + + /// + public void CopyTo(T[] array, int arrayIndex) + { + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException( + nameof(arrayIndex), + arrayIndex, + $"{nameof(arrayIndex)} is less than 0."); + } + + if (array.Length - arrayIndex < this.LengthUnsafe) + { + throw new ArgumentException( + "The number of elements in the source ImVectorWrapper is greater than the available space from arrayIndex to the end of the destination array.", + nameof(array)); + } + + fixed (void* p = array) + Buffer.MemoryCopy(this.DataUnsafe, p, this.LengthUnsafe * sizeof(T), this.LengthUnsafe * sizeof(T)); + } + + /// + /// Ensures that the capacity of this list is at least the specified .
+ /// On growth, the new capacity exactly matches . + ///
+ /// The minimum capacity to ensure. + /// Whether the capacity has been changed. + public bool EnsureCapacity(int capacity) => this.CapacityUnsafe < capacity && this.SetCapacity(capacity); + + /// + /// Ensures that the capacity of this list is at least the specified .
+ /// On growth, the new capacity may exceed . + ///
+ /// The minimum capacity to ensure. + /// Whether the capacity has been changed. + public bool EnsureCapacityExponential(int capacity) + => this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)this.LengthUnsafe))); + + /// + /// Resizes the underlying array and fills with zeroes if grown. + /// + /// New size. + /// New default value. + /// Whether to skip calling destroyer function. + public void Resize(int size, in T defaultValue = default, bool skipDestroyer = false) + { + this.EnsureCapacity(size); + var old = this.LengthUnsafe; + if (old > size && !skipDestroyer && this.destroyer is not null) + { + foreach (var v in this.DataSpan[size..]) + this.destroyer(&v); + } + + this.LengthUnsafe = size; + if (old < size) + this.DataSpan[old..].Fill(defaultValue); + } + + /// + public bool Remove(in T item) + { + var index = this.IndexOf(item); + if (index == -1) + return false; + + this.RemoveAt(index); + return true; + } + + /// + public int IndexOf(in T item) + { + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + { + if (Equals(item, this.DataUnsafe[i])) + return i; + } + + return -1; + } + + /// + public void Insert(int index, in T item) + { + // Note: index == this.LengthUnsafe is okay; we're just adding to the end then + if (index < 0 || index > this.LengthUnsafe) + throw new IndexOutOfRangeException(); + + this.EnsureCapacityExponential(this.CapacityUnsafe + 1); + var num = this.LengthUnsafe - index; + Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + 1, num * sizeof(T), num * sizeof(T)); + this.DataUnsafe[index] = item; + } + + /// + public void InsertRange(int index, IEnumerable items) + { + if (items is ICollection { Count: var count }) + { + this.EnsureCapacityExponential(this.LengthUnsafe + count); + var num = this.LengthUnsafe - index; + Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + count, num * sizeof(T), num * sizeof(T)); + foreach (var item in items) + this.DataUnsafe[index++] = item; + } + else + { + foreach (var item in items) + this.Insert(index++, item); + } + } + + /// + public void InsertRange(int index, Span items) + { + this.EnsureCapacityExponential(this.LengthUnsafe + items.Length); + var num = this.LengthUnsafe - index; + Buffer.MemoryCopy(this.DataUnsafe + index, this.DataUnsafe + index + items.Length, num * sizeof(T), num * sizeof(T)); + foreach (var item in items) + this.DataUnsafe[index++] = item; + } + + /// + /// Removes the element at the given index. + /// + /// The index. + /// Whether to skip calling the destroyer function. + public void RemoveAt(int index, bool skipDestroyer = false) + { + this.EnsureIndex(index); + var num = this.LengthUnsafe - index - 1; + if (!skipDestroyer) + this.destroyer?.Invoke(&this.DataUnsafe[index]); + + Buffer.MemoryCopy(this.DataUnsafe + index + 1, this.DataUnsafe + index, num * sizeof(T), num * sizeof(T)); + } + + /// + void IList.RemoveAt(int index) => this.RemoveAt(index); + + /// + void IList.RemoveAt(int index) => this.RemoveAt(index); + + /// + /// Sets the capacity exactly as requested. + /// + /// New capacity. + /// Whether the capacity has been changed. + /// If is less than . + /// If memory for the requested capacity cannot be allocated. + public bool SetCapacity(int capacity) + { + if (capacity < this.LengthUnsafe) + throw new ArgumentOutOfRangeException(nameof(capacity), capacity, null); + + if (capacity == this.LengthUnsafe) + { + if (capacity == 0 && this.DataUnsafe is not null) + { + ImGuiNative.igMemFree(this.DataUnsafe); + this.DataUnsafe = null; + } + + return false; + } + + var oldAlloc = this.DataUnsafe; + var oldSpan = new Span(oldAlloc, this.CapacityUnsafe); + + var newAlloc = (T*)(capacity == 0 + ? null + : ImGuiNative.igMemAlloc(checked((uint)(capacity * sizeof(T))))); + + if (newAlloc is null && capacity > 0) + throw new OutOfMemoryException(); + + var newSpan = new Span(newAlloc, capacity); + + if (!oldSpan.IsEmpty && !newSpan.IsEmpty) + oldSpan[..this.LengthUnsafe].CopyTo(newSpan); +// #if DEBUG +// new Span(newAlloc + this.LengthUnsafe, sizeof(T) * (capacity - this.LengthUnsafe)).Fill(0xCC); +// #endif + + if (oldAlloc != null) + ImGuiNative.igMemFree(oldAlloc); + + this.DataUnsafe = newAlloc; + this.CapacityUnsafe = capacity; + + return true; + } + + /// + void ICollection.Add(T item) => this.Add(in item); + + /// + bool ICollection.Contains(T item) => this.Contains(in item); + + /// + void ICollection.CopyTo(Array array, int index) + { + if (index < 0) + { + throw new ArgumentOutOfRangeException( + nameof(index), + index, + $"{nameof(index)} is less than 0."); + } + + if (array.Length - index < this.LengthUnsafe) + { + throw new ArgumentException( + "The number of elements in the source ImVectorWrapper is greater than the available space from arrayIndex to the end of the destination array.", + nameof(array)); + } + + foreach (var i in Enumerable.Range(0, this.LengthUnsafe)) + array.SetValue(this.DataUnsafe[i], index); + } + + /// + bool ICollection.Remove(T item) => this.Remove(in item); + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + int IList.Add(object? value) + { + this.Add(value is null ? default : (T)value); + return this.LengthUnsafe - 1; + } + + /// + bool IList.Contains(object? value) => this.Contains(value is null ? default : (T)value); + + /// + int IList.IndexOf(object? value) => this.IndexOf(value is null ? default : (T)value); + + /// + void IList.Insert(int index, object? value) => this.Insert(index, value is null ? default : (T)value); + + /// + void IList.Remove(object? value) => this.Remove(value is null ? default : (T)value); + + /// + int IList.IndexOf(T item) => this.IndexOf(in item); + + /// + void IList.Insert(int index, T item) => this.Insert(index, in item); + + private int EnsureIndex(int i) => i >= 0 && i < this.LengthUnsafe ? i : throw new IndexOutOfRangeException(); +} diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index 2fb735640..5712f419b 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -1,6 +1,5 @@ -using System; - using Serilog; +using Serilog.Core; using Serilog.Events; namespace Dalamud.Logging.Internal; @@ -33,6 +32,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Verbose(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, null, values); @@ -42,6 +42,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Verbose(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Verbose, messageTemplate, exception, values); @@ -50,6 +51,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Debug(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, null, values); @@ -59,6 +61,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Debug(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Debug, messageTemplate, exception, values); @@ -67,6 +70,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Information(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, null, values); @@ -76,6 +80,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Information(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Information, messageTemplate, exception, values); @@ -84,6 +89,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Warning(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, null, values); @@ -93,6 +99,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Warning(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Warning, messageTemplate, exception, values); @@ -101,6 +108,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Error(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, null, values); @@ -110,6 +118,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Error(Exception? exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values); @@ -118,6 +127,7 @@ public class ModuleLog ///
/// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Fatal(string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, null, values); @@ -127,9 +137,11 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. + [MessageTemplateFormatMethod("messageTemplate")] public void Fatal(Exception exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Fatal, messageTemplate, exception, values); + [MessageTemplateFormatMethod("messageTemplate")] private void WriteLog( LogEventLevel level, string messageTemplate, Exception? exception = null, params object[] values) { diff --git a/Dalamud/NativeFunctions.cs b/Dalamud/NativeFunctions.cs index b77f71d08..92dfe5dd7 100644 --- a/Dalamud/NativeFunctions.cs +++ b/Dalamud/NativeFunctions.cs @@ -137,6 +137,7 @@ internal static partial class NativeFunctions /// /// MB_* from winuser. /// + [Flags] public enum MessageBoxType : uint { /// From 473e24301d281027e7e8385ee5516d850444d3a5 Mon Sep 17 00:00:00 2001 From: Sirius902 <10891979+Sirius902@users.noreply.github.com> Date: Sun, 26 Nov 2023 14:04:38 -0800 Subject: [PATCH 05/13] Fix incorrect ImGui code (#1546) * Add missing ImGui.EndTabBar * Add more ImGui fixes --- .../Internal/Windows/PluginStatWindow.cs | 3 +- .../Windows/Settings/SettingsWindow.cs | 2 + .../Windows/StyleEditor/StyleEditorWindow.cs | 189 +++++++++--------- 3 files changed, 99 insertions(+), 95 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index 44e43fbd3..a1d93bb8c 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -44,7 +44,8 @@ internal class PluginStatWindow : Window { var pluginManager = Service.Get(); - ImGui.BeginTabBar("Stat Tabs"); + if (!ImGui.BeginTabBar("Stat Tabs")) + return; if (ImGui.BeginTabItem("Draw times")) { diff --git a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs index 4f77c0502..7d4489f8d 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/SettingsWindow.cs @@ -155,6 +155,8 @@ internal class SettingsWindow : Window ImGui.EndTabItem(); } } + + ImGui.EndTabBar(); } ImGui.SetCursorPos(windowSize - ImGuiHelpers.ScaledVector2(70)); diff --git a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs index 3a3e871b0..c202a36ce 100644 --- a/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs +++ b/Dalamud/Interface/Internal/Windows/StyleEditor/StyleEditorWindow.cs @@ -211,121 +211,122 @@ public class StyleEditorWindow : Window if (ImGui.BeginTabItem(Loc.Localize("StyleEditorVariables", "Variables"))) { - ImGui.BeginChild($"ScrollingVars", ImGuiHelpers.ScaledVector2(0, -32), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground); + if (ImGui.BeginChild($"ScrollingVars", ImGuiHelpers.ScaledVector2(0, -32), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground)) + { + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); + ImGui.SliderFloat2("WindowPadding", ref style.WindowPadding, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("FramePadding", ref style.FramePadding, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("CellPadding", ref style.CellPadding, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("ItemSpacing", ref style.ItemSpacing, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("ItemInnerSpacing", ref style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); + ImGui.SliderFloat2("TouchExtraPadding", ref style.TouchExtraPadding, 0.0f, 10.0f, "%.0f"); + ImGui.SliderFloat("IndentSpacing", ref style.IndentSpacing, 0.0f, 30.0f, "%.0f"); + ImGui.SliderFloat("ScrollbarSize", ref style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); + ImGui.SliderFloat("GrabMinSize", ref style.GrabMinSize, 1.0f, 20.0f, "%.0f"); + ImGui.Text("Borders"); + ImGui.SliderFloat("WindowBorderSize", ref style.WindowBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.SliderFloat("ChildBorderSize", ref style.ChildBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.SliderFloat("PopupBorderSize", ref style.PopupBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.SliderFloat("FrameBorderSize", ref style.FrameBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.SliderFloat("TabBorderSize", ref style.TabBorderSize, 0.0f, 1.0f, "%.0f"); + ImGui.Text("Rounding"); + ImGui.SliderFloat("WindowRounding", ref style.WindowRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("ChildRounding", ref style.ChildRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("FrameRounding", ref style.FrameRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("PopupRounding", ref style.PopupRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("ScrollbarRounding", ref style.ScrollbarRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("GrabRounding", ref style.GrabRounding, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("LogSliderDeadzone", ref style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); + ImGui.SliderFloat("TabRounding", ref style.TabRounding, 0.0f, 12.0f, "%.0f"); + ImGui.Text("Alignment"); + ImGui.SliderFloat2("WindowTitleAlign", ref style.WindowTitleAlign, 0.0f, 1.0f, "%.2f"); + var windowMenuButtonPosition = (int)style.WindowMenuButtonPosition + 1; + if (ImGui.Combo("WindowMenuButtonPosition", ref windowMenuButtonPosition, "None\0Left\0Right\0")) + style.WindowMenuButtonPosition = (ImGuiDir)(windowMenuButtonPosition - 1); + ImGui.SliderFloat2("ButtonTextAlign", ref style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Alignment applies when a button is larger than its text content."); + ImGui.SliderFloat2("SelectableTextAlign", ref style.SelectableTextAlign, 0.0f, 1.0f, "%.2f"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker("Alignment applies when a selectable is larger than its text content."); + ImGui.SliderFloat2("DisplaySafeAreaPadding", ref style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); + ImGui.SameLine(); + ImGuiComponents.HelpMarker( + "Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured)."); - ImGui.SliderFloat2("WindowPadding", ref style.WindowPadding, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("FramePadding", ref style.FramePadding, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("CellPadding", ref style.CellPadding, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("ItemSpacing", ref style.ItemSpacing, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("ItemInnerSpacing", ref style.ItemInnerSpacing, 0.0f, 20.0f, "%.0f"); - ImGui.SliderFloat2("TouchExtraPadding", ref style.TouchExtraPadding, 0.0f, 10.0f, "%.0f"); - ImGui.SliderFloat("IndentSpacing", ref style.IndentSpacing, 0.0f, 30.0f, "%.0f"); - ImGui.SliderFloat("ScrollbarSize", ref style.ScrollbarSize, 1.0f, 20.0f, "%.0f"); - ImGui.SliderFloat("GrabMinSize", ref style.GrabMinSize, 1.0f, 20.0f, "%.0f"); - ImGui.Text("Borders"); - ImGui.SliderFloat("WindowBorderSize", ref style.WindowBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.SliderFloat("ChildBorderSize", ref style.ChildBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.SliderFloat("PopupBorderSize", ref style.PopupBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.SliderFloat("FrameBorderSize", ref style.FrameBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.SliderFloat("TabBorderSize", ref style.TabBorderSize, 0.0f, 1.0f, "%.0f"); - ImGui.Text("Rounding"); - ImGui.SliderFloat("WindowRounding", ref style.WindowRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("ChildRounding", ref style.ChildRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("FrameRounding", ref style.FrameRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("PopupRounding", ref style.PopupRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("ScrollbarRounding", ref style.ScrollbarRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("GrabRounding", ref style.GrabRounding, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("LogSliderDeadzone", ref style.LogSliderDeadzone, 0.0f, 12.0f, "%.0f"); - ImGui.SliderFloat("TabRounding", ref style.TabRounding, 0.0f, 12.0f, "%.0f"); - ImGui.Text("Alignment"); - ImGui.SliderFloat2("WindowTitleAlign", ref style.WindowTitleAlign, 0.0f, 1.0f, "%.2f"); - var windowMenuButtonPosition = (int)style.WindowMenuButtonPosition + 1; - if (ImGui.Combo("WindowMenuButtonPosition", ref windowMenuButtonPosition, "None\0Left\0Right\0")) - style.WindowMenuButtonPosition = (ImGuiDir)(windowMenuButtonPosition - 1); - ImGui.SliderFloat2("ButtonTextAlign", ref style.ButtonTextAlign, 0.0f, 1.0f, "%.2f"); - ImGui.SameLine(); - ImGuiComponents.HelpMarker("Alignment applies when a button is larger than its text content."); - ImGui.SliderFloat2("SelectableTextAlign", ref style.SelectableTextAlign, 0.0f, 1.0f, "%.2f"); - ImGui.SameLine(); - ImGuiComponents.HelpMarker("Alignment applies when a selectable is larger than its text content."); - ImGui.SliderFloat2("DisplaySafeAreaPadding", ref style.DisplaySafeAreaPadding, 0.0f, 30.0f, "%.0f"); - ImGui.SameLine(); - ImGuiComponents.HelpMarker( - "Adjust if you cannot see the edges of your screen (e.g. on a TV where scaling has not been configured)."); - ImGui.EndTabItem(); - - ImGui.EndChild(); + ImGui.EndChild(); + } ImGui.EndTabItem(); } if (ImGui.BeginTabItem(Loc.Localize("StyleEditorColors", "Colors"))) { - ImGui.BeginChild("ScrollingColors", ImGuiHelpers.ScaledVector2(0, -30), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground); - - ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); - - if (ImGui.RadioButton("Opaque", this.alphaFlags == ImGuiColorEditFlags.None)) - this.alphaFlags = ImGuiColorEditFlags.None; - ImGui.SameLine(); - if (ImGui.RadioButton("Alpha", this.alphaFlags == ImGuiColorEditFlags.AlphaPreview)) - this.alphaFlags = ImGuiColorEditFlags.AlphaPreview; - ImGui.SameLine(); - if (ImGui.RadioButton("Both", this.alphaFlags == ImGuiColorEditFlags.AlphaPreviewHalf)) - this.alphaFlags = ImGuiColorEditFlags.AlphaPreviewHalf; - ImGui.SameLine(); - - ImGuiComponents.HelpMarker( - "In the color list:\n" + - "Left-click on color square to open color picker,\n" + - "Right-click to open edit options menu."); - - foreach (var imGuiCol in Enum.GetValues()) + if (ImGui.BeginChild("ScrollingColors", ImGuiHelpers.ScaledVector2(0, -30), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground)) { - if (imGuiCol == ImGuiCol.COUNT) - continue; + ImGui.SetCursorPosY(ImGui.GetCursorPosY() - 5); - ImGui.PushID(imGuiCol.ToString()); + if (ImGui.RadioButton("Opaque", this.alphaFlags == ImGuiColorEditFlags.None)) + this.alphaFlags = ImGuiColorEditFlags.None; + ImGui.SameLine(); + if (ImGui.RadioButton("Alpha", this.alphaFlags == ImGuiColorEditFlags.AlphaPreview)) + this.alphaFlags = ImGuiColorEditFlags.AlphaPreview; + ImGui.SameLine(); + if (ImGui.RadioButton("Both", this.alphaFlags == ImGuiColorEditFlags.AlphaPreviewHalf)) + this.alphaFlags = ImGuiColorEditFlags.AlphaPreviewHalf; + ImGui.SameLine(); - ImGui.ColorEdit4("##color", ref style.Colors[(int)imGuiCol], ImGuiColorEditFlags.AlphaBar | this.alphaFlags); + ImGuiComponents.HelpMarker( + "In the color list:\n" + + "Left-click on color square to open color picker,\n" + + "Right-click to open edit options menu."); - ImGui.SameLine(0.0f, style.ItemInnerSpacing.X); - ImGui.TextUnformatted(imGuiCol.ToString()); - - ImGui.PopID(); - } - - ImGui.Separator(); - - foreach (var property in typeof(DalamudColors).GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - ImGui.PushID(property.Name); - - var colorVal = property.GetValue(workStyle.BuiltInColors); - if (colorVal == null) + foreach (var imGuiCol in Enum.GetValues()) { - colorVal = property.GetValue(StyleModelV1.DalamudStandard.BuiltInColors); - property.SetValue(workStyle.BuiltInColors, colorVal); + if (imGuiCol == ImGuiCol.COUNT) + continue; + + ImGui.PushID(imGuiCol.ToString()); + + ImGui.ColorEdit4("##color", ref style.Colors[(int)imGuiCol], ImGuiColorEditFlags.AlphaBar | this.alphaFlags); + + ImGui.SameLine(0.0f, style.ItemInnerSpacing.X); + ImGui.TextUnformatted(imGuiCol.ToString()); + + ImGui.PopID(); } - var color = (Vector4)colorVal; + ImGui.Separator(); - if (ImGui.ColorEdit4("##color", ref color, ImGuiColorEditFlags.AlphaBar | this.alphaFlags)) + foreach (var property in typeof(DalamudColors).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { - property.SetValue(workStyle.BuiltInColors, color); - workStyle.BuiltInColors?.Apply(); + ImGui.PushID(property.Name); + + var colorVal = property.GetValue(workStyle.BuiltInColors); + if (colorVal == null) + { + colorVal = property.GetValue(StyleModelV1.DalamudStandard.BuiltInColors); + property.SetValue(workStyle.BuiltInColors, colorVal); + } + + var color = (Vector4)colorVal; + + if (ImGui.ColorEdit4("##color", ref color, ImGuiColorEditFlags.AlphaBar | this.alphaFlags)) + { + property.SetValue(workStyle.BuiltInColors, color); + workStyle.BuiltInColors?.Apply(); + } + + ImGui.SameLine(0.0f, style.ItemInnerSpacing.X); + ImGui.TextUnformatted(property.Name); + + ImGui.PopID(); } - ImGui.SameLine(0.0f, style.ItemInnerSpacing.X); - ImGui.TextUnformatted(property.Name); - - ImGui.PopID(); + ImGui.EndChild(); } - ImGui.EndChild(); - ImGui.EndTabItem(); } From fcebd150774b4d481c70f8d967e5da4e0f8e02f0 Mon Sep 17 00:00:00 2001 From: Anna Date: Tue, 28 Nov 2023 17:04:36 +0000 Subject: [PATCH 06/13] Send Dalamud user-agent when downloading plugins (#1550) Sets the user-agent on all HappyHttp requests to `Dalamud/`, and pass `Accept: application/zip` in plugin downloads. --- Dalamud/Networking/Http/HappyHttpClient.cs | 14 +++++++++++++- Dalamud/Plugin/Internal/PluginManager.cs | 14 +++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index 8459f1453..4379a698f 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -1,6 +1,9 @@ using System; using System.Net; using System.Net.Http; +using System.Net.Http.Headers; + +using Dalamud.Utility; namespace Dalamud.Networking.Http; @@ -25,7 +28,16 @@ internal class HappyHttpClient : IDisposable, IServiceType { AutomaticDecompression = DecompressionMethods.All, ConnectCallback = this.SharedHappyEyeballsCallback.ConnectCallback, - }); + }) + { + DefaultRequestHeaders = + { + UserAgent = + { + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + }, + }, + }; } /// diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index ff6b045be..9a651c64e 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -5,6 +5,8 @@ using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -1196,7 +1198,17 @@ internal partial class PluginManager : IDisposable, IServiceType private async Task DownloadPluginAsync(RemotePluginManifest repoManifest, bool useTesting) { var downloadUrl = useTesting ? repoManifest.DownloadLinkTesting : repoManifest.DownloadLinkInstall; - var response = await this.happyHttpClient.SharedHttpClient.GetAsync(downloadUrl); + var request = new HttpRequestMessage(HttpMethod.Get, downloadUrl) + { + Headers = + { + Accept = + { + new MediaTypeWithQualityHeaderValue("application/zip"), + }, + }, + }; + var response = await this.happyHttpClient.SharedHttpClient.SendAsync(request); response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStreamAsync(); From b66be84b939147505a4221a39da4c4b50ec18358 Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 29 Nov 2023 06:20:16 +0900 Subject: [PATCH 07/13] Better Service dependency handling (#1535) --- .../Internal/DalamudConfiguration.cs | 2 +- Dalamud/Dalamud.cs | 2 +- .../Game/Addon/Events/AddonEventManager.cs | 2 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/Config/GameConfig.cs | 2 +- Dalamud/Game/DutyState/DutyState.cs | 2 +- .../UniversalisMarketBoardUploader.cs | 9 +- .../Game/Network/Internal/NetworkHandlers.cs | 10 +- Dalamud/Game/TargetSigScanner.cs | 2 +- Dalamud/Interface/DragDrop/DragDropManager.cs | 2 +- .../Interface/GameFonts/GameFontManager.cs | 2 +- .../Interface/Internal/DalamudInterface.cs | 106 +++++++------ .../Interface/Internal/InterfaceManager.cs | 2 +- .../Internal/Windows/ChangelogWindow.cs | 14 +- .../Internal/Windows/ConsoleWindow.cs | 5 +- .../PluginInstaller/PluginInstallerWindow.cs | 7 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 56 ++++--- Dalamud/IoC/Internal/ServiceContainer.cs | 7 +- Dalamud/Plugin/Internal/PluginManager.cs | 28 ++-- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 4 - .../Plugin/Internal/Types/PluginRepository.cs | 65 ++++---- Dalamud/Plugin/Ipc/Internal/DataShare.cs | 2 +- Dalamud/ServiceManager.cs | 132 +++++++++++++--- Dalamud/Service{T}.cs | 145 ++++++++++++++---- Dalamud/Storage/ReliableFileStorage.cs | 2 +- 25 files changed, 415 insertions(+), 197 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 35d5261da..76c8f3603 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -21,7 +21,7 @@ namespace Dalamud.Configuration.Internal; /// Class containing Dalamud settings. /// [Serializable] -[ServiceManager.Service] +[ServiceManager.ProvidedService] #pragma warning disable SA1015 [InherentDependency] // We must still have this when unloading #pragma warning restore SA1015 diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index f50a39aa3..9896b87a6 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -30,7 +30,7 @@ namespace Dalamud; /// /// The main Dalamud class containing all subsystems. /// -[ServiceManager.Service] +[ServiceManager.ProvidedService] internal sealed class Dalamud : IServiceType { #region Internals diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index a91f5437c..d8f3427ef 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Events; /// Service provider for addon event management. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class AddonEventManager : IDisposable, IServiceType { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index c7184ca11..08a2d59ef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// This class provides events for in-game addon lifecycles. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class AddonLifecycle : IDisposable, IServiceType { private static readonly ModuleLog Log = new("AddonLifecycle"); diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index ae3205abc..b82d64f24 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -12,7 +12,7 @@ namespace Dalamud.Game.Config; /// This class represents the game's configuration. ///
[InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable { private readonly GameConfigAddressResolver address = new(); diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 6dda95a66..66356033b 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -12,7 +12,7 @@ namespace Dalamud.Game.DutyState; /// This class represents the state of the currently occupied duty. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class DutyState : IDisposable, IServiceType, IDutyState { private readonly DutyStateAddressResolver address; diff --git a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs index b3175cad3..34a255e19 100644 --- a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs +++ b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Net.Http; using System.Text; @@ -22,14 +21,14 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader private const string ApiKey = "GGD6RdSfGyRiHM5WDnAo0Nj9Nv7aC5NDhMj3BebT"; - private readonly HttpClient httpClient = Service.Get().SharedHttpClient; + private readonly HttpClient httpClient; /// /// Initializes a new instance of the class. /// - public UniversalisMarketBoardUploader() - { - } + /// An instance of . + public UniversalisMarketBoardUploader(HappyHttpClient happyHttpClient) => + this.httpClient = happyHttpClient.SharedHttpClient; /// public async Task Upload(MarketBoardItemRequest request) diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 01e92a373..76d3b5659 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -13,6 +13,7 @@ using Dalamud.Game.Network.Internal.MarketBoardUploaders; using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis; using Dalamud.Game.Network.Structures; using Dalamud.Hooking; +using Dalamud.Networking.Http; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI.Info; using Lumina.Excel.GeneratedSheets; @@ -23,7 +24,7 @@ namespace Dalamud.Game.Network.Internal; /// /// This class handles network notifications and uploading market board data. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class NetworkHandlers : IDisposable, IServiceType { private readonly IMarketBoardUploader uploader; @@ -55,9 +56,12 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType private bool disposing; [ServiceManager.ServiceConstructor] - private NetworkHandlers(GameNetwork gameNetwork, TargetSigScanner sigScanner) + private NetworkHandlers( + GameNetwork gameNetwork, + TargetSigScanner sigScanner, + HappyHttpClient happyHttpClient) { - this.uploader = new UniversalisMarketBoardUploader(); + this.uploader = new UniversalisMarketBoardUploader(happyHttpClient); this.addressResolver = new NetworkHandlersAddressResolver(); this.addressResolver.Setup(sigScanner); diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index 9242c5e83..35c82562e 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -11,7 +11,7 @@ namespace Dalamud.Game; /// [PluginInterface] [InterfaceVersion("1.0")] -[ServiceManager.Service] +[ServiceManager.ProvidedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index e8641035f..151ef28a0 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -15,7 +15,7 @@ namespace Dalamud.Interface.DragDrop; /// and can be used to create ImGui drag and drop sources and targets for those external events. /// [PluginInterface] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index 71661682d..b3454e085 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -22,7 +22,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Loads game font for use in ImGui. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal class GameFontManager : IServiceType { private static readonly string?[] FontNames = diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 816352d80..18ab538c4 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1,7 +1,5 @@ -using System; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; using System.Numerics; using System.Reflection; @@ -9,6 +7,7 @@ using System.Runtime.InteropServices; using CheapLoc; using Dalamud.Configuration.Internal; +using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Game.Internal; @@ -25,14 +24,14 @@ using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; -using ImGuiScene; + using ImPlotNET; using PInvoke; using Serilog.Events; @@ -48,9 +47,11 @@ internal class DalamudInterface : IDisposable, IServiceType private const float CreditsDarkeningMaxAlpha = 0.8f; private static readonly ModuleLog Log = new("DUI"); - + + private readonly Dalamud dalamud; private readonly DalamudConfiguration configuration; - + private readonly InterfaceManager interfaceManager; + private readonly ChangelogWindow changelogWindow; private readonly ColorDemoWindow colorDemoWindow; private readonly ComponentDemoWindow componentDemoWindow; @@ -92,11 +93,16 @@ internal class DalamudInterface : IDisposable, IServiceType DalamudConfiguration configuration, InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, PluginImageCache pluginImageCache, - Branding branding) + Branding branding, + Game.Framework framework, + ClientState clientState, + TitleScreenMenu titleScreenMenu, + GameGui gameGui) { + this.dalamud = dalamud; this.configuration = configuration; + this.interfaceManager = interfaceManagerWithScene.Manager; - var interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; @@ -104,13 +110,20 @@ internal class DalamudInterface : IDisposable, IServiceType this.dataWindow = new DataWindow() { IsOpen = false }; this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow() { IsOpen = false }; this.imeWindow = new ImeWindow() { IsOpen = false }; - this.consoleWindow = new ConsoleWindow() { IsOpen = configuration.LogOpenAtStartup }; + this.consoleWindow = new ConsoleWindow(configuration) { IsOpen = configuration.LogOpenAtStartup }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; - this.pluginWindow = new PluginInstallerWindow(pluginImageCache) { IsOpen = false }; + this.pluginWindow = new PluginInstallerWindow(pluginImageCache, configuration) { IsOpen = false }; this.settingsWindow = new SettingsWindow() { IsOpen = false }; this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; - this.titleScreenMenuWindow = new TitleScreenMenuWindow() { IsOpen = false }; + this.titleScreenMenuWindow = new TitleScreenMenuWindow( + clientState, + dalamud, + configuration, + framework, + gameGui, + this.interfaceManager, + titleScreenMenu) { IsOpen = false }; this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; @@ -136,7 +149,7 @@ internal class DalamudInterface : IDisposable, IServiceType ImGuiManagedAsserts.AssertsEnabled = configuration.AssertsEnabledAtStartup; this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup; - interfaceManager.Draw += this.OnDraw; + this.interfaceManager.Draw += this.OnDraw; var tsm = Service.Get(); tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, () => this.OpenPluginInstaller()); @@ -173,7 +186,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// public void Dispose() { - Service.Get().Draw -= this.OnDraw; + this.interfaceManager.Draw -= this.OnDraw; this.WindowSystem.RemoveAllWindows(); @@ -356,7 +369,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// Toggles the . /// /// The data kind to switch to after opening. - public void ToggleDataWindow(string dataKind = null) + public void ToggleDataWindow(string? dataKind = null) { this.dataWindow.Toggle(); if (dataKind != null && this.dataWindow.IsOpen) @@ -378,7 +391,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// /// Toggles the . /// - public void ToggleIMEWindow() => this.imeWindow.Toggle(); + public void ToggleImeWindow() => this.imeWindow.Toggle(); /// /// Toggles the . @@ -504,7 +517,8 @@ internal class DalamudInterface : IDisposable, IServiceType private void DrawCreditsDarkeningAnimation() { - using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding | ImGuiStyleVar.WindowBorderSize, 0f); + using var style1 = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f); + using var style2 = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 0f); using var color = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 0)); ImGui.SetNextWindowPos(new Vector2(0, 0)); @@ -579,18 +593,16 @@ internal class DalamudInterface : IDisposable, IServiceType { if (ImGui.BeginMainMenuBar()) { - var dalamud = Service.Get(); - var configuration = Service.Get(); var pluginManager = Service.Get(); if (ImGui.BeginMenu("Dalamud")) { ImGui.MenuItem("Draw dev menu", string.Empty, ref this.isImGuiDrawDevMenu); - var devBarAtStartup = configuration.DevBarOpenAtStartup; + var devBarAtStartup = this.configuration.DevBarOpenAtStartup; if (ImGui.MenuItem("Draw dev menu at startup", string.Empty, ref devBarAtStartup)) { - configuration.DevBarOpenAtStartup ^= true; - configuration.QueueSave(); + this.configuration.DevBarOpenAtStartup ^= true; + this.configuration.QueueSave(); } ImGui.Separator(); @@ -607,25 +619,25 @@ internal class DalamudInterface : IDisposable, IServiceType if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, EntryPoint.LogLevelSwitch.MinimumLevel == logLevel)) { EntryPoint.LogLevelSwitch.MinimumLevel = logLevel; - configuration.LogLevel = logLevel; - configuration.QueueSave(); + this.configuration.LogLevel = logLevel; + this.configuration.QueueSave(); } } ImGui.EndMenu(); } - var logSynchronously = configuration.LogSynchronously; + var logSynchronously = this.configuration.LogSynchronously; if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously)) { - configuration.LogSynchronously = logSynchronously; - configuration.QueueSave(); + this.configuration.LogSynchronously = logSynchronously; + this.configuration.QueueSave(); EntryPoint.InitLogging( - dalamud.StartInfo.LogPath!, - dalamud.StartInfo.BootShowConsole, - configuration.LogSynchronously, - dalamud.StartInfo.LogName); + this.dalamud.StartInfo.LogPath!, + this.dalamud.StartInfo.BootShowConsole, + this.configuration.LogSynchronously, + this.dalamud.StartInfo.LogName); } var antiDebug = Service.Get(); @@ -637,8 +649,8 @@ internal class DalamudInterface : IDisposable, IServiceType else antiDebug.Disable(); - configuration.IsAntiAntiDebugEnabled = newEnabled; - configuration.QueueSave(); + this.configuration.IsAntiAntiDebugEnabled = newEnabled; + this.configuration.QueueSave(); } ImGui.Separator(); @@ -730,10 +742,10 @@ internal class DalamudInterface : IDisposable, IServiceType } } - if (ImGui.MenuItem("Report crashes at shutdown", null, configuration.ReportShutdownCrashes)) + if (ImGui.MenuItem("Report crashes at shutdown", null, this.configuration.ReportShutdownCrashes)) { - configuration.ReportShutdownCrashes = !configuration.ReportShutdownCrashes; - configuration.QueueSave(); + this.configuration.ReportShutdownCrashes = !this.configuration.ReportShutdownCrashes; + this.configuration.QueueSave(); } ImGui.Separator(); @@ -744,7 +756,7 @@ internal class DalamudInterface : IDisposable, IServiceType } ImGui.MenuItem(Util.AssemblyVersion, false); - ImGui.MenuItem(dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); + ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); ImGui.MenuItem($"D: {Util.GetGitHash()}[{Util.GetGitCommitCount()}] CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.Interop.Resolver.Version}]", false); ImGui.MenuItem($"CLR: {Environment.Version}", false); @@ -766,10 +778,10 @@ internal class DalamudInterface : IDisposable, IServiceType ImGuiManagedAsserts.AssertsEnabled = val; } - if (ImGui.MenuItem("Enable asserts at startup", null, configuration.AssertsEnabledAtStartup)) + if (ImGui.MenuItem("Enable asserts at startup", null, this.configuration.AssertsEnabledAtStartup)) { - configuration.AssertsEnabledAtStartup = !configuration.AssertsEnabledAtStartup; - configuration.QueueSave(); + this.configuration.AssertsEnabledAtStartup = !this.configuration.AssertsEnabledAtStartup; + this.configuration.QueueSave(); } if (ImGui.MenuItem("Clear focus")) @@ -779,7 +791,7 @@ internal class DalamudInterface : IDisposable, IServiceType if (ImGui.MenuItem("Clear stacks")) { - Service.Get().ClearStacks(); + this.interfaceManager.ClearStacks(); } if (ImGui.MenuItem("Dump style")) @@ -792,7 +804,7 @@ internal class DalamudInterface : IDisposable, IServiceType { if (propertyInfo.PropertyType == typeof(Vector2)) { - var vec2 = (Vector2)propertyInfo.GetValue(style); + var vec2 = (Vector2)propertyInfo.GetValue(style)!; info += $"{propertyInfo.Name} = new Vector2({vec2.X.ToString(enCulture)}f, {vec2.Y.ToString(enCulture)}f),\n"; } else @@ -815,9 +827,9 @@ internal class DalamudInterface : IDisposable, IServiceType Log.Information(info); } - if (ImGui.MenuItem("Show dev bar info", null, configuration.ShowDevBarInfo)) + if (ImGui.MenuItem("Show dev bar info", null, this.configuration.ShowDevBarInfo)) { - configuration.ShowDevBarInfo = !configuration.ShowDevBarInfo; + this.configuration.ShowDevBarInfo = !this.configuration.ShowDevBarInfo; } ImGui.EndMenu(); @@ -827,7 +839,7 @@ internal class DalamudInterface : IDisposable, IServiceType { if (ImGui.MenuItem("Replace ExceptionHandler")) { - dalamud.ReplaceExceptionHandler(); + this.dalamud.ReplaceExceptionHandler(); } ImGui.EndMenu(); @@ -922,7 +934,7 @@ internal class DalamudInterface : IDisposable, IServiceType if (Service.Get().GameUiHidden) ImGui.BeginMenu("UI is hidden...", false); - if (configuration.ShowDevBarInfo) + if (this.configuration.ShowDevBarInfo) { ImGui.PushFont(InterfaceManager.MonoFont); @@ -931,9 +943,9 @@ internal class DalamudInterface : IDisposable, IServiceType ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false); - var videoMem = Service.Get().GetD3dMemoryInfo(); + var videoMem = this.interfaceManager.GetD3dMemoryInfo(); ImGui.BeginMenu( - !videoMem.HasValue ? $"V:???" : $"V:{Util.FormatBytes(videoMem.Value.Used)}", + !videoMem.HasValue ? "V:???" : $"V:{Util.FormatBytes(videoMem.Value.Used)}", false); ImGui.PopFont(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 9de87c6e3..c666a96a9 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1285,7 +1285,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// Represents an instance of InstanceManager with scene ready for use. /// - [ServiceManager.Service] + [ServiceManager.ProvidedService] public class InterfaceManagerWithScene : IServiceType { /// diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 4d1a8b5f0..e3f318223 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -67,7 +67,7 @@ internal sealed class ChangelogWindow : Window, IDisposable // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - this.MakeFont(); + Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); } private enum State @@ -98,7 +98,7 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - this.MakeFont(); + this.MakeFont(Service.Get()); this.state = State.WindowFadeIn; this.windowFade.Reset(); @@ -379,12 +379,6 @@ internal sealed class ChangelogWindow : Window, IDisposable this.logoTexture.Dispose(); } - private void MakeFont() - { - if (this.bannerFont == null) - { - var gfm = Service.Get(); - this.bannerFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); - } - } + private void MakeFont(GameFontManager gfm) => + this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 63045ed36..b285520d4 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -56,11 +56,10 @@ internal class ConsoleWindow : Window, IDisposable /// /// Initializes a new instance of the class. /// - public ConsoleWindow() + /// An instance of . + public ConsoleWindow(DalamudConfiguration configuration) : base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { - var configuration = Service.Get(); - this.autoScroll = configuration.LogAutoScroll; this.autoOpen = configuration.LogOpenAtStartup; SerilogEventSink.Instance.LogLine += this.OnLogLine; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 687526c9a..4233c169b 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -69,7 +69,7 @@ internal class PluginInstallerWindow : Window, IDisposable private string[] testerImagePaths = new string[5]; private string testerIconPath = string.Empty; - private IDalamudTextureWrap?[] testerImages; + private IDalamudTextureWrap?[]? testerImages; private IDalamudTextureWrap? testerIcon; private bool testerError = false; @@ -132,9 +132,10 @@ internal class PluginInstallerWindow : Window, IDisposable /// Initializes a new instance of the class. /// /// An instance of class. - public PluginInstallerWindow(PluginImageCache imageCache) + /// An instance of . + public PluginInstallerWindow(PluginImageCache imageCache, DalamudConfiguration configuration) : base( - Locs.WindowTitle + (Service.Get().DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", + Locs.WindowTitle + (configuration.DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar) { this.IsOpen = true; diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index a4ad62f4f..4034695e5 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -12,8 +12,8 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; + using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows; @@ -25,6 +25,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable private const float TargetFontSizePt = 18f; private const float TargetFontSizePx = TargetFontSizePt * 4 / 3; + private readonly ClientState clientState; + private readonly DalamudConfiguration configuration; + private readonly Framework framework; + private readonly GameGui gameGui; + private readonly TitleScreenMenu titleScreenMenu; + private readonly IDalamudTextureWrap shadeTexture; private readonly Dictionary shadeEasings = new(); @@ -39,12 +45,32 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// /// Initializes a new instance of the class. /// - public TitleScreenMenuWindow() + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + public TitleScreenMenuWindow( + ClientState clientState, + Dalamud dalamud, + DalamudConfiguration configuration, + Framework framework, + GameGui gameGui, + InterfaceManager interfaceManager, + TitleScreenMenu titleScreenMenu) : base( "TitleScreenMenuOverlay", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) { + this.clientState = clientState; + this.configuration = configuration; + this.framework = framework; + this.gameGui = gameGui; + this.titleScreenMenu = titleScreenMenu; + this.IsOpen = true; this.DisableWindowSounds = true; this.ForceMainWindow = true; @@ -53,17 +79,13 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; - var dalamud = Service.Get(); - var interfaceManager = Service.Get(); - var shadeTex = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmShade.png")); this.shadeTexture = shadeTex ?? throw new Exception("Could not load TSM background texture."); - var framework = Service.Get(); framework.Update += this.FrameworkOnUpdate; } - + private enum State { Hide, @@ -95,8 +117,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable public void Dispose() { this.shadeTexture.Dispose(); - var framework = Service.Get(); - framework.Update -= this.FrameworkOnUpdate; + this.framework.Update -= this.FrameworkOnUpdate; } /// @@ -106,9 +127,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable return; var scale = ImGui.GetIO().FontGlobalScale; - var entries = Service.Get().Entries - .OrderByDescending(x => x.IsInternal) - .ToList(); + var entries = this.titleScreenMenu.Entries.OrderByDescending(x => x.IsInternal).ToList(); switch (this.state) { @@ -369,17 +388,14 @@ internal class TitleScreenMenuWindow : Window, IDisposable private void FrameworkOnUpdate(IFramework framework) { - var clientState = Service.Get(); - this.IsOpen = !clientState.IsLoggedIn; + this.IsOpen = !this.clientState.IsLoggedIn; - var configuration = Service.Get(); - if (!configuration.ShowTsm) + if (!this.configuration.ShowTsm) this.IsOpen = false; - var gameGui = Service.Get(); - var charaSelect = gameGui.GetAddonByName("CharaSelect", 1); - var charaMake = gameGui.GetAddonByName("CharaMake", 1); - var titleDcWorldMap = gameGui.GetAddonByName("TitleDCWorldMap", 1); + var charaSelect = this.gameGui.GetAddonByName("CharaSelect", 1); + var charaMake = this.gameGui.GetAddonByName("CharaMake", 1); + var titleDcWorldMap = this.gameGui.GetAddonByName("TitleDCWorldMap", 1); if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index ce7ce25a1..5b141979e 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -16,7 +15,7 @@ namespace Dalamud.IoC.Internal; /// This is only used to resolve dependencies for plugins. /// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// -[ServiceManager.Service] +[ServiceManager.ProvidedService] internal class ServiceContainer : IServiceProvider, IServiceType { private static readonly ModuleLog Log = new("SERVICECONTAINER"); @@ -228,7 +227,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; - if (serviceType.GetCustomAttribute() != null) + if (serviceType.GetCustomAttribute() != null) { if (scope == null) { @@ -299,7 +298,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType var contains = types.Any(x => x.IsAssignableTo(type)); // Scoped services are created on-demand - return contains || type.GetCustomAttribute() != null; + return contains || type.GetCustomAttribute() != null; } var parameters = ctor.GetParameters(); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 9a651c64e..363d01f26 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -40,7 +39,7 @@ namespace Dalamud.Plugin.Internal; /// Class responsible for loading and unloading plugins. /// NOTE: ALL plugin exposed services are marked as dependencies for PluginManager in Service{T}. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] #pragma warning disable SA1015 // DalamudTextureWrap registers textures to dispose with IM @@ -85,6 +84,9 @@ internal partial class PluginManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly HappyHttpClient happyHttpClient = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly ChatGui chatGui = Service.Get(); + static PluginManager() { DalamudApiLevel = typeof(PluginManager).Assembly.GetName().Version!.Major; @@ -131,12 +133,13 @@ internal partial class PluginManager : IDisposable, IServiceType throw new InvalidDataException("Couldn't deserialize banned plugins manifest."); } - this.openInstallerWindowPluginChangelogsLink = Service.Get().AddChatLinkHandler("Dalamud", 1003, (_, _) => + this.openInstallerWindowPluginChangelogsLink = this.chatGui.AddChatLinkHandler("Dalamud", 1003, (_, _) => { Service.GetNullable()?.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.Changelogs); }); - this.configuration.PluginTestingOptIns ??= new List(); + this.configuration.PluginTestingOptIns ??= new(); + this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient); this.ApplyPatches(); } @@ -199,6 +202,11 @@ internal partial class PluginManager : IDisposable, IServiceType } } + /// + /// Gets the main repository. + /// + public PluginRepository MainRepo { get; } + /// /// Gets a list of all plugin repositories. The main repo should always be first. /// @@ -284,11 +292,9 @@ internal partial class PluginManager : IDisposable, IServiceType /// The header text to send to chat prior to any update info. public void PrintUpdatedPlugins(List? updateMetadata, string header) { - var chatGui = Service.Get(); - if (updateMetadata is { Count: > 0 }) { - chatGui.Print(new XivChatEntry + this.chatGui.Print(new XivChatEntry { Message = new SeString(new List() { @@ -307,11 +313,11 @@ internal partial class PluginManager : IDisposable, IServiceType { if (metadata.Status == PluginUpdateStatus.StatusKind.Success) { - chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); + this.chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); } else { - chatGui.Print(new XivChatEntry + this.chatGui.Print(new XivChatEntry { Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version, PluginUpdateStatus.LocalizeUpdateStatusKind(metadata.Status)), Type = XivChatType.Urgent, @@ -407,10 +413,10 @@ internal partial class PluginManager : IDisposable, IServiceType /// A representing the asynchronous operation. public async Task SetPluginReposFromConfigAsync(bool notify) { - var repos = new List() { PluginRepository.MainRepo }; + var repos = new List { this.MainRepo }; repos.AddRange(this.configuration.ThirdRepoList .Where(repo => repo.IsEnabled) - .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled))); + .Select(repo => new PluginRepository(this.happyHttpClient, repo.Url, repo.IsEnabled))); this.Repos = repos; await this.ReloadPluginMastersAsync(notify); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 5d132fd9c..91f1625a7 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -267,10 +267,6 @@ internal class LocalPlugin : IDisposable var pluginManager = await Service.GetAsync(); var dalamud = await Service.GetAsync(); - // UiBuilder constructor requires the following two. - await Service.GetAsync(); - await Service.GetAsync(); - if (this.manifest.LoadRequiredState == 0) _ = await Service.GetAsync(); diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index 3bf67ecd7..18c528910 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -28,47 +28,44 @@ internal class PluginRepository private static readonly ModuleLog Log = new("PLUGINR"); - private static readonly HttpClient HttpClient = new(new SocketsHttpHandler - { - AutomaticDecompression = DecompressionMethods.All, - ConnectCallback = Service.Get().SharedHappyEyeballsCallback.ConnectCallback, - }) - { - Timeout = TimeSpan.FromSeconds(20), - DefaultRequestHeaders = - { - Accept = - { - new MediaTypeWithQualityHeaderValue("application/json"), - }, - CacheControl = new CacheControlHeaderValue - { - NoCache = true, - }, - UserAgent = - { - new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), - }, - }, - }; + private readonly HttpClient httpClient; /// /// Initializes a new instance of the class. /// + /// An instance of . /// The plugin master URL. /// Whether the plugin repo is enabled. - public PluginRepository(string pluginMasterUrl, bool isEnabled) + public PluginRepository(HappyHttpClient happyHttpClient, string pluginMasterUrl, bool isEnabled) { + this.httpClient = new(new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = happyHttpClient.SharedHappyEyeballsCallback.ConnectCallback, + }) + { + Timeout = TimeSpan.FromSeconds(20), + DefaultRequestHeaders = + { + Accept = + { + new MediaTypeWithQualityHeaderValue("application/json"), + }, + CacheControl = new CacheControlHeaderValue + { + NoCache = true, + }, + UserAgent = + { + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + }, + }, + }; this.PluginMasterUrl = pluginMasterUrl; this.IsThirdParty = pluginMasterUrl != MainRepoUrl; this.IsEnabled = isEnabled; } - /// - /// Gets a new instance of the class for the main repo. - /// - public static PluginRepository MainRepo => new(MainRepoUrl, true); - /// /// Gets the pluginmaster.json URL. /// @@ -94,6 +91,14 @@ internal class PluginRepository /// public PluginRepositoryState State { get; private set; } + /// + /// Gets a new instance of the class for the main repo. + /// + /// An instance of . + /// The new instance of main repository. + public static PluginRepository CreateMainRepo(HappyHttpClient happyHttpClient) => + new(happyHttpClient, MainRepoUrl, true); + /// /// Reload the plugin master asynchronously in a task. /// @@ -107,7 +112,7 @@ internal class PluginRepository { Log.Information($"Fetching repo: {this.PluginMasterUrl}"); - using var response = await HttpClient.GetAsync(this.PluginMasterUrl); + using var response = await this.httpClient.GetAsync(this.PluginMasterUrl); response.EnsureSuccessStatusCode(); var data = await response.Content.ReadAsStringAsync(); diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index 5d0faabda..a3e314b80 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -13,7 +13,7 @@ namespace Dalamud.Plugin.Ipc.Internal; /// /// This class facilitates sharing data-references of standard types between plugins without using more expensive IPC. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal class DataShare : IServiceType { private readonly Dictionary caches = new(); diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 453fa3530..46a6ba509 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading; @@ -29,9 +30,17 @@ internal static class ServiceManager /// public static readonly ModuleLog Log = new("SVC"); - private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); +#if DEBUG + /// + /// Marks which service constructor the current thread's in. For use from only. + /// + internal static readonly ThreadLocal CurrentConstructorServiceType = new(); + [SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Debugging purposes")] private static readonly List LoadedServices = new(); +#endif + + private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); private static ManualResetEvent unloadResetEvent = new(false); @@ -86,21 +95,34 @@ internal static class ServiceManager /// Instance of . public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner) { +#if DEBUG lock (LoadedServices) { - void ProvideService(T service) where T : IServiceType - { - Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute"); - Service.Provide(service); - LoadedServices.Add(typeof(T)); - } - ProvideService(dalamud); ProvideService(fs); ProvideService(configuration); ProvideService(new ServiceContainer()); ProvideService(scanner); } + + return; + + void ProvideService(T service) where T : IServiceType + { + Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute"); + Service.Provide(service); + LoadedServices.Add(typeof(T)); + } +#else + ProvideService(dalamud); + ProvideService(fs); + ProvideService(configuration); + ProvideService(new ServiceContainer()); + ProvideService(scanner); + return; + + void ProvideService(T service) where T : IServiceType => Service.Provide(service); +#endif } /// @@ -171,7 +193,22 @@ internal static class ServiceManager { try { - await Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); + var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); + while (await Task.WhenAny(whenBlockingComplete, Task.Delay(30000)) != whenBlockingComplete) + { + if (NativeFunctions.MessageBoxW( + IntPtr.Zero, + "Dalamud is taking a long time to load. Would you like to continue without Dalamud?\n" + + "This can be caused by a faulty plugin, or a bug in Dalamud.", + "Dalamud", + NativeFunctions.MessageBoxType.IconWarning | NativeFunctions.MessageBoxType.YesNo) == 6) + { + throw new TimeoutException( + "Failed to load services in the given time limit, " + + "and the user chose to continue without Dalamud."); + } + } + BlockingServicesLoadedTaskCompletionSource.SetResult(); Timings.Event("BlockingServices Initialized"); } @@ -215,13 +252,14 @@ internal static class ServiceManager tasks.Add((Task)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( - "StartLoader", + nameof(Service.StartLoader), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, null)); servicesToLoad.Remove(serviceType); +#if DEBUG tasks.Add(tasks.Last().ContinueWith(task => { if (task.IsFaulted) @@ -231,6 +269,7 @@ internal static class ServiceManager LoadedServices.Add(serviceType); } })); +#endif } if (!tasks.Any()) @@ -350,10 +389,12 @@ internal static class ServiceManager null); } +#if DEBUG lock (LoadedServices) { LoadedServices.Clear(); } +#endif unloadResetEvent.Set(); } @@ -373,7 +414,7 @@ internal static class ServiceManager /// The type of service this type is. public static ServiceKind GetServiceKind(this Type type) { - var attr = type.GetCustomAttribute(true)?.GetType(); + var attr = type.GetCustomAttribute(true)?.GetType(); if (attr == null) return ServiceKind.None; @@ -381,13 +422,13 @@ internal static class ServiceManager type.IsAssignableTo(typeof(IServiceType)), "Service did not inherit from IServiceType"); - if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService))) + if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedServiceAttribute))) return ServiceKind.BlockingEarlyLoadedService; - if (attr.IsAssignableTo(typeof(EarlyLoadedService))) + if (attr.IsAssignableTo(typeof(EarlyLoadedServiceAttribute))) return ServiceKind.EarlyLoadedService; - if (attr.IsAssignableTo(typeof(ScopedService))) + if (attr.IsAssignableTo(typeof(ScopedServiceAttribute))) return ServiceKind.ScopedService; return ServiceKind.ProvidedService; @@ -414,16 +455,57 @@ internal static class ServiceManager /// Indicates that the class is a service. /// [AttributeUsage(AttributeTargets.Class)] - public class Service : Attribute + public abstract class ServiceAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// The kind of the service. + protected ServiceAttribute(ServiceKind kind) => this.Kind = kind; + + /// + /// Gets the kind of the service. + /// + public ServiceKind Kind { get; } + } + + /// + /// Indicates that the class is a service, that is provided by some other source. + /// + [AttributeUsage(AttributeTargets.Class)] + public class ProvidedServiceAttribute : ServiceAttribute + { + /// + /// Initializes a new instance of the class. + /// + public ProvidedServiceAttribute() + : base(ServiceKind.ProvidedService) + { + } } /// /// Indicates that the class is a service, and will be instantiated automatically on startup. /// [AttributeUsage(AttributeTargets.Class)] - public class EarlyLoadedService : Service + public class EarlyLoadedServiceAttribute : ServiceAttribute { + /// + /// Initializes a new instance of the class. + /// + public EarlyLoadedServiceAttribute() + : this(ServiceKind.EarlyLoadedService) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The service kind. + protected EarlyLoadedServiceAttribute(ServiceKind kind) + : base(kind) + { + } } /// @@ -431,8 +513,15 @@ internal static class ServiceManager /// blocking game main thread until it completes. /// [AttributeUsage(AttributeTargets.Class)] - public class BlockingEarlyLoadedService : EarlyLoadedService + public class BlockingEarlyLoadedServiceAttribute : EarlyLoadedServiceAttribute { + /// + /// Initializes a new instance of the class. + /// + public BlockingEarlyLoadedServiceAttribute() + : base(ServiceKind.BlockingEarlyLoadedService) + { + } } /// @@ -440,8 +529,15 @@ internal static class ServiceManager /// service scope, and that it cannot be created outside of a scope. /// [AttributeUsage(AttributeTargets.Class)] - public class ScopedService : Service + public class ScopedServiceAttribute : ServiceAttribute { + /// + /// Initializes a new instance of the class. + /// + public ScopedServiceAttribute() + : base(ServiceKind.ScopedService) + { + } } /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index b609c9082..9c7f0411d 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -1,6 +1,5 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -20,17 +19,26 @@ namespace Dalamud; /// Only used internally within Dalamud, if plugins need access to things it should be _only_ via DI. /// /// The class you want to store in the service locator. +[SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")] internal static class Service where T : IServiceType { + private static readonly ServiceManager.ServiceAttribute ServiceAttribute; private static TaskCompletionSource instanceTcs = new(); + private static List? dependencyServices; static Service() { - var exposeToPlugins = typeof(T).GetCustomAttribute() != null; + var type = typeof(T); + ServiceAttribute = + type.GetCustomAttribute(true) + ?? throw new InvalidOperationException( + $"{nameof(T)} is missing {nameof(ServiceManager.ServiceAttribute)} annotations."); + + var exposeToPlugins = type.GetCustomAttribute() != null; if (exposeToPlugins) - ServiceManager.Log.Debug("Service<{0}>: Static ctor called; will be exposed to plugins", typeof(T).Name); + ServiceManager.Log.Debug("Service<{0}>: Static ctor called; will be exposed to plugins", type.Name); else - ServiceManager.Log.Debug("Service<{0}>: Static ctor called", typeof(T).Name); + ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name); if (exposeToPlugins) Service.Get().RegisterSingleton(instanceTcs.Task); @@ -63,8 +71,8 @@ internal static class Service where T : IServiceType /// Object to set. public static void Provide(T obj) { - instanceTcs.SetResult(obj); ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name); + instanceTcs.SetResult(obj); } /// @@ -83,6 +91,21 @@ internal static class Service where T : IServiceType /// The object. public static T Get() { +#if DEBUG + if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService + && ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType) + { + var deps = ServiceHelpers.GetDependencies(currentServiceType); + if (!deps.Contains(typeof(T))) + { + throw new InvalidOperationException( + $"Calling {nameof(Service)}<{typeof(T)}>.{nameof(Get)} which is not one of the" + + $" dependency services is forbidden from the service constructor of {currentServiceType}." + + $" This has a high chance of introducing hard-to-debug hangs."); + } + } +#endif + if (!instanceTcs.Task.IsCompleted) instanceTcs.Task.Wait(); return instanceTcs.Task.Result; @@ -116,12 +139,16 @@ internal static class Service where T : IServiceType } /// - /// Gets an enumerable containing Service<T>s that are required for this Service to initialize without blocking. + /// Gets an enumerable containing s that are required for this Service to initialize + /// without blocking. /// /// List of dependency services. [UsedImplicitly] public static List GetDependencyServices() { + if (dependencyServices is not null) + return dependencyServices; + var res = new List(); ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name); @@ -189,19 +216,42 @@ internal static class Service where T : IServiceType ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name); } - return res - .Distinct() - .ToList(); + var deps = res + .Distinct() + .ToList(); + if (typeof(T).GetCustomAttribute() is not null) + { + var offenders = deps.Where( + x => x.GetCustomAttribute(true)!.Kind + is not ServiceManager.ServiceKind.BlockingEarlyLoadedService + and not ServiceManager.ServiceKind.ProvidedService) + .ToArray(); + if (offenders.Any()) + { + ServiceManager.Log.Error( + "{me} is a {bels}; it can only depend on {bels} and {ps}.\nOffending dependencies:\n{offenders}", + typeof(T), + nameof(ServiceManager.BlockingEarlyLoadedServiceAttribute), + nameof(ServiceManager.BlockingEarlyLoadedServiceAttribute), + nameof(ServiceManager.ProvidedServiceAttribute), + string.Join("\n", offenders.Select(x => $"\t* {x.Name}"))); + } + } + + return dependencyServices = deps; } - [UsedImplicitly] - private static Task StartLoader() + /// + /// Starts the service loader. Only to be called from . + /// + /// The loader task. + internal static Task StartLoader() { if (instanceTcs.Task.IsCompleted) throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed."); - var attr = typeof(T).GetCustomAttribute(true)?.GetType(); - if (attr?.IsAssignableTo(typeof(ServiceManager.EarlyLoadedService)) != true) + var attr = ServiceAttribute.GetType(); + if (attr.IsAssignableTo(typeof(ServiceManager.EarlyLoadedServiceAttribute)) != true) throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService"); return Task.Run(Timings.AttachTimingHandle(async () => @@ -212,6 +262,7 @@ internal static class Service where T : IServiceType var instance = await ConstructObject(); instanceTcs.SetResult(instance); + List? tasks = null; foreach (var method in typeof(T).GetMethods( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { @@ -221,9 +272,24 @@ internal static class Service where T : IServiceType ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name); var args = await Task.WhenAll(method.GetParameters().Select( x => ResolveServiceFromTypeAsync(x.ParameterType))); - method.Invoke(instance, args); + try + { + if (method.Invoke(instance, args) is Task task) + { + tasks ??= new(); + tasks.Add(task); + } + } + catch (Exception e) + { + tasks ??= new(); + tasks.Add(Task.FromException(e)); + } } + if (tasks is not null) + await Task.WhenAll(tasks); + ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name); return instance; } @@ -303,7 +369,19 @@ internal static class Service where T : IServiceType ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType))); using (Timings.Start($"{typeof(T).Name} Construct")) { +#if DEBUG + ServiceManager.CurrentConstructorServiceType.Value = typeof(Service); + try + { + return (T)ctor.Invoke(args)!; + } + finally + { + ServiceManager.CurrentConstructorServiceType.Value = null; + } +#else return (T)ctor.Invoke(args)!; +#endif } } @@ -328,30 +406,43 @@ internal static class Service where T : IServiceType internal static class ServiceHelpers { /// - /// Get a list of dependencies for a service. Only accepts Service<T> types. - /// These are returned as Service<T> types. + /// Get a list of dependencies for a service. Only accepts types. + /// These are returned as types. /// /// The dependencies for this service. /// A list of dependencies. public static List GetDependencies(Type serviceType) { +#if DEBUG + if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>)) + { + throw new ArgumentException( + $"Expected an instance of {nameof(Service)}<>", + nameof(serviceType)); + } +#endif + return (List)serviceType.InvokeMember( - "GetDependencyServices", - BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, - null, - null, - null) ?? new List(); + nameof(Service.GetDependencyServices), + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, + null, + null) ?? new List(); } /// - /// Get the Service<T> type for a given service type. + /// Get the type for a given service type. /// This will throw if the service type is not a valid service. /// - /// The type to obtain a Service<T> for. - /// The Service<T>. + /// The type to obtain a for. + /// The . public static Type GetAsService(Type type) { - return typeof(Service<>) - .MakeGenericType(type); +#if DEBUG + if (!type.IsAssignableTo(typeof(IServiceType))) + throw new ArgumentException($"Expected an instance of {nameof(IServiceType)}", nameof(type)); +#endif + + return typeof(Service<>).MakeGenericType(type); } } diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 9feb17c0d..a013e95b5 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -21,7 +21,7 @@ namespace Dalamud.Storage; /// /// This is not an early-loaded service, as it is needed before they are initialized. /// -[ServiceManager.Service] +[ServiceManager.ProvidedService] public class ReliableFileStorage : IServiceType, IDisposable { private static readonly ModuleLog Log = new("VFS"); From 01153a24805fb31b9db59dd7ac85d4588c86f636 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 21 Nov 2023 13:49:00 +0900 Subject: [PATCH 08/13] Add DisposeSafety --- Dalamud/Utility/DisposeSafety.cs | 392 +++++++++++++++++++++++++++++++ 1 file changed, 392 insertions(+) create mode 100644 Dalamud/Utility/DisposeSafety.cs diff --git a/Dalamud/Utility/DisposeSafety.cs b/Dalamud/Utility/DisposeSafety.cs new file mode 100644 index 000000000..909c4e932 --- /dev/null +++ b/Dalamud/Utility/DisposeSafety.cs @@ -0,0 +1,392 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Disposables; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Dalamud.Utility; + +/// +/// Utilities for disposing stuff. +/// +public static class DisposeSafety +{ + /// + /// Interface that marks a disposable that it can call back on dispose. + /// + public interface IDisposeCallback : IDisposable + { + /// + /// Event to be fired before object dispose. First parameter is the object iself. + /// + event Action? BeforeDispose; + + /// + /// Event to be fired after object dispose. First parameter is the object iself. + /// + event Action? AfterDispose; + } + + /// + /// Returns a proxy that on dispose will dispose the result of the given + /// .
+ /// If any exception has occurred, it will be ignored. + ///
+ /// The task. + /// A disposable type. + /// The proxy . + public static IDisposable ToDisposableIgnoreExceptions(this Task task) + where T : IDisposable + { + return Disposable.Create(() => task.ContinueWith(r => + { + _ = r.Exception; + if (r.IsCompleted) + { + try + { + r.Dispose(); + } + catch + { + // ignore + } + } + })); + } + + /// + /// Transforms into a , disposing the content as necessary. + /// + /// The task. + /// Ignore all exceptions. + /// A disposable type. + /// A wrapper for the task. + public static Task ToContentDisposedTask(this Task task, bool ignoreAllExceptions = false) + where T : IDisposable => task.ContinueWith( + r => + { + if (!r.IsCompletedSuccessfully) + return ignoreAllExceptions ? Task.CompletedTask : r; + try + { + r.Result.Dispose(); + } + catch (Exception e) + { + if (!ignoreAllExceptions) + { + return Task.FromException( + new AggregateException( + new[] { e }.Concat( + (IEnumerable)r.Exception?.InnerExceptions + ?? new[] { new OperationCanceledException() }))); + } + } + + return Task.CompletedTask; + }).Unwrap(); + + /// + /// Returns a proxy that on dispose will dispose all the elements of the given + /// of s. + /// + /// The disposables. + /// The disposable types. + /// The proxy . + /// Error. + public static IDisposable AggregateToDisposable(this IEnumerable? disposables) + where T : IDisposable + { + if (disposables is not T[] array) + array = disposables?.ToArray() ?? Array.Empty(); + + return Disposable.Create(() => + { + List exceptions = null; + foreach (var d in array) + { + try + { + d?.Dispose(); + } + catch (Exception de) + { + exceptions ??= new(); + exceptions.Add(de); + } + } + + if (exceptions is not null) + throw new AggregateException(exceptions); + }); + } + + /// + /// Utility class for managing finalizing stuff. + /// + public class ScopedFinalizer : IDisposeCallback, IAsyncDisposable + { + private readonly List objects = new(); + + /// + public event Action? BeforeDispose; + + /// + public event Action? AfterDispose; + + /// + public void EnsureCapacity(int capacity) => this.objects.EnsureCapacity(capacity); + + /// + /// The parameter. + [return: NotNullIfNotNull(nameof(d))] + public T? Add(T? d) where T : IDisposable + { + if (d is not null) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + [return: NotNullIfNotNull(nameof(d))] + public Action? Add(Action? d) + { + if (d is not null) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + [return: NotNullIfNotNull(nameof(d))] + public Func? Add(Func? d) + { + if (d is not null) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + public GCHandle Add(GCHandle d) + { + if (d != default) + this.objects.Add(this.CheckAdd(d)); + + return d; + } + + /// + /// Queue all the given to be disposed later. + /// + /// Disposables. + public void AddRange(IEnumerable ds) => + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + + /// + /// Queue all the given to be run later. + /// + /// Actions. + public void AddRange(IEnumerable ds) => + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + + /// + /// Queue all the given returning to be run later. + /// + /// Func{Task}s. + public void AddRange(IEnumerable?> ds) => + this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d))); + + /// + /// Queue all the given to be disposed later. + /// + /// GCHandles. + public void AddRange(IEnumerable ds) => + this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d))); + + /// + /// Cancel all pending disposals. + /// + /// Use this after successful initialization of multiple disposables. + public void Cancel() + { + foreach (var o in this.objects) + this.CheckRemove(o); + this.objects.Clear(); + } + + /// + /// This for method chaining. + public ScopedFinalizer WithEnsureCapacity(int capacity) + { + this.EnsureCapacity(capacity); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(IDisposable d) + { + this.Add(d); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(Action d) + { + this.Add(d); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(Func d) + { + this.Add(d); + return this; + } + + /// + /// This for method chaining. + public ScopedFinalizer With(GCHandle d) + { + this.Add(d); + return this; + } + + /// + public void Dispose() + { + this.BeforeDispose?.InvokeSafely(this); + + List? exceptions = null; + while (this.objects.Any()) + { + var obj = this.objects[^1]; + this.objects.RemoveAt(this.objects.Count - 1); + + try + { + switch (obj) + { + case IDisposable x: + x.Dispose(); + break; + case Action a: + a.Invoke(); + break; + case Func a: + a.Invoke().Wait(); + break; + case GCHandle a: + a.Free(); + break; + } + } + catch (Exception ex) + { + exceptions ??= new(); + exceptions.Add(ex); + } + } + + this.objects.TrimExcess(); + + if (exceptions is not null) + { + var exs = exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); + try + { + this.AfterDispose?.Invoke(this, exs); + } + catch + { + // whatever + } + + throw exs; + } + } + + /// + public async ValueTask DisposeAsync() + { + this.BeforeDispose?.InvokeSafely(this); + + List? exceptions = null; + while (this.objects.Any()) + { + var obj = this.objects[^1]; + this.objects.RemoveAt(this.objects.Count - 1); + + try + { + switch (obj) + { + case IAsyncDisposable x: + await x.DisposeAsync(); + break; + case IDisposable x: + x.Dispose(); + break; + case Func a: + await a.Invoke(); + break; + case Action a: + a.Invoke(); + break; + case GCHandle a: + a.Free(); + break; + } + } + catch (Exception ex) + { + exceptions ??= new(); + exceptions.Add(ex); + } + } + + this.objects.TrimExcess(); + + if (exceptions is not null) + { + var exs = exceptions.Count == 1 ? exceptions[0] : new AggregateException(exceptions); + try + { + this.AfterDispose?.Invoke(this, exs); + } + catch + { + // whatever + } + + throw exs; + } + } + + private T CheckAdd(T item) + { + if (item is IDisposeCallback dc) + dc.BeforeDispose += this.OnItemDisposed; + + return item; + } + + private void CheckRemove(object item) + { + if (item is IDisposeCallback dc) + dc.BeforeDispose -= this.OnItemDisposed; + } + + private void OnItemDisposed(IDisposeCallback obj) + { + obj.BeforeDispose -= this.OnItemDisposed; + this.objects.Remove(obj); + } + } +} From a72f407357a748a1dc2ceedec02e276ae95efa6b Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Tue, 21 Nov 2023 13:49:23 +0900 Subject: [PATCH 09/13] Add DalamudAssetManager --- Dalamud/DalamudAsset.cs | 146 +++++++ Dalamud/Interface/Internal/Branding.cs | 58 --- .../Interface/Internal/DalamudInterface.cs | 21 +- .../Interface/Internal/InterfaceManager.cs | 12 +- .../Internal/Windows/ChangelogWindow.cs | 7 +- .../Internal/Windows/PluginImageCache.cs | 106 ++--- .../Windows/Settings/Tabs/SettingsTabAbout.cs | 7 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 15 +- .../Storage/Assets/DalamudAssetAttribute.cs | 36 ++ .../Storage/Assets/DalamudAssetExtensions.cs | 17 + Dalamud/Storage/Assets/DalamudAssetManager.cs | 365 ++++++++++++++++++ .../DalamudAssetOnlineSourceAttribute.cs | 48 +++ .../Assets/DalamudAssetPathAttribute.cs | 21 + Dalamud/Storage/Assets/DalamudAssetPurpose.cs | 27 ++ .../Assets/DalamudAssetRawTextureAttribute.cs | 45 +++ .../Storage/Assets/IDalamudAssetManager.cs | 79 ++++ Dalamud/Utility/EnumExtensions.cs | 36 +- 17 files changed, 867 insertions(+), 179 deletions(-) create mode 100644 Dalamud/DalamudAsset.cs delete mode 100644 Dalamud/Interface/Internal/Branding.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetAttribute.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetExtensions.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetManager.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetPurpose.cs create mode 100644 Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs create mode 100644 Dalamud/Storage/Assets/IDalamudAssetManager.cs diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs new file mode 100644 index 000000000..5b641c487 --- /dev/null +++ b/Dalamud/DalamudAsset.cs @@ -0,0 +1,146 @@ +using Dalamud.Storage.Assets; + +namespace Dalamud; + +/// +/// Specifies an asset that has been shipped as Dalamud Asset.
+/// Any asset can cease to exist at any point, even if the enum value exists.
+/// Either ship your own assets, or be prepared for errors. +///
+public enum DalamudAsset +{ + /// + /// Nothing. + /// + [DalamudAsset(DalamudAssetPurpose.Empty, data: new byte[0])] + Unspecified = 0, + + /// + /// : The fallback empty texture. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 })] + [DalamudAssetRawTexture(4, 8, 4, SharpDX.DXGI.Format.BC1_UNorm)] + Empty4X4 = 1000, + + /// + /// : The Dalamud logo. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "logo.png")] + Logo = 1001, + + /// + /// : The Dalamud logo, but smaller. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "tsmLogo.png")] + LogoSmall = 1002, + + /// + /// : The default plugin icon. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "defaultIcon.png")] + DefaultIcon = 1003, + + /// + /// : The disabled plugin icon. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "disabledIcon.png")] + DisabledIcon = 1004, + + /// + /// : The outdated installable plugin icon. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "outdatedInstallableIcon.png")] + OutdatedInstallableIcon = 1005, + + /// + /// : The plugin trouble icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "troubleIcon.png")] + TroubleIcon = 1006, + + /// + /// : The plugin update icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "updateIcon.png")] + UpdateIcon = 1007, + + /// + /// : The plugin installed icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "installedIcon.png")] + InstalledIcon = 1008, + + /// + /// : The third party plugin icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "thirdIcon.png")] + ThirdIcon = 1009, + + /// + /// : The installed third party plugin icon overlay. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "thirdInstalledIcon.png")] + ThirdInstalledIcon = 1010, + + /// + /// : The API bump explainer icon. + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "changelogApiBump.png")] + ChangelogApiBumpIcon = 1011, + + /// + /// : The background shade for + /// . + /// + [DalamudAsset(DalamudAssetPurpose.TextureFromPng)] + [DalamudAssetPath("UIRes", "tsmShade.png")] + TitleScreenMenuShade = 1012, + + /// + /// : Noto Sans CJK JP Medium. + /// + [DalamudAsset(DalamudAssetPurpose.Font)] + [DalamudAssetPath("UIRes", "NotoSansCJKjp-Regular.otf")] + [DalamudAssetPath("UIRes", "NotoSansCJKjp-Medium.otf")] + NotoSansJpMedium = 2000, + + /// + /// : Noto Sans CJK KR Regular. + /// + [DalamudAsset(DalamudAssetPurpose.Font)] + [DalamudAssetPath("UIRes", "NotoSansCJKkr-Regular.otf")] + [DalamudAssetPath("UIRes", "NotoSansKR-Regular.otf")] + NotoSansKrRegular = 2001, + + /// + /// : Inconsolata Regular. + /// + [DalamudAsset(DalamudAssetPurpose.Font)] + [DalamudAssetPath("UIRes", "Inconsolata-Regular.ttf")] + InconsolataRegular = 2002, + + /// + /// : FontAwesome Free Solid. + /// + [DalamudAsset(DalamudAssetPurpose.Font)] + [DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")] + FontAwesomeFreeSolid = 2003, + + /// + /// : Game symbol fonts being used as webfonts at Lodestone. + /// + [DalamudAsset(DalamudAssetPurpose.Font, required: true)] + [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")] + LodestoneGameSymbol = 2004, +} diff --git a/Dalamud/Interface/Internal/Branding.cs b/Dalamud/Interface/Internal/Branding.cs deleted file mode 100644 index 4162cabeb..000000000 --- a/Dalamud/Interface/Internal/Branding.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.IO; - -using Dalamud.IoC.Internal; - -namespace Dalamud.Interface.Internal; - -/// -/// Class containing various textures used by Dalamud windows for branding purposes. -/// -[ServiceManager.EarlyLoadedService] -#pragma warning disable SA1015 -[InherentDependency] // Can't load textures before this -#pragma warning restore SA1015 -internal class Branding : IServiceType, IDisposable -{ - private readonly Dalamud dalamud; - private readonly TextureManager tm; - - /// - /// Initializes a new instance of the class. - /// - /// Dalamud instance. - /// TextureManager instance. - [ServiceManager.ServiceConstructor] - public Branding(Dalamud dalamud, TextureManager tm) - { - this.dalamud = dalamud; - this.tm = tm; - - this.LoadTextures(); - } - - /// - /// Gets a full-size Dalamud logo texture. - /// - public IDalamudTextureWrap Logo { get; private set; } = null!; - - /// - /// Gets a small Dalamud logo texture. - /// - public IDalamudTextureWrap LogoSmall { get; private set; } = null!; - - /// - public void Dispose() - { - this.Logo.Dispose(); - this.LogoSmall.Dispose(); - } - - private void LoadTextures() - { - this.Logo = this.tm.GetTextureFromFile(new FileInfo(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png"))) - ?? throw new Exception("Could not load logo."); - - this.LogoSmall = this.tm.GetTextureFromFile(new FileInfo(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png"))) - ?? throw new Exception("Could not load TSM logo."); - } -} diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 18ab538c4..1a6e71194 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -26,6 +26,7 @@ using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; +using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.System.Framework; @@ -93,7 +94,7 @@ internal class DalamudInterface : IDisposable, IServiceType DalamudConfiguration configuration, InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, PluginImageCache pluginImageCache, - Branding branding, + DalamudAssetManager dalamudAssetManager, Game.Framework framework, ClientState clientState, TitleScreenMenu titleScreenMenu, @@ -118,11 +119,10 @@ internal class DalamudInterface : IDisposable, IServiceType this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; this.titleScreenMenuWindow = new TitleScreenMenuWindow( clientState, - dalamud, configuration, + dalamudAssetManager, framework, gameGui, - this.interfaceManager, titleScreenMenu) { IsOpen = false }; this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; @@ -152,12 +152,21 @@ internal class DalamudInterface : IDisposable, IServiceType this.interfaceManager.Draw += this.OnDraw; var tsm = Service.Get(); - tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, () => this.OpenPluginInstaller()); - tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), branding.LogoSmall, this.OpenSettings); + tsm.AddEntryCore( + Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + this.OpenPluginInstaller); + tsm.AddEntryCore( + Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + this.OpenSettings); if (!configuration.DalamudBetaKind.IsNullOrEmpty()) { - tsm.AddEntryCore(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), branding.LogoSmall, () => this.isImGuiDrawDevMenu = true); + tsm.AddEntryCore( + Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + () => this.isImGuiDrawDevMenu = true); } this.creditsDarkeningAnimation.Point1 = Vector2.Zero; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index c666a96a9..52e849c0e 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Windowing; +using Dalamud.Storage.Assets; using Dalamud.Utility; using Dalamud.Utility.Timing; using ImGuiNET; @@ -1063,10 +1064,15 @@ internal class InterfaceManager : IDisposable, IServiceType } [ServiceManager.CallWhenServicesReady] - private void ContinueConstruction(TargetSigScanner sigScanner, Framework framework) + private void ContinueConstruction( + TargetSigScanner sigScanner, + DalamudAssetManager dalamudAssetManager, + DalamudConfiguration configuration) { + dalamudAssetManager.WaitForAllRequiredAssets().Wait(); + this.address.Setup(sigScanner); - framework.RunOnFrameworkThread(() => + this.framework.RunOnFrameworkThread(() => { while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero) { @@ -1078,7 +1084,7 @@ internal class InterfaceManager : IDisposable, IServiceType try { - if (Service.Get().WindowIsImmersive) + if (configuration.WindowIsImmersive) this.SetImmersiveMode(true); } catch (Exception ex) diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index e3f318223..b9e7ab686 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; +using Dalamud.Storage.Assets; using Dalamud.Utility; using ImGuiNET; @@ -32,7 +33,6 @@ internal sealed class ChangelogWindow : Window, IDisposable "; private readonly TitleScreenMenuWindow tsmWindow; - private readonly IDalamudTextureWrap logoTexture; private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f)) { @@ -47,6 +47,7 @@ internal sealed class ChangelogWindow : Window, IDisposable }; private IDalamudTextureWrap? apiBumpExplainerTexture; + private IDalamudTextureWrap? logoTexture; private GameFontHandle? bannerFont; private State state = State.WindowFadeIn; @@ -63,8 +64,6 @@ internal sealed class ChangelogWindow : Window, IDisposable this.tsmWindow = tsmWindow; this.Namespace = "DalamudChangelogWindow"; - this.logoTexture = Service.Get().Logo; - // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); @@ -188,6 +187,7 @@ internal sealed class ChangelogWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f))) { + this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); ImGui.Image(this.logoTexture.ImGuiHandle, logoSize); } } @@ -376,7 +376,6 @@ internal sealed class ChangelogWindow : Window, IDisposable /// public void Dispose() { - this.logoTexture.Dispose(); } private void MakeFont(GameFontManager gfm) => diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index b721b08c3..528507229 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; @@ -12,8 +11,8 @@ using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; using Dalamud.Plugin.Internal.Types.Manifest; +using Dalamud.Storage.Assets; using Dalamud.Utility; -using ImGuiScene; using Serilog; namespace Dalamud.Interface.Internal.Windows; @@ -47,12 +46,6 @@ internal class PluginImageCache : IDisposable, IServiceType private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api6/{0}/{1}/images/{2}"; private const string MainRepoDip17ImageUrl = "https://raw.githubusercontent.com/goatcorp/PluginDistD17/main/{0}/{1}/images/{2}"; - [ServiceManager.ServiceDependency] - private readonly InterfaceManager.InterfaceManagerWithScene imWithScene = Service.Get(); - - [ServiceManager.ServiceDependency] - private readonly Branding branding = Service.Get(); - [ServiceManager.ServiceDependency] private readonly HappyHttpClient happyHttpClient = Service.Get(); @@ -64,35 +57,12 @@ internal class PluginImageCache : IDisposable, IServiceType private readonly ConcurrentDictionary pluginIconMap = new(); private readonly ConcurrentDictionary pluginImagesMap = new(); - - private readonly Task emptyTextureTask; - private readonly Task disabledIconTask; - private readonly Task outdatedInstallableIconTask; - private readonly Task defaultIconTask; - private readonly Task troubleIconTask; - private readonly Task updateIconTask; - private readonly Task installedIconTask; - private readonly Task thirdIconTask; - private readonly Task thirdInstalledIconTask; - private readonly Task corePluginIconTask; + private readonly DalamudAssetManager dalamudAssetManager; [ServiceManager.ServiceConstructor] - private PluginImageCache(Dalamud dalamud) + private PluginImageCache(Dalamud dalamud, DalamudAssetManager dalamudAssetManager) { - Task? TaskWrapIfNonNull(IDalamudTextureWrap? tw) => tw == null ? null : Task.FromResult(tw!); - var imwst = Task.Run(() => this.imWithScene); - - this.emptyTextureTask = imwst.ContinueWith(task => task.Result.Manager.LoadImageRaw(new byte[64], 8, 8, 4)!); - this.defaultIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.disabledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "disabledIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.outdatedInstallableIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "outdatedInstallableIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.troubleIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.updateIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.installedIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "installedIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.thirdIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.thirdInstalledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdInstalledIcon.png"))) ?? this.emptyTextureTask).Unwrap(); - this.corePluginIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(this.branding.LogoSmall)).Unwrap(); - + this.dalamudAssetManager = dalamudAssetManager; this.downloadTask = Task.Factory.StartNew( () => this.DownloadTask(8), TaskCreationOptions.LongRunning); this.loadTask = Task.Factory.StartNew( @@ -102,72 +72,62 @@ internal class PluginImageCache : IDisposable, IServiceType /// /// Gets the fallback empty texture. /// - public IDalamudTextureWrap EmptyTexture => this.emptyTextureTask.IsCompleted - ? this.emptyTextureTask.Result - : this.emptyTextureTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap EmptyTexture => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.Empty4X4); /// /// Gets the disabled plugin icon. /// - public IDalamudTextureWrap DisabledIcon => this.disabledIconTask.IsCompleted - ? this.disabledIconTask.Result - : this.disabledIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap DisabledIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DisabledIcon, this.EmptyTexture); /// /// Gets the outdated installable plugin icon. /// - public IDalamudTextureWrap OutdatedInstallableIcon => this.outdatedInstallableIconTask.IsCompleted - ? this.outdatedInstallableIconTask.Result - : this.outdatedInstallableIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap OutdatedInstallableIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.OutdatedInstallableIcon, this.EmptyTexture); /// /// Gets the default plugin icon. /// - public IDalamudTextureWrap DefaultIcon => this.defaultIconTask.IsCompleted - ? this.defaultIconTask.Result - : this.defaultIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap DefaultIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DefaultIcon, this.EmptyTexture); /// /// Gets the plugin trouble icon overlay. /// - public IDalamudTextureWrap TroubleIcon => this.troubleIconTask.IsCompleted - ? this.troubleIconTask.Result - : this.troubleIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap TroubleIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TroubleIcon, this.EmptyTexture); /// /// Gets the plugin update icon overlay. /// - public IDalamudTextureWrap UpdateIcon => this.updateIconTask.IsCompleted - ? this.updateIconTask.Result - : this.updateIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap UpdateIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.UpdateIcon, this.EmptyTexture); /// /// Gets the plugin installed icon overlay. /// - public IDalamudTextureWrap InstalledIcon => this.installedIconTask.IsCompleted - ? this.installedIconTask.Result - : this.installedIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap InstalledIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.InstalledIcon, this.EmptyTexture); /// /// Gets the third party plugin icon overlay. /// - public IDalamudTextureWrap ThirdIcon => this.thirdIconTask.IsCompleted - ? this.thirdIconTask.Result - : this.thirdIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap ThirdIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.ThirdIcon, this.EmptyTexture); /// /// Gets the installed third party plugin icon overlay. /// - public IDalamudTextureWrap ThirdInstalledIcon => this.thirdInstalledIconTask.IsCompleted - ? this.thirdInstalledIconTask.Result - : this.thirdInstalledIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap ThirdInstalledIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon, this.EmptyTexture); /// /// Gets the core plugin icon. /// - public IDalamudTextureWrap CorePluginIcon => this.corePluginIconTask.IsCompleted - ? this.corePluginIconTask.Result - : this.corePluginIconTask.GetAwaiter().GetResult(); + public IDalamudTextureWrap CorePluginIcon => + this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall, this.EmptyTexture); /// public void Dispose() @@ -185,22 +145,6 @@ internal class PluginImageCache : IDisposable, IServiceType this.downloadQueue.Dispose(); this.loadQueue.Dispose(); - foreach (var task in new[] - { - this.defaultIconTask, - this.troubleIconTask, - this.updateIconTask, - this.installedIconTask, - this.thirdIconTask, - this.thirdInstalledIconTask, - this.corePluginIconTask, - }) - { - task.Wait(); - if (task.IsCompletedSuccessfully) - task.Result.Dispose(); - } - foreach (var icon in this.pluginIconMap.Values) { icon?.Dispose(); @@ -319,7 +263,7 @@ internal class PluginImageCache : IDisposable, IServiceType if (bytes == null) return null; - var interfaceManager = this.imWithScene.Manager; + var interfaceManager = (await Service.GetAsync()).Manager; var framework = await Service.GetAsync(); IDalamudTextureWrap? image; diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs index 9b6a32617..5b6f6b02f 100644 --- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs +++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs @@ -11,6 +11,7 @@ using Dalamud.Interface.GameFonts; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin.Internal; +using Dalamud.Storage.Assets; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game.UI; using ImGuiNET; @@ -171,19 +172,16 @@ Dalamud is licensed under AGPL v3 or later. Contribute at: https://github.com/goatcorp/Dalamud "; - private readonly IDalamudTextureWrap logoTexture; private readonly Stopwatch creditsThrottler; private string creditsText; private bool resetNow = false; + private IDalamudTextureWrap? logoTexture; private GameFontHandle? thankYouFont; public SettingsTabAbout() { - var branding = Service.Get(); - - this.logoTexture = branding.Logo; this.creditsThrottler = new(); } @@ -251,6 +249,7 @@ Contribute at: https://github.com/goatcorp/Dalamud const float imageSize = 190f; ImGui.SameLine((ImGui.GetWindowWidth() / 2) - (imageSize / 2)); + this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo); ImGui.Image(this.logoTexture.ImGuiHandle, ImGuiHelpers.ScaledVector2(imageSize)); ImGuiHelpers.ScaledDummy(0, 20f); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index 4034695e5..f60ebe4ef 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.IO; using System.Linq; using System.Numerics; @@ -12,6 +11,7 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; +using Dalamud.Storage.Assets; using ImGuiNET; @@ -46,19 +46,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// Initializes a new instance of the class. /// /// An instance of . - /// An instance of . /// An instance of . + /// An instance of . /// An instance of . - /// An instance of . /// An instance of . /// An instance of . public TitleScreenMenuWindow( ClientState clientState, - Dalamud dalamud, DalamudConfiguration configuration, + DalamudAssetManager dalamudAssetManager, Framework framework, GameGui gameGui, - InterfaceManager interfaceManager, TitleScreenMenu titleScreenMenu) : base( "TitleScreenMenuOverlay", @@ -79,9 +77,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; - var shadeTex = - interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmShade.png")); - this.shadeTexture = shadeTex ?? throw new Exception("Could not load TSM background texture."); + this.shadeTexture = dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade); framework.Update += this.FrameworkOnUpdate; } @@ -116,7 +112,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// public void Dispose() { - this.shadeTexture.Dispose(); this.framework.Update -= this.FrameworkOnUpdate; } @@ -386,7 +381,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable return isHover; } - private void FrameworkOnUpdate(IFramework framework) + private void FrameworkOnUpdate(IFramework unused) { this.IsOpen = !this.clientState.IsLoggedIn; diff --git a/Dalamud/Storage/Assets/DalamudAssetAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetAttribute.cs new file mode 100644 index 000000000..a3527cdbc --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetAttribute.cs @@ -0,0 +1,36 @@ +namespace Dalamud.Storage.Assets; + +/// +/// Stores the basic information of a Dalamud asset. +/// +[AttributeUsage(AttributeTargets.Field)] +internal class DalamudAssetAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The purpose. + /// The data. + /// Whether the asset is required. + public DalamudAssetAttribute(DalamudAssetPurpose purpose, byte[]? data = null, bool required = true) + { + this.Purpose = purpose; + this.Data = data; + this.Required = required; + } + + /// + /// Gets the purpose of the asset. + /// + public DalamudAssetPurpose Purpose { get; } + + /// + /// Gets the data, if available. + /// + public byte[]? Data { get; } + + /// + /// Gets a value indicating whether the asset is required. + /// + public bool Required { get; } +} diff --git a/Dalamud/Storage/Assets/DalamudAssetExtensions.cs b/Dalamud/Storage/Assets/DalamudAssetExtensions.cs new file mode 100644 index 000000000..9181f1a5d --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetExtensions.cs @@ -0,0 +1,17 @@ +using Dalamud.Utility; + +namespace Dalamud.Storage.Assets; + +/// +/// Extension methods for . +/// +public static class DalamudAssetExtensions +{ + /// + /// Gets the purpose. + /// + /// The asset. + /// The purpose. + public static DalamudAssetPurpose GetPurpose(this DalamudAsset asset) => + asset.GetAttribute()?.Purpose ?? DalamudAssetPurpose.Empty; +} diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs new file mode 100644 index 000000000..bbfd60636 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -0,0 +1,365 @@ +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Networking.Http; +using Dalamud.Utility; +using Dalamud.Utility.Timing; + +using JetBrains.Annotations; + +using Serilog; + +namespace Dalamud.Storage.Assets; + +/// +/// A concrete class for . +/// +[PluginInterface] +[ServiceManager.BlockingEarlyLoadedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudAssetManager +{ + private const int DownloadAttemptCount = 10; + private const int RenameAttemptCount = 10; + + private readonly object syncRoot = new(); + private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); + private readonly Dictionary?> fileStreams; + private readonly Dictionary?> textureWraps; + private readonly Dalamud dalamud; + private readonly HappyHttpClient httpClient; + private readonly string localSourceDirectory; + private readonly CancellationTokenSource cancellationTokenSource; + + private bool isDisposed; + + [ServiceManager.ServiceConstructor] + private DalamudAssetManager(Dalamud dalamud, HappyHttpClient httpClient) + { + this.dalamud = dalamud; + this.httpClient = httpClient; + this.localSourceDirectory = Path.Combine(this.dalamud.AssetDirectory.FullName, "..", "local"); + Directory.CreateDirectory(this.localSourceDirectory); + this.scopedFinalizer.Add(this.cancellationTokenSource = new()); + + this.fileStreams = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); + this.textureWraps = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null); + + var loadTimings = Timings.Start("DAM LoadAll"); + this.WaitForAllRequiredAssets().ContinueWith(_ => loadTimings.Dispose()); + } + + /// + public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4); + + /// + public void Dispose() + { + lock (this.syncRoot) + { + if (this.isDisposed) + return; + + this.isDisposed = true; + } + + this.cancellationTokenSource.Cancel(); + Task.WaitAll( + Array.Empty() + .Concat(this.fileStreams.Values) + .Concat(this.textureWraps.Values) + .Where(x => x is not null) + .ToArray()); + this.scopedFinalizer.Dispose(); + } + + /// + /// Waits for all the required assets to be ready. Will result in a faulted task, if any of the required assets + /// has failed to load. + /// + /// The task. + [Pure] + public Task WaitForAllRequiredAssets() + { + lock (this.syncRoot) + { + return Task.WhenAll( + Enum.GetValues() + .Where(x => x is not DalamudAsset.Empty4X4) + .Select(this.CreateStreamAsync) + .Select(x => x.ToContentDisposedTask())); + } + } + + /// + [Pure] + public bool IsStreamImmediatelyAvailable(DalamudAsset asset) => + asset.GetAttribute()?.Data is not null + || this.fileStreams[asset]?.IsCompletedSuccessfully is true; + + /// + [Pure] + public Stream CreateStream(DalamudAsset asset) + { + var s = this.CreateStreamAsync(asset); + s.Wait(); + if (s.IsCompletedSuccessfully) + return s.Result; + if (s.Exception is not null) + throw new AggregateException(s.Exception.InnerExceptions); + throw new OperationCanceledException(); + } + + /// + [Pure] + public Task CreateStreamAsync(DalamudAsset asset) + { + if (asset.GetAttribute() is { Data: { } rawData }) + return Task.FromResult(new MemoryStream(rawData, false)); + + Task task; + lock (this.syncRoot) + { + if (this.isDisposed) + throw new ObjectDisposedException(nameof(DalamudAssetManager)); + + task = this.fileStreams[asset] ??= CreateInnerAsync(); + } + + return this.TransformImmediate( + task, + x => (Stream)new FileStream( + x.Name, + FileMode.Open, + FileAccess.Read, + FileShare.Read, + 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan)); + + async Task CreateInnerAsync() + { + string path; + List exceptions = null; + foreach (var name in asset.GetAttributes().Select(x => x.FileName)) + { + if (!File.Exists(path = Path.Combine(this.dalamud.AssetDirectory.FullName, name))) + continue; + + try + { + return File.OpenRead(path); + } + catch (Exception e) when (e is not OperationCanceledException) + { + exceptions ??= new(); + exceptions.Add(e); + } + } + + if (File.Exists(path = Path.Combine(this.localSourceDirectory, asset.ToString()))) + { + try + { + return File.OpenRead(path); + } + catch (Exception e) when (e is not OperationCanceledException) + { + exceptions ??= new(); + exceptions.Add(e); + } + } + + var tempPath = $"{path}.{Environment.ProcessId:x}.{Environment.CurrentManagedThreadId:x}"; + try + { + for (var i = 0; i < DownloadAttemptCount; i++) + { + var attemptedAny = false; + foreach (var url in asset.GetAttributes()) + { + Log.Information("[{who}] {asset}: Trying {url}", nameof(DalamudAssetManager), asset, url); + attemptedAny = true; + + try + { + await using var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write); + await url.DownloadAsync( + this.httpClient.SharedHttpClient, + tempPathStream, + this.cancellationTokenSource.Token); + tempPathStream.Dispose(); + for (var j = RenameAttemptCount; ; j--) + { + try + { + File.Move(tempPath, path); + } + catch (IOException ioe) + { + if (j == 0) + throw; + Log.Warning( + ioe, + "[{who}] {asset}: Renaming failed; trying again {n} more times", + nameof(DalamudAssetManager), + asset, + j); + await Task.Delay(1000, this.cancellationTokenSource.Token); + continue; + } + + return File.OpenRead(path); + } + } + catch (Exception e) when (e is not OperationCanceledException) + { + Log.Error(e, "[{who}] {asset}: Failed {url}", nameof(DalamudAssetManager), asset, url); + } + } + + if (!attemptedAny) + throw new FileNotFoundException($"Failed to find the asset {asset}.", asset.ToString()); + + // Wait up to 5 minutes + var delay = Math.Min(300, (1 << i) * 1000); + Log.Error( + "[{who}] {asset}: Failed to download. Trying again in {sec} seconds...", + nameof(DalamudAssetManager), + asset, + delay); + await Task.Delay(delay * 1000, this.cancellationTokenSource.Token); + } + + throw new FileNotFoundException($"Failed to load the asset {asset}.", asset.ToString()); + } + catch (Exception e) when (e is not OperationCanceledException) + { + exceptions ??= new(); + exceptions.Add(e); + try + { + File.Delete(tempPath); + } + catch + { + // don't care + } + } + + throw new AggregateException(exceptions); + } + } + + /// + [Pure] + public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) => + ExtractResult(this.GetDalamudTextureWrapAsync(asset)); + + /// + [Pure] + [return: NotNullIfNotNull(nameof(defaultWrap))] + public IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap) + { + var task = this.GetDalamudTextureWrapAsync(asset); + return task.IsCompletedSuccessfully ? task.Result : defaultWrap; + } + + /// + [Pure] + public Task GetDalamudTextureWrapAsync(DalamudAsset asset) + { + var purpose = asset.GetPurpose(); + if (purpose is not DalamudAssetPurpose.TextureFromPng and not DalamudAssetPurpose.TextureFromRaw) + throw new ArgumentOutOfRangeException(nameof(asset), asset, "The asset cannot be taken as a Texture2D."); + + Task task; + lock (this.syncRoot) + { + if (this.isDisposed) + throw new ObjectDisposedException(nameof(DalamudAssetManager)); + + task = this.textureWraps[asset] ??= CreateInnerAsync(); + } + + return task; + + async Task CreateInnerAsync() + { + var buf = Array.Empty(); + try + { + var im = (await Service.GetAsync()).Manager; + await using var stream = await this.CreateStreamAsync(asset); + var length = checked((int)stream.Length); + buf = ArrayPool.Shared.Rent(length); + stream.ReadExactly(buf, 0, length); + var image = purpose switch + { + DalamudAssetPurpose.TextureFromPng => im.LoadImage(buf), + DalamudAssetPurpose.TextureFromRaw => + asset.GetAttribute() is { } raw + ? im.LoadImageFromDxgiFormat(buf, raw.Pitch, raw.Width, raw.Height, raw.Format) + : throw new InvalidOperationException( + "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."), + _ => null, + }; + var disposeDeferred = + this.scopedFinalizer.Add(image) + ?? throw new InvalidOperationException("Something went wrong very badly"); + return new DisposeSuppressingDalamudTextureWrap(disposeDeferred); + } + catch (Exception e) + { + Log.Error(e, "[{name}] Failed to load {asset}.", nameof(DalamudAssetManager), asset); + throw; + } + finally + { + ArrayPool.Shared.Return(buf); + } + } + } + + private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult(); + + private Task TransformImmediate(Task task, Func transformer) + { + if (task.IsCompletedSuccessfully) + return Task.FromResult(transformer(task.Result)); + if (task.Exception is { } exc) + return Task.FromException(exc); + return task.ContinueWith(_ => this.TransformImmediate(task, transformer)).Unwrap(); + } + + private class DisposeSuppressingDalamudTextureWrap : IDalamudTextureWrap + { + private readonly IDalamudTextureWrap innerWrap; + + public DisposeSuppressingDalamudTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap; + + /// + public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle; + + /// + public int Width => this.innerWrap.Width; + + /// + public int Height => this.innerWrap.Height; + + /// + public void Dispose() + { + // suppressed + } + } +} diff --git a/Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs new file mode 100644 index 000000000..25ed995d7 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs @@ -0,0 +1,48 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Dalamud.Storage.Assets; + +/// +/// Marks that an asset can be download from online. +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] +internal class DalamudAssetOnlineSourceAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The URL. + public DalamudAssetOnlineSourceAttribute(string url) + { + this.Url = url; + } + + /// + /// Gets the source URL of the file. + /// + public string Url { get; } + + /// + /// Downloads to the given stream. + /// + /// The client. + /// The stream. + /// The cancellation token. + /// The task. + public async Task DownloadAsync(HttpClient client, Stream stream, CancellationToken cancellationToken) + { + using var resp = await client.GetAsync(this.Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + resp.EnsureSuccessStatusCode(); + if (resp.StatusCode != HttpStatusCode.OK) + throw new NotSupportedException($"Only 200 OK is supported; got {resp.StatusCode}"); + + await using var readStream = await resp.Content.ReadAsStreamAsync(cancellationToken); + await readStream.CopyToAsync(stream, cancellationToken); + if (resp.Content.Headers.ContentLength is { } length && stream.Length != length) + throw new IOException($"Expected {length} bytes; got {stream.Length} bytes."); + } +} diff --git a/Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs new file mode 100644 index 000000000..1df52aa39 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs @@ -0,0 +1,21 @@ +using System.IO; + +namespace Dalamud.Storage.Assets; + +/// +/// File names to look up in Dalamud assets. +/// +[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)] +internal class DalamudAssetPathAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The path components. + public DalamudAssetPathAttribute(params string[] pathComponents) => this.FileName = Path.Join(pathComponents); + + /// + /// Gets the file name. + /// + public string FileName { get; } +} diff --git a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs new file mode 100644 index 000000000..b059cb3d6 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs @@ -0,0 +1,27 @@ +namespace Dalamud.Storage.Assets; + +/// +/// Purposes of a Dalamud asset. +/// +public enum DalamudAssetPurpose +{ + /// + /// The asset has no purpose. + /// + Empty = 0, + + /// + /// The asset is a .png file, and can be purposed as a . + /// + TextureFromPng = 10, + + /// + /// The asset is a raw texture, and can be purposed as a . + /// + TextureFromRaw = 1001, + + /// + /// The asset is a font file. + /// + Font = 2000, +} diff --git a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs new file mode 100644 index 000000000..b79abb7d7 --- /dev/null +++ b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs @@ -0,0 +1,45 @@ +using SharpDX.DXGI; + +namespace Dalamud.Storage.Assets; + +/// +/// Provide raw texture data directly. +/// +[AttributeUsage(AttributeTargets.Field)] +internal class DalamudAssetRawTextureAttribute : Attribute +{ + /// + /// Initializes a new instance of the class. + /// + /// The width. + /// The pitch. + /// The height. + /// The format. + public DalamudAssetRawTextureAttribute(int width, int pitch, int height, Format format) + { + this.Width = width; + this.Pitch = pitch; + this.Height = height; + this.Format = format; + } + + /// + /// Gets the width. + /// + public int Width { get; } + + /// + /// Gets the pitch. + /// + public int Pitch { get; } + + /// + /// Gets the height. + /// + public int Height { get; } + + /// + /// Gets the format. + /// + public Format Format { get; } +} diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs new file mode 100644 index 000000000..4fb83df80 --- /dev/null +++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs @@ -0,0 +1,79 @@ +using System.Diagnostics.Contracts; +using System.IO; +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; + +namespace Dalamud.Storage.Assets; + +/// +/// Holds Dalamud Assets' handles hostage, so that they do not get closed while Dalamud is running.
+/// Also, attempts to load optional assets.
+///
+/// Note on
+/// It will help you get notified if you discard the result of functions, mostly likely because of a mistake. +/// Think of C++ [[nodiscard]]. Also, like the intended meaning of the attribute, such methods will not have +/// externally visible state changes. +///
+internal interface IDalamudAssetManager +{ + /// + /// Gets the shared texture wrap for . + /// + IDalamudTextureWrap Empty4X4 { get; } + + /// + /// Gets whether the stream for the asset is instantly available. + /// + /// The asset. + /// Whether the stream of an asset is immediately available. + [Pure] + bool IsStreamImmediatelyAvailable(DalamudAsset asset); + + /// + /// Creates a stream backed by the specified asset, waiting as necessary.
+ /// Call after use. + ///
+ /// The asset. + /// The stream. + [Pure] + Stream CreateStream(DalamudAsset asset); + + /// + /// Creates a stream backed by the specified asset.
+ /// Call after use. + ///
+ /// The asset. + /// The stream, wrapped inside a . + [Pure] + Task CreateStreamAsync(DalamudAsset asset); + + /// + /// Gets a shared instance of , after waiting as necessary.
+ /// Calls to is unnecessary; they will be ignored. + ///
+ /// The texture asset. + /// The texture wrap. + [Pure] + IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset); + + /// + /// Gets a shared instance of if it is available instantly; + /// if it is not ready, returns .
+ /// Calls to is unnecessary; they will be ignored. + ///
+ /// The texture asset. + /// The default return value, if the asset is not ready for whatever reason. + /// The texture wrap. + [Pure] + IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap); + + /// + /// Gets a shared instance of in a .
+ /// Calls to is unnecessary; they will be ignored. + ///
+ /// The texture asset. + /// The new texture wrap, wrapped inside a . + [Pure] + Task GetDalamudTextureWrapAsync(DalamudAsset asset); +} diff --git a/Dalamud/Utility/EnumExtensions.cs b/Dalamud/Utility/EnumExtensions.cs index 0bb60962e..493e6be1f 100644 --- a/Dalamud/Utility/EnumExtensions.cs +++ b/Dalamud/Utility/EnumExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System.Collections.Generic; using System.Linq; namespace Dalamud.Utility; @@ -8,6 +8,26 @@ namespace Dalamud.Utility; /// public static class EnumExtensions { + /// + /// Gets attributes on an enum. + /// + /// The type of attribute to get. + /// The enum value that has an attached attribute. + /// The enumerable of the attached attributes. + public static IEnumerable GetAttributes(this Enum value) + where TAttribute : Attribute + { + var type = value.GetType(); + var name = Enum.GetName(type, value); + if (name.IsNullOrEmpty()) + return Array.Empty(); + + return type.GetField(name)? + .GetCustomAttributes(false) + .OfType() + ?? Array.Empty(); + } + /// /// Gets an attribute on an enum. /// @@ -15,18 +35,8 @@ public static class EnumExtensions /// The enum value that has an attached attribute. /// The attached attribute, if any. public static TAttribute? GetAttribute(this Enum value) - where TAttribute : Attribute - { - var type = value.GetType(); - var name = Enum.GetName(type, value); - if (name.IsNullOrEmpty()) - return null; - - return type.GetField(name)? - .GetCustomAttributes(false) - .OfType() - .SingleOrDefault(); - } + where TAttribute : Attribute => + value.GetAttributes().SingleOrDefault(); /// /// Gets an indicator if enum has been flagged as obsolete (deprecated). From d1fad70e8ff92ff11696ca10389a8015dd5c76c2 Mon Sep 17 00:00:00 2001 From: Soreepeong Date: Wed, 29 Nov 2023 05:14:58 +0900 Subject: [PATCH 10/13] Disable game symbol download for the time being --- Dalamud/DalamudAsset.cs | 4 ++-- Dalamud/Storage/Assets/DalamudAssetManager.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs index 5b641c487..184193796 100644 --- a/Dalamud/DalamudAsset.cs +++ b/Dalamud/DalamudAsset.cs @@ -140,7 +140,7 @@ public enum DalamudAsset /// /// : Game symbol fonts being used as webfonts at Lodestone. /// - [DalamudAsset(DalamudAssetPurpose.Font, required: true)] - [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")] + [DalamudAsset(DalamudAssetPurpose.Font, required: false)] + // [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")] LodestoneGameSymbol = 2004, } diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index bbfd60636..30441f479 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -96,6 +96,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA return Task.WhenAll( Enum.GetValues() .Where(x => x is not DalamudAsset.Empty4X4) + .Where(x => x.GetAttribute()?.Required is true) .Select(this.CreateStreamAsync) .Select(x => x.ToContentDisposedTask())); } From a71cb813841909eba9a7e8f260e1bc57d91f312b Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 29 Nov 2023 06:43:41 +0900 Subject: [PATCH 11/13] Hold Shift to display Toggle Dev Menu TSM entry (#1538) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Interface/Internal/DalamudInterface.cs | 42 +++++++----- .../Internal/Windows/TitleScreenMenuWindow.cs | 29 +++++---- .../TitleScreenMenu/TitleScreenMenu.cs | 65 +++++++++++++++---- .../TitleScreenMenu/TitleScreenMenuEntry.cs | 28 +++++++- 4 files changed, 124 insertions(+), 40 deletions(-) diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 1a6e71194..1dcc5c0c7 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -9,6 +9,7 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; +using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Gui; using Dalamud.Game.Internal; using Dalamud.Interface.Animation.EasingFunctions; @@ -151,23 +152,32 @@ internal class DalamudInterface : IDisposable, IServiceType this.interfaceManager.Draw += this.OnDraw; - var tsm = Service.Get(); - tsm.AddEntryCore( - Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), - dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), - this.OpenPluginInstaller); - tsm.AddEntryCore( - Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), - dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), - this.OpenSettings); + Service.GetAsync().ContinueWith( + _ => + { + titleScreenMenu.AddEntryCore( + Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + this.OpenPluginInstaller); + titleScreenMenu.AddEntryCore( + Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + this.OpenSettings); - if (!configuration.DalamudBetaKind.IsNullOrEmpty()) - { - tsm.AddEntryCore( - Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), - dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), - () => this.isImGuiDrawDevMenu = true); - } + titleScreenMenu.AddEntryCore( + "Toggle Dev Menu", + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + () => Service.GetNullable()?.ToggleDevMenu(), + VirtualKey.SHIFT); + + if (!configuration.DalamudBetaKind.IsNullOrEmpty()) + { + titleScreenMenu.AddEntryCore( + Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), + dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall), + () => this.isImGuiDrawDevMenu = true); + } + }); this.creditsDarkeningAnimation.Point1 = Vector2.Zero; this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha); diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index f60ebe4ef..42bca89ff 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -31,7 +31,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable private readonly GameGui gameGui; private readonly TitleScreenMenu titleScreenMenu; - private readonly IDalamudTextureWrap shadeTexture; + private readonly Lazy shadeTexture; private readonly Dictionary shadeEasings = new(); private readonly Dictionary moveEasings = new(); @@ -77,7 +77,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; - this.shadeTexture = dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade); + this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade)); framework.Update += this.FrameworkOnUpdate; } @@ -122,15 +122,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable return; var scale = ImGui.GetIO().FontGlobalScale; - var entries = this.titleScreenMenu.Entries.OrderByDescending(x => x.IsInternal).ToList(); + var entries = this.titleScreenMenu.Entries; switch (this.state) { case State.Show: { - for (var i = 0; i < entries.Count; i++) + var i = 0; + foreach (var entry in entries) { - var entry = entries[i]; + if (!entry.IsShowConditionSatisfied()) + continue; if (!this.moveEasings.TryGetValue(entry.Id, out var moveEasing)) { @@ -150,7 +152,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable moveEasing.Update(); - var finalPos = (i + 1) * this.shadeTexture.Height * scale; + var finalPos = (i + 1) * this.shadeTexture.Value.Height * scale; var pos = moveEasing.Value * finalPos; // FIXME(goat): Sometimes, easings can overshoot and bring things out of alignment. @@ -164,6 +166,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable var cursor = ImGui.GetCursorPos(); cursor.Y = (float)pos; ImGui.SetCursorPos(cursor); + i++; } if (!ImGui.IsWindowHovered(ImGuiHoveredFlags.RootAndChildWindows | @@ -196,17 +199,20 @@ internal class TitleScreenMenuWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)this.fadeOutEasing.Value)) { - for (var i = 0; i < entries.Count; i++) + var i = 0; + foreach (var entry in entries) { - var entry = entries[i]; + if (!entry.IsShowConditionSatisfied()) + continue; - var finalPos = (i + 1) * this.shadeTexture.Height * scale; + var finalPos = (i + 1) * this.shadeTexture.Value.Height * scale; this.DrawEntry(entry, i != 0, true, i == 0, false, false); var cursor = ImGui.GetCursorPos(); cursor.Y = finalPos; ImGui.SetCursorPos(cursor); + i++; } } @@ -280,7 +286,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, (float)shadeEasing.Value)) { - ImGui.Image(this.shadeTexture.ImGuiHandle, new Vector2(this.shadeTexture.Width * scale, this.shadeTexture.Height * scale)); + var texture = this.shadeTexture.Value; + ImGui.Image(texture.ImGuiHandle, new Vector2(texture.Width, texture.Height) * scale); } var isHover = ImGui.IsItemHovered(); @@ -358,7 +365,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable // Drop shadow using (ImRaii.PushColor(ImGuiCol.Text, 0xFF000000)) { - for (int i = 0, i_ = (int)Math.Ceiling(1 * scale); i < i_; i++) + for (int i = 0, to = (int)Math.Ceiling(1 * scale); i < to; i++) { ImGui.SetCursorPos(new Vector2(cursor.X, cursor.Y + i)); ImGui.Text(entry.Name); diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs index 6665bbafb..1f9a5bc76 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs @@ -2,11 +2,12 @@ using System.Linq; using System.Reflection; +using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Plugin.Services; -using ImGuiScene; +using Dalamud.Utility; namespace Dalamud.Interface; @@ -23,14 +24,32 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu internal const uint TextureSize = 64; private readonly List entries = new(); + private TitleScreenMenuEntry[]? entriesView; [ServiceManager.ServiceConstructor] private TitleScreenMenu() { } + /// + /// Event to be called when the entry list has been changed. + /// + internal event Action? EntryListChange; + /// - public IReadOnlyList Entries => this.entries; + public IReadOnlyList Entries + { + get + { + lock (this.entries) + { + if (!this.entries.Any()) + return Array.Empty(); + + return this.entriesView ??= this.entries.OrderByDescending(x => x.IsInternal).ToArray(); + } + } + } /// public TitleScreenMenuEntry AddEntry(string text, IDalamudTextureWrap texture, Action onTriggered) @@ -40,19 +59,23 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu throw new ArgumentException("Texture must be 64x64"); } + TitleScreenMenuEntry entry; lock (this.entries) { var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == Assembly.GetCallingAssembly()).ToList(); var priority = entriesOfAssembly.Any() ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) : 0; - var entry = new TitleScreenMenuEntry(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); + entry = new(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); var i = this.entries.BinarySearch(entry); if (i < 0) i = ~i; this.entries.Insert(i, entry); - return entry; + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); + return entry; } /// @@ -63,15 +86,19 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu throw new ArgumentException("Texture must be 64x64"); } + TitleScreenMenuEntry entry; lock (this.entries) { - var entry = new TitleScreenMenuEntry(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); + entry = new(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); var i = this.entries.BinarySearch(entry); if (i < 0) i = ~i; this.entries.Insert(i, entry); - return entry; + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); + return entry; } /// @@ -80,7 +107,10 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu lock (this.entries) { this.entries.Remove(entry); + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); } /// @@ -99,15 +129,19 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu throw new ArgumentException("Texture must be 64x64"); } + TitleScreenMenuEntry entry; lock (this.entries) { - var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered) + entry = new(null, priority, text, texture, onTriggered) { IsInternal = true, }; this.entries.Add(entry); - return entry; + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); + return entry; } /// @@ -116,28 +150,37 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu /// The text to show. /// The texture to show. /// The action to execute when the option is selected. + /// The keys that have to be held to display the menu. /// A object that can be used to manage the entry. /// Thrown when the texture provided does not match the required resolution(64x64). - internal TitleScreenMenuEntry AddEntryCore(string text, IDalamudTextureWrap texture, Action onTriggered) + internal TitleScreenMenuEntry AddEntryCore( + string text, + IDalamudTextureWrap texture, + Action onTriggered, + params VirtualKey[] showConditionKeys) { if (texture.Height != TextureSize || texture.Width != TextureSize) { throw new ArgumentException("Texture must be 64x64"); } + TitleScreenMenuEntry entry; lock (this.entries) { var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == null).ToList(); var priority = entriesOfAssembly.Any() ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) : 0; - var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered) + entry = new(null, priority, text, texture, onTriggered, showConditionKeys) { IsInternal = true, }; this.entries.Add(entry); - return entry; + this.entriesView = null; } + + this.EntryListChange?.InvokeSafely(); + return entry; } } diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs index 76382ace2..8a400db7c 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenuEntry.cs @@ -1,5 +1,9 @@ -using System.Reflection; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Reflection; +using Dalamud.Game.ClientState.Keys; using Dalamud.Interface.Internal; namespace Dalamud.Interface; @@ -19,13 +23,21 @@ public class TitleScreenMenuEntry : IComparable /// The text to show. /// The texture to show. /// The action to execute when the option is selected. - internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered) + /// The keys that have to be held to display the menu. + internal TitleScreenMenuEntry( + Assembly? callingAssembly, + ulong priority, + string text, + IDalamudTextureWrap texture, + Action onTriggered, + IEnumerable? showConditionKeys = null) { this.CallingAssembly = callingAssembly; this.Priority = priority; this.Name = text; this.Texture = texture; this.onTriggered = onTriggered; + this.ShowConditionKeys = (showConditionKeys ?? Array.Empty()).ToImmutableSortedSet(); } /// @@ -58,6 +70,11 @@ public class TitleScreenMenuEntry : IComparable /// internal Guid Id { get; init; } = Guid.NewGuid(); + /// + /// Gets the keys that have to be pressed to show the menu. + /// + internal IReadOnlySet ShowConditionKeys { get; init; } + /// public int CompareTo(TitleScreenMenuEntry? other) { @@ -84,6 +101,13 @@ public class TitleScreenMenuEntry : IComparable return 0; } + /// + /// Determines the displaying condition of this menu entry is met. + /// + /// True if met. + internal bool IsShowConditionSatisfied() => + this.ShowConditionKeys.All(x => Service.GetNullable()?[x] is true); + /// /// Trigger the action associated with this entry. /// From 13346b04db6d657216a6653b82d5f85cf2fc3295 Mon Sep 17 00:00:00 2001 From: KazWolfe Date: Tue, 28 Nov 2023 13:52:38 -0800 Subject: [PATCH 12/13] Remove second HttpClient from PluginRepository (#1551) Co-authored-by: goat <16760685+goaaats@users.noreply.github.com> --- .../Plugin/Internal/Types/PluginRepository.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index 18c528910..8de25aa08 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -6,12 +6,14 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using Dalamud.Logging.Internal; using Dalamud.Networking.Http; using Dalamud.Plugin.Internal.Types.Manifest; using Dalamud.Utility; + using Newtonsoft.Json; namespace Dalamud.Plugin.Internal.Types; @@ -26,8 +28,9 @@ internal class PluginRepository /// public const string MainRepoUrl = "https://kamori.goats.dev/Plugin/PluginMaster"; - private static readonly ModuleLog Log = new("PLUGINR"); + private const int HttpRequestTimeoutSeconds = 20; + private static readonly ModuleLog Log = new("PLUGINR"); private readonly HttpClient httpClient; /// @@ -112,7 +115,8 @@ internal class PluginRepository { Log.Information($"Fetching repo: {this.PluginMasterUrl}"); - using var response = await this.httpClient.GetAsync(this.PluginMasterUrl); + using var response = await this.GetPluginMaster(this.PluginMasterUrl); + response.EnsureSuccessStatusCode(); var data = await response.Content.ReadAsStringAsync(); @@ -204,4 +208,17 @@ internal class PluginRepository return true; } + + private async Task GetPluginMaster(string url, int timeout = HttpRequestTimeoutSeconds) + { + var httpClient = Service.Get().SharedHttpClient; + + var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.CacheControl = new CacheControlHeaderValue { NoCache = true }; + + using var requestCts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)); + + return await httpClient.SendAsync(request, requestCts.Token); + } } From 334a3076ac79fb9869b838c1d223e44e56e125b0 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 28 Nov 2023 22:55:38 +0100 Subject: [PATCH 13/13] [master] Update ClientStructs (#1528) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 090e0c244..cc6687524 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 090e0c244df668454616026188c1363e5d25a1bc +Subproject commit cc668752416a8459a3c23345c51277e359803de8