diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index feed79772..a35248062 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -380,7 +380,7 @@ namespace Dalamud.Injector startInfo.BootShowConsole = args.Contains("--console"); startInfo.BootEnableEtw = args.Contains("--etw"); startInfo.BootLogPath = GetLogPath(startInfo.LogPath, "dalamud.boot", startInfo.LogName); - startInfo.BootEnabledGameFixes = new List { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "clr_failfast_hijack", "prevent_icmphandle_crashes" }; + startInfo.BootEnabledGameFixes = new List { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save", "prevent_icmphandle_crashes" }; startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0; diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 791ba2158..831b25cbc 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Threading; @@ -16,9 +17,11 @@ using JetBrains.Annotations; using Lumina; using Lumina.Data; using Lumina.Data.Files; +using Lumina.Data.Parsing.Tex.Buffers; using Lumina.Excel; using Newtonsoft.Json; using Serilog; +using SharpDX.DXGI; namespace Dalamud.Data; @@ -187,15 +190,17 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager /// /// The icon ID. /// The containing the icon. - /// TODO(v9): remove in api9 in favor of GetIcon(uint iconId, bool highResolution) + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(uint iconId) => this.GetIcon(this.Language, iconId, false); /// + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(uint iconId, bool highResolution) => this.GetIcon(this.Language, iconId, highResolution); /// + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(bool isHq, uint iconId) { var type = isHq ? "hq/" : string.Empty; @@ -208,11 +213,12 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager /// The requested language. /// The icon ID. /// The containing the icon. - /// TODO(v9): remove in api9 in favor of GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution) + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId) => this.GetIcon(iconLanguage, iconId, false); /// + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution) { var type = iconLanguage switch @@ -233,11 +239,12 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). /// The icon ID. /// The containing the icon. - /// TODO(v9): remove in api9 in favor of GetIcon(string? type, uint iconId, bool highResolution) + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(string? type, uint iconId) => this.GetIcon(type, iconId, false); /// + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(string? type, uint iconId, bool highResolution) { var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; @@ -259,14 +266,39 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager } /// + [Obsolete("Use ITextureProvider instead")] public TexFile? GetHqIcon(uint iconId) => this.GetIcon(true, iconId); /// + [Obsolete("Use ITextureProvider instead")] + [return: NotNullIfNotNull(nameof(tex))] public TextureWrap? GetImGuiTexture(TexFile? tex) - => tex == null ? null : Service.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4); + { + if (tex is null) + return null; + var im = Service.Get(); + var buffer = tex.TextureBuffer; + var bpp = 1 << (((int)tex.Header.Format & (int)TexFile.TextureFormat.BppMask) >> + (int)TexFile.TextureFormat.BppShift); + + var (dxgiFormat, conversion) = TexFile.GetDxgiFormatFromTextureFormat(tex.Header.Format, false); + if (conversion != TexFile.DxgiFormatConversion.NoConversion || !im.SupportsDxgiFormat((Format)dxgiFormat)) + { + dxgiFormat = (int)Format.B8G8R8A8_UNorm; + buffer = buffer.Filter(0, 0, TexFile.TextureFormat.B8G8R8A8); + bpp = 32; + } + + var pitch = buffer is BlockCompressionTextureBuffer + ? Math.Max(1, (buffer.Width + 3) / 4) * 2 * bpp + : ((buffer.Width * bpp) + 7) / 8; + return im.LoadImageFromDxgiFormat(buffer.RawData, pitch, buffer.Width, buffer.Height, (Format)dxgiFormat); + } + /// + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTexture(string path) => this.GetImGuiTexture(this.GetFile(path)); @@ -276,26 +308,32 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager /// The icon ID. /// The containing the icon. /// TODO(v9): remove in api9 in favor of GetImGuiTextureIcon(uint iconId, bool highResolution) + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(uint iconId) => this.GetImGuiTexture(this.GetIcon(iconId, false)); /// + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution) => this.GetImGuiTexture(this.GetIcon(iconId, highResolution)); /// + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId) => this.GetImGuiTexture(this.GetIcon(isHq, iconId)); /// + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId) => this.GetImGuiTexture(this.GetIcon(iconLanguage, iconId)); /// + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(string type, uint iconId) => this.GetImGuiTexture(this.GetIcon(type, iconId)); /// + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureHqIcon(uint iconId) => this.GetImGuiTexture(this.GetHqIcon(iconId)); diff --git a/Dalamud/Game/ClientState/Objects/Types/Character.cs b/Dalamud/Game/ClientState/Objects/Types/Character.cs index cdd1515b0..ee8418362 100644 --- a/Dalamud/Game/ClientState/Objects/Types/Character.cs +++ b/Dalamud/Game/ClientState/Objects/Types/Character.cs @@ -26,52 +26,52 @@ public unsafe class Character : GameObject /// /// Gets the current HP of this Chara. /// - public uint CurrentHp => this.Struct->Health; + public uint CurrentHp => this.Struct->CharacterData.Health; /// /// Gets the maximum HP of this Chara. /// - public uint MaxHp => this.Struct->MaxHealth; + public uint MaxHp => this.Struct->CharacterData.MaxHealth; /// /// Gets the current MP of this Chara. /// - public uint CurrentMp => this.Struct->Mana; + public uint CurrentMp => this.Struct->CharacterData.Mana; /// /// Gets the maximum MP of this Chara. /// - public uint MaxMp => this.Struct->MaxMana; + public uint MaxMp => this.Struct->CharacterData.MaxMana; /// /// Gets the current GP of this Chara. /// - public uint CurrentGp => this.Struct->GatheringPoints; + public uint CurrentGp => this.Struct->CharacterData.GatheringPoints; /// /// Gets the maximum GP of this Chara. /// - public uint MaxGp => this.Struct->MaxGatheringPoints; + public uint MaxGp => this.Struct->CharacterData.MaxGatheringPoints; /// /// Gets the current CP of this Chara. /// - public uint CurrentCp => this.Struct->CraftingPoints; + public uint CurrentCp => this.Struct->CharacterData.CraftingPoints; /// /// Gets the maximum CP of this Chara. /// - public uint MaxCp => this.Struct->MaxCraftingPoints; + public uint MaxCp => this.Struct->CharacterData.MaxCraftingPoints; /// /// Gets the ClassJob of this Chara. /// - public ExcelResolver ClassJob => new(this.Struct->ClassJob); + public ExcelResolver ClassJob => new(this.Struct->CharacterData.ClassJob); /// /// Gets the level of this Chara. /// - public byte Level => this.Struct->Level; + public byte Level => this.Struct->CharacterData.Level; /// /// Gets a byte array describing the visual appearance of this Chara. @@ -97,7 +97,7 @@ public unsafe class Character : GameObject /// /// Gets the current online status of the character. /// - public ExcelResolver OnlineStatus => new(this.Struct->OnlineStatus); + public ExcelResolver OnlineStatus => new(this.Struct->CharacterData.OnlineStatus); /// /// Gets the status flags. diff --git a/Dalamud/Interface/Internal/DalamudTextureWrap.cs b/Dalamud/Interface/Internal/DalamudTextureWrap.cs index 97fb1dd0b..039873f1f 100644 --- a/Dalamud/Interface/Internal/DalamudTextureWrap.cs +++ b/Dalamud/Interface/Internal/DalamudTextureWrap.cs @@ -4,11 +4,19 @@ using ImGuiScene; namespace Dalamud.Interface.Internal; +/// +/// Base TextureWrap interface for all Dalamud-owned texture wraps. +/// Used to avoid referencing ImGuiScene. +/// +public interface IDalamudTextureWrap : TextureWrap +{ +} + /// /// Safety harness for ImGuiScene textures that will defer destruction until /// the end of the frame. /// -public class DalamudTextureWrap : TextureWrap +public class DalamudTextureWrap : IDalamudTextureWrap { private readonly TextureWrap wrappedWrap; diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6bb45b325..841511f55 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -26,6 +26,10 @@ using ImGuiNET; using ImGuiScene; using PInvoke; using Serilog; +using SharpDX; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; // general dev notes, here because it's easiest @@ -303,6 +307,62 @@ internal class InterfaceManager : IDisposable, IServiceType return null; } + /// + /// Check whether the current D3D11 Device supports the given DXGI format. + /// + /// DXGI format to check. + /// Whether it is supported. + public bool SupportsDxgiFormat(Format dxgiFormat) => this.scene is null + ? throw new InvalidOperationException("Scene isn't ready.") + : this.scene.Device.CheckFormatSupport(dxgiFormat).HasFlag(FormatSupport.Texture2D); + + /// + /// Load an image from a span of bytes of specified format. + /// + /// The data to load. + /// The pitch(stride) in bytes. + /// The width in pixels. + /// The height in pixels. + /// Format of the texture. + /// A texture, ready to use in ImGui. + public TextureWrap LoadImageFromDxgiFormat(Span data, int pitch, int width, int height, Format dxgiFormat) + { + if (this.scene == null) + throw new InvalidOperationException("Scene isn't ready."); + + ShaderResourceView resView; + unsafe + { + fixed (void* pData = data) + { + var texDesc = new Texture2DDescription + { + Width = width, + Height = height, + MipLevels = 1, + ArraySize = 1, + Format = dxgiFormat, + SampleDescription = new(1, 0), + Usage = ResourceUsage.Immutable, + BindFlags = BindFlags.ShaderResource, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None, + }; + + using var texture = new Texture2D(this.Device, texDesc, new DataRectangle(new(pData), pitch)); + resView = new(this.Device, texture, new() + { + Format = texDesc.Format, + Dimension = ShaderResourceViewDimension.Texture2D, + Texture2D = { MipLevels = texDesc.MipLevels }, + }); + } + } + + // no sampler for now because the ImGui implementation we copied doesn't allow for changing it + return new D3DTextureWrap(resView, width, height); + } + #nullable restore /// @@ -609,12 +669,19 @@ internal class InterfaceManager : IDisposable, IServiceType var pRes = this.presentHook.Original(swapChain, syncInterval, presentFlags); this.RenderImGui(); + this.DisposeTextures(); return pRes; } this.RenderImGui(); + this.DisposeTextures(); + return this.presentHook.Original(swapChain, syncInterval, presentFlags); + } + + private void DisposeTextures() + { if (this.deferredDisposeTextures.Count > 0) { Log.Verbose("[IM] Disposing {Count} textures", this.deferredDisposeTextures.Count); @@ -625,8 +692,6 @@ internal class InterfaceManager : IDisposable, IServiceType this.deferredDisposeTextures.Clear(); } - - return this.presentHook.Original(swapChain, syncInterval, presentFlags); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs new file mode 100644 index 000000000..4b2f1f362 --- /dev/null +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -0,0 +1,598 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Numerics; + +using Dalamud.Data; +using Dalamud.Game; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; +using Dalamud.Plugin.Services; +using ImGuiScene; +using Lumina.Data.Files; + +namespace Dalamud.Interface.Internal; + +/// +/// Service responsible for loading and disposing ImGui texture wraps. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.BlockingEarlyLoadedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionProvider +{ + private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; + private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; + + private const uint MillisecondsEvictionTime = 2000; + + private static readonly ModuleLog Log = new("TEXM"); + + private readonly Framework framework; + private readonly DataManager dataManager; + private readonly InterfaceManager im; + private readonly DalamudStartInfo startInfo; + + private readonly Dictionary activeTextures = new(); + + private TextureWrap? fallbackTextureWrap; + + /// + /// Initializes a new instance of the class. + /// + /// Framework instance. + /// DataManager instance. + /// InterfaceManager instance. + /// DalamudStartInfo instance. + [ServiceManager.ServiceConstructor] + public TextureManager(Framework framework, DataManager dataManager, InterfaceManager im, DalamudStartInfo startInfo) + { + this.framework = framework; + this.dataManager = dataManager; + this.im = im; + this.startInfo = startInfo; + + this.framework.Update += this.FrameworkOnUpdate; + + Service.GetAsync().ContinueWith(_ => this.CreateFallbackTexture()); + } + + /// + public event ITextureSubstitutionProvider.TextureDataInterceptorDelegate? InterceptTexDataLoad; + + /// + /// Get a texture handle for a specific icon. + /// + /// The ID of the icon to load. + /// Options to be considered when loading the icon. + /// + /// The language to be considered when loading the icon, if the icon has versions for multiple languages. + /// If null, default to the game's current language. + /// + /// + /// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set. + /// + /// + /// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used + /// to render the icon. + /// + public TextureManagerTextureWrap? GetIcon(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false) + { + var path = this.GetIconPath(iconId, flags, language); + return path == null ? null : this.CreateWrap(path, keepAlive); + } + + /// + /// Get a path for a specific icon's .tex file. + /// + /// The ID of the icon to look up. + /// Options to be considered when loading the icon. + /// + /// The language to be considered when loading the icon, if the icon has versions for multiple languages. + /// If null, default to the game's current language. + /// + /// + /// Null, if the icon does not exist in the specified configuration, or the path to the texture's .tex file, + /// which can be loaded via IDataManager. + /// + public string? GetIconPath(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null) + { + var hiRes = flags.HasFlag(ITextureProvider.IconFlags.HiRes); + + // 1. Item + var path = FormatIconPath( + iconId, + flags.HasFlag(ITextureProvider.IconFlags.ItemHighQuality) ? "hq/" : string.Empty, + hiRes); + if (this.dataManager.FileExists(path)) + return path; + + language ??= this.startInfo.Language; + var languageFolder = language switch + { + ClientLanguage.Japanese => "ja/", + ClientLanguage.English => "en/", + ClientLanguage.German => "de/", + ClientLanguage.French => "fr/", + _ => throw new ArgumentOutOfRangeException(nameof(language), $"Unknown Language: {language}"), + }; + + // 2. Regular icon, with language, hi-res + path = FormatIconPath( + iconId, + languageFolder, + hiRes); + if (this.dataManager.FileExists(path)) + return path; + + if (hiRes) + { + // 3. Regular icon, with language, no hi-res + path = FormatIconPath( + iconId, + languageFolder, + false); + if (this.dataManager.FileExists(path)) + return path; + } + + // 4. Regular icon, without language, hi-res + path = FormatIconPath( + iconId, + null, + hiRes); + if (this.dataManager.FileExists(path)) + return path; + + // 4. Regular icon, without language, no hi-res + if (hiRes) + { + path = FormatIconPath( + iconId, + null, + false); + if (this.dataManager.FileExists(path)) + return path; + } + + return null; + } + + /// + /// Get a texture handle for the texture at the specified path. + /// You may only specify paths in the game's VFS. + /// + /// The path to the texture in the game's VFS. + /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set. + /// Null, if the icon does not exist, or a texture wrap that can be used to render the texture. + public TextureManagerTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + if (Path.IsPathRooted(path)) + throw new ArgumentException("Use GetTextureFromFile() to load textures directly from a file.", nameof(path)); + + return !this.dataManager.FileExists(path) ? null : this.CreateWrap(path, keepAlive); + } + + /// + /// Get a texture handle for the image or texture, specified by the passed FileInfo. + /// You may only specify paths on the native file system. + /// + /// This API can load .png and .tex files. + /// + /// The FileInfo describing the image or texture file. + /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set. + /// Null, if the file does not exist, or a texture wrap that can be used to render the texture. + public TextureManagerTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) + { + ArgumentNullException.ThrowIfNull(file); + return !file.Exists ? null : this.CreateWrap(file.FullName, keepAlive); + } + + /// + /// Get a texture handle for the specified Lumina TexFile. + /// + /// The texture to obtain a handle to. + /// A texture wrap that can be used to render the texture. + public IDalamudTextureWrap? GetTexture(TexFile file) + { + ArgumentNullException.ThrowIfNull(file); + + if (!this.im.IsReady) + throw new InvalidOperationException("Cannot create textures before scene is ready"); + +#pragma warning disable CS0618 + return this.dataManager.GetImGuiTexture(file) as IDalamudTextureWrap; +#pragma warning restore CS0618 + } + + /// + public void Dispose() + { + this.fallbackTextureWrap?.Dispose(); + this.framework.Update -= this.FrameworkOnUpdate; + + Log.Verbose("Disposing {Num} left behind textures."); + + foreach (var activeTexture in this.activeTextures) + { + activeTexture.Value.Wrap?.Dispose(); + } + + this.activeTextures.Clear(); + } + + /// + /// Get texture info. + /// + /// Path to the texture. + /// Whether or not the texture should be reloaded if it was unloaded. + /// + /// If true, exceptions caused by texture load will not be caught. + /// If false, exceptions will be caught and a dummy texture will be returned to prevent plugins from using invalid texture handles. + /// + /// Info object storing texture metadata. + internal TextureInfo GetInfo(string path, bool refresh = true, bool rethrow = false) + { + TextureInfo? info; + lock (this.activeTextures) + { + this.activeTextures.TryGetValue(path, out info); + } + + if (info == null) + { + info = new TextureInfo(); + lock (this.activeTextures) + { + if (!this.activeTextures.TryAdd(path, info)) + Log.Warning("Texture {Path} tracked twice", path); + } + } + + if (refresh && info.KeepAliveCount == 0) + info.LastAccess = DateTime.UtcNow; + + if (info is { Wrap: not null }) + return info; + + if (refresh) + { + if (!this.im.IsReady) + throw new InvalidOperationException("Cannot create textures before scene is ready"); + + string? interceptPath = null; + this.InterceptTexDataLoad?.Invoke(path, ref interceptPath); + + if (interceptPath != null) + { + Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath); + path = interceptPath; + } + + TextureWrap? wrap; + try + { + // We want to load this from the disk, probably, if the path has a root + // Not sure if this can cause issues with e.g. network drives, might have to rethink + // and add a flag instead if it does. + if (Path.IsPathRooted(path)) + { + if (Path.GetExtension(path) == ".tex") + { + // Attempt to load via Lumina + var file = this.dataManager.GameData.GetFileFromDisk(path); + wrap = this.GetTexture(file); + Log.Verbose("Texture {Path} loaded FS via Lumina", path); + } + else + { + // Attempt to load image + wrap = this.im.LoadImage(path); + Log.Verbose("Texture {Path} loaded FS via LoadImage", path); + } + } + else + { + // Load regularly from dats + var file = this.dataManager.GetFile(path); + if (file == null) + throw new Exception("Could not load TexFile from dat."); + + wrap = this.GetTexture(file); + Log.Verbose("Texture {Path} loaded from SqPack", path); + } + + if (wrap == null) + throw new Exception("Could not create texture"); + + // TODO: We could support this, but I don't think it's worth it at the moment. + var extents = new Vector2(wrap.Width, wrap.Height); + if (info.Extents != Vector2.Zero && info.Extents != extents) + Log.Warning("Texture at {Path} changed size between reloads, this is currently not supported.", path); + + info.Extents = extents; + } + catch (Exception e) + { + Log.Error(e, "Could not load texture from {Path}", path); + + // When creating the texture initially, we want to be able to pass errors back to the plugin + if (rethrow) + throw; + + // This means that the load failed due to circumstances outside of our control, + // and we can't do anything about it. Return a dummy texture so that the plugin still + // has something to draw. + wrap = this.fallbackTextureWrap; + } + + info.Wrap = wrap; + } + + return info; + } + + /// + /// Notify the system about an instance of a texture wrap being disposed. + /// If required conditions are met, the texture will be unloaded at the next update. + /// + /// The path to the texture. + /// Whether or not this handle was created in keep-alive mode. + internal void NotifyTextureDisposed(string path, bool keepAlive) + { + var info = this.GetInfo(path, false); + info.RefCount--; + + if (keepAlive) + info.KeepAliveCount--; + + // Clean it up by the next update. If it's re-requested in-between, we don't reload it. + if (info.RefCount <= 0) + info.LastAccess = default; + } + + private static string FormatIconPath(uint iconId, string? type, bool highResolution) + { + var format = highResolution ? HighResolutionIconFileFormat : IconFileFormat; + + type ??= string.Empty; + if (type.Length > 0 && !type.EndsWith("/")) + type += "/"; + + return string.Format(format, iconId / 1000, type, iconId); + } + + private TextureManagerTextureWrap? CreateWrap(string path, bool keepAlive) + { + // This will create the texture. + // That's fine, it's probably used immediately and this will let the plugin catch load errors. + var info = this.GetInfo(path, rethrow: true); + info.RefCount++; + + if (keepAlive) + info.KeepAliveCount++; + + return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this); + } + + private void FrameworkOnUpdate(Framework fw) + { + lock (this.activeTextures) + { + var toRemove = new List(); + + foreach (var texInfo in this.activeTextures) + { + if (texInfo.Value.RefCount == 0) + { + Log.Verbose("Evicting {Path} since no refs", texInfo.Key); + + Debug.Assert(texInfo.Value.KeepAliveCount == 0, "texInfo.Value.KeepAliveCount == 0"); + + texInfo.Value.Wrap?.Dispose(); + texInfo.Value.Wrap = null; + toRemove.Add(texInfo.Key); + continue; + } + + if (texInfo.Value.KeepAliveCount > 0 || texInfo.Value.Wrap == null) + continue; + + if (DateTime.UtcNow - texInfo.Value.LastAccess > TimeSpan.FromMilliseconds(MillisecondsEvictionTime)) + { + Log.Verbose("Evicting {Path} since too old", texInfo.Key); + texInfo.Value.Wrap.Dispose(); + texInfo.Value.Wrap = null; + } + } + + foreach (var path in toRemove) + { + this.activeTextures.Remove(path); + } + } + } + + private void CreateFallbackTexture() + { + var fallbackTexBytes = new byte[] { 0xFF, 0x00, 0xDC, 0xFF }; + this.fallbackTextureWrap = this.im.LoadImageRaw(fallbackTexBytes, 1, 1, 4); + Debug.Assert(this.fallbackTextureWrap != null, "this.fallbackTextureWrap != null"); + } + + /// + /// Internal representation of a managed texture. + /// + internal class TextureInfo + { + /// + /// Gets or sets the actual texture wrap. May be unpopulated. + /// + public TextureWrap? Wrap { get; set; } + + /// + /// Gets or sets the time the texture was last accessed. + /// + public DateTime LastAccess { get; set; } + + /// + /// Gets or sets the number of active holders of this texture. + /// + public uint RefCount { get; set; } + + /// + /// Gets or sets the number of active holders that want this texture to stay alive forever. + /// + public uint KeepAliveCount { get; set; } + + /// + /// Gets or sets the extents of the texture. + /// + public Vector2 Extents { get; set; } + } +} + +/// +/// Plugin-scoped version of a texture manager. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDisposable +{ + private readonly TextureManager textureManager; + + private readonly List trackedTextures = new(); + + /// + /// Initializes a new instance of the class. + /// + /// TextureManager instance. + public TextureManagerPluginScoped(TextureManager textureManager) + { + this.textureManager = textureManager; + } + + /// + public IDalamudTextureWrap? GetIcon( + uint iconId, + ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.ItemHighQuality, + ClientLanguage? language = null, + bool keepAlive = false) + { + var wrap = this.textureManager.GetIcon(iconId, flags, language, keepAlive); + if (wrap == null) + return null; + + this.trackedTextures.Add(wrap); + return wrap; + } + + /// + public string? GetIconPath(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null) + => this.textureManager.GetIconPath(iconId, flags, language); + + /// + public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + var wrap = this.textureManager.GetTextureFromGame(path, keepAlive); + if (wrap == null) + return null; + + this.trackedTextures.Add(wrap); + return wrap; + } + + /// + public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive) + { + ArgumentNullException.ThrowIfNull(file); + + var wrap = this.textureManager.GetTextureFromFile(file, keepAlive); + if (wrap == null) + return null; + + this.trackedTextures.Add(wrap); + return wrap; + } + + /// + public IDalamudTextureWrap? GetTexture(TexFile file) + => this.textureManager.GetTexture(file); + + /// + public void Dispose() + { + // Dispose all leaked textures + foreach (var textureWrap in this.trackedTextures.Where(x => !x.IsDisposed)) + { + textureWrap.Dispose(); + } + } +} + +/// +/// Wrap. +/// +internal class TextureManagerTextureWrap : IDalamudTextureWrap +{ + private readonly TextureManager manager; + private readonly string path; + private readonly bool keepAlive; + + /// + /// Initializes a new instance of the class. + /// + /// The path to the texture. + /// The extents of the texture. + /// Keep alive or not. + /// Manager that we obtained this from. + internal TextureManagerTextureWrap(string path, Vector2 extents, bool keepAlive, TextureManager manager) + { + this.path = path; + this.keepAlive = keepAlive; + this.manager = manager; + this.Width = (int)extents.X; + this.Height = (int)extents.Y; + } + + /// + public IntPtr ImGuiHandle => this.manager.GetInfo(this.path).Wrap!.ImGuiHandle; + + /// + public int Width { get; private set; } + + /// + public int Height { get; private set; } + + /// + /// Gets a value indicating whether or not this wrap has already been disposed. + /// If true, the handle may be invalid. + /// + internal bool IsDisposed { get; private set; } + + /// + public void Dispose() + { + lock (this) + { + if (!this.IsDisposed) + this.manager.NotifyTextureDisposed(this.path, this.keepAlive); + + this.IsDisposed = true; + } + } +} diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index 4a0cfe21a..5ad5868c3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,8 +1,9 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Numerics; -using Dalamud.Data; -using Dalamud.Utility; +using Dalamud.Plugin.Services; using ImGuiNET; using ImGuiScene; using Serilog; @@ -14,13 +15,18 @@ namespace Dalamud.Interface.Internal.Windows.Data; /// internal class TexWidget : IDataWindowWidget { + private readonly List addedTextures = new(); + + private string iconId = "18"; + private bool hiRes = true; + private bool hq = false; + private bool keepAlive = false; private string inputTexPath = string.Empty; - private TextureWrap? debugTex; private Vector2 inputTexUv0 = Vector2.Zero; private Vector2 inputTexUv1 = Vector2.One; private Vector4 inputTintCol = Vector4.One; private Vector2 inputTexScale = Vector2.Zero; - + /// public DataKind DataKind { get; init; } = DataKind.Tex; @@ -36,34 +42,87 @@ internal class TexWidget : IDataWindowWidget /// public void Draw() { - var dataManager = Service.Get(); + var texManager = Service.Get(); - ImGui.InputText("Tex Path", ref this.inputTexPath, 255); - ImGui.InputFloat2("UV0", ref this.inputTexUv0); - ImGui.InputFloat2("UV1", ref this.inputTexUv1); - ImGui.InputFloat4("Tint", ref this.inputTintCol); - ImGui.InputFloat2("Scale", ref this.inputTexScale); - - if (ImGui.Button("Load Tex")) + ImGui.InputText("Icon ID", ref this.iconId, 32); + ImGui.Checkbox("HQ Item", ref this.hq); + ImGui.Checkbox("Hi-Res", ref this.hiRes); + ImGui.Checkbox("Keep alive", ref this.keepAlive); + if (ImGui.Button("Load Icon")) { try { - this.debugTex = dataManager.GetImGuiTexture(this.inputTexPath); - this.inputTexScale = new Vector2(this.debugTex?.Width ?? 0, this.debugTex?.Height ?? 0); + var flags = ITextureProvider.IconFlags.None; + if (this.hq) + flags |= ITextureProvider.IconFlags.ItemHighQuality; + + if (this.hiRes) + flags |= ITextureProvider.IconFlags.HiRes; + + this.addedTextures.Add(texManager.GetIcon(uint.Parse(this.iconId), flags, keepAlive: this.keepAlive)); } catch (Exception ex) { Log.Error(ex, "Could not load tex"); } } + + ImGui.Separator(); + ImGui.InputText("Tex Path", ref this.inputTexPath, 255); + if (ImGui.Button("Load Tex")) + { + try + { + this.addedTextures.Add(texManager.GetTextureFromGame(this.inputTexPath, this.keepAlive)); + } + catch (Exception ex) + { + Log.Error(ex, "Could not load tex"); + } + } + + if (ImGui.Button("Load File")) + { + try + { + this.addedTextures.Add(texManager.GetTextureFromFile(new FileInfo(this.inputTexPath), this.keepAlive)); + } + catch (Exception ex) + { + Log.Error(ex, "Could not load tex"); + } + } + + ImGui.Separator(); + ImGui.InputFloat2("UV0", ref this.inputTexUv0); + ImGui.InputFloat2("UV1", ref this.inputTexUv1); + ImGui.InputFloat4("Tint", ref this.inputTintCol); + ImGui.InputFloat2("Scale", ref this.inputTexScale); ImGuiHelpers.ScaledDummy(10); - if (this.debugTex != null) + TextureWrap? toRemove = null; + for (var i = 0; i < this.addedTextures.Count; i++) { - ImGui.Image(this.debugTex.ImGuiHandle, this.inputTexScale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); - ImGuiHelpers.ScaledDummy(5); - Util.ShowObject(this.debugTex); + if (ImGui.CollapsingHeader($"Tex #{i}")) + { + var tex = this.addedTextures[i]; + + var scale = new Vector2(tex.Width, tex.Height); + if (this.inputTexScale != Vector2.Zero) + scale = this.inputTexScale; + + ImGui.Image(tex.ImGuiHandle, scale, this.inputTexUv0, this.inputTexUv1, this.inputTintCol); + + if (ImGui.Button($"X##{i}")) + toRemove = tex; + } + } + + if (toRemove != null) + { + toRemove.Dispose(); + this.addedTextures.Remove(toRemove); } } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 4e27d50bd..35fa40013 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -569,7 +569,12 @@ internal class PluginInstallerWindow : Window, IDisposable this.filterText = selectable.Localization; lock (this.listLock) + { this.ResortPlugins(); + + // Positions of plugins within the list is likely to change + this.openPluginCollapsibles.Clear(); + } } } @@ -2963,7 +2968,18 @@ internal class PluginInstallerWindow : Window, IDisposable break; case PluginSortKind.LastUpdate: this.pluginListAvailable.Sort((p1, p2) => p2.LastUpdate.CompareTo(p1.LastUpdate)); - this.pluginListInstalled.Sort((p1, p2) => p2.Manifest.LastUpdate.CompareTo(p1.Manifest.LastUpdate)); + this.pluginListInstalled.Sort((p1, p2) => + { + // We need to get remote manifests here, as the local manifests will have the time when the current version is installed, + // not the actual time of the last update, as the plugin may be pending an update + IPluginManifest? p2Considered = this.pluginListAvailable.FirstOrDefault(x => x.InternalName == p2.InternalName); + p2Considered ??= p2.Manifest; + + IPluginManifest? p1Considered = this.pluginListAvailable.FirstOrDefault(x => x.InternalName == p1.InternalName); + p1Considered ??= p1.Manifest; + + return p2Considered.LastUpdate.CompareTo(p1Considered.LastUpdate); + }); break; case PluginSortKind.NewOrNot: this.pluginListAvailable.Sort((p1, p2) => this.WasPluginSeen(p1.InternalName) diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index feac634f3..db748303e 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -12,6 +12,9 @@ namespace Dalamud.IoC.Internal; /// /// A simple singleton-only IOC container that provides (optional) version-based dependency resolution. +/// +/// This is only used to resolve dependencies for plugins. +/// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// internal class ServiceContainer : IServiceProvider, IServiceType { @@ -31,7 +34,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType /// Register a singleton object of any type into the current IOC container. /// /// The existing instance to register in the container. - /// The interface to register. + /// The type to register. public void RegisterSingleton(Task instance) { if (instance == null) @@ -40,19 +43,27 @@ internal class ServiceContainer : IServiceProvider, IServiceType } this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T)); + this.RegisterInterfaces(typeof(T)); + } - var resolveViaTypes = typeof(T) - .GetCustomAttributes() - .OfType() - .Select(x => x.GetType().GetGenericArguments().First()); + /// + /// Register the interfaces that can resolve this type. + /// + /// The type to register. + public void RegisterInterfaces(Type type) + { + var resolveViaTypes = type + .GetCustomAttributes() + .OfType() + .Select(x => x.GetType().GetGenericArguments().First()); foreach (var resolvableType in resolveViaTypes) { - Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", typeof(T).FullName ?? "???"); + Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", type.FullName ?? "???"); Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed"); - Debug.Assert(typeof(T).IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type"); + Debug.Assert(type.IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type"); - this.interfaceToTypeMap[resolvableType] = typeof(T); + this.interfaceToTypeMap[resolvableType] = type; } } @@ -95,18 +106,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType parameters .Select(async p => { - if (p.parameterType.GetCustomAttribute() != null) - { - if (scopeImpl == null) - { - Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!); - return null; - } - - return await scopeImpl.CreatePrivateScopedObject(p.parameterType, scopedObjects); - } - - var service = await this.GetService(p.parameterType, scopedObjects); + var service = await this.GetService(p.parameterType, scopeImpl, scopedObjects); if (service == null) { @@ -168,22 +168,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType foreach (var prop in props) { - object service = null; - - if (prop.propertyInfo.PropertyType.GetCustomAttribute() != null) - { - if (scopeImpl == null) - { - Log.Error("Failed to create {TypeName}, depends on scoped service but no scope", objectType.FullName!); - } - else - { - service = await scopeImpl.CreatePrivateScopedObject(prop.propertyInfo.PropertyType, publicScopes); - } - } - - service ??= await this.GetService(prop.propertyInfo.PropertyType, publicScopes); - + var service = await this.GetService(prop.propertyInfo.PropertyType, scopeImpl, publicScopes); if (service == null) { Log.Error("Requested service type {TypeName} was not available (null)", prop.propertyInfo.PropertyType.FullName!); @@ -203,7 +188,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType public IServiceScope GetScope() => new ServiceScopeImpl(this); /// - object? IServiceProvider.GetService(Type serviceType) => this.GetService(serviceType); + object? IServiceProvider.GetService(Type serviceType) => this.GetSingletonService(serviceType); private static bool CheckInterfaceVersion(RequiredVersionAttribute? requiredVersion, Type parameterType) { @@ -228,9 +213,23 @@ internal class ServiceContainer : IServiceProvider, IServiceType return false; } - private async Task GetService(Type serviceType, object[] scopedObjects) + private async Task GetService(Type serviceType, ServiceScopeImpl? scope, object[] scopedObjects) { - var singletonService = await this.GetService(serviceType); + if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) + serviceType = implementingType; + + if (serviceType.GetCustomAttribute() != null) + { + if (scope == null) + { + Log.Error("Failed to create {TypeName}, is scoped but no scope provided", serviceType.FullName!); + return null; + } + + return await scope.CreatePrivateScopedObject(serviceType, scopedObjects); + } + + var singletonService = await this.GetSingletonService(serviceType, false); if (singletonService != null) { return singletonService; @@ -246,9 +245,9 @@ internal class ServiceContainer : IServiceProvider, IServiceType return scoped; } - private async Task GetService(Type serviceType) + private async Task GetSingletonService(Type serviceType, bool tryGetInterface = true) { - if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) + if (tryGetInterface && this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; if (!this.instances.TryGetValue(serviceType, out var service)) @@ -285,13 +284,24 @@ internal class ServiceContainer : IServiceProvider, IServiceType private bool ValidateCtor(ConstructorInfo ctor, Type[] types) { + bool IsTypeValid(Type type) + { + var contains = types.Any(x => x.IsAssignableTo(type)); + + // Scoped services are created on-demand + return contains || type.GetCustomAttribute() != null; + } + var parameters = ctor.GetParameters(); foreach (var parameter in parameters) { - var contains = types.Any(x => x.IsAssignableTo(parameter.ParameterType)); + var valid = IsTypeValid(parameter.ParameterType); + + // If this service is provided by an interface + if (!valid && this.interfaceToTypeMap.TryGetValue(parameter.ParameterType, out var implementationType)) + valid = IsTypeValid(implementationType); - // Scoped services are created on-demand - if (!contains && parameter.ParameterType.GetCustomAttribute() == null) + if (!valid) { Log.Error("Failed to validate {TypeName}, unable to find any services that satisfy the type", parameter.ParameterType.FullName!); return false; diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index 4eac646ad..a47303ea6 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -1,4 +1,6 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using ImGuiScene; using Lumina; @@ -86,6 +88,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(uint iconId, bool highResolution = false); /// @@ -95,6 +98,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false); /// @@ -104,6 +108,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false); /// @@ -112,6 +117,7 @@ public interface IDataManager /// The icon ID. /// Return the high resolution version. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false); /// @@ -120,6 +126,7 @@ public interface IDataManager /// A value indicating whether the icon should be HQ. /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(bool isHq, uint iconId); /// @@ -127,6 +134,7 @@ public interface IDataManager /// /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TexFile? GetHqIcon(uint iconId); /// @@ -134,6 +142,8 @@ public interface IDataManager /// /// The Lumina . /// A that can be used to draw the texture. + [Obsolete("Use ITextureProvider instead")] + [return: NotNullIfNotNull(nameof(tex))] public TextureWrap? GetImGuiTexture(TexFile? tex); /// @@ -141,6 +151,7 @@ public interface IDataManager /// /// The internal path to the texture. /// A that can be used to draw the texture. + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTexture(string path); /// @@ -149,6 +160,7 @@ public interface IDataManager /// A value indicating whether the icon should be HQ. /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId); /// @@ -157,6 +169,7 @@ public interface IDataManager /// The requested language. /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId); /// @@ -165,6 +178,7 @@ public interface IDataManager /// The type of the icon (e.g. 'hq' to get the HQ variant of an item icon). /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(string type, uint iconId); /// @@ -172,5 +186,6 @@ public interface IDataManager /// /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureHqIcon(uint iconId); } diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs new file mode 100644 index 000000000..091b2ed67 --- /dev/null +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -0,0 +1,96 @@ +using System; +using System.IO; + +using Dalamud.Interface.Internal; +using Lumina.Data.Files; + +namespace Dalamud.Plugin.Services; + +/// +/// Service that grants you access to textures you may render via ImGui. +/// +public interface ITextureProvider +{ + /// + /// Flags describing the icon you wish to receive. + /// + [Flags] + public enum IconFlags + { + /// + /// Low-resolution, standard quality icon. + /// + None = 0, + + /// + /// If this icon is an item icon, and it has a high-quality variant, receive the high-quality version. + /// Null if the item does not have a high-quality variant. + /// + ItemHighQuality = 1 << 0, + + /// + /// Get the hi-resolution version of the icon, if it exists. + /// + HiRes = 1 << 1, + } + + /// + /// Get a texture handle for a specific icon. + /// + /// The ID of the icon to load. + /// Options to be considered when loading the icon. + /// + /// The language to be considered when loading the icon, if the icon has versions for multiple languages. + /// If null, default to the game's current language. + /// + /// + /// Prevent Dalamud from automatically unloading this icon to save memory. Usually does not need to be set. + /// + /// + /// Null, if the icon does not exist in the specified configuration, or a texture wrap that can be used + /// to render the icon. + /// + public IDalamudTextureWrap? GetIcon(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false); + + /// + /// Get a path for a specific icon's .tex file. + /// + /// The ID of the icon to look up. + /// Options to be considered when loading the icon. + /// + /// The language to be considered when loading the icon, if the icon has versions for multiple languages. + /// If null, default to the game's current language. + /// + /// + /// Null, if the icon does not exist in the specified configuration, or the path to the texture's .tex file, + /// which can be loaded via IDataManager. + /// + public string? GetIconPath(uint iconId, IconFlags flags = IconFlags.HiRes, ClientLanguage? language = null); + + /// + /// Get a texture handle for the texture at the specified path. + /// You may only specify paths in the game's VFS. + /// + /// The path to the texture in the game's VFS. + /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set. + /// Null, if the icon does not exist, or a texture wrap that can be used to render the texture. + public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false); + + /// + /// Get a texture handle for the image or texture, specified by the passed FileInfo. + /// You may only specify paths on the native file system. + /// + /// This API can load .png and .tex files. + /// + /// The FileInfo describing the image or texture file. + /// Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set. + /// Null, if the file does not exist, or a texture wrap that can be used to render the texture. + public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false); + + /// + /// Get a texture handle for the specified Lumina TexFile. + /// + /// The texture to obtain a handle to. + /// A texture wrap that can be used to render the texture. + public IDalamudTextureWrap GetTexture(TexFile file); +} diff --git a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs new file mode 100644 index 000000000..90be71adb --- /dev/null +++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs @@ -0,0 +1,20 @@ +namespace Dalamud.Plugin.Services; + +/// +/// Service that grants you the ability to replace texture data that is to be loaded by Dalamud. +/// +public interface ITextureSubstitutionProvider +{ + /// + /// Delegate describing a function that may be used to intercept and replace texture data. + /// The path assigned may point to another texture inside the game's dats, or a .tex file or image on the disk. + /// + /// The path to the texture that is to be loaded. + /// The path that should be loaded instead. + public delegate void TextureDataInterceptorDelegate(string path, ref string? replacementPath); + + /// + /// Event that will be called once Dalamud wants to load texture data. + /// + public event TextureDataInterceptorDelegate? InterceptTexDataLoad; +} diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 41ffe33ca..d1c1002bd 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -132,11 +132,20 @@ internal static class ServiceManager var dependencyServicesMap = new Dictionary>(); var getAsyncTaskMap = new Dictionary(); + var serviceContainer = Service.Get(); + foreach (var serviceType in Assembly.GetExecutingAssembly().GetTypes()) { var serviceKind = serviceType.GetServiceKind(); - if (serviceKind is ServiceKind.None or ServiceKind.ScopedService) + if (serviceKind is ServiceKind.None) continue; + + // Scoped service do not go through Service, so we must let ServiceContainer know what their interfaces map to + if (serviceKind is ServiceKind.ScopedService) + { + serviceContainer.RegisterInterfaces(serviceType); + continue; + } Debug.Assert( !serviceKind.HasFlag(ServiceKind.ManualService) && !serviceKind.HasFlag(ServiceKind.ScopedService), diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 8962a47b9..782d73171 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 8962a47b95f96bec53e58680bd9d1e7f38610d40 +Subproject commit 782d7317176f232d7108b2b3a4cb75de67fc3a8a