From 0c22dddd0ef9991be32f115fe1947ea09a1ade3b Mon Sep 17 00:00:00 2001 From: mo Date: Mon, 1 Jun 2026 21:52:49 +0100 Subject: [PATCH] feat: IW3 xanim dumping/loading in CoD4 Mod Tools raw binary format (#768) * feat: IW3 dump xanim to cod4 mod tools compatible binary * chore: add XAnimPartType enum to game headers * chore: use XAnimPartType in XAnimDumperIW3 * chore: extract xanim filename into XAnimCommon * chore: prefer emplace_back over push_back * chore: small code style improvements * chore: use proper unsigned types for XAnimParts structs * chore: use better understandable calculations for bitfields * chore: use game names for parts * chore: rename method to WriteNoteTracks * chore: adds comments and improve clearity of what the game does * chore: extract stream writing methods into StreamUtils * chore: use vec3 for XAnimPartTransFrames mins and size * chore: properly differ between XQuat and XQuat2 structs * chore: use constants for xanim flags * chore: use optional for delta track quats and trans * chore: split delta track writing methods into quat and trans * chore: add assertion for bDelta * chore: simplify quat frame encoding indexing * chore: simplify float to int bit casting * chore: do not throw exception on failing to reconstruct bone tracks * feat: add xanim loader for iw3 * fix: make sure to sort quats and trans like the game * chore: prevent empty dumped files on bad xanim data * chore: ensure no exception on zero frames in xanim notifies * test: add system test for iw3 xanims --------- Co-authored-by: Jan Laupetin --- docs/SupportedAssetTypes.md | 2 +- src/Common/Game/IW3/IW3_Assets.h | 47 +- src/Common/Game/IW4/IW4_Assets.h | 40 +- src/Common/Game/IW5/IW5_Assets.h | 28 +- src/Common/Game/T5/T5_Assets.h | 45 +- src/Common/Game/T6/T6_Assets.h | 34 +- src/ObjCommon/XAnim/XAnimCommon.cpp | 11 + src/ObjCommon/XAnim/XAnimCommon.h | 8 + src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp | 3 +- .../Game/IW3/XAnim/XAnimLoaderIW3.cpp | 781 +++++++++++++++ .../Game/IW3/XAnim/XAnimLoaderIW3.h | 13 + src/ObjWriting/Game/IW3/ObjWriterIW3.cpp | 3 +- .../Game/IW3/XAnim/XAnimDumperIW3.cpp | 937 ++++++++++++++++++ .../Game/IW3/XAnim/XAnimDumperIW3.h | 13 + src/Utils/Utils/StreamUtils.cpp | 36 + src/Utils/Utils/StreamUtils.h | 34 + test/SystemTests.lua | 2 + test/SystemTests/Game/IW3/XAnim/test_anim | Bin 0 -> 17108 bytes test/SystemTests/Game/IW3/XAnimIW3.cpp | 62 ++ 19 files changed, 2044 insertions(+), 55 deletions(-) create mode 100644 src/ObjCommon/XAnim/XAnimCommon.cpp create mode 100644 src/ObjCommon/XAnim/XAnimCommon.h create mode 100644 src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp create mode 100644 src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.h create mode 100644 src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp create mode 100644 src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.h create mode 100644 src/Utils/Utils/StreamUtils.cpp create mode 100644 src/Utils/Utils/StreamUtils.h create mode 100644 test/SystemTests/Game/IW3/XAnim/test_anim create mode 100644 test/SystemTests/Game/IW3/XAnimIW3.cpp diff --git a/docs/SupportedAssetTypes.md b/docs/SupportedAssetTypes.md index f2d9e404..54d3c121 100644 --- a/docs/SupportedAssetTypes.md +++ b/docs/SupportedAssetTypes.md @@ -12,7 +12,7 @@ The following section specify which assets are supported to be dumped to disk (u | Asset Type | Dumping Support | Loading Support | Notes | | -------------------- | --------------- | --------------- | ---------------------------------------------------------------------------- | | PhysPreset | ✅ | ✅ | | -| XAnimParts | ❌ | ❌ | | +| XAnimParts | ✅ | ✅ | | | XModel | ✅ | ✅ | Model data can be exported to `XMODEL_EXPORT/XMODEL_BIN`, `OBJ`, `GLB/GLTF`. | | Material | ✅ | ✅ | | | MaterialTechniqueSet | ✅ | ✅ | For shaders: only dumps/loads shader bytecode. | diff --git a/src/Common/Game/IW3/IW3_Assets.h b/src/Common/Game/IW3/IW3_Assets.h index 84380acd..8c66b817 100644 --- a/src/Common/Game/IW3/IW3_Assets.h +++ b/src/Common/Game/IW3/IW3_Assets.h @@ -181,7 +181,7 @@ namespace IW3 union XAnimIndices { - char* _1; + unsigned char* _1; uint16_t* _2; void* data; }; @@ -197,7 +197,7 @@ namespace IW3 union XAnimDynamicIndicesTrans { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; @@ -209,8 +209,8 @@ namespace IW3 struct XAnimPartTransFrames { - float mins[3]; - float size[3]; + vec3_t mins; + vec3_t size; XAnimDynamicFrames frames; XAnimDynamicIndicesTrans indices; }; @@ -224,31 +224,36 @@ namespace IW3 struct XAnimPartTrans { uint16_t size; - char smallTrans; + unsigned char smallTrans; XAnimPartTransData u; }; struct type_align(4) XQuat + { + int16_t value[4]; + }; + + struct type_align(4) XQuat2 { int16_t value[2]; }; union XAnimDynamicIndicesQuat { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; struct XAnimDeltaPartQuatDataFrames { - XQuat* frames; + XQuat2* frames; XAnimDynamicIndicesQuat indices; }; union XAnimDeltaPartQuatData { XAnimDeltaPartQuatDataFrames frames; - XQuat frame0; + XQuat2 frame0; }; struct XAnimDeltaPartQuat @@ -263,6 +268,22 @@ namespace IW3 XAnimDeltaPartQuat* quat; }; + enum XAnimPartType + { + PART_TYPE_NO_QUAT = 0x0, + PART_TYPE_HALF_QUAT = 0x1, + PART_TYPE_FULL_QUAT = 0x2, + PART_TYPE_HALF_QUAT_NO_SIZE = 0x3, + PART_TYPE_FULL_QUAT_NO_SIZE = 0x4, + PART_TYPE_SMALL_TRANS = 0x5, + PART_TYPE_TRANS = 0x6, + PART_TYPE_TRANS_NO_SIZE = 0x7, + PART_TYPE_NO_TRANS = 0x8, + PART_TYPE_ALL = 0x9, + + PART_TYPE_COUNT + }; + struct XAnimParts { const char* name; @@ -274,20 +295,20 @@ namespace IW3 uint16_t numframes; bool bLoop; bool bDelta; - unsigned char boneCount[10]; - char notifyCount; - char assetType; + unsigned char boneCount[PART_TYPE_COUNT]; + unsigned char notifyCount; + unsigned char assetType; bool isDefault; unsigned int randomDataShortCount; unsigned int indexCount; float framerate; float frequency; ScriptString* names; - char* dataByte; + unsigned char* dataByte; int16_t* dataShort; int* dataInt; int16_t* randomDataShort; - char* randomDataByte; + unsigned char* randomDataByte; int* randomDataInt; XAnimIndices indices; XAnimNotifyInfo* notify; diff --git a/src/Common/Game/IW4/IW4_Assets.h b/src/Common/Game/IW4/IW4_Assets.h index 58e6925f..a378ded6 100644 --- a/src/Common/Game/IW4/IW4_Assets.h +++ b/src/Common/Game/IW4/IW4_Assets.h @@ -267,7 +267,7 @@ namespace IW4 union XAnimIndices { - char* _1; + unsigned char* _1; uint16_t* _2; void* data; }; @@ -289,14 +289,14 @@ namespace IW4 union XAnimDynamicIndicesTrans { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; struct type_align32(4) XAnimPartTransFrames { - float mins[3]; - float size[3]; + vec3_t mins; + vec3_t size; XAnimDynamicFrames frames; XAnimDynamicIndicesTrans indices; }; @@ -310,13 +310,13 @@ namespace IW4 struct XAnimPartTrans { uint16_t size; - char smallTrans; + unsigned char smallTrans; XAnimPartTransData u; }; union XAnimDynamicIndicesQuat2 { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; @@ -345,7 +345,7 @@ namespace IW4 union XAnimDynamicIndicesQuat { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; @@ -379,6 +379,22 @@ namespace IW4 XAnimDeltaPartQuat* quat; }; + enum XAnimPartType + { + PART_TYPE_NO_QUAT = 0x0, + PART_TYPE_HALF_QUAT = 0x1, + PART_TYPE_FULL_QUAT = 0x2, + PART_TYPE_HALF_QUAT_NO_SIZE = 0x3, + PART_TYPE_FULL_QUAT_NO_SIZE = 0x4, + PART_TYPE_SMALL_TRANS = 0x5, + PART_TYPE_TRANS = 0x6, + PART_TYPE_TRANS_NO_SIZE = 0x7, + PART_TYPE_NO_TRANS = 0x8, + PART_TYPE_ALL = 0x9, + + PART_TYPE_COUNT + }; + struct XAnimParts { const char* name; @@ -389,20 +405,20 @@ namespace IW4 uint16_t randomDataIntCount; uint16_t numframes; char flags; - unsigned char boneCount[10]; - char notifyCount; - char assetType; + unsigned char boneCount[PART_TYPE_COUNT]; + unsigned char notifyCount; + unsigned char assetType; bool isDefault; unsigned int randomDataShortCount; unsigned int indexCount; float framerate; float frequency; ScriptString* names; - char* dataByte; + unsigned char* dataByte; int16_t* dataShort; int* dataInt; int16_t* randomDataShort; - char* randomDataByte; + unsigned char* randomDataByte; int* randomDataInt; XAnimIndices indices; XAnimNotifyInfo* notify; diff --git a/src/Common/Game/IW5/IW5_Assets.h b/src/Common/Game/IW5/IW5_Assets.h index 3dcc1ecc..d6a436ff 100644 --- a/src/Common/Game/IW5/IW5_Assets.h +++ b/src/Common/Game/IW5/IW5_Assets.h @@ -313,14 +313,14 @@ namespace IW5 union XAnimDynamicIndicesTrans { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; struct type_align32(4) XAnimPartTransFrames { - float mins[3]; - float size[3]; + vec3_t mins; + vec3_t size; XAnimDynamicFrames frames; XAnimDynamicIndicesTrans indices; }; @@ -340,7 +340,7 @@ namespace IW5 union XAnimDynamicIndicesQuat2 { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; @@ -369,7 +369,7 @@ namespace IW5 union XAnimDynamicIndicesQuat { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; @@ -403,6 +403,22 @@ namespace IW5 XAnimDeltaPartQuat* quat; }; + enum XAnimPartType + { + PART_TYPE_NO_QUAT = 0x0, + PART_TYPE_HALF_QUAT = 0x1, + PART_TYPE_FULL_QUAT = 0x2, + PART_TYPE_HALF_QUAT_NO_SIZE = 0x3, + PART_TYPE_FULL_QUAT_NO_SIZE = 0x4, + PART_TYPE_SMALL_TRANS = 0x5, + PART_TYPE_TRANS = 0x6, + PART_TYPE_TRANS_NO_SIZE = 0x7, + PART_TYPE_NO_TRANS = 0x8, + PART_TYPE_ALL = 0x9, + + PART_TYPE_COUNT + }; + struct XAnimParts { const char* name; @@ -413,7 +429,7 @@ namespace IW5 unsigned short randomDataIntCount; unsigned short numframes; unsigned char flags; - unsigned char boneCount[10]; + unsigned char boneCount[PART_TYPE_COUNT]; unsigned char notifyCount; unsigned char assetType; bool isDefault; diff --git a/src/Common/Game/T5/T5_Assets.h b/src/Common/Game/T5/T5_Assets.h index 7ffd2c64..bc64f873 100644 --- a/src/Common/Game/T5/T5_Assets.h +++ b/src/Common/Game/T5/T5_Assets.h @@ -303,7 +303,7 @@ namespace T5 union XAnimIndices { - char* _1; + unsigned char* _1; uint16_t* _2; void* data; }; @@ -325,14 +325,14 @@ namespace T5 union XAnimDynamicIndicesTrans { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; struct XAnimPartTransFrames { - float mins[3]; - float size[3]; + vec3_t mins; + vec3_t size; XAnimDynamicFrames frames; XAnimDynamicIndicesTrans indices; }; @@ -346,31 +346,36 @@ namespace T5 struct XAnimPartTrans { uint16_t size; - char smallTrans; + unsigned char smallTrans; XAnimPartTransData u; }; struct type_align(4) XQuat + { + int16_t value[4]; + }; + + struct type_align(4) XQuat2 { int16_t value[2]; }; union XAnimDynamicIndicesQuat { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; struct XAnimDeltaPartQuatDataFrames { - XQuat* frames; + XQuat2* frames; XAnimDynamicIndicesQuat indices; }; union XAnimDeltaPartQuatData { XAnimDeltaPartQuatDataFrames frames; - XQuat frame0; + XQuat2 frame0; }; struct XAnimDeltaPartQuat @@ -385,6 +390,22 @@ namespace T5 XAnimDeltaPartQuat* quat; }; + enum XAnimPartType + { + PART_TYPE_NO_QUAT = 0x0, + PART_TYPE_HALF_QUAT = 0x1, + PART_TYPE_FULL_QUAT = 0x2, + PART_TYPE_HALF_QUAT_NO_SIZE = 0x3, + PART_TYPE_FULL_QUAT_NO_SIZE = 0x4, + PART_TYPE_SMALL_TRANS = 0x5, + PART_TYPE_TRANS = 0x6, + PART_TYPE_TRANS_NO_SIZE = 0x7, + PART_TYPE_NO_TRANS = 0x8, + PART_TYPE_ALL = 0x9, + + PART_TYPE_COUNT + }; + struct XAnimParts { const char* name; @@ -399,9 +420,9 @@ namespace T5 bool bLeftHandGripIK; bool bStreamable; unsigned int streamedFileSize; - unsigned char boneCount[10]; + unsigned char boneCount[PART_TYPE_COUNT]; unsigned char notifyCount; - char assetType; + unsigned char assetType; bool isDefault; unsigned int randomDataShortCount; unsigned int indexCount; @@ -410,11 +431,11 @@ namespace T5 float primedLength; float loopEntryTime; uint16_t* names; - char* dataByte; + unsigned char* dataByte; int16_t* dataShort; int* dataInt; int16_t* randomDataShort; - char* randomDataByte; + unsigned char* randomDataByte; int* randomDataInt; XAnimIndices indices; XAnimNotifyInfo* notify; diff --git a/src/Common/Game/T6/T6_Assets.h b/src/Common/Game/T6/T6_Assets.h index 8522c3fe..b0b631bf 100644 --- a/src/Common/Game/T6/T6_Assets.h +++ b/src/Common/Game/T6/T6_Assets.h @@ -468,11 +468,27 @@ namespace T6 union XAnimIndices { - char* _1; + unsigned char* _1; uint16_t* _2; void* data; }; + enum XAnimPartType + { + PART_TYPE_NO_QUAT = 0x0, + PART_TYPE_HALF_QUAT = 0x1, + PART_TYPE_FULL_QUAT = 0x2, + PART_TYPE_HALF_QUAT_NO_SIZE = 0x3, + PART_TYPE_FULL_QUAT_NO_SIZE = 0x4, + PART_TYPE_SMALL_TRANS = 0x5, + PART_TYPE_TRANS = 0x6, + PART_TYPE_TRANS_NO_SIZE = 0x7, + PART_TYPE_NO_TRANS = 0x8, + PART_TYPE_ALL = 0x9, + + PART_TYPE_COUNT + }; + struct XAnimParts { const char* name; @@ -487,9 +503,9 @@ namespace T6 bool bDelta3D; bool bLeftHandGripIK; unsigned int streamedFileSize; - unsigned char boneCount[10]; + unsigned char boneCount[PART_TYPE_COUNT]; unsigned char notifyCount; - char assetType; + unsigned char assetType; bool isDefault; unsigned int randomDataShortCount; unsigned int indexCount; @@ -498,11 +514,11 @@ namespace T6 float primedLength; float loopEntryTime; uint16_t* names; - char* dataByte; + unsigned char* dataByte; int16_t* dataShort; int* dataInt; int16_t* randomDataShort; - char* randomDataByte; + unsigned char* randomDataByte; int* randomDataInt; XAnimIndices indices; XAnimNotifyInfo* notify; @@ -5627,7 +5643,7 @@ namespace T6 union XAnimDynamicIndicesTrans { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; @@ -5648,13 +5664,13 @@ namespace T6 struct XAnimPartTrans { uint16_t size; - char smallTrans; + unsigned char smallTrans; XAnimPartTransData u; }; union XAnimDynamicIndicesDeltaQuat2 { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; @@ -5683,7 +5699,7 @@ namespace T6 union XAnimDynamicIndicesDeltaQuat { - char _1[1]; + unsigned char _1[1]; uint16_t _2[1]; }; diff --git a/src/ObjCommon/XAnim/XAnimCommon.cpp b/src/ObjCommon/XAnim/XAnimCommon.cpp new file mode 100644 index 00000000..539ff141 --- /dev/null +++ b/src/ObjCommon/XAnim/XAnimCommon.cpp @@ -0,0 +1,11 @@ +#include "XAnimCommon.h" + +#include + +namespace xanim +{ + std::string GetCompiledFileNameForAssetName(const std::string& assetName) + { + return std::format("xanim/{}", assetName); + } +} // namespace xanim diff --git a/src/ObjCommon/XAnim/XAnimCommon.h b/src/ObjCommon/XAnim/XAnimCommon.h new file mode 100644 index 00000000..efa0fb18 --- /dev/null +++ b/src/ObjCommon/XAnim/XAnimCommon.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace xanim +{ + [[nodiscard]] std::string GetCompiledFileNameForAssetName(const std::string& assetName); +} diff --git a/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp b/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp index 0289eac4..d0e648b4 100644 --- a/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp +++ b/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp @@ -18,6 +18,7 @@ #include "RawFile/AssetLoaderRawFileIW3.h" #include "Sound/LoaderSoundCurveIW3.h" #include "StringTable/AssetLoaderStringTableIW3.h" +#include "XAnim/XAnimLoaderIW3.h" #include @@ -99,7 +100,7 @@ namespace collection.AddAssetCreator(phys_preset::CreateRawLoaderIW3(memory, searchPath, zone)); collection.AddAssetCreator(phys_preset::CreateGdtLoaderIW3(memory, gdt, zone)); - // collection.AddAssetCreator(std::make_unique(memory)); + collection.AddAssetCreator(xanim::CreateLoaderIW3(memory, searchPath, zone)); collection.AddAssetCreator(xmodel::CreateLoaderIW3(memory, searchPath, zone)); collection.AddAssetCreator(material::CreateLoaderIW3(memory, searchPath)); // collection.AddAssetCreator(std::make_unique(memory)); diff --git a/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp b/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp new file mode 100644 index 00000000..e988fecc --- /dev/null +++ b/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp @@ -0,0 +1,781 @@ +#include "XAnimLoaderIW3.h" + +#include "Utils/Alignment.h" +#include "Utils/Logging/Log.h" +#include "Utils/StreamUtils.h" +#include "XAnim/XAnimCommon.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace IW3; + +namespace +{ + constexpr uint16_t RAW_VERSION = 17; + + constexpr uint8_t FLAG_LOOPED = 1u; + constexpr uint8_t FLAG_DELTA = 2u; + + // The linker decodes raw trans size[] with these exact float literals. + // They correspond to 1.0f / 255.0f and 1.0f / 65535.0f, but we keep the + // decompiled values to preserve binary-stable round trips. + constexpr auto HALF_TRANS_SIZE_SCALE = 0.003921568859368563f; + constexpr auto FULL_TRANS_SIZE_SCALE = 0.00001525902189314365f; + enum class QuatType : uint8_t + { + NO_QUAT = 0, + HALF_QUAT = 1, + FULL_QUAT = 2, + HALF_QUAT_NO_SIZE = 3, + FULL_QUAT_NO_SIZE = 4, + }; + + enum class TransType : uint8_t + { + SMALL_TRANS = 5, + FULL_TRANS = 6, + TRANS_NO_SIZE = 7, + NO_TRANS = 8, + }; + + struct QuatTrack + { + QuatType type = QuatType::NO_QUAT; + std::vector indices; + std::vector values; + }; + + struct TransTrack + { + TransType type = TransType::NO_TRANS; + std::vector indices; + std::array mins{}; + std::array size{}; + std::vector byteFrames; + std::vector shortFrames; + std::array constant{}; + }; + + struct BoneTrack + { + std::string name; + QuatTrack quat; + TransTrack trans; + }; + + struct FlatDataWriteCursor + { + std::vector dataByte; + std::vector dataShort; + std::vector dataInt; + std::vector randomDataByte; + std::vector randomDataShort; + std::vector indices; + }; + + void PrintError(const XAnimParts& parts, const std::string& message) + { + con::error("Cannot load xanim \"{}\": {}", parts.name, message); + } + + [[nodiscard]] bool UseByteIndices(const XAnimParts& parts) + { + return parts.numframes < 256; + } + + [[nodiscard]] int FloatBitsToInt(const float value) + { + union + { + int i; + float f; + }; + + f = value; + return i; + } + + void WriteFloat3(FlatDataWriteCursor& writeCursor, const std::array& value) + { + for (const float f : value) + writeCursor.dataInt.emplace_back(FloatBitsToInt(f)); + } + + [[nodiscard]] float DecodeRawTransSize(const float value, const bool smallTrans) + { + const auto scale = smallTrans ? HALF_TRANS_SIZE_SCALE : FULL_TRANS_SIZE_SCALE; + return value * scale; + } + + void ConsumeQuat(std::istream& stream, XQuat& quat) + { + quat.value[0] = stream::ReadValue(stream); + quat.value[1] = stream::ReadValue(stream); + quat.value[2] = stream::ReadValue(stream); + + int32_t temp = 0x3FFF0001 - (quat.value[0] * quat.value[0] + quat.value[1] * quat.value[1] + quat.value[2] * quat.value[2]); + if (temp <= 0) + temp = 0; + else + temp = static_cast(std::floor(std::sqrt(static_cast(temp)) + 0.5f)); + + assert(temp >= std::numeric_limits::min() && temp <= std::numeric_limits::max()); + quat.value[3] = static_cast(temp); + } + + void ConsumeQuat2(std::istream& stream, XQuat2& quat2) + { + quat2.value[0] = stream::ReadValue(stream); + + int32_t temp = 0x3FFF0001 - quat2.value[0] * quat2.value[0]; + if (temp <= 0) + temp = 0; + else + temp = static_cast(floor(std::sqrt(static_cast(temp)) + 0.5f)); + + assert(temp >= std::numeric_limits::min() && temp <= std::numeric_limits::max()); + quat2.value[1] = static_cast(temp); + } + + void FlipQuat(XQuat& quat) + { + quat.value[0] = static_cast(-quat.value[0]); + quat.value[1] = static_cast(-quat.value[1]); + quat.value[2] = static_cast(-quat.value[2]); + quat.value[3] = static_cast(-quat.value[3]); + } + + void FlipQuat2(XQuat2& quat) + { + quat.value[0] = static_cast(-quat.value[0]); + quat.value[1] = static_cast(-quat.value[1]); + } + + template + void LoadIndicesIfNeeded(std::istream& stream, T& indices, const uint16_t numIndices, const bool useByteIndices, const uint16_t numLoopFrames) + { + if (useByteIndices) + { + // The raw format omits indices when a track covers every loop frame in order. + if (numIndices >= numLoopFrames) + std::iota(&indices._1[0], &indices._1[numIndices], 0); + else + stream::Read(stream, indices._1, numIndices * sizeof(uint8_t)); + } + else + { + // The raw format omits indices when a track covers every loop frame in order. + if (numIndices >= numLoopFrames) + std::iota(&indices._2[0], &indices._2[numIndices], 0); + else + stream::Read(stream, indices._2, numIndices * sizeof(uint16_t)); + } + } + + void LoadIndicesIfNeeded( + std::istream& stream, std::vector& indices, const uint16_t numIndices, const bool useByteIndices, const uint16_t numLoopFrames) + { + // The raw format omits indices when a track covers every loop frame in order. + if (numIndices >= numLoopFrames) + { + indices.resize(numIndices); + std::ranges::iota(indices, 0); + } + else if (useByteIndices) + { + indices.reserve(numIndices); + for (auto i = 0u; i < numIndices; i++) + indices.emplace_back(stream::ReadValue(stream)); + } + else + { + indices.resize(numIndices); + stream::Read(stream, indices.data(), numIndices * sizeof(uint16_t)); + } + } + + void ReadTransTrack(std::istream& stream, TransTrack& transTrack, const uint16_t numLoopFrames, const bool useByteIndices) + { + const auto numTransIndices = stream::ReadValue(stream); + if (numTransIndices == 0) + { + transTrack.type = TransType::NO_TRANS; + return; + } + + if (numTransIndices == 1) + { + transTrack.type = TransType::TRANS_NO_SIZE; + for (auto& value : transTrack.constant) + value = stream::ReadValue(stream); + return; + } + + LoadIndicesIfNeeded(stream, transTrack.indices, numTransIndices, useByteIndices, numLoopFrames); + + const auto smallTrans = stream::ReadValue(stream); + transTrack.type = smallTrans ? TransType::SMALL_TRANS : TransType::FULL_TRANS; + + for (auto& value : transTrack.mins) + value = stream::ReadValue(stream); + for (auto& value : transTrack.size) + value = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + + if (smallTrans) + { + transTrack.byteFrames.resize(numTransIndices * 3); + stream::Read(stream, transTrack.byteFrames.data(), numTransIndices * sizeof(uint8_t) * 3); + } + else + { + transTrack.shortFrames.resize(numTransIndices * 3); + stream::Read(stream, transTrack.shortFrames.data(), numTransIndices * sizeof(uint16_t) * 3); + } + } + + void ReadQuatTrack( + std::istream& stream, QuatTrack& quatTrack, const uint16_t numLoopFrames, const bool useByteIndices, const bool flipQuat, const bool halfQuat) + { + const auto numQuatIndices = stream::ReadValue(stream); + if (numQuatIndices == 0) + { + assert(halfQuat); + quatTrack.type = QuatType::NO_QUAT; + return; + } + + if (numQuatIndices == 1) + { + quatTrack.type = halfQuat ? QuatType::HALF_QUAT_NO_SIZE : QuatType::FULL_QUAT_NO_SIZE; + if (halfQuat) + { + XQuat2 quat2; + ConsumeQuat2(stream, quat2); + if (flipQuat) + FlipQuat2(quat2); + + quatTrack.values.reserve(2); + quatTrack.values.emplace_back(quat2.value[0]); + quatTrack.values.emplace_back(quat2.value[1]); + } + else + { + XQuat quat; + ConsumeQuat(stream, quat); + if (flipQuat) + FlipQuat(quat); + + quatTrack.values.reserve(4); + quatTrack.values.emplace_back(quat.value[0]); + quatTrack.values.emplace_back(quat.value[1]); + quatTrack.values.emplace_back(quat.value[2]); + quatTrack.values.emplace_back(quat.value[3]); + } + + return; + } + + LoadIndicesIfNeeded(stream, quatTrack.indices, numQuatIndices, useByteIndices, numLoopFrames); + + if (halfQuat) + { + quatTrack.type = QuatType::HALF_QUAT; + quatTrack.values.resize(numQuatIndices * 2); + auto* quats2 = reinterpret_cast(quatTrack.values.data()); + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; quatIndexNum++) + { + auto& curFrame = quats2[quatIndexNum]; + ConsumeQuat2(stream, curFrame); + + if (quatIndexNum > 0) + { + const auto& prevFrame = quats2[quatIndexNum - 1]; + if (prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] < 0) + FlipQuat2(curFrame); + } + else if (flipQuat) + FlipQuat2(curFrame); + } + } + else + { + quatTrack.type = QuatType::FULL_QUAT; + quatTrack.values.resize(numQuatIndices * 4); + auto* quats = reinterpret_cast(quatTrack.values.data()); + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; quatIndexNum++) + { + auto& curFrame = quats[quatIndexNum]; + ConsumeQuat(stream, curFrame); + + if (quatIndexNum > 0) + { + const auto& prevFrame = quats[quatIndexNum - 1]; + const auto dot = prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] + prevFrame.value[2] * curFrame.value[2] + + prevFrame.value[3] * curFrame.value[3]; + if (dot < 0) + FlipQuat(curFrame); + } + else if (flipQuat) + FlipQuat(curFrame); + } + } + } + + void ApplyWriteCursorToParts(XAnimParts& parts, const FlatDataWriteCursor& writeCursor, MemoryManager& memory) + { + if (!writeCursor.dataByte.empty()) + { + parts.dataByteCount = static_cast(writeCursor.dataByte.size()); + parts.dataByte = memory.Alloc(parts.dataByteCount); + std::memcpy(parts.dataByte, writeCursor.dataByte.data(), parts.dataByteCount * sizeof(uint8_t)); + } + + if (!writeCursor.dataShort.empty()) + { + parts.dataShortCount = static_cast(writeCursor.dataShort.size()); + parts.dataShort = memory.Alloc(parts.dataShortCount); + std::memcpy(parts.dataShort, writeCursor.dataShort.data(), parts.dataShortCount * sizeof(int16_t)); + } + + if (!writeCursor.dataInt.empty()) + { + parts.dataIntCount = static_cast(writeCursor.dataInt.size()); + parts.dataInt = memory.Alloc(parts.dataIntCount); + std::memcpy(parts.dataInt, writeCursor.dataInt.data(), parts.dataIntCount * sizeof(int32_t)); + } + + if (!writeCursor.randomDataByte.empty()) + { + parts.randomDataByteCount = static_cast(writeCursor.randomDataByte.size()); + parts.randomDataByte = memory.Alloc(parts.randomDataByteCount); + std::memcpy(parts.randomDataByte, writeCursor.randomDataByte.data(), parts.randomDataByteCount * sizeof(uint8_t)); + } + + if (!writeCursor.randomDataShort.empty()) + { + parts.randomDataShortCount = static_cast(writeCursor.randomDataShort.size()); + parts.randomDataShort = memory.Alloc(parts.randomDataShortCount); + std::memcpy(parts.randomDataShort, writeCursor.randomDataShort.data(), parts.randomDataShortCount * sizeof(int16_t)); + } + + if (!writeCursor.indices.empty()) + { + parts.indexCount = static_cast(writeCursor.indices.size()); + parts.indices._2 = memory.Alloc(parts.indexCount); + std::memcpy(parts.indices._2, writeCursor.indices.data(), parts.indexCount * sizeof(uint16_t)); + } + } + + void WritePackedIndices(FlatDataWriteCursor& writeCursor, const std::vector& indices, const bool useByteIndices) + { + const auto indexCount = indices.size(); + writeCursor.dataShort.emplace_back(static_cast(indexCount - 1)); // storedSize + + if (useByteIndices) + { + for (const auto index : indices) + { + assert(index <= std::numeric_limits::max()); + writeCursor.dataByte.emplace_back(static_cast(index)); + } + } + else if (indexCount >= 65) + { + // The linker moves 16-bit frame indices into the top-level indices pool only when + // the in-memory stored size is at least 64, i.e. frameCount >= 65. + std::ranges::copy(indices, std::back_inserter(writeCursor.indices)); + + // The game inserts checkpoint values in dataShort + // Those checkpoint values are copied from positions in the full index list: the first entry, then every 256th entry, and always the final entry. + // The final entry is included even when it does not land exactly on a 256-entry boundary. + const auto longTableSize = ((indexCount - 2) / 256u) + 1; + for (auto i = 0u; i < longTableSize; i++) + writeCursor.dataShort.emplace_back(indices[256 * i]); + writeCursor.dataShort.emplace_back(indices[indices.size() - 1]); + } + else + { + std::ranges::copy(indices, std::back_inserter(writeCursor.dataShort)); + } + } + + void ProcessQuatTrack(FlatDataWriteCursor& writeCursor, const QuatTrack& quatTrack, XAnimParts& parts, const bool useByteIndices) + { + switch (quatTrack.type) + { + case QuatType::NO_QUAT: + parts.boneCount[PART_TYPE_NO_QUAT]++; + break; + + case QuatType::HALF_QUAT: + parts.boneCount[PART_TYPE_HALF_QUAT]++; + WritePackedIndices(writeCursor, quatTrack.indices, useByteIndices); + assert(quatTrack.values.size() == quatTrack.indices.size() * 2); + std::ranges::copy(quatTrack.values, std::back_inserter(writeCursor.randomDataShort)); + break; + + case QuatType::FULL_QUAT: + parts.boneCount[PART_TYPE_FULL_QUAT]++; + WritePackedIndices(writeCursor, quatTrack.indices, useByteIndices); + assert(quatTrack.values.size() == quatTrack.indices.size() * 4); + std::ranges::copy(quatTrack.values, std::back_inserter(writeCursor.randomDataShort)); + break; + + case QuatType::HALF_QUAT_NO_SIZE: + parts.boneCount[PART_TYPE_HALF_QUAT_NO_SIZE]++; + assert(quatTrack.values.size() == 2); + std::ranges::copy(quatTrack.values, std::back_inserter(writeCursor.dataShort)); + break; + + case QuatType::FULL_QUAT_NO_SIZE: + parts.boneCount[PART_TYPE_FULL_QUAT_NO_SIZE]++; + assert(quatTrack.values.size() == 4); + std::ranges::copy(quatTrack.values, std::back_inserter(writeCursor.dataShort)); + break; + } + } + + void ProcessTransTrack(FlatDataWriteCursor& writeCursor, const TransTrack& transTrack, const size_t boneIndex, XAnimParts& parts, const bool useByteIndices) + { + assert(boneIndex <= std::numeric_limits::max()); + writeCursor.dataByte.emplace_back(static_cast(boneIndex)); + + switch (transTrack.type) + { + case TransType::SMALL_TRANS: + parts.boneCount[PART_TYPE_SMALL_TRANS]++; + WritePackedIndices(writeCursor, transTrack.indices, useByteIndices); + WriteFloat3(writeCursor, transTrack.mins); + WriteFloat3(writeCursor, transTrack.size); + assert(transTrack.byteFrames.size() == transTrack.indices.size() * 3); + std::ranges::copy(transTrack.byteFrames, std::back_inserter(writeCursor.randomDataByte)); + break; + + case TransType::FULL_TRANS: + parts.boneCount[PART_TYPE_TRANS]++; + WritePackedIndices(writeCursor, transTrack.indices, useByteIndices); + WriteFloat3(writeCursor, transTrack.mins); + WriteFloat3(writeCursor, transTrack.size); + assert(transTrack.shortFrames.size() == transTrack.indices.size() * 3); + std::ranges::copy(transTrack.shortFrames, std::back_inserter(writeCursor.randomDataShort)); + break; + + case TransType::TRANS_NO_SIZE: + parts.boneCount[PART_TYPE_TRANS_NO_SIZE]++; + WriteFloat3(writeCursor, transTrack.constant); + break; + + case TransType::NO_TRANS: + parts.boneCount[PART_TYPE_NO_TRANS]++; + break; + } + } + + class XAnimLoader final : public AssetCreator + { + public: + XAnimLoader(MemoryManager& memory, ISearchPath& searchPath, ZoneScriptStrings& scriptStrings) + : m_memory(memory), + m_search_path(searchPath), + m_script_strings(scriptStrings) + { + } + + AssetCreationResult CreateAsset(const std::string& assetName, AssetCreationContext& context) override + { + const auto file = m_search_path.Open(xanim::GetCompiledFileNameForAssetName(assetName)); + if (!file.IsOpen()) + return AssetCreationResult::NoAction(); + + auto* parts = m_memory.Alloc(); + parts->name = m_memory.Dup(assetName.c_str()); + + AssetRegistration registration(assetName, parts); + if (!LoadFromFile(*file.m_stream, *parts, registration)) + { + con::error("Failed to load xanim \"{}\"", assetName); + return AssetCreationResult::Failure(); + } + + return AssetCreationResult::Success(context.AddAsset(std::move(registration))); + } + + private: + void ReadNoteTracks(std::istream& stream, XAnimParts& parts, AssetRegistration& registration) const + { + const auto numDiskNoteTracks = stream::ReadValue(stream); + assert(numDiskNoteTracks + 1 <= std::numeric_limits::max()); + + uint8_t numNoteTracks; + if (numDiskNoteTracks == std::numeric_limits::max()) + { + PrintError(parts, "Could not add \"end\" notify as maximum notify entries were reached"); + numNoteTracks = numDiskNoteTracks; + } + else + numNoteTracks = numDiskNoteTracks + 1; + + parts.notifyCount = numNoteTracks; + parts.notify = m_memory.Alloc(numNoteTracks); + + for (auto notifyIndex = 0u; notifyIndex < numDiskNoteTracks; notifyIndex++) + { + auto& notify = parts.notify[notifyIndex]; + + const auto notifyName = stream::ReadCString(stream); + notify.name = m_script_strings.AddOrGetScriptString(notifyName); + registration.AddScriptString(notify.name); + + const auto frame = stream::ReadValue(stream); + notify.time = parts.numframes > 0 ? static_cast(frame) / static_cast(parts.numframes) : 0; + assert(notify.time >= 0.0f && notify.time <= 1.0f); + } + + if (numNoteTracks > numDiskNoteTracks) + { + const auto endScriptString = m_script_strings.AddOrGetScriptString("end"); + registration.AddScriptString(endScriptString); + parts.notify[numDiskNoteTracks].name = endScriptString; + parts.notify[numDiskNoteTracks].time = 1.0f; + } + } + + void LoadDeltaQuats(std::istream& stream, XAnimDeltaPart& delta, const bool useByteIndices, const uint16_t numLoopFrames) const + { + const auto numQuatIndices = stream::ReadValue(stream); + if (numQuatIndices == 0) + return; + + if (numQuatIndices == 1) + { + delta.quat = static_cast(m_memory.AllocRaw(offsetof(XAnimDeltaPartQuat, u) + sizeof(XAnimDeltaPartQuatData::frame0))); + delta.quat->size = 0; + ConsumeQuat2(stream, delta.quat->u.frame0); + return; + } + + const auto indicesArraySize = + useByteIndices ? numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_1) : numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_2); + + delta.quat = static_cast( + m_memory.AllocRaw(offsetof(XAnimDeltaPartQuat, u) + offsetof(XAnimDeltaPartQuatDataFrames, indices) + indicesArraySize)); + + auto& quatIndices = delta.quat->u.frames.indices; + LoadIndicesIfNeeded(stream, quatIndices, numQuatIndices, useByteIndices, numLoopFrames); + + delta.quat->size = static_cast(numQuatIndices - 1); + delta.quat->u.frames.frames = m_memory.Alloc(numQuatIndices); + + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum) + { + auto& curFrame = delta.quat->u.frames.frames[quatIndexNum]; + ConsumeQuat2(stream, curFrame); + + if (quatIndexNum > 0) + { + const auto& prevFrame = delta.quat->u.frames.frames[quatIndexNum - 1]; + if (prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] < 0) + FlipQuat2(curFrame); + } + } + } + + void LoadDeltaTrans(std::istream& stream, XAnimDeltaPart& delta, const bool useByteIndices, const uint16_t numLoopFrames) const + { + const auto numTransIndices = stream::ReadValue(stream); + if (numTransIndices == 0) + return; + + if (numTransIndices == 1) + { + delta.trans = static_cast(m_memory.AllocRaw(offsetof(XAnimPartTrans, u) + sizeof(XAnimPartTransData::frame0))); + delta.trans->size = 0; + delta.trans->u.frame0.x = stream::ReadValue(stream); + delta.trans->u.frame0.y = stream::ReadValue(stream); + delta.trans->u.frame0.z = stream::ReadValue(stream); + return; + } + const auto indicesArraySize = + useByteIndices ? numTransIndices * sizeof(XAnimDynamicIndicesTrans::_1) : numTransIndices * sizeof(XAnimDynamicIndicesTrans::_2); + + delta.trans = + static_cast(m_memory.AllocRaw(offsetof(XAnimPartTrans, u) + offsetof(XAnimPartTransFrames, indices) + indicesArraySize)); + + auto& frames = delta.trans->u.frames; + LoadIndicesIfNeeded(stream, frames.indices, numTransIndices, useByteIndices, numLoopFrames); + + const auto smallTrans = stream::ReadValue(stream); + delta.trans->smallTrans = smallTrans ? 1 : 0; + + frames.mins.x = stream::ReadValue(stream); + frames.mins.y = stream::ReadValue(stream); + frames.mins.z = stream::ReadValue(stream); + + frames.size.x = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + frames.size.y = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + frames.size.z = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + + delta.trans->size = static_cast(numTransIndices - 1); + if (smallTrans) + { + frames.frames._1 = m_memory.Alloc(numTransIndices); + stream::Read(stream, frames.frames._1, numTransIndices * sizeof(ByteVec)); + } + else + { + frames.frames._2 = m_memory.Alloc(numTransIndices); + stream::Read(stream, frames.frames._2, numTransIndices * sizeof(UShortVec)); + } + } + + void LoadDeltaTrack(std::istream& stream, XAnimParts& parts, const bool useByteIndices, const uint16_t numLoopFrames) const + { + auto* delta = m_memory.Alloc(); + parts.deltaPart = delta; + + LoadDeltaQuats(stream, *delta, useByteIndices, numLoopFrames); + LoadDeltaTrans(stream, *delta, useByteIndices, numLoopFrames); + } + + bool LoadFromFile(std::istream& stream, XAnimParts& parts, AssetRegistration& registration) const + { + const auto fileVersion = stream::ReadValue(stream); + if (fileVersion != RAW_VERSION) + { + PrintError(parts, std::format("Unsupported version number {} (expected {})", fileVersion, RAW_VERSION)); + return false; + } + + const auto numFrames = stream::ReadValue(stream); + const auto boneCount = stream::ReadValue(stream); + const auto flags = stream::ReadValue(stream); + const auto assetType = stream::ReadValue(stream); + const auto framerate = stream::ReadValue(stream); + if (stream.fail()) + { + PrintError(parts, "Truncated file"); + return false; + } + + const bool isLooped = flags & FLAG_LOOPED; + const bool hasDelta = flags & FLAG_DELTA; + const uint16_t numLoopFrames = isLooped ? numFrames + 1u : numFrames; + + parts.numframes = numLoopFrames - 1; + parts.bLoop = isLooped; + parts.bDelta = hasDelta; + parts.assetType = assetType; + parts.framerate = static_cast(framerate); + parts.frequency = parts.numframes > 0 ? parts.framerate / static_cast(parts.numframes) : 0; + + const auto useByteIndices = UseByteIndices(parts); + + if (hasDelta) + LoadDeltaTrack(stream, parts, useByteIndices, numLoopFrames); + + std::vector boneTracks; + if (boneCount > 0) + { + const auto bitmaskSize = utils::Align(boneCount, 8u) / 8u; + std::vector flipQuatBits(bitmaskSize, 0); + std::vector halfQuatBits(bitmaskSize, 0); + stream::Read(stream, flipQuatBits.data(), bitmaskSize); + stream::Read(stream, halfQuatBits.data(), bitmaskSize); + + boneTracks.resize(boneCount); + for (size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex) + boneTracks[boneIndex].name = stream::ReadCString(stream); + + for (size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex) + { + auto& boneTrack = boneTracks[boneIndex]; + + const bool flipQuat = flipQuatBits[boneIndex / 8u] & static_cast(1u << (boneIndex % 8u)); + const bool halfQuat = halfQuatBits[boneIndex / 8u] & static_cast(1u << (boneIndex % 8u)); + + ReadQuatTrack(stream, boneTrack.quat, numLoopFrames, useByteIndices, flipQuat, halfQuat); + ReadTransTrack(stream, boneTrack.trans, numLoopFrames, useByteIndices); + } + } + + ReadNoteTracks(stream, parts, registration); + + FlatDataWriteCursor writeCursor; + + std::vector boneOrder(boneCount); + std::ranges::iota(boneOrder, 0); + + std::ranges::sort(boneOrder, + [&boneTracks](const size_t i0, const size_t i1) + { + const auto type0 = std::to_underlying(boneTracks[i0].quat.type); + const auto type1 = std::to_underlying(boneTracks[i1].quat.type); + if (type0 != type1) + return type0 < type1; + + return i0 < i1; + }); + + // The parts bone indices are based on the quats order + std::vector boneTrackIndexToPartsBoneIndex(boneCount); + parts.names = m_memory.Alloc(boneCount); + for (auto partsBoneIndex = 0u; partsBoneIndex < boneCount; ++partsBoneIndex) + { + const auto boneTrackIndex = boneOrder[partsBoneIndex]; + boneTrackIndexToPartsBoneIndex[boneTrackIndex] = partsBoneIndex; + ProcessQuatTrack(writeCursor, boneTracks[boneTrackIndex].quat, parts, useByteIndices); + + // Names are based on quats order so apply them here as well + const auto scrString = m_script_strings.AddOrGetScriptString(boneTracks[boneTrackIndex].name); + parts.names[partsBoneIndex] = scrString; + registration.AddScriptString(scrString); + } + + // Trans are ordered differently + std::ranges::sort(boneOrder, + [&boneTracks, &boneTrackIndexToPartsBoneIndex](const size_t i0, const size_t i1) + { + const auto type0 = std::to_underlying(boneTracks[i0].trans.type); + const auto type1 = std::to_underlying(boneTracks[i1].trans.type); + if (type0 != type1) + return type0 < type1; + + return boneTrackIndexToPartsBoneIndex[i0] < boneTrackIndexToPartsBoneIndex[i1]; + }); + for (auto partsBoneIndex = 0u; partsBoneIndex < boneCount; ++partsBoneIndex) + { + const auto boneTrackIndex = boneOrder[partsBoneIndex]; + ProcessTransTrack(writeCursor, boneTracks[boneTrackIndex].trans, boneTrackIndexToPartsBoneIndex[boneTrackIndex], parts, useByteIndices); + } + + ApplyWriteCursorToParts(parts, writeCursor, m_memory); + parts.boneCount[PART_TYPE_ALL] = static_cast(boneCount); + + assert(stream.peek() == std::char_traits::eof()); + return true; + } + + MemoryManager& m_memory; + ISearchPath& m_search_path; + ZoneScriptStrings& m_script_strings; + }; +} // namespace + +namespace xanim +{ + std::unique_ptr> CreateLoaderIW3(MemoryManager& memory, ISearchPath& searchPath, Zone& zone) + { + return std::make_unique(memory, searchPath, zone.m_script_strings); + } +} // namespace xanim diff --git a/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.h b/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.h new file mode 100644 index 00000000..92dfab55 --- /dev/null +++ b/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Asset/IAssetCreator.h" +#include "Game/IW3/IW3.h" +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +#include + +namespace xanim +{ + std::unique_ptr> CreateLoaderIW3(MemoryManager& memory, ISearchPath& searchPath, Zone& zone); +} // namespace xanim diff --git a/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp b/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp index 6d1d6346..b0b828c2 100644 --- a/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp +++ b/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp @@ -12,13 +12,14 @@ #include "Sound/LoadedSoundDumperIW3.h" #include "Sound/SndCurveDumperIW3.h" #include "StringTable/StringTableDumperIW3.h" +#include "XAnim/XAnimDumperIW3.h" using namespace IW3; void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context) { RegisterAssetDumper(std::make_unique()); - // REGISTER_DUMPER(AssetDumperXAnimParts) + RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique( diff --git a/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp b/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp new file mode 100644 index 00000000..2274940e --- /dev/null +++ b/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp @@ -0,0 +1,937 @@ +#include "XAnimDumperIW3.h" + +#include "Utils/Alignment.h" +#include "Utils/StreamUtils.h" +#include "XAnim/XAnimCommon.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace IW3; + +namespace +{ + constexpr uint16_t RAW_VERSION = 17; + + constexpr uint8_t FLAG_LOOPED = 1u; + constexpr uint8_t FLAG_DELTA = 2u; + + // The linker decodes raw trans size[] with these exact float literals. + // They correspond to 1.0f / 255.0f and 1.0f / 65535.0f, but we keep the + // decompiled values to preserve binary-stable round trips. + constexpr auto HALF_TRANS_SIZE_SCALE = 0.003921568859368563f; + constexpr auto FULL_TRANS_SIZE_SCALE = 0.00001525902189314365f; + + enum class QuatType : uint8_t + { + NO_QUAT = 0, + HALF_QUAT = 1, + FULL_QUAT = 2, + HALF_QUAT_NO_SIZE = 3, + FULL_QUAT_NO_SIZE = 4, + }; + + enum class TransType : uint8_t + { + SMALL_TRANS = 5, + FULL_TRANS = 6, + TRANS_NO_SIZE = 7, + NO_TRANS = 8, + }; + + struct QuatTrack + { + QuatType type = QuatType::NO_QUAT; + std::vector indices; + std::vector values; + }; + + struct TransTrack + { + TransType type = TransType::NO_TRANS; + std::vector indices; + std::array mins{}; + std::array size{}; + std::vector byteFrames; + std::vector shortFrames; + std::array constant{}; + }; + + struct BoneTrack + { + std::string name; + QuatTrack quat; + TransTrack trans; + }; + + struct FlatDataCursor + { + const uint8_t* dataByte; + const int16_t* dataShort; + const int* dataInt; + const uint8_t* randomDataByte; + const int16_t* randomDataShort; + const uint16_t* indices; + }; + + struct DeltaQuatTrack + { + bool keyframed = false; + std::vector indices; + std::vector values; + }; + + struct DeltaTransTrack + { + bool keyframed = false; + bool smallTrans = false; + std::vector indices; + std::array mins{}; + std::array size{}; + std::vector byteFrames; + std::vector shortFrames; + std::array constant{}; + }; + + struct DeltaTrack + { + std::optional quat; + std::optional trans; + }; + + struct EncodedQuatTrack + { + bool flipQuat = false; + std::vector storedValues; + }; + + [[nodiscard]] const std::string& ResolveScriptString(const XAssetInfo& asset, const ScriptString value) + { + assert(asset.m_zone != nullptr && value < asset.m_zone->m_script_strings.Count()); + return asset.m_zone->m_script_strings[value]; + } + + [[nodiscard]] uint16_t GetNumLoopFrames(const XAnimParts& parts) + { + assert(parts.numframes < std::numeric_limits::max()); + // Raw non-looped xanims store numframes + 1 in keyed track counts/header fields. + return static_cast(parts.numframes + 1u); + } + + [[nodiscard]] bool UseByteIndices(const XAnimParts& parts) + { + return parts.numframes < 256; + } + + [[nodiscard]] float IntBitsToFloat(const int value) + { + union + { + int i; + float f; + }; + + i = value; + return f; + } + + [[nodiscard]] std::array ReadFloat3(const int*& dataInt) + { + std::array result{}; + for (float& i : result) + i = IntBitsToFloat(*dataInt++); + return result; + } + + template [[nodiscard]] const T* AdvancePtr(const T* ptr, const size_t count) + { + if (count == 0uz) + return ptr; + + assert(ptr != nullptr); + return ptr + count; + } + + [[nodiscard]] std::vector ReadPackedIndices(FlatDataCursor& cursor, const uint16_t storedSize, const bool useByteIndices) + { + const auto count = static_cast(storedSize) + 1uz; + std::vector result(count); + + if (useByteIndices) + { + for (auto i = 0uz; i < count; i++) + result[i] = cursor.dataByte[i]; + + cursor.dataByte += count; + return result; + } + + // The linker moves 16-bit frame indices into the top-level indices pool only when + // the in-memory stored size is at least 64, i.e. frameCount >= 65. + if (storedSize >= 64u) + { + for (auto i = 0uz; i < count; i++) + result[i] = cursor.indices[i]; + + cursor.indices += count; + + // The game inserts checkpoint values in dataShort + // Those checkpoint values are copied from positions in the full index list: the first entry, then every 256th entry, and always the final entry. + // The final entry is included even when it does not land exactly on a 256-entry boundary. + cursor.dataShort += ((count - 2uz) / 256u) + 2uz; + return result; + } + + for (auto i = 0uz; i < count; i++) + result[i] = static_cast(cursor.dataShort[i]); + + cursor.dataShort += count; + return result; + } + + [[nodiscard]] bool IsSequentialCoverage(const std::vector& indices, const uint16_t numLoopFrames) + { + if (indices.size() != numLoopFrames) + return false; + + for (auto i = 0uz; i < indices.size(); i++) + { + if (indices[i] != i) + return false; + } + + return true; + } + + [[nodiscard]] bool QuatTypeUsesHalf(const QuatType type) + { + return type == QuatType::NO_QUAT || type == QuatType::HALF_QUAT || type == QuatType::HALF_QUAT_NO_SIZE; + } + + [[nodiscard]] float EncodeRawTransSize(const float value, const bool smallTrans) + { + const auto scale = smallTrans ? HALF_TRANS_SIZE_SCALE : FULL_TRANS_SIZE_SCALE; + return value / scale; + } + + [[nodiscard]] int64_t ComputeQuatDot(const int16_t* lhs, const int16_t* rhs, const size_t componentCount) + { + int64_t result = 0; + for (auto i = 0uz; i < componentCount; i++) + result += static_cast(lhs[i]) * static_cast(rhs[i]); + + return result; + } + + [[nodiscard]] EncodedQuatTrack EncodeQuatFrames(const int16_t* values, const size_t frameCount, const size_t componentCount, const bool allowFlipQuat) + { + assert(componentCount == 2uz || componentCount == 4uz); + + EncodedQuatTrack result; + if (frameCount == 0uz) + return result; + + const auto storedComponentCount = componentCount - 1uz; + result.storedValues.reserve(frameCount * storedComponentCount); + + // Raw IW3 xanims store only N-1 quat components. The loader reconstructs the + // final component with a positive sqrt, applies the per-bone flip bit, and then + // continuity-corrects subsequent frames by optionally negating whole quats. + result.flipQuat = allowFlipQuat && values[storedComponentCount] < 0; + const auto targetNegativeOmitted = result.flipQuat; + + for (auto frameIndex = 0uz; frameIndex < frameCount; frameIndex++) + { + const auto* frame = &values[frameIndex * componentCount]; + const auto omittedNegative = frame[storedComponentCount] < 0; + + auto continuityNegated = false; + if (frameIndex > 0uz && omittedNegative != targetNegativeOmitted) + { + const auto* prevFrame = &values[(frameIndex - 1uz) * componentCount]; + continuityNegated = ComputeQuatDot(prevFrame, frame, componentCount) > 0; + } + + const auto rawNegated = result.flipQuat != continuityNegated; + const auto sign = rawNegated ? -1 : 1; + + for (auto componentIndex = 0uz; componentIndex < storedComponentCount; componentIndex++) + { + const auto value = static_cast(frame[componentIndex]) * sign; + assert(value >= std::numeric_limits::min() && value <= std::numeric_limits::max()); + result.storedValues.emplace_back(static_cast(value)); + } + } + + return result; + } + + [[nodiscard]] EncodedQuatTrack EncodeQuatTrack(const QuatTrack& quat) + { + switch (quat.type) + { + case QuatType::NO_QUAT: + return {}; + + case QuatType::HALF_QUAT_NO_SIZE: + assert(quat.values.size() == 2uz); + return EncodeQuatFrames(quat.values.data(), 1uz, 2uz, true); + + case QuatType::FULL_QUAT_NO_SIZE: + assert(quat.values.size() == 4uz); + return EncodeQuatFrames(quat.values.data(), 1uz, 4uz, true); + + case QuatType::HALF_QUAT: + { + const auto frameCount = quat.indices.size(); + assert(quat.values.size() == frameCount * 2uz); + return EncodeQuatFrames(quat.values.data(), frameCount, 2uz, true); + } + + case QuatType::FULL_QUAT: + { + const auto frameCount = quat.indices.size(); + assert(quat.values.size() == frameCount * 4uz); + return EncodeQuatFrames(quat.values.data(), frameCount, 4uz, true); + } + } + + assert(false); + return {}; + } + + [[nodiscard]] EncodedQuatTrack EncodeDeltaQuatTrack(const DeltaTrack& delta) + { + if (!delta.quat) + return {}; + + // Delta quats are serialized without the per-bone flipQuat mask used by normal bone quats. + if (!delta.quat->keyframed) + { + assert(delta.quat->values.size() == 2uz); + return EncodeQuatFrames(delta.quat->values.data(), 1uz, 2uz, false); + } + + const auto frameCount = delta.quat->indices.size(); + assert(delta.quat->values.size() == frameCount * 2uz); + return EncodeQuatFrames(delta.quat->values.data(), frameCount, 2uz, false); + } + + std::string CreateReconstructionError(const XAssetInfo& asset, const char* field) + { + return std::format("IW3 xanim raw reconstruction cursor mismatch for asset \"{}\" in {}", asset.m_name, field); + } + + [[nodiscard]] std::expected, std::string> ReconstructBoneTracks(const XAssetInfo& asset) + { + const auto& parts = *asset.Asset(); + const auto nameCount = static_cast(parts.boneCount[PART_TYPE_ALL]); + const auto useByteIndices = UseByteIndices(parts); + + std::vector bones(nameCount); + for (auto i = 0uz; i < nameCount; i++) + bones[i].name = ResolveScriptString(asset, parts.names[i]); + + // Root indices should only ever be used when it is !useByteIndices, therefore we should be safe to always use the short version + assert(!useByteIndices || parts.indices._1 == nullptr); + + auto cursor = FlatDataCursor{ + .dataByte = parts.dataByte, + .dataShort = parts.dataShort, + .dataInt = parts.dataInt, + .randomDataByte = parts.randomDataByte, + .randomDataShort = parts.randomDataShort, + .indices = parts.indices._2, + }; + + size_t boneIndex = 0; + + for (auto i = 0u; i < parts.boneCount[PART_TYPE_NO_QUAT]; i++, boneIndex++) + bones[boneIndex].quat.type = QuatType::NO_QUAT; + + for (auto i = 0u; i < parts.boneCount[PART_TYPE_HALF_QUAT]; i++, boneIndex++) + { + auto& quat = bones[boneIndex].quat; + quat.type = QuatType::HALF_QUAT; + const auto storedSize = static_cast(*cursor.dataShort++); + const auto frameCount = static_cast(storedSize) + 1uz; + quat.indices = ReadPackedIndices(cursor, storedSize, useByteIndices); + quat.values.assign(cursor.randomDataShort, cursor.randomDataShort + frameCount * 2uz); + cursor.randomDataShort += frameCount * 2uz; + } + + for (auto i = 0u; i < parts.boneCount[PART_TYPE_FULL_QUAT]; i++, boneIndex++) + { + auto& quat = bones[boneIndex].quat; + quat.type = QuatType::FULL_QUAT; + const auto storedSize = static_cast(*cursor.dataShort++); + const auto frameCount = static_cast(storedSize) + 1uz; + quat.indices = ReadPackedIndices(cursor, storedSize, useByteIndices); + quat.values.assign(cursor.randomDataShort, cursor.randomDataShort + frameCount * 4uz); + cursor.randomDataShort += frameCount * 4uz; + } + + for (auto i = 0u; i < parts.boneCount[PART_TYPE_HALF_QUAT_NO_SIZE]; i++, boneIndex++) + { + auto& quat = bones[boneIndex].quat; + quat.type = QuatType::HALF_QUAT_NO_SIZE; + quat.values.assign(cursor.dataShort, cursor.dataShort + 2); + cursor.dataShort += 2; + } + + for (auto i = 0u; i < parts.boneCount[PART_TYPE_FULL_QUAT_NO_SIZE]; i++, boneIndex++) + { + auto& quat = bones[boneIndex].quat; + quat.type = QuatType::FULL_QUAT_NO_SIZE; + quat.values.assign(cursor.dataShort, cursor.dataShort + 4); + cursor.dataShort += 4; + } + + std::vector transAssigned(nameCount, false); + + for (auto i = 0u; i < parts.boneCount[PART_TYPE_SMALL_TRANS]; i++) + { + const auto bone = static_cast(*cursor.dataByte++); + assert(bone < nameCount && !transAssigned[bone]); + + auto& trans = bones[bone].trans; + transAssigned[bone] = true; + trans.type = TransType::SMALL_TRANS; + + const auto storedSize = static_cast(*cursor.dataShort++); + const auto frameCount = static_cast(storedSize) + 1uz; + trans.mins = ReadFloat3(cursor.dataInt); + trans.size = ReadFloat3(cursor.dataInt); + trans.indices = ReadPackedIndices(cursor, storedSize, useByteIndices); + trans.byteFrames.assign(cursor.randomDataByte, cursor.randomDataByte + frameCount * 3uz); + cursor.randomDataByte += frameCount * 3uz; + } + + for (auto i = 0u; i < parts.boneCount[PART_TYPE_TRANS]; i++) + { + const auto bone = static_cast(*cursor.dataByte++); + assert(bone < nameCount && !transAssigned[bone]); + + auto& trans = bones[bone].trans; + transAssigned[bone] = true; + trans.type = TransType::FULL_TRANS; + + const auto storedSize = static_cast(*cursor.dataShort++); + const auto frameCount = static_cast(storedSize) + 1uz; + trans.mins = ReadFloat3(cursor.dataInt); + trans.size = ReadFloat3(cursor.dataInt); + trans.indices = ReadPackedIndices(cursor, storedSize, useByteIndices); + trans.shortFrames.reserve(frameCount * 3uz); + for (auto frame = 0uz; frame < frameCount * 3uz; frame++) + trans.shortFrames.emplace_back(static_cast(*cursor.randomDataShort++)); + } + + for (auto i = 0u; i < parts.boneCount[PART_TYPE_TRANS_NO_SIZE]; i++) + { + const auto bone = static_cast(*cursor.dataByte++); + assert(bone < nameCount && !transAssigned[bone]); + + auto& trans = bones[bone].trans; + transAssigned[bone] = true; + trans.type = TransType::TRANS_NO_SIZE; + trans.constant = ReadFloat3(cursor.dataInt); + } + + for (auto i = 0u; i < parts.boneCount[PART_TYPE_NO_TRANS]; i++) + { + const auto bone = static_cast(*cursor.dataByte++); + assert(bone < nameCount && !transAssigned[bone]); + + bones[bone].trans.type = TransType::NO_TRANS; + transAssigned[bone] = true; + } + + for (auto i = 0uz; i < nameCount; i++) + assert(transAssigned[i]); + + const auto dataByteEnd = AdvancePtr(parts.dataByte, parts.dataByteCount); + const auto dataShortEnd = AdvancePtr(parts.dataShort, parts.dataShortCount); + const auto dataIntEnd = AdvancePtr(parts.dataInt, parts.dataIntCount); + const auto randomDataByteEnd = AdvancePtr(parts.randomDataByte, parts.randomDataByteCount); + const auto randomDataShortEnd = AdvancePtr(parts.randomDataShort, parts.randomDataShortCount); + + if (cursor.dataByte != dataByteEnd) + return std::unexpected(CreateReconstructionError(asset, "dataByte")); + if (cursor.dataShort != dataShortEnd) + return std::unexpected(CreateReconstructionError(asset, "dataShort")); + if (cursor.dataInt != dataIntEnd) + return std::unexpected(CreateReconstructionError(asset, "dataInt")); + if (cursor.randomDataByte != randomDataByteEnd) + return std::unexpected(CreateReconstructionError(asset, "randomDataByte")); + if (cursor.randomDataShort != randomDataShortEnd) + return std::unexpected(CreateReconstructionError(asset, "randomDataShort")); + + if (!useByteIndices) + { + const auto indicesEnd = AdvancePtr(parts.indices._2, parts.indexCount); + if (cursor.indices != indicesEnd) + return std::unexpected(CreateReconstructionError(asset, "indices")); + } + else + { + assert(parts.indexCount == 0); + } + + return bones; + } + + [[nodiscard]] DeltaTrack ReconstructDeltaTrack(const XAnimParts& parts) + { + DeltaTrack result; + + assert(static_cast(parts.deltaPart) == static_cast(parts.bDelta)); + if (!parts.deltaPart) + return result; + + const auto numLoopFrames = GetNumLoopFrames(parts); + const auto useByteIndices = UseByteIndices(parts); + + if (const auto* quat = parts.deltaPart->quat; quat) + { + result.quat.emplace(); + + if (quat->size > 0) + { + result.quat->keyframed = true; + const auto frameCount = static_cast(quat->size) + 1uz; + result.quat->values.reserve(frameCount * 2uz); + result.quat->indices.reserve(frameCount); + + for (auto i = 0uz; i < frameCount; i++) + { + result.quat->values.emplace_back(quat->u.frames.frames[i].value[0]); + result.quat->values.emplace_back(quat->u.frames.frames[i].value[1]); + } + + if (useByteIndices) + { + for (auto i = 0uz; i < frameCount; i++) + result.quat->indices.emplace_back(static_cast(quat->u.frames.indices._1[i])); + } + else + { + for (auto i = 0uz; i < frameCount; i++) + result.quat->indices.emplace_back(quat->u.frames.indices._2[i]); + } + + assert(result.quat->indices.size() <= numLoopFrames); + } + else + { + result.quat->values.emplace_back(quat->u.frame0.value[0]); + result.quat->values.emplace_back(quat->u.frame0.value[1]); + } + } + + if (const auto* trans = parts.deltaPart->trans; trans) + { + result.trans.emplace(); + + if (trans->size > 0) + { + result.trans->keyframed = true; + result.trans->smallTrans = trans->smallTrans; + result.trans->mins = {trans->u.frames.mins.x, trans->u.frames.mins.y, trans->u.frames.mins.z}; + result.trans->size = {trans->u.frames.size.x, trans->u.frames.size.y, trans->u.frames.size.z}; + + const auto frameCount = static_cast(trans->size) + 1uz; + result.trans->indices.reserve(frameCount); + if (useByteIndices) + { + for (auto i = 0uz; i < frameCount; i++) + result.trans->indices.emplace_back(static_cast(trans->u.frames.indices._1[i])); + } + else + { + for (auto i = 0uz; i < frameCount; i++) + result.trans->indices.emplace_back(trans->u.frames.indices._2[i]); + } + + if (trans->smallTrans) + { + result.trans->byteFrames.reserve(frameCount * 3uz); + for (auto i = 0uz; i < frameCount; i++) + { + result.trans->byteFrames.emplace_back(trans->u.frames.frames._1[i][0]); + result.trans->byteFrames.emplace_back(trans->u.frames.frames._1[i][1]); + result.trans->byteFrames.emplace_back(trans->u.frames.frames._1[i][2]); + } + } + else + { + result.trans->shortFrames.reserve(frameCount * 3uz); + for (auto i = 0uz; i < frameCount; i++) + { + result.trans->shortFrames.emplace_back(trans->u.frames.frames._2[i][0]); + result.trans->shortFrames.emplace_back(trans->u.frames.frames._2[i][1]); + result.trans->shortFrames.emplace_back(trans->u.frames.frames._2[i][2]); + } + } + } + else + { + result.trans->constant = {trans->u.frame0.v[0], trans->u.frame0.v[1], trans->u.frame0.v[2]}; + } + } + + return result; + } + + void WriteIndicesIfNeeded(std::ostream& stream, const std::vector& indices, const uint16_t numLoopFrames, const bool useByteIndices) + { + if (indices.empty()) + return; + + // The raw format omits indices when a track covers every loop frame in order. + if (indices.size() >= numLoopFrames) + { + assert(IsSequentialCoverage(indices, numLoopFrames)); + return; + } + + if (useByteIndices) + { + for (const auto index : indices) + { + assert(index <= std::numeric_limits::max()); + const auto asByte = static_cast(index); + stream::WriteValue(stream, asByte); + } + } + else + { + for (const auto index : indices) + stream::WriteValue(stream, index); + } + } + + void WriteQuatTrack( + std::ostream& stream, const QuatTrack& quat, const EncodedQuatTrack& encodedQuat, const uint16_t numLoopFrames, const bool useByteIndices) + { + switch (quat.type) + { + case QuatType::NO_QUAT: + { + stream::WriteValue(stream, static_cast(0)); + break; + } + + case QuatType::HALF_QUAT_NO_SIZE: + { + assert(encodedQuat.storedValues.size() == 1uz); + stream::WriteValue(stream, static_cast(1)); + stream::WriteValue(stream, encodedQuat.storedValues[0]); + break; + } + + case QuatType::FULL_QUAT_NO_SIZE: + { + assert(encodedQuat.storedValues.size() == 3uz); + stream::WriteValue(stream, static_cast(1)); + for (const auto value : encodedQuat.storedValues) + stream::WriteValue(stream, value); + break; + } + + case QuatType::HALF_QUAT: + { + const auto frameCount = quat.indices.size(); + assert(frameCount > 0uz); + assert(quat.values.size() == frameCount * 2uz); + assert(encodedQuat.storedValues.size() == frameCount); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, quat.indices, numLoopFrames, useByteIndices); + for (const auto value : encodedQuat.storedValues) + stream::WriteValue(stream, value); + break; + } + + case QuatType::FULL_QUAT: + { + const auto frameCount = quat.indices.size(); + assert(frameCount > 0uz); + assert(quat.values.size() == frameCount * 4uz); + assert(encodedQuat.storedValues.size() == frameCount * 3uz); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, quat.indices, numLoopFrames, useByteIndices); + for (const auto value : encodedQuat.storedValues) + stream::WriteValue(stream, value); + break; + } + } + } + + void WriteTransTrack(std::ostream& stream, const TransTrack& trans, const uint16_t numLoopFrames, const bool useByteIndices) + { + switch (trans.type) + { + case TransType::NO_TRANS: + { + stream::WriteValue(stream, static_cast(0)); + break; + } + + case TransType::TRANS_NO_SIZE: + { + stream::WriteValue(stream, static_cast(1)); + for (const auto value : trans.constant) + stream::WriteValue(stream, value); + break; + } + + case TransType::SMALL_TRANS: + { + const auto frameCount = trans.indices.size(); + assert(frameCount > 0uz); + assert(trans.byteFrames.size() == frameCount * 3uz); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, trans.indices, numLoopFrames, useByteIndices); + + constexpr auto smallTrans = static_cast(1); + stream::WriteValue(stream, smallTrans); + + for (const auto value : trans.mins) + stream::WriteValue(stream, value); + for (const auto value : trans.size) + stream::WriteValue(stream, EncodeRawTransSize(value, true)); + + stream::Write(stream, trans.byteFrames.data(), trans.byteFrames.size()); + break; + } + + case TransType::FULL_TRANS: + { + const auto frameCount = trans.indices.size(); + assert(frameCount > 0uz); + assert(trans.shortFrames.size() == frameCount * 3uz); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, trans.indices, numLoopFrames, useByteIndices); + + constexpr auto smallTrans = static_cast(0); + stream::WriteValue(stream, smallTrans); + + for (const auto value : trans.mins) + stream::WriteValue(stream, value); + for (const auto value : trans.size) + stream::WriteValue(stream, EncodeRawTransSize(value, false)); + + for (const auto value : trans.shortFrames) + stream::WriteValue(stream, value); + break; + } + } + } + + void WriteDeltaQuatTrack(std::ostream& stream, const DeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices) + { + const auto encodedDeltaQuat = EncodeDeltaQuatTrack(delta); + + if (!delta.quat) + { + stream::WriteValue(stream, static_cast(0)); + } + else if (!delta.quat->keyframed) + { + assert(encodedDeltaQuat.storedValues.size() == 1uz); + stream::WriteValue(stream, static_cast(1)); + stream::WriteValue(stream, encodedDeltaQuat.storedValues[0]); + } + else + { + const auto frameCount = delta.quat->indices.size(); + assert(frameCount > 0uz); + assert(delta.quat->values.size() == frameCount * 2uz); + assert(encodedDeltaQuat.storedValues.size() == frameCount); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, delta.quat->indices, numLoopFrames, useByteIndices); + for (const auto value : encodedDeltaQuat.storedValues) + stream::WriteValue(stream, value); + } + } + + void WriteDeltaTransTrack(std::ostream& stream, const DeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices) + { + if (!delta.trans) + { + stream::WriteValue(stream, static_cast(0)); + return; + } + + if (!delta.trans->keyframed) + { + stream::WriteValue(stream, static_cast(1)); + for (const auto value : delta.trans->constant) + stream::WriteValue(stream, value); + return; + } + + const auto frameCount = delta.trans->indices.size(); + assert(frameCount > 0uz); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, delta.trans->indices, numLoopFrames, useByteIndices); + + const auto smallTrans = static_cast(delta.trans->smallTrans ? 1 : 0); + stream::WriteValue(stream, smallTrans); + for (const auto value : delta.trans->mins) + stream::WriteValue(stream, value); + + if (delta.trans->smallTrans) + { + assert(delta.trans->byteFrames.size() == frameCount * 3uz); + for (const auto value : delta.trans->size) + stream::WriteValue(stream, EncodeRawTransSize(value, true)); + + stream::Write(stream, delta.trans->byteFrames.data(), delta.trans->byteFrames.size()); + } + else + { + assert(delta.trans->shortFrames.size() == frameCount * 3uz); + for (const auto value : delta.trans->size) + stream::WriteValue(stream, EncodeRawTransSize(value, false)); + + for (const auto value : delta.trans->shortFrames) + stream::WriteValue(stream, value); + } + } + + void WriteDeltaTrack(std::ostream& stream, const DeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices) + { + WriteDeltaQuatTrack(stream, delta, numLoopFrames, useByteIndices); + WriteDeltaTransTrack(stream, delta, numLoopFrames, useByteIndices); + } + + void WriteNoteTracks(std::ostream& stream, const XAssetInfo& asset) + { + const auto& parts = *asset.Asset(); + const auto notifyCount = static_cast(parts.notifyCount); + + size_t rawNotifyCount = notifyCount; + if (notifyCount > 0uz) + { + const auto& lastName = ResolveScriptString(asset, parts.notify[notifyCount - 1].name); + const auto lastTime = parts.notify[notifyCount - 1].time; + + // The linker appends a synthetic "end" notify at 1.0f to the loaded asset state. + if (lastName == "end" && std::abs(lastTime - 1.0f) < 0.0001f) + rawNotifyCount--; + } + + assert(rawNotifyCount < 255uz); + const auto rawNotifyCountByte = static_cast(rawNotifyCount); + stream::WriteValue(stream, rawNotifyCountByte); + + for (auto i = 0uz; i < rawNotifyCount; i++) + { + stream::WriteCString(stream, ResolveScriptString(asset, parts.notify[i].name)); + + uint16_t frame = 0; + if (parts.numframes > 0) + { + const auto scaled = static_cast(std::lround(parts.notify[i].time * static_cast(parts.numframes))); + assert(scaled >= 0 && scaled <= std::numeric_limits::max()); + frame = static_cast(scaled); + } + + stream::WriteValue(stream, frame); + } + } +} // namespace + +namespace xanim +{ + void DumperIW3::DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) + { + const auto* parts = asset.Asset(); + + auto maybeBoneTracks = ReconstructBoneTracks(asset); + if (!maybeBoneTracks.has_value()) + { + con::error(maybeBoneTracks.error()); + return; + } + const auto boneTracks = std::move(maybeBoneTracks).value(); + + const auto assetFile = context.OpenAssetFile(GetCompiledFileNameForAssetName(asset.m_name)); + if (!assetFile) + return; + + const auto numLoopFrames = GetNumLoopFrames(*parts); + const auto useByteIndices = UseByteIndices(*parts); + const auto deltaTrack = ReconstructDeltaTrack(*parts); + + std::vector encodedBoneQuats; + encodedBoneQuats.reserve(boneTracks.size()); + for (const auto& bone : boneTracks) + encodedBoneQuats.emplace_back(EncodeQuatTrack(bone.quat)); + + auto& stream = *assetFile; + + const auto flags = static_cast((parts->bLoop ? FLAG_LOOPED : 0u) | (parts->bDelta ? FLAG_DELTA : 0u)); + const auto boneCount = static_cast(parts->boneCount[PART_TYPE_ALL]); + const auto assetType = static_cast(parts->assetType); + const auto framerate = static_cast(std::lround(parts->framerate)); + + stream::WriteValue(stream, RAW_VERSION); + // Looped raws store numframes directly; non-looped raws store numframes + 1. + stream::WriteValue(stream, static_cast(parts->bLoop ? parts->numframes : numLoopFrames)); + stream::WriteValue(stream, boneCount); + stream::WriteValue(stream, flags); + stream::WriteValue(stream, assetType); + stream::WriteValue(stream, framerate); + + if (parts->bDelta) + WriteDeltaTrack(stream, deltaTrack, numLoopFrames, useByteIndices); + + if (!boneTracks.empty()) + { + const auto bitmaskSize = utils::Align(boneTracks.size(), 8u) / 8u; + std::vector flipQuat(bitmaskSize, 0); + std::vector halfQuat(bitmaskSize, 0); + + for (size_t i = 0u; i < boneTracks.size(); i++) + { + if (encodedBoneQuats[i].flipQuat) + flipQuat[i / 8u] |= static_cast(1u << (i % 8u)); + + if (QuatTypeUsesHalf(boneTracks[i].quat.type)) + halfQuat[i / 8u] |= static_cast(1u << (i % 8u)); + } + + stream::Write(stream, flipQuat.data(), flipQuat.size()); + stream::Write(stream, halfQuat.data(), halfQuat.size()); + + for (const auto& bone : boneTracks) + stream::WriteCString(stream, bone.name); + + for (auto i = 0uz; i < boneTracks.size(); i++) + { + WriteQuatTrack(stream, boneTracks[i].quat, encodedBoneQuats[i], numLoopFrames, useByteIndices); + WriteTransTrack(stream, boneTracks[i].trans, numLoopFrames, useByteIndices); + } + } + + WriteNoteTracks(stream, asset); + } +} // namespace xanim diff --git a/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.h b/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.h new file mode 100644 index 00000000..7b6b78d9 --- /dev/null +++ b/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Dumping/AbstractAssetDumper.h" +#include "Game/IW3/IW3.h" + +namespace xanim +{ + class DumperIW3 final : public AbstractAssetDumper + { + protected: + void DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) override; + }; +} // namespace xanim diff --git a/src/Utils/Utils/StreamUtils.cpp b/src/Utils/Utils/StreamUtils.cpp new file mode 100644 index 00000000..aec2c9d3 --- /dev/null +++ b/src/Utils/Utils/StreamUtils.cpp @@ -0,0 +1,36 @@ +#include "StreamUtils.h" + +#include +#include +#include + +namespace stream +{ + size_t Read(std::istream& stream, void* out, const size_t outSize) + { + stream.read(static_cast(out), static_cast(outSize)); + return static_cast(stream.gcount()); + } + + std::string ReadCString(std::istream& stream) + { + std::string result; + std::getline(stream, result, '\0'); + return result; + } + + void Write(std::ostream& stream, const void* in, const size_t inSize) + { + stream.write(static_cast(in), static_cast(inSize)); + } + + void WriteCString(std::ostream& stream, const std::string& value) + { + stream.write(value.c_str(), static_cast(value.size() + 1)); + } + + void WriteCString(std::ostream& stream, const char* value) + { + stream.write(value, static_cast(std::strlen(value) + 1)); + } +} // namespace stream diff --git a/src/Utils/Utils/StreamUtils.h b/src/Utils/Utils/StreamUtils.h new file mode 100644 index 00000000..6bebc775 --- /dev/null +++ b/src/Utils/Utils/StreamUtils.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace stream +{ + size_t Read(std::istream& stream, void* out, size_t outSize); + + template T ReadValue(std::istream& stream) + { + T value{}; + stream.read(reinterpret_cast(&value), static_cast(sizeof(value))); + + return value; + } + + template void ReadValue(std::istream& stream, T& value) + { + stream.read(reinterpret_cast(&value), static_cast(sizeof(value))); + } + + std::string ReadCString(std::istream& stream); + + void Write(std::ostream& stream, const void* in, size_t inSize); + + template void WriteValue(std::ostream& stream, const T& value) + { + stream.write(reinterpret_cast(&value), static_cast(sizeof(value))); + } + + void WriteCString(std::ostream& stream, const std::string& value); + void WriteCString(std::ostream& stream, const char* value); +} // namespace stream diff --git a/test/SystemTests.lua b/test/SystemTests.lua index 991ce0e3..d7ebe7a3 100644 --- a/test/SystemTests.lua +++ b/test/SystemTests.lua @@ -41,6 +41,7 @@ function SystemTests:project() Utils:include(includes) ZoneLoading:include(includes) ZoneWriting:include(includes) + ObjCommonTestUtils:include(includes) ObjLoading:include(includes) ObjCompiling:include(includes) ObjWriting:include(includes) @@ -53,6 +54,7 @@ function SystemTests:project() links:linkto(Utils) links:linkto(ZoneLoading) links:linkto(ZoneWriting) + links:linkto(ObjCommonTestUtils) links:linkto(ObjLoading) links:linkto(ObjCompiling) links:linkto(ObjWriting) diff --git a/test/SystemTests/Game/IW3/XAnim/test_anim b/test/SystemTests/Game/IW3/XAnim/test_anim new file mode 100644 index 0000000000000000000000000000000000000000..78d298e7c52099817890d65ca7f37007bba8e314 GIT binary patch literal 17108 zcmeHvby!tf*Y8}L-gJjZ=ca4ZrHF)q0@5JIItC^Na+EC!7+_F}MJNWPf*42}C@83a zV1P)2bnam9b>~{!eLU~`z2CjxbML>Ghi9z8m}C9sm^H?%g*aFN4gvx}2_Rr&AOfbs z@72`l9IFBFN98z$0Gzln79jk94`-+`_yd4#VUe5mMH>Hln*4g2{(74IdYb=wTKsyF ze?2XKJ*|E{t$#gD{s!bAns6XZIG83JP!kTS2?y4MgKNS8HsK(fa*$0q$fg`*Qx38z z2icT^Y|244?U*UCUcO|1IO7lR4;Q4mz2GPUfInLiAl*cSb^N?7^|wl9QL^ z-x5wvmYh;pa`Lg{a@MbDCdMHrSQv2%dDefLkn0Q0d;=)C$Ja*enAm&DQQ_bd4)xai6@-B319)?hoJai;kT;cgw40LG zlAqQildoGq?Fr9cTVT_BJm0S%3sR?o_q`kQtqQz)x8yG=c+eA*A5@UoU63zWDB5wR zz^-uQYuPW`)Ud(`iAhLGNz2H}DK1u4QB~K~(>E|OF|)L_cXW1j^YBD+{G@6=avgLu zHj=N2-IwL0(xBpaXT6J)&GfuIib87nb4sq}tH|v}(aUvGiPcI+pBnBdcHWS`ZrzPi zt+;x{qYR;4DDR(BNcelid$p>*zJZ#u$zo}9IerTc zhVh5x9}ShnS;_4m-V~Q6dwJM7?vSj(@Rqn%*`tH@@s9FQeX|K47E!vVPX#M|?joP5 zSHAk)G4Y4Wr_Ra53#z2fOG$caqa9z9Le<1NtCF15Vmj9(b*LJA&rV#bdg8lh;=Bsz zYCUsC#s5d{>2T$_UU1S~@nwI1e6>Q-z`=M~1=B(0_%rf%2fO36SqtHM$bvtDkc}(3& zsYk-8yC~|&oB4ed1lcg1>EJ3l)(O;R0zQ-lrqO`^?90e}iy)txnO zu=}ADjRN$u_7SJ*1ehcWfQS_AZ;O>w^vrA=mwO&(s8dSNNQMTb7+uHEg#2nKDMfu5 zYLrK4IFvs`Pc!r>`RGMR^UySg4y6c{VJxR4L%N2N2&oq(36*BJQc}?{hAyQLWn%&$ z2{V(Bl7amj_a-D`TPF|CxtL+rL&99lJ60Z~4_SqT*_cnPZo*VdIC_k0G{y{gau3Hu zfHRQVfz#aMF=i;0Ydofl^_FWaW{AZL<;rLj_h1YOErFD}P{!34b7UbG(!_-#uA!Lm zg&3~6n9rPLfr>D$#b9w?QZI%gm>Gn?$}@LjCE) z&$I2X>4}c>GOrzy{-PeJ+nF>+)2ln2w2txk)tRIawCL5jq*FlKI$>`c8dT?!G`ZmR zT0JRz{?HrOq&<@{_3=rnqvVENNmW0S8#I!%M@$=xlb()fzgc{iXSA!P=E91x_Nwp6 z564zk)}(NaudeV*H5%Vi{xel~T(I0IZFbDCTs7_a*o*So)S9uX3aQk^+-4;kY?A_Nb zu=DWUz7V3;;T`=e1Ph~X3=|5fL=lI~MM@8?9KI}i?~vHf9I{+Yw7--$@%hjql+W?4q3u&<%f$ptX;+oK1ZmV;%HraC zsjrpOB*UrvDi$*3)a@!kvSPFcDwDFiX?&{LvhQh8s#m0q=*nus67h7Pt}F7KzMyU+ zI7V;RAQ1H#k(yrx_At6N#{?o630iiJYqd9NVl7Kk+zyNO)R3NlG=#nG&#~^ z;v?E=-9yA%v|~Em0w-uZT1@@`nycD%eotDp%6e$?%U>1(Ad9uX$Ul0&2FcLO&dJ@= zln;U7KN&7n5WwbDc`+VvVY32lICKE0LorH5G0h^J7bc+aDGD_Kh=NWD0pH=A1OQu6 zOl}K(3;fsEh9xueP zH}^n_0ERf0$9D-#vQG-5N-`MzB#X(H!buDQES0hRLX|N&K@sC?r7>O|w~T_@6o6a6 zC5XujAdjM7_%VJdE^ooI%HEsjj5&$4HHdkT?Z}wD43q~9(TNrSbEQbLH zgF?#?`1+P-93BC8aY;-W3V+6)VulGk!BWj0W5#Pd!uYojvFwCOuy?oL!wgU;LZC#U z5K9dxz<6pt3Y()lA4@%6fXO|IF!}&$0#I6nrOrTo6xI7B$9QdshJefWG0ls`81MT8 zvqYf`xtV1d|LFV?}7j z!EJCVgVO*=5ukzdcF+I-enOKFupH<4?_tF>xJv*8V0#Rw*y((Hb5-L=5(T% zqn|$Oku_=#N>XlnzTCp|$9a&Pi$Wpn#RC5lX|P`G2>XPmlGt&k7`;n0hAB=zDVD$# zp;N?@nf&xq5}8bHI#KEYbAfhI`XzIY_Ll;I^@HZ5D9`##+n}hxYNrLt&oKLFk0pzk zELyej7N#OSRluBSMnA)^%e0~si8f3ldMm#H(~>U7ufepTAH?OGaUByuHztXmBeI7n zN6!{L!jz!1Ey3!IXm!Hsl4FjJY}F!IQUB%zRdfx^t?jY7|3jYVwBsU^+=BFY5eXiY z7AL}%tmciU#foU~S5uFR3<-Um`zazMYBRT5^bc{txsRfIB}j9UVtkU3b1q^ZBu3}_ z#T3OG=9h~piSW>tiggi}&<~0o;x}S6iJj%EU`UEz=CfId6xZQBv*0aN!*gyyLpp<} zX5oc&F;D%%eQCC&jz?%gNUDm*mEIxxglCjC`^yplRA6a@V1&aPjy>T}W5+iOxKLUK z{rDNw4Gk2JMSW0T@hsFBT_(O3__LBl{{;4|oua#e1?#w|3y5HC7ZU-6EFSR&l*Q^0 zKY|9JN|G9AIXW)&oTUu1WDQtXfQHf>vj!Ba#4{C;Ts6kRNkmYibKyD?s1eVULzL9l zuyz1bRacP8l3vsbsLVMzB2v!Ol3R`JU>eCLAo|QgnPfzb31q{O5N4^YDU!rYmhA$a z%)xYqHyp{`H(6$WcqA#Rd7IQT%NLz(+?^!6e`h%yN@uxM7 z?+E>>jfFr9ZQe$X=tmQ>-7a{Vnrve)Bt#9f84VSq_>&?ihW9UQL`o-lnJg}I`c1ji7rA@I_UvH~R9#DOkx~BWa6+>Wffo9-5|h zYt;fFyG`@%rSN3g3Xx>5m? zT*Ij}PN}$l4vsS|1;f9fq}*Z^Z9`i}SGcm4SBtDBxqYtk-V2@%9(HkDQD5n>x$&em zmAV!F_79lcx+|-$8BQQ*i;2e=7%$h*;)Z5Ed3I%b+7>FnSSuhRr2vDEu7Q~qbU_}T z&8*$1du%gn59$>Ak+m1y5c`1@iC%(qKYBU#J?kL)1kwZOJt#SVmd3tg9Yqt4H?WSO zb;s*jC(t>_A4g|@N#4TLnB&;HNjTaXh>YRGauB5zlvH$0Y#gzzm5=d(PC?~WjwWmL zpHecd92JjOI()r<#acihk(*m``O+xYC948_^Zb2ozpICfM<43^zP47kEgRYt<7+&a>R5VI$@Hx&RD9+QYiOlE{3wop66TJ8 z5@-(q@;I%tgX?!8yv2=d;XD%AeLl`XSi_}SXs!FxE_GE0yF4Su9ZQ<5d8yR`vnA34 zr&i^iZPiL-66{9AFV~y6wrH#0T^%DaoCp;ZvB{LAG<&uI-PylRHy*ME(f27B{R^k> zQ%YFVC^fB^HH$i@m#}6~2}oyAtMp?L{tph z-1xmVQBhS>*U;3`*3l)w4J|_>6LSl)m9>qn9h{*#IWJl2vdk5qr$vQ+V&r(Igi;vq zysJV#(G`58!t7|dK4xKw)HgnEVdB(wAD^)A^Phc!!i1>reS*W7)SEt=!v?6geWJqb zsLy<2vU~)*@{_U_1a{|VX6+@O&o9U-Bj)B;X9)^cAeBUiTfoh5G5tx|Pr|n)CFEq~DEk9vqCcAt0#H9J5U8587 z#XB}mCs=IW zacNqWtg++yw4kN-j_PT7OZOeOr<=)xJ8Y-7S~x^7r!vhc5$~sj%r8Y$O!b)Uj%c3> zGH2t^x3^BaSzX*#Ff(JTyftPv-|^8F%Q@zf6`T9!_+3jkUYWOd7ueuR-M!*)cstd8 zwa9vZ+Nm}7LkDP)-U^`y=ypCHplc^d1Y5KAChtuI;^%W@T}`?_1h#v+x1WbQeSKp@sB4TIgVK`bHJ%pmTbr zvi8v7^gXH`LrLjcYF0z0=`WPUhr-ipA;@xZ~=scR)%2JKT1@2-V3Op3( z2kBo>9t~3)A!TdANqqXsDxl9hDVd1XsOs z;%qqphW}_Jv40(aYMOW(k*%>sk<$|PM(t072j+%h(*`@t{lbWbE6oV&5knm_`SlY9 z>&%e#tp+t_bzubtyUaI*MH-y4s1LO?khL5Mnb&7peGERTzule?IHqUn(&f{jr|YWh zU90!R?YdWs-cL{cmF;?YtJ>Ww^q8wZF5jg`SnccfgS5llbEN>u!cotcrnAE=ESR9< zOnS6_mv)I3-ydY{Ct7d9U9|nQ<-+f22Wi)Zo!5D#y&_~#_pVM&z)=!W*WQ1Eq<%Ybxy5g>yY$nbgLsGo(ISIsN0H`sVUS9mQ&PPjWtms*V?rHTWujEvTt@ ztavMTyx^()^wqVOgJm8hKZ}1ZaV?%_pOEO_p~0|af+bs(S4Hw!2OM#raap;f>aRlL zNWO5cBP>zNM*(oT3Tol5$Q7PTqJ?t)y3(3g9QMIISFwQUvL<`E4gAw|oAHe(T4?UH z`PlSB3y%odL378^sKLHO58H%GJ-0UP%GuhPd}3{RNPEo9!H;J?d?-ozCe)}>OYFN= zFW96r;$A2Gg?}pWMQwM=+)z!~5R1lD{dvZjWY5AB*?Uc-Lt`{zBgZ);256--Y)e_N?N8U9kas9{v9?(8Yw-q!lSP5SLm z8(;V&e>nfXYngU~!ADJNtCyFW7YsG4AGUndK2#pp%C6!6KLq~YhkzLz0z7=|5%6C& zU|}TJ)W*X&jN+Cy@Strez6*n9fK=;$kyRky%+GevC}oNxFx|K=6-?%79yGY{5I-&-+V1g-j?cNO;JL8N(D7|p=fj0pXtv6=QizKVN%)guDrK|6!skcPeXq}+mpiZgpYtnh02fkNG6dl=hxugo6 z3+vcA3VMyYm;5&B9UOT+QD*QLMC` zX$GymezgXM8~aOM>^D>z32xkKcxL2OlcAx^$f2h92HS^A-nkmM4~)JE)wk|We%?!( z``Y~Uz7C=FWkHYTx3~J&uBb6;Kb-lhWcWBH>Y;+nU4bpGGR>Dvd@qSp;!9m_3x)07 zME=0%WN5Z-K%O^E4uS#533B_^%6yOST3&und6M} zb)oZlv=6WA2XD^zzG3UH{W3Fu_zkO3iz@I&rbdGr`#LqNbIzl#eaG5Ko0sLQlt)IN zM=r@23VwdgDsS$$2EB#ED)ytL_SgVXL}bu%`8yo=0_(ym-Pgaplw9Avsr|6@`fig?3NjNvuC)b7dv`T-S&N%~+dM%M z@NYYYx`B=sb-o4qhSp?$C7Ms$L4h;#$)CRqSkDQ6`zD|;TiSV4Kz%0W$47qm>7d>( zymgam!@IacCJM(T2-9PCX2g)CW1r`oK>Fxw>OFLBWH&tm-8k}^u^x^1`HFc0EgBX_ zt5DGFR_MD$De^rwmvWFJoz{_`lkiSb~_jDCapcz{K-L%I4pXs5vK-Z!*^NKp^F&fzw&ko;m( zCr{*l_?F6Z6(Lo=_vNabP8QiugspYxmq~Pp?m=MV_t=-po!ybMTyXIhy9f|L3ING6(KgngZ!AtLiNH4!e52Im7dMx!k*ILb5yK!&}yZC#CHR z?ZYaU4$qmGHK6D8ShRP8 z<720#Ev!bTrO21?CVSUiKic2kUCP(>@{P?>a(DQvnWfEr%brUu>m5XDmbl&+-dnYH zx%1D2N=>&%!%CI+-SmfeD(Bq5fM}(NdtEQHLfYM-r>8>IJ-VA(;p8sUO{$D=XZ`4{ zq`TkmR;?2C;OiNvoOM^~SyK7NJ)kG4e4YDf&(t$jch%nJXA5p>y%FUzZr^*@ILbA9 z-6OiwUiQ0R{6VQ->=E0Q+(dWp>a=Lx;_m;=^P98Vz4p4^gRU+uEu)KFPBfjLJL242 zzl1gG5dTV>yUSkuc{`t%!-nb>fmw%`N_HE;gALf|FlY-3!?pK+IWxufTa8UEtl>$I zi!0o4H4w9MzwxF>6!EaCza=!Va_Fs};P%yGjR%S6y=9nWiCyffe&jI&wQ#ylWDc3s|+GQCe=ts!Q*F%6hMie)orL z(~1l4vZW8KoPD=j(qq-r#?zu1tGB=J5L~_{?qjcjL68M2gz_Aq><2{ z_8w^`^rXH;J`tqT3XxZY>*)uPTZE$-rpO_}g=;^79U(hsFUUu>6}f{Ah{8i-V1!sb zQ2?W0M+FOw0I%z8K@ZDsXfZtV?D?Sql+gJeCOU*xePp6d=zNP5Sc`7>t^t(M*&Y$F z6+PU+fG+IV2QF|5oPMbYrU7q-BUp{(mHr8?BFXn&19`&1Tt38?a5~E!IZ4pD8i(8` zT)y-ac}t+Dbs(JttJFDUh(Jmq5XJ}v$=r};B(soF!iVGmq=!(O(gI6VoZ5tZCVWe6 zhV*&r3^G7)OPNE43F?qy@Ae{H1d#d|X&}g?yCU&~O;{h^!8u$c+M8qWjd96m1WMux2 zlR!aaEkP_(99c)WeO(p_B3ND5MYa-W;e`Ba!iW!{<+=!b zYx24@5mh6t?l+=O02?1f=i?yTfCl zJ2%}Ji6Eq2RYNus4qTH#0tw{nqOeV#UuTyBKhwox``_zT&gC%$c;f~?dpHo3IpySU z8^k}I;{QBwXxcu2FHm;YI8Y!UaxN@r@4DqQwNNqNlk~%3U%itUm(~k<+bwv9cdR|Q za3tK?d)0zLc(3;;BW-=7PcWk5~$YIuYZ0FXZ4v zfbX%8n-iCO=YowVz1O`7@}6qB7SODjRH7Y{yAaSzXWA+TO|4-7{zE+Rb+A z?3DjaTUyTahZ{ol_?g8wG#Q)ci?R?@mrBi41vS(ynf!=4O)xVXai`5)|AaWulroW$-297b1IXE@1B@jI~jSWbIx;e%boT)yUBZZs^@$rgYWdrotnIQ+jQQ3DmHf+ z_0-h+n-8gMJ2}PS`L(qmDuH!Uyg2Zz6&@CB3aXs)_s^h|({zvGl+&;!3`lTU*j{6Z&1f;X(Iowy&Yxo+P?OYq)xyC#x? zWBfi%6a}63Yn;dla`d}D(G?Ww_io}*kbu9~q;1g4z>QOV{@y_)Q(OGmb^-u*!0~A% zr##}=sDu4ZAMbaSQ>SiHse;qbVdLaZJLQ(sVrS6Yt@QL+-rHnG^W5q?0t-87?)mY| zGxTc(Qmi$M*us6RON_F@YpmT2-@>abAx1_)9P0o*Bj1PBN!7S}pUFFa`&P+<#camS zC`Q(__6-yI#wl`+9BpcHFgu^RZ8AAqhl);kX4lL&PIzZ0%&Sk*v+L&hCO>E2pEsM7 z%W0jzKlw4ol6q#!OZgO+k?LM)<+~xq^@vH!&z)j<( z0eb@XjJNq`21bpa^)CzjYdqM0I51-z@gECJ7$5d)3tTlm>z5YzdCbW_C@^*`-hXxA z*0J;cy8|ajh>n4$pxt1^qZShmL$zR=Q*o`A(MTDWyF%r z+Q)i??#$f4G6YL9kFuViM>FeKRV-SjHoBU5H!A}@#yFNO0k+Wo$~gmaXKvkGj}(u8 zyTwIVH`;sKo}m4+?amH@`B1^#vxGAP;d!|PoBqkX5`tHscYZnHeh*KXxi(}a7(hi(19>sVO zoNmUc2`maCW6VeaGfzoQS>o>|ELW= zOFfI9pGHAU0A9gcNC-HA^R0MZ3s46|LvYQfxD7)%oy8?Q(pZVU;yhbt51zUh$B)8u zJcj4a5XA7~;U+lp>l>49IwrX>hf&jMjFwMe)O;MH?E1uA3!e}ihkqY3D5lvC@ zI4psVQV7?z#cQ;78mqtMGuYdUak>(sBH#i{ML_T*CO?kr`@l9qKNfPBSh4(bax!EcgD|sJUVxc&C->)I$8hTe5TL@}omSs=Paotz_g( zeu@9aDj6mwiLl-beLI&!{W9mBd}^jZ>`=NV^(AQ}V-%UU?$00-m)P)K92F5Y5lU$l zKdM=L)>pzqKL2#1=ttoj2@1qrT$b@8NFJ>rehZa39&zgW*pB|!NoIY89pBR8z7Bl2 zdM)97ah<}|@jBs#2PuQEKeXRTdh$N~J5T!ghNSoD>8sjLzImT?w!OHUC(f$t$Bce# z+h`+WL!A56N2bTAJY)qkJf~-S=F~{z3(*y%!QDVQ~RP2;+32|5KQ=$OB{jo_T zg)5J8e!iBu>!_*(7k@!?lYG6P^Fc3Vl91uv+iH3OIT7I+lHC5=Hfjo^>{f&09QFh* zPUy!@NB?bwrmC)mU+snG3BS%nm$FZ2u3c%JzoCEVDsw*9;OF&VD#iG8b{2Jq+1DEl z)OL&Jn_sB!$l1Al)H9a$aN;6jm32#;=5AGb%bR9q_2;c;v?rFex$g8Yr25?U6HFt5Esqhj6#B`a$CAD*Bd#%w4a2l zlI3Yzky!EX8DAFG$*AOn%w((97L@)hGh{xj9Sk5xKmIf{K#qR2bEMTQs5E8tx`|5Z z%*bJrl!qZdZ83;SZz{!JD>52pH6EhO%v124>zid={M%v%ep zhC9rNcSXh;O^LaYlZhs>*_4?PBm2yn*?Ge&*J|fj2EJDh%xfC3>kt5k|Iu*NWFPPR za`*o{oZgawxkZC(m>aOTg;Ar2*r2y~iSge)Vf?*b%pu*z9gG3)PF!$D5`#OE5j+^| z#+}6CpP1(3KI~nUE{yK`in*4&c1#}7j-g%rg5}8U#N>oQ%uS!iG4+37^4ccst&rCk zKii7&pa&zJ?HJwh3ZvWVu(#}BD!enX#CXe#paY)4hT5b$H!oUB)!owT*x*a)bRJF5>pZG6V-*sKoX6DfGX5_M-tm`(bZf zI=f(BPjuqmnuYsmJv{0t!wLnU5i1S&g5MI(<--0jz)l)~52WyJ3O82mE4VO;J+5;E ze@mT#Nn~jlO~U;>6>rcxQ&<&kn8)OvxTY%}#ryDBXeNWn$ZJ3IG&n<$8`@OOrHhgC;&`+#^8XL$digms^>5Sc6s6H^PdeB z4u`}mB(5NpZm_!kFX9h8BmVb$8KN16(ofCX(o+?08?Q*IBwg0@xu9)*MzS(Fe93jD z!&bv*mNI{BI#GU4_3nnea%*M3&~uMVB_6CvD_r0iTM}@i35eM@U;DxH%(?Gcy4W8c zjoBL(jrgp~+p8%SHuPA<&^4pbg1g~(nwRqGR|e@PdBt9IUbY!cez7knyjAqI(fzTf zy^V9Pl(V0A2z5waIyj*=Xnp0t!nT2jS8p?o+m$YcjdYg=mP1We*t!_KRC4c|D z)1ECY&DxRMO`7!#BsbT6Y&RbGgVAuyC@F00#dbsf;4c+s2J8Ii9|`Fn_St_Q=pFEi z&Ql@+3rmxGbt);aYR;oNmGWt06w$cHCyH z8|jP4v{=J^nJ!j=VFWyvHPC9U@N$#8pyQMt;?^^yp?*aq2 z=F}R-?(Ma7p(1Bh0qG?nMP4xawHA;ZDG-^x3{TjR&*nlGC+4(0{)?9Q3) z*wPZsWVXeWo<`B}Y}L%YQ&MkS9(jGN`FZ*Eo*OJw=G~rMDB4=N)j4M5Xm-0?YpwtG z%e={ZjF!HZyr?D|mZT*K4rTOcz}qTCa)L|cWP_lKOQ;t&uy?QENs!s@9%WbeWE*;T z|Hpz&3(%Cb70%g+=~I-8)vs>{P?8I z1ZgXvBi)@C`REnX?%17Mp>sU@w`M#aUc2@5x&Ch(!>mt{n^gT*$9uon>&=WiR({n> zBVqj`_tm6R^AEIEUp-@RuXVNL*_VYZFSGMeh0bg5TmVJB-m1w;_g;FRy}; z{*v~!YZEP>^{*wJ{$82mZGUpOW|i;f_}=Gi1Uw$7X{e#zLsIo<2UhhqV0 zlT|AcyHAci&d4y`8+k{h@JcYxS&3sBly7vx^N} zb)64y9s%sOgZ?H4bG#RzTSCB#_ww*73SI#s+(Uz(K&h%}!h3CG_~qd;IPHcPQa~9P zaV-Npf0lJkH#ve&6*l1Zkd5K_hUGWnB8?N??6m#f-8l6}z~DK-m#Vg#R@E-Om3rz*XkVB!9`GrfS8a)To%yxBVYkt zKp*^pahQjd!8rsR4}_;O0Q|k@W(G5PK&+yS#8-YT(=7A)Omy@&PH^a*$+1CE_*p|@ zbMyX<8@DScz?vSGm%kDic<|Y?H7I&yW`+qB5Rd>WLPnXeIDGIvA@&g~JYm-{BEv&C zI0y7Zs^=N@V3Je@suz_d5(S)q(sdg^$*>WsY5*V4agz=U-76%1jL=D260`4F$Dd1w zD>k9qt_~(ozO9%pSM7dT!Cw_P_clA|MRCxRpKtW1P7D3n$-8tDd>sKUIVXXd9}GA| z5vgAv5~=Dscp{3TR~*4}_cE|vu}mx{#F3Jh3%?>LwF$g+Li=I46)Pw8we`psKNh@p zdHP!K549{miKUfCRk#18`FRJ|gCxG@Jx~(?(ShILbtkL>JDDePzcVq88lW{t4Zt|f zIsRK(FhKuj8CK;l-ouI(lESZztJ=H5KKQn+3J_w;z-ynfu8tW8urO@{GFogz zGYc8m@&nYMg1kZ&824>ro?g5J6ML{h>X^MF!Nmdcrvl0_;PIERVyv z&3~6Omhy)yMlrh;v13Pkq^!VZ+IV>!SFkx0mC@$NjIf|N|XQH1T>6}feD6#SIwe*q(>T?GID literal 0 HcmV?d00001 diff --git a/test/SystemTests/Game/IW3/XAnimIW3.cpp b/test/SystemTests/Game/IW3/XAnimIW3.cpp new file mode 100644 index 00000000..7c11c0b7 --- /dev/null +++ b/test/SystemTests/Game/IW3/XAnimIW3.cpp @@ -0,0 +1,62 @@ +#include "Game/IW3/XAnim/XAnimDumperIW3.h" +#include "Game/IW3/XAnim/XAnimLoaderIW3.h" +#include "OatTestPaths.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" +#include "ZoneLoading.h" + +#include +#include +#include +#include +#include + +using namespace std::literals; +namespace fs = std::filesystem; + +namespace +{ + TEST_CASE("XAnim Loading/Dumping (IW3)", "[iw3][system]") + { + MockSearchPath searchPath; + + const auto filePath = oat::paths::GetTestDirectory() / "SystemTests/Game/IW3/XAnim/test_anim"; + const auto fileSize = static_cast(fs::file_size(filePath)); + + std::ifstream file(filePath, std::ios::binary); + REQUIRE(file.is_open()); + + const auto data = std::make_unique(fileSize); + file.read(data.get(), fileSize); + + searchPath.AddFileData("xanim/test_anim", std::string(data.get(), fileSize)); + + Zone zone("MockZone", 0, GameId::IW3, GamePlatform::PC); + AssetCreatorCollection creatorCollection(zone); + IgnoredAssetLookup ignoredAssetLookup; + MemoryManager memoryManager; + const auto loader = xanim::CreateLoaderIW3(memoryManager, searchPath, zone); + AssetCreationContext context(zone, &creatorCollection, &ignoredAssetLookup); + + const auto result = loader->CreateAsset("test_anim", context); + + REQUIRE(result.HasBeenSuccessful()); + const auto* assetInfo = reinterpret_cast*>(result.GetAssetInfo()); + const auto* parts = assetInfo->Asset(); + + REQUIRE(parts->name == "test_anim"s); + REQUIRE(parts->numframes > 0); + + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + xanim::DumperIW3 dumper; + AssetDumpingContext dumpingContext(zone, "", mockOutput, mockObjPath, std::nullopt); + dumper.Dump(dumpingContext); + + const auto* outAnimFile = mockOutput.GetMockedFile("xanim/test_anim"); + REQUIRE(outAnimFile != nullptr); + + REQUIRE(outAnimFile->m_data.size() == fileSize); + REQUIRE(memcmp(outAnimFile->m_data.data(), data.get(), fileSize) == 0); + } +} // namespace