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);