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 00000000..78d298e7 Binary files /dev/null and b/test/SystemTests/Game/IW3/XAnim/test_anim differ 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