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