From 9a1122503640d821a64c67c0cec12841be3684fa Mon Sep 17 00:00:00 2001 From: Passive <20432486+PassiveModding@users.noreply.github.com> Date: Wed, 17 Jul 2024 23:36:06 +1000 Subject: [PATCH] Havok stuff (zzz) --- Meddle/Meddle.Plugin/Utils/ParseUtil.cs | 7 +- Meddle/Meddle.UI/Configuration.cs | 4 +- Meddle/Meddle.UI/Program.cs | 2 +- Meddle/Meddle.UI/Util/SkeletonUtil.cs | 44 +++++ Meddle/Meddle.UI/Windows/Views/ExportView.cs | 30 +--- Meddle/Meddle.UI/Windows/Views/SklbView.cs | 19 +- .../Skeletons/Havok/HavokCCUtils.cs | 164 ++++++++++++++++++ .../Skeletons/Havok/HavokUtils.cs | 160 +++++++++++++++++ .../Meddle.Utils/Skeletons/Havok/HavokXml.cs | 51 ------ .../Havok/Models/HavokPartialSkeleton.cs | 16 ++ .../Skeletons/Havok/Models/HavokSkeleton.cs | 21 +++ .../Havok/Models/HavokSkeletonMapping.cs | 21 +++ .../Skeletons/Havok/XmlMapping.cs | 46 ----- .../Skeletons/Havok/XmlSkeleton.cs | 95 ---------- .../Meddle.Utils/Skeletons/Havok/XmlUtils.cs | 3 +- 15 files changed, 451 insertions(+), 232 deletions(-) create mode 100644 Meddle/Meddle.Utils/Skeletons/Havok/HavokCCUtils.cs create mode 100644 Meddle/Meddle.Utils/Skeletons/Havok/HavokUtils.cs delete mode 100644 Meddle/Meddle.Utils/Skeletons/Havok/HavokXml.cs create mode 100644 Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokPartialSkeleton.cs create mode 100644 Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokSkeleton.cs create mode 100644 Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokSkeletonMapping.cs delete mode 100644 Meddle/Meddle.Utils/Skeletons/Havok/XmlMapping.cs delete mode 100644 Meddle/Meddle.Utils/Skeletons/Havok/XmlSkeleton.cs diff --git a/Meddle/Meddle.Plugin/Utils/ParseUtil.cs b/Meddle/Meddle.Plugin/Utils/ParseUtil.cs index 72b6677..9cdf70f 100644 --- a/Meddle/Meddle.Plugin/Utils/ParseUtil.cs +++ b/Meddle/Meddle.Plugin/Utils/ParseUtil.cs @@ -11,6 +11,7 @@ using Meddle.Utils.Files.Structs.Material; using Meddle.Utils.Models; using Meddle.Utils.Skeletons.Havok; +using Meddle.Utils.Skeletons.Havok.Models; using Attach = Meddle.Plugin.Skeleton.Attach; using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture; @@ -186,12 +187,12 @@ public unsafe ExportUtil.AttachedModelGroup HandleAttachGroup(CharacterBase* att return attachGroup; } - private unsafe List ParseSkeletons(Human* human) + private unsafe List ParseSkeletons(Human* human) { var skeletonResourceHandles = new Span>(human->Skeleton->SkeletonResourceHandles, human->Skeleton->PartialSkeletonCount); - var skeletons = new List(); + var skeletons = new List(); foreach (var skeletonPtr in skeletonResourceHandles) { var skeletonResourceHandle = skeletonPtr.Value; @@ -217,7 +218,7 @@ private unsafe List ParseSkeletons(Human* human) var xml = HkUtil.HkxToXml(tempFile); return xml; }).GetAwaiter().GetResult(); - var havokXml = new HavokXml(hkXml); + var havokXml = HavokUtils.ParseHavokXml(hkXml); skeletons.Add(havokXml); } finally diff --git a/Meddle/Meddle.UI/Configuration.cs b/Meddle/Meddle.UI/Configuration.cs index 5ae8ae7..9d73279 100644 --- a/Meddle/Meddle.UI/Configuration.cs +++ b/Meddle/Meddle.UI/Configuration.cs @@ -12,6 +12,7 @@ public class Configuration public int WindowHeight { get; set; } public int DisplayScale { get; set; } public int FpsLimit { get; set; } + public bool AssetCcResolve { get; set; } public static Configuration Load() { @@ -25,7 +26,8 @@ public static Configuration Load() WindowHeight = 720, DisplayScale = 1, FpsLimit = 60, - InteropPort = 5000 + InteropPort = 5000, + AssetCcResolve = false }; } diff --git a/Meddle/Meddle.UI/Program.cs b/Meddle/Meddle.UI/Program.cs index 5f23c13..aecfe2a 100644 --- a/Meddle/Meddle.UI/Program.cs +++ b/Meddle/Meddle.UI/Program.cs @@ -19,7 +19,7 @@ public class Program private readonly ILoggerFactory logFactory = LoggerFactory.Create(builder => builder.AddConsole()); private readonly ILogger logger; - public Configuration Configuration; + public static Configuration Configuration; public Sdl2Window Window; public GraphicsDevice GraphicsDevice; public ImGuiHandler ImGuiHandler; diff --git a/Meddle/Meddle.UI/Util/SkeletonUtil.cs b/Meddle/Meddle.UI/Util/SkeletonUtil.cs index 3e043b8..45787e2 100644 --- a/Meddle/Meddle.UI/Util/SkeletonUtil.cs +++ b/Meddle/Meddle.UI/Util/SkeletonUtil.cs @@ -1,10 +1,40 @@ using System.Diagnostics; +using Meddle.Utils.Skeletons.Havok; +using Meddle.Utils.Skeletons.Havok.Models; namespace Meddle.UI.Util; public class SkeletonUtil { public static string ParseHavokInput(byte[] data) + { + if (Program.Configuration.AssetCcResolve) + { + return ParseHavokInputCc(data); + } + else + { + return ParseHavokInputInterop(data); + } + } + + public static (string, HavokSkeleton) ProcessHavokInput(byte[] data) + { + if (Program.Configuration.AssetCcResolve) + { + var str = ParseHavokInputCc(data); + var skeleton = HavokCCUtils.ParseHavokXml(str); + return (str, skeleton); + } + else + { + var str = ParseHavokInputInterop(data); + var skeleton = HavokUtils.ParseHavokXml(str); + return (str, skeleton); + } + } + + public static string ParseHavokInputCc(byte[] data) { File.WriteAllBytes("./data/input.pap", data); var program = Process.Start("./data/NotAssetCc.exe", new[] {"./data/input.pap", "./data/output.pap"}); @@ -12,4 +42,18 @@ public static string ParseHavokInput(byte[] data) var parseResult = File.ReadAllText("./data/output.pap"); return parseResult; } + + public static string ParseHavokInputInterop(byte[] data) + { + var tempPath = Path.GetTempFileName(); + File.WriteAllBytes(tempPath, data); + + using var message = new HttpRequestMessage(HttpMethod.Post, $"http://localhost:{Program.Configuration.InteropPort}/parsesklb"); + using var content = new StringContent(tempPath); + message.Content = content; + using var client = new HttpClient(); + var response = client.SendAsync(message).GetAwaiter().GetResult(); + var result = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + return result; + } } diff --git a/Meddle/Meddle.UI/Windows/Views/ExportView.cs b/Meddle/Meddle.UI/Windows/Views/ExportView.cs index ecc7b6d..61602c4 100644 --- a/Meddle/Meddle.UI/Windows/Views/ExportView.cs +++ b/Meddle/Meddle.UI/Windows/Views/ExportView.cs @@ -9,6 +9,7 @@ using Meddle.Utils.Materials; using Meddle.Utils.Models; using Meddle.Utils.Skeletons.Havok; +using Meddle.Utils.Skeletons.Havok.Models; using Meddle.Utils.Skeletons.HavokAnim; using SharpGLTF.Materials; using SharpGLTF.Scenes; @@ -24,7 +25,6 @@ private record SklbGroup(SklbFile File, string Path); private readonly Dictionary models = new(); private readonly Dictionary skeletons = new(); - private string? animation; private Dictionary views = new(); private Task loadTask = Task.CompletedTask; private CancellationTokenSource cts = new(); @@ -230,7 +230,6 @@ public void Draw() { models.Clear(); skeletons.Clear(); - animation = null; var newModels = HandleMdls(input.Split("\n")); foreach (var (key, value) in newModels) { @@ -242,17 +241,6 @@ public void Draw() { skeletons[key] = value; } - - var animLines = input.Split("\n").Select(x => x.Trim()).Where(x => x.EndsWith(".pap")).ToList(); - if (animLines.Count > 0) - { - var lookupResult = pack.GetFile(animLines[0]); - if (lookupResult != null) - { - var papFile = new PapFile(lookupResult.Value.file.RawData); - animation = SkeletonUtil.ParseHavokInput(papFile.HavokData.ToArray()); - } - } }); } @@ -277,18 +265,12 @@ public void Draw() if (ImGui.Button("Export as GLTF")) { var sklbs = this.skeletons - .Select(x => (x.Key, SkeletonUtil.ParseHavokInput(x.Value.File.Skeleton.ToArray()))) - .ToDictionary(x => x.Key, y => new HavokXml(y.Item2)); - - HavokAnimation.BindingContainer? anim = null; - if (animation != null) - { - anim = HavokAnimation.ParseDocument(this.animation)[0]; - } + .Select(x => (x.Key, SkeletonUtil.ProcessHavokInput(x.Value.File.Skeleton.ToArray()))) + .ToDictionary(x => x.Key, y => y.Item2.Item2); cts?.Cancel(); cts = new CancellationTokenSource(); - exportTask = Task.Run(() => RunExport(models, sklbs, anim, cts.Token), cts.Token); + exportTask = Task.Run(() => RunExport(models, sklbs, cts.Token), cts.Token); } if (exportTask.IsFaulted) @@ -321,11 +303,11 @@ private void DrawParameters() ImGui.Checkbox("Lip Stick", ref customizeData.LipStick); } - private void RunExport(Dictionary modelDict, Dictionary sklbDict, HavokAnimation.BindingContainer? animation, CancellationToken token = default) + private void RunExport(Dictionary modelDict, Dictionary sklbDict, CancellationToken token = default) { var scene = new SceneBuilder(); var havokXmls = sklbDict.Values.ToArray(); - var bones = XmlUtils.GetBoneMap(havokXmls, animation, out var root).ToArray(); + var bones = XmlUtils.GetBoneMap(havokXmls, out var root).ToArray(); var boneNodes = bones.Cast().ToArray(); var catchlightTexture = pack.GetFile("chara/common/texture/sphere_d_array.tex"); if (catchlightTexture == null) diff --git a/Meddle/Meddle.UI/Windows/Views/SklbView.cs b/Meddle/Meddle.UI/Windows/Views/SklbView.cs index 1093683..ecab3a3 100644 --- a/Meddle/Meddle.UI/Windows/Views/SklbView.cs +++ b/Meddle/Meddle.UI/Windows/Views/SklbView.cs @@ -4,6 +4,7 @@ using Meddle.UI.Util; using Meddle.Utils.Files; using Meddle.Utils.Skeletons.Havok; +using Meddle.Utils.Skeletons.Havok.Models; namespace Meddle.UI.Windows.Views; @@ -20,8 +21,7 @@ public SklbView(SklbFile file, Configuration configuration) this.configuration = configuration; } - private string? parseResult; - private HavokXml? havokXml; + private (string, HavokSkeleton)? parseResult; public void Draw() { ImGui.Text($"Version: {file.Header.Version} [{(uint)file.Header.Version:X8}]"); @@ -29,21 +29,20 @@ public void Draw() if (ImGui.Button("Parse")) { - parseResult = SkeletonUtil.ParseHavokInput(file.Skeleton.ToArray()); - havokXml = new HavokXml(parseResult); + parseResult = SkeletonUtil.ProcessHavokInput(file.Skeleton.ToArray()); } if (ImGui.CollapsingHeader("Havok XML") && parseResult != null) { - ImGui.TextUnformatted(parseResult); + ImGui.TextUnformatted(parseResult.Value.Item1); } - if (ImGui.CollapsingHeader("Parsed XML") && havokXml != null) + if (ImGui.CollapsingHeader("Parsed XML") && parseResult != null) { ImGui.SeparatorText("Skeletons"); - for (var i = 0; i < havokXml.Skeletons.Length; i++) + for (var i = 0; i < parseResult.Value.Item2.Skeletons.Length; i++) { - var skeleton = havokXml.Skeletons[i]; + var skeleton = parseResult.Value.Item2.Skeletons[i]; ImGui.BulletText($"Bone Count: {skeleton.BoneNames.Length}"); // scroll box ImGui.BeginChild($"Skeleton {i}", new Vector2(0, 200), ImGuiChildFlags.Border); @@ -59,9 +58,9 @@ public void Draw() } ImGui.SeparatorText("Mappings"); - for (var i = 0; i < havokXml.Mappings.Length; i++) + for (var i = 0; i < parseResult.Value.Item2.Mappings.Length; i++) { - var mapping = havokXml.Mappings[i]; + var mapping = parseResult.Value.Item2.Mappings[i]; ImGui.Text($"Mapping {i}"); ImGui.BulletText($"Id: {mapping.Id}"); ImGui.BulletText($"Bone Mappings: {mapping.BoneMappings.Length}"); diff --git a/Meddle/Meddle.Utils/Skeletons/Havok/HavokCCUtils.cs b/Meddle/Meddle.Utils/Skeletons/Havok/HavokCCUtils.cs new file mode 100644 index 0000000..600d2d2 --- /dev/null +++ b/Meddle/Meddle.Utils/Skeletons/Havok/HavokCCUtils.cs @@ -0,0 +1,164 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using System.Xml; +using Meddle.Utils.Skeletons.Havok.Models; + +namespace Meddle.Utils.Skeletons.Havok; + +public class HavokCCUtils +{ + public static HavokSkeleton ParseHavokXml(string xml) + { + var document = new XmlDocument(); + document.LoadXml( xml ); + + var skeletons = document.SelectNodes( "/hkpackfile/hksection/hkobject[@class='hkaSkeleton']" )! + .Cast< XmlElement >() + .Select(ParsePartialSkeletonXml).ToArray(); + + var mappings = document.SelectNodes( "/hkpackfile/hksection/hkobject[@class='hkaSkeletonMapper']" )! + .Cast< XmlElement >() + .Select(ParseSkeletonMappingXml).ToArray(); + + var animationContainer = document.SelectSingleNode( "/hkpackfile/hksection/hkobject[@class='hkaAnimationContainer']" )!; + var animationSkeletons = animationContainer + .SelectSingleNode( "hkparam[@name='skeletons']" )!; + + // A recurring theme in Havok XML is that IDs start with a hash + // If you see a string[1..], that's probably what it is + var mainSkeletonStr = animationSkeletons.ChildNodes[ 0 ]!.InnerText.Split('\n').Select(x => x.Trim()).First(x => !string.IsNullOrWhiteSpace(x)); + var mainSkeleton = int.Parse( mainSkeletonStr[ 1.. ] ); + + var skeleton = new HavokSkeleton + { + Skeletons = skeletons, + Mappings = mappings, + MainSkeleton = mainSkeleton + }; + + return skeleton; + } + + public static HavokPartialSkeleton ParsePartialSkeletonXml( XmlElement element ) { + var id = int.Parse( element.GetAttribute( "name" )[ 1.. ] ); + + var referencePose = ReadReferencePose( element ); + var parentIndices = ReadParentIndices( element ); + var boneNames = ReadBoneNames( element ); + + var skeleton = new HavokPartialSkeleton + { + Id = id, + ReferencePose = referencePose, + ParentIndices = parentIndices, + BoneNames = boneNames + }; + + return skeleton; + } + + private static float[][] ReadReferencePose( XmlElement element ) { + var referencePose = element.SelectSingleNode("hkparam[@name='referencePose']")!.InnerText; + var lines = referencePose.Split('\n').Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .ToArray(); + + + var referencePoseArr = new float[lines.Length][]; + for (int i = 0; i < lines.Length; i++) + { + referencePoseArr[i] = ParseVec12(lines[i]); + } + + return referencePoseArr; + } + + private static int[] ReadParentIndices( XmlElement element ) { + var parentIndices = element.SelectSingleNode("hkparam[@name='parentIndices']")!.InnerText; + + var lines = parentIndices.Split('\n').Select(x => x.Trim()) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .SelectMany(x => x.Split(' ')) + .Select(int.Parse) + .ToArray(); + + return lines; + } + + private static string[] ReadBoneNames( XmlElement element ) { + var bones = element.SelectSingleNode("hkparam[@name='bones']")!; + var boneNames = bones.SelectNodes("hkobject")! + .Cast() + .Select(x => x.SelectSingleNode("hkparam[@name='name']")!.InnerText) + .ToArray(); + + return boneNames; + } + + public static HavokSkeletonMapping ParseSkeletonMappingXml( XmlElement element ) { + var mapping = element.SelectSingleNode( "hkparam[@name='mapping']" )!; + var id = int.Parse( element.GetAttribute( "name" )[ 1.. ] ); + + var skeletonANode = mapping.SelectSingleNode( "hkobject/hkparam[@name='skeletonA']" )!; + var skeletonA = int.Parse( skeletonANode.InnerText[ 1.. ] ); + + var skeletonBNode = mapping.SelectSingleNode( "hkobject/hkparam[@name='skeletonB']" )!; + var skeletonB = int.Parse( skeletonBNode.InnerText[ 1.. ] ); + + var simpleMappings = mapping.SelectSingleNode( "hkobject/hkparam[@name='simpleMappings']" )!; + + var children = simpleMappings.SelectNodes( "hkobject" )!; + var boneMappings = new HavokSkeletonMapping.BoneMapping[children.Count]; + + for( var i = 0; i < children.Count; i++ ) { + var child = children[ i ]!; + var boneA = int.Parse( child.SelectSingleNode( "hkparam[@name='boneA']" )?.InnerText ?? "0" ); + var boneB = int.Parse( child.SelectSingleNode( "hkparam[@name='boneB']" )?.InnerText ?? "0" ); + var transform = ParseVec12( child.SelectSingleNode( "hkparam[@name='aFromBTransform']" )!.InnerText ); + + var mappingClass = new HavokSkeletonMapping.BoneMapping( boneA, boneB, transform ); + boneMappings[ i ] = mappingClass; + } + + var skMapping = new HavokSkeletonMapping + { + Id = id, + SkeletonA = skeletonA, + SkeletonB = skeletonB, + BoneMappings = boneMappings + }; + + return skMapping; + } + + /// Parses a vec12 from Havok XML. + /// The inner text of the vec12 node. + /// An array of floats. + public static float[] ParseVec12(string innerText) + { + // (0.000000 1.613955 0.043956)(0.707107 0.000000 0.707107 0.000000)(1.000000 1.000000 1.000000) + + var buf = new float[12]; + var floats = new List(); + var matches = Regex.Matches(innerText, @"-?\d+\.\d+"); + foreach (Match match in matches) + { + floats.Add(float.Parse(match.Value, CultureInfo.InvariantCulture)); + } + + buf[0] = floats[0]; + buf[1] = floats[1]; + buf[2] = floats[2]; + + buf[4] = floats[3]; + buf[5] = floats[4]; + buf[6] = floats[5]; + buf[7] = floats[6]; + + buf[8] = floats[7]; + buf[9] = floats[8]; + buf[10] = floats[9]; + + return buf; + } +} diff --git a/Meddle/Meddle.Utils/Skeletons/Havok/HavokUtils.cs b/Meddle/Meddle.Utils/Skeletons/Havok/HavokUtils.cs new file mode 100644 index 0000000..c5973eb --- /dev/null +++ b/Meddle/Meddle.Utils/Skeletons/Havok/HavokUtils.cs @@ -0,0 +1,160 @@ +using System.Xml; +using Meddle.Utils.Skeletons.Havok.Models; + +namespace Meddle.Utils.Skeletons.Havok; + +public class HavokUtils +{ + public static HavokSkeleton ParseHavokXml(string xml) + { + var document = new XmlDocument(); + document.LoadXml( xml ); + + var skeletons = document.SelectNodes( "/hktagfile/object[@type='hkaSkeleton']" )! + .Cast< XmlElement >() + .Select(ParsePartialSkeletonXml).ToArray(); + + var mappings = document.SelectNodes( "/hktagfile/object[@type='hkaSkeletonMapper']" )! + .Cast< XmlElement >() + .Select(ParseSkeletonMappingXml).ToArray(); + + var animationContainer = document.SelectSingleNode( "/hktagfile/object[@type='hkaAnimationContainer']" )!; + var animationSkeletons = animationContainer + .SelectNodes( "array[@name='skeletons']" )! + .Cast< XmlElement >() + .First(); + + // A recurring theme in Havok XML is that IDs start with a hash + // If you see a string[1..], that's probably what it is + var mainSkeletonStr = animationSkeletons.ChildNodes[ 0 ]!.InnerText; + var mainSkeleton = int.Parse( mainSkeletonStr[ 1.. ] ); + + + var skeleton = new HavokSkeleton + { + Skeletons = skeletons, + Mappings = mappings, + MainSkeleton = mainSkeleton + }; + + return skeleton; + } + + public static HavokPartialSkeleton ParsePartialSkeletonXml( XmlElement element ) { + var id = int.Parse( element.GetAttribute( "id" )[ 1.. ] ); + + var referencePose = ReadReferencePose( element ); + var parentIndices = ReadParentIndices( element ); + var boneNames = ReadBoneNames( element ); + + var skeleton = new HavokPartialSkeleton + { + Id = id, + ReferencePose = referencePose, + ParentIndices = parentIndices, + BoneNames = boneNames + }; + + return skeleton; + } + + private static float[][] ReadReferencePose( XmlElement element ) { + var referencePose = element.GetElementsByTagName( "array" ) + .Cast< XmlElement >() + .Where( x => x.GetAttribute( "name" ) == "referencePose" ) + .ToArray()[ 0 ]; + + var size = int.Parse( referencePose.GetAttribute( "size" ) ); + + var referencePoseArr = new float[size][]; + + var i = 0; + foreach( var node in referencePose.ChildNodes.Cast< XmlElement >() ) { + referencePoseArr[ i ] = XmlUtils.ParseVec12( node.InnerText ); + i += 1; + } + + return referencePoseArr; + } + + private static int[] ReadParentIndices( XmlElement element ) { + var parentIndices = element.GetElementsByTagName( "array" ) + .Cast< XmlElement >() + .Where( x => x.GetAttribute( "name" ) == "parentIndices" ) + .ToArray()[ 0 ]; + + var parentIndicesArr = new int[int.Parse( parentIndices.GetAttribute( "size" ) )]; + + var parentIndicesStr = parentIndices.InnerText.Split( "\n" ) + .Select( x => x.Trim() ) + .Where( x => !string.IsNullOrWhiteSpace( x ) ) + .ToArray(); + + var i = 0; + foreach( var str2 in parentIndicesStr ) { + foreach( var str3 in str2.Split( " " ) ) { + parentIndicesArr[ i ] = int.Parse( str3 ); + i++; + } + } + + return parentIndicesArr; + } + + private static string[] ReadBoneNames( XmlElement element ) { + var bonesObj = element.GetElementsByTagName( "array" ) + .Cast< XmlElement >() + .Where( x => x.GetAttribute( "name" ) == "bones" ) + .ToArray()[ 0 ]; + + var bones = new string[int.Parse( bonesObj.GetAttribute( "size" ) )]; + + var boneNames = bonesObj.GetElementsByTagName( "struct" ) + .Cast< XmlElement >() + .Select( x => x.GetElementsByTagName( "string" ) + .Cast< XmlElement >() + .First( y => y.GetAttribute( "name" ) == "name" ) ); + + var i = 0; + foreach( var boneName in boneNames ) { + bones[ i ] = boneName.InnerText; + i++; + } + + return bones; + } + + public static HavokSkeletonMapping ParseSkeletonMappingXml( XmlElement element ) { + var id = int.Parse( element.GetAttribute( "id" )[ 1.. ] ); + + var skeletonANode = element.SelectSingleNode( "struct/ref[@name='skeletonA']" )!; + var skeletonA = int.Parse( skeletonANode.InnerText[ 1.. ] ); + + var skeletonBNode = element.SelectSingleNode( "struct/ref[@name='skeletonB']" )!; + var skeletonB = int.Parse( skeletonBNode.InnerText[ 1.. ] ); + + var simpleMappings = ( XmlElement )element.SelectSingleNode( "struct/array[@name='simpleMappings']" )!; + var count = int.Parse( simpleMappings.GetAttribute( "size" ) ); + var boneMappings = new HavokSkeletonMapping.BoneMapping[count]; + + for( var i = 0; i < count; i++ ) { + var mapping = simpleMappings.SelectSingleNode( $"struct[{i + 1}]" )!; + var boneA = int.Parse( mapping.SelectSingleNode( "int[@name='boneA']" )?.InnerText ?? "0" ); + var boneB = int.Parse( mapping.SelectSingleNode( "int[@name='boneB']" )?.InnerText ?? "0" ); + var transform = XmlUtils.ParseVec12( mapping.SelectSingleNode( "vec12[@name='aFromBTransform']" )!.InnerText ); + + var mappingClass = new HavokSkeletonMapping.BoneMapping( boneA, boneB, transform ); + boneMappings[ i ] = mappingClass; + } + + var skMapping = new HavokSkeletonMapping + { + Id = id, + SkeletonA = skeletonA, + SkeletonB = skeletonB, + BoneMappings = boneMappings + }; + + return skMapping; + } +} diff --git a/Meddle/Meddle.Utils/Skeletons/Havok/HavokXml.cs b/Meddle/Meddle.Utils/Skeletons/Havok/HavokXml.cs deleted file mode 100644 index 211e198..0000000 --- a/Meddle/Meddle.Utils/Skeletons/Havok/HavokXml.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Xml; - -// ReSharper disable NotAccessedField.Global -// ReSharper disable MemberCanBePrivate.Global - -namespace Meddle.Utils.Skeletons.Havok; - -public class HavokXml { - public readonly XmlSkeleton[] Skeletons; - public readonly XmlMapping[] Mappings; - public readonly int MainSkeleton; - - /// Constructs a new HavokXml object from the given XML string. - /// The XML data. - public HavokXml( string xml ) { - var document = new XmlDocument(); - document.LoadXml( xml ); - - Skeletons = document.SelectNodes( "/hktagfile/object[@type='hkaSkeleton']" )! - .Cast< XmlElement >() - .Select( x => new XmlSkeleton( x ) ).ToArray(); - - Mappings = document.SelectNodes( "/hktagfile/object[@type='hkaSkeletonMapper']" )! - .Cast< XmlElement >() - .Select( x => new XmlMapping( x ) ).ToArray(); - - var animationContainer = document.SelectSingleNode( "/hktagfile/object[@type='hkaAnimationContainer']" )!; - var animationSkeletons = animationContainer - .SelectNodes( "array[@name='skeletons']" )! - .Cast< XmlElement >() - .First(); - - // A recurring theme in Havok XML is that IDs start with a hash - // If you see a string[1..], that's probably what it is - var mainSkeleton = animationSkeletons.ChildNodes[ 0 ]!.InnerText; - MainSkeleton = int.Parse( mainSkeleton[ 1.. ] ); - } - - /// - /// Gets the "main" skeleton from the XML file. - /// This assumes the skeleton represented in the animation container is the main skeleton. - /// - public XmlSkeleton GetMainSkeleton() { - return GetSkeletonById( MainSkeleton ); - } - - /// Gets a skeleton by its ID. - public XmlSkeleton GetSkeletonById( int id ) { - return Skeletons.First( x => x.Id == id ); - } -} diff --git a/Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokPartialSkeleton.cs b/Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokPartialSkeleton.cs new file mode 100644 index 0000000..d42ee16 --- /dev/null +++ b/Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokPartialSkeleton.cs @@ -0,0 +1,16 @@ +namespace Meddle.Utils.Skeletons.Havok.Models; + +public class HavokPartialSkeleton +{ + /// The ID of the skeleton. + public int Id { get; init; } + + /// The reference pose of the skeleton (also known as the "resting" or "base" pose). + public float[][] ReferencePose { get; init; } + + /// The parent indices of the skeleton. The root bone will have a parent index of -1. + public int[] ParentIndices { get; init; } + + /// The names of the bones in the skeleton. A bone's "ID" is represented by the index it has in this array. + public string[] BoneNames { get; init; } +} diff --git a/Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokSkeleton.cs b/Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokSkeleton.cs new file mode 100644 index 0000000..947f86a --- /dev/null +++ b/Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokSkeleton.cs @@ -0,0 +1,21 @@ +namespace Meddle.Utils.Skeletons.Havok.Models; + +public class HavokSkeleton +{ + public HavokPartialSkeleton[] Skeletons { get; init; } + public HavokSkeletonMapping[] Mappings { get; init; } + public int MainSkeleton { get; init; } + + /// + /// Gets the "main" skeleton from the XML file. + /// This assumes the skeleton represented in the animation container is the main skeleton. + /// + public HavokPartialSkeleton GetMainSkeleton() { + return GetSkeletonById( MainSkeleton ); + } + + /// Gets a skeleton by its ID. + public HavokPartialSkeleton GetSkeletonById( int id ) { + return Skeletons.First( x => x.Id == id ); + } +} diff --git a/Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokSkeletonMapping.cs b/Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokSkeletonMapping.cs new file mode 100644 index 0000000..a75c87d --- /dev/null +++ b/Meddle/Meddle.Utils/Skeletons/Havok/Models/HavokSkeletonMapping.cs @@ -0,0 +1,21 @@ +namespace Meddle.Utils.Skeletons.Havok.Models; + +public class HavokSkeletonMapping +{ + public int Id { get; set; } + public int SkeletonA { get; set; } + public int SkeletonB { get; set; } + public BoneMapping[] BoneMappings { get; set; } + + public class BoneMapping { + public readonly int BoneA; + public readonly int BoneB; + public readonly float[] Transform; + + public BoneMapping( int boneA, int boneB, float[] transform ) { + BoneA = boneA; + BoneB = boneB; + Transform = transform; + } + } +} diff --git a/Meddle/Meddle.Utils/Skeletons/Havok/XmlMapping.cs b/Meddle/Meddle.Utils/Skeletons/Havok/XmlMapping.cs deleted file mode 100644 index 4b111be..0000000 --- a/Meddle/Meddle.Utils/Skeletons/Havok/XmlMapping.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Xml; - -namespace Meddle.Utils.Skeletons.Havok; - -public class XmlMapping { - public readonly int Id; - public readonly int SkeletonA; - public readonly int SkeletonB; - public readonly BoneMapping[] BoneMappings; - - public XmlMapping( XmlElement element ) { - Id = int.Parse( element.GetAttribute( "id" )[ 1.. ] ); - - var skeletonA = element.SelectSingleNode( "struct/ref[@name='skeletonA']" )!; - SkeletonA = int.Parse( skeletonA.InnerText[ 1.. ] ); - - var skeletonB = element.SelectSingleNode( "struct/ref[@name='skeletonB']" )!; - SkeletonB = int.Parse( skeletonB.InnerText[ 1.. ] ); - - var simpleMappings = ( XmlElement )element.SelectSingleNode( "struct/array[@name='simpleMappings']" )!; - var count = int.Parse( simpleMappings.GetAttribute( "size" ) ); - BoneMappings = new BoneMapping[count]; - - for( var i = 0; i < count; i++ ) { - var mapping = simpleMappings.SelectSingleNode( $"struct[{i + 1}]" )!; - var boneA = int.Parse( mapping.SelectSingleNode( "int[@name='boneA']" )?.InnerText ?? "0" ); - var boneB = int.Parse( mapping.SelectSingleNode( "int[@name='boneB']" )?.InnerText ?? "0" ); - var transform = XmlUtils.ParseVec12( mapping.SelectSingleNode( "vec12[@name='aFromBTransform']" )!.InnerText ); - - var mappingClass = new BoneMapping( boneA, boneB, transform ); - BoneMappings[ i ] = mappingClass; - } - } - - public class BoneMapping { - public readonly int BoneA; - public readonly int BoneB; - public readonly float[] Transform; - - public BoneMapping( int boneA, int boneB, float[] transform ) { - BoneA = boneA; - BoneB = boneB; - Transform = transform; - } - } -} diff --git a/Meddle/Meddle.Utils/Skeletons/Havok/XmlSkeleton.cs b/Meddle/Meddle.Utils/Skeletons/Havok/XmlSkeleton.cs deleted file mode 100644 index 2097bdb..0000000 --- a/Meddle/Meddle.Utils/Skeletons/Havok/XmlSkeleton.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System.Xml; - -// ReSharper disable MemberCanBePrivate.Global -// ReSharper disable UnusedAutoPropertyAccessor.Global - -namespace Meddle.Utils.Skeletons.Havok; - -/// Representation of a skeleton in XML data. -public class XmlSkeleton { - /// The ID of the skeleton. - public readonly int Id; - - /// The reference pose of the skeleton (also known as the "resting" or "base" pose). - public readonly float[][] ReferencePose; - - /// The parent indices of the skeleton. The root bone will have a parent index of -1. - public readonly int[] ParentIndices; - - /// The names of the bones in the skeleton. A bone's "ID" is represented by the index it has in this array. - public readonly string[] BoneNames; - - public XmlSkeleton( XmlElement element ) { - Id = int.Parse( element.GetAttribute( "id" )[ 1.. ] ); - - ReferencePose = ReadReferencePose( element ); - ParentIndices = ReadParentIndices( element ); - BoneNames = ReadBoneNames( element ); - } - - private float[][] ReadReferencePose( XmlElement element ) { - var referencePose = element.GetElementsByTagName( "array" ) - .Cast< XmlElement >() - .Where( x => x.GetAttribute( "name" ) == "referencePose" ) - .ToArray()[ 0 ]; - - var size = int.Parse( referencePose.GetAttribute( "size" ) ); - - var referencePoseArr = new float[size][]; - - var i = 0; - foreach( var node in referencePose.ChildNodes.Cast< XmlElement >() ) { - referencePoseArr[ i ] = XmlUtils.ParseVec12( node.InnerText ); - i += 1; - } - - return referencePoseArr; - } - - private int[] ReadParentIndices( XmlElement element ) { - var parentIndices = element.GetElementsByTagName( "array" ) - .Cast< XmlElement >() - .Where( x => x.GetAttribute( "name" ) == "parentIndices" ) - .ToArray()[ 0 ]; - - var parentIndicesArr = new int[int.Parse( parentIndices.GetAttribute( "size" ) )]; - - var parentIndicesStr = parentIndices.InnerText.Split( "\n" ) - .Select( x => x.Trim() ) - .Where( x => !string.IsNullOrWhiteSpace( x ) ) - .ToArray(); - - var i = 0; - foreach( var str2 in parentIndicesStr ) { - foreach( var str3 in str2.Split( " " ) ) { - parentIndicesArr[ i ] = int.Parse( str3 ); - i++; - } - } - - return parentIndicesArr; - } - - private string[] ReadBoneNames( XmlElement element ) { - var bonesObj = element.GetElementsByTagName( "array" ) - .Cast< XmlElement >() - .Where( x => x.GetAttribute( "name" ) == "bones" ) - .ToArray()[ 0 ]; - - var bones = new string[int.Parse( bonesObj.GetAttribute( "size" ) )]; - - var boneNames = bonesObj.GetElementsByTagName( "struct" ) - .Cast< XmlElement >() - .Select( x => x.GetElementsByTagName( "string" ) - .Cast< XmlElement >() - .First( y => y.GetAttribute( "name" ) == "name" ) ); - - var i = 0; - foreach( var boneName in boneNames ) { - bones[ i ] = boneName.InnerText; - i++; - } - - return bones; - } -} diff --git a/Meddle/Meddle.Utils/Skeletons/Havok/XmlUtils.cs b/Meddle/Meddle.Utils/Skeletons/Havok/XmlUtils.cs index e723b41..2390fb5 100644 --- a/Meddle/Meddle.Utils/Skeletons/Havok/XmlUtils.cs +++ b/Meddle/Meddle.Utils/Skeletons/Havok/XmlUtils.cs @@ -1,6 +1,7 @@ using System.Globalization; using System.Numerics; using System.Text.RegularExpressions; +using Meddle.Utils.Skeletons.Havok.Models; using SharpGLTF.Transforms; namespace Meddle.Utils.Skeletons.Havok; @@ -43,7 +44,7 @@ public static AffineTransform CreateAffineTransform(ReadOnlySpan refPos) /// A list of HavokXml instances. /// The root bone node. /// A mapping of bone name to node in the scene. - public static List GetBoneMap(IEnumerable skeletons, out BoneNodeBuilder? root) + public static List GetBoneMap(IEnumerable skeletons, out BoneNodeBuilder? root) { List boneMap = new(); root = null;