From b75b30c03d04cf815c78cefcc2ff56f9499b3bdb Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 30 Jul 2023 22:11:00 +0200 Subject: [PATCH 01/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] 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/34] [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/34] 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/34] 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/34] 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. From 0d20ff25e431ee53b52becd0057b513aea46e4a2 Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Sat, 5 Aug 2023 13:11:24 +0200 Subject: [PATCH 20/34] fix: missing ## for id in DisabledButton --- Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs index 181bbbfd7..907ad0aeb 100644 --- a/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs +++ b/Dalamud/Interface/Components/ImGuiComponents.DisabledButton.cs @@ -25,7 +25,7 @@ public static partial class ImGuiComponents var text = icon.ToIconString(); if (id.HasValue) - text = $"{text}{id}"; + text = $"{text}##{id}"; var button = DisabledButton(text, defaultColor, activeColor, hoveredColor, alphaMult); From cd3d23e0fd6f1a477d245d86f7ccb82aa9db0530 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Tue, 8 Aug 2023 16:16:41 +0200 Subject: [PATCH 21/34] Update ClientStructs (#1339) 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 782d73171..155511a61 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 782d7317176f232d7108b2b3a4cb75de67fc3a8a +Subproject commit 155511a61559c529719d3810eaf8fb9336482878 From b4c24305f23140194dc198355ec6e6060515ade6 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 18:42:23 +0200 Subject: [PATCH 22/34] chore: add pre-filled FTUE level to config, so that existing users will not get a FTUE once we add it --- Dalamud/Configuration/Internal/DalamudConfiguration.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 39c53c3cb..ac410527c 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -69,6 +69,12 @@ internal sealed class DalamudConfiguration : IServiceType /// public string LastVersion { get; set; } = null; + /// + /// Gets or sets a value indicating the last seen FTUE version. + /// Unused for now, added to prevent existing users from seeing level 0 FTUE. + /// + public int SeenFtueLevel { get; set; } = 1; + /// /// Gets or sets the last loaded Dalamud version. /// From 426e7f53efe999a05ec8a5211ef725a4357151da Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 18:42:52 +0200 Subject: [PATCH 23/34] build: 7.10.0.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 12068a35c..494c0f297 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.9.0.0 + 7.10.0.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From ebc4baca585abadcd326292d379a7ba0408537a1 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 21:36:46 +0200 Subject: [PATCH 24/34] fix: return DalamudTextureWrap from LoadImageFromDxgiFormat(), cast explicitly --- Dalamud/Interface/Internal/InterfaceManager.cs | 4 ++-- Dalamud/Interface/Internal/TextureManager.cs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 841511f55..f46c7272d 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -325,7 +325,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// 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) + public IDalamudTextureWrap LoadImageFromDxgiFormat(Span data, int pitch, int width, int height, Format dxgiFormat) { if (this.scene == null) throw new InvalidOperationException("Scene isn't ready."); @@ -360,7 +360,7 @@ internal class InterfaceManager : IDisposable, IServiceType } // no sampler for now because the ImGui implementation we copied doesn't allow for changing it - return new D3DTextureWrap(resView, width, height); + return new DalamudTextureWrap(new D3DTextureWrap(resView, width, height)); } #nullable restore diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 4b2f1f362..a7c5a005e 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -209,7 +209,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP throw new InvalidOperationException("Cannot create textures before scene is ready"); #pragma warning disable CS0618 - return this.dataManager.GetImGuiTexture(file) as IDalamudTextureWrap; + return (IDalamudTextureWrap)this.dataManager.GetImGuiTexture(file); #pragma warning restore CS0618 } @@ -332,6 +332,10 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP // and we can't do anything about it. Return a dummy texture so that the plugin still // has something to draw. wrap = this.fallbackTextureWrap; + + // Prevent divide-by-zero + if (info.Extents == Vector2.Zero) + info.Extents = Vector2.One; } info.Wrap = wrap; From dc95d7e8de55b5c1bbcca915c83385c757118a33 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 21:39:01 +0200 Subject: [PATCH 25/34] build: 7.10.1.0 --- Dalamud/Dalamud.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Dalamud.csproj b/Dalamud/Dalamud.csproj index 494c0f297..e2da1a057 100644 --- a/Dalamud/Dalamud.csproj +++ b/Dalamud/Dalamud.csproj @@ -8,7 +8,7 @@ - 7.10.0.0 + 7.10.1.0 XIV Launcher addon framework $(DalamudVersion) $(DalamudVersion) From 39e389080ce515ecc85511f40beeeec247de6522 Mon Sep 17 00:00:00 2001 From: goat Date: Tue, 8 Aug 2023 22:04:30 +0200 Subject: [PATCH 26/34] chore: throw if TextureManagerTextureWrap is used beyond disposal --- Dalamud/Interface/Internal/TextureManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index a7c5a005e..717bd8081 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -574,7 +574,9 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap } /// - public IntPtr ImGuiHandle => this.manager.GetInfo(this.path).Wrap!.ImGuiHandle; + public IntPtr ImGuiHandle => !this.IsDisposed ? + this.manager.GetInfo(this.path).Wrap!.ImGuiHandle : + throw new InvalidOperationException("Texture already disposed. You may not render it."); /// public int Width { get; private set; } From d1fad810cfe74ca320bdd2ad3902fe04bc5ed95c Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 9 Aug 2023 01:42:05 +0200 Subject: [PATCH 27/34] fix: set initial refcount inside lock to prevent race condition with cleanup code when loading from a task --- Dalamud/Interface/Internal/TextureManager.cs | 46 +++++++++++++------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 717bd8081..294edbb0a 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -239,24 +239,33 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// 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) + internal TextureInfo? GetInfo(string path, bool refresh = true, bool rethrow = false) { TextureInfo? info; lock (this.activeTextures) { - this.activeTextures.TryGetValue(path, out info); + if (!this.activeTextures.TryGetValue(path, out info)) + { + Debug.Assert(rethrow, "This should never run when getting outside of creator"); + + if (!refresh) + return null; + + // NOTE: We need to init the refcount here while locking the collection! + // Otherwise, if this is loaded from a task, cleanup might already try to delete it + // before it can be increased. + info = new TextureInfo + { + RefCount = 1, + }; + + this.activeTextures.Add(path, info); + } + + if (info == null) + throw new Exception("null info in activeTextures"); } - 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; @@ -353,6 +362,14 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP internal void NotifyTextureDisposed(string path, bool keepAlive) { var info = this.GetInfo(path, false); + + // This texture was already disposed + if (info == null) + { + Log.Warning("Disposing unknown texture {Path}", path); + return; + } + info.RefCount--; if (keepAlive) @@ -378,8 +395,7 @@ 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, rethrow: true); - info.RefCount++; + var info = this.GetInfo(path, rethrow: true)!; if (keepAlive) info.KeepAliveCount++; @@ -575,7 +591,7 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap /// public IntPtr ImGuiHandle => !this.IsDisposed ? - this.manager.GetInfo(this.path).Wrap!.ImGuiHandle : + this.manager.GetInfo(this.path)!.Wrap!.ImGuiHandle : throw new InvalidOperationException("Texture already disposed. You may not render it."); /// From 5cfddb23dd1b79f6470cb0fdb5b0437dc0ed25e5 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 9 Aug 2023 01:46:52 +0200 Subject: [PATCH 28/34] fix: actually increase refcount if we have multiple handles --- Dalamud/Interface/Internal/TextureManager.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 294edbb0a..66a2cf731 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -250,20 +250,18 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP if (!refresh) return null; - - // NOTE: We need to init the refcount here while locking the collection! - // Otherwise, if this is loaded from a task, cleanup might already try to delete it - // before it can be increased. - info = new TextureInfo - { - RefCount = 1, - }; + info = new TextureInfo(); this.activeTextures.Add(path, info); } if (info == null) throw new Exception("null info in activeTextures"); + + // NOTE: We need to increase the refcount here while locking the collection! + // Otherwise, if this is loaded from a task, cleanup might already try to delete it + // before it can be increased. + info.RefCount++; } if (refresh && info.KeepAliveCount == 0) From 8a300cc98e96ca569c4f969c1ce734a5c8287f91 Mon Sep 17 00:00:00 2001 From: bleatbot <106497096+bleatbot@users.noreply.github.com> Date: Wed, 9 Aug 2023 20:19:15 +0200 Subject: [PATCH 29/34] Update ClientStructs (#1344) 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 155511a61..9b2eab0f2 160000 --- a/lib/FFXIVClientStructs +++ b/lib/FFXIVClientStructs @@ -1 +1 @@ -Subproject commit 155511a61559c529719d3810eaf8fb9336482878 +Subproject commit 9b2eab0f212030c062427b307b96118881d36b99 From 24ad2d4c8b4ab5ba4effd03253752d58dea0e124 Mon Sep 17 00:00:00 2001 From: goat Date: Wed, 9 Aug 2023 21:22:29 +0200 Subject: [PATCH 30/34] chore: simplify refcounting logic, more concurrency fixes --- Dalamud/Interface/Internal/TextureManager.cs | 185 +++++++++---------- 1 file changed, 90 insertions(+), 95 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 66a2cf731..905cac55a 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -233,13 +233,12 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// 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) + internal TextureInfo GetInfo(string path, bool rethrow = false) { TextureInfo? info; lock (this.activeTextures) @@ -248,106 +247,94 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP { Debug.Assert(rethrow, "This should never run when getting outside of creator"); - if (!refresh) - return null; - info = new TextureInfo(); this.activeTextures.Add(path, info); } if (info == null) throw new Exception("null info in activeTextures"); - - // NOTE: We need to increase the refcount here while locking the collection! - // Otherwise, if this is loaded from a task, cleanup might already try to delete it - // before it can be increased. - info.RefCount++; } - if (refresh && info.KeepAliveCount == 0) + if (info.KeepAliveCount == 0) info.LastAccess = DateTime.UtcNow; if (info is { Wrap: not null }) return info; - if (refresh) - { - if (!this.im.IsReady) + if (!this.im.IsReady) throw new InvalidOperationException("Cannot create textures before scene is ready"); - string? interceptPath = null; - this.InterceptTexDataLoad?.Invoke(path, ref interceptPath); + string? interceptPath = null; + this.InterceptTexDataLoad?.Invoke(path, ref interceptPath); - if (interceptPath != null) - { - Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath); - path = interceptPath; - } + if (interceptPath != null) + { + Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath); + path = interceptPath; + } - TextureWrap? wrap; - try + 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)) { - // 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") { - 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); - } + // 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 { - // 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); + // Attempt to load image + wrap = this.im.LoadImage(path); + Log.Verbose("Texture {Path} loaded FS via LoadImage", 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) + else { - 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; - - // Prevent divide-by-zero - if (info.Extents == Vector2.Zero) - info.Extents = Vector2.One; + // 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"); - info.Wrap = wrap; + // 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; + + // Prevent divide-by-zero + if (info.Extents == Vector2.Zero) + info.Extents = Vector2.One; } + info.Wrap = wrap; return info; } @@ -359,23 +346,23 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP /// Whether or not this handle was created in keep-alive mode. internal void NotifyTextureDisposed(string path, bool keepAlive) { - var info = this.GetInfo(path, false); - - // This texture was already disposed - if (info == null) + lock (this.activeTextures) { - Log.Warning("Disposing unknown texture {Path}", path); - return; + if (!this.activeTextures.TryGetValue(path, out var info)) + { + Log.Warning("Disposing texture that didn't exist: {Path}", path); + return; + } + + 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; } - - 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) @@ -391,14 +378,22 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP 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)!; + lock (this.activeTextures) + { + // 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); - if (keepAlive) - info.KeepAliveCount++; + // We need to increase the refcounts here while locking the collection! + // Otherwise, if this is loaded from a task, cleanup might already try to delete it + // before it can be increased. + info.RefCount++; - return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this); + if (keepAlive) + info.KeepAliveCount++; + + return new TextureManagerTextureWrap(path, info.Extents, keepAlive, this); + } } private void FrameworkOnUpdate(Framework fw) @@ -589,7 +584,7 @@ internal class TextureManagerTextureWrap : IDalamudTextureWrap /// public IntPtr ImGuiHandle => !this.IsDisposed ? - this.manager.GetInfo(this.path)!.Wrap!.ImGuiHandle : + this.manager.GetInfo(this.path).Wrap!.ImGuiHandle : throw new InvalidOperationException("Texture already disposed. You may not render it."); /// From 4c15df80b9cf636d7e08c106292818e704d495f0 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 10 Aug 2023 19:11:48 +0200 Subject: [PATCH 31/34] feat: add ITextureSubstitutionProvider.InvalidatePaths() --- Dalamud/Interface/Internal/TextureManager.cs | 45 +++++++++++++++---- .../Services/ITextureSubstitutionProvider.cs | 19 +++++++- 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 905cac55a..ad7c0f2f8 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -213,6 +213,39 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP #pragma warning restore CS0618 } + /// + public string GetSubstitutedPath(string originalPath) + { + if (this.InterceptTexDataLoad == null) + return originalPath; + + string? interceptPath = null; + this.InterceptTexDataLoad.Invoke(originalPath, ref interceptPath); + + if (interceptPath != null) + { + Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", originalPath, interceptPath); + return interceptPath; + } + + return originalPath; + } + + /// + public void InvalidatePaths(IEnumerable paths) + { + lock (this.activeTextures) + { + foreach (var path in paths) + { + if (!this.activeTextures.TryGetValue(path, out var info) || info == null) + continue; + + info.Wrap = null; + } + } + } + /// public void Dispose() { @@ -263,16 +296,10 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP 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; - } + // Substitute the path here for loading, instead of when getting the respective TextureInfo + path = this.GetSubstitutedPath(path); + TextureWrap? wrap; try { diff --git a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs index 90be71adb..3ddd7d13e 100644 --- a/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs +++ b/Dalamud/Plugin/Services/ITextureSubstitutionProvider.cs @@ -1,4 +1,6 @@ -namespace Dalamud.Plugin.Services; +using System.Collections.Generic; + +namespace Dalamud.Plugin.Services; /// /// Service that grants you the ability to replace texture data that is to be loaded by Dalamud. @@ -17,4 +19,19 @@ public interface ITextureSubstitutionProvider /// Event that will be called once Dalamud wants to load texture data. /// public event TextureDataInterceptorDelegate? InterceptTexDataLoad; + + /// + /// Get a path that may be substituted by a subscriber to ITextureSubstitutionProvider. + /// + /// The original path to substitute. + /// The original path, if no subscriber is registered or there is no substitution, or the substituted path. + public string GetSubstitutedPath(string originalPath); + + /// + /// Notify Dalamud about substitution status for files at the specified VFS paths changing. + /// You should call this with all paths that were either previously substituted and are no longer, + /// and paths that are newly substituted. + /// + /// The paths with a changed substitution status. + public void InvalidatePaths(IEnumerable paths); } From bfbfe8c91867132297e00b45021d33ae54dc2fe2 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 12 Aug 2023 12:03:45 +0200 Subject: [PATCH 32/34] chore: ModuleLog.Error() exception can be nullable --- Dalamud/Logging/Internal/ModuleLog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Logging/Internal/ModuleLog.cs b/Dalamud/Logging/Internal/ModuleLog.cs index d93730f36..c6c66e81a 100644 --- a/Dalamud/Logging/Internal/ModuleLog.cs +++ b/Dalamud/Logging/Internal/ModuleLog.cs @@ -108,7 +108,7 @@ public class ModuleLog /// The exception that caused the error. /// The message template. /// Values to log. - public void Error(Exception exception, string messageTemplate, params object[] values) + public void Error(Exception? exception, string messageTemplate, params object[] values) => this.WriteLog(LogEventLevel.Error, messageTemplate, exception, values); /// From 593b338a26e72bf91aa93b37d43d2181b49db6f4 Mon Sep 17 00:00:00 2001 From: goat Date: Sat, 12 Aug 2023 12:04:22 +0200 Subject: [PATCH 33/34] fix: don't try to load changelogs for dev plugins, show changelogs that loaded if any failed --- Dalamud/Interface/Internal/TextureManager.cs | 1 + .../DalamudChangelogManager.cs | 29 ++++++++++++------- .../PluginInstaller/PluginInstallerWindow.cs | 9 +++++- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index ad7c0f2f8..1bc5198e3 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -241,6 +241,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP if (!this.activeTextures.TryGetValue(path, out var info) || info == null) continue; + info.Wrap?.Dispose(); info.Wrap = null; } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs index 984732509..a9ad0c21a 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/DalamudChangelogManager.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Dalamud.Networking.Http; using Dalamud.Plugin.Internal; using Dalamud.Utility; +using Serilog; namespace Dalamud.Interface.Internal.Windows.PluginInstaller; @@ -44,27 +45,35 @@ internal class DalamudChangelogManager this.Changelogs = null; var dalamudChangelogs = await client.GetFromJsonAsync>(DalamudChangelogUrl); - var changelogs = dalamudChangelogs.Select(x => new DalamudChangelogEntry(x)).Cast(); + var changelogs = dalamudChangelogs.Select(x => new DalamudChangelogEntry(x)).Cast().ToList(); foreach (var plugin in this.manager.InstalledPlugins) { - if (!plugin.IsThirdParty) + if (!plugin.IsThirdParty && !plugin.IsDev) { - var pluginChangelogs = await client.GetFromJsonAsync(string.Format( - PluginChangelogUrl, - plugin.Manifest.InternalName, - plugin.Manifest.Dip17Channel)); + try + { + var pluginChangelogs = await client.GetFromJsonAsync(string.Format( + PluginChangelogUrl, + plugin.Manifest.InternalName, + plugin.Manifest.Dip17Channel)); - changelogs = changelogs.Concat(pluginChangelogs.Versions - .Where(x => x.Dip17Track == plugin.Manifest.Dip17Channel) - .Select(x => new PluginChangelogEntry(plugin, x))); + changelogs.AddRange(pluginChangelogs.Versions + .Where(x => x.Dip17Track == + plugin.Manifest.Dip17Channel) + .Select(x => new PluginChangelogEntry(plugin, x))); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to load changelog for {PluginName}", plugin.Manifest.Name); + } } else { if (plugin.Manifest.Changelog.IsNullOrWhitespace()) continue; - changelogs = changelogs.Append(new PluginChangelogEntry(plugin)); + changelogs.Add(new PluginChangelogEntry(plugin)); } } diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 35fa40013..4cc7e35c3 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -967,7 +967,14 @@ internal class PluginInstallerWindow : Window, IDisposable { this.dalamudChangelogRefreshTaskCts = new CancellationTokenSource(); this.dalamudChangelogRefreshTask = - Task.Run(this.dalamudChangelogManager.ReloadChangelogAsync, this.dalamudChangelogRefreshTaskCts.Token); + Task.Run(this.dalamudChangelogManager.ReloadChangelogAsync, this.dalamudChangelogRefreshTaskCts.Token) + .ContinueWith(t => + { + if (!t.IsCompletedSuccessfully) + { + Log.Error(t.Exception, "Failed to load changelogs."); + } + }); } return; From 7c428e6b72422aca79cbcfc3f60cab0f3d47c615 Mon Sep 17 00:00:00 2001 From: goat Date: Sun, 13 Aug 2023 22:42:59 +0200 Subject: [PATCH 34/34] feat: allow individual toggling of plugins in a single custom collection from the installed plugins page --- .../PluginInstaller/PluginInstallerWindow.cs | 109 ++++++++++-------- Dalamud/Plugin/Internal/Profiles/Profile.cs | 3 + 2 files changed, 63 insertions(+), 49 deletions(-) diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 4cc7e35c3..b648a8204 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -2366,6 +2366,10 @@ internal class PluginInstallerWindow : Window, IDisposable var config = Service.Get(); var applicableForProfiles = plugin.Manifest.SupportsProfiles && !plugin.IsDev; + var profilesThatWantThisPlugin = profileManager.Profiles + .Where(x => x.WantsPlugin(plugin.InternalName) != null) + .ToArray(); + var isInSingleProfile = profilesThatWantThisPlugin.Length == 1; var isDefaultPlugin = profileManager.IsInDefaultProfile(plugin.Manifest.InternalName); // Disable everything if the updater is running or another plugin is operating @@ -2449,6 +2453,10 @@ internal class PluginInstallerWindow : Window, IDisposable ImGui.EndPopup(); } + var inMultipleProfiles = !isDefaultPlugin && !isInSingleProfile; + var inSingleNonDefaultProfileWhichIsDisabled = + isInSingleProfile && !profilesThatWantThisPlugin.First().IsEnabled; + if (plugin.State is PluginState.UnloadError or PluginState.LoadError or PluginState.DependencyResolutionFailed && !plugin.IsDev) { ImGuiComponents.DisabledButton(FontAwesomeIcon.Frown); @@ -2456,80 +2464,77 @@ internal class PluginInstallerWindow : Window, IDisposable if (ImGui.IsItemHovered()) ImGui.SetTooltip(Locs.PluginButtonToolTip_UnloadFailed); } - else if (disabled || !isDefaultPlugin) + else if (disabled || inMultipleProfiles || inSingleNonDefaultProfileWhichIsDisabled) { ImGuiComponents.DisabledToggleButton(toggleId, isLoadedAndUnloadable); - if (!isDefaultPlugin && ImGui.IsItemHovered()) - ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInDefault); + if (inMultipleProfiles && ImGui.IsItemHovered()) + ImGui.SetTooltip(Locs.PluginButtonToolTip_NeedsToBeInSingleProfile); + else if (inSingleNonDefaultProfileWhichIsDisabled && ImGui.IsItemHovered()) + ImGui.SetTooltip(Locs.PluginButtonToolTip_SingleProfileDisabled(profilesThatWantThisPlugin.First().Name)); } else { if (ImGuiComponents.ToggleButton(toggleId, ref isLoadedAndUnloadable)) { - // TODO: We can technically let profile manager take care of unloading/loading the plugin, but we should figure out error handling first. + var applicableProfile = profilesThatWantThisPlugin.First(); + Log.Verbose("Switching {InternalName} in {Profile} to {State}", + plugin.InternalName, applicableProfile, isLoadedAndUnloadable); + + try + { + // Reload the devPlugin manifest if it's a dev plugin + // The plugin might rely on changed values in the manifest + if (plugin.IsDev) + { + plugin.ReloadManifest(); + } + } + catch (Exception ex) + { + Log.Error(ex, "Could not reload DevPlugin manifest"); + } + + // NOTE: We don't use the profile manager to actually handle loading/unloading here, + // because that might cause us to show an error if a plugin we don't actually care about + // fails to load/unload. Instead, we just do it ourselves and then update the profile. + // There is probably a smarter way to handle this, but it's probably more code. if (!isLoadedAndUnloadable) { this.enableDisableStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.DisablingSingle; - Task.Run(() => + Task.Run(async () => { - if (plugin.IsDev) - { - plugin.ReloadManifest(); - } - - var unloadTask = Task.Run(() => plugin.UnloadAsync()) - .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name)); - - unloadTask.Wait(); - if (!unloadTask.Result) - { - this.enableDisableStatus = OperationStatus.Complete; - return; - } - - // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, false, false)) - .GetAwaiter().GetResult(); - this.enableDisableStatus = OperationStatus.Complete; + await plugin.UnloadAsync(); + await applicableProfile.AddOrUpdateAsync( + plugin.Manifest.InternalName, false, false); notifications.AddNotification(Locs.Notifications_PluginDisabled(plugin.Manifest.Name), Locs.Notifications_PluginDisabledTitle, NotificationType.Success); + }).ContinueWith(t => + { + this.enableDisableStatus = OperationStatus.Complete; + this.DisplayErrorContinuation(t, Locs.ErrorModal_UnloadFail(plugin.Name)); }); } else { - var enabler = new Task(() => + async Task Enabler() { this.enableDisableStatus = OperationStatus.InProgress; this.loadingIndicatorKind = LoadingIndicatorKind.EnablingSingle; - if (plugin.IsDev) - { - plugin.ReloadManifest(); - } + await applicableProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false); + await plugin.LoadAsync(PluginLoadReason.Installer); - // TODO: Work this out - Task.Run(() => profileManager.DefaultProfile.AddOrUpdateAsync(plugin.Manifest.InternalName, true, false)) - .GetAwaiter().GetResult(); + notifications.AddNotification(Locs.Notifications_PluginEnabled(plugin.Manifest.Name), Locs.Notifications_PluginEnabledTitle, NotificationType.Success); + } - var loadTask = Task.Run(() => plugin.LoadAsync(PluginLoadReason.Installer)) - .ContinueWith( - this.DisplayErrorContinuation, - Locs.ErrorModal_LoadFail(plugin.Name)); - - loadTask.Wait(); + var continuation = (Task t) => + { this.enableDisableStatus = OperationStatus.Complete; - - if (!loadTask.Result) - return; - - notifications.AddNotification( - Locs.Notifications_PluginEnabled(plugin.Manifest.Name), - Locs.Notifications_PluginEnabledTitle, - NotificationType.Success); - }); + this.DisplayErrorContinuation(t, Locs.ErrorModal_LoadFail(plugin.Name)); + }; if (availableUpdate != default && !availableUpdate.InstalledPlugin.IsDev) { @@ -2539,17 +2544,19 @@ internal class PluginInstallerWindow : Window, IDisposable if (shouldUpdate) { + // We need to update the profile right here, because PM will not enable the plugin otherwise + await applicableProfile.AddOrUpdateAsync(plugin.InternalName, true, false); await this.UpdateSinglePlugin(availableUpdate); } else { - enabler.Start(); + _ = Task.Run(Enabler).ContinueWith(continuation); } }); } else { - enabler.Start(); + _ = Task.Run(Enabler).ContinueWith(continuation); } } } @@ -3259,6 +3266,10 @@ internal class PluginInstallerWindow : Window, IDisposable public static string PluginButtonToolTip_UnloadFailed => Loc.Localize("InstallerLoadUnloadFailedTooltip", "Plugin load/unload failed, please restart your game and try again."); public static string PluginButtonToolTip_NeedsToBeInDefault => Loc.Localize("InstallerUnloadNeedsToBeInDefault", "This plugin is in one or more collections. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it manually, remove it from all collections."); + + public static string PluginButtonToolTip_NeedsToBeInSingleProfile => Loc.Localize("InstallerUnloadNeedsToBeInSingleProfile", "This plugin is in more than one collection. If you want to enable or disable it, please do so by enabling or disabling the collections it is in.\nIf you want to manage it here, make sure it is only in a single collection."); + + public static string PluginButtonToolTip_SingleProfileDisabled(string name) => Loc.Localize("InstallerSingleProfileDisabled", "The collection '{0}' which contains this plugin is disabled.\nPlease enable it in the collections manager to toggle the plugin individually.").Format(name); #endregion diff --git a/Dalamud/Plugin/Internal/Profiles/Profile.cs b/Dalamud/Plugin/Internal/Profiles/Profile.cs index 61d521e89..ac46d9153 100644 --- a/Dalamud/Plugin/Internal/Profiles/Profile.cs +++ b/Dalamud/Plugin/Internal/Profiles/Profile.cs @@ -232,4 +232,7 @@ internal class Profile if (apply) await this.manager.ApplyAllWantStatesAsync(); } + + /// + public override string ToString() => $"{this.Guid} ({this.Name})"; }