diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index dad010e59..da4f241ac 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -263,6 +263,14 @@ public sealed class UiBuilder : IDisposable => this.InterfaceManagerWithScene?.LoadImageRaw(imageData, width, height, numChannels) ?? throw new InvalidOperationException("Load failed."); + /// + /// Loads an ULD file that can load textures containing multiple icons in a single texture. + /// + /// The path of the requested ULD file. + /// A wrapper around said ULD file. + public UldWrapper LoadUld(string uldPath) + => new(this, uldPath); + /// /// Asynchronously loads an image from the specified file, when it's possible to do so. /// diff --git a/Dalamud/Interface/UldWrapper.cs b/Dalamud/Interface/UldWrapper.cs new file mode 100644 index 000000000..d41256fa2 --- /dev/null +++ b/Dalamud/Interface/UldWrapper.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Dalamud.Data; +using Dalamud.Utility; +using ImGuiScene; +using Lumina.Data.Files; +using Lumina.Data.Parsing.Uld; + +namespace Dalamud.Interface; + +/// Wrapper for multi-icon sprite sheets defined by ULD files. +public class UldWrapper : IDisposable +{ + private readonly DataManager data; + private readonly UiBuilder uiBuilder; + private readonly Dictionary textures = new(); + + /// Initializes a new instance of the class, wrapping an ULD file. + /// The UiBuilder used to load textures. + /// The requested ULD file. + internal UldWrapper(UiBuilder uiBuilder, string uldPath) + { + this.uiBuilder = uiBuilder; + this.data = Service.Get(); + this.Uld = this.data.GetFile(uldPath); + } + + /// Gets the loaded ULD file if it exists. + public UldFile? Uld { get; private set; } + + /// Gets a value indicating whether the requested ULD could be loaded. + public bool Valid + => this.Uld != null; + + /// Load a part of a multi-icon sheet as a texture. + /// The path of the requested texture. + /// The index of the desired icon. + /// A TextureWrap containing the requested part if it exists and null otherwise. + public TextureWrap? LoadTexturePart(string texturePath, int part) + { + if (!this.Valid) + { + return null; + } + + if (!this.textures.TryGetValue(texturePath, out var texture)) + { + var tuple = this.GetTexture(texturePath); + if (!tuple.HasValue) + { + return null; + } + + texture = tuple.Value; + this.textures[texturePath] = texture; + } + + return this.CreateTexture(texture.Id, texture.Width, texture.Height, texture.HD, texture.RgbaData, part); + } + + /// Clear all stored data and remove the loaded ULD. + public void Dispose() + { + this.textures.Clear(); + this.Uld = null; + } + + private TextureWrap? CreateTexture(uint id, int width, int height, bool hd, byte[] rgbaData, int partIdx) + { + var idx = 0; + UldRoot.PartData? partData = null; + + // Iterate over all available parts that have the corresponding TextureId, + // count up to the required one and return it if it exists. + foreach (var part in this.Uld!.Parts.SelectMany(p => p.Parts)) + { + if (part.TextureId != id) + { + continue; + } + + if (idx++ == partIdx) + { + partData = part; + break; + } + } + + if (!partData.HasValue) + { + return null; + } + + // Double all dimensions for HD textures. + var d = hd ? partData.Value with + { + H = (ushort)(partData.Value.H * 2), + W = (ushort)(partData.Value.W * 2), + U = (ushort)(partData.Value.U * 2), + V = (ushort)(partData.Value.V * 2), + } : partData.Value; + + return this.CopyRect(width, height, rgbaData, d); + } + + private TextureWrap? CopyRect(int width, int height, byte[] rgbaData, UldRoot.PartData part) + { + if (part.V + part.W > width || part.U + part.H > height) + { + return null; + } + + var imageData = new byte[part.W * part.H * 4]; + + // Iterate over all lines and copy the relevant ones, + // assuming a 4-byte-per-pixel standard layout. + for (var y = 0; y < part.H; ++y) + { + var inputSlice = rgbaData.AsSpan().Slice((((part.V + y) * width) + part.U) * 4, part.W * 4); + var outputSlice = imageData.AsSpan(y * part.W * 4); + inputSlice.CopyTo(outputSlice); + } + + return this.uiBuilder.LoadImageRaw(imageData, part.W, part.H, 4); + } + + private (uint Id, int Width, int Height, bool HD, byte[] RgbaData)? GetTexture(string texturePath) + { + if (!this.Valid) + { + return null; + } + + // Always replace the HD version with the regular one as ULDs do not contain the HD suffix. + texturePath = texturePath.Replace("_hr1", string.Empty); + + // Search the requested texture asset in the ULD and store its ID if it exists. + var id = uint.MaxValue; + foreach (var part in this.Uld!.AssetData) + { + var maxLength = Math.Min(part.Path.Length, texturePath.AsSpan().Length); + if (part.Path.AsSpan()[..maxLength].SequenceEqual(texturePath.AsSpan()[..maxLength])) + { + id = part.Id; + break; + } + } + + if (id == uint.MaxValue) + { + return null; + } + + // Try to load HD textures first. + var hrPath = texturePath.Replace(".tex", "_hr1.tex"); + var hd = true; + var file = this.data.GetFile(hrPath); + if (file == null) + { + hd = false; + file = this.data.GetFile(texturePath); + + // Neither texture could be loaded. + if (file == null) + { + return null; + } + } + + return (id, file.Header.Width, file.Header.Height, hd, file.GetRgbaImageData()); + } +}