diff --git a/Dalamud/DalamudAsset.cs b/Dalamud/DalamudAsset.cs
new file mode 100644
index 000000000..5b641c487
--- /dev/null
+++ b/Dalamud/DalamudAsset.cs
@@ -0,0 +1,146 @@
+using Dalamud.Storage.Assets;
+
+namespace Dalamud;
+
+///
+/// Specifies an asset that has been shipped as Dalamud Asset.
+/// Any asset can cease to exist at any point, even if the enum value exists.
+/// Either ship your own assets, or be prepared for errors.
+///
+public enum DalamudAsset
+{
+ ///
+ /// Nothing.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Empty, data: new byte[0])]
+ Unspecified = 0,
+
+ ///
+ /// : The fallback empty texture.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromRaw, data: new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 })]
+ [DalamudAssetRawTexture(4, 8, 4, SharpDX.DXGI.Format.BC1_UNorm)]
+ Empty4X4 = 1000,
+
+ ///
+ /// : The Dalamud logo.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "logo.png")]
+ Logo = 1001,
+
+ ///
+ /// : The Dalamud logo, but smaller.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "tsmLogo.png")]
+ LogoSmall = 1002,
+
+ ///
+ /// : The default plugin icon.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "defaultIcon.png")]
+ DefaultIcon = 1003,
+
+ ///
+ /// : The disabled plugin icon.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "disabledIcon.png")]
+ DisabledIcon = 1004,
+
+ ///
+ /// : The outdated installable plugin icon.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "outdatedInstallableIcon.png")]
+ OutdatedInstallableIcon = 1005,
+
+ ///
+ /// : The plugin trouble icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "troubleIcon.png")]
+ TroubleIcon = 1006,
+
+ ///
+ /// : The plugin update icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "updateIcon.png")]
+ UpdateIcon = 1007,
+
+ ///
+ /// : The plugin installed icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "installedIcon.png")]
+ InstalledIcon = 1008,
+
+ ///
+ /// : The third party plugin icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "thirdIcon.png")]
+ ThirdIcon = 1009,
+
+ ///
+ /// : The installed third party plugin icon overlay.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "thirdInstalledIcon.png")]
+ ThirdInstalledIcon = 1010,
+
+ ///
+ /// : The API bump explainer icon.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "changelogApiBump.png")]
+ ChangelogApiBumpIcon = 1011,
+
+ ///
+ /// : The background shade for
+ /// .
+ ///
+ [DalamudAsset(DalamudAssetPurpose.TextureFromPng)]
+ [DalamudAssetPath("UIRes", "tsmShade.png")]
+ TitleScreenMenuShade = 1012,
+
+ ///
+ /// : Noto Sans CJK JP Medium.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font)]
+ [DalamudAssetPath("UIRes", "NotoSansCJKjp-Regular.otf")]
+ [DalamudAssetPath("UIRes", "NotoSansCJKjp-Medium.otf")]
+ NotoSansJpMedium = 2000,
+
+ ///
+ /// : Noto Sans CJK KR Regular.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font)]
+ [DalamudAssetPath("UIRes", "NotoSansCJKkr-Regular.otf")]
+ [DalamudAssetPath("UIRes", "NotoSansKR-Regular.otf")]
+ NotoSansKrRegular = 2001,
+
+ ///
+ /// : Inconsolata Regular.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font)]
+ [DalamudAssetPath("UIRes", "Inconsolata-Regular.ttf")]
+ InconsolataRegular = 2002,
+
+ ///
+ /// : FontAwesome Free Solid.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font)]
+ [DalamudAssetPath("UIRes", "FontAwesomeFreeSolid.otf")]
+ FontAwesomeFreeSolid = 2003,
+
+ ///
+ /// : Game symbol fonts being used as webfonts at Lodestone.
+ ///
+ [DalamudAsset(DalamudAssetPurpose.Font, required: true)]
+ [DalamudAssetOnlineSource("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")]
+ LodestoneGameSymbol = 2004,
+}
diff --git a/Dalamud/Interface/Internal/Branding.cs b/Dalamud/Interface/Internal/Branding.cs
deleted file mode 100644
index 4162cabeb..000000000
--- a/Dalamud/Interface/Internal/Branding.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.IO;
-
-using Dalamud.IoC.Internal;
-
-namespace Dalamud.Interface.Internal;
-
-///
-/// Class containing various textures used by Dalamud windows for branding purposes.
-///
-[ServiceManager.EarlyLoadedService]
-#pragma warning disable SA1015
-[InherentDependency] // Can't load textures before this
-#pragma warning restore SA1015
-internal class Branding : IServiceType, IDisposable
-{
- private readonly Dalamud dalamud;
- private readonly TextureManager tm;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// Dalamud instance.
- /// TextureManager instance.
- [ServiceManager.ServiceConstructor]
- public Branding(Dalamud dalamud, TextureManager tm)
- {
- this.dalamud = dalamud;
- this.tm = tm;
-
- this.LoadTextures();
- }
-
- ///
- /// Gets a full-size Dalamud logo texture.
- ///
- public IDalamudTextureWrap Logo { get; private set; } = null!;
-
- ///
- /// Gets a small Dalamud logo texture.
- ///
- public IDalamudTextureWrap LogoSmall { get; private set; } = null!;
-
- ///
- public void Dispose()
- {
- this.Logo.Dispose();
- this.LogoSmall.Dispose();
- }
-
- private void LoadTextures()
- {
- this.Logo = this.tm.GetTextureFromFile(new FileInfo(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "logo.png")))
- ?? throw new Exception("Could not load logo.");
-
- this.LogoSmall = this.tm.GetTextureFromFile(new FileInfo(Path.Combine(this.dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png")))
- ?? throw new Exception("Could not load TSM logo.");
- }
-}
diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs
index 18ab538c4..1a6e71194 100644
--- a/Dalamud/Interface/Internal/DalamudInterface.cs
+++ b/Dalamud/Interface/Internal/DalamudInterface.cs
@@ -26,6 +26,7 @@ using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal;
+using Dalamud.Storage.Assets;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
@@ -93,7 +94,7 @@ internal class DalamudInterface : IDisposable, IServiceType
DalamudConfiguration configuration,
InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene,
PluginImageCache pluginImageCache,
- Branding branding,
+ DalamudAssetManager dalamudAssetManager,
Game.Framework framework,
ClientState clientState,
TitleScreenMenu titleScreenMenu,
@@ -118,11 +119,10 @@ internal class DalamudInterface : IDisposable, IServiceType
this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false };
this.titleScreenMenuWindow = new TitleScreenMenuWindow(
clientState,
- dalamud,
configuration,
+ dalamudAssetManager,
framework,
gameGui,
- this.interfaceManager,
titleScreenMenu) { IsOpen = false };
this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false };
this.profilerWindow = new ProfilerWindow() { IsOpen = false };
@@ -152,12 +152,21 @@ internal class DalamudInterface : IDisposable, IServiceType
this.interfaceManager.Draw += this.OnDraw;
var tsm = Service.Get();
- tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, () => this.OpenPluginInstaller());
- tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), branding.LogoSmall, this.OpenSettings);
+ tsm.AddEntryCore(
+ Loc.Localize("TSMDalamudPlugins", "Plugin Installer"),
+ dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall),
+ this.OpenPluginInstaller);
+ tsm.AddEntryCore(
+ Loc.Localize("TSMDalamudSettings", "Dalamud Settings"),
+ dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall),
+ this.OpenSettings);
if (!configuration.DalamudBetaKind.IsNullOrEmpty())
{
- tsm.AddEntryCore(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), branding.LogoSmall, () => this.isImGuiDrawDevMenu = true);
+ tsm.AddEntryCore(
+ Loc.Localize("TSMDalamudDevMenu", "Developer Menu"),
+ dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall),
+ () => this.isImGuiDrawDevMenu = true);
}
this.creditsDarkeningAnimation.Point1 = Vector2.Zero;
diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs
index c666a96a9..52e849c0e 100644
--- a/Dalamud/Interface/Internal/InterfaceManager.cs
+++ b/Dalamud/Interface/Internal/InterfaceManager.cs
@@ -21,6 +21,7 @@ using Dalamud.Interface.Internal.Notifications;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
+using Dalamud.Storage.Assets;
using Dalamud.Utility;
using Dalamud.Utility.Timing;
using ImGuiNET;
@@ -1063,10 +1064,15 @@ internal class InterfaceManager : IDisposable, IServiceType
}
[ServiceManager.CallWhenServicesReady]
- private void ContinueConstruction(TargetSigScanner sigScanner, Framework framework)
+ private void ContinueConstruction(
+ TargetSigScanner sigScanner,
+ DalamudAssetManager dalamudAssetManager,
+ DalamudConfiguration configuration)
{
+ dalamudAssetManager.WaitForAllRequiredAssets().Wait();
+
this.address.Setup(sigScanner);
- framework.RunOnFrameworkThread(() =>
+ this.framework.RunOnFrameworkThread(() =>
{
while ((this.GameWindowHandle = NativeFunctions.FindWindowEx(IntPtr.Zero, this.GameWindowHandle, "FFXIVGAME", IntPtr.Zero)) != IntPtr.Zero)
{
@@ -1078,7 +1084,7 @@ internal class InterfaceManager : IDisposable, IServiceType
try
{
- if (Service.Get().WindowIsImmersive)
+ if (configuration.WindowIsImmersive)
this.SetImmersiveMode(true);
}
catch (Exception ex)
diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
index e3f318223..b9e7ab686 100644
--- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs
@@ -11,6 +11,7 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Internal;
+using Dalamud.Storage.Assets;
using Dalamud.Utility;
using ImGuiNET;
@@ -32,7 +33,6 @@ internal sealed class ChangelogWindow : Window, IDisposable
";
private readonly TitleScreenMenuWindow tsmWindow;
- private readonly IDalamudTextureWrap logoTexture;
private readonly InOutCubic windowFade = new(TimeSpan.FromSeconds(2.5f))
{
@@ -47,6 +47,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
};
private IDalamudTextureWrap? apiBumpExplainerTexture;
+ private IDalamudTextureWrap? logoTexture;
private GameFontHandle? bannerFont;
private State state = State.WindowFadeIn;
@@ -63,8 +64,6 @@ internal sealed class ChangelogWindow : Window, IDisposable
this.tsmWindow = tsmWindow;
this.Namespace = "DalamudChangelogWindow";
- this.logoTexture = Service.Get().Logo;
-
// If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch
if (WarrantsChangelog())
Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result));
@@ -188,6 +187,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
using (ImRaii.PushStyle(ImGuiStyleVar.Alpha, Math.Clamp(this.windowFade.EasedPoint.X - 0.5f, 0f, 1f)))
{
+ this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo);
ImGui.Image(this.logoTexture.ImGuiHandle, logoSize);
}
}
@@ -376,7 +376,6 @@ internal sealed class ChangelogWindow : Window, IDisposable
///
public void Dispose()
{
- this.logoTexture.Dispose();
}
private void MakeFont(GameFontManager gfm) =>
diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
index b721b08c3..528507229 100644
--- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
+++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs
@@ -1,4 +1,3 @@
-using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
@@ -12,8 +11,8 @@ using Dalamud.Networking.Http;
using Dalamud.Plugin.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Internal.Types.Manifest;
+using Dalamud.Storage.Assets;
using Dalamud.Utility;
-using ImGuiScene;
using Serilog;
namespace Dalamud.Interface.Internal.Windows;
@@ -47,12 +46,6 @@ internal class PluginImageCache : IDisposable, IServiceType
private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api6/{0}/{1}/images/{2}";
private const string MainRepoDip17ImageUrl = "https://raw.githubusercontent.com/goatcorp/PluginDistD17/main/{0}/{1}/images/{2}";
- [ServiceManager.ServiceDependency]
- private readonly InterfaceManager.InterfaceManagerWithScene imWithScene = Service.Get();
-
- [ServiceManager.ServiceDependency]
- private readonly Branding branding = Service.Get();
-
[ServiceManager.ServiceDependency]
private readonly HappyHttpClient happyHttpClient = Service.Get();
@@ -64,35 +57,12 @@ internal class PluginImageCache : IDisposable, IServiceType
private readonly ConcurrentDictionary pluginIconMap = new();
private readonly ConcurrentDictionary pluginImagesMap = new();
-
- private readonly Task emptyTextureTask;
- private readonly Task disabledIconTask;
- private readonly Task outdatedInstallableIconTask;
- private readonly Task defaultIconTask;
- private readonly Task troubleIconTask;
- private readonly Task updateIconTask;
- private readonly Task installedIconTask;
- private readonly Task thirdIconTask;
- private readonly Task thirdInstalledIconTask;
- private readonly Task corePluginIconTask;
+ private readonly DalamudAssetManager dalamudAssetManager;
[ServiceManager.ServiceConstructor]
- private PluginImageCache(Dalamud dalamud)
+ private PluginImageCache(Dalamud dalamud, DalamudAssetManager dalamudAssetManager)
{
- Task? TaskWrapIfNonNull(IDalamudTextureWrap? tw) => tw == null ? null : Task.FromResult(tw!);
- var imwst = Task.Run(() => this.imWithScene);
-
- this.emptyTextureTask = imwst.ContinueWith(task => task.Result.Manager.LoadImageRaw(new byte[64], 8, 8, 4)!);
- this.defaultIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))) ?? this.emptyTextureTask).Unwrap();
- this.disabledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "disabledIcon.png"))) ?? this.emptyTextureTask).Unwrap();
- this.outdatedInstallableIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "outdatedInstallableIcon.png"))) ?? this.emptyTextureTask).Unwrap();
- this.troubleIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))) ?? this.emptyTextureTask).Unwrap();
- this.updateIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))) ?? this.emptyTextureTask).Unwrap();
- this.installedIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "installedIcon.png"))) ?? this.emptyTextureTask).Unwrap();
- this.thirdIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdIcon.png"))) ?? this.emptyTextureTask).Unwrap();
- this.thirdInstalledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdInstalledIcon.png"))) ?? this.emptyTextureTask).Unwrap();
- this.corePluginIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(this.branding.LogoSmall)).Unwrap();
-
+ this.dalamudAssetManager = dalamudAssetManager;
this.downloadTask = Task.Factory.StartNew(
() => this.DownloadTask(8), TaskCreationOptions.LongRunning);
this.loadTask = Task.Factory.StartNew(
@@ -102,72 +72,62 @@ internal class PluginImageCache : IDisposable, IServiceType
///
/// Gets the fallback empty texture.
///
- public IDalamudTextureWrap EmptyTexture => this.emptyTextureTask.IsCompleted
- ? this.emptyTextureTask.Result
- : this.emptyTextureTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap EmptyTexture =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.Empty4X4);
///
/// Gets the disabled plugin icon.
///
- public IDalamudTextureWrap DisabledIcon => this.disabledIconTask.IsCompleted
- ? this.disabledIconTask.Result
- : this.disabledIconTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap DisabledIcon =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DisabledIcon, this.EmptyTexture);
///
/// Gets the outdated installable plugin icon.
///
- public IDalamudTextureWrap OutdatedInstallableIcon => this.outdatedInstallableIconTask.IsCompleted
- ? this.outdatedInstallableIconTask.Result
- : this.outdatedInstallableIconTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap OutdatedInstallableIcon =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.OutdatedInstallableIcon, this.EmptyTexture);
///
/// Gets the default plugin icon.
///
- public IDalamudTextureWrap DefaultIcon => this.defaultIconTask.IsCompleted
- ? this.defaultIconTask.Result
- : this.defaultIconTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap DefaultIcon =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.DefaultIcon, this.EmptyTexture);
///
/// Gets the plugin trouble icon overlay.
///
- public IDalamudTextureWrap TroubleIcon => this.troubleIconTask.IsCompleted
- ? this.troubleIconTask.Result
- : this.troubleIconTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap TroubleIcon =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TroubleIcon, this.EmptyTexture);
///
/// Gets the plugin update icon overlay.
///
- public IDalamudTextureWrap UpdateIcon => this.updateIconTask.IsCompleted
- ? this.updateIconTask.Result
- : this.updateIconTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap UpdateIcon =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.UpdateIcon, this.EmptyTexture);
///
/// Gets the plugin installed icon overlay.
///
- public IDalamudTextureWrap InstalledIcon => this.installedIconTask.IsCompleted
- ? this.installedIconTask.Result
- : this.installedIconTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap InstalledIcon =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.InstalledIcon, this.EmptyTexture);
///
/// Gets the third party plugin icon overlay.
///
- public IDalamudTextureWrap ThirdIcon => this.thirdIconTask.IsCompleted
- ? this.thirdIconTask.Result
- : this.thirdIconTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap ThirdIcon =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.ThirdIcon, this.EmptyTexture);
///
/// Gets the installed third party plugin icon overlay.
///
- public IDalamudTextureWrap ThirdInstalledIcon => this.thirdInstalledIconTask.IsCompleted
- ? this.thirdInstalledIconTask.Result
- : this.thirdInstalledIconTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap ThirdInstalledIcon =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.ThirdInstalledIcon, this.EmptyTexture);
///
/// Gets the core plugin icon.
///
- public IDalamudTextureWrap CorePluginIcon => this.corePluginIconTask.IsCompleted
- ? this.corePluginIconTask.Result
- : this.corePluginIconTask.GetAwaiter().GetResult();
+ public IDalamudTextureWrap CorePluginIcon =>
+ this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall, this.EmptyTexture);
///
public void Dispose()
@@ -185,22 +145,6 @@ internal class PluginImageCache : IDisposable, IServiceType
this.downloadQueue.Dispose();
this.loadQueue.Dispose();
- foreach (var task in new[]
- {
- this.defaultIconTask,
- this.troubleIconTask,
- this.updateIconTask,
- this.installedIconTask,
- this.thirdIconTask,
- this.thirdInstalledIconTask,
- this.corePluginIconTask,
- })
- {
- task.Wait();
- if (task.IsCompletedSuccessfully)
- task.Result.Dispose();
- }
-
foreach (var icon in this.pluginIconMap.Values)
{
icon?.Dispose();
@@ -319,7 +263,7 @@ internal class PluginImageCache : IDisposable, IServiceType
if (bytes == null)
return null;
- var interfaceManager = this.imWithScene.Manager;
+ var interfaceManager = (await Service.GetAsync()).Manager;
var framework = await Service.GetAsync();
IDalamudTextureWrap? image;
diff --git a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs
index 9b6a32617..5b6f6b02f 100644
--- a/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs
+++ b/Dalamud/Interface/Internal/Windows/Settings/Tabs/SettingsTabAbout.cs
@@ -11,6 +11,7 @@ using Dalamud.Interface.GameFonts;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Internal;
+using Dalamud.Storage.Assets;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using ImGuiNET;
@@ -171,19 +172,16 @@ Dalamud is licensed under AGPL v3 or later.
Contribute at: https://github.com/goatcorp/Dalamud
";
- private readonly IDalamudTextureWrap logoTexture;
private readonly Stopwatch creditsThrottler;
private string creditsText;
private bool resetNow = false;
+ private IDalamudTextureWrap? logoTexture;
private GameFontHandle? thankYouFont;
public SettingsTabAbout()
{
- var branding = Service.Get();
-
- this.logoTexture = branding.Logo;
this.creditsThrottler = new();
}
@@ -251,6 +249,7 @@ Contribute at: https://github.com/goatcorp/Dalamud
const float imageSize = 190f;
ImGui.SameLine((ImGui.GetWindowWidth() / 2) - (imageSize / 2));
+ this.logoTexture ??= Service.Get().GetDalamudTextureWrap(DalamudAsset.Logo);
ImGui.Image(this.logoTexture.ImGuiHandle, ImGuiHelpers.ScaledVector2(imageSize));
ImGuiHelpers.ScaledDummy(0, 20f);
diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
index 4034695e5..f60ebe4ef 100644
--- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
+++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using System.IO;
using System.Linq;
using System.Numerics;
@@ -12,6 +11,7 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services;
+using Dalamud.Storage.Assets;
using ImGuiNET;
@@ -46,19 +46,17 @@ internal class TitleScreenMenuWindow : Window, IDisposable
/// Initializes a new instance of the class.
///
/// An instance of .
- /// An instance of .
/// An instance of .
+ /// An instance of .
/// An instance of .
- /// An instance of .
/// An instance of .
/// An instance of .
public TitleScreenMenuWindow(
ClientState clientState,
- Dalamud dalamud,
DalamudConfiguration configuration,
+ DalamudAssetManager dalamudAssetManager,
Framework framework,
GameGui gameGui,
- InterfaceManager interfaceManager,
TitleScreenMenu titleScreenMenu)
: base(
"TitleScreenMenuOverlay",
@@ -79,9 +77,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
this.PositionCondition = ImGuiCond.Always;
this.RespectCloseHotkey = false;
- var shadeTex =
- interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmShade.png"));
- this.shadeTexture = shadeTex ?? throw new Exception("Could not load TSM background texture.");
+ this.shadeTexture = dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.TitleScreenMenuShade);
framework.Update += this.FrameworkOnUpdate;
}
@@ -116,7 +112,6 @@ internal class TitleScreenMenuWindow : Window, IDisposable
///
public void Dispose()
{
- this.shadeTexture.Dispose();
this.framework.Update -= this.FrameworkOnUpdate;
}
@@ -386,7 +381,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
return isHover;
}
- private void FrameworkOnUpdate(IFramework framework)
+ private void FrameworkOnUpdate(IFramework unused)
{
this.IsOpen = !this.clientState.IsLoggedIn;
diff --git a/Dalamud/Storage/Assets/DalamudAssetAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetAttribute.cs
new file mode 100644
index 000000000..a3527cdbc
--- /dev/null
+++ b/Dalamud/Storage/Assets/DalamudAssetAttribute.cs
@@ -0,0 +1,36 @@
+namespace Dalamud.Storage.Assets;
+
+///
+/// Stores the basic information of a Dalamud asset.
+///
+[AttributeUsage(AttributeTargets.Field)]
+internal class DalamudAssetAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The purpose.
+ /// The data.
+ /// Whether the asset is required.
+ public DalamudAssetAttribute(DalamudAssetPurpose purpose, byte[]? data = null, bool required = true)
+ {
+ this.Purpose = purpose;
+ this.Data = data;
+ this.Required = required;
+ }
+
+ ///
+ /// Gets the purpose of the asset.
+ ///
+ public DalamudAssetPurpose Purpose { get; }
+
+ ///
+ /// Gets the data, if available.
+ ///
+ public byte[]? Data { get; }
+
+ ///
+ /// Gets a value indicating whether the asset is required.
+ ///
+ public bool Required { get; }
+}
diff --git a/Dalamud/Storage/Assets/DalamudAssetExtensions.cs b/Dalamud/Storage/Assets/DalamudAssetExtensions.cs
new file mode 100644
index 000000000..9181f1a5d
--- /dev/null
+++ b/Dalamud/Storage/Assets/DalamudAssetExtensions.cs
@@ -0,0 +1,17 @@
+using Dalamud.Utility;
+
+namespace Dalamud.Storage.Assets;
+
+///
+/// Extension methods for .
+///
+public static class DalamudAssetExtensions
+{
+ ///
+ /// Gets the purpose.
+ ///
+ /// The asset.
+ /// The purpose.
+ public static DalamudAssetPurpose GetPurpose(this DalamudAsset asset) =>
+ asset.GetAttribute()?.Purpose ?? DalamudAssetPurpose.Empty;
+}
diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs
new file mode 100644
index 000000000..bbfd60636
--- /dev/null
+++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs
@@ -0,0 +1,365 @@
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using Dalamud.Interface.Internal;
+using Dalamud.IoC;
+using Dalamud.IoC.Internal;
+using Dalamud.Networking.Http;
+using Dalamud.Utility;
+using Dalamud.Utility.Timing;
+
+using JetBrains.Annotations;
+
+using Serilog;
+
+namespace Dalamud.Storage.Assets;
+
+///
+/// A concrete class for .
+///
+[PluginInterface]
+[ServiceManager.BlockingEarlyLoadedService]
+#pragma warning disable SA1015
+[ResolveVia]
+#pragma warning restore SA1015
+internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudAssetManager
+{
+ private const int DownloadAttemptCount = 10;
+ private const int RenameAttemptCount = 10;
+
+ private readonly object syncRoot = new();
+ private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new();
+ private readonly Dictionary?> fileStreams;
+ private readonly Dictionary?> textureWraps;
+ private readonly Dalamud dalamud;
+ private readonly HappyHttpClient httpClient;
+ private readonly string localSourceDirectory;
+ private readonly CancellationTokenSource cancellationTokenSource;
+
+ private bool isDisposed;
+
+ [ServiceManager.ServiceConstructor]
+ private DalamudAssetManager(Dalamud dalamud, HappyHttpClient httpClient)
+ {
+ this.dalamud = dalamud;
+ this.httpClient = httpClient;
+ this.localSourceDirectory = Path.Combine(this.dalamud.AssetDirectory.FullName, "..", "local");
+ Directory.CreateDirectory(this.localSourceDirectory);
+ this.scopedFinalizer.Add(this.cancellationTokenSource = new());
+
+ this.fileStreams = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null);
+ this.textureWraps = Enum.GetValues().ToDictionary(x => x, _ => (Task?)null);
+
+ var loadTimings = Timings.Start("DAM LoadAll");
+ this.WaitForAllRequiredAssets().ContinueWith(_ => loadTimings.Dispose());
+ }
+
+ ///
+ public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4);
+
+ ///
+ public void Dispose()
+ {
+ lock (this.syncRoot)
+ {
+ if (this.isDisposed)
+ return;
+
+ this.isDisposed = true;
+ }
+
+ this.cancellationTokenSource.Cancel();
+ Task.WaitAll(
+ Array.Empty()
+ .Concat(this.fileStreams.Values)
+ .Concat(this.textureWraps.Values)
+ .Where(x => x is not null)
+ .ToArray());
+ this.scopedFinalizer.Dispose();
+ }
+
+ ///
+ /// Waits for all the required assets to be ready. Will result in a faulted task, if any of the required assets
+ /// has failed to load.
+ ///
+ /// The task.
+ [Pure]
+ public Task WaitForAllRequiredAssets()
+ {
+ lock (this.syncRoot)
+ {
+ return Task.WhenAll(
+ Enum.GetValues()
+ .Where(x => x is not DalamudAsset.Empty4X4)
+ .Select(this.CreateStreamAsync)
+ .Select(x => x.ToContentDisposedTask()));
+ }
+ }
+
+ ///
+ [Pure]
+ public bool IsStreamImmediatelyAvailable(DalamudAsset asset) =>
+ asset.GetAttribute()?.Data is not null
+ || this.fileStreams[asset]?.IsCompletedSuccessfully is true;
+
+ ///
+ [Pure]
+ public Stream CreateStream(DalamudAsset asset)
+ {
+ var s = this.CreateStreamAsync(asset);
+ s.Wait();
+ if (s.IsCompletedSuccessfully)
+ return s.Result;
+ if (s.Exception is not null)
+ throw new AggregateException(s.Exception.InnerExceptions);
+ throw new OperationCanceledException();
+ }
+
+ ///
+ [Pure]
+ public Task CreateStreamAsync(DalamudAsset asset)
+ {
+ if (asset.GetAttribute() is { Data: { } rawData })
+ return Task.FromResult(new MemoryStream(rawData, false));
+
+ Task task;
+ lock (this.syncRoot)
+ {
+ if (this.isDisposed)
+ throw new ObjectDisposedException(nameof(DalamudAssetManager));
+
+ task = this.fileStreams[asset] ??= CreateInnerAsync();
+ }
+
+ return this.TransformImmediate(
+ task,
+ x => (Stream)new FileStream(
+ x.Name,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read,
+ 4096,
+ FileOptions.Asynchronous | FileOptions.SequentialScan));
+
+ async Task CreateInnerAsync()
+ {
+ string path;
+ List exceptions = null;
+ foreach (var name in asset.GetAttributes().Select(x => x.FileName))
+ {
+ if (!File.Exists(path = Path.Combine(this.dalamud.AssetDirectory.FullName, name)))
+ continue;
+
+ try
+ {
+ return File.OpenRead(path);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ exceptions ??= new();
+ exceptions.Add(e);
+ }
+ }
+
+ if (File.Exists(path = Path.Combine(this.localSourceDirectory, asset.ToString())))
+ {
+ try
+ {
+ return File.OpenRead(path);
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ exceptions ??= new();
+ exceptions.Add(e);
+ }
+ }
+
+ var tempPath = $"{path}.{Environment.ProcessId:x}.{Environment.CurrentManagedThreadId:x}";
+ try
+ {
+ for (var i = 0; i < DownloadAttemptCount; i++)
+ {
+ var attemptedAny = false;
+ foreach (var url in asset.GetAttributes())
+ {
+ Log.Information("[{who}] {asset}: Trying {url}", nameof(DalamudAssetManager), asset, url);
+ attemptedAny = true;
+
+ try
+ {
+ await using var tempPathStream = File.Open(tempPath, FileMode.Create, FileAccess.Write);
+ await url.DownloadAsync(
+ this.httpClient.SharedHttpClient,
+ tempPathStream,
+ this.cancellationTokenSource.Token);
+ tempPathStream.Dispose();
+ for (var j = RenameAttemptCount; ; j--)
+ {
+ try
+ {
+ File.Move(tempPath, path);
+ }
+ catch (IOException ioe)
+ {
+ if (j == 0)
+ throw;
+ Log.Warning(
+ ioe,
+ "[{who}] {asset}: Renaming failed; trying again {n} more times",
+ nameof(DalamudAssetManager),
+ asset,
+ j);
+ await Task.Delay(1000, this.cancellationTokenSource.Token);
+ continue;
+ }
+
+ return File.OpenRead(path);
+ }
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ Log.Error(e, "[{who}] {asset}: Failed {url}", nameof(DalamudAssetManager), asset, url);
+ }
+ }
+
+ if (!attemptedAny)
+ throw new FileNotFoundException($"Failed to find the asset {asset}.", asset.ToString());
+
+ // Wait up to 5 minutes
+ var delay = Math.Min(300, (1 << i) * 1000);
+ Log.Error(
+ "[{who}] {asset}: Failed to download. Trying again in {sec} seconds...",
+ nameof(DalamudAssetManager),
+ asset,
+ delay);
+ await Task.Delay(delay * 1000, this.cancellationTokenSource.Token);
+ }
+
+ throw new FileNotFoundException($"Failed to load the asset {asset}.", asset.ToString());
+ }
+ catch (Exception e) when (e is not OperationCanceledException)
+ {
+ exceptions ??= new();
+ exceptions.Add(e);
+ try
+ {
+ File.Delete(tempPath);
+ }
+ catch
+ {
+ // don't care
+ }
+ }
+
+ throw new AggregateException(exceptions);
+ }
+ }
+
+ ///
+ [Pure]
+ public IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset) =>
+ ExtractResult(this.GetDalamudTextureWrapAsync(asset));
+
+ ///
+ [Pure]
+ [return: NotNullIfNotNull(nameof(defaultWrap))]
+ public IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap)
+ {
+ var task = this.GetDalamudTextureWrapAsync(asset);
+ return task.IsCompletedSuccessfully ? task.Result : defaultWrap;
+ }
+
+ ///
+ [Pure]
+ public Task GetDalamudTextureWrapAsync(DalamudAsset asset)
+ {
+ var purpose = asset.GetPurpose();
+ if (purpose is not DalamudAssetPurpose.TextureFromPng and not DalamudAssetPurpose.TextureFromRaw)
+ throw new ArgumentOutOfRangeException(nameof(asset), asset, "The asset cannot be taken as a Texture2D.");
+
+ Task task;
+ lock (this.syncRoot)
+ {
+ if (this.isDisposed)
+ throw new ObjectDisposedException(nameof(DalamudAssetManager));
+
+ task = this.textureWraps[asset] ??= CreateInnerAsync();
+ }
+
+ return task;
+
+ async Task CreateInnerAsync()
+ {
+ var buf = Array.Empty();
+ try
+ {
+ var im = (await Service.GetAsync()).Manager;
+ await using var stream = await this.CreateStreamAsync(asset);
+ var length = checked((int)stream.Length);
+ buf = ArrayPool.Shared.Rent(length);
+ stream.ReadExactly(buf, 0, length);
+ var image = purpose switch
+ {
+ DalamudAssetPurpose.TextureFromPng => im.LoadImage(buf),
+ DalamudAssetPurpose.TextureFromRaw =>
+ asset.GetAttribute() is { } raw
+ ? im.LoadImageFromDxgiFormat(buf, raw.Pitch, raw.Width, raw.Height, raw.Format)
+ : throw new InvalidOperationException(
+ "TextureFromRaw must accompany a DalamudAssetRawTextureAttribute."),
+ _ => null,
+ };
+ var disposeDeferred =
+ this.scopedFinalizer.Add(image)
+ ?? throw new InvalidOperationException("Something went wrong very badly");
+ return new DisposeSuppressingDalamudTextureWrap(disposeDeferred);
+ }
+ catch (Exception e)
+ {
+ Log.Error(e, "[{name}] Failed to load {asset}.", nameof(DalamudAssetManager), asset);
+ throw;
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(buf);
+ }
+ }
+ }
+
+ private static T ExtractResult(Task t) => t.IsCompleted ? t.Result : t.GetAwaiter().GetResult();
+
+ private Task TransformImmediate(Task task, Func transformer)
+ {
+ if (task.IsCompletedSuccessfully)
+ return Task.FromResult(transformer(task.Result));
+ if (task.Exception is { } exc)
+ return Task.FromException(exc);
+ return task.ContinueWith(_ => this.TransformImmediate(task, transformer)).Unwrap();
+ }
+
+ private class DisposeSuppressingDalamudTextureWrap : IDalamudTextureWrap
+ {
+ private readonly IDalamudTextureWrap innerWrap;
+
+ public DisposeSuppressingDalamudTextureWrap(IDalamudTextureWrap wrap) => this.innerWrap = wrap;
+
+ ///
+ public IntPtr ImGuiHandle => this.innerWrap.ImGuiHandle;
+
+ ///
+ public int Width => this.innerWrap.Width;
+
+ ///
+ public int Height => this.innerWrap.Height;
+
+ ///
+ public void Dispose()
+ {
+ // suppressed
+ }
+ }
+}
diff --git a/Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs
new file mode 100644
index 000000000..25ed995d7
--- /dev/null
+++ b/Dalamud/Storage/Assets/DalamudAssetOnlineSourceAttribute.cs
@@ -0,0 +1,48 @@
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Dalamud.Storage.Assets;
+
+///
+/// Marks that an asset can be download from online.
+///
+[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
+internal class DalamudAssetOnlineSourceAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The URL.
+ public DalamudAssetOnlineSourceAttribute(string url)
+ {
+ this.Url = url;
+ }
+
+ ///
+ /// Gets the source URL of the file.
+ ///
+ public string Url { get; }
+
+ ///
+ /// Downloads to the given stream.
+ ///
+ /// The client.
+ /// The stream.
+ /// The cancellation token.
+ /// The task.
+ public async Task DownloadAsync(HttpClient client, Stream stream, CancellationToken cancellationToken)
+ {
+ using var resp = await client.GetAsync(this.Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
+ resp.EnsureSuccessStatusCode();
+ if (resp.StatusCode != HttpStatusCode.OK)
+ throw new NotSupportedException($"Only 200 OK is supported; got {resp.StatusCode}");
+
+ await using var readStream = await resp.Content.ReadAsStreamAsync(cancellationToken);
+ await readStream.CopyToAsync(stream, cancellationToken);
+ if (resp.Content.Headers.ContentLength is { } length && stream.Length != length)
+ throw new IOException($"Expected {length} bytes; got {stream.Length} bytes.");
+ }
+}
diff --git a/Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs
new file mode 100644
index 000000000..1df52aa39
--- /dev/null
+++ b/Dalamud/Storage/Assets/DalamudAssetPathAttribute.cs
@@ -0,0 +1,21 @@
+using System.IO;
+
+namespace Dalamud.Storage.Assets;
+
+///
+/// File names to look up in Dalamud assets.
+///
+[AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
+internal class DalamudAssetPathAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The path components.
+ public DalamudAssetPathAttribute(params string[] pathComponents) => this.FileName = Path.Join(pathComponents);
+
+ ///
+ /// Gets the file name.
+ ///
+ public string FileName { get; }
+}
diff --git a/Dalamud/Storage/Assets/DalamudAssetPurpose.cs b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs
new file mode 100644
index 000000000..b059cb3d6
--- /dev/null
+++ b/Dalamud/Storage/Assets/DalamudAssetPurpose.cs
@@ -0,0 +1,27 @@
+namespace Dalamud.Storage.Assets;
+
+///
+/// Purposes of a Dalamud asset.
+///
+public enum DalamudAssetPurpose
+{
+ ///
+ /// The asset has no purpose.
+ ///
+ Empty = 0,
+
+ ///
+ /// The asset is a .png file, and can be purposed as a .
+ ///
+ TextureFromPng = 10,
+
+ ///
+ /// The asset is a raw texture, and can be purposed as a .
+ ///
+ TextureFromRaw = 1001,
+
+ ///
+ /// The asset is a font file.
+ ///
+ Font = 2000,
+}
diff --git a/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs
new file mode 100644
index 000000000..b79abb7d7
--- /dev/null
+++ b/Dalamud/Storage/Assets/DalamudAssetRawTextureAttribute.cs
@@ -0,0 +1,45 @@
+using SharpDX.DXGI;
+
+namespace Dalamud.Storage.Assets;
+
+///
+/// Provide raw texture data directly.
+///
+[AttributeUsage(AttributeTargets.Field)]
+internal class DalamudAssetRawTextureAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The width.
+ /// The pitch.
+ /// The height.
+ /// The format.
+ public DalamudAssetRawTextureAttribute(int width, int pitch, int height, Format format)
+ {
+ this.Width = width;
+ this.Pitch = pitch;
+ this.Height = height;
+ this.Format = format;
+ }
+
+ ///
+ /// Gets the width.
+ ///
+ public int Width { get; }
+
+ ///
+ /// Gets the pitch.
+ ///
+ public int Pitch { get; }
+
+ ///
+ /// Gets the height.
+ ///
+ public int Height { get; }
+
+ ///
+ /// Gets the format.
+ ///
+ public Format Format { get; }
+}
diff --git a/Dalamud/Storage/Assets/IDalamudAssetManager.cs b/Dalamud/Storage/Assets/IDalamudAssetManager.cs
new file mode 100644
index 000000000..4fb83df80
--- /dev/null
+++ b/Dalamud/Storage/Assets/IDalamudAssetManager.cs
@@ -0,0 +1,79 @@
+using System.Diagnostics.Contracts;
+using System.IO;
+using System.Threading.Tasks;
+
+using Dalamud.Interface.Internal;
+
+namespace Dalamud.Storage.Assets;
+
+///
+/// Holds Dalamud Assets' handles hostage, so that they do not get closed while Dalamud is running.
+/// Also, attempts to load optional assets.
+///
+/// Note on
+/// It will help you get notified if you discard the result of functions, mostly likely because of a mistake.
+/// Think of C++ [[nodiscard]]. Also, like the intended meaning of the attribute, such methods will not have
+/// externally visible state changes.
+///
+internal interface IDalamudAssetManager
+{
+ ///
+ /// Gets the shared texture wrap for .
+ ///
+ IDalamudTextureWrap Empty4X4 { get; }
+
+ ///
+ /// Gets whether the stream for the asset is instantly available.
+ ///
+ /// The asset.
+ /// Whether the stream of an asset is immediately available.
+ [Pure]
+ bool IsStreamImmediatelyAvailable(DalamudAsset asset);
+
+ ///
+ /// Creates a stream backed by the specified asset, waiting as necessary.
+ /// Call after use.
+ ///
+ /// The asset.
+ /// The stream.
+ [Pure]
+ Stream CreateStream(DalamudAsset asset);
+
+ ///
+ /// Creates a stream backed by the specified asset.
+ /// Call after use.
+ ///
+ /// The asset.
+ /// The stream, wrapped inside a .
+ [Pure]
+ Task CreateStreamAsync(DalamudAsset asset);
+
+ ///
+ /// Gets a shared instance of , after waiting as necessary.
+ /// Calls to is unnecessary; they will be ignored.
+ ///
+ /// The texture asset.
+ /// The texture wrap.
+ [Pure]
+ IDalamudTextureWrap GetDalamudTextureWrap(DalamudAsset asset);
+
+ ///
+ /// Gets a shared instance of if it is available instantly;
+ /// if it is not ready, returns .
+ /// Calls to is unnecessary; they will be ignored.
+ ///
+ /// The texture asset.
+ /// The default return value, if the asset is not ready for whatever reason.
+ /// The texture wrap.
+ [Pure]
+ IDalamudTextureWrap? GetDalamudTextureWrap(DalamudAsset asset, IDalamudTextureWrap? defaultWrap);
+
+ ///
+ /// Gets a shared instance of in a .
+ /// Calls to is unnecessary; they will be ignored.
+ ///
+ /// The texture asset.
+ /// The new texture wrap, wrapped inside a .
+ [Pure]
+ Task GetDalamudTextureWrapAsync(DalamudAsset asset);
+}
diff --git a/Dalamud/Utility/EnumExtensions.cs b/Dalamud/Utility/EnumExtensions.cs
index 0bb60962e..493e6be1f 100644
--- a/Dalamud/Utility/EnumExtensions.cs
+++ b/Dalamud/Utility/EnumExtensions.cs
@@ -1,4 +1,4 @@
-using System;
+using System.Collections.Generic;
using System.Linq;
namespace Dalamud.Utility;
@@ -8,6 +8,26 @@ namespace Dalamud.Utility;
///
public static class EnumExtensions
{
+ ///
+ /// Gets attributes on an enum.
+ ///
+ /// The type of attribute to get.
+ /// The enum value that has an attached attribute.
+ /// The enumerable of the attached attributes.
+ public static IEnumerable GetAttributes(this Enum value)
+ where TAttribute : Attribute
+ {
+ var type = value.GetType();
+ var name = Enum.GetName(type, value);
+ if (name.IsNullOrEmpty())
+ return Array.Empty();
+
+ return type.GetField(name)?
+ .GetCustomAttributes(false)
+ .OfType()
+ ?? Array.Empty();
+ }
+
///
/// Gets an attribute on an enum.
///
@@ -15,18 +35,8 @@ public static class EnumExtensions
/// The enum value that has an attached attribute.
/// The attached attribute, if any.
public static TAttribute? GetAttribute(this Enum value)
- where TAttribute : Attribute
- {
- var type = value.GetType();
- var name = Enum.GetName(type, value);
- if (name.IsNullOrEmpty())
- return null;
-
- return type.GetField(name)?
- .GetCustomAttributes(false)
- .OfType()
- .SingleOrDefault();
- }
+ where TAttribute : Attribute =>
+ value.GetAttributes().SingleOrDefault();
///
/// Gets an indicator if enum has been flagged as obsolete (deprecated).