From dc845b766e654a1cfef2ff1c792091c3999eabea Mon Sep 17 00:00:00 2001 From: ackwell Date: Mon, 1 Jan 2024 00:57:27 +1100 Subject: [PATCH] Clean up top-level conversion utilities. --- Penumbra/Import/Models/HavokConverter.cs | 60 +++++++++------------ Penumbra/Import/Models/ModelManager.cs | 9 +--- Penumbra/Import/Models/SkeletonConverter.cs | 47 +++++++++------- 3 files changed, 55 insertions(+), 61 deletions(-) diff --git a/Penumbra/Import/Models/HavokConverter.cs b/Penumbra/Import/Models/HavokConverter.cs index 515c6f97..7f87d50a 100644 --- a/Penumbra/Import/Models/HavokConverter.cs +++ b/Penumbra/Import/Models/HavokConverter.cs @@ -2,24 +2,19 @@ using FFXIVClientStructs.Havok; namespace Penumbra.Import.Models; -// TODO: where should this live? interop i guess, in penum? or game data? -public unsafe class HavokConverter +public static unsafe class HavokConverter { - /// Creates a temporary file and returns its path. - /// Path to a temporary file. - private string CreateTempFile() + /// Creates a temporary file and returns its path. + private static string CreateTempFile() { - var s = File.Create(Path.GetTempFileName()); - s.Close(); - return s.Name; + var stream = File.Create(Path.GetTempFileName()); + stream.Close(); + return stream.Name; } - /// Converts a .hkx file to a .xml file. - /// A byte array representing the .hkx file. - /// A string representing the .xml file. - /// Thrown if parsing the .hkx file fails. - /// Thrown if writing the .xml file fails. - public string HkxToXml(byte[] hkx) + /// Converts a .hkx file to a .xml file. + /// A byte array representing the .hkx file. + public static string HkxToXml(byte[] hkx) { var tempHkx = CreateTempFile(); File.WriteAllBytes(tempHkx, hkx); @@ -27,7 +22,7 @@ public unsafe class HavokConverter var resource = Read(tempHkx); File.Delete(tempHkx); - if (resource == null) throw new Exception("HavokReadException"); + if (resource == null) throw new Exception("Failed to read havok file."); var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers | hkSerializeUtil.SaveOptionBits.TextFormat @@ -42,12 +37,9 @@ public unsafe class HavokConverter return bytes; } - /// Converts a .xml file to a .hkx file. - /// A string representing the .xml file. - /// A byte array representing the .hkx file. - /// Thrown if parsing the .xml file fails. - /// Thrown if writing the .hkx file fails. - public byte[] XmlToHkx(string xml) + /// Converts an .xml file to a .hkx file. + /// A string representing the .xml file. + public static byte[] XmlToHkx(string xml) { var tempXml = CreateTempFile(); File.WriteAllText(tempXml, xml); @@ -55,7 +47,7 @@ public unsafe class HavokConverter var resource = Read(tempXml); File.Delete(tempXml); - if (resource == null) throw new Exception("HavokReadException"); + if (resource == null) throw new Exception("Failed to read havok file."); var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers | hkSerializeUtil.SaveOptionBits.WriteAttributes; @@ -74,9 +66,8 @@ public unsafe class HavokConverter /// The type is guessed automatically by Havok. /// This pointer might be null - you should check for that. /// - /// Path to a file on the filesystem. - /// A (potentially null) pointer to an hkResource. - private hkResource* Read(string filePath) + /// Path to a file on the filesystem. + private static hkResource* Read(string filePath) { var path = Marshal.StringToHGlobalAnsi(filePath); @@ -87,18 +78,15 @@ public unsafe class HavokConverter loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry(); loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry(); - // TODO: probably can loadfrombuffer this + // TODO: probably can use LoadFromBuffer for this. var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions); return resource; } - /// Serializes an hkResource* to a temporary file. - /// A pointer to the hkResource, opened through Read(). - /// Flags representing how to serialize the file. - /// An opened FileStream of a temporary file. You are expected to read the file and delete it. - /// Thrown if accessing the root level container fails. - /// Thrown if an unknown failure in writing occurs. - private FileStream Write( + /// Serializes an hkResource* to a temporary file. + /// A pointer to the hkResource, opened through Read(). + /// Flags representing how to serialize the file. + private static FileStream Write( hkResource* resource, hkSerializeUtil.SaveOptionBits optionBits ) @@ -125,16 +113,16 @@ public unsafe class HavokConverter var name = "hkRootLevelContainer"; var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry); - if (resourcePtr == null) throw new Exception("HavokWriteException"); + if (resourcePtr == null) throw new Exception("Failed to retrieve havok root level container resource."); var hkRootLevelContainerClass = classNameRegistry->GetClassByName(name); - if (hkRootLevelContainerClass == null) throw new Exception("HavokWriteException"); + if (hkRootLevelContainerClass == null) throw new Exception("Failed to retrieve havok root level container type."); hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions); } finally { oStream.Dtor(); } - if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("HavokFailureException"); + if (result->Result == hkResult.hkResultEnum.Failure) throw new Exception("Failed to serialize havok file."); return new FileStream(tempFile, FileMode.Open); } diff --git a/Penumbra/Import/Models/ModelManager.cs b/Penumbra/Import/Models/ModelManager.cs index 9f72619f..a56d7168 100644 --- a/Penumbra/Import/Models/ModelManager.cs +++ b/Penumbra/Import/Models/ModelManager.cs @@ -89,17 +89,12 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable if (_sklb == null) return null; - // TODO: Consider making these static methods. // TODO: work out how i handle this havok deal. running it outside the framework causes an immediate ctd. - var havokConverter = new HavokConverter(); - var xmlTask = _manager._framework.RunOnFrameworkThread(() => havokConverter.HkxToXml(_sklb.Skeleton)); + var xmlTask = _manager._framework.RunOnFrameworkThread(() => HavokConverter.HkxToXml(_sklb.Skeleton)); xmlTask.Wait(cancel); var xml = xmlTask.Result; - var skeletonConverter = new SkeletonConverter(); - var skeleton = skeletonConverter.FromXml(xml); - - return skeleton; + return SkeletonConverter.FromXml(xml); } public bool Equals(IAction? other) diff --git a/Penumbra/Import/Models/SkeletonConverter.cs b/Penumbra/Import/Models/SkeletonConverter.cs index e265e5c3..24bcf3e0 100644 --- a/Penumbra/Import/Models/SkeletonConverter.cs +++ b/Penumbra/Import/Models/SkeletonConverter.cs @@ -4,10 +4,11 @@ using Penumbra.Import.Models.Export; namespace Penumbra.Import.Models; -// TODO: tempted to say that this living here is more okay? that or next to havok converter, wherever that ends up. -public class SkeletonConverter +public static class SkeletonConverter { - public XivSkeleton FromXml(string xml) + /// Parse XIV skeleton data from a havok XML tagfile. + /// Havok XML tagfile containing skeleton data. + public static XivSkeleton FromXml(string xml) { var document = new XmlDocument(); document.LoadXml(xml); @@ -16,14 +17,14 @@ public class SkeletonConverter var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']"); if (skeletonNode == null) - throw new InvalidDataException(); + throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}."); var referencePose = ReadReferencePose(skeletonNode); var parentIndices = ReadParentIndices(skeletonNode); var boneNames = ReadBoneNames(skeletonNode); if (boneNames.Length != parentIndices.Length || boneNames.Length != referencePose.Length) - throw new InvalidDataException(); + throw new InvalidDataException($"Mismatch in bone value array lengths: names({boneNames.Length}) parents({parentIndices.Length}) pose({referencePose.Length})"); var bones = referencePose .Zip(parentIndices, boneNames) @@ -38,27 +39,27 @@ public class SkeletonConverter }; }) .ToArray(); - + return new XivSkeleton(bones); } - /// Get the main skeleton ID for a given skeleton document. - /// XML skeleton document. - private string GetMainSkeletonId(XmlNode node) + /// Get the main skeleton ID for a given skeleton document. + /// XML skeleton document. + private static string GetMainSkeletonId(XmlNode node) { var animationSkeletons = node .SelectSingleNode("/hktagfile/object[@type='hkaAnimationContainer']/array[@name='skeletons']")? .ChildNodes; if (animationSkeletons?.Count != 1) - throw new Exception($"Assumption broken: Expected 1 hkaAnimationContainer skeleton, got {animationSkeletons?.Count ?? 0}"); + throw new Exception($"Assumption broken: Expected 1 hkaAnimationContainer skeleton, got {animationSkeletons?.Count ?? 0}."); return animationSkeletons[0]!.InnerText; } - /// Read the reference pose transforms for a skeleton. - /// XML node for the skeleton. - private XivSkeleton.Transform[] ReadReferencePose(XmlNode node) + /// Read the reference pose transforms for a skeleton. + /// XML node for the skeleton. + private static XivSkeleton.Transform[] ReadReferencePose(XmlNode node) { return ReadArray( CheckExists(node.SelectSingleNode("array[@name='referencePose']")), @@ -75,7 +76,9 @@ public class SkeletonConverter ); } - private float[] ReadVec12(XmlNode node) + /// Read a 12-item vector from a tagfile. + /// Havok Vec12 XML node. + private static float[] ReadVec12(XmlNode node) { var array = node.ChildNodes .Cast() @@ -89,12 +92,14 @@ public class SkeletonConverter .ToArray(); if (array.Length != 12) - throw new InvalidDataException(); + throw new InvalidDataException($"Unexpected Vector12 length ({array.Length})."); return array; } - private int[] ReadParentIndices(XmlNode node) + /// Read the bone parent relations for a skeleton. + /// XML node for the skeleton. + private static int[] ReadParentIndices(XmlNode node) { // todo: would be neat to genericise array between bare and children return CheckExists(node.SelectSingleNode("array[@name='parentIndices']")) @@ -104,7 +109,9 @@ public class SkeletonConverter .ToArray(); } - private string[] ReadBoneNames(XmlNode node) + /// Read the names of bones in a skeleton. + /// XML node for the skeleton. + private static string[] ReadBoneNames(XmlNode node) { return ReadArray( CheckExists(node.SelectSingleNode("array[@name='bones']")), @@ -112,7 +119,10 @@ public class SkeletonConverter ); } - private T[] ReadArray(XmlNode node, Func convert) + /// Read an XML tagfile array, converting it via the provided conversion function. + /// Tagfile XML array node. + /// Function to convert array item nodes to required data types. + private static T[] ReadArray(XmlNode node, Func convert) { var element = (XmlElement)node; @@ -125,6 +135,7 @@ public class SkeletonConverter return array; } + /// Check if the argument is null, returning a non-nullable value if it exists, and throwing if not. private static T CheckExists(T? value) { ArgumentNullException.ThrowIfNull(value);