From b75b30c03d04cf815c78cefcc2ff56f9499b3bdb Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 30 Jul 2023 22:11:00 +0200 Subject: [PATCH 01/19] fix: clear list of viewing plugins in installer when resorting --- .../Windows/PluginInstaller/PluginInstallerWindow.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 4e27d50bd..4ed25c9e1 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(); + } } } From f69fb6cc037e98f2df9f201b1138f86469ec2791 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 30 Jul 2023 22:16:03 +0200 Subject: [PATCH 02/19] fix: use LastUpdated in remote manifest when sorting installed plugins Will make sure installed plugins with pending updates will still be sorted as expected --- .../PluginInstaller/PluginInstallerWindow.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 4ed25c9e1..35fa40013 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2968,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) From 29debac9e1da99e8559c057b858c65186fe69d60 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Sun, 30 Jul 2023 18:26:50 -0700 Subject: [PATCH 03/19] Character.cs Obsoletes --- .../ClientState/Objects/Types/Character.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) 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. From b6cfe339464478c0dafaa3a0336cdc7ae6a683e7 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 02:35:37 +0200 Subject: [PATCH 04/19] feat: new ITextureProvider, ITextureSubstitutionProvider, TextureManager services Ref-counts textures and evicts when not used --- .../Interface/Internal/DalamudTextureWrap.cs | 10 +- Dalamud/Interface/Internal/TextureManager.cs | 458 ++++++++++++++++++ .../Windows/Data/Widgets/TexWidget.cs | 80 ++- Dalamud/Plugin/Services/IDataManager.cs | 15 +- Dalamud/Plugin/Services/ITextureProvider.cs | 69 +++ .../Services/ITextureSubstitutionProvider.cs | 21 + 6 files changed, 635 insertions(+), 18 deletions(-) create mode 100644 Dalamud/Interface/Internal/TextureManager.cs create mode 100644 Dalamud/Plugin/Services/ITextureProvider.cs create mode 100644 Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs 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/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs new file mode 100644 index 000000000..05b10263b --- /dev/null +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -0,0 +1,458 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; + +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; +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 DalamudStartInfo startInfo; + + private readonly Dictionary activeTextures = new(); + + /// + /// Initializes a new instance of the class. + /// + /// Framework instance. + /// DataManager instance. + /// DalamudStartInfo instance. + [ServiceManager.ServiceConstructor] + public TextureManager(Framework framework, DataManager dataManager, DalamudStartInfo startInfo) + { + this.framework = framework; + this.dataManager = dataManager; + this.startInfo = startInfo; + + this.framework.Update += this.FrameworkOnUpdate; + } + + /// + 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.ItemHighQuality, ClientLanguage? language = null, bool keepAlive = false) + { + 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 this.CreateWrap(path, keepAlive); + + 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 this.CreateWrap(path, keepAlive); + + if (hiRes) + { + // 3. Regular icon, with language, no hi-res + path = FormatIconPath( + iconId, + languageFolder, + false); + if (this.dataManager.FileExists(path)) + return this.CreateWrap(path, keepAlive); + } + + // 4. Regular icon, without language, hi-res + path = FormatIconPath( + iconId, + null, + hiRes); + if (this.dataManager.FileExists(path)) + return this.CreateWrap(path, keepAlive); + + // 4. Regular icon, without language, no hi-res + if (hiRes) + { + path = FormatIconPath( + iconId, + null, + false); + if (this.dataManager.FileExists(path)) + return this.CreateWrap(path, keepAlive); + } + + 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? GetTextureFromGamePath(string path, bool keepAlive) + { + return !this.dataManager.FileExists(path) ? null : this.CreateWrap(path, keepAlive); + } + + /// + public void 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. + /// Info object storing texture metadata. + internal TextureInfo GetInfo(string path, bool refresh = true) + { + 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, this might not be an issue", path); + } + } + + if (refresh && info.KeepAliveCount == 0) + info.LastAccess = DateTime.Now; + + if (info is { Wrap: not null }) + return info; + + if (refresh) + { + byte[]? interceptData = null; + this.InterceptTexDataLoad?.Invoke(path, ref interceptData); + + // TODO: Do we also want to support loading from actual fs here? Doesn't seem to be a big deal, collect interest + + TexFile? file; + if (interceptData != null) + { + // TODO: upstream to lumina + file = Activator.CreateInstance(); + var type = typeof(TexFile); + type.GetProperty("Data", BindingFlags.NonPublic | BindingFlags.Instance)!.GetSetMethod()! + .Invoke(file, new object[] { interceptData }); + type.GetProperty("Reader", BindingFlags.NonPublic | BindingFlags.Instance)!.GetSetMethod()! + .Invoke(file, new object[] { new LuminaBinaryReader(file.Data) }); + file.LoadFile(); + } + else + { + file = this.dataManager.GetFile(path); + } + + var wrap = this.dataManager.GetImGuiTexture(file); + 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, keepAlive); + info.RefCount++; + + if (keepAlive) + info.KeepAliveCount++; + + return new TextureManagerTextureWrap(path, 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.Now - 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); + } + } + } + + /// + /// 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; } + } +} + +/// +/// 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 DataManager dataManager; + private readonly TextureManager textureManager; + + private readonly List trackedTextures = new(); + + /// + /// Initializes a new instance of the class. + /// + /// DataManager instance. + /// TextureManager instance. + public TextureManagerPluginScoped(DataManager dataManager, TextureManager textureManager) + { + this.dataManager = dataManager; + 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 IDalamudTextureWrap? GetTextureFromGamePath(string path, bool keepAlive = false) + { + var wrap = this.textureManager.GetTextureFromGamePath(path, keepAlive); + if (wrap == null) + return null; + + this.trackedTextures.Add(wrap); + return wrap; + } + + /// + public IDalamudTextureWrap GetTexture(TexFile file) + { + return this.dataManager.GetImGuiTexture(file) as DalamudTextureWrap ?? throw new ArgumentException("Could not load texture"); + } + + /// + 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; + + private int? width; + private int? height; + + /// + /// Initializes a new instance of the class. + /// + /// The path to the texture. + /// Keep alive or not. + /// Manager that we obtained this from. + internal TextureManagerTextureWrap(string path, bool keepAlive, TextureManager manager) + { + this.path = path; + this.keepAlive = keepAlive; + this.manager = manager; + } + + /// + public IntPtr ImGuiHandle => this.manager.GetInfo(this.path).Wrap!.ImGuiHandle; + + /// + public int Width => this.width ??= this.manager.GetInfo(this.path).Wrap!.Width; + + /// + public int Height => this.height ??= this.manager.GetInfo(this.path).Wrap!.Height; + + /// + /// 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..d5335d170 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Numerics; using Dalamud.Data; +using Dalamud.Plugin.Services; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -14,13 +16,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 +43,75 @@ 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.GetTextureFromGamePath(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/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index fa8c5bf43..8e36905f9 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -1,4 +1,5 @@ -using System.Collections.ObjectModel; +using System; +using System.Collections.ObjectModel; using ImGuiScene; using Lumina; @@ -91,6 +92,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetIcon(uint iconId, bool highResolution = false); /// @@ -100,6 +102,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false); /// @@ -109,6 +112,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false); /// @@ -117,6 +121,7 @@ public interface IDataManager /// The icon ID. /// Return the high resolution version. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false); /// @@ -125,6 +130,7 @@ public interface IDataManager /// A value indicating whether the icon should be HQ. /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetIcon(bool isHq, uint iconId); /// @@ -132,6 +138,7 @@ public interface IDataManager /// /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TexFile? GetHqIcon(uint iconId); /// @@ -139,6 +146,7 @@ public interface IDataManager /// /// The Lumina . /// A that can be used to draw the texture. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTexture(TexFile? tex); /// @@ -146,6 +154,7 @@ public interface IDataManager /// /// The internal path to the texture. /// A that can be used to draw the texture. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTexture(string path); /// @@ -154,6 +163,7 @@ public interface IDataManager /// A value indicating whether the icon should be HQ. /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId); /// @@ -162,6 +172,7 @@ public interface IDataManager /// The requested language. /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager instead")] public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId); /// @@ -170,6 +181,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 ITextureManager instead")] public TextureWrap? GetImGuiTextureIcon(string type, uint iconId); /// @@ -177,5 +189,6 @@ public interface IDataManager /// /// The icon ID. /// The containing the icon. + [Obsolete("Use ITextureManager 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..56ed310e8 --- /dev/null +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -0,0 +1,69 @@ +using System; + +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 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? GetTextureFromGamePath(string path, 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..fcc27f8e6 --- /dev/null +++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs @@ -0,0 +1,21 @@ +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 to the texture that is to be loaded. + /// The texture data. Null by default, assign something if you wish to replace the data from the game dats. + public delegate void TextureDataInterceptorDelegate(string path, ref byte[]? data); + + /// + /// Event that will be called once Dalamud wants to load texture data. + /// If you have data that should replace the data from the game dats, assign it to the + /// data argument. + /// + public event TextureDataInterceptorDelegate? InterceptTexDataLoad; +} From 8df9821f0e69814aa3b836363d81b5695376f7c6 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 18:43:41 +0200 Subject: [PATCH 05/19] fix: use HiRes instead of ItemHighQuality by default on TextureManager --- Dalamud/Interface/Internal/TextureManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 05b10263b..6429e530d 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -75,7 +75,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// 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.ItemHighQuality, ClientLanguage? language = null, bool keepAlive = false) + public TextureManagerTextureWrap? GetIcon(uint iconId, ITextureProvider.IconFlags flags = ITextureProvider.IconFlags.HiRes, ClientLanguage? language = null, bool keepAlive = false) { var hiRes = flags.HasFlag(ITextureProvider.IconFlags.HiRes); From 22a6261c98ce9952f69e52006bfc3428d0640ad1 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 18:46:44 +0200 Subject: [PATCH 06/19] feat: add support to load textures from files --- Dalamud/Interface/Internal/TextureManager.cs | 109 ++++++++++++++---- .../Windows/Data/Widgets/TexWidget.cs | 17 ++- Dalamud/Plugin/Services/ITextureProvider.cs | 14 ++- .../Services/ITextureSubstitutionProvider.cs | 9 +- 4 files changed, 116 insertions(+), 33 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 6429e530d..22529d8c4 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; -using System.Reflection; using Dalamud.Data; using Dalamud.Game; @@ -11,7 +11,6 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using ImGuiScene; -using Lumina.Data; using Lumina.Data.Files; namespace Dalamud.Interface.Internal; @@ -36,6 +35,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP private readonly Framework framework; private readonly DataManager dataManager; + private readonly InterfaceManager im; private readonly DalamudStartInfo startInfo; private readonly Dictionary activeTextures = new(); @@ -45,12 +45,14 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// /// Framework instance. /// DataManager instance. + /// InterfaceManager instance. /// DalamudStartInfo instance. [ServiceManager.ServiceConstructor] - public TextureManager(Framework framework, DataManager dataManager, DalamudStartInfo startInfo) + 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; @@ -145,10 +147,30 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// 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? GetTextureFromGamePath(string path, bool keepAlive) + public TextureManagerTextureWrap? GetTextureFromGame(string path, bool keepAlive) { + 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) + { + ArgumentNullException.ThrowIfNull(file); + return !file.Exists ? null : this.CreateWrap(file.FullName, keepAlive); + } /// public void Dispose() @@ -185,7 +207,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP lock (this.activeTextures) { if (!this.activeTextures.TryAdd(path, info)) - Log.Warning("Texture {Path} tracked twice, this might not be an issue", path); + Log.Warning("Texture {Path} tracked twice", path); } } @@ -197,29 +219,53 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP if (refresh) { - byte[]? interceptData = null; - this.InterceptTexDataLoad?.Invoke(path, ref interceptData); - - // TODO: Do we also want to support loading from actual fs here? Doesn't seem to be a big deal, collect interest + if (!this.im.IsReady) + throw new InvalidOperationException("Cannot create textures before scene is ready"); - TexFile? file; - if (interceptData != null) + string? interceptPath = null; + this.InterceptTexDataLoad?.Invoke(path, ref interceptPath); + + if (interceptPath != null) { - // TODO: upstream to lumina - file = Activator.CreateInstance(); - var type = typeof(TexFile); - type.GetProperty("Data", BindingFlags.NonPublic | BindingFlags.Instance)!.GetSetMethod()! - .Invoke(file, new object[] { interceptData }); - type.GetProperty("Reader", BindingFlags.NonPublic | BindingFlags.Instance)!.GetSetMethod()! - .Invoke(file, new object[] { new LuminaBinaryReader(file.Data) }); - file.LoadFile(); + Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath); + path = interceptPath; + } + + TextureWrap? wrap; + + // TODO: The actual loading here may fail due to circumstances outside of our control. + // We should create a fallback texture and return it instead, so that plugins don't crash. + + // 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.dataManager.GetImGuiTexture(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 { - file = this.dataManager.GetFile(path); + // Load regularly from dats + var file = this.dataManager.GetFile(path); + wrap = this.dataManager.GetImGuiTexture(file); + Log.Verbose("Texture {Path} loaded from SqPack", path); } - - var wrap = this.dataManager.GetImGuiTexture(file); + + if (wrap == null) + throw new Exception("Could not create texture"); + info.Wrap = wrap; } @@ -377,9 +423,24 @@ internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDis } /// - public IDalamudTextureWrap? GetTextureFromGamePath(string path, bool keepAlive = false) + public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) { - var wrap = this.textureManager.GetTextureFromGamePath(path, keepAlive); + 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; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs index d5335d170..5ad5868c3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TexWidget.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; +using System.IO; using System.Numerics; -using Dalamud.Data; using Dalamud.Plugin.Services; -using Dalamud.Utility; using ImGuiNET; using ImGuiScene; using Serilog; @@ -74,7 +73,19 @@ internal class TexWidget : IDataWindowWidget { try { - this.addedTextures.Add(texManager.GetTextureFromGamePath(this.inputTexPath, this.keepAlive)); + 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) { diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index 56ed310e8..b2ffbb5cd 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using Dalamud.Interface.Internal; using Lumina.Data.Files; @@ -58,7 +59,18 @@ public interface ITextureProvider /// 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? GetTextureFromGamePath(string path, bool keepAlive = false); + 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. diff --git a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs index fcc27f8e6..90be71adb 100644 --- a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs +++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs @@ -7,15 +7,14 @@ 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 texture data. Null by default, assign something if you wish to replace the data from the game dats. - public delegate void TextureDataInterceptorDelegate(string path, ref byte[]? data); - + /// 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. - /// If you have data that should replace the data from the game dats, assign it to the - /// data argument. /// public event TextureDataInterceptorDelegate? InterceptTexDataLoad; } From 7a6916c732e4241978a8664d9ef2b53e50413017 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 18:48:43 +0200 Subject: [PATCH 07/19] feat: add fallback texture, persist size --- Dalamud/Interface/Internal/TextureManager.cs | 106 +++++++++++++------ 1 file changed, 71 insertions(+), 35 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 22529d8c4..9cff8f54d 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Numerics; using Dalamud.Data; using Dalamud.Game; @@ -40,6 +41,8 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP private readonly Dictionary activeTextures = new(); + private TextureWrap? fallbackTextureWrap; + /// /// Initializes a new instance of the class. /// @@ -56,6 +59,8 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP this.startInfo = startInfo; this.framework.Update += this.FrameworkOnUpdate; + + Service.GetAsync().ContinueWith(_ => this.CreateFallbackTexture()); } /// @@ -175,6 +180,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// public void Dispose() { + this.fallbackTextureWrap?.Dispose(); this.framework.Update -= this.FrameworkOnUpdate; Log.Verbose("Disposing {Num} left behind textures."); @@ -192,8 +198,12 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// /// 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) + internal TextureInfo GetInfo(string path, bool refresh = true, bool rethrow = false) { TextureInfo? info; lock (this.activeTextures) @@ -232,39 +242,53 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP } TextureWrap? wrap; - - // TODO: The actual loading here may fail due to circumstances outside of our control. - // We should create a fallback texture and return it instead, so that plugins don't crash. - - // 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)) + try { - if (Path.GetExtension(path) == ".tex") + // 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)) { - // Attempt to load via Lumina - var file = this.dataManager.GameData.GetFileFromDisk(path); - wrap = this.dataManager.GetImGuiTexture(file); - Log.Verbose("Texture {Path} loaded FS via Lumina", path); + if (Path.GetExtension(path) == ".tex") + { + // Attempt to load via Lumina + var file = this.dataManager.GameData.GetFileFromDisk(path); + wrap = this.dataManager.GetImGuiTexture(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 { - // Attempt to load image - wrap = this.im.LoadImage(path); - Log.Verbose("Texture {Path} loaded FS via LoadImage", path); + // Load regularly from dats + var file = this.dataManager.GetFile(path); + wrap = this.dataManager.GetImGuiTexture(file); + Log.Verbose("Texture {Path} loaded from SqPack", path); } - } - else - { - // Load regularly from dats - var file = this.dataManager.GetFile(path); - wrap = this.dataManager.GetImGuiTexture(file); - Log.Verbose("Texture {Path} loaded from SqPack", path); - } + + if (wrap == null) + throw new Exception("Could not create texture"); - if (wrap == null) - throw new Exception("Could not create texture"); + info.Extents = new Vector2(wrap.Width, wrap.Height); + } + 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; } @@ -306,13 +330,13 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP { // 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, keepAlive); + var info = this.GetInfo(path, rethrow: true); info.RefCount++; if (keepAlive) info.KeepAliveCount++; - return new TextureManagerTextureWrap(path, keepAlive, this); + return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this); } private void FrameworkOnUpdate(Framework fw) @@ -353,6 +377,13 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP } } + 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. /// @@ -377,6 +408,11 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// 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; } } } @@ -474,30 +510,30 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap private readonly string path; private readonly bool keepAlive; - private int? width; - private int? height; - /// /// 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, bool keepAlive, TextureManager manager) + 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 => this.width ??= this.manager.GetInfo(this.path).Wrap!.Width; + public int Width { get; private set; } /// - public int Height => this.height ??= this.manager.GetInfo(this.path).Wrap!.Height; + public int Height { get; private set; } /// /// Gets a value indicating whether or not this wrap has already been disposed. From 3d0d5e9bc083a1b22040a2abe8f2cecd5419b9b9 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 18:51:01 +0200 Subject: [PATCH 08/19] feat: deprecate all DataManager texture funcs --- Dalamud/Data/DataManager.cs | 19 +++++++++-- Dalamud/Interface/Internal/TextureManager.cs | 34 +++++++++++++------- Dalamud/Plugin/Services/IDataManager.cs | 24 +++++++------- 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 407a1b0da..8c0a33081 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -185,15 +185,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; @@ -206,11 +208,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 @@ -231,11 +234,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; @@ -257,14 +261,17 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager } /// + [Obsolete("Use ITextureProvider instead")] public TexFile? GetHqIcon(uint iconId) => this.GetIcon(true, iconId); /// + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTexture(TexFile? tex) => tex == null ? null : Service.Get().LoadImageRaw(tex.GetRgbaImageData(), tex.Header.Width, tex.Header.Height, 4); /// + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTexture(string path) => this.GetImGuiTexture(this.GetFile(path)); @@ -274,26 +281,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/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 9cff8f54d..ba0a7045d 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -176,7 +176,21 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP 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); + +#pragma warning disable CS0618 + return this.dataManager.GetImGuiTexture(file) as IDalamudTextureWrap; +#pragma warning restore CS0618 + } + /// public void Dispose() { @@ -253,7 +267,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP { // Attempt to load via Lumina var file = this.dataManager.GameData.GetFileFromDisk(path); - wrap = this.dataManager.GetImGuiTexture(file); + wrap = this.GetTexture(file); Log.Verbose("Texture {Path} loaded FS via Lumina", path); } else @@ -267,7 +281,10 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP { // Load regularly from dats var file = this.dataManager.GetFile(path); - wrap = this.dataManager.GetImGuiTexture(file); + if (file == null) + throw new Exception("Could not load TexFile from dat."); + + wrap = this.GetTexture(file); Log.Verbose("Texture {Path} loaded from SqPack", path); } @@ -427,7 +444,6 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP #pragma warning restore SA1015 internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDisposable { - private readonly DataManager dataManager; private readonly TextureManager textureManager; private readonly List trackedTextures = new(); @@ -435,11 +451,9 @@ internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDis /// /// Initializes a new instance of the class. /// - /// DataManager instance. /// TextureManager instance. - public TextureManagerPluginScoped(DataManager dataManager, TextureManager textureManager) + public TextureManagerPluginScoped(TextureManager textureManager) { - this.dataManager = dataManager; this.textureManager = textureManager; } @@ -485,10 +499,8 @@ internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDis } /// - public IDalamudTextureWrap GetTexture(TexFile file) - { - return this.dataManager.GetImGuiTexture(file) as DalamudTextureWrap ?? throw new ArgumentException("Could not load texture"); - } + public IDalamudTextureWrap? GetTexture(TexFile file) + => this.textureManager.GetTexture(file); /// public void Dispose() diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index 8e36905f9..4f08cf618 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -92,7 +92,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(uint iconId, bool highResolution = false); /// @@ -102,7 +102,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(ClientLanguage iconLanguage, uint iconId, bool highResolution = false); /// @@ -112,7 +112,7 @@ public interface IDataManager /// The icon ID. /// Return high resolution version. /// The containing the icon. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(string? type, uint iconId, bool highResolution = false); /// @@ -121,7 +121,7 @@ public interface IDataManager /// The icon ID. /// Return the high resolution version. /// The containing the icon. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(uint iconId, bool highResolution = false); /// @@ -130,7 +130,7 @@ public interface IDataManager /// A value indicating whether the icon should be HQ. /// The icon ID. /// The containing the icon. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TexFile? GetIcon(bool isHq, uint iconId); /// @@ -138,7 +138,7 @@ public interface IDataManager /// /// The icon ID. /// The containing the icon. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TexFile? GetHqIcon(uint iconId); /// @@ -146,7 +146,7 @@ public interface IDataManager /// /// The Lumina . /// A that can be used to draw the texture. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTexture(TexFile? tex); /// @@ -154,7 +154,7 @@ public interface IDataManager /// /// The internal path to the texture. /// A that can be used to draw the texture. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTexture(string path); /// @@ -163,7 +163,7 @@ public interface IDataManager /// A value indicating whether the icon should be HQ. /// The icon ID. /// The containing the icon. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(bool isHq, uint iconId); /// @@ -172,7 +172,7 @@ public interface IDataManager /// The requested language. /// The icon ID. /// The containing the icon. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(ClientLanguage iconLanguage, uint iconId); /// @@ -181,7 +181,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 ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureIcon(string type, uint iconId); /// @@ -189,6 +189,6 @@ public interface IDataManager /// /// The icon ID. /// The containing the icon. - [Obsolete("Use ITextureManager instead")] + [Obsolete("Use ITextureProvider instead")] public TextureWrap? GetImGuiTextureHqIcon(uint iconId); } From 2a9409a242ce3839519e73778c0e6279b6cafc9f Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 18:51:05 +0200 Subject: [PATCH 09/19] fix: keepAlive defaults --- Dalamud/Interface/Internal/TextureManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index ba0a7045d..ed7065456 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -152,7 +152,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// 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) + public TextureManagerTextureWrap? GetTextureFromGame(string path, bool keepAlive = false) { ArgumentException.ThrowIfNullOrEmpty(path); @@ -171,7 +171,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// 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) + public TextureManagerTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false) { ArgumentNullException.ThrowIfNull(file); return !file.Exists ? null : this.CreateWrap(file.FullName, keepAlive); From 1df2ccfb1adef1421ceeee98374a97e1e6498366 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 18:51:23 +0200 Subject: [PATCH 10/19] fix: throw if IM is not ready --- Dalamud/Interface/Internal/TextureManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index ed7065456..8f6f6b474 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -185,6 +185,9 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP 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; From e6ef219b80f2fb0adf5e8ca820fc7115ccbe1dd1 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 18:51:26 +0200 Subject: [PATCH 11/19] feat: warn if texture changes size between reloads --- Dalamud/Interface/Internal/TextureManager.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 8f6f6b474..b61ad74da 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -294,7 +294,12 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP if (wrap == null) throw new Exception("Could not create texture"); - info.Extents = new Vector2(wrap.Width, wrap.Height); + // 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) { From 44ca7ce8487590289798fdca0916d488b9220545 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 18:51:27 +0200 Subject: [PATCH 12/19] fix: dispose textures correctly in ReShade mode --- Dalamud/Interface/Internal/InterfaceManager.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6bb45b325..d53a29f25 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -609,12 +609,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 +632,6 @@ internal class InterfaceManager : IDisposable, IServiceType this.deferredDisposeTextures.Clear(); } - - return this.presentHook.Original(swapChain, syncInterval, presentFlags); } [MethodImpl(MethodImplOptions.AggressiveInlining)] From c78f6e1fe5bebbf25150ae8b1e648aa417fe2a0c Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 22:58:21 +0200 Subject: [PATCH 13/19] fix: use UtcNow to date textures --- Dalamud/Interface/Internal/TextureManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index b61ad74da..2f8ac27d9 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -239,7 +239,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP } if (refresh && info.KeepAliveCount == 0) - info.LastAccess = DateTime.Now; + info.LastAccess = DateTime.UtcNow; if (info is { Wrap: not null }) return info; @@ -387,7 +387,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP if (texInfo.Value.KeepAliveCount > 0 || texInfo.Value.Wrap == null) continue; - if (DateTime.Now - texInfo.Value.LastAccess > TimeSpan.FromMilliseconds(MillisecondsEvictionTime)) + if (DateTime.UtcNow - texInfo.Value.LastAccess > TimeSpan.FromMilliseconds(MillisecondsEvictionTime)) { Log.Verbose("Evicting {Path} since too old", texInfo.Key); texInfo.Value.Wrap.Dispose(); From ee181c9a896fcc7bc32e063145030fdb4ced00a4 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 2 Aug 2023 23:03:58 +0200 Subject: [PATCH 14/19] feat(DI): support scoped services with interfaces --- Dalamud/IoC/Internal/ServiceContainer.cs | 98 +++++++++++++----------- Dalamud/ServiceManager.cs | 11 ++- 2 files changed, 64 insertions(+), 45 deletions(-) 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/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), From a47ea5445207fe6520e8f742900954d9d5f4aa50 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 3 Aug 2023 12:41:27 +0200 Subject: [PATCH 15/19] chore: disable non-functional xivfix --- Dalamud.Injector/EntryPoint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 5a4be696c53563b8cc785b1f3efdd803faaf0e4a Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Thu, 3 Aug 2023 12:45:03 +0200 Subject: [PATCH 16/19] [master] Update ClientStructs (#1330) Co-authored-by: github-actions[bot] --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 8962a47b9..782d73171 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 8962a47b95f96bec53e58680bd9d1e7f38610d40 +Subproject commit 782d7317176f232d7108b2b3a4cb75de67fc3a8a From b1211fe5d1144719e5d472aaf547ec6a268c24fa Mon Sep 17 00:00:00 2001 From: srkizer Date: Thu, 3 Aug 2023 19:50:17 +0900 Subject: [PATCH 17/19] DataManager.GetImGuiTexture: skip converting to bgra8888 when possible (#1333) --- Dalamud/Data/DataManager.cs | 27 ++++++++- .../Interface/Internal/InterfaceManager.cs | 60 +++++++++++++++++++ Dalamud/Plugin/Services/IDataManager.cs | 2 + 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index 407a1b0da..f450175f7 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; @@ -261,9 +264,31 @@ public sealed class DataManager : IDisposable, IServiceType, IDataManager => this.GetIcon(true, iconId); /// + [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); + } + /// public TextureWrap? GetImGuiTexture(string path) => this.GetImGuiTexture(this.GetFile(path)); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 6bb45b325..42402ed97 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 /// diff --git a/Dalamud/Plugin/Services/IDataManager.cs b/Dalamud/Plugin/Services/IDataManager.cs index fa8c5bf43..e79a58fd5 100644 --- a/Dalamud/Plugin/Services/IDataManager.cs +++ b/Dalamud/Plugin/Services/IDataManager.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; using ImGuiScene; using Lumina; @@ -139,6 +140,7 @@ public interface IDataManager /// /// The Lumina . /// A that can be used to draw the texture. + [return: NotNullIfNotNull(nameof(tex))] public TextureWrap? GetImGuiTexture(TexFile? tex); /// From 389ff91097b8f67330e1d830ca6ebdb6e74d84b5 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 3 Aug 2023 12:54:03 +0200 Subject: [PATCH 18/19] upgrade CS --- lib/FFXIVClientStructs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/FFXIVClientStructs b/lib/FFXIVClientStructs index 93db21d9b..782d73171 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 93db21d9b6fb5cc671cd25c79a6ac933f3ca6710 +Subproject commit 782d7317176f232d7108b2b3a4cb75de67fc3a8a From 8df154c1a9da3ec2593f67387a96e92bc2c47942 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 3 Aug 2023 19:18:38 +0200 Subject: [PATCH 19/19] feat: add ITextureProvider.GetIconPath() function to allow for icon path lookups by plugins --- Dalamud/Interface/Internal/TextureManager.cs | 33 +++++++++++++++++--- Dalamud/Plugin/Services/ITextureProvider.cs | 15 +++++++++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 2f8ac27d9..4b2f1f362 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -83,6 +83,25 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// 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); @@ -92,7 +111,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP flags.HasFlag(ITextureProvider.IconFlags.ItemHighQuality) ? "hq/" : string.Empty, hiRes); if (this.dataManager.FileExists(path)) - return this.CreateWrap(path, keepAlive); + return path; language ??= this.startInfo.Language; var languageFolder = language switch @@ -110,7 +129,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP languageFolder, hiRes); if (this.dataManager.FileExists(path)) - return this.CreateWrap(path, keepAlive); + return path; if (hiRes) { @@ -120,7 +139,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP languageFolder, false); if (this.dataManager.FileExists(path)) - return this.CreateWrap(path, keepAlive); + return path; } // 4. Regular icon, without language, hi-res @@ -129,7 +148,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP null, hiRes); if (this.dataManager.FileExists(path)) - return this.CreateWrap(path, keepAlive); + return path; // 4. Regular icon, without language, no hi-res if (hiRes) @@ -139,7 +158,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP null, false); if (this.dataManager.FileExists(path)) - return this.CreateWrap(path, keepAlive); + return path; } return null; @@ -480,6 +499,10 @@ internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDis 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) { diff --git a/Dalamud/Plugin/Services/ITextureProvider.cs b/Dalamud/Plugin/Services/ITextureProvider.cs index b2ffbb5cd..091b2ed67 100644 --- a/Dalamud/Plugin/Services/ITextureProvider.cs +++ b/Dalamud/Plugin/Services/ITextureProvider.cs @@ -52,6 +52,21 @@ public interface ITextureProvider /// 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.