From d88593c50003669346ca581d35faf3e10ad2e4ce Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Oct 2025 17:52:28 +0200 Subject: [PATCH 1/2] Current State. --- Luna | 2 +- OtterGui | 2 +- .../Manager/IndividualCollections.cs | 4 +- .../Collections/ModCollectionInheritance.cs | 2 +- Penumbra/Import/Models/IoNotifier.cs | 80 +-- Penumbra/Import/Models/ModelManager.cs | 652 +++++++++--------- Penumbra/Import/TexToolsImporter.Archives.cs | 15 +- .../Textures/CombinedTexture.Manipulation.cs | 219 +++--- .../Textures/CombinedTexture.Operations.cs | 293 ++++---- Penumbra/Import/Textures/PathSelectCombo.cs | 80 +++ Penumbra/Import/Textures/TextureDrawer.cs | 272 +++----- .../Hooks/Animation/LoadCharacterVfx.cs | 2 +- .../Interop/Hooks/Animation/PlayFootstep.cs | 20 +- .../ResourceTree/ResourceTreeApiHelper.cs | 10 + Penumbra/Penumbra.csproj | 2 + .../ModEditWindow.QuickImport.cs | 4 +- .../AdvancedWindow/ModEditWindow.Textures.cs | 19 +- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- .../ResourceTreeViewerFactory.cs | 2 +- 19 files changed, 856 insertions(+), 826 deletions(-) create mode 100644 Penumbra/Import/Textures/PathSelectCombo.cs diff --git a/Luna b/Luna index 7214f079..78216203 160000 --- a/Luna +++ b/Luna @@ -1 +1 @@ -Subproject commit 7214f079cb9b8eeea6fa1a9fe1c6ca8118049969 +Subproject commit 78216203f4570a6194fce9422204d8abb536c828 diff --git a/OtterGui b/OtterGui index f3544447..9af1e5fc 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f354444776591ae423e2d8374aae346308d81424 +Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87 diff --git a/Penumbra/Collections/Manager/IndividualCollections.cs b/Penumbra/Collections/Manager/IndividualCollections.cs index 67ab0b21..2aaedefe 100644 --- a/Penumbra/Collections/Manager/IndividualCollections.cs +++ b/Penumbra/Collections/Manager/IndividualCollections.cs @@ -1,5 +1,5 @@ using Dalamud.Game.ClientState.Objects.Enums; -using OtterGui.Filesystem; +using Luna; using Penumbra.GameData.Actors; using Penumbra.GameData.DataContainers.Bases; using Penumbra.GameData.Enums; @@ -224,7 +224,7 @@ public sealed partial class IndividualCollections { return identifier.Type switch { - IdentifierType.Player => $"{identifier.PlayerName} ({_actors.Data.ToWorldName(identifier.HomeWorld)})", + IdentifierType.Player => $"{identifier.PlayerName} ({_actors.Data.ToWorldName(identifier.HomeWorld)})", IdentifierType.Retainer => $"{identifier.PlayerName} (Retainer)", IdentifierType.Owned => $"{identifier.PlayerName} ({_actors.Data.ToWorldName(identifier.HomeWorld)})'s {_actors.Data.ToName(identifier.Kind, identifier.DataId)}", diff --git a/Penumbra/Collections/ModCollectionInheritance.cs b/Penumbra/Collections/ModCollectionInheritance.cs index 151ed7db..8580badb 100644 --- a/Penumbra/Collections/ModCollectionInheritance.cs +++ b/Penumbra/Collections/ModCollectionInheritance.cs @@ -1,4 +1,4 @@ -using OtterGui.Filesystem; +using Luna; namespace Penumbra.Collections; diff --git a/Penumbra/Import/Models/IoNotifier.cs b/Penumbra/Import/Models/IoNotifier.cs index 56ef7103..c9d3ba8f 100644 --- a/Penumbra/Import/Models/IoNotifier.cs +++ b/Penumbra/Import/Models/IoNotifier.cs @@ -1,40 +1,40 @@ -using OtterGui.Log; - -namespace Penumbra.Import.Models; - -public record class IoNotifier -{ - private readonly List _messages = []; - private string _context = ""; - - /// Create a new notifier with the specified context appended to any other context already present. - public IoNotifier WithContext(string context) - => this with { _context = $"{_context}{context}: "}; - - /// Send a warning with any current context to notification channels. - public void Warning(string content) - => SendMessage(content, Logger.LogLevel.Warning); - - /// Get the current warnings for this notifier. - /// This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. - public IEnumerable GetWarnings() - => _messages; - - /// Create an exception with any current context. - [StackTraceHidden] - public Exception Exception(string message) - => Exception(message); - - /// Create an exception of the provided type with any current context. - [StackTraceHidden] - public TException Exception(string message) - where TException : Exception, new() - => (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!; - - private void SendMessage(string message, Logger.LogLevel type) - { - var fullText = $"{_context}{message}"; - Penumbra.Log.Message(type, fullText); - _messages.Add(fullText); - } -} +using Luna; + +namespace Penumbra.Import.Models; + +public record IoNotifier(Logger Log) +{ + private readonly List _messages = []; + private string _context = ""; + + /// Create a new notifier with the specified context appended to any other context already present. + public IoNotifier WithContext(string context) + => this with { _context = $"{_context}{context}: " }; + + /// Send a warning with any current context to notification channels. + public void Warning(string content) + => SendMessage(content, Logger.LogLevel.Warning); + + /// Get the current warnings for this notifier. + /// This does not currently filter to notifications with the current notifier's context - it will return all IO notifications from all notifiers. + public IEnumerable GetWarnings() + => _messages; + + /// Create an exception with any current context. + [StackTraceHidden] + public Exception Exception(string message) + => Exception(message); + + /// Create an exception of the provided type with any current context. + [StackTraceHidden] + public TException Exception(string message) + where TException : Exception, new() + => (TException)Activator.CreateInstance(typeof(TException), $"{_context}{message}")!; + + private void SendMessage(string message, Logger.LogLevel type) + { + var fullText = $"{_context}{message}"; + Log.Message(type, fullText); + _messages.Add(fullText); + } +} diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 21826671..3f684307 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,323 +1,329 @@ -using Dalamud.Plugin.Services; -using Lumina.Data.Parsing; -using OtterGui.Tasks; -using Penumbra.Collections.Manager; -using Penumbra.GameData; -using Penumbra.GameData.Data; -using Penumbra.GameData.Enums; -using Penumbra.GameData.Files; -using Penumbra.GameData.Structs; -using Penumbra.Import.Models.Export; -using Penumbra.Import.Models.Import; -using Penumbra.Import.Textures; -using Penumbra.Meta; -using Penumbra.Meta.Files; -using Penumbra.Meta.Manipulations; -using SharpGLTF.Scenes; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; - -namespace Penumbra.Import.Models; - -using Schema2 = SharpGLTF.Schema2; -using LuminaMaterial = Lumina.Models.Materials.Material; - -public sealed class ModelManager(IFramework framework, MetaFileManager metaFileManager, ActiveCollections collections, GamePathParser parser) - : SingleTaskQueue, IDisposable, Luna.IService -{ - private readonly IFramework _framework = framework; - - private readonly ConcurrentDictionary _tasks = new(); - - private bool _disposed; - - public void Dispose() - { - _disposed = true; - foreach (var (_, cancel) in _tasks.Values.ToArray()) - cancel.Cancel(); - _tasks.Clear(); - } - - public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, - string outputPath) - => EnqueueWithResult( - new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), - action => action.Notifier - ); - - public Task<(MdlFile?, IoNotifier)> ImportGltf(string inputPath) - => EnqueueWithResult( - new ImportGltfAction(inputPath), - action => (action.Out, action.Notifier) - ); - - /// Try to find the .sklb paths for a .mdl file. - /// .mdl file to look up the skeletons for. - /// Modified extra skeleton template parameters. - public string[] ResolveSklbsForMdl(string mdlPath, KeyValuePair[] estManipulations) - { - var info = parser.GetFileInfo(mdlPath); - if (info.FileType is not FileType.Model) - return []; - - var baseSkeleton = GamePaths.Sklb.Customization(info.GenderRace, "base", 1); - - return info.ObjectType switch - { - ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body - => [baseSkeleton, ..ResolveEstSkeleton(EstType.Body, info, estManipulations)], - ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head - => [baseSkeleton, ..ResolveEstSkeleton(EstType.Head, info, estManipulations)], - ObjectType.Equipment => [baseSkeleton], - ObjectType.Accessory => [baseSkeleton], - ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], - ObjectType.Character when info.BodySlot is BodySlot.Hair - => [baseSkeleton, ..ResolveEstSkeleton(EstType.Hair, info, estManipulations)], - ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear - => [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)], - ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), - ObjectType.DemiHuman => [GamePaths.Sklb.DemiHuman(info.PrimaryId)], - ObjectType.Monster => [GamePaths.Sklb.Monster(info.PrimaryId)], - ObjectType.Weapon => [GamePaths.Sklb.Weapon(info.PrimaryId)], - _ => [], - }; - } - - private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, KeyValuePair[] estManipulations) - { - // Try to find an EST entry from the manipulations provided. - var modEst = estManipulations - .FirstOrNull( - est => est.Key.GenderRace == info.GenderRace - && est.Key.Slot == type - && est.Key.SetId == info.PrimaryId - ); - - // Try to use an entry from provided manipulations, falling back to the current collection. - var targetId = modEst?.Value - ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) - ?? EstFile.GetDefault(metaFileManager, type, info.GenderRace, info.PrimaryId); - - // If there's no entries, we can assume that there's no additional skeleton. - if (targetId == EstEntry.Zero) - return []; - - return [GamePaths.Sklb.Customization(info.GenderRace, type.ToName(), targetId.AsId)]; - } - - /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. - private string? ResolveMtrlPath(string rawPath, IoNotifier notifier) - { - // TODO: this should probably be chosen in the export settings - var variantId = 1; - - // Get standardised paths - var absolutePath = rawPath.StartsWith('/') - ? LuminaMaterial.ResolveRelativeMaterialPath(rawPath, variantId) - : rawPath; - var relativePath = rawPath.StartsWith('/') - ? rawPath - : '/' + Path.GetFileName(rawPath); - - if (absolutePath == null) - { - notifier.Warning($"Material path \"{rawPath}\" could not be resolved."); - return null; - } - - var info = parser.GetFileInfo(absolutePath); - if (info.FileType is not FileType.Material) - { - notifier.Warning($"Material path {rawPath} does not conform to material conventions."); - return null; - } - - var resolvedPath = info.ObjectType switch - { - ObjectType.Character => GamePaths.Mtrl.Customization( - info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant), - _ => absolutePath, - }; - - Penumbra.Log.Debug($"Resolved material {rawPath} to {resolvedPath}"); - - return resolvedPath; - } - - private Task Enqueue(IAction action) - { - if (_disposed) - return Task.FromException(new ObjectDisposedException(nameof(ModelManager))); - - Task task; - lock (_tasks) - { - task = _tasks.GetOrAdd(action, a => - { - var token = new CancellationTokenSource(); - var t = Enqueue(a, token.Token); - t.ContinueWith(_ => - { - lock (_tasks) - { - return _tasks.TryRemove(a, out var unused); - } - }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); - return (t, token); - }).Item1; - } - - return task; - } - - private Task EnqueueWithResult(TAction action, Func process) - where TAction : IAction - => Enqueue(action).ContinueWith(task => - { - if (task is { IsFaulted: true, Exception: not null }) - throw task.Exception; - - return process(action); - }, TaskScheduler.Default); - - private class ExportToGltfAction( - ModelManager manager, - ExportConfig config, - MdlFile mdl, - IEnumerable sklbPaths, - Func read, - string outputPath) - : IAction - { - public readonly IoNotifier Notifier = new(); - - public void Execute(CancellationToken cancel) - { - Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}..."); - - Penumbra.Log.Debug("[GLTF Export] Reading skeletons..."); - var xivSkeletons = BuildSkeletons(cancel); - - Penumbra.Log.Debug("[GLTF Export] Reading materials..."); - var materials = mdl.Materials - .Select(path => (path, material: BuildMaterial(path, Notifier, cancel))) - .Where(pair => pair.material != null) - .ToDictionary(pair => pair.path, pair => pair.material!.Value); - - Penumbra.Log.Debug("[GLTF Export] Converting model..."); - var model = ModelExporter.Export(config, mdl, xivSkeletons, materials, Notifier); - - Penumbra.Log.Debug("[GLTF Export] Building scene..."); - var scene = new SceneBuilder(); - model.AddToScene(scene); - - Penumbra.Log.Debug("[GLTF Export] Saving..."); - var gltfModel = scene.ToGltf2(); - gltfModel.Save(outputPath); - Penumbra.Log.Debug("[GLTF Export] Done."); - } - - /// Attempt to read out the pertinent information from the sklb file paths provided. - private IEnumerable BuildSkeletons(CancellationToken cancel) - { - // We're intentionally filtering failed reads here - the failure will - // be picked up, if relevant, when the model tries to create mappings - // for a bone in the failed sklb. - var havokTasks = sklbPaths - .Select(read) - .Where(bytes => bytes != null) - .Select(bytes => new SklbFile(bytes!)) - .Index() - .Select(CreateHavokTask) - .ToArray(); - - // Result waits automatically. - return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result)); - - // The havok methods we're relying on for this conversion are a bit - // finicky at the best of times, and can outright cause a CTD if they - // get upset. Running each conversion on its own tick seems to make - // this consistently non-crashy across my testing. - Task CreateHavokTask((int Index, SklbFile Sklb) pair) - => manager._framework.RunOnTick( - () => HavokConverter.HkxToXml(pair.Sklb.Skeleton), - delayTicks: pair.Index, cancellationToken: cancel); - } - - /// Read a .mtrl and populate its textures. - private MaterialExporter.Material? BuildMaterial(string relativePath, IoNotifier notifier, CancellationToken cancel) - { - var path = manager.ResolveMtrlPath(relativePath, notifier); - if (path == null) - return null; - - var bytes = read(path); - if (bytes == null) - return null; - - var mtrl = new MtrlFile(bytes); - - return new MaterialExporter.Material - { - Mtrl = mtrl, - Textures = mtrl.ShaderPackage.Samplers.ToDictionary( - sampler => (TextureUsage)sampler.SamplerId, - sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) - ), - }; - } - - /// Read a texture referenced by a .mtrl and convert it into an ImageSharp image. - private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) - { - // Work out the texture's path - the DX11 material flag controls a file name prefix. - GamePaths.Tex.HandleDx11Path(texture, out var texturePath); - var bytes = read(texturePath); - if (bytes == null) - return CreateDummyImage(); - - using var textureData = new MemoryStream(bytes); - var image = TexFileParser.Parse(textureData); - var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; - return pngImage ?? throw new Exception("Failed to convert texture to png."); - } - - private static Image CreateDummyImage() - { - var image = new Image(1, 1); - image[0, 0] = Color.White; - return image; - } - - public bool Equals(IAction? other) - { - if (other is not ExportToGltfAction rhs) - return false; - - // TODO: compare configuration and such - return true; - } - } - - private partial class ImportGltfAction(string inputPath) : IAction - { - public MdlFile? Out; - public readonly IoNotifier Notifier = new(); - - public void Execute(CancellationToken cancel) - { - var model = Schema2.ModelRoot.Load(inputPath); - - Out = ModelImporter.Import(model, Notifier); - } - - public bool Equals(IAction? other) - { - if (other is not ImportGltfAction rhs) - return false; - - return true; - } - } -} +using Dalamud.Plugin.Services; +using Lumina.Data.Parsing; +using Luna; +using OtterGui.Tasks; +using Penumbra.Collections.Manager; +using Penumbra.GameData; +using Penumbra.GameData.Data; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Files; +using Penumbra.GameData.Structs; +using Penumbra.Import.Models.Export; +using Penumbra.Import.Models.Import; +using Penumbra.Import.Textures; +using Penumbra.Meta; +using Penumbra.Meta.Files; +using Penumbra.Meta.Manipulations; +using SharpGLTF.Scenes; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace Penumbra.Import.Models; + +using Schema2 = SharpGLTF.Schema2; +using LuminaMaterial = Lumina.Models.Materials.Material; + +public sealed class ModelManager( + Logger log, + IFramework framework, + MetaFileManager metaFileManager, + ActiveCollections collections, + GamePathParser parser) + : SingleTaskQueue, IDisposable, IService +{ + public readonly Logger Log = log; + private readonly IFramework _framework = framework; + + private readonly ConcurrentDictionary _tasks = new(); + + private bool _disposed; + + public void Dispose() + { + _disposed = true; + foreach (var (_, cancel) in _tasks.Values.ToArray()) + cancel.Cancel(); + _tasks.Clear(); + } + + public Task ExportToGltf(in ExportConfig config, MdlFile mdl, IEnumerable sklbPaths, Func read, + string outputPath) + => EnqueueWithResult( + new ExportToGltfAction(this, config, mdl, sklbPaths, read, outputPath), + action => action.Notifier + ); + + public Task<(MdlFile?, IoNotifier)> ImportGltf(string inputPath) + => EnqueueWithResult( + new ImportGltfAction(this, inputPath), + action => (action.Out, action.Notifier) + ); + + /// Try to find the .sklb paths for a .mdl file. + /// .mdl file to look up the skeletons for. + /// Modified extra skeleton template parameters. + public string[] ResolveSklbsForMdl(string mdlPath, KeyValuePair[] estManipulations) + { + var info = parser.GetFileInfo(mdlPath); + if (info.FileType is not FileType.Model) + return []; + + var baseSkeleton = GamePaths.Sklb.Customization(info.GenderRace, "base", 1); + + return info.ObjectType switch + { + ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Body, info, estManipulations)], + ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Head, info, estManipulations)], + ObjectType.Equipment => [baseSkeleton], + ObjectType.Accessory => [baseSkeleton], + ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], + ObjectType.Character when info.BodySlot is BodySlot.Hair + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Hair, info, estManipulations)], + ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear + => [baseSkeleton, ..ResolveEstSkeleton(EstType.Face, info, estManipulations)], + ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), + ObjectType.DemiHuman => [GamePaths.Sklb.DemiHuman(info.PrimaryId)], + ObjectType.Monster => [GamePaths.Sklb.Monster(info.PrimaryId)], + ObjectType.Weapon => [GamePaths.Sklb.Weapon(info.PrimaryId)], + _ => [], + }; + } + + private string[] ResolveEstSkeleton(EstType type, GameObjectInfo info, KeyValuePair[] estManipulations) + { + // Try to find an EST entry from the manipulations provided. + var modEst = estManipulations + .FirstOrNull(est => est.Key.GenderRace == info.GenderRace + && est.Key.Slot == type + && est.Key.SetId == info.PrimaryId + ); + + // Try to use an entry from provided manipulations, falling back to the current collection. + var targetId = modEst?.Value + ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) + ?? EstFile.GetDefault(metaFileManager, type, info.GenderRace, info.PrimaryId); + + // If there's no entries, we can assume that there's no additional skeleton. + if (targetId == EstEntry.Zero) + return []; + + return [GamePaths.Sklb.Customization(info.GenderRace, type.ToName(), targetId.AsId)]; + } + + /// Try to resolve the absolute path to a .mtrl from the potentially-partial path provided by a model. + private string? ResolveMtrlPath(string rawPath, IoNotifier notifier) + { + // TODO: this should probably be chosen in the export settings + var variantId = 1; + + // Get standardised paths + var absolutePath = rawPath.StartsWith('/') + ? LuminaMaterial.ResolveRelativeMaterialPath(rawPath, variantId) + : rawPath; + var relativePath = rawPath.StartsWith('/') + ? rawPath + : '/' + Path.GetFileName(rawPath); + + if (absolutePath == null) + { + notifier.Warning($"Material path \"{rawPath}\" could not be resolved."); + return null; + } + + var info = parser.GetFileInfo(absolutePath); + if (info.FileType is not FileType.Material) + { + notifier.Warning($"Material path {rawPath} does not conform to material conventions."); + return null; + } + + var resolvedPath = info.ObjectType switch + { + ObjectType.Character => GamePaths.Mtrl.Customization( + info.GenderRace, info.BodySlot, info.PrimaryId, relativePath, out _, out _, info.Variant), + _ => absolutePath, + }; + + Penumbra.Log.Debug($"Resolved material {rawPath} to {resolvedPath}"); + + return resolvedPath; + } + + private Task Enqueue(IAction action) + { + if (_disposed) + return Task.FromException(new ObjectDisposedException(nameof(ModelManager))); + + Task task; + lock (_tasks) + { + task = _tasks.GetOrAdd(action, a => + { + var token = new CancellationTokenSource(); + var t = Enqueue(a, token.Token); + t.ContinueWith(_ => + { + lock (_tasks) + { + return _tasks.TryRemove(a, out var unused); + } + }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + return (t, token); + }).Item1; + } + + return task; + } + + private Task EnqueueWithResult(TAction action, Func process) + where TAction : IAction + => Enqueue(action).ContinueWith(task => + { + if (task is { IsFaulted: true, Exception: not null }) + throw task.Exception; + + return process(action); + }, TaskScheduler.Default); + + private class ExportToGltfAction( + ModelManager manager, + ExportConfig config, + MdlFile mdl, + IEnumerable sklbPaths, + Func read, + string outputPath) + : IAction + { + public readonly IoNotifier Notifier = new(manager.Log); + + public void Execute(CancellationToken cancel) + { + Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}..."); + + Penumbra.Log.Debug("[GLTF Export] Reading skeletons..."); + var xivSkeletons = BuildSkeletons(cancel); + + Penumbra.Log.Debug("[GLTF Export] Reading materials..."); + var materials = mdl.Materials + .Select(path => (path, material: BuildMaterial(path, Notifier, cancel))) + .Where(pair => pair.material != null) + .ToDictionary(pair => pair.path, pair => pair.material!.Value); + + Penumbra.Log.Debug("[GLTF Export] Converting model..."); + var model = ModelExporter.Export(config, mdl, xivSkeletons, materials, Notifier); + + Penumbra.Log.Debug("[GLTF Export] Building scene..."); + var scene = new SceneBuilder(); + model.AddToScene(scene); + + Penumbra.Log.Debug("[GLTF Export] Saving..."); + var gltfModel = scene.ToGltf2(); + gltfModel.Save(outputPath); + Penumbra.Log.Debug("[GLTF Export] Done."); + } + + /// Attempt to read out the pertinent information from the sklb file paths provided. + private IEnumerable BuildSkeletons(CancellationToken cancel) + { + // We're intentionally filtering failed reads here - the failure will + // be picked up, if relevant, when the model tries to create mappings + // for a bone in the failed sklb. + var havokTasks = sklbPaths + .Select(read) + .Where(bytes => bytes != null) + .Select(bytes => new SklbFile(bytes!)) + .Index() + .Select(CreateHavokTask) + .ToArray(); + + // Result waits automatically. + return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result)); + + // The havok methods we're relying on for this conversion are a bit + // finicky at the best of times, and can outright cause a CTD if they + // get upset. Running each conversion on its own tick seems to make + // this consistently non-crashy across my testing. + Task CreateHavokTask((int Index, SklbFile Sklb) pair) + => manager._framework.RunOnTick( + () => HavokConverter.HkxToXml(pair.Sklb.Skeleton), + delayTicks: pair.Index, cancellationToken: cancel); + } + + /// Read a .mtrl and populate its textures. + private MaterialExporter.Material? BuildMaterial(string relativePath, IoNotifier notifier, CancellationToken cancel) + { + var path = manager.ResolveMtrlPath(relativePath, notifier); + if (path == null) + return null; + + var bytes = read(path); + if (bytes == null) + return null; + + var mtrl = new MtrlFile(bytes); + + return new MaterialExporter.Material + { + Mtrl = mtrl, + Textures = mtrl.ShaderPackage.Samplers.ToDictionary( + sampler => (TextureUsage)sampler.SamplerId, + sampler => ConvertImage(mtrl.Textures[sampler.TextureIndex], cancel) + ), + }; + } + + /// Read a texture referenced by a .mtrl and convert it into an ImageSharp image. + private Image ConvertImage(MtrlFile.Texture texture, CancellationToken cancel) + { + // Work out the texture's path - the DX11 material flag controls a file name prefix. + GamePaths.Tex.HandleDx11Path(texture, out var texturePath); + var bytes = read(texturePath); + if (bytes == null) + return CreateDummyImage(); + + using var textureData = new MemoryStream(bytes); + var image = TexFileParser.Parse(textureData); + var pngImage = TextureManager.ConvertToPng(image, cancel).AsPng; + return pngImage ?? throw new Exception("Failed to convert texture to png."); + } + + private static Image CreateDummyImage() + { + var image = new Image(1, 1); + image[0, 0] = Color.White; + return image; + } + + public bool Equals(IAction? other) + { + if (other is not ExportToGltfAction) + return false; + + // TODO: compare configuration and such + return true; + } + } + + private class ImportGltfAction(ModelManager manager, string inputPath) : IAction + { + public MdlFile? Out; + public readonly IoNotifier Notifier = new(manager.Log); + + public void Execute(CancellationToken cancel) + { + var model = Schema2.ModelRoot.Load(inputPath); + + Out = ModelImporter.Import(model, Notifier); + } + + public bool Equals(IAction? other) + { + if (other is not ImportGltfAction) + return false; + + return true; + } + } +} diff --git a/Penumbra/Import/TexToolsImporter.Archives.cs b/Penumbra/Import/TexToolsImporter.Archives.cs index 0040a784..21e9e6d1 100644 --- a/Penumbra/Import/TexToolsImporter.Archives.cs +++ b/Penumbra/Import/TexToolsImporter.Archives.cs @@ -1,10 +1,9 @@ using Dalamud.Utility; +using Luna; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using OtterGui.Filesystem; using Penumbra.Import.Structs; using Penumbra.Mods; -using Penumbra.Services; using SharpCompress.Archives; using SharpCompress.Archives.Rar; using SharpCompress.Archives.SevenZip; @@ -16,7 +15,7 @@ namespace Penumbra.Import; public partial class TexToolsImporter { - private static readonly ExtractionOptions _extractionOptions = new() + private static readonly ExtractionOptions ExtractionOptions = new() { ExtractFullPath = true, Overwrite = true, @@ -79,7 +78,7 @@ public partial class TexToolsImporter using var t = new StreamReader(s); using var j = new JsonTextReader(t); var obj = JObject.Load(j); - name = obj[nameof(Mod.Name)]?.Value()?.RemoveInvalidPathSymbols() ?? string.Empty; + name = obj[nameof(Mod.Name)]?.Value()?.RemoveInvalidFileNameSymbols() ?? string.Empty; if (name.Length == 0) throw new Exception("Invalid mod archive: mod meta has no name."); @@ -142,16 +141,16 @@ public partial class TexToolsImporter switch (Path.GetExtension(reader.Entry.Key)) { case ".mdl": - _migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); + _migrationManager.MigrateMdlDuringExtraction(reader, _currentModDirectory!.FullName, ExtractionOptions); break; case ".mtrl": - _migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions); + _migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, ExtractionOptions); break; case ".tex": - _migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions); + _migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, ExtractionOptions); break; default: - reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions); + reader.WriteEntryToDirectory(_currentModDirectory!.FullName, ExtractionOptions); break; } } diff --git a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs index 21003104..46eef5f5 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Manipulation.cs @@ -1,9 +1,5 @@ -using Dalamud.Bindings.ImGui; -using OtterGui.Raii; -using OtterGui; -using Dalamud.Interface.Utility; using ImSharp; -using Penumbra.UI; +using OtterGui.Text; using Rgba32 = SixLabors.ImageSharp.PixelFormats.Rgba32; namespace Penumbra.Import.Textures; @@ -29,20 +25,19 @@ public partial class CombinedTexture private const float BWeight = 0.0722f; // @formatter:off - private static readonly IReadOnlyList<(string Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = - new[] - { - ("No Transform (Identity)", Matrix4x4.Identity, Vector4.Zero ), - ("Grayscale (Average)", new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), - ("Grayscale (Weighted)", new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), - ("Grayscale (Average) to Alpha", new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), - ("Grayscale (Weighted) to Alpha", new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), - ("Make Opaque (Drop Alpha)", new Matrix4x4(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), - ("Extract Red", new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), - ("Extract Green", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), - ("Extract Blue", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), - ("Extract Alpha", new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW ), - }; + private static readonly IReadOnlyList<(StringU8 Label, Matrix4x4 Multiplier, Vector4 Constant)> PredefinedColorTransforms = + [ + (new StringU8("No Transform (Identity)"u8), Matrix4x4.Identity, Vector4.Zero ), + (new StringU8("Grayscale (Average)"u8), new Matrix4x4(OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), + (new StringU8("Grayscale (Weighted)"u8), new Matrix4x4(RWeight, RWeight, RWeight, 0.0f, GWeight, GWeight, GWeight, 0.0f, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f), Vector4.Zero ), + (new StringU8("Grayscale (Average) to Alpha"u8), new Matrix4x4(OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, OneThird, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + (new StringU8("Grayscale (Weighted) to Alpha"u8), new Matrix4x4(RWeight, RWeight, RWeight, RWeight, GWeight, GWeight, GWeight, GWeight, BWeight, BWeight, BWeight, BWeight, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.Zero ), + (new StringU8("Make Opaque (Drop Alpha)"u8), new Matrix4x4(1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + (new StringU8("Extract Red"u8), new Matrix4x4(1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + (new StringU8("Extract Green"u8), new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + (new StringU8("Extract Blue"u8), new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f), Vector4.UnitW ), + (new StringU8("Extract Alpha"u8), new Matrix4x4(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f), Vector4.UnitW ), + ]; // @formatter:on private Vector4 DataLeft(int offset) @@ -211,15 +206,15 @@ public partial class CombinedTexture return transformed; } - private static bool DragFloat(string label, float width, ref float value) + private static bool DragFloat(Utf8StringHandler label, float width, ref float value) { var tmp = value; - ImGui.TableNextColumn(); - ImGui.SetNextItemWidth(width); - if (ImGui.DragFloat(label, ref tmp, 0.001f, -1f, 1f)) + Im.Table.NextColumn(); + Im.Item.SetNextWidth(width); + if (Im.Drag(label, ref tmp, speed: 0.001f, min: -1f, max: 1f)) value = tmp; - return ImGui.IsItemDeactivatedAfterEdit(); + return Im.Item.DeactivatedAfterEdit; } public void DrawMatrixInputLeft(float width) @@ -230,53 +225,69 @@ public partial class CombinedTexture Update(); } + private sealed class CombineOperationCombo() : SimpleFilterCombo(SimpleFilterType.None) + { + private static readonly CombineOp[] UserValues = Enum.GetValues().Where(c => (int)c >= 0).ToArray(); + + public override StringU8 DisplayString(in CombineOp value) + => new(value.ToLabelU8()); + + public override string FilterString(in CombineOp value) + => value.ToLabel(); + + public override IEnumerable GetBaseItems() + => UserValues; + + public override StringU8 Tooltip(in CombineOp value) + => new(value.Tooltip()); + } + + private sealed class ResizeOperationCombo() : SimpleFilterCombo(SimpleFilterType.None) + { + private static readonly ResizeOp[] UserValues = Enum.GetValues().Where(c => (int)c >= 0).ToArray(); + + public override StringU8 DisplayString(in ResizeOp value) + => new(value.ToLabelU8()); + + public override string FilterString(in ResizeOp value) + => value.ToLabel(); + + public override IEnumerable GetBaseItems() + => UserValues; + } + + private readonly CombineOperationCombo _combineCombo = new(); + private readonly ResizeOperationCombo _resizeCombo = new(); + public void DrawMatrixInputRight(float width) { var ret = DrawMatrixInput(ref _multiplierRight, ref _constantRight, width); ret |= DrawMatrixTools(ref _multiplierRight, ref _constantRight); - ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); - ImGui.DragInt("##XOffset", ref _offsetX, 0.5f); - ret |= ImGui.IsItemDeactivatedAfterEdit(); + Im.Item.SetNextWidthScaled(75); + Im.Drag("##XOffset"u8, ref _offsetX, speed: 0.5f); + ret |= Im.Item.DeactivatedAfterEdit; Im.Line.Same(); - ImGui.SetNextItemWidth(75.0f * UiHelpers.Scale); - ImGui.DragInt("Offsets##YOffset", ref _offsetY, 0.5f); - ret |= ImGui.IsItemDeactivatedAfterEdit(); - - ImGui.SetNextItemWidth(200.0f * UiHelpers.Scale); - using (var c = ImRaii.Combo("Combine Operation", CombineOpLabels[(int)_combineOp])) - { - if (c) - foreach (var op in Enum.GetValues()) - { - if ((int)op < 0) // Negative codes are for internal use only. - continue; - - if (ImGui.Selectable(CombineOpLabels[(int)op], op == _combineOp)) - { - _combineOp = op; - ret = true; - } - - ImGuiUtil.SelectableHelpMarker(CombineOpTooltips[(int)op]); - } - } + Im.Item.SetNextWidthScaled(75); + Im.Drag("Offsets##YOffset"u8, ref _offsetY, speed: 0.5f); + ret |= Im.Item.DeactivatedAfterEdit; + Im.Item.SetNextWidthScaled(200); + ret |= _combineCombo.Draw("Combine Operation"u8, ref _combineOp, StringU8.Empty, 200 * Im.Style.GlobalScale); var resizeOp = GetActualResizeOp(_resizeOp, _combineOp); - using (var dis = ImRaii.Disabled((int)resizeOp < 0)) + using (Im.Disabled((int)resizeOp < 0)) { - ret |= ImGuiUtil.GenericEnumCombo("Resizing Mode", 200.0f * UiHelpers.Scale, _resizeOp, out _resizeOp, - Enum.GetValues().Where(op => (int)op >= 0), op => ResizeOpLabels[(int)op]); + ret |= _resizeCombo.Draw("Resizing Mode"u8, ref _resizeOp, StringU8.Empty, 200 * Im.Style.GlobalScale); } - using (var dis = ImRaii.Disabled(_combineOp != CombineOp.CopyChannels)) + using (Im.Disabled(_combineOp != CombineOp.CopyChannels)) { - ImGui.TextUnformatted("Copy"); + Im.Text("Copy"u8); foreach (var channel in Enum.GetValues()) { Im.Line.Same(); var copy = (_copyChannels & channel) != 0; - if (ImGui.Checkbox(channel.ToString(), ref copy)) + if (Im.Checkbox(channel.ToString(), ref copy)) { _copyChannels = copy ? _copyChannels | channel : _copyChannels & ~channel; ret = true; @@ -290,62 +301,52 @@ public partial class CombinedTexture private static bool DrawMatrixInput(ref Matrix4x4 multiplier, ref Vector4 constant, float width) { - using var table = ImRaii.Table(string.Empty, 5, ImGuiTableFlags.BordersInner | ImGuiTableFlags.SizingFixedFit); + using var table = Im.Table.Begin(StringU8.Empty, 5, TableFlags.BordersInner | TableFlags.SizingFixedFit); if (!table) return false; var changes = false; - ImGui.TableNextColumn(); - ImGui.TableNextColumn(); - ImGuiUtil.Center("R"); - ImGui.TableNextColumn(); - ImGuiUtil.Center("G"); - ImGui.TableNextColumn(); - ImGuiUtil.Center("B"); - ImGui.TableNextColumn(); - ImGuiUtil.Center("A"); + table.NextColumn(); + table.NextColumn(); + ImEx.TextCentered("R"u8); + table.NextColumn(); + ImEx.TextCentered("G"u8); + table.NextColumn(); + ImEx.TextCentered("B"u8); + table.NextColumn(); + ImEx.TextCentered("A"u8); var inputWidth = width / 6; - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("R "); - changes |= DragFloat("##RR", inputWidth, ref multiplier.M11); - changes |= DragFloat("##RG", inputWidth, ref multiplier.M12); - changes |= DragFloat("##RB", inputWidth, ref multiplier.M13); - changes |= DragFloat("##RA", inputWidth, ref multiplier.M14); + table.DrawFrameColumn("R "u8); + changes |= DragFloat("##RR"u8, inputWidth, ref multiplier.M11); + changes |= DragFloat("##RG"u8, inputWidth, ref multiplier.M12); + changes |= DragFloat("##RB"u8, inputWidth, ref multiplier.M13); + changes |= DragFloat("##RA"u8, inputWidth, ref multiplier.M14); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("G "); - changes |= DragFloat("##GR", inputWidth, ref multiplier.M21); - changes |= DragFloat("##GG", inputWidth, ref multiplier.M22); - changes |= DragFloat("##GB", inputWidth, ref multiplier.M23); - changes |= DragFloat("##GA", inputWidth, ref multiplier.M24); + table.DrawFrameColumn("G "u8); + changes |= DragFloat("##GR"u8, inputWidth, ref multiplier.M21); + changes |= DragFloat("##GG"u8, inputWidth, ref multiplier.M22); + changes |= DragFloat("##GB"u8, inputWidth, ref multiplier.M23); + changes |= DragFloat("##GA"u8, inputWidth, ref multiplier.M24); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("B "); - changes |= DragFloat("##BR", inputWidth, ref multiplier.M31); - changes |= DragFloat("##BG", inputWidth, ref multiplier.M32); - changes |= DragFloat("##BB", inputWidth, ref multiplier.M33); - changes |= DragFloat("##BA", inputWidth, ref multiplier.M34); + table.DrawFrameColumn("B "u8); + changes |= DragFloat("##BR"u8, inputWidth, ref multiplier.M31); + changes |= DragFloat("##BG"u8, inputWidth, ref multiplier.M32); + changes |= DragFloat("##BB"u8, inputWidth, ref multiplier.M33); + changes |= DragFloat("##BA"u8, inputWidth, ref multiplier.M34); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("A "); - changes |= DragFloat("##AR", inputWidth, ref multiplier.M41); - changes |= DragFloat("##AG", inputWidth, ref multiplier.M42); - changes |= DragFloat("##AB", inputWidth, ref multiplier.M43); - changes |= DragFloat("##AA", inputWidth, ref multiplier.M44); + table.DrawFrameColumn("A "u8); + changes |= DragFloat("##AR"u8, inputWidth, ref multiplier.M41); + changes |= DragFloat("##AG"u8, inputWidth, ref multiplier.M42); + changes |= DragFloat("##AB"u8, inputWidth, ref multiplier.M43); + changes |= DragFloat("##AA"u8, inputWidth, ref multiplier.M44); - ImGui.TableNextColumn(); - ImGui.AlignTextToFramePadding(); - ImGui.Text("1 "); - changes |= DragFloat("##1R", inputWidth, ref constant.X); - changes |= DragFloat("##1G", inputWidth, ref constant.Y); - changes |= DragFloat("##1B", inputWidth, ref constant.Z); - changes |= DragFloat("##1A", inputWidth, ref constant.W); + table.DrawFrameColumn("1 "u8); + changes |= DragFloat("##1R"u8, inputWidth, ref constant.X); + changes |= DragFloat("##1G"u8, inputWidth, ref constant.Y); + changes |= DragFloat("##1B"u8, inputWidth, ref constant.Z); + changes |= DragFloat("##1A"u8, inputWidth, ref constant.W); return changes; } @@ -354,28 +355,28 @@ public partial class CombinedTexture { var changes = PresetCombo(ref multiplier, ref constant); Im.Line.Same(); - ImGui.Dummy(ImGuiHelpers.ScaledVector2(20, 0)); + Im.ScaledDummy(20); Im.Line.Same(); - ImGui.TextUnformatted("Invert"); + Im.Text("Invert"u8); Im.Line.Same(); Channels channels = 0; - if (ImGui.Button("Colors")) + if (Im.Button("Colors"u8)) channels |= Channels.Red | Channels.Green | Channels.Blue; Im.Line.Same(); - if (ImGui.Button("R")) + if (Im.Button("R"u8)) channels |= Channels.Red; Im.Line.Same(); - if (ImGui.Button("G")) + if (Im.Button("G"u8)) channels |= Channels.Green; Im.Line.Same(); - if (ImGui.Button("B")) + if (Im.Button("B"u8)) channels |= Channels.Blue; Im.Line.Same(); - if (ImGui.Button("A")) + if (Im.Button("A"u8)) channels |= Channels.Alpha; changes |= InvertChannels(channels, ref multiplier, ref constant); @@ -384,14 +385,14 @@ public partial class CombinedTexture private static bool PresetCombo(ref Matrix4x4 multiplier, ref Vector4 constant) { - using var combo = ImRaii.Combo("Presets", string.Empty, ImGuiComboFlags.NoPreview); + using var combo = Im.Combo.Begin("Presets"u8, StringU8.Empty, ComboFlags.NoPreview); if (!combo) return false; var ret = false; foreach (var (label, preMultiplier, preConstant) in PredefinedColorTransforms) { - if (!ImGui.Selectable(label, multiplier == preMultiplier && constant == preConstant)) + if (!Im.Selectable(label, multiplier == preMultiplier && constant == preConstant)) continue; multiplier = preMultiplier; diff --git a/Penumbra/Import/Textures/CombinedTexture.Operations.cs b/Penumbra/Import/Textures/CombinedTexture.Operations.cs index 8494f12b..c1e50885 100644 --- a/Penumbra/Import/Textures/CombinedTexture.Operations.cs +++ b/Penumbra/Import/Textures/CombinedTexture.Operations.cs @@ -1,146 +1,147 @@ -namespace Penumbra.Import.Textures; - -public partial class CombinedTexture -{ - private enum CombineOp - { - LeftMultiply = -4, - LeftCopy = -3, - RightCopy = -2, - Invalid = -1, - Over = 0, - Under = 1, - RightMultiply = 2, - CopyChannels = 3, - } - - private enum ResizeOp - { - LeftOnly = -2, - RightOnly = -1, - None = 0, - ToLeft = 1, - ToRight = 2, - } - - [Flags] - private enum Channels : byte - { - Red = 1, - Green = 2, - Blue = 4, - Alpha = 8, - } - - private static readonly IReadOnlyList CombineOpLabels = new[] - { - "Overlay over Input", - "Input over Overlay", - "Replace Input", - "Copy Channels", - }; - - private static readonly IReadOnlyList CombineOpTooltips = new[] - { - "Standard composition.\nApply the overlay over the input.", - "Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.", - "Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.", - "Replace some input channels with those from the overlay.\nUseful for Multi maps.", - }; - - private static readonly IReadOnlyList ResizeOpLabels = new string[] - { - "No Resizing", - "Adjust Overlay to Input", - "Adjust Input to Overlay", - }; - - private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) - => combineOp switch - { - CombineOp.LeftCopy => ResizeOp.LeftOnly, - CombineOp.LeftMultiply => ResizeOp.LeftOnly, - CombineOp.RightCopy => ResizeOp.RightOnly, - CombineOp.RightMultiply => ResizeOp.RightOnly, - CombineOp.Over => resizeOp, - CombineOp.Under => resizeOp, - CombineOp.CopyChannels => resizeOp, - _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), - }; - - private CombineOp GetActualCombineOp() - { - var combineOp = (_left.IsLoaded, _right.IsLoaded) switch - { - (true, true) => _combineOp, - (true, false) => CombineOp.LeftMultiply, - (false, true) => CombineOp.RightMultiply, - (false, false) => CombineOp.Invalid, - }; - - if (combineOp == CombineOp.CopyChannels) - { - if (_copyChannels == 0) - combineOp = CombineOp.LeftMultiply; - else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) - combineOp = CombineOp.RightMultiply; - } - - return combineOp switch - { - CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, - CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, - _ => combineOp, - }; - } - - - private static bool InvertChannels(Channels channels, ref Matrix4x4 multiplier, ref Vector4 constant) - { - if (channels.HasFlag(Channels.Red)) - InvertRed(ref multiplier, ref constant); - if (channels.HasFlag(Channels.Green)) - InvertGreen(ref multiplier, ref constant); - if (channels.HasFlag(Channels.Blue)) - InvertBlue(ref multiplier, ref constant); - if (channels.HasFlag(Channels.Alpha)) - InvertAlpha(ref multiplier, ref constant); - return channels != 0; - } - - private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M11 = -multiplier.M11; - multiplier.M21 = -multiplier.M21; - multiplier.M31 = -multiplier.M31; - multiplier.M41 = -multiplier.M41; - constant.X = 1.0f - constant.X; - } - - private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M12 = -multiplier.M12; - multiplier.M22 = -multiplier.M22; - multiplier.M32 = -multiplier.M32; - multiplier.M42 = -multiplier.M42; - constant.Y = 1.0f - constant.Y; - } - - private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M13 = -multiplier.M13; - multiplier.M23 = -multiplier.M23; - multiplier.M33 = -multiplier.M33; - multiplier.M43 = -multiplier.M43; - constant.Z = 1.0f - constant.Z; - } - - private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) - { - multiplier.M14 = -multiplier.M14; - multiplier.M24 = -multiplier.M24; - multiplier.M34 = -multiplier.M34; - multiplier.M44 = -multiplier.M44; - constant.W = 1.0f - constant.W; - } -} +using Luna.Generators; + +namespace Penumbra.Import.Textures; + +public partial class CombinedTexture +{ + [NamedEnum("ToLabel")] + [TooltipEnum] + public enum CombineOp + { + LeftMultiply = -4, + LeftCopy = -3, + RightCopy = -2, + Invalid = -1, + + [Name("Overlay over Input")] + [Tooltip("Standard composition.\nApply the overlay over the input.")] + Over = 0, + + [Name("Input over Overlay")] + [Tooltip("Standard composition, reversed.\nApply the input over the overlay ; can be used to fix some wrong imports.")] + Under = 1, + + [Name("Replace Input")] + [Tooltip("Completely replace the input with the overlay.\nCan be used to select the destination file as input and the source file as overlay.")] + RightMultiply = 2, + + [Name("Copy Channels")] + [Tooltip("Replace some input channels with those from the overlay.\nUseful for Multi maps.")] + CopyChannels = 3, + } + + [NamedEnum("ToLabel")] + public enum ResizeOp + { + LeftOnly = -2, + RightOnly = -1, + + [Name("No Resizing")] + None = 0, + + [Name("Adjust Overlay to Input")] + ToLeft = 1, + + [Name("Adjust Input to Overlay")] + ToRight = 2, + } + + [Flags] + [NamedEnum] + public enum Channels : byte + { + Red = 1, + Green = 2, + Blue = 4, + Alpha = 8, + } + + private static ResizeOp GetActualResizeOp(ResizeOp resizeOp, CombineOp combineOp) + => combineOp switch + { + CombineOp.LeftCopy => ResizeOp.LeftOnly, + CombineOp.LeftMultiply => ResizeOp.LeftOnly, + CombineOp.RightCopy => ResizeOp.RightOnly, + CombineOp.RightMultiply => ResizeOp.RightOnly, + CombineOp.Over => resizeOp, + CombineOp.Under => resizeOp, + CombineOp.CopyChannels => resizeOp, + _ => throw new ArgumentException($"Invalid combine operation {combineOp}"), + }; + + private CombineOp GetActualCombineOp() + { + var combineOp = (_left.IsLoaded, _right.IsLoaded) switch + { + (true, true) => _combineOp, + (true, false) => CombineOp.LeftMultiply, + (false, true) => CombineOp.RightMultiply, + (false, false) => CombineOp.Invalid, + }; + + if (combineOp == CombineOp.CopyChannels) + { + if (_copyChannels == 0) + combineOp = CombineOp.LeftMultiply; + else if (_copyChannels == (Channels.Red | Channels.Green | Channels.Blue | Channels.Alpha)) + combineOp = CombineOp.RightMultiply; + } + + return combineOp switch + { + CombineOp.LeftMultiply when _multiplierLeft.IsIdentity && _constantLeft == Vector4.Zero => CombineOp.LeftCopy, + CombineOp.RightMultiply when _multiplierRight.IsIdentity && _constantRight == Vector4.Zero => CombineOp.RightCopy, + _ => combineOp, + }; + } + + + private static bool InvertChannels(Channels channels, ref Matrix4x4 multiplier, ref Vector4 constant) + { + if (channels.HasFlag(Channels.Red)) + InvertRed(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Green)) + InvertGreen(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Blue)) + InvertBlue(ref multiplier, ref constant); + if (channels.HasFlag(Channels.Alpha)) + InvertAlpha(ref multiplier, ref constant); + return channels != 0; + } + + private static void InvertRed(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M11 = -multiplier.M11; + multiplier.M21 = -multiplier.M21; + multiplier.M31 = -multiplier.M31; + multiplier.M41 = -multiplier.M41; + constant.X = 1.0f - constant.X; + } + + private static void InvertGreen(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M12 = -multiplier.M12; + multiplier.M22 = -multiplier.M22; + multiplier.M32 = -multiplier.M32; + multiplier.M42 = -multiplier.M42; + constant.Y = 1.0f - constant.Y; + } + + private static void InvertBlue(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M13 = -multiplier.M13; + multiplier.M23 = -multiplier.M23; + multiplier.M33 = -multiplier.M33; + multiplier.M43 = -multiplier.M43; + constant.Z = 1.0f - constant.Z; + } + + private static void InvertAlpha(ref Matrix4x4 multiplier, ref Vector4 constant) + { + multiplier.M14 = -multiplier.M14; + multiplier.M24 = -multiplier.M24; + multiplier.M34 = -multiplier.M34; + multiplier.M44 = -multiplier.M44; + constant.W = 1.0f - constant.W; + } +} diff --git a/Penumbra/Import/Textures/PathSelectCombo.cs b/Penumbra/Import/Textures/PathSelectCombo.cs new file mode 100644 index 00000000..25539c9a --- /dev/null +++ b/Penumbra/Import/Textures/PathSelectCombo.cs @@ -0,0 +1,80 @@ +using Dalamud.Plugin.Services; +using ImSharp; +using Penumbra.Api.Enums; +using Penumbra.Interop.ResourceTree; +using Penumbra.Mods.Editor; +using Penumbra.UI.Classes; + +namespace Penumbra.Import.Textures; + +public abstract class PathSelectCombo(IDataManager dataManager) : FilterComboBase +{ + public bool Draw(Utf8StringHandler label, Utf8StringHandler tooltip, string current, + int skipPrefix, out string newPath) + { + _skipPrefix = skipPrefix; + _selected = current; + if (!base.Draw(label, current.Length > 0 ? current : "Choose a modded texture from this mod here..."u8, tooltip, + Im.ContentRegion.Available.X, out var ret)) + { + newPath = string.Empty; + return false; + } + + newPath = ret.SearchPath; + return true; + } + + public record PathData(StringU8 Path, string SearchPath, bool IsOnPlayer, bool IsGame); + private int _skipPrefix; + private string _selected = string.Empty; + + protected abstract IEnumerable GetFiles(); + protected abstract ISet GetPlayerResources(); + + protected override IEnumerable GetItems() + { + var playerResources = GetPlayerResources(); + var files = GetFiles(); + + foreach (var (file, game) in files.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) + .Prepend((f.File.FullName, false))) + .Where(p => p.Item2 ? dataManager.FileExists(p.Item1) : File.Exists(p.Item1))) + { + var onPlayer = playerResources.Contains(file); + var displayString = game ? new StringU8($"--> {file}") : new StringU8(file.AsSpan(_skipPrefix)); + yield return new PathData(displayString, file, onPlayer, game); + } + } + + protected override float ItemHeight + => Im.Style.TextHeightWithSpacing; + + protected override bool DrawItem(in PathData item, int globalIndex, bool selected) + { + var textColor = item.IsOnPlayer ? ColorId.HandledConflictMod.Value() : + item.IsGame ? ColorId.FolderExpanded.Value() : ColorParameter.Default; + bool ret; + using (ImGuiColor.Text.Push(textColor)) + { + ret = Im.Selectable(item.Path, selected); + } + + Im.Tooltip.OnHover(item.IsGame + ? "This is a game path and refers to an unmanipulated file from your game data."u8 + : "This is a path to a modded file on your file system."u8); + return ret; + } + + protected override bool IsSelected(PathData item, int globalIndex) + => string.Equals(_selected, item.SearchPath, StringComparison.OrdinalIgnoreCase); +} + +public sealed class TextureSelectCombo(ResourceTreeFactory resources, ModEditor editor, IDataManager dataManager) : PathSelectCombo(dataManager) +{ + protected override IEnumerable GetFiles() + => editor.Files.Tex; + + protected override ISet GetPlayerResources() + => ResourceTreeApiHelper.GetPlayerResourcesOfType(resources, ResourceType.Tex); +} diff --git a/Penumbra/Import/Textures/TextureDrawer.cs b/Penumbra/Import/Textures/TextureDrawer.cs index 81b39d27..ba217209 100644 --- a/Penumbra/Import/Textures/TextureDrawer.cs +++ b/Penumbra/Import/Textures/TextureDrawer.cs @@ -1,168 +1,104 @@ -using Dalamud.Bindings.ImGui; -using Dalamud.Interface; -using ImSharp; -using Lumina.Data.Files; -using OtterGui; -using OtterGui.Raii; -using OtterGui.Widgets; -using OtterTex; -using Penumbra.Mods.Editor; -using Penumbra.String.Classes; -using Penumbra.UI; -using Penumbra.UI.Classes; -using MouseWheelType = OtterGui.Widgets.MouseWheelType; - -namespace Penumbra.Import.Textures; - -public static class TextureDrawer -{ - public static void Draw(Texture texture, Vector2 size) - { - if (texture.TextureWrap != null) - { - size = texture.TextureWrap.Size.Contain(size); - - ImGui.Image(texture.TextureWrap.Handle, size); - DrawData(texture); - } - else if (texture.LoadError != null) - { - const string link = "https://aka.ms/vcredist"; - ImGui.TextUnformatted("Could not load file:"); - - if (texture.LoadError is DllNotFoundException) - { - ImGuiUtil.TextColored(Colors.RegexWarningBorder, - "A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable."); - if (ImGui.Button("Microsoft VC Redistributables")) - Dalamud.Utility.Util.OpenLink(link); - ImGuiUtil.HoverTooltip($"Open {link} in your browser."); - } - - ImGuiUtil.TextColored(Colors.RegexWarningBorder, texture.LoadError.ToString()); - } - } - - public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, string label, string hint, string tooltip, - string startPath, FileDialogService fileDialog, string defaultModImportPath) - { - tmpPath ??= current.Path; - using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, - new Vector2(UiHelpers.ScaleX3, ImGui.GetStyle().ItemSpacing.Y)); - ImGui.SetNextItemWidth(-2 * ImGui.GetFrameHeight() - 7 * UiHelpers.Scale); - ImGui.InputTextWithHint(label, hint, ref tmpPath, Utf8GamePath.MaxGamePathLength); - if (ImGui.IsItemDeactivatedAfterEdit()) - current.Load(textures, tmpPath); - - ImGuiUtil.HoverTooltip(tooltip); - Im.Line.Same(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Folder.ToIconString(), new Vector2(ImGui.GetFrameHeight()), string.Empty, false, - true)) - { - if (defaultModImportPath.Length > 0) - startPath = defaultModImportPath; - - void UpdatePath(bool success, List paths) - { - if (success && paths.Count > 0) - current.Load(textures, paths[0]); - } - - fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false); - } - - Im.Line.Same(); - if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Recycle.ToIconString(), new Vector2(ImGui.GetFrameHeight()), - "Reload the currently selected path.", false, - true)) - current.Reload(textures); - } - - private static void DrawData(Texture texture) - { - using var table = ImRaii.Table("##data", 2, ImGuiTableFlags.SizingFixedFit); - ImGuiUtil.DrawTableColumn("Width"); - ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Width.ToString()); - ImGuiUtil.DrawTableColumn("Height"); - ImGuiUtil.DrawTableColumn(texture.TextureWrap!.Height.ToString()); - ImGuiUtil.DrawTableColumn("File Type"); - ImGuiUtil.DrawTableColumn(texture.Type.ToString()); - ImGuiUtil.DrawTableColumn("Bitmap Size"); - ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)"); - switch (texture.BaseImage.Image) - { - case ScratchImage s: - ImGuiUtil.DrawTableColumn("Format"); - ImGuiUtil.DrawTableColumn(s.Meta.Format.ToString()); - ImGuiUtil.DrawTableColumn("Mip Levels"); - ImGuiUtil.DrawTableColumn(s.Meta.MipLevels.ToString()); - ImGuiUtil.DrawTableColumn("Data Size"); - ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)"); - ImGuiUtil.DrawTableColumn("Number of Images"); - ImGuiUtil.DrawTableColumn(s.Images.Length.ToString()); - break; - case TexFile t: - ImGuiUtil.DrawTableColumn("Format"); - ImGuiUtil.DrawTableColumn(t.Header.Format.ToString()); - ImGuiUtil.DrawTableColumn("Mip Levels"); - ImGuiUtil.DrawTableColumn(t.Header.MipCount.ToString()); - ImGuiUtil.DrawTableColumn("Data Size"); - ImGuiUtil.DrawTableColumn($"{Functions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); - break; - } - } - - public sealed class PathSelectCombo(TextureManager textures, ModEditor editor, Func> getPlayerResources) - : FilterComboCache<(string Path, bool Game, bool IsOnPlayer)>(() => CreateFiles(textures, editor, getPlayerResources), - MouseWheelType.None, Penumbra.Log) - { - private int _skipPrefix = 0; - - protected override string ToString((string Path, bool Game, bool IsOnPlayer) obj) - => obj.Path; - - protected override bool DrawSelectable(int globalIdx, bool selected) - { - var (path, game, isOnPlayer) = Items[globalIdx]; - bool ret; - using (var color = ImGuiColor.Text.Push(ColorId.FolderExpanded.Value(), game)) - { - color.Push(ImGuiColor.Text, ColorId.HandledConflictMod.Value(), isOnPlayer); - var equals = string.Equals(CurrentSelection.Path, path, StringComparison.OrdinalIgnoreCase); - var p = game ? $"--> {path}" : path[_skipPrefix..]; - ret = ImGui.Selectable(p, selected) && !equals; - } - - ImGuiUtil.HoverTooltip(game - ? "This is a game path and refers to an unmanipulated file from your game data." - : "This is a path to a modded file on your file system."); - return ret; - } - - private static IReadOnlyList<(string Path, bool Game, bool IsOnPlayer)> CreateFiles(TextureManager textures, ModEditor editor, - Func> getPlayerResources) - { - var playerResources = getPlayerResources(); - - return editor.Files.Tex.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true)) - .Prepend((f.File.FullName, false))) - .Where(p => p.Item2 ? textures.GameFileExists(p.Item1) : File.Exists(p.Item1)) - .Select(p => (p.Item1, p.Item2, playerResources.Contains(p.Item1))) - .ToList(); - } - - public bool Draw(string label, string tooltip, string current, int skipPrefix, out string newPath) - { - _skipPrefix = skipPrefix; - var startPath = current.Length > 0 ? current : "Choose a modded texture from this mod here..."; - if (!Draw(label, startPath, tooltip, -0.0001f, ImGui.GetTextLineHeightWithSpacing())) - { - newPath = current; - return false; - } - - newPath = CurrentSelection.Item1; - return true; - } - } -} +using ImSharp; +using Lumina.Data.Files; +using Luna; +using OtterTex; +using Penumbra.UI; +using Penumbra.UI.Classes; +using VectorExtensions = Luna.VectorExtensions; + +namespace Penumbra.Import.Textures; + +public static class TextureDrawer +{ + public static void Draw(Texture texture, Vector2 size) + { + if (texture.TextureWrap != null) + { + size = VectorExtensions.Contain(texture.TextureWrap.Size, size); + + Im.Image.Draw(texture.TextureWrap.Id(), size); + DrawData(texture); + } + else if (texture.LoadError != null) + { + const string link = "https://aka.ms/vcredist"; + Im.Text("Could not load file:"u8); + + if (texture.LoadError is DllNotFoundException) + { + Im.Text("A texture handling dependency could not be found. Try installing a current Microsoft VC Redistributable."u8, + Colors.RegexWarningBorder); + if (Im.Button("Microsoft VC Redistributables"u8)) + Dalamud.Utility.Util.OpenLink(link); + Im.Tooltip.OnHover($"Open {link} in your browser."); + } + + Im.Text($"{texture.LoadError}", Colors.RegexWarningBorder); + } + } + + public static void PathInputBox(TextureManager textures, Texture current, ref string? tmpPath, ReadOnlySpan label, + ReadOnlySpan hint, ReadOnlySpan tooltip, + string startPath, FileDialogService fileDialog, string defaultModImportPath) + { + tmpPath ??= current.Path; + using var spacing = ImStyleDouble.ItemSpacing.PushX(UiHelpers.ScaleX3); + Im.Item.SetNextWidth(-2 * Im.Style.FrameHeight - 7 * Im.Style.GlobalScale); + if (ImEx.InputOnDeactivation.Text(label, tmpPath, out tmpPath, hint)) + current.Load(textures, tmpPath); + + Im.Tooltip.OnHover(tooltip); + Im.Line.Same(); + if (ImEx.Icon.Button(LunaStyle.FolderIcon)) + { + if (defaultModImportPath.Length > 0) + startPath = defaultModImportPath; + + void UpdatePath(bool success, List paths) + { + if (success && paths.Count > 0) + current.Load(textures, paths[0]); + } + + fileDialog.OpenFilePicker("Open Image...", "Textures{.png,.dds,.tex,.tga}", UpdatePath, 1, startPath, false); + } + + Im.Line.Same(); + if (ImEx.Icon.Button(LunaStyle.RefreshIcon, "Reload the currently selected path."u8)) + current.Reload(textures); + } + + private static void DrawData(Texture texture) + { + using var table = Im.Table.Begin("##data"u8, 2, TableFlags.SizingFixedFit); + table.DrawColumn("Width"u8); + table.DrawColumn($"{texture.TextureWrap!.Width}"); + table.DrawColumn("Height"u8); + table.DrawColumn($"{texture.TextureWrap!.Height}"); + table.DrawColumn("File Type"u8); + table.DrawColumn($"{texture.Type}"); + table.DrawColumn("Bitmap Size"u8); + table.DrawColumn($"{FormattingFunctions.HumanReadableSize(texture.RgbaPixels.Length)} ({texture.RgbaPixels.Length} Bytes)"); + switch (texture.BaseImage.Image) + { + case ScratchImage s: + table.DrawColumn("Format"u8); + table.DrawColumn($"{s.Meta.Format}"); + table.DrawColumn("Mip Levels"u8); + table.DrawColumn($"{s.Meta.MipLevels}"); + table.DrawColumn("Data Size"u8); + table.DrawColumn($"{FormattingFunctions.HumanReadableSize(s.Pixels.Length)} ({s.Pixels.Length} Bytes)"); + table.DrawColumn("Number of Images"u8); + table.DrawColumn($"{s.Images.Length}"); + break; + case TexFile t: + table.DrawColumn("Format"u8); + table.DrawColumn($"{t.Header.Format}"); + table.DrawColumn("Mip Levels"u8); + table.DrawColumn($"{t.Header.MipCount}"); + table.DrawColumn("Data Size"u8); + table.DrawColumn($"{FormattingFunctions.HumanReadableSize(t.ImageData.Length)} ({t.ImageData.Length} Bytes)"); + break; + } + } +} diff --git a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs index 9a57ca12..ab26def0 100644 --- a/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs +++ b/Penumbra/Interop/Hooks/Animation/LoadCharacterVfx.cs @@ -1,5 +1,5 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object; -using OtterGui.Services; +using Luna; using Penumbra.Collections; using Penumbra.CrashHandler.Buffers; using Penumbra.GameData; diff --git a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs index 858357c8..56a3a115 100644 --- a/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs +++ b/Penumbra/Interop/Hooks/Animation/PlayFootstep.cs @@ -1,10 +1,10 @@ -using FFXIVClientStructs.FFXIV.Client.Game.Object; -using OtterGui.Services; -using Penumbra.GameData; -using Penumbra.Interop.PathResolving; - -namespace Penumbra.Interop.Hooks.Animation; - +using FFXIVClientStructs.FFXIV.Client.Game.Object; +using Luna; +using Penumbra.GameData; +using Penumbra.Interop.PathResolving; + +namespace Penumbra.Interop.Hooks.Animation; + public sealed unsafe class PlayFootstep : FastHook { private readonly GameState _state; @@ -12,9 +12,9 @@ public sealed unsafe class PlayFootstep : FastHook public PlayFootstep(HookManager hooks, GameState state, CollectionResolver collectionResolver) { - _state = state; + _state = state; _collectionResolver = collectionResolver; - Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, !HookOverrides.Instance.Animation.PlayFootstep); + Task = hooks.CreateHook("Play Footstep", Sigs.FootStepSound, Detour, !HookOverrides.Instance.Animation.PlayFootstep); } public delegate void Delegate(GameObject* gameObject, int id, int unk); @@ -27,4 +27,4 @@ public sealed unsafe class PlayFootstep : FastHook Task.Result.Original(gameObject, id, unk); _state.RestoreAnimationData(last); } -} +} diff --git a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs index 48690e98..5383beab 100644 --- a/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs +++ b/Penumbra/Interop/ResourceTree/ResourceTreeApiHelper.cs @@ -9,6 +9,16 @@ namespace Penumbra.Interop.ResourceTree; internal static class ResourceTreeApiHelper { + public static HashSet GetPlayerResourcesOfType(ResourceTreeFactory factory, ResourceType type) + { + var resources = GetResourcesOfType(factory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) + .Values + .SelectMany(r => r.Values) + .Select(r => r.Item1); + + return new HashSet(resources, StringComparer.OrdinalIgnoreCase); + } + public static Dictionary>> GetResourcePathDictionaries( IEnumerable<(ICharacter, ResourceTree)> resourceTrees) { diff --git a/Penumbra/Penumbra.csproj b/Penumbra/Penumbra.csproj index d28c575e..bf385f5c 100644 --- a/Penumbra/Penumbra.csproj +++ b/Penumbra/Penumbra.csproj @@ -71,6 +71,8 @@ + + diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs index 26777830..975338c3 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.QuickImport.cs @@ -1,7 +1,5 @@ using Dalamud.Interface; -using Dalamud.Bindings.ImGui; using ImSharp; -using Lumina.Data; using OtterGui.Text; using Penumbra.Api.Enums; using Penumbra.GameData.Files; @@ -20,7 +18,7 @@ public partial class ModEditWindow private readonly ResourceTreeViewer _quickImportViewer; private readonly Dictionary<(Utf8GamePath, IWritable?), QuickImportAction> _quickImportActions = new(); - private HashSet GetPlayerResourcesOfType(ResourceType type) + public HashSet GetPlayerResourcesOfType(ResourceType type) { var resources = ResourceTreeApiHelper .GetResourcesOfType(_resourceTreeFactory.FromObjectTable(ResourceTreeFactory.Flags.LocalPlayerRelatedOnly), type) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs index c3f9fbf1..e420dbb5 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Textures.cs @@ -15,10 +15,10 @@ public partial class ModEditWindow { private readonly TextureManager _textures; - private readonly Texture _left = new(); - private readonly Texture _right = new(); - private readonly CombinedTexture _center; - private readonly TextureDrawer.PathSelectCombo _textureSelectCombo; + private readonly Texture _left = new(); + private readonly Texture _right = new(); + private readonly CombinedTexture _center; + private readonly TextureSelectCombo _textureSelectCombo; private bool _overlayCollapsed = true; private bool _addMipMaps = true; @@ -49,13 +49,13 @@ public partial class ModEditWindow return; using var id = ImRaii.PushId(label); - ImEx.TextFramed(label, new Vector2(-1, 0), ImGuiColor.FrameBackground.Get()); + ImEx.TextFramed(label, Im.ContentRegion.Available with { Y = 0 }, ImGuiColor.FrameBackground.Get()); ImGui.NewLine(); using (ImRaii.Disabled(!_center.SaveTask.IsCompleted)) { - TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input", "Import Image...", - "Can import game paths as well as your own files.", Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); + TextureDrawer.PathInputBox(_textures, tex, ref tex.TmpPath, "##input"u8, "Import Image..."u8, + "Can import game paths as well as your own files."u8, Mod!.ModPath.FullName, _fileDialog, _config.DefaultModImportPath); if (_textureSelectCombo.Draw("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.", tex.Path, Mod.ModPath.FullName.Length + 1, out var newPath) @@ -200,7 +200,6 @@ public partial class ModEditWindow case TaskStatus.WaitingToRun: case TaskStatus.Running: ImGuiUtil.DrawTextButton("Computing...", -Vector2.UnitX, Colors.PressEnterWarningBg); - break; case TaskStatus.Canceled: case TaskStatus.Faulted: @@ -210,9 +209,7 @@ public partial class ModEditWindow ImGuiUtil.TextWrapped(_center.SaveTask.Exception?.ToString() ?? "Unknown Error"); break; } - default: - ImGui.Dummy(new Vector2(1, ImGui.GetFrameHeight())); - break; + default: ImGui.Dummy(new Vector2(1, ImGui.GetFrameHeight())); break; } ImGui.NewLine(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 218d70f3..d6657420 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -656,7 +656,7 @@ public partial class ModEditWindow : IndexedWindow, IDisposable () => Mod?.ModPath.FullName ?? string.Empty, (bytes, path, _) => new PbdTab(bytes, path)); _center = new CombinedTexture(_left, _right); - _textureSelectCombo = new TextureDrawer.PathSelectCombo(textures, editor, () => GetPlayerResourcesOfType(ResourceType.Tex)); + _textureSelectCombo = new TextureSelectCombo(resourceTreeFactory, editor, gameData); _resourceTreeFactory = resourceTreeFactory; _quickImportViewer = resourceTreeViewerFactory.Create(1, OnQuickImportRefresh, DrawQuickImportActions); _communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModEditWindow); diff --git a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs index 4a030462..5b1af9dc 100644 --- a/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs +++ b/Penumbra/UI/AdvancedWindow/ResourceTreeViewerFactory.cs @@ -15,7 +15,7 @@ public class ResourceTreeViewerFactory( PcpService pcpService, IDataManager gameData, FileDialogService fileDialog, - FileCompactor compactor) : Luna.IService + FileCompactor compactor) : IService { public ResourceTreeViewer Create(int actionCapacity, Action onRefresh, Action drawActions) => new(config, treeFactory, changedItemDrawer, incognito, actionCapacity, onRefresh, drawActions, communicator, pcpService, gameData, From 7ed81a982365fa99164a2ab5d8cdb6801987c0d7 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Wed, 22 Oct 2025 17:53:02 +0200 Subject: [PATCH 2/2] Update OtterGui. --- OtterGui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/OtterGui b/OtterGui index f3544447..9af1e5fc 160000 --- a/OtterGui +++ b/OtterGui @@ -1 +1 @@ -Subproject commit f354444776591ae423e2d8374aae346308d81424 +Subproject commit 9af1e5fce4c13ef98842807d4f593dec8ae80c87