mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 20:54:16 +01:00
Clean up top-level conversion utilities.
This commit is contained in:
parent
f1379af92c
commit
dc845b766e
3 changed files with 55 additions and 61 deletions
|
|
@ -2,24 +2,19 @@ using FFXIVClientStructs.Havok;
|
||||||
|
|
||||||
namespace Penumbra.Import.Models;
|
namespace Penumbra.Import.Models;
|
||||||
|
|
||||||
// TODO: where should this live? interop i guess, in penum? or game data?
|
public static unsafe class HavokConverter
|
||||||
public unsafe class HavokConverter
|
|
||||||
{
|
{
|
||||||
/// <summary> Creates a temporary file and returns its path. </summary>
|
/// <summary> Creates a temporary file and returns its path. </summary>
|
||||||
/// <returns>Path to a temporary file.</returns>
|
private static string CreateTempFile()
|
||||||
private string CreateTempFile()
|
|
||||||
{
|
{
|
||||||
var s = File.Create(Path.GetTempFileName());
|
var stream = File.Create(Path.GetTempFileName());
|
||||||
s.Close();
|
stream.Close();
|
||||||
return s.Name;
|
return stream.Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Converts a .hkx file to a .xml file. </summary>
|
/// <summary> Converts a .hkx file to a .xml file. </summary>
|
||||||
/// <param name="hkx"> A byte array representing the .hkx file. </param>
|
/// <param name="hkx"> A byte array representing the .hkx file. </param>
|
||||||
/// <returns>A string representing the .xml file.</returns>
|
public static string HkxToXml(byte[] hkx)
|
||||||
/// <exception cref="Exceptions.HavokReadException">Thrown if parsing the .hkx file fails.</exception>
|
|
||||||
/// <exception cref="Exceptions.HavokWriteException">Thrown if writing the .xml file fails.</exception>
|
|
||||||
public string HkxToXml(byte[] hkx)
|
|
||||||
{
|
{
|
||||||
var tempHkx = CreateTempFile();
|
var tempHkx = CreateTempFile();
|
||||||
File.WriteAllBytes(tempHkx, hkx);
|
File.WriteAllBytes(tempHkx, hkx);
|
||||||
|
|
@ -27,7 +22,7 @@ public unsafe class HavokConverter
|
||||||
var resource = Read(tempHkx);
|
var resource = Read(tempHkx);
|
||||||
File.Delete(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
|
var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
|
||||||
| hkSerializeUtil.SaveOptionBits.TextFormat
|
| hkSerializeUtil.SaveOptionBits.TextFormat
|
||||||
|
|
@ -42,12 +37,9 @@ public unsafe class HavokConverter
|
||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Converts a .xml file to a .hkx file.</summary>
|
/// <summary> Converts an .xml file to a .hkx file. </summary>
|
||||||
/// <param name="xml"> A string representing the .xml file. </param>
|
/// <param name="xml"> A string representing the .xml file. </param>
|
||||||
/// <returns>A byte array representing the .hkx file.</returns>
|
public static byte[] XmlToHkx(string xml)
|
||||||
/// <exception cref="Exceptions.HavokReadException">Thrown if parsing the .xml file fails.</exception>
|
|
||||||
/// <exception cref="Exceptions.HavokWriteException">Thrown if writing the .hkx file fails.</exception>
|
|
||||||
public byte[] XmlToHkx(string xml)
|
|
||||||
{
|
{
|
||||||
var tempXml = CreateTempFile();
|
var tempXml = CreateTempFile();
|
||||||
File.WriteAllText(tempXml, xml);
|
File.WriteAllText(tempXml, xml);
|
||||||
|
|
@ -55,7 +47,7 @@ public unsafe class HavokConverter
|
||||||
var resource = Read(tempXml);
|
var resource = Read(tempXml);
|
||||||
File.Delete(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
|
var options = hkSerializeUtil.SaveOptionBits.SerializeIgnoredMembers
|
||||||
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
|
| hkSerializeUtil.SaveOptionBits.WriteAttributes;
|
||||||
|
|
@ -75,8 +67,7 @@ public unsafe class HavokConverter
|
||||||
/// This pointer might be null - you should check for that.
|
/// This pointer might be null - you should check for that.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="filePath"> Path to a file on the filesystem. </param>
|
/// <param name="filePath"> Path to a file on the filesystem. </param>
|
||||||
/// <returns>A (potentially null) pointer to an hkResource.</returns>
|
private static hkResource* Read(string filePath)
|
||||||
private hkResource* Read(string filePath)
|
|
||||||
{
|
{
|
||||||
var path = Marshal.StringToHGlobalAnsi(filePath);
|
var path = Marshal.StringToHGlobalAnsi(filePath);
|
||||||
|
|
||||||
|
|
@ -87,7 +78,7 @@ public unsafe class HavokConverter
|
||||||
loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
|
loadOptions->ClassNameRegistry = builtinTypeRegistry->GetClassNameRegistry();
|
||||||
loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
|
loadOptions->TypeInfoRegistry = builtinTypeRegistry->GetTypeInfoRegistry();
|
||||||
|
|
||||||
// TODO: probably can loadfrombuffer this
|
// TODO: probably can use LoadFromBuffer for this.
|
||||||
var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions);
|
var resource = hkSerializeUtil.LoadFromFile((byte*)path, null, loadOptions);
|
||||||
return resource;
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
@ -95,10 +86,7 @@ public unsafe class HavokConverter
|
||||||
/// <summary> Serializes an hkResource* to a temporary file. </summary>
|
/// <summary> Serializes an hkResource* to a temporary file. </summary>
|
||||||
/// <param name="resource"> A pointer to the hkResource, opened through Read(). </param>
|
/// <param name="resource"> A pointer to the hkResource, opened through Read(). </param>
|
||||||
/// <param name="optionBits"> Flags representing how to serialize the file. </param>
|
/// <param name="optionBits"> Flags representing how to serialize the file. </param>
|
||||||
/// <returns>An opened FileStream of a temporary file. You are expected to read the file and delete it.</returns>
|
private static FileStream Write(
|
||||||
/// <exception cref="Exceptions.HavokWriteException">Thrown if accessing the root level container fails.</exception>
|
|
||||||
/// <exception cref="Exceptions.HavokFailureException">Thrown if an unknown failure in writing occurs.</exception>
|
|
||||||
private FileStream Write(
|
|
||||||
hkResource* resource,
|
hkResource* resource,
|
||||||
hkSerializeUtil.SaveOptionBits optionBits
|
hkSerializeUtil.SaveOptionBits optionBits
|
||||||
)
|
)
|
||||||
|
|
@ -125,16 +113,16 @@ public unsafe class HavokConverter
|
||||||
var name = "hkRootLevelContainer";
|
var name = "hkRootLevelContainer";
|
||||||
|
|
||||||
var resourcePtr = (hkRootLevelContainer*)resource->GetContentsPointer(name, typeInfoRegistry);
|
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);
|
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);
|
hkSerializeUtil.Save(result, resourcePtr, hkRootLevelContainerClass, oStream.StreamWriter.ptr, saveOptions);
|
||||||
}
|
}
|
||||||
finally { oStream.Dtor(); }
|
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);
|
return new FileStream(tempFile, FileMode.Open);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -89,17 +89,12 @@ public sealed class ModelManager : SingleTaskQueue, IDisposable
|
||||||
if (_sklb == null)
|
if (_sklb == null)
|
||||||
return 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.
|
// 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);
|
xmlTask.Wait(cancel);
|
||||||
var xml = xmlTask.Result;
|
var xml = xmlTask.Result;
|
||||||
|
|
||||||
var skeletonConverter = new SkeletonConverter();
|
return SkeletonConverter.FromXml(xml);
|
||||||
var skeleton = skeletonConverter.FromXml(xml);
|
|
||||||
|
|
||||||
return skeleton;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(IAction? other)
|
public bool Equals(IAction? other)
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,11 @@ using Penumbra.Import.Models.Export;
|
||||||
|
|
||||||
namespace Penumbra.Import.Models;
|
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 static class SkeletonConverter
|
||||||
public class SkeletonConverter
|
|
||||||
{
|
{
|
||||||
public XivSkeleton FromXml(string xml)
|
/// <summary> Parse XIV skeleton data from a havok XML tagfile. </summary>
|
||||||
|
/// <param name="xml"> Havok XML tagfile containing skeleton data. </param>
|
||||||
|
public static XivSkeleton FromXml(string xml)
|
||||||
{
|
{
|
||||||
var document = new XmlDocument();
|
var document = new XmlDocument();
|
||||||
document.LoadXml(xml);
|
document.LoadXml(xml);
|
||||||
|
|
@ -16,14 +17,14 @@ public class SkeletonConverter
|
||||||
|
|
||||||
var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']");
|
var skeletonNode = document.SelectSingleNode($"/hktagfile/object[@type='hkaSkeleton'][@id='{mainSkeletonId}']");
|
||||||
if (skeletonNode == null)
|
if (skeletonNode == null)
|
||||||
throw new InvalidDataException();
|
throw new InvalidDataException($"Failed to find skeleton with id {mainSkeletonId}.");
|
||||||
|
|
||||||
var referencePose = ReadReferencePose(skeletonNode);
|
var referencePose = ReadReferencePose(skeletonNode);
|
||||||
var parentIndices = ReadParentIndices(skeletonNode);
|
var parentIndices = ReadParentIndices(skeletonNode);
|
||||||
var boneNames = ReadBoneNames(skeletonNode);
|
var boneNames = ReadBoneNames(skeletonNode);
|
||||||
|
|
||||||
if (boneNames.Length != parentIndices.Length || boneNames.Length != referencePose.Length)
|
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
|
var bones = referencePose
|
||||||
.Zip(parentIndices, boneNames)
|
.Zip(parentIndices, boneNames)
|
||||||
|
|
@ -44,21 +45,21 @@ public class SkeletonConverter
|
||||||
|
|
||||||
/// <summary> Get the main skeleton ID for a given skeleton document. </summary>
|
/// <summary> Get the main skeleton ID for a given skeleton document. </summary>
|
||||||
/// <param name="node"> XML skeleton document. </param>
|
/// <param name="node"> XML skeleton document. </param>
|
||||||
private string GetMainSkeletonId(XmlNode node)
|
private static string GetMainSkeletonId(XmlNode node)
|
||||||
{
|
{
|
||||||
var animationSkeletons = node
|
var animationSkeletons = node
|
||||||
.SelectSingleNode("/hktagfile/object[@type='hkaAnimationContainer']/array[@name='skeletons']")?
|
.SelectSingleNode("/hktagfile/object[@type='hkaAnimationContainer']/array[@name='skeletons']")?
|
||||||
.ChildNodes;
|
.ChildNodes;
|
||||||
|
|
||||||
if (animationSkeletons?.Count != 1)
|
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;
|
return animationSkeletons[0]!.InnerText;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Read the reference pose transforms for a skeleton. </summary>
|
/// <summary> Read the reference pose transforms for a skeleton. </summary>
|
||||||
/// <param name="node"> XML node for the skeleton. </param>
|
/// <param name="node"> XML node for the skeleton. </param>
|
||||||
private XivSkeleton.Transform[] ReadReferencePose(XmlNode node)
|
private static XivSkeleton.Transform[] ReadReferencePose(XmlNode node)
|
||||||
{
|
{
|
||||||
return ReadArray(
|
return ReadArray(
|
||||||
CheckExists(node.SelectSingleNode("array[@name='referencePose']")),
|
CheckExists(node.SelectSingleNode("array[@name='referencePose']")),
|
||||||
|
|
@ -75,7 +76,9 @@ public class SkeletonConverter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private float[] ReadVec12(XmlNode node)
|
/// <summary> Read a 12-item vector from a tagfile. </summary>
|
||||||
|
/// <param name="node"> Havok Vec12 XML node. </param>
|
||||||
|
private static float[] ReadVec12(XmlNode node)
|
||||||
{
|
{
|
||||||
var array = node.ChildNodes
|
var array = node.ChildNodes
|
||||||
.Cast<XmlNode>()
|
.Cast<XmlNode>()
|
||||||
|
|
@ -89,12 +92,14 @@ public class SkeletonConverter
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (array.Length != 12)
|
if (array.Length != 12)
|
||||||
throw new InvalidDataException();
|
throw new InvalidDataException($"Unexpected Vector12 length ({array.Length}).");
|
||||||
|
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
private int[] ReadParentIndices(XmlNode node)
|
/// <summary> Read the bone parent relations for a skeleton. </summary>
|
||||||
|
/// <param name="node"> XML node for the skeleton. </param>
|
||||||
|
private static int[] ReadParentIndices(XmlNode node)
|
||||||
{
|
{
|
||||||
// todo: would be neat to genericise array between bare and children
|
// todo: would be neat to genericise array between bare and children
|
||||||
return CheckExists(node.SelectSingleNode("array[@name='parentIndices']"))
|
return CheckExists(node.SelectSingleNode("array[@name='parentIndices']"))
|
||||||
|
|
@ -104,7 +109,9 @@ public class SkeletonConverter
|
||||||
.ToArray();
|
.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string[] ReadBoneNames(XmlNode node)
|
/// <summary> Read the names of bones in a skeleton. </summary>
|
||||||
|
/// <param name="node"> XML node for the skeleton. </param>
|
||||||
|
private static string[] ReadBoneNames(XmlNode node)
|
||||||
{
|
{
|
||||||
return ReadArray(
|
return ReadArray(
|
||||||
CheckExists(node.SelectSingleNode("array[@name='bones']")),
|
CheckExists(node.SelectSingleNode("array[@name='bones']")),
|
||||||
|
|
@ -112,7 +119,10 @@ public class SkeletonConverter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private T[] ReadArray<T>(XmlNode node, Func<XmlNode, T> convert)
|
/// <summary> Read an XML tagfile array, converting it via the provided conversion function. </summary>
|
||||||
|
/// <param name="node"> Tagfile XML array node. </param>
|
||||||
|
/// <param name="convert"> Function to convert array item nodes to required data types. </param>
|
||||||
|
private static T[] ReadArray<T>(XmlNode node, Func<XmlNode, T> convert)
|
||||||
{
|
{
|
||||||
var element = (XmlElement)node;
|
var element = (XmlElement)node;
|
||||||
|
|
||||||
|
|
@ -125,6 +135,7 @@ public class SkeletonConverter
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary> Check if the argument is null, returning a non-nullable value if it exists, and throwing if not. </summary>
|
||||||
private static T CheckExists<T>(T? value)
|
private static T CheckExists<T>(T? value)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(value);
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue