feat: add support to load textures from files

This commit is contained in:
goat 2023-08-02 18:46:44 +02:00
parent 8df9821f0e
commit 22a6261c98
No known key found for this signature in database
GPG key ID: 49E2AA8C6A76498B
4 changed files with 116 additions and 33 deletions

View file

@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Game; using Dalamud.Game;
@ -11,7 +11,6 @@ using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ImGuiScene; using ImGuiScene;
using Lumina.Data;
using Lumina.Data.Files; using Lumina.Data.Files;
namespace Dalamud.Interface.Internal; namespace Dalamud.Interface.Internal;
@ -36,6 +35,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
private readonly Framework framework; private readonly Framework framework;
private readonly DataManager dataManager; private readonly DataManager dataManager;
private readonly InterfaceManager im;
private readonly DalamudStartInfo startInfo; private readonly DalamudStartInfo startInfo;
private readonly Dictionary<string, TextureInfo> activeTextures = new(); private readonly Dictionary<string, TextureInfo> activeTextures = new();
@ -45,12 +45,14 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
/// </summary> /// </summary>
/// <param name="framework">Framework instance.</param> /// <param name="framework">Framework instance.</param>
/// <param name="dataManager">DataManager instance.</param> /// <param name="dataManager">DataManager instance.</param>
/// <param name="im">InterfaceManager instance.</param>
/// <param name="startInfo">DalamudStartInfo instance.</param> /// <param name="startInfo">DalamudStartInfo instance.</param>
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
public TextureManager(Framework framework, DataManager dataManager, DalamudStartInfo startInfo) public TextureManager(Framework framework, DataManager dataManager, InterfaceManager im, DalamudStartInfo startInfo)
{ {
this.framework = framework; this.framework = framework;
this.dataManager = dataManager; this.dataManager = dataManager;
this.im = im;
this.startInfo = startInfo; this.startInfo = startInfo;
this.framework.Update += this.FrameworkOnUpdate; this.framework.Update += this.FrameworkOnUpdate;
@ -145,11 +147,31 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
/// <param name="path">The path to the texture in the game's VFS.</param> /// <param name="path">The path to the texture in the game's VFS.</param>
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param> /// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
/// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns> /// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns>
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); return !this.dataManager.FileExists(path) ? null : this.CreateWrap(path, keepAlive);
} }
/// <summary>
/// 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.
/// </summary>
/// <param name="file">The FileInfo describing the image or texture file.</param>
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
/// <returns>Null, if the file does not exist, or a texture wrap that can be used to render the texture.</returns>
public TextureManagerTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive)
{
ArgumentNullException.ThrowIfNull(file);
return !file.Exists ? null : this.CreateWrap(file.FullName, keepAlive);
}
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
{ {
@ -185,7 +207,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
lock (this.activeTextures) lock (this.activeTextures)
{ {
if (!this.activeTextures.TryAdd(path, info)) 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) if (refresh)
{ {
byte[]? interceptData = null; if (!this.im.IsReady)
this.InterceptTexDataLoad?.Invoke(path, ref interceptData); throw new InvalidOperationException("Cannot create textures before scene is ready");
// TODO: Do we also want to support loading from actual fs here? Doesn't seem to be a big deal, collect interest string? interceptPath = null;
this.InterceptTexDataLoad?.Invoke(path, ref interceptPath);
TexFile? file; if (interceptPath != null)
if (interceptData != null)
{ {
// TODO: upstream to lumina Log.Verbose("Intercept: {OriginalPath} => {ReplacePath}", path, interceptPath);
file = Activator.CreateInstance<TexFile>(); path = interceptPath;
var type = typeof(TexFile); }
type.GetProperty("Data", BindingFlags.NonPublic | BindingFlags.Instance)!.GetSetMethod()!
.Invoke(file, new object[] { interceptData }); TextureWrap? wrap;
type.GetProperty("Reader", BindingFlags.NonPublic | BindingFlags.Instance)!.GetSetMethod()!
.Invoke(file, new object[] { new LuminaBinaryReader(file.Data) }); // TODO: The actual loading here may fail due to circumstances outside of our control.
file.LoadFile(); // 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<TexFile>(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 else
{ {
file = this.dataManager.GetFile<TexFile>(path); // Load regularly from dats
var file = this.dataManager.GetFile<TexFile>(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; info.Wrap = wrap;
} }
@ -377,9 +423,24 @@ internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDis
} }
/// <inheritdoc/> /// <inheritdoc/>
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;
}
/// <inheritdoc/>
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive)
{
ArgumentNullException.ThrowIfNull(file);
var wrap = this.textureManager.GetTextureFromFile(file, keepAlive);
if (wrap == null) if (wrap == null)
return null; return null;

View file

@ -1,10 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Numerics; using System.Numerics;
using Dalamud.Data;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
using ImGuiScene; using ImGuiScene;
using Serilog; using Serilog;
@ -74,7 +73,19 @@ internal class TexWidget : IDataWindowWidget
{ {
try 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) catch (Exception ex)
{ {

View file

@ -1,4 +1,5 @@
using System; using System;
using System.IO;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Lumina.Data.Files; using Lumina.Data.Files;
@ -58,7 +59,18 @@ public interface ITextureProvider
/// <param name="path">The path to the texture in the game's VFS.</param> /// <param name="path">The path to the texture in the game's VFS.</param>
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param> /// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
/// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns> /// <returns>Null, if the icon does not exist, or a texture wrap that can be used to render the texture.</returns>
public IDalamudTextureWrap? GetTextureFromGamePath(string path, bool keepAlive = false); public IDalamudTextureWrap? GetTextureFromGame(string path, bool keepAlive = false);
/// <summary>
/// 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.
/// </summary>
/// <param name="file">The FileInfo describing the image or texture file.</param>
/// <param name="keepAlive">Prevent Dalamud from automatically unloading this texture to save memory. Usually does not need to be set.</param>
/// <returns>Null, if the file does not exist, or a texture wrap that can be used to render the texture.</returns>
public IDalamudTextureWrap? GetTextureFromFile(FileInfo file, bool keepAlive = false);
/// <summary> /// <summary>
/// Get a texture handle for the specified Lumina TexFile. /// Get a texture handle for the specified Lumina TexFile.

View file

@ -7,15 +7,14 @@ public interface ITextureSubstitutionProvider
{ {
/// <summary> /// <summary>
/// Delegate describing a function that may be used to intercept and replace texture data. /// 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.
/// </summary> /// </summary>
/// <param name="path">The path to the texture that is to be loaded.</param> /// <param name="path">The path to the texture that is to be loaded.</param>
/// <param name="data">The texture data. Null by default, assign something if you wish to replace the data from the game dats.</param> /// <param name="replacementPath">The path that should be loaded instead.</param>
public delegate void TextureDataInterceptorDelegate(string path, ref byte[]? data); public delegate void TextureDataInterceptorDelegate(string path, ref string? replacementPath);
/// <summary> /// <summary>
/// Event that will be called once Dalamud wants to load texture 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.
/// </summary> /// </summary>
public event TextureDataInterceptorDelegate? InterceptTexDataLoad; public event TextureDataInterceptorDelegate? InterceptTexDataLoad;
} }