Merge remote-tracking branch 'origin/master' into net8-rollup

This commit is contained in:
github-actions[bot] 2023-11-28 21:55:56 +00:00
commit de584c8fa0
62 changed files with 3085 additions and 774 deletions

View file

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

View file

@ -50,4 +50,10 @@
<Private>false</Private>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Update="Dalamud.CorePlugin.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View file

@ -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": []
}

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
@ -20,7 +21,7 @@ namespace Dalamud.Configuration.Internal;
/// Class containing Dalamud settings.
/// </summary>
[Serializable]
[ServiceManager.Service]
[ServiceManager.ProvidedService]
#pragma warning disable SA1015
[InherentDependency<ReliableFileStorage>] // We must still have this when unloading
#pragma warning restore SA1015
@ -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
/// <summary>
/// Event that occurs when dalamud configuration is saved.
/// </summary>
public event DalamudConfigurationSavedDelegate DalamudConfigurationSaved;
public event DalamudConfigurationSavedDelegate? DalamudConfigurationSaved;
/// <summary>
/// Gets or sets a list of muted works.
/// </summary>
public List<string> BadWords { get; set; }
public List<string>? BadWords { get; set; }
/// <summary>
/// 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
/// <summary>
/// Gets or sets the language code to load Dalamud localization with.
/// </summary>
public string LanguageOverride { get; set; } = null;
public string? LanguageOverride { get; set; } = null;
/// <summary>
/// Gets or sets the last loaded Dalamud version.
/// </summary>
public string LastVersion { get; set; } = null;
public string? LastVersion { get; set; } = null;
/// <summary>
/// Gets or sets a value indicating the last seen FTUE version.
@ -84,7 +85,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
/// <summary>
/// Gets or sets the last loaded Dalamud version.
/// </summary>
public string LastChangelogMajorMinor { get; set; } = null;
public string? LastChangelogMajorMinor { get; set; } = null;
/// <summary>
/// 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.
/// </summary>
[SuppressMessage("ReSharper", "InconsistentNaming", Justification = "ABI")]
public bool EnablePluginUISoundEffects { get; set; }
/// <summary>
@ -266,7 +268,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable
/// <summary>
/// Gets or sets the kind of beta to download when <see cref="DalamudBetaKey"/> matches the server value.
/// </summary>
public string DalamudBetaKind { get; set; }
public string? DalamudBetaKind { get; set; }
/// <summary>
/// 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<ReliableFileStorage>.Get().WriteAllText(
this.configPath, JsonConvert.SerializeObject(this, SerializerSettings));

View file

@ -30,7 +30,7 @@ namespace Dalamud;
/// <summary>
/// The main Dalamud class containing all subsystems.
/// </summary>
[ServiceManager.Service]
[ServiceManager.ProvidedService]
internal sealed class Dalamud : IServiceType
{
#region Internals

146
Dalamud/DalamudAsset.cs Normal file
View file

@ -0,0 +1,146 @@
using Dalamud.Storage.Assets;
namespace Dalamud;
/// <summary>
/// Specifies an asset that has been shipped as Dalamud Asset.<br />
/// <strong>Any asset can cease to exist at any point, even if the enum value exists.</strong><br />
/// Either ship your own assets, or be prepared for errors.
/// </summary>
public enum DalamudAsset
{
/// <summary>
/// Nothing.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Empty, data: new byte[0])]
Unspecified = 0,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromRaw"/>: The fallback empty texture.
/// </summary>
[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,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The Dalamud logo.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "logo.png")]
Logo = 1001,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The Dalamud logo, but smaller.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "tsmLogo.png")]
LogoSmall = 1002,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The default plugin icon.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "defaultIcon.png")]
DefaultIcon = 1003,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The disabled plugin icon.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "disabledIcon.png")]
DisabledIcon = 1004,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The outdated installable plugin icon.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "outdatedInstallableIcon.png")]
OutdatedInstallableIcon = 1005,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin trouble icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "troubleIcon.png")]
TroubleIcon = 1006,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin update icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "updateIcon.png")]
UpdateIcon = 1007,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The plugin installed icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "installedIcon.png")]
InstalledIcon = 1008,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The third party plugin icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "thirdIcon.png")]
ThirdIcon = 1009,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The installed third party plugin icon overlay.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "thirdInstalledIcon.png")]
ThirdInstalledIcon = 1010,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The API bump explainer icon.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "changelogApiBump.png")]
ChangelogApiBumpIcon = 1011,
/// <summary>
/// <see cref="DalamudAssetPurpose.TextureFromPng"/>: The background shade for
/// <see cref="Interface.Internal.Windows.TitleScreenMenuWindow"/>.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
[DalamudAssetPath("UIRes", "tsmShade.png")]
TitleScreenMenuShade = 1012,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK JP Medium.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "NotoSansCJKjp-Regular.otf")]
[DalamudAssetPath("UIRes", "NotoSansCJKjp-Medium.otf")]
NotoSansJpMedium = 2000,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Noto Sans CJK KR Regular.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "NotoSansCJKkr-Regular.otf")]
[DalamudAssetPath("UIRes", "NotoSansKR-Regular.otf")]
NotoSansKrRegular = 2001,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Inconsolata Regular.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "Inconsolata-Regular.ttf")]
InconsolataRegular = 2002,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: FontAwesome Free Solid.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font)]
[DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")]
FontAwesomeFreeSolid = 2003,
/// <summary>
/// <see cref="DalamudAssetPurpose.Font"/>: Game symbol fonts being used as webfonts at Lodestone.
/// </summary>
[DalamudAsset(DalamudAssetPurpose.Font, required: false)]
// [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")]
LodestoneGameSymbol = 2004,
}

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Events;
/// Service provider for addon event management.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal unsafe class AddonEventManager : IDisposable, IServiceType
{
/// <summary>

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
/// This class provides events for in-game addon lifecycles.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal unsafe class AddonLifecycle : IDisposable, IServiceType
{
private static readonly ModuleLog Log = new("AddonLifecycle");

View file

@ -12,7 +12,7 @@ namespace Dalamud.Game.Config;
/// This class represents the game's configuration.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
{
private readonly GameConfigAddressResolver address = new();

View file

@ -12,7 +12,7 @@ namespace Dalamud.Game.DutyState;
/// This class represents the state of the currently occupied duty.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
{
private readonly DutyStateAddressResolver address;

View file

@ -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<HappyHttpClient>.Get().SharedHttpClient;
private readonly HttpClient httpClient;
/// <summary>
/// Initializes a new instance of the <see cref="UniversalisMarketBoardUploader"/> class.
/// </summary>
public UniversalisMarketBoardUploader()
{
}
/// <param name="happyHttpClient">An instance of <see cref="HappyHttpClient"/>.</param>
public UniversalisMarketBoardUploader(HappyHttpClient happyHttpClient) =>
this.httpClient = happyHttpClient.SharedHttpClient;
/// <inheritdoc/>
public async Task Upload(MarketBoardItemRequest request)

View file

@ -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;
/// <summary>
/// This class handles network notifications and uploading market board data.
/// </summary>
[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);

View file

@ -11,7 +11,7 @@ namespace Dalamud.Game;
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.Service]
[ServiceManager.ProvidedService]
#pragma warning disable SA1015
[ResolveVia<ISigScanner>]
#pragma warning restore SA1015

View file

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

View file

@ -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.
/// </summary>
[PluginInterface]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
#pragma warning disable SA1015
[ResolveVia<IDragDropManager>]
#pragma warning restore SA1015

View file

@ -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<FontTableEntry>(data, this.FileHeader.FontTableHeaderOffset + Marshal.SizeOf<FontTableHeader>() + (Marshal.SizeOf<FontTableEntry>() * 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<KerningTableEntry>(data, this.FileHeader.KerningTableHeaderOffset + Marshal.SizeOf<KerningTableHeader>() + (Marshal.SizeOf<KerningTableEntry>() * i)));
}
@ -51,6 +50,14 @@ public class FdtReader
/// </summary>
public List<KerningTableEntry> Distances { get; init; } = new();
/// <summary>
/// Finds the glyph index for the corresponding codepoint.
/// </summary>
/// <param name="codepoint">Unicode codepoint (UTF-32 value).</param>
/// <returns>Corresponding index, or a negative number according to <see cref="List{T}.BinarySearch(int,int,T,System.Collections.Generic.IComparer{T}?)"/>.</returns>
public int FindGlyphIndex(int codepoint) =>
this.Glyphs.BinarySearch(new FontTableEntry { CharUtf8 = CodePointToUtf8Int32(codepoint) });
/// <summary>
/// Finds glyph definition for corresponding codepoint.
/// </summary>
@ -58,7 +65,7 @@ public class FdtReader
/// <returns>Corresponding FontTableEntry, or null if not found.</returns>
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<T>(byte[] data, int offset)
{
var len = Marshal.SizeOf<T>();
if (offset + len > data.Length)
throw new Exception("Data too short");
fixed (byte* ptr = data)
return Marshal.PtrToStructure<T>(new(ptr + offset));
}
private static int CodePointToUtf8Int32(int codepoint)
/// <summary>
/// Translates a UTF-32 codepoint to a <see cref="uint"/> containing a UTF-8 character.
/// </summary>
/// <param name="codepoint">The codepoint.</param>
/// <returns>The uint.</returns>
internal static int CodePointToUtf8Int32(int codepoint)
{
if (codepoint <= 0x7F)
{
@ -131,6 +133,16 @@ public class FdtReader
}
}
private static unsafe T StructureFromByteArray<T>(byte[] data, int offset)
{
var len = Marshal.SizeOf<T>();
if (offset + len > data.Length)
throw new Exception("Data too short");
fixed (byte* ptr = data)
return Marshal.PtrToStructure<T>(new(ptr + offset));
}
private static int Utf8Uint32ToCodePoint(int n)
{
if ((n & 0xFFFFFF80) == 0)
@ -252,7 +264,7 @@ public class FdtReader
/// Glyph table entry.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct FontTableEntry : IComparable<FontTableEntry>
public struct FontTableEntry : IComparable<FontTableEntry>
{
/// <summary>
/// Mapping of texture channel index to byte index.
@ -367,7 +379,7 @@ public class FdtReader
/// Kerning table entry.
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public unsafe struct KerningTableEntry : IComparable<KerningTableEntry>
public struct KerningTableEntry : IComparable<KerningTableEntry>
{
/// <summary>
/// Integer representation of a Unicode character in UTF-8 in reverse order, read in little endian, for the left character.

View file

@ -22,7 +22,7 @@ namespace Dalamud.Interface.GameFonts;
/// <summary>
/// Loads game font for use in ImGui.
/// </summary>
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal class GameFontManager : IServiceType
{
private static readonly string?[] FontNames =
@ -257,7 +257,7 @@ internal class GameFontManager : IServiceType
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
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);
}
/// <summary>
@ -269,7 +269,7 @@ internal class GameFontManager : IServiceType
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
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);
}
/// <summary>

View file

@ -1,5 +1,3 @@
using System;
namespace Dalamud.Interface.GameFonts;
/// <summary>
@ -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
/// <param name="family">Font family.</param>
/// <param name="size">Font size in points.</param>
/// <returns>Recommended GameFontFamilyAndSize.</returns>
public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size)
public static GameFontFamilyAndSize GetRecommendedFamilyAndSize(GameFontFamily family, float size) =>
family switch
{
if (size <= 0)
return GameFontFamilyAndSize.Undefined;
switch (family)
_ when size <= 0 => GameFontFamilyAndSize.Undefined,
GameFontFamily.Undefined => GameFontFamilyAndSize.Undefined,
GameFontFamily.Axis => size switch
{
case GameFontFamily.Undefined:
return GameFontFamilyAndSize.Undefined;
<= ((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;
/// <summary>
/// Calculates the adjustment to width resulting fron Weight and SkewStrength.
/// </summary>
/// <param name="header">Font header.</param>
/// <param name="glyph">Glyph.</param>
/// <returns>Width adjustment in pixel unit.</returns>
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);
}
/// <summary>
@ -265,16 +263,8 @@ public struct GameFontStyle
/// <param name="reader">Font information.</param>
/// <param name="glyph">Glyph.</param>
/// <returns>Width adjustment in pixel unit.</returns>
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);
/// <inheritdoc/>
public override string ToString()

View file

@ -1,58 +0,0 @@
using System.IO;
using Dalamud.IoC.Internal;
namespace Dalamud.Interface.Internal;
/// <summary>
/// Class containing various textures used by Dalamud windows for branding purposes.
/// </summary>
[ServiceManager.EarlyLoadedService]
#pragma warning disable SA1015
[InherentDependency<InterfaceManager.InterfaceManagerWithScene>] // Can't load textures before this
#pragma warning restore SA1015
internal class Branding : IServiceType, IDisposable
{
private readonly Dalamud dalamud;
private readonly TextureManager tm;
/// <summary>
/// Initializes a new instance of the <see cref="Branding"/> class.
/// </summary>
/// <param name="dalamud">Dalamud instance.</param>
/// <param name="tm">TextureManager instance.</param>
[ServiceManager.ServiceConstructor]
public Branding(Dalamud dalamud, TextureManager tm)
{
this.dalamud = dalamud;
this.tm = tm;
this.LoadTextures();
}
/// <summary>
/// Gets a full-size Dalamud logo texture.
/// </summary>
public IDalamudTextureWrap Logo { get; private set; } = null!;
/// <summary>
/// Gets a small Dalamud logo texture.
/// </summary>
public IDalamudTextureWrap LogoSmall { get; private set; } = null!;
/// <inheritdoc/>
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.");
}
}

View file

@ -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,7 +7,9 @@ using System.Runtime.InteropServices;
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;
@ -25,14 +25,15 @@ 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.Storage.Assets;
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;
@ -49,7 +50,9 @@ internal class DalamudInterface : IDisposable, IServiceType
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;
@ -92,11 +95,16 @@ internal class DalamudInterface : IDisposable, IServiceType
DalamudConfiguration configuration,
InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene,
PluginImageCache pluginImageCache,
Branding branding)
DalamudAssetManager dalamudAssetManager,
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 +112,19 @@ 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,
configuration,
dalamudAssetManager,
framework,
gameGui,
titleScreenMenu) { IsOpen = false };
this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false };
this.profilerWindow = new ProfilerWindow() { IsOpen = false };
this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false };
@ -136,16 +150,34 @@ 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<TitleScreenMenu>.Get();
tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, () => this.OpenPluginInstaller());
tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), branding.LogoSmall, this.OpenSettings);
Service<InterfaceManager.InterfaceManagerWithScene>.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);
titleScreenMenu.AddEntryCore(
"Toggle Dev Menu",
dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall),
() => Service<DalamudInterface>.GetNullable()?.ToggleDevMenu(),
VirtualKey.SHIFT);
if (!configuration.DalamudBetaKind.IsNullOrEmpty())
{
tsm.AddEntryCore(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), branding.LogoSmall, () => this.isImGuiDrawDevMenu = true);
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);
@ -173,7 +205,7 @@ internal class DalamudInterface : IDisposable, IServiceType
/// <inheritdoc/>
public void Dispose()
{
Service<InterfaceManager>.Get().Draw -= this.OnDraw;
this.interfaceManager.Draw -= this.OnDraw;
this.WindowSystem.RemoveAllWindows();
@ -356,7 +388,7 @@ internal class DalamudInterface : IDisposable, IServiceType
/// Toggles the <see cref="DataWindow"/>.
/// </summary>
/// <param name="dataKind">The data kind to switch to after opening.</param>
public void ToggleDataWindow(string dataKind = null)
public void ToggleDataWindow(string? dataKind = null)
{
this.dataWindow.Toggle();
if (dataKind != null && this.dataWindow.IsOpen)
@ -378,7 +410,7 @@ internal class DalamudInterface : IDisposable, IServiceType
/// <summary>
/// Toggles the <see cref="ImeWindow"/>.
/// </summary>
public void ToggleIMEWindow() => this.imeWindow.Toggle();
public void ToggleImeWindow() => this.imeWindow.Toggle();
/// <summary>
/// Toggles the <see cref="ConsoleWindow"/>.
@ -504,7 +536,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 +612,16 @@ internal class DalamudInterface : IDisposable, IServiceType
{
if (ImGui.BeginMainMenuBar())
{
var dalamud = Service<Dalamud>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.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 +638,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<AntiDebug>.Get();
@ -637,8 +668,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 +761,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 +775,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 +797,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 +810,7 @@ internal class DalamudInterface : IDisposable, IServiceType
if (ImGui.MenuItem("Clear stacks"))
{
Service<InterfaceManager>.Get().ClearStacks();
this.interfaceManager.ClearStacks();
}
if (ImGui.MenuItem("Dump style"))
@ -792,7 +823,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 +846,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 +858,7 @@ internal class DalamudInterface : IDisposable, IServiceType
{
if (ImGui.MenuItem("Replace ExceptionHandler"))
{
dalamud.ReplaceExceptionHandler();
this.dalamud.ReplaceExceptionHandler();
}
ImGui.EndMenu();
@ -922,7 +953,7 @@ internal class DalamudInterface : IDisposable, IServiceType
if (Service<GameGui>.Get().GameUiHidden)
ImGui.BeginMenu("UI is hidden...", false);
if (configuration.ShowDevBarInfo)
if (this.configuration.ShowDevBarInfo)
{
ImGui.PushFont(InterfaceManager.MonoFont);
@ -931,9 +962,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<InterfaceManager>.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();

View file

@ -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;
@ -52,8 +53,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;
/// <summary>
/// The default font size, in points.
/// </summary>
public const float DefaultFontSizePt = 12.0f;
/// <summary>
/// The default font size, in pixels.
/// </summary>
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.
@ -1055,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)
{
@ -1070,7 +1084,7 @@ internal class InterfaceManager : IDisposable, IServiceType
try
{
if (Service<DalamudConfiguration>.Get().WindowIsImmersive)
if (configuration.WindowIsImmersive)
this.SetImmersiveMode(true);
}
catch (Exception ex)
@ -1277,7 +1291,7 @@ internal class InterfaceManager : IDisposable, IServiceType
/// <summary>
/// Represents an instance of InstanceManager with scene ready for use.
/// </summary>
[ServiceManager.Service]
[ServiceManager.ProvidedService]
public class InterfaceManagerWithScene : IServiceType
{
/// <summary>

View file

@ -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,11 +64,9 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.tsmWindow = tsmWindow;
this.Namespace = "DalamudChangelogWindow";
this.logoTexture = Service<Branding>.Get().Logo;
// If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch
if (WarrantsChangelog())
this.MakeFont();
Service<GameFontManager>.GetAsync().ContinueWith(t => this.MakeFont(t.Result));
}
private enum State
@ -98,7 +97,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
Service<DalamudInterface>.Get().SetCreditsDarkeningAnimation(true);
this.tsmWindow.AllowDrawing = false;
this.MakeFont();
this.MakeFont(Service<GameFontManager>.Get());
this.state = State.WindowFadeIn;
this.windowFade.Reset();
@ -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<DalamudAssetManager>.Get().GetDalamudTextureWrap(DalamudAsset.Logo);
ImGui.Image(this.logoTexture.ImGuiHandle, logoSize);
}
}
@ -376,15 +376,8 @@ internal sealed class ChangelogWindow : Window, IDisposable
/// </summary>
public void Dispose()
{
this.logoTexture.Dispose();
}
private void MakeFont()
{
if (this.bannerFont == null)
{
var gfm = Service<GameFontManager>.Get();
this.bannerFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18));
}
}
private void MakeFont(GameFontManager gfm) =>
this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18));
}

View file

@ -56,11 +56,10 @@ internal class ConsoleWindow : Window, IDisposable
/// <summary>
/// Initializes a new instance of the <see cref="ConsoleWindow"/> class.
/// </summary>
public ConsoleWindow()
/// <param name="configuration">An instance of <see cref="DalamudConfiguration"/>.</param>
public ConsoleWindow(DalamudConfiguration configuration)
: base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
{
var configuration = Service<DalamudConfiguration>.Get();
this.autoScroll = configuration.LogAutoScroll;
this.autoOpen = configuration.LogOpenAtStartup;
SerilogEventSink.Instance.LogLine += this.OnLogLine;

View file

@ -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<IDataWindowWidget> orderedModules;

View file

@ -1,7 +1,6 @@
using System;
using System.Linq;
using System.Linq;
namespace Dalamud.Interface.Internal.Windows;
namespace Dalamud.Interface.Internal.Windows.Data;
/// <summary>
/// Class representing a date window entry.

View file

@ -28,8 +28,14 @@ public class AddonLifecycleWidget : IDataWindowWidget
/// <inheritdoc/>
public void Load()
{
this.AddonLifecycle = Service<AddonLifecycle>.GetNullable();
if (this.AddonLifecycle is not null) this.Ready = true;
Service<AddonLifecycle>
.GetAsync()
.ContinueWith(
r =>
{
this.AddonLifecycle = r.Result;
this.Ready = true;
});
}
/// <inheritdoc/>

View file

@ -39,11 +39,29 @@ internal class FontAwesomeTestWidget : IDataWindowWidget
{
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;
}

View file

@ -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<InterfaceManager.InterfaceManagerWithScene>.Get();
[ServiceManager.ServiceDependency]
private readonly Branding branding = Service<Branding>.Get();
[ServiceManager.ServiceDependency]
private readonly HappyHttpClient happyHttpClient = Service<HappyHttpClient>.Get();
@ -64,35 +57,12 @@ internal class PluginImageCache : IDisposable, IServiceType
private readonly ConcurrentDictionary<string, IDalamudTextureWrap?> pluginIconMap = new();
private readonly ConcurrentDictionary<string, IDalamudTextureWrap?[]?> pluginImagesMap = new();
private readonly Task<IDalamudTextureWrap> emptyTextureTask;
private readonly Task<IDalamudTextureWrap> disabledIconTask;
private readonly Task<IDalamudTextureWrap> outdatedInstallableIconTask;
private readonly Task<IDalamudTextureWrap> defaultIconTask;
private readonly Task<IDalamudTextureWrap> troubleIconTask;
private readonly Task<IDalamudTextureWrap> updateIconTask;
private readonly Task<IDalamudTextureWrap> installedIconTask;
private readonly Task<IDalamudTextureWrap> thirdIconTask;
private readonly Task<IDalamudTextureWrap> thirdInstalledIconTask;
private readonly Task<IDalamudTextureWrap> corePluginIconTask;
private readonly DalamudAssetManager dalamudAssetManager;
[ServiceManager.ServiceConstructor]
private PluginImageCache(Dalamud dalamud)
private PluginImageCache(Dalamud dalamud, DalamudAssetManager dalamudAssetManager)
{
Task<IDalamudTextureWrap>? 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
/// <summary>
/// Gets the fallback empty texture.
/// </summary>
public IDalamudTextureWrap EmptyTexture => this.emptyTextureTask.IsCompleted
? this.emptyTextureTask.Result
: this.emptyTextureTask.GetAwaiter().GetResult();
public IDalamudTextureWrap EmptyTexture =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.Empty4X4);
/// <summary>
/// Gets the disabled plugin icon.
/// </summary>
public IDalamudTextureWrap DisabledIcon => this.disabledIconTask.IsCompleted
? this.disabledIconTask.Result
: this.disabledIconTask.GetAwaiter().GetResult();
public IDalamudTextureWrap DisabledIcon =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DisabledIcon, this.EmptyTexture);
/// <summary>
/// Gets the outdated installable plugin icon.
/// </summary>
public IDalamudTextureWrap OutdatedInstallableIcon => this.outdatedInstallableIconTask.IsCompleted
? this.outdatedInstallableIconTask.Result
: this.outdatedInstallableIconTask.GetAwaiter().GetResult();
public IDalamudTextureWrap OutdatedInstallableIcon =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.OutdatedInstallableIcon, this.EmptyTexture);
/// <summary>
/// Gets the default plugin icon.
/// </summary>
public IDalamudTextureWrap DefaultIcon => this.defaultIconTask.IsCompleted
? this.defaultIconTask.Result
: this.defaultIconTask.GetAwaiter().GetResult();
public IDalamudTextureWrap DefaultIcon =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DefaultIcon, this.EmptyTexture);
/// <summary>
/// Gets the plugin trouble icon overlay.
/// </summary>
public IDalamudTextureWrap TroubleIcon => this.troubleIconTask.IsCompleted
? this.troubleIconTask.Result
: this.troubleIconTask.GetAwaiter().GetResult();
public IDalamudTextureWrap TroubleIcon =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TroubleIcon, this.EmptyTexture);
/// <summary>
/// Gets the plugin update icon overlay.
/// </summary>
public IDalamudTextureWrap UpdateIcon => this.updateIconTask.IsCompleted
? this.updateIconTask.Result
: this.updateIconTask.GetAwaiter().GetResult();
public IDalamudTextureWrap UpdateIcon =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.UpdateIcon, this.EmptyTexture);
/// <summary>
/// Gets the plugin installed icon overlay.
/// </summary>
public IDalamudTextureWrap InstalledIcon => this.installedIconTask.IsCompleted
? this.installedIconTask.Result
: this.installedIconTask.GetAwaiter().GetResult();
public IDalamudTextureWrap InstalledIcon =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.InstalledIcon, this.EmptyTexture);
/// <summary>
/// Gets the third party plugin icon overlay.
/// </summary>
public IDalamudTextureWrap ThirdIcon => this.thirdIconTask.IsCompleted
? this.thirdIconTask.Result
: this.thirdIconTask.GetAwaiter().GetResult();
public IDalamudTextureWrap ThirdIcon =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.ThirdIcon, this.EmptyTexture);
/// <summary>
/// Gets the installed third party plugin icon overlay.
/// </summary>
public IDalamudTextureWrap ThirdInstalledIcon => this.thirdInstalledIconTask.IsCompleted
? this.thirdInstalledIconTask.Result
: this.thirdInstalledIconTask.GetAwaiter().GetResult();
public IDalamudTextureWrap ThirdInstalledIcon =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon, this.EmptyTexture);
/// <summary>
/// Gets the core plugin icon.
/// </summary>
public IDalamudTextureWrap CorePluginIcon => this.corePluginIconTask.IsCompleted
? this.corePluginIconTask.Result
: this.corePluginIconTask.GetAwaiter().GetResult();
public IDalamudTextureWrap CorePluginIcon =>
this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall, this.EmptyTexture);
/// <inheritdoc/>
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<InterfaceManager.InterfaceManagerWithScene>.GetAsync()).Manager;
var framework = await Service<Framework>.GetAsync();
IDalamudTextureWrap? image;

View file

@ -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 <see cref="PluginInstallerWindow"/> class.
/// </summary>
/// <param name="imageCache">An instance of <see cref="PluginImageCache"/> class.</param>
public PluginInstallerWindow(PluginImageCache imageCache)
/// <param name="configuration">An instance of <see cref="DalamudConfiguration"/>.</param>
public PluginInstallerWindow(PluginImageCache imageCache, DalamudConfiguration configuration)
: base(
Locs.WindowTitle + (Service<DalamudConfiguration>.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;

View file

@ -44,7 +44,8 @@ internal class PluginStatWindow : Window
{
var pluginManager = Service<PluginManager>.Get();
ImGui.BeginTabBar("Stat Tabs");
if (!ImGui.BeginTabBar("Stat Tabs"))
return;
if (ImGui.BeginTabItem("Draw times"))
{

View file

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

View file

@ -155,6 +155,8 @@ internal class SettingsWindow : Window
ImGui.EndTabItem();
}
}
ImGui.EndTabBar();
}
ImGui.SetCursorPos(windowSize - ImGuiHelpers.ScaledVector2(70));

View file

@ -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<Branding>.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<DalamudAssetManager>.Get().GetDalamudTextureWrap(DalamudAsset.Logo);
ImGui.Image(this.logoTexture.ImGuiHandle, ImGuiHelpers.ScaledVector2(imageSize));
ImGuiHelpers.ScaledDummy(0, 20f);

View file

@ -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<InterfaceManager>.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"))
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 = 9.6f / 12.0f;
ImGui.GetIO().FontGlobalScale = this.globalUiScale;
ImGui.SameLine();
if (ImGui.Button(buttonLabel, buttonSize) && Math.Abs(this.globalUiScale - scale) > float.Epsilon)
{
ImGui.GetIO().FontGlobalScale = this.globalUiScale = scale;
interfaceManager.RebuildFonts();
}
ImGui.SameLine();
if (ImGui.Button("12pt##DalamudSettingsGlobalUiScaleReset12"))
{
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();
}
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;

View file

@ -211,8 +211,8 @@ 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.SliderFloat2("WindowPadding", ref style.WindowPadding, 0.0f, 20.0f, "%.0f");
@ -254,17 +254,17 @@ public class StyleEditorWindow : Window
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.EndTabItem();
}
if (ImGui.BeginTabItem(Loc.Localize("StyleEditorColors", "Colors")))
{
ImGui.BeginChild("ScrollingColors", ImGuiHelpers.ScaledVector2(0, -30), true, ImGuiWindowFlags.HorizontalScrollbar | ImGuiWindowFlags.NoBackground);
if (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))
@ -325,6 +325,7 @@ public class StyleEditorWindow : Window
}
ImGui.EndChild();
}
ImGui.EndTabItem();
}

View file

@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
@ -12,8 +11,9 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
using Dalamud.Storage.Assets;
using ImGuiNET;
using ImGuiScene;
namespace Dalamud.Interface.Internal.Windows;
@ -25,7 +25,13 @@ internal class TitleScreenMenuWindow : Window, IDisposable
private const float TargetFontSizePt = 18f;
private const float TargetFontSizePx = TargetFontSizePt * 4 / 3;
private readonly IDalamudTextureWrap shadeTexture;
private readonly ClientState clientState;
private readonly DalamudConfiguration configuration;
private readonly Framework framework;
private readonly GameGui gameGui;
private readonly TitleScreenMenu titleScreenMenu;
private readonly Lazy<IDalamudTextureWrap> shadeTexture;
private readonly Dictionary<Guid, InOutCubic> shadeEasings = new();
private readonly Dictionary<Guid, InOutQuint> moveEasings = new();
@ -39,12 +45,30 @@ internal class TitleScreenMenuWindow : Window, IDisposable
/// <summary>
/// Initializes a new instance of the <see cref="TitleScreenMenuWindow"/> class.
/// </summary>
public TitleScreenMenuWindow()
/// <param name="clientState">An instance of <see cref="ClientState"/>.</param>
/// <param name="configuration">An instance of <see cref="DalamudConfiguration"/>.</param>
/// <param name="dalamudAssetManager">An instance of <see cref="DalamudAssetManager"/>.</param>
/// <param name="framework">An instance of <see cref="Framework"/>.</param>
/// <param name="titleScreenMenu">An instance of <see cref="TitleScreenMenu"/>.</param>
/// <param name="gameGui">An instance of <see cref="gameGui"/>.</param>
public TitleScreenMenuWindow(
ClientState clientState,
DalamudConfiguration configuration,
DalamudAssetManager dalamudAssetManager,
Framework framework,
GameGui gameGui,
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,14 +77,8 @@ internal class TitleScreenMenuWindow : Window, IDisposable
this.PositionCondition = ImGuiCond.Always;
this.RespectCloseHotkey = false;
var dalamud = Service<Dalamud>.Get();
var interfaceManager = Service<InterfaceManager>.Get();
this.shadeTexture = new(() => dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade));
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<Framework>.Get();
framework.Update += this.FrameworkOnUpdate;
}
@ -94,9 +112,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
/// <inheritdoc/>
public void Dispose()
{
this.shadeTexture.Dispose();
var framework = Service<Framework>.Get();
framework.Update -= this.FrameworkOnUpdate;
this.framework.Update -= this.FrameworkOnUpdate;
}
/// <inheritdoc/>
@ -106,17 +122,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable
return;
var scale = ImGui.GetIO().FontGlobalScale;
var entries = Service<TitleScreenMenu>.Get().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))
{
@ -136,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.
@ -150,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 |
@ -182,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++;
}
}
@ -266,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();
@ -344,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);
@ -367,19 +388,16 @@ internal class TitleScreenMenuWindow : Window, IDisposable
return isHover;
}
private void FrameworkOnUpdate(IFramework framework)
private void FrameworkOnUpdate(IFramework unused)
{
var clientState = Service<ClientState>.Get();
this.IsOpen = !clientState.IsLoggedIn;
this.IsOpen = !this.clientState.IsLoggedIn;
var configuration = Service<DalamudConfiguration>.Get();
if (!configuration.ShowTsm)
if (!this.configuration.ShowTsm)
this.IsOpen = false;
var gameGui = Service<GameGui>.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;
}

View file

@ -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<TitleScreenMenuEntry> entries = new();
private TitleScreenMenuEntry[]? entriesView;
[ServiceManager.ServiceConstructor]
private TitleScreenMenu()
{
}
/// <summary>
/// Event to be called when the entry list has been changed.
/// </summary>
internal event Action? EntryListChange;
/// <inheritdoc/>
public IReadOnlyList<TitleScreenMenuEntry> Entries => this.entries;
public IReadOnlyList<TitleScreenMenuEntry> Entries
{
get
{
lock (this.entries)
{
if (!this.entries.Any())
return Array.Empty<TitleScreenMenuEntry>();
return this.entriesView ??= this.entries.OrderByDescending(x => x.IsInternal).ToArray();
}
}
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
@ -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;
}
/// <inheritdoc/>
@ -80,7 +107,10 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu
lock (this.entries)
{
this.entries.Remove(entry);
this.entriesView = null;
}
this.EntryListChange?.InvokeSafely();
}
/// <summary>
@ -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;
}
/// <summary>
@ -116,28 +150,37 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu
/// <param name="text">The text to show.</param>
/// <param name="texture">The texture to show.</param>
/// <param name="onTriggered">The action to execute when the option is selected.</param>
/// <param name="showConditionKeys">The keys that have to be held to display the menu.</param>
/// <returns>A <see cref="TitleScreenMenu"/> object that can be used to manage the entry.</returns>
/// <exception cref="ArgumentException">Thrown when the texture provided does not match the required resolution(64x64).</exception>
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;
}
}

View file

@ -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<TitleScreenMenuEntry>
/// <param name="text">The text to show.</param>
/// <param name="texture">The texture to show.</param>
/// <param name="onTriggered">The action to execute when the option is selected.</param>
internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, IDalamudTextureWrap texture, Action onTriggered)
/// <param name="showConditionKeys">The keys that have to be held to display the menu.</param>
internal TitleScreenMenuEntry(
Assembly? callingAssembly,
ulong priority,
string text,
IDalamudTextureWrap texture,
Action onTriggered,
IEnumerable<VirtualKey>? showConditionKeys = null)
{
this.CallingAssembly = callingAssembly;
this.Priority = priority;
this.Name = text;
this.Texture = texture;
this.onTriggered = onTriggered;
this.ShowConditionKeys = (showConditionKeys ?? Array.Empty<VirtualKey>()).ToImmutableSortedSet();
}
/// <summary>
@ -58,6 +70,11 @@ public class TitleScreenMenuEntry : IComparable<TitleScreenMenuEntry>
/// </summary>
internal Guid Id { get; init; } = Guid.NewGuid();
/// <summary>
/// Gets the keys that have to be pressed to show the menu.
/// </summary>
internal IReadOnlySet<VirtualKey> ShowConditionKeys { get; init; }
/// <inheritdoc/>
public int CompareTo(TitleScreenMenuEntry? other)
{
@ -84,6 +101,13 @@ public class TitleScreenMenuEntry : IComparable<TitleScreenMenuEntry>
return 0;
}
/// <summary>
/// Determines the displaying condition of this menu entry is met.
/// </summary>
/// <returns>True if met.</returns>
internal bool IsShowConditionSatisfied() =>
this.ShowConditionKeys.All(x => Service<KeyState>.GetNullable()?[x] is true);
/// <summary>
/// Trigger the action associated with this entry.
/// </summary>

View file

@ -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
/// </summary>
public static float GlobalScale { get; private set; }
/// <summary>
/// Gets a value indicating whether ImGui is initialized and ready for use.<br />
/// This does not necessarily mean you can call drawing functions.
/// </summary>
public static unsafe bool IsImGuiInitialized =>
ImGui.GetCurrentContext() is not 0 && ImGui.GetIO().NativePtr is not null;
/// <summary>
/// Gets the global Dalamud scale; even available before drawing is ready.<br />
/// If you are sure that drawing is ready, at the point of using this, use <see cref="GlobalScale"/> instead.
/// </summary>
public static float GlobalScaleSafe =>
IsImGuiInitialized ? ImGui.GetIO().FontGlobalScale : Service<DalamudConfiguration>.Get().GlobalUiScale;
/// <summary>
/// Check if the current ImGui window is on the main viewport.
/// Only valid within a window.
@ -174,6 +189,47 @@ public static class ImGuiHelpers
}
}
/// <summary>
/// Unscales fonts after they have been rendered onto atlas.
/// </summary>
/// <param name="fontPtr">Font to scale.</param>
/// <param name="scale">Scale.</param>
/// <param name="round">If a positive number is given, numbers will be rounded to this.</param>
public static unsafe void AdjustGlyphMetrics(this ImFontPtr fontPtr, float scale, float round = 0f)
{
Func<float, float> 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<ImFontGlyphHotDataReal>(
(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<ImFontGlyphReal>((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<ImFontKerningPair>((void*)font->KerningPairs.Data, font->KerningPairs.Size))
kp.AdvanceXAdjustment = rounder(kp.AdvanceXAdjustment * scale);
foreach (ref var fkp in new Span<float>((void*)font->FrequentKerningPairs.Data, font->FrequentKerningPairs.Size))
fkp = rounder(fkp * scale);
}
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
@ -183,50 +239,86 @@ public static class ImGuiHelpers
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
/// <param name="rangeLow">Low codepoint range to copy.</param>
/// <param name="rangeHigh">High codepoing range to copy.</param>
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);
/// <summary>
/// Fills missing glyphs in target font from source font, if both are not null.
/// </summary>
/// <param name="source">Source font.</param>
/// <param name="target">Target font.</param>
/// <param name="missingOnly">Whether to copy missing glyphs only.</param>
/// <param name="rebuildLookupTable">Whether to call target.BuildLookupTable().</param>
/// <param name="rangeLow">Low codepoint range to copy.</param>
/// <param name="rangeHigh">High codepoing range to copy.</param>
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<int>();
unsafe
{
var glyphs = (ImFontGlyphReal*)source.Value!.Glyphs.Data;
for (int j = 0, k = source.Value!.Glyphs.Size; j < k; j++)
{
Debug.Assert(glyphs != null, nameof(glyphs) + " != null");
var glyph = &glyphs[j];
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 glyph = &glyphs![j];
if (glyph->Codepoint < rangeLow || glyph->Codepoint > rangeHigh)
continue;
var prevGlyphPtr = (ImFontGlyphReal*)target.Value!.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr;
var prevGlyphPtr = (ImFontGlyphReal*)target.FindGlyphNoFallback((ushort)glyph->Codepoint).NativePtr;
if ((IntPtr)prevGlyphPtr == IntPtr.Zero)
{
addedCodepoints.Add(glyph->Codepoint);
target.Value!.AddGlyph(
target.Value!.ConfigData,
target.AddGlyph(
target.ConfigData,
(ushort)glyph->Codepoint,
glyph->TextureIndex,
glyph->X0 * scale,
((glyph->Y0 - source.Value!.Ascent) * scale) + target.Value!.Ascent,
((glyph->Y0 - source.Ascent) * scale) + target.Ascent,
glyph->X1 * scale,
((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent,
((glyph->Y1 - source.Ascent) * scale) + target.Ascent,
glyph->U0,
glyph->V0,
glyph->U1,
glyph->V1,
glyph->AdvanceX * scale);
changed = true;
}
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->Y0 = ((glyph->Y0 - source.Ascent) * scale) + target.Ascent;
prevGlyphPtr->X1 = glyph->X1 * scale;
prevGlyphPtr->Y1 = ((glyph->Y1 - source.Value!.Ascent) * scale) + target.Value!.Ascent;
prevGlyphPtr->Y1 = ((glyph->Y1 - source.Ascent) * scale) + target.Ascent;
prevGlyphPtr->U0 = glyph->U0;
prevGlyphPtr->V0 = glyph->V0;
prevGlyphPtr->U1 = glyph->U1;
@ -235,19 +327,22 @@ public static class ImGuiHelpers
}
}
var kernPairs = source.Value!.KerningPairs;
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.Value.AddKerningPair(kernPairs[j].Left, kernPairs[j].Right, kernPairs[j].AdvanceXAdjustment);
}
target.AddKerningPair(kernPairs[j].Left, kernPairs[j].Right, kernPairs[j].AdvanceXAdjustment);
changed = true;
}
if (rebuildLookupTable && target.Value!.Glyphs.Size > 0)
target.Value!.BuildLookupTableNonstandard();
if (changed && rebuildLookupTable)
target.BuildLookupTableNonstandard();
}
/// <summary>
@ -302,21 +397,35 @@ public static class ImGuiHelpers
/// Center the ImGui cursor for a certain text.
/// </summary>
/// <param name="text">The text to center for.</param>
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);
/// <summary>
/// Center the ImGui cursor for an item with a certain width.
/// </summary>
/// <param name="itemWidth">The width to center for.</param>
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));
/// <summary>
/// Determines whether <paramref name="ptr"/> is empty.
/// </summary>
/// <param name="ptr">The pointer.</param>
/// <returns>Whether it is empty.</returns>
public static unsafe bool IsNull(this ImFontPtr ptr) => ptr.NativePtr == null;
/// <summary>
/// Determines whether <paramref name="ptr"/> is not null and loaded.
/// </summary>
/// <param name="ptr">The pointer.</param>
/// <returns>Whether it is empty.</returns>
public static unsafe bool IsNotNullAndLoaded(this ImFontPtr ptr) => ptr.NativePtr != null && ptr.IsLoaded();
/// <summary>
/// Determines whether <paramref name="ptr"/> is empty.
/// </summary>
/// <param name="ptr">The pointer.</param>
/// <returns>Whether it is empty.</returns>
public static unsafe bool IsNull(this ImFontAtlasPtr ptr) => ptr.NativePtr == null;
/// <summary>
/// Get data needed for each new frame.
@ -330,19 +439,57 @@ public static class ImGuiHelpers
/// ImFontGlyph the correct version.
/// </summary>
[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.
/// </summary>
[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);
}
}
}

View file

@ -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;
/// <summary>
/// Utility methods for <see cref="ImVectorWrapper{T}"/>.
/// </summary>
public static class ImVectorWrapper
{
/// <summary>
/// Creates a new instance of the <see cref="ImVectorWrapper{T}"/> struct, initialized with
/// <paramref name="sourceEnumerable"/>.<br />
/// You must call <see cref="ImVectorWrapper{T}.Dispose"/> after use.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="sourceEnumerable">The initial data.</param>
/// <param name="destroyer">The destroyer function to call on item removal.</param>
/// <param name="minCapacity">The minimum capacity of the new vector.</param>
/// <returns>The new wrapped vector, that has to be disposed after use.</returns>
public static ImVectorWrapper<T> CreateFromEnumerable<T>(
IEnumerable<T> sourceEnumerable,
ImVectorWrapper<T>.ImGuiNativeDestroyDelegate? destroyer = null,
int minCapacity = 0)
where T : unmanaged
{
var res = new ImVectorWrapper<T>(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<T> 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;
}
}
/// <summary>
/// Creates a new instance of the <see cref="ImVectorWrapper{T}"/> struct, initialized with
/// <paramref name="sourceSpan"/>.<br />
/// You must call <see cref="ImVectorWrapper{T}.Dispose"/> after use.
/// </summary>
/// <typeparam name="T">The item type.</typeparam>
/// <param name="sourceSpan">The initial data.</param>
/// <param name="destroyer">The destroyer function to call on item removal.</param>
/// <param name="minCapacity">The minimum capacity of the new vector.</param>
/// <returns>The new wrapped vector, that has to be disposed after use.</returns>
public static ImVectorWrapper<T> CreateFromSpan<T>(
ReadOnlySpan<T> sourceSpan,
ImVectorWrapper<T>.ImGuiNativeDestroyDelegate? destroyer = null,
int minCapacity = 0)
where T : unmanaged
{
var res = new ImVectorWrapper<T>(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;
}
}
/// <summary>
/// Wraps <see cref="ImFontAtlas.ConfigData"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImFontConfig> ConfigDataWrapped(this ImFontAtlasPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->ConfigData, ImGuiNative.ImFontConfig_destroy);
/// <summary>
/// Wraps <see cref="ImFontAtlas.Fonts"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImFontPtr> FontsWrapped(this ImFontAtlasPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->Fonts, x => ImGuiNative.ImFont_destroy(x->NativePtr));
/// <summary>
/// Wraps <see cref="ImFontAtlas.Textures"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImFontAtlasTexture> TexturesWrapped(this ImFontAtlasPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->Textures);
/// <summary>
/// Wraps <see cref="ImFont.Glyphs"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImGuiHelpers.ImFontGlyphReal> GlyphsWrapped(this ImFontPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->Glyphs);
/// <summary>
/// Wraps <see cref="ImFont.IndexedHotData"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ImGuiHelpers.ImFontGlyphHotDataReal> IndexedHotDataWrapped(this ImFontPtr obj)
=> obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->IndexedHotData);
/// <summary>
/// Wraps <see cref="ImFont.IndexLookup"/> into a <see cref="ImVectorWrapper{T}"/>.<br />
/// This does not need to be disposed.
/// </summary>
/// <param name="obj">The owner object.</param>
/// <returns>The wrapped vector.</returns>
public static unsafe ImVectorWrapper<ushort> IndexLookupWrapped(this ImFontPtr obj) =>
obj.NativePtr is null
? throw new NullReferenceException()
: new(&obj.NativePtr->IndexLookup);
}
/// <summary>
/// Wrapper for ImVector.
/// </summary>
/// <typeparam name="T">Contained type.</typeparam>
public unsafe struct ImVectorWrapper<T> : IList<T>, IList, IReadOnlyList<T>, IDisposable
where T : unmanaged
{
private ImVector* vector;
private ImGuiNativeDestroyDelegate? destroyer;
/// <summary>
/// Initializes a new instance of the <see cref="ImVectorWrapper{T}"/> struct.<br />
/// If <paramref name="ownership"/> is set to true, you must call <see cref="Dispose"/> after use,
/// and the underlying memory for <see cref="ImVector"/> must have been allocated using
/// <see cref="ImGuiNative.igMemAlloc"/>. Otherwise, it will crash.
/// </summary>
/// <param name="vector">The underlying vector.</param>
/// <param name="destroyer">The destroyer function to call on item removal.</param>
/// <param name="ownership">Whether this wrapper owns the vector.</param>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="ImVectorWrapper{T}"/> struct.<br />
/// You must call <see cref="Dispose"/> after use.
/// </summary>
/// <param name="initialCapacity">The initial capacity.</param>
/// <param name="destroyer">The destroyer function to call on item removal.</param>
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;
}
}
/// <summary>
/// Destroy callback for items.
/// </summary>
/// <param name="self">Pointer to self.</param>
public delegate void ImGuiNativeDestroyDelegate(T* self);
/// <summary>
/// Gets the raw vector.
/// </summary>
public ImVector* RawVector => this.vector;
/// <summary>
/// Gets a <see cref="Span{T}"/> view of the underlying ImVector{T}, for the range of <see cref="Length"/>.
/// </summary>
public Span<T> DataSpan => new(this.DataUnsafe, this.LengthUnsafe);
/// <summary>
/// Gets a <see cref="Span{T}"/> view of the underlying ImVector{T}, for the range of <see cref="Capacity"/>.
/// </summary>
public Span<T> StorageSpan => new(this.DataUnsafe, this.CapacityUnsafe);
/// <summary>
/// Gets a value indicating whether this <see cref="ImVectorWrapper{T}"/> is disposed.
/// </summary>
public bool IsDisposed => this.vector is null;
/// <summary>
/// Gets a value indicating whether this <see cref="ImVectorWrapper{T}"/> has the ownership of the underlying
/// <see cref="ImVector"/>.
/// </summary>
public bool HasOwnership { get; private set; }
/// <summary>
/// Gets the underlying <see cref="ImVector"/>.
/// </summary>
public ImVector* Vector =>
this.vector is null ? throw new ObjectDisposedException(nameof(ImVectorWrapper<T>)) : this.vector;
/// <summary>
/// Gets the number of items contained inside the underlying ImVector{T}.
/// </summary>
public int Length => this.LengthUnsafe;
/// <summary>
/// Gets the number of items <b>that can be</b> contained inside the underlying ImVector{T}.
/// </summary>
public int Capacity => this.CapacityUnsafe;
/// <summary>
/// Gets the pointer to the first item in the data inside underlying ImVector{T}.
/// </summary>
public T* Data => this.DataUnsafe;
/// <summary>
/// Gets the reference to the number of items contained inside the underlying ImVector{T}.
/// </summary>
public ref int LengthUnsafe => ref *&this.Vector->Size;
/// <summary>
/// Gets the reference to the number of items <b>that can be</b> contained inside the underlying ImVector{T}.
/// </summary>
public ref int CapacityUnsafe => ref *&this.Vector->Capacity;
/// <summary>
/// Gets the reference to the pointer to the first item in the data inside underlying ImVector{T}.
/// </summary>
/// <remarks>This may be null, if <see cref="Capacity"/> is zero.</remarks>
public ref T* DataUnsafe => ref *(T**)&this.Vector->Data;
/// <inheritdoc cref="ICollection{T}.IsReadOnly"/>
public bool IsReadOnly => false;
/// <inheritdoc/>
int ICollection.Count => this.LengthUnsafe;
/// <inheritdoc/>
bool ICollection.IsSynchronized => false;
/// <inheritdoc/>
object ICollection.SyncRoot { get; } = new();
/// <inheritdoc/>
int ICollection<T>.Count => this.LengthUnsafe;
/// <inheritdoc/>
int IReadOnlyCollection<T>.Count => this.LengthUnsafe;
/// <inheritdoc/>
bool IList.IsFixedSize => false;
/// <summary>
/// Gets the element at the specified index as a reference.
/// </summary>
/// <param name="index">Index of the item.</param>
/// <exception cref="IndexOutOfRangeException">If <paramref name="index"/> is out of range.</exception>
public ref T this[int index] => ref this.DataUnsafe[this.EnsureIndex(index)];
/// <inheritdoc/>
T IReadOnlyList<T>.this[int index] => this[index];
/// <inheritdoc/>
object? IList.this[int index]
{
get => this[index];
set => this[index] = value is null ? default : (T)value;
}
/// <inheritdoc/>
T IList<T>.this[int index]
{
get => this[index];
set => this[index] = value;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public IEnumerator<T> GetEnumerator()
{
foreach (var i in Enumerable.Range(0, this.LengthUnsafe))
yield return this[i];
}
/// <inheritdoc cref="ICollection{T}.Add"/>
public void Add(in T item)
{
this.EnsureCapacityExponential(this.LengthUnsafe + 1);
this.DataUnsafe[this.LengthUnsafe++] = item;
}
/// <inheritdoc cref="List{T}.AddRange"/>
public void AddRange(IEnumerable<T> items)
{
if (items is ICollection { Count: var count })
this.EnsureCapacityExponential(this.LengthUnsafe + count);
foreach (var item in items)
this.Add(item);
}
/// <inheritdoc cref="List{T}.AddRange"/>
public void AddRange(Span<T> items)
{
this.EnsureCapacityExponential(this.LengthUnsafe + items.Length);
foreach (var item in items)
this.Add(item);
}
/// <inheritdoc cref="ICollection{T}.Clear"/>
public void Clear() => this.Clear(false);
/// <summary>
/// Clears this vector, optionally skipping destroyer invocation.
/// </summary>
/// <param name="skipDestroyer">Whether to skip destroyer invocation.</param>
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;
}
/// <inheritdoc cref="ICollection{T}.Contains"/>
public bool Contains(in T item) => this.IndexOf(in item) != -1;
/// <summary>
/// Size down the underlying ImVector{T}.
/// </summary>
/// <param name="reservation">Capacity to reserve.</param>
/// <returns>Whether the capacity has been changed.</returns>
public bool Compact(int reservation) => this.SetCapacity(Math.Max(reservation, this.LengthUnsafe));
/// <inheritdoc cref="ICollection{T}.CopyTo"/>
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<T> 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));
}
/// <summary>
/// Ensures that the capacity of this list is at least the specified <paramref name="capacity"/>.<br />
/// On growth, the new capacity exactly matches <paramref name="capacity"/>.
/// </summary>
/// <param name="capacity">The minimum capacity to ensure.</param>
/// <returns>Whether the capacity has been changed.</returns>
public bool EnsureCapacity(int capacity) => this.CapacityUnsafe < capacity && this.SetCapacity(capacity);
/// <summary>
/// Ensures that the capacity of this list is at least the specified <paramref name="capacity"/>.<br />
/// On growth, the new capacity may exceed <paramref name="capacity"/>.
/// </summary>
/// <param name="capacity">The minimum capacity to ensure.</param>
/// <returns>Whether the capacity has been changed.</returns>
public bool EnsureCapacityExponential(int capacity)
=> this.EnsureCapacity(1 << ((sizeof(int) * 8) - BitOperations.LeadingZeroCount((uint)this.LengthUnsafe)));
/// <summary>
/// Resizes the underlying array and fills with zeroes if grown.
/// </summary>
/// <param name="size">New size.</param>
/// <param name="defaultValue">New default value.</param>
/// <param name="skipDestroyer">Whether to skip calling destroyer function.</param>
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);
}
/// <inheritdoc cref="ICollection{T}.Remove"/>
public bool Remove(in T item)
{
var index = this.IndexOf(item);
if (index == -1)
return false;
this.RemoveAt(index);
return true;
}
/// <inheritdoc cref="IList{T}.IndexOf"/>
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;
}
/// <inheritdoc cref="IList{T}.Insert"/>
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;
}
/// <inheritdoc cref="List{T}.InsertRange"/>
public void InsertRange(int index, IEnumerable<T> 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);
}
}
/// <inheritdoc cref="List{T}.AddRange"/>
public void InsertRange(int index, Span<T> 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;
}
/// <summary>
/// Removes the element at the given index.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="skipDestroyer">Whether to skip calling the destroyer function.</param>
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));
}
/// <inheritdoc/>
void IList<T>.RemoveAt(int index) => this.RemoveAt(index);
/// <inheritdoc/>
void IList.RemoveAt(int index) => this.RemoveAt(index);
/// <summary>
/// Sets the capacity exactly as requested.
/// </summary>
/// <param name="capacity">New capacity.</param>
/// <returns>Whether the capacity has been changed.</returns>
/// <exception cref="ArgumentOutOfRangeException">If <paramref name="capacity"/> is less than <see cref="Length"/>.</exception>
/// <exception cref="OutOfMemoryException">If memory for the requested capacity cannot be allocated.</exception>
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<T>(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<T>(newAlloc, capacity);
if (!oldSpan.IsEmpty && !newSpan.IsEmpty)
oldSpan[..this.LengthUnsafe].CopyTo(newSpan);
// #if DEBUG
// new Span<byte>(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;
}
/// <inheritdoc/>
void ICollection<T>.Add(T item) => this.Add(in item);
/// <inheritdoc/>
bool ICollection<T>.Contains(T item) => this.Contains(in item);
/// <inheritdoc/>
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<T> 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);
}
/// <inheritdoc/>
bool ICollection<T>.Remove(T item) => this.Remove(in item);
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();
/// <inheritdoc/>
int IList.Add(object? value)
{
this.Add(value is null ? default : (T)value);
return this.LengthUnsafe - 1;
}
/// <inheritdoc/>
bool IList.Contains(object? value) => this.Contains(value is null ? default : (T)value);
/// <inheritdoc/>
int IList.IndexOf(object? value) => this.IndexOf(value is null ? default : (T)value);
/// <inheritdoc/>
void IList.Insert(int index, object? value) => this.Insert(index, value is null ? default : (T)value);
/// <inheritdoc/>
void IList.Remove(object? value) => this.Remove(value is null ? default : (T)value);
/// <inheritdoc/>
int IList<T>.IndexOf(T item) => this.IndexOf(in item);
/// <inheritdoc/>
void IList<T>.Insert(int index, T item) => this.Insert(index, in item);
private int EnsureIndex(int i) => i >= 0 && i < this.LengthUnsafe ? i : throw new IndexOutOfRangeException();
}

View file

@ -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.
/// </summary>
[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<ServiceManager.ScopedService>() != null)
if (serviceType.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != 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<ServiceManager.ScopedService>() != null;
return contains || type.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != null;
}
var parameters = ctor.GetParameters();

View file

@ -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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Verbose(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Verbose, messageTemplate, null, values);
@ -42,6 +42,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Debug(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Debug, messageTemplate, null, values);
@ -59,6 +61,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Information(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Information, messageTemplate, null, values);
@ -76,6 +80,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Warning(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Warning, messageTemplate, null, values);
@ -93,6 +99,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Error(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Error, messageTemplate, null, values);
@ -110,6 +118,7 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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
/// </summary>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[MessageTemplateFormatMethod("messageTemplate")]
public void Fatal(string messageTemplate, params object[] values)
=> this.WriteLog(LogEventLevel.Fatal, messageTemplate, null, values);
@ -127,9 +137,11 @@ public class ModuleLog
/// <param name="exception">The exception that caused the error.</param>
/// <param name="messageTemplate">The message template.</param>
/// <param name="values">Values to log.</param>
[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)
{

View file

@ -137,6 +137,7 @@ internal static partial class NativeFunctions
/// <summary>
/// MB_* from winuser.
/// </summary>
[Flags]
public enum MessageBoxType : uint
{
/// <summary>

View file

@ -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),
},
},
};
}
/// <summary>

View file

@ -1,10 +1,11 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
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;
@ -38,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}.
/// </summary>
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
#pragma warning disable SA1015
// DalamudTextureWrap registers textures to dispose with IM
@ -83,6 +84,9 @@ internal partial class PluginManager : IDisposable, IServiceType
[ServiceManager.ServiceDependency]
private readonly HappyHttpClient happyHttpClient = Service<HappyHttpClient>.Get();
[ServiceManager.ServiceDependency]
private readonly ChatGui chatGui = Service<ChatGui>.Get();
static PluginManager()
{
DalamudApiLevel = typeof(PluginManager).Assembly.GetName().Version!.Major;
@ -129,12 +133,13 @@ internal partial class PluginManager : IDisposable, IServiceType
throw new InvalidDataException("Couldn't deserialize banned plugins manifest.");
}
this.openInstallerWindowPluginChangelogsLink = Service<ChatGui>.Get().AddChatLinkHandler("Dalamud", 1003, (_, _) =>
this.openInstallerWindowPluginChangelogsLink = this.chatGui.AddChatLinkHandler("Dalamud", 1003, (_, _) =>
{
Service<DalamudInterface>.GetNullable()?.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.Changelogs);
});
this.configuration.PluginTestingOptIns ??= new List<PluginTestingOptIn>();
this.configuration.PluginTestingOptIns ??= new();
this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient);
// NET8 CHORE
//this.ApplyPatches();
@ -198,6 +203,11 @@ internal partial class PluginManager : IDisposable, IServiceType
}
}
/// <summary>
/// Gets the main repository.
/// </summary>
public PluginRepository MainRepo { get; }
/// <summary>
/// Gets a list of all plugin repositories. The main repo should always be first.
/// </summary>
@ -283,11 +293,9 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <param name="header">The header text to send to chat prior to any update info.</param>
public void PrintUpdatedPlugins(List<PluginUpdateStatus>? updateMetadata, string header)
{
var chatGui = Service<ChatGui>.Get();
if (updateMetadata is { Count: > 0 })
{
chatGui.Print(new XivChatEntry
this.chatGui.Print(new XivChatEntry
{
Message = new SeString(new List<Payload>()
{
@ -306,11 +314,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 +415,10 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task SetPluginReposFromConfigAsync(bool notify)
{
var repos = new List<PluginRepository>() { PluginRepository.MainRepo };
var repos = new List<PluginRepository> { 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);
@ -1199,7 +1207,17 @@ internal partial class PluginManager : IDisposable, IServiceType
private async Task<Stream> 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();

View file

@ -267,10 +267,6 @@ internal class LocalPlugin : IDisposable
var pluginManager = await Service<PluginManager>.GetAsync();
var dalamud = await Service<Dalamud>.GetAsync();
// UiBuilder constructor requires the following two.
await Service<InterfaceManager>.GetAsync();
await Service<GameFontManager>.GetAsync();
if (this.manifest.LoadRequiredState == 0)
_ = await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync();

View file

@ -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,12 +28,23 @@ internal class PluginRepository
/// </summary>
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 HttpClient HttpClient = new(new SocketsHttpHandler
private static readonly ModuleLog Log = new("PLUGINR");
private readonly HttpClient httpClient;
/// <summary>
/// Initializes a new instance of the <see cref="PluginRepository"/> class.
/// </summary>
/// <param name="happyHttpClient">An instance of <see cref="HappyHttpClient"/>.</param>
/// <param name="pluginMasterUrl">The plugin master URL.</param>
/// <param name="isEnabled">Whether the plugin repo is enabled.</param>
public PluginRepository(HappyHttpClient happyHttpClient, string pluginMasterUrl, bool isEnabled)
{
this.httpClient = new(new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.All,
ConnectCallback = Service<HappyHttpClient>.Get().SharedHappyEyeballsCallback.ConnectCallback,
ConnectCallback = happyHttpClient.SharedHappyEyeballsCallback.ConnectCallback,
})
{
Timeout = TimeSpan.FromSeconds(20),
@ -51,24 +64,11 @@ internal class PluginRepository
},
},
};
/// <summary>
/// Initializes a new instance of the <see cref="PluginRepository"/> class.
/// </summary>
/// <param name="pluginMasterUrl">The plugin master URL.</param>
/// <param name="isEnabled">Whether the plugin repo is enabled.</param>
public PluginRepository(string pluginMasterUrl, bool isEnabled)
{
this.PluginMasterUrl = pluginMasterUrl;
this.IsThirdParty = pluginMasterUrl != MainRepoUrl;
this.IsEnabled = isEnabled;
}
/// <summary>
/// Gets a new instance of the <see cref="PluginRepository"/> class for the main repo.
/// </summary>
public static PluginRepository MainRepo => new(MainRepoUrl, true);
/// <summary>
/// Gets the pluginmaster.json URL.
/// </summary>
@ -94,6 +94,14 @@ internal class PluginRepository
/// </summary>
public PluginRepositoryState State { get; private set; }
/// <summary>
/// Gets a new instance of the <see cref="PluginRepository"/> class for the main repo.
/// </summary>
/// <param name="happyHttpClient">An instance of <see cref="HappyHttpClient"/>.</param>
/// <returns>The new instance of main repository.</returns>
public static PluginRepository CreateMainRepo(HappyHttpClient happyHttpClient) =>
new(happyHttpClient, MainRepoUrl, true);
/// <summary>
/// Reload the plugin master asynchronously in a task.
/// </summary>
@ -107,7 +115,8 @@ internal class PluginRepository
{
Log.Information($"Fetching repo: {this.PluginMasterUrl}");
using var response = await HttpClient.GetAsync(this.PluginMasterUrl);
using var response = await this.GetPluginMaster(this.PluginMasterUrl);
response.EnsureSuccessStatusCode();
var data = await response.Content.ReadAsStringAsync();
@ -199,4 +208,17 @@ internal class PluginRepository
return true;
}
private async Task<HttpResponseMessage> GetPluginMaster(string url, int timeout = HttpRequestTimeoutSeconds)
{
var httpClient = Service<HappyHttpClient>.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);
}
}

View file

@ -13,7 +13,7 @@ namespace Dalamud.Plugin.Ipc.Internal;
/// <summary>
/// This class facilitates sharing data-references of standard types between plugins without using more expensive IPC.
/// </summary>
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal class DataShare : IServiceType
{
private readonly Dictionary<string, DataCache> caches = new();

View file

@ -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
/// </summary>
public static readonly ModuleLog Log = new("SVC");
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
#if DEBUG
/// <summary>
/// Marks which service constructor the current thread's in. For use from <see cref="Service{T}"/> only.
/// </summary>
internal static readonly ThreadLocal<Type?> CurrentConstructorServiceType = new();
[SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Debugging purposes")]
private static readonly List<Type> LoadedServices = new();
#endif
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
private static ManualResetEvent unloadResetEvent = new(false);
@ -86,21 +95,34 @@ internal static class ServiceManager
/// <param name="scanner">Instance of <see cref="TargetSigScanner"/>.</param>
public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner)
{
#if DEBUG
lock (LoadedServices)
{
void ProvideService<T>(T service) where T : IServiceType
{
Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute");
Service<T>.Provide(service);
LoadedServices.Add(typeof(T));
}
ProvideService(dalamud);
ProvideService(fs);
ProvideService(configuration);
ProvideService(new ServiceContainer());
ProvideService(scanner);
}
return;
void ProvideService<T>(T service) where T : IServiceType
{
Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute");
Service<T>.Provide(service);
LoadedServices.Add(typeof(T));
}
#else
ProvideService(dalamud);
ProvideService(fs);
ProvideService(configuration);
ProvideService(new ServiceContainer());
ProvideService(scanner);
return;
void ProvideService<T>(T service) where T : IServiceType => Service<T>.Provide(service);
#endif
}
/// <summary>
@ -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<IServiceType>.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
/// <returns>The type of service this type is.</returns>
public static ServiceKind GetServiceKind(this Type type)
{
var attr = type.GetCustomAttribute<Service>(true)?.GetType();
var attr = type.GetCustomAttribute<ServiceAttribute>(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.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class Service : Attribute
public abstract class ServiceAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ServiceAttribute"/> class.
/// </summary>
/// <param name="kind">The kind of the service.</param>
protected ServiceAttribute(ServiceKind kind) => this.Kind = kind;
/// <summary>
/// Gets the kind of the service.
/// </summary>
public ServiceKind Kind { get; }
}
/// <summary>
/// Indicates that the class is a service, that is provided by some other source.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class ProvidedServiceAttribute : ServiceAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ProvidedServiceAttribute"/> class.
/// </summary>
public ProvidedServiceAttribute()
: base(ServiceKind.ProvidedService)
{
}
}
/// <summary>
/// Indicates that the class is a service, and will be instantiated automatically on startup.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class EarlyLoadedService : Service
public class EarlyLoadedServiceAttribute : ServiceAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="EarlyLoadedServiceAttribute"/> class.
/// </summary>
public EarlyLoadedServiceAttribute()
: this(ServiceKind.EarlyLoadedService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EarlyLoadedServiceAttribute"/> class.
/// </summary>
/// <param name="kind">The service kind.</param>
protected EarlyLoadedServiceAttribute(ServiceKind kind)
: base(kind)
{
}
}
/// <summary>
@ -431,8 +513,15 @@ internal static class ServiceManager
/// blocking game main thread until it completes.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class BlockingEarlyLoadedService : EarlyLoadedService
public class BlockingEarlyLoadedServiceAttribute : EarlyLoadedServiceAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="BlockingEarlyLoadedServiceAttribute"/> class.
/// </summary>
public BlockingEarlyLoadedServiceAttribute()
: base(ServiceKind.BlockingEarlyLoadedService)
{
}
}
/// <summary>
@ -440,8 +529,15 @@ internal static class ServiceManager
/// service scope, and that it cannot be created outside of a scope.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class ScopedService : Service
public class ScopedServiceAttribute : ServiceAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ScopedServiceAttribute"/> class.
/// </summary>
public ScopedServiceAttribute()
: base(ServiceKind.ScopedService)
{
}
}
/// <summary>

View file

@ -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.
/// </remarks>
/// <typeparam name="T">The class you want to store in the service locator.</typeparam>
[SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")]
internal static class Service<T> where T : IServiceType
{
private static readonly ServiceManager.ServiceAttribute ServiceAttribute;
private static TaskCompletionSource<T> instanceTcs = new();
private static List<Type>? dependencyServices;
static Service()
{
var exposeToPlugins = typeof(T).GetCustomAttribute<PluginInterfaceAttribute>() != null;
var type = typeof(T);
ServiceAttribute =
type.GetCustomAttribute<ServiceManager.ServiceAttribute>(true)
?? throw new InvalidOperationException(
$"{nameof(T)} is missing {nameof(ServiceManager.ServiceAttribute)} annotations.");
var exposeToPlugins = type.GetCustomAttribute<PluginInterfaceAttribute>() != 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<ServiceContainer>.Get().RegisterSingleton(instanceTcs.Task);
@ -63,8 +71,8 @@ internal static class Service<T> where T : IServiceType
/// <param name="obj">Object to set.</param>
public static void Provide(T obj)
{
instanceTcs.SetResult(obj);
ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name);
instanceTcs.SetResult(obj);
}
/// <summary>
@ -83,6 +91,21 @@ internal static class Service<T> where T : IServiceType
/// <returns>The object.</returns>
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<IServiceType>)}<{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<T> where T : IServiceType
}
/// <summary>
/// Gets an enumerable containing Service&lt;T&gt;s that are required for this Service to initialize without blocking.
/// Gets an enumerable containing <see cref="Service{T}"/>s that are required for this Service to initialize
/// without blocking.
/// </summary>
/// <returns>List of dependency services.</returns>
[UsedImplicitly]
public static List<Type> GetDependencyServices()
{
if (dependencyServices is not null)
return dependencyServices;
var res = new List<Type>();
ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name);
@ -189,19 +216,42 @@ internal static class Service<T> where T : IServiceType
ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name);
}
return res
var deps = res
.Distinct()
.ToList();
if (typeof(T).GetCustomAttribute<ServiceManager.BlockingEarlyLoadedServiceAttribute>() is not null)
{
var offenders = deps.Where(
x => x.GetCustomAttribute<ServiceManager.ServiceAttribute>(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}")));
}
}
[UsedImplicitly]
private static Task<T> StartLoader()
return dependencyServices = deps;
}
/// <summary>
/// Starts the service loader. Only to be called from <see cref="ServiceManager"/>.
/// </summary>
/// <returns>The loader task.</returns>
internal static Task<T> StartLoader()
{
if (instanceTcs.Task.IsCompleted)
throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed.");
var attr = typeof(T).GetCustomAttribute<ServiceManager.Service>(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<T> where T : IServiceType
var instance = await ConstructObject();
instanceTcs.SetResult(instance);
List<Task>? tasks = null;
foreach (var method in typeof(T).GetMethods(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
@ -221,8 +272,23 @@ internal static class Service<T> 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,8 +369,20 @@ internal static class Service<T> where T : IServiceType
ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType)));
using (Timings.Start($"{typeof(T).Name} Construct"))
{
#if DEBUG
ServiceManager.CurrentConstructorServiceType.Value = typeof(Service<T>);
try
{
return (T)ctor.Invoke(args)!;
}
finally
{
ServiceManager.CurrentConstructorServiceType.Value = null;
}
#else
return (T)ctor.Invoke(args)!;
#endif
}
}
/// <summary>
@ -328,15 +406,24 @@ internal static class Service<T> where T : IServiceType
internal static class ServiceHelpers
{
/// <summary>
/// Get a list of dependencies for a service. Only accepts Service&lt;T&gt; types.
/// These are returned as Service&lt;T&gt; types.
/// Get a list of dependencies for a service. Only accepts <see cref="Service{T}"/> types.
/// These are returned as <see cref="Service{T}"/> types.
/// </summary>
/// <param name="serviceType">The dependencies for this service.</param>
/// <returns>A list of dependencies.</returns>
public static List<Type> GetDependencies(Type serviceType)
{
#if DEBUG
if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>))
{
throw new ArgumentException(
$"Expected an instance of {nameof(Service<IServiceType>)}<>",
nameof(serviceType));
}
#endif
return (List<Type>)serviceType.InvokeMember(
"GetDependencyServices",
nameof(Service<IServiceType>.GetDependencyServices),
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
@ -344,14 +431,18 @@ internal static class ServiceHelpers
}
/// <summary>
/// Get the Service&lt;T&gt; type for a given service type.
/// Get the <see cref="Service{T}"/> type for a given service type.
/// This will throw if the service type is not a valid service.
/// </summary>
/// <param name="type">The type to obtain a Service&lt;T&gt; for.</param>
/// <returns>The Service&lt;T&gt;.</returns>
/// <param name="type">The type to obtain a <see cref="Service{T}"/> for.</param>
/// <returns>The <see cref="Service{T}"/>.</returns>
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);
}
}

View file

@ -0,0 +1,36 @@
namespace Dalamud.Storage.Assets;
/// <summary>
/// Stores the basic information of a Dalamud asset.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
internal class DalamudAssetAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudAssetAttribute"/> class.
/// </summary>
/// <param name="purpose">The purpose.</param>
/// <param name="data">The data.</param>
/// <param name="required">Whether the asset is required.</param>
public DalamudAssetAttribute(DalamudAssetPurpose purpose, byte[]? data = null, bool required = true)
{
this.Purpose = purpose;
this.Data = data;
this.Required = required;
}
/// <summary>
/// Gets the purpose of the asset.
/// </summary>
public DalamudAssetPurpose Purpose { get; }
/// <summary>
/// Gets the data, if available.
/// </summary>
public byte[]? Data { get; }
/// <summary>
/// Gets a value indicating whether the asset is required.
/// </summary>
public bool Required { get; }
}

View file

@ -0,0 +1,17 @@
using Dalamud.Utility;
namespace Dalamud.Storage.Assets;
/// <summary>
/// Extension methods for <see cref="DalamudAsset"/>.
/// </summary>
public static class DalamudAssetExtensions
{
/// <summary>
/// Gets the purpose.
/// </summary>
/// <param name="asset">The asset.</param>
/// <returns>The purpose.</returns>
public static DalamudAssetPurpose GetPurpose(this DalamudAsset asset) =>
asset.GetAttribute<DalamudAssetAttribute>()?.Purpose ?? DalamudAssetPurpose.Empty;
}

View file

@ -0,0 +1,366 @@
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;
/// <summary>
/// A concrete class for <see cref="IDalamudAssetManager"/>.
/// </summary>
[PluginInterface]
[ServiceManager.BlockingEarlyLoadedService]
#pragma warning disable SA1015
[ResolveVia<IDalamudAssetManager>]
#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<DalamudAsset, Task<FileStream>?> fileStreams;
private readonly Dictionary<DalamudAsset, Task<IDalamudTextureWrap>?> 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<DalamudAsset>().ToDictionary(x => x, _ => (Task<FileStream>?)null);
this.textureWraps = Enum.GetValues<DalamudAsset>().ToDictionary(x => x, _ => (Task<IDalamudTextureWrap>?)null);
var loadTimings = Timings.Start("DAM LoadAll");
this.WaitForAllRequiredAssets().ContinueWith(_ => loadTimings.Dispose());
}
/// <inheritdoc/>
public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4);
/// <inheritdoc/>
public void Dispose()
{
lock (this.syncRoot)
{
if (this.isDisposed)
return;
this.isDisposed = true;
}
this.cancellationTokenSource.Cancel();
Task.WaitAll(
Array.Empty<Task>()
.Concat(this.fileStreams.Values)
.Concat(this.textureWraps.Values)
.Where(x => x is not null)
.ToArray());
this.scopedFinalizer.Dispose();
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The task.</returns>
[Pure]
public Task WaitForAllRequiredAssets()
{
lock (this.syncRoot)
{
return Task.WhenAll(
Enum.GetValues<DalamudAsset>()
.Where(x => x is not DalamudAsset.Empty4X4)
.Where(x => x.GetAttribute<DalamudAssetAttribute>()?.Required is true)
.Select(this.CreateStreamAsync)
.Select(x => x.ToContentDisposedTask()));
}
}
/// <inheritdoc/>
[Pure]
public bool IsStreamImmediatelyAvailable(DalamudAsset asset) =>
asset.GetAttribute<DalamudAssetAttribute>()?.Data is not null
|| this.fileStreams[asset]?.IsCompletedSuccessfully is true;
/// <inheritdoc/>
[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();
}
/// <inheritdoc/>
[Pure]
public Task<Stream> CreateStreamAsync(DalamudAsset asset)
{
if (asset.GetAttribute<DalamudAssetAttribute>() is { Data: { } rawData })
return Task.FromResult<Stream>(new MemoryStream(rawData, false));
Task<FileStream> 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<FileStream> CreateInnerAsync()
{
string path;
List<Exception?> exceptions = null;
foreach (var name in asset.GetAttributes<DalamudAssetPathAttribute>().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<DalamudAssetOnlineSourceAttribute>())
{
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);
}
}
/// <inheritdoc/>
[Pure]
public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) =>
ExtractResult(this.GetDalamudTextureWrapAsync(asset));
/// <inheritdoc/>
[Pure]
[return: NotNullIfNotNull(nameof(defaultWrap))]
public IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap)
{
var task = this.GetDalamudTextureWrapAsync(asset);
return task.IsCompletedSuccessfully ? task.Result : defaultWrap;
}
/// <inheritdoc/>
[Pure]
public Task<IDalamudTextureWrap> 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<IDalamudTextureWrap> task;
lock (this.syncRoot)
{
if (this.isDisposed)
throw new ObjectDisposedException(nameof(DalamudAssetManager));
task = this.textureWraps[asset] ??= CreateInnerAsync();
}
return task;
async Task<IDalamudTextureWrap> CreateInnerAsync()
{
var buf = Array.Empty<byte>();
try
{
var im = (await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync()).Manager;
await using var stream = await this.CreateStreamAsync(asset);
var length = checked((int)stream.Length);
buf = ArrayPool<byte>.Shared.Rent(length);
stream.ReadExactly(buf, 0, length);
var image = purpose switch
{
DalamudAssetPurpose.TextureFromPng => im.LoadImage(buf),
DalamudAssetPurpose.TextureFromRaw =>
asset.GetAttribute<DalamudAssetRawTextureAttribute>() 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<byte>.Shared.Return(buf);
}
}
}
private static T ExtractResult<T>(Task<T> t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
private Task<TOut> TransformImmediate<TIn, TOut>(Task<TIn> task, Func<TIn, TOut> transformer)
{
if (task.IsCompletedSuccessfully)
return Task.FromResult(transformer(task.Result));
if (task.Exception is { } exc)
return Task.FromException<TOut>(exc);
return task.ContinueWith(_ => this.TransformImmediate(task, transformer)).Unwrap();
}
private class DisposeSuppressingDalamudTextureWrap : IDalamudTextureWrap
{
private readonly IDalamudTextureWrap innerWrap;
public DisposeSuppressingDalamudTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap;
/// <inheritdoc/>
public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle;
/// <inheritdoc/>
public int Width => this.innerWrap.Width;
/// <inheritdoc/>
public int Height => this.innerWrap.Height;
/// <inheritdoc/>
public void Dispose()
{
// suppressed
}
}
}

View file

@ -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;
/// <summary>
/// Marks that an asset can be download from online.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
internal class DalamudAssetOnlineSourceAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudAssetOnlineSourceAttribute"/> class.
/// </summary>
/// <param name="url">The URL.</param>
public DalamudAssetOnlineSourceAttribute(string url)
{
this.Url = url;
}
/// <summary>
/// Gets the source URL of the file.
/// </summary>
public string Url { get; }
/// <summary>
/// Downloads to the given stream.
/// </summary>
/// <param name="client">The client.</param>
/// <param name="stream">The stream.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The task.</returns>
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.");
}
}

View file

@ -0,0 +1,21 @@
using System.IO;
namespace Dalamud.Storage.Assets;
/// <summary>
/// File names to look up in Dalamud assets.
/// </summary>
[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
internal class DalamudAssetPathAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudAssetPathAttribute"/> class.
/// </summary>
/// <param name="pathComponents">The path components.</param>
public DalamudAssetPathAttribute(params string[] pathComponents) => this.FileName = Path.Join(pathComponents);
/// <summary>
/// Gets the file name.
/// </summary>
public string FileName { get; }
}

View file

@ -0,0 +1,27 @@
namespace Dalamud.Storage.Assets;
/// <summary>
/// Purposes of a Dalamud asset.
/// </summary>
public enum DalamudAssetPurpose
{
/// <summary>
/// The asset has no purpose.
/// </summary>
Empty = 0,
/// <summary>
/// The asset is a .png file, and can be purposed as a <see cref="SharpDX.Direct3D11.Texture2D"/>.
/// </summary>
TextureFromPng = 10,
/// <summary>
/// The asset is a raw texture, and can be purposed as a <see cref="SharpDX.Direct3D11.Texture2D"/>.
/// </summary>
TextureFromRaw = 1001,
/// <summary>
/// The asset is a font file.
/// </summary>
Font = 2000,
}

View file

@ -0,0 +1,45 @@
using SharpDX.DXGI;
namespace Dalamud.Storage.Assets;
/// <summary>
/// Provide raw texture data directly.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
internal class DalamudAssetRawTextureAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="DalamudAssetRawTextureAttribute"/> class.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="pitch">The pitch.</param>
/// <param name="height">The height.</param>
/// <param name="format">The format.</param>
public DalamudAssetRawTextureAttribute(int width, int pitch, int height, Format format)
{
this.Width = width;
this.Pitch = pitch;
this.Height = height;
this.Format = format;
}
/// <summary>
/// Gets the width.
/// </summary>
public int Width { get; }
/// <summary>
/// Gets the pitch.
/// </summary>
public int Pitch { get; }
/// <summary>
/// Gets the height.
/// </summary>
public int Height { get; }
/// <summary>
/// Gets the format.
/// </summary>
public Format Format { get; }
}

View file

@ -0,0 +1,79 @@
using System.Diagnostics.Contracts;
using System.IO;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
namespace Dalamud.Storage.Assets;
/// <summary>
/// Holds Dalamud Assets' handles hostage, so that they do not get closed while Dalamud is running.<br />
/// Also, attempts to load optional assets.<br />
/// <br />
/// <strong>Note on <see cref="PureAttribute"/></strong><br />
/// 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.
/// </summary>
internal interface IDalamudAssetManager
{
/// <summary>
/// Gets the shared texture wrap for <see cref="DalamudAsset.Empty4X4"/>.
/// </summary>
IDalamudTextureWrap Empty4X4 { get; }
/// <summary>
/// Gets whether the stream for the asset is instantly available.
/// </summary>
/// <param name="asset">The asset.</param>
/// <returns>Whether the stream of an asset is immediately available.</returns>
[Pure]
bool IsStreamImmediatelyAvailable(DalamudAsset asset);
/// <summary>
/// Creates a stream backed by the specified asset, waiting as necessary.<br />
/// <strong>Call <see cref="IDisposable.Dispose"/> after use.</strong>
/// </summary>
/// <param name="asset">The asset.</param>
/// <returns>The stream.</returns>
[Pure]
Stream CreateStream(DalamudAsset asset);
/// <summary>
/// Creates a stream backed by the specified asset.<br />
/// <strong>Call <see cref="IDisposable.Dispose"/> after use.</strong>
/// </summary>
/// <param name="asset">The asset.</param>
/// <returns>The stream, wrapped inside a <see cref="Stream"/>.</returns>
[Pure]
Task<Stream> CreateStreamAsync(DalamudAsset asset);
/// <summary>
/// Gets a shared instance of <see cref="IDalamudTextureWrap"/>, after waiting as necessary.<br />
/// Calls to <see cref="IDisposable.Dispose"/> is unnecessary; they will be ignored.
/// </summary>
/// <param name="asset">The texture asset.</param>
/// <returns>The texture wrap.</returns>
[Pure]
IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset);
/// <summary>
/// Gets a shared instance of <see cref="IDalamudTextureWrap"/> if it is available instantly;
/// if it is not ready, returns <paramref name="defaultWrap"/>.<br />
/// Calls to <see cref="IDisposable.Dispose"/> is unnecessary; they will be ignored.
/// </summary>
/// <param name="asset">The texture asset.</param>
/// <param name="defaultWrap">The default return value, if the asset is not ready for whatever reason.</param>
/// <returns>The texture wrap.</returns>
[Pure]
IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap);
/// <summary>
/// Gets a shared instance of <see cref="IDalamudTextureWrap"/> in a <see cref="Task{T}"/>.<br />
/// Calls to <see cref="IDisposable.Dispose"/> is unnecessary; they will be ignored.
/// </summary>
/// <param name="asset">The texture asset.</param>
/// <returns>The new texture wrap, wrapped inside a <see cref="Task{T}"/>.</returns>
[Pure]
Task<IDalamudTextureWrap> GetDalamudTextureWrapAsync(DalamudAsset asset);
}

View file

@ -21,7 +21,7 @@ namespace Dalamud.Storage;
/// <remarks>
/// This is not an early-loaded service, as it is needed before they are initialized.
/// </remarks>
[ServiceManager.Service]
[ServiceManager.ProvidedService]
public class ReliableFileStorage : IServiceType, IDisposable
{
private static readonly ModuleLog Log = new("VFS");

View file

@ -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;
/// <summary>
/// Utilities for disposing stuff.
/// </summary>
public static class DisposeSafety
{
/// <summary>
/// Interface that marks a disposable that it can call back on dispose.
/// </summary>
public interface IDisposeCallback : IDisposable
{
/// <summary>
/// Event to be fired before object dispose. First parameter is the object iself.
/// </summary>
event Action<IDisposeCallback>? BeforeDispose;
/// <summary>
/// Event to be fired after object dispose. First parameter is the object iself.
/// </summary>
event Action<IDisposeCallback, Exception?>? AfterDispose;
}
/// <summary>
/// Returns a proxy <see cref="IDisposable"/> that on dispose will dispose the result of the given
/// <see cref="Task{T}"/>.<br />
/// If any exception has occurred, it will be ignored.
/// </summary>
/// <param name="task">The task.</param>
/// <typeparam name="T">A disposable type.</typeparam>
/// <returns>The proxy <see cref="IDisposable"/>.</returns>
public static IDisposable ToDisposableIgnoreExceptions<T>(this Task<T> task)
where T : IDisposable
{
return Disposable.Create(() => task.ContinueWith(r =>
{
_ = r.Exception;
if (r.IsCompleted)
{
try
{
r.Dispose();
}
catch
{
// ignore
}
}
}));
}
/// <summary>
/// Transforms <paramref name="task"/> into a <see cref="Task"/>, disposing the content as necessary.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="ignoreAllExceptions">Ignore all exceptions.</param>
/// <typeparam name="T">A disposable type.</typeparam>
/// <returns>A wrapper for the task.</returns>
public static Task ToContentDisposedTask<T>(this Task<T> 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<Exception>)r.Exception?.InnerExceptions
?? new[] { new OperationCanceledException() })));
}
}
return Task.CompletedTask;
}).Unwrap();
/// <summary>
/// Returns a proxy <see cref="IDisposable"/> that on dispose will dispose all the elements of the given
/// <see cref="IEnumerable{T}"/> of <typeparamref name="T"/>s.
/// </summary>
/// <param name="disposables">The disposables.</param>
/// <typeparam name="T">The disposable types.</typeparam>
/// <returns>The proxy <see cref="IDisposable"/>.</returns>
/// <exception cref="AggregateException">Error.</exception>
public static IDisposable AggregateToDisposable<T>(this IEnumerable<T>? disposables)
where T : IDisposable
{
if (disposables is not T[] array)
array = disposables?.ToArray() ?? Array.Empty<T>();
return Disposable.Create(() =>
{
List<Exception?> 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);
});
}
/// <summary>
/// Utility class for managing finalizing stuff.
/// </summary>
public class ScopedFinalizer : IDisposeCallback, IAsyncDisposable
{
private readonly List<object> objects = new();
/// <inheritdoc/>
public event Action<IDisposeCallback>? BeforeDispose;
/// <inheritdoc/>
public event Action<IDisposeCallback, Exception?>? AfterDispose;
/// <inheritdoc cref="Stack{T}.EnsureCapacity"/>
public void EnsureCapacity(int capacity) => this.objects.EnsureCapacity(capacity);
/// <inheritdoc cref="Stack{T}.Push"/>
/// <returns>The parameter.</returns>
[return: NotNullIfNotNull(nameof(d))]
public T? Add<T>(T? d) where T : IDisposable
{
if (d is not null)
this.objects.Add(this.CheckAdd(d));
return d;
}
/// <inheritdoc cref="Stack{T}.Push"/>
[return: NotNullIfNotNull(nameof(d))]
public Action? Add(Action? d)
{
if (d is not null)
this.objects.Add(this.CheckAdd(d));
return d;
}
/// <inheritdoc cref="Stack{T}.Push"/>
[return: NotNullIfNotNull(nameof(d))]
public Func<Task>? Add(Func<Task>? d)
{
if (d is not null)
this.objects.Add(this.CheckAdd(d));
return d;
}
/// <inheritdoc cref="Stack{T}.Push"/>
public GCHandle Add(GCHandle d)
{
if (d != default)
this.objects.Add(this.CheckAdd(d));
return d;
}
/// <summary>
/// Queue all the given <see cref="IDisposable"/> to be disposed later.
/// </summary>
/// <param name="ds">Disposables.</param>
public void AddRange(IEnumerable<IDisposable?> ds) =>
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
/// <summary>
/// Queue all the given <see cref="IDisposable"/> to be run later.
/// </summary>
/// <param name="ds">Actions.</param>
public void AddRange(IEnumerable<Action?> ds) =>
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
/// <summary>
/// Queue all the given <see cref="Func{T}"/> returning <see cref="Task"/> to be run later.
/// </summary>
/// <param name="ds">Func{Task}s.</param>
public void AddRange(IEnumerable<Func<Task>?> ds) =>
this.objects.AddRange(ds.Where(d => d is not null).Select(d => (object)this.CheckAdd(d)));
/// <summary>
/// Queue all the given <see cref="GCHandle"/> to be disposed later.
/// </summary>
/// <param name="ds">GCHandles.</param>
public void AddRange(IEnumerable<GCHandle> ds) =>
this.objects.AddRange(ds.Select(d => (object)this.CheckAdd(d)));
/// <summary>
/// Cancel all pending disposals.
/// </summary>
/// <remarks>Use this after successful initialization of multiple disposables.</remarks>
public void Cancel()
{
foreach (var o in this.objects)
this.CheckRemove(o);
this.objects.Clear();
}
/// <inheritdoc cref="Stack{T}.EnsureCapacity"/>
/// <returns>This for method chaining.</returns>
public ScopedFinalizer WithEnsureCapacity(int capacity)
{
this.EnsureCapacity(capacity);
return this;
}
/// <inheritdoc cref="Add{T}"/>
/// <returns>This for method chaining.</returns>
public ScopedFinalizer With(IDisposable d)
{
this.Add(d);
return this;
}
/// <inheritdoc cref="Add(Action)"/>
/// <returns>This for method chaining.</returns>
public ScopedFinalizer With(Action d)
{
this.Add(d);
return this;
}
/// <inheritdoc cref="Add(Func{Task})"/>
/// <returns>This for method chaining.</returns>
public ScopedFinalizer With(Func<Task> d)
{
this.Add(d);
return this;
}
/// <inheritdoc cref="Add(GCHandle)"/>
/// <returns>This for method chaining.</returns>
public ScopedFinalizer With(GCHandle d)
{
this.Add(d);
return this;
}
/// <inheritdoc/>
public void Dispose()
{
this.BeforeDispose?.InvokeSafely(this);
List<Exception>? 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<Task> 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;
}
}
/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
this.BeforeDispose?.InvokeSafely(this);
List<Exception>? 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<Task> 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>(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);
}
}
}

View file

@ -1,4 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Dalamud.Utility;
@ -8,6 +8,26 @@ namespace Dalamud.Utility;
/// </summary>
public static class EnumExtensions
{
/// <summary>
/// Gets attributes on an enum.
/// </summary>
/// <typeparam name="TAttribute">The type of attribute to get.</typeparam>
/// <param name="value">The enum value that has an attached attribute.</param>
/// <returns>The enumerable of the attached attributes.</returns>
public static IEnumerable<TAttribute> GetAttributes<TAttribute>(this Enum value)
where TAttribute : Attribute
{
var type = value.GetType();
var name = Enum.GetName(type, value);
if (name.IsNullOrEmpty())
return Array.Empty<TAttribute>();
return type.GetField(name)?
.GetCustomAttributes(false)
.OfType<TAttribute>()
?? Array.Empty<TAttribute>();
}
/// <summary>
/// Gets an attribute on an enum.
/// </summary>
@ -15,18 +35,8 @@ public static class EnumExtensions
/// <param name="value">The enum value that has an attached attribute.</param>
/// <returns>The attached attribute, if any.</returns>
public static TAttribute? GetAttribute<TAttribute>(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<TAttribute>()
.SingleOrDefault();
}
where TAttribute : Attribute =>
value.GetAttributes<TAttribute>().SingleOrDefault();
/// <summary>
/// Gets an indicator if enum has been flagged as obsolete (deprecated).

@ -1 +1 @@
Subproject commit 090e0c244df668454616026188c1363e5d25a1bc
Subproject commit cc668752416a8459a3c23345c51277e359803de8