From aa7f0bace9a55da8fdcd08d7acb427081735f153 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 19:49:13 +1100 Subject: [PATCH 1/6] Wire up hair EST resolution --- .../Import/Models/Export/ModelExporter.cs | 9 +- Penumbra/Import/Models/ModelManager.cs | 89 ++++++++++++++----- .../ModEditWindow.Models.MdlTab.cs | 42 ++++++--- Penumbra/UI/AdvancedWindow/ModEditWindow.cs | 2 +- 4 files changed, 104 insertions(+), 38 deletions(-) diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 2060c323..07b37eeb 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -22,7 +22,7 @@ public class ModelExporter } /// Export a model in preparation for usage in a glTF file. If provided, skeleton will be used to skin the resulting meshes where appropriate. - public static Model Export(MdlFile mdl, XivSkeleton? xivSkeleton) + public static Model Export(MdlFile mdl, IEnumerable? xivSkeleton) { var gltfSkeleton = xivSkeleton != null ? ConvertSkeleton(xivSkeleton) : null; var meshes = ConvertMeshes(mdl, gltfSkeleton); @@ -50,12 +50,15 @@ public class ModelExporter } /// Convert XIV skeleton data into a glTF-compatible node tree, with mappings. - private static GltfSkeleton? ConvertSkeleton(XivSkeleton skeleton) + private static GltfSkeleton? ConvertSkeleton(IEnumerable skeletons) { NodeBuilder? root = null; var names = new Dictionary(); var joints = new List(); - foreach (var bone in skeleton.Bones) + + // Flatten out the bones across all the recieved skeletons, but retain a reference to the parent skeleton for lookups. + var iterator = skeletons.SelectMany(skeleton => skeleton.Bones.Select(bone => (skeleton, bone))); + foreach (var (skeleton, bone) in iterator) { if (names.ContainsKey(bone.Name)) continue; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index dd796a42..a9e1b32d 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -1,14 +1,19 @@ using Dalamud.Plugin.Services; +using OtterGui; 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.Meta.Manipulations; using SharpGLTF.Scenes; namespace Penumbra.Import.Models; -public sealed class ModelManager(IFramework framework, GamePathParser _parser) : SingleTaskQueue, IDisposable +public sealed class ModelManager(IFramework framework, ActiveCollections collections, GamePathParser parser) : SingleTaskQueue, IDisposable { private readonly IFramework _framework = framework; @@ -24,31 +29,55 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, SklbFile? sklb, string outputPath) - => Enqueue(new ExportToGltfAction(this, mdl, sklb, outputPath)); + public Task ExportToGltf(MdlFile mdl, IEnumerable? sklbs, string outputPath) + => Enqueue(new ExportToGltfAction(this, mdl, sklbs, outputPath)); - /// Try to find the .sklb path for a .mdl file. - /// .mdl file to look up the skeleton for. - public string? ResolveSklbForMdl(string mdlPath) + /// Try to find the .sklb paths for a .mdl file. + /// .mdl file to look up the skeletons for. + public string[]? ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations) { - var info = _parser.GetFileInfo(mdlPath); + var info = parser.GetFileInfo(mdlPath); if (info.FileType is not FileType.Model) return null; + var baseSkeleton = GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1); + return info.ObjectType switch { - ObjectType.Equipment => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1), - ObjectType.Accessory => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1), - ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", - 1), + 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, ResolveHairSkeleton(info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), - ObjectType.DemiHuman => GamePaths.DemiHuman.Sklb.Path(info.PrimaryId), - ObjectType.Monster => GamePaths.Monster.Sklb.Path(info.PrimaryId), - ObjectType.Weapon => GamePaths.Weapon.Sklb.Path(info.PrimaryId), + ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], + ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], + ObjectType.Weapon => [GamePaths.Weapon.Sklb.Path(info.PrimaryId)], _ => null, }; } + private string ResolveHairSkeleton(GameObjectInfo info, EstManipulation[] estManipulations) + { + // TODO: might be able to genericse this over esttype based on incoming info + var (gender, race) = info.GenderRace.Split(); + var modEst = estManipulations + .FirstOrNull(est => + est.Gender == gender + && est.Race == race + && est.Slot == EstManipulation.EstType.Hair + && est.SetId == info.PrimaryId + ); + + // Try to use an entry from the current mod, falling back to the current collection, and finally an unmodified value. + var targetId = modEst?.Entry + ?? collections.Current.MetaCache?.GetEstEntry(EstManipulation.EstType.Hair, info.GenderRace, info.PrimaryId) + ?? info.PrimaryId; + + // TODO: i'm not conviced ToSuffix is correct - check! + return GamePaths.Skeleton.Sklb.Path(info.GenderRace, info.BodySlot.ToSuffix(), targetId); + } + private Task Enqueue(IAction action) { if (_disposed) @@ -75,16 +104,16 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : return task; } - private class ExportToGltfAction(ModelManager manager, MdlFile mdl, SklbFile? sklb, string outputPath) + private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable? sklbs, string outputPath) : IAction { public void Execute(CancellationToken cancel) { - Penumbra.Log.Debug("Reading skeleton."); - var xivSkeleton = BuildSkeleton(cancel); + Penumbra.Log.Debug("Reading skeletons."); + var xivSkeletons = BuildSkeletons(cancel); Penumbra.Log.Debug("Converting model."); - var model = ModelExporter.Export(mdl, xivSkeleton); + var model = ModelExporter.Export(mdl, xivSkeletons); Penumbra.Log.Debug("Building scene."); var scene = new SceneBuilder(); @@ -96,16 +125,28 @@ public sealed class ModelManager(IFramework framework, GamePathParser _parser) : } /// Attempt to read out the pertinent information from a .sklb. - private XivSkeleton? BuildSkeleton(CancellationToken cancel) + private IEnumerable? BuildSkeletons(CancellationToken cancel) { - if (sklb == null) + if (sklbs == null) return null; - var xmlTask = manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(sklb.Skeleton)); - xmlTask.Wait(cancel); - var xml = xmlTask.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((SklbFile Sklb, int Index) pair) => + manager._framework.RunOnTick( + () => HavokConverter.HkxToXml(pair.Sklb.Skeleton), + delayTicks: pair.Index + ); - return SkeletonConverter.FromXml(xml); + var havokTasks = sklbs + .WithIndex() + .Select(CreateHavokTask) + .ToArray(); + Task.WaitAll(havokTasks, cancel); + + return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result)); } public bool Equals(IAction? other) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index b8573780..d4e75487 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -1,7 +1,7 @@ using OtterGui; using Penumbra.GameData; using Penumbra.GameData.Files; -using Penumbra.Mods; +using Penumbra.Meta.Manipulations; using Penumbra.String.Classes; namespace Penumbra.UI.AdvancedWindow; @@ -21,15 +21,14 @@ public partial class ModEditWindow public bool PendingIo { get; private set; } public string? IoException { get; private set; } - public MdlTab(ModEditWindow edit, byte[] bytes, string path, IMod? mod) + public MdlTab(ModEditWindow edit, byte[] bytes, string path) { _edit = edit; Mdl = new MdlFile(bytes); _attributes = CreateAttributes(Mdl); - if (mod != null) - FindGamePaths(path, mod); + FindGamePaths(path); } /// @@ -42,9 +41,13 @@ public partial class ModEditWindow /// Find the list of game paths that may correspond to this model. /// Resolved path to a .mdl. - /// Mod within which the .mdl is resolved. - private void FindGamePaths(string path, IMod mod) + private void FindGamePaths(string path) { + // If there's no current mod (somehow), there's nothing to resolve the model within. + var mod = _edit._editor.Mod; + if (mod == null) + return; + if (!Path.IsPathRooted(path) && Utf8GamePath.FromString(path, out var p)) { GamePaths = [p]; @@ -71,15 +74,34 @@ public partial class ModEditWindow }); } + private EstManipulation[] GetCurrentEstManipulations() + { + var mod = _edit._editor.Mod; + var option = _edit._editor.Option; + if (mod == null || option == null) + return []; + + // Filter then prepend the current option to ensure it's chosen first. + return mod.AllSubMods + .Where(subMod => subMod != option) + .Prepend(option) + .SelectMany(subMod => subMod.Manipulations) + .Where(manipulation => manipulation.ManipulationType == MetaManipulation.Type.Est) + .Select(manipulation => manipulation.Est) + .ToArray(); + } + /// Export model to an interchange format. /// Disk path to save the resulting file to. public void Export(string outputPath, Utf8GamePath mdlPath) { - SklbFile? sklb = null; + IEnumerable? sklbs = null; try { - var sklbPath = _edit._models.ResolveSklbForMdl(mdlPath.ToString()); - sklb = sklbPath != null ? ReadSklb(sklbPath) : null; + var sklbPaths = _edit._models.ResolveSklbsForMdl(mdlPath.ToString(), GetCurrentEstManipulations()); + sklbs = sklbPaths != null + ? sklbPaths.Select(ReadSklb).ToArray() + : null; } catch (Exception exception) { @@ -88,7 +110,7 @@ public partial class ModEditWindow } PendingIo = true; - _edit._models.ExportToGltf(Mdl, sklb, outputPath) + _edit._models.ExportToGltf(Mdl, sklbs, outputPath) .ContinueWith(task => { IoException = task.Exception?.ToString(); diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs index 167adafe..8d3e32f9 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.cs @@ -600,7 +600,7 @@ public partial class ModEditWindow : Window, IDisposable (bytes, path, writable) => new MtrlTab(this, new MtrlFile(bytes), path, writable)); _modelTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Models", ".mdl", () => PopulateIsOnPlayer(_editor.Files.Mdl, ResourceType.Mdl), DrawModelPanel, () => _mod?.ModPath.FullName ?? string.Empty, - (bytes, path, _) => new MdlTab(this, bytes, path, _mod)); + (bytes, path, _) => new MdlTab(this, bytes, path)); _shaderPackageTab = new FileEditor(this, gameData, config, _editor.Compactor, _fileDialog, "Shaders", ".shpk", () => PopulateIsOnPlayer(_editor.Files.Shpk, ResourceType.Shpk), DrawShaderPackagePanel, () => _mod?.ModPath.FullName ?? string.Empty, From 0440324432dd00e0c14d55f16aa65336736e95d6 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 20:16:15 +1100 Subject: [PATCH 2/6] Genericise est logic to handle face --- Penumbra/Import/Models/ModelManager.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index a9e1b32d..4564968d 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -48,7 +48,9 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ObjectType.Accessory => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Hair - => [baseSkeleton, ResolveHairSkeleton(info, estManipulations)], + => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], + ObjectType.Character when info.BodySlot is BodySlot.Face + => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], @@ -57,24 +59,22 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } - private string ResolveHairSkeleton(GameObjectInfo info, EstManipulation[] estManipulations) + private string ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations) { - // TODO: might be able to genericse this over esttype based on incoming info var (gender, race) = info.GenderRace.Split(); var modEst = estManipulations .FirstOrNull(est => est.Gender == gender && est.Race == race - && est.Slot == EstManipulation.EstType.Hair + && est.Slot == type && est.SetId == info.PrimaryId ); // Try to use an entry from the current mod, falling back to the current collection, and finally an unmodified value. var targetId = modEst?.Entry - ?? collections.Current.MetaCache?.GetEstEntry(EstManipulation.EstType.Hair, info.GenderRace, info.PrimaryId) + ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) ?? info.PrimaryId; - // TODO: i'm not conviced ToSuffix is correct - check! return GamePaths.Skeleton.Sklb.Path(info.GenderRace, info.BodySlot.ToSuffix(), targetId); } From 8bc71fb1b318b6589dc3a564f5698ccd80ccdf2e Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 20:47:44 +1100 Subject: [PATCH 3/6] Fix viera ears --- Penumbra.GameData | 2 +- Penumbra/Import/Models/ModelManager.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index 83c01275..e4ab3e91 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit 83c012752cd9d13d39248eda85ab18cc59070a76 +Subproject commit e4ab3e914ab8b5651cea313af367e811a253d174 diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 4564968d..7ea19aab 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -49,7 +49,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ObjectType.Character when info.BodySlot is BodySlot.Body or BodySlot.Tail => [baseSkeleton], ObjectType.Character when info.BodySlot is BodySlot.Hair => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], - ObjectType.Character when info.BodySlot is BodySlot.Face + ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], @@ -75,7 +75,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) ?? info.PrimaryId; - return GamePaths.Skeleton.Sklb.Path(info.GenderRace, info.BodySlot.ToSuffix(), targetId); + return GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId); } private Task Enqueue(IAction action) From 3f8ac1e8d04f95ad85576212d25c1cc3f493b4f0 Mon Sep 17 00:00:00 2001 From: ackwell Date: Sun, 7 Jan 2024 21:56:21 +1100 Subject: [PATCH 4/6] Add support for body and head slot EST --- Penumbra/Import/Models/ModelManager.cs | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 7ea19aab..692a214f 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -44,13 +44,17 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return info.ObjectType switch { + ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Body + => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Body, info, estManipulations)], + ObjectType.Equipment when info.EquipSlot.ToSlot() is EquipSlot.Head + => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.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(EstManipulation.EstType.Hair, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Hair, info, estManipulations)], ObjectType.Character when info.BodySlot is BodySlot.Face or BodySlot.Ear - => [baseSkeleton, ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], + => [baseSkeleton, ..ResolveEstSkeleton(EstManipulation.EstType.Face, info, estManipulations)], ObjectType.Character => throw new Exception($"Currently unsupported human model type \"{info.BodySlot}\"."), ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], @@ -59,8 +63,9 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect }; } - private string ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations) + private string[] ResolveEstSkeleton(EstManipulation.EstType type, GameObjectInfo info, EstManipulation[] estManipulations) { + // Try to find an EST entry from the manipulations provided. var (gender, race) = info.GenderRace.Split(); var modEst = estManipulations .FirstOrNull(est => @@ -70,12 +75,16 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect && est.SetId == info.PrimaryId ); - // Try to use an entry from the current mod, falling back to the current collection, and finally an unmodified value. + // Try to use an entry from provided manipulations, falling back to the current collection. var targetId = modEst?.Entry ?? collections.Current.MetaCache?.GetEstEntry(type, info.GenderRace, info.PrimaryId) - ?? info.PrimaryId; + ?? 0; - return GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId); + // If there's no entries, we can assume that there's no additional skeleton. + if (targetId == 0) + return []; + + return [GamePaths.Skeleton.Sklb.Path(info.GenderRace, EstManipulation.ToName(type), targetId)]; } private Task Enqueue(IAction action) From 2f6905cf357b81aed33e67476575a39809538db5 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 14:42:16 +0100 Subject: [PATCH 5/6] Minimal cleanup. --- Penumbra/Communication/ChangedItemClick.cs | 1 - .../Import/Models/Export/ModelExporter.cs | 5 ++- Penumbra/Import/Models/ModelManager.cs | 43 +++++++++---------- .../ModEditWindow.Models.MdlTab.cs | 13 +++--- 4 files changed, 30 insertions(+), 32 deletions(-) diff --git a/Penumbra/Communication/ChangedItemClick.cs b/Penumbra/Communication/ChangedItemClick.cs index b11f2306..754570e2 100644 --- a/Penumbra/Communication/ChangedItemClick.cs +++ b/Penumbra/Communication/ChangedItemClick.cs @@ -1,6 +1,5 @@ using OtterGui.Classes; using Penumbra.Api.Enums; -using Penumbra.GameData.Enums; namespace Penumbra.Communication; diff --git a/Penumbra/Import/Models/Export/ModelExporter.cs b/Penumbra/Import/Models/Export/ModelExporter.cs index 07b37eeb..8271f266 100644 --- a/Penumbra/Import/Models/Export/ModelExporter.cs +++ b/Penumbra/Import/Models/Export/ModelExporter.cs @@ -56,11 +56,12 @@ public class ModelExporter var names = new Dictionary(); var joints = new List(); - // Flatten out the bones across all the recieved skeletons, but retain a reference to the parent skeleton for lookups. + // Flatten out the bones across all the received skeletons, but retain a reference to the parent skeleton for lookups. var iterator = skeletons.SelectMany(skeleton => skeleton.Bones.Select(bone => (skeleton, bone))); foreach (var (skeleton, bone) in iterator) { - if (names.ContainsKey(bone.Name)) continue; + if (names.ContainsKey(bone.Name)) + continue; var node = new NodeBuilder(bone.Name); names[bone.Name] = joints.Count; diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 692a214f..f4c17080 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -29,16 +29,17 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect _tasks.Clear(); } - public Task ExportToGltf(MdlFile mdl, IEnumerable? sklbs, string outputPath) + public Task ExportToGltf(MdlFile mdl, IEnumerable sklbs, string outputPath) => Enqueue(new ExportToGltfAction(this, mdl, sklbs, outputPath)); /// Try to find the .sklb paths for a .mdl file. /// .mdl file to look up the skeletons for. - public string[]? ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations) + /// Modified extra skeleton template parameters. + public string[] ResolveSklbsForMdl(string mdlPath, EstManipulation[] estManipulations) { var info = parser.GetFileInfo(mdlPath); if (info.FileType is not FileType.Model) - return null; + return []; var baseSkeleton = GamePaths.Skeleton.Sklb.Path(info.GenderRace, "base", 1); @@ -59,7 +60,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect ObjectType.DemiHuman => [GamePaths.DemiHuman.Sklb.Path(info.PrimaryId)], ObjectType.Monster => [GamePaths.Monster.Sklb.Path(info.PrimaryId)], ObjectType.Weapon => [GamePaths.Weapon.Sklb.Path(info.PrimaryId)], - _ => null, + _ => [], }; } @@ -113,31 +114,38 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect return task; } - private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable? sklbs, string outputPath) + private class ExportToGltfAction(ModelManager manager, MdlFile mdl, IEnumerable sklbs, string outputPath) : IAction { public void Execute(CancellationToken cancel) { - Penumbra.Log.Debug("Reading skeletons."); + Penumbra.Log.Debug($"[GLTF Export] Exporting model to {outputPath}..."); + Penumbra.Log.Debug("[GLTF Export] Reading skeletons..."); var xivSkeletons = BuildSkeletons(cancel); - Penumbra.Log.Debug("Converting model."); + Penumbra.Log.Debug("[GLTF Export] Converting model..."); var model = ModelExporter.Export(mdl, xivSkeletons); - Penumbra.Log.Debug("Building scene."); + Penumbra.Log.Debug("[GLTF Export] Building scene..."); var scene = new SceneBuilder(); model.AddToScene(scene); - Penumbra.Log.Debug("Saving."); + Penumbra.Log.Debug("[GLTF Export] Saving..."); var gltfModel = scene.ToGltf2(); gltfModel.SaveGLTF(outputPath); + Penumbra.Log.Debug("[GLTF Export] Done."); } /// Attempt to read out the pertinent information from a .sklb. - private IEnumerable? BuildSkeletons(CancellationToken cancel) + private IEnumerable BuildSkeletons(CancellationToken cancel) { - if (sklbs == null) - return null; + var havokTasks = sklbs + .WithIndex() + .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 @@ -146,16 +154,7 @@ public sealed class ModelManager(IFramework framework, ActiveCollections collect Task CreateHavokTask((SklbFile Sklb, int Index) pair) => manager._framework.RunOnTick( () => HavokConverter.HkxToXml(pair.Sklb.Skeleton), - delayTicks: pair.Index - ); - - var havokTasks = sklbs - .WithIndex() - .Select(CreateHavokTask) - .ToArray(); - Task.WaitAll(havokTasks, cancel); - - return havokTasks.Select(task => SkeletonConverter.FromXml(task.Result)); + delayTicks: pair.Index, cancellationToken: cancel); } public bool Equals(IAction? other) diff --git a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs index d4e75487..dbedc164 100644 --- a/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs +++ b/Penumbra/UI/AdvancedWindow/ModEditWindow.Models.MdlTab.cs @@ -86,31 +86,30 @@ public partial class ModEditWindow .Where(subMod => subMod != option) .Prepend(option) .SelectMany(subMod => subMod.Manipulations) - .Where(manipulation => manipulation.ManipulationType == MetaManipulation.Type.Est) + .Where(manipulation => manipulation.ManipulationType is MetaManipulation.Type.Est) .Select(manipulation => manipulation.Est) .ToArray(); } /// Export model to an interchange format. /// Disk path to save the resulting file to. + /// The game path of the model. public void Export(string outputPath, Utf8GamePath mdlPath) { - IEnumerable? sklbs = null; + IEnumerable skeletons; try { var sklbPaths = _edit._models.ResolveSklbsForMdl(mdlPath.ToString(), GetCurrentEstManipulations()); - sklbs = sklbPaths != null - ? sklbPaths.Select(ReadSklb).ToArray() - : null; + skeletons = sklbPaths.Select(ReadSklb).ToArray(); } catch (Exception exception) { - IoException = exception?.ToString(); + IoException = exception.ToString(); return; } PendingIo = true; - _edit._models.ExportToGltf(Mdl, sklbs, outputPath) + _edit._models.ExportToGltf(Mdl, skeletons, outputPath) .ContinueWith(task => { IoException = task.Exception?.ToString(); From 2e935a637815371cf2e4e65ffa9d200447024567 Mon Sep 17 00:00:00 2001 From: Ottermandias Date: Sun, 7 Jan 2024 15:14:58 +0100 Subject: [PATCH 6/6] Update GameData. --- Penumbra.GameData | 2 +- Penumbra/Collections/Manager/CollectionStorage.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Penumbra.GameData b/Penumbra.GameData index e4ab3e91..1dad8d07 160000 --- a/Penumbra.GameData +++ b/Penumbra.GameData @@ -1 +1 @@ -Subproject commit e4ab3e914ab8b5651cea313af367e811a253d174 +Subproject commit 1dad8d07047be0851f518cdac2b1c8bc76a7be98 diff --git a/Penumbra/Collections/Manager/CollectionStorage.cs b/Penumbra/Collections/Manager/CollectionStorage.cs index 7c94d705..c43c3817 100644 --- a/Penumbra/Collections/Manager/CollectionStorage.cs +++ b/Penumbra/Collections/Manager/CollectionStorage.cs @@ -17,10 +17,10 @@ public class CollectionStorage : IReadOnlyList, IDisposable private readonly ModStorage _modStorage; /// The empty collection is always available at Index 0. - private readonly List _collections = new() - { + private readonly List _collections = + [ ModCollection.Empty, - }; + ]; public readonly ModCollection DefaultNamed;