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.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<string, TextureInfo> activeTextures = new();
@ -45,12 +45,14 @@ internal class TextureManager : IDisposable, IServiceType, ITextureSubstitutionP
/// </summary>
/// <param name="framework">Framework instance.</param>
/// <param name="dataManager">DataManager instance.</param>
/// <param name="im">InterfaceManager instance.</param>
/// <param name="startInfo">DalamudStartInfo instance.</param>
[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
/// <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>
/// <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);
}
/// <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/>
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<TexFile>();
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<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
{
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;
}
@ -377,9 +423,24 @@ internal class TextureManagerPluginScoped : ITextureProvider, IServiceType, IDis
}
/// <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)
return null;

View file

@ -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)
{

View file

@ -1,4 +1,5 @@
using System;
using System.IO;
using Dalamud.Interface.Internal;
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="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>
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>
/// Get a texture handle for the specified Lumina TexFile.

View file

@ -7,15 +7,14 @@ public interface ITextureSubstitutionProvider
{
/// <summary>
/// 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>
/// <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>
public delegate void TextureDataInterceptorDelegate(string path, ref byte[]? data);
/// <param name="replacementPath">The path that should be loaded instead.</param>
public delegate void TextureDataInterceptorDelegate(string path, ref string? replacementPath);
/// <summary>
/// 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>
public event TextureDataInterceptorDelegate? InterceptTexDataLoad;
}