From c448ddd06ac622aa25f1ff7863bc0b7b2a5da3b5 Mon Sep 17 00:00:00 2001 From: Jan Laupetin Date: Thu, 4 Jun 2026 13:00:02 +0200 Subject: [PATCH] refactor: use generic dumper for iw3 xanims --- src/ObjCommon/XAnim/XAnimCommon.cpp | 40 + src/ObjCommon/XAnim/XAnimCommon.h | 12 + .../Game/IW3/XAnim/XAnimDumperIW3.cpp | 998 +++--------------- src/ObjWriting/XAnim/CompiledXAnimWriter.cpp | 484 +++++++++ src/ObjWriting/XAnim/CompiledXAnimWriter.h | 10 + src/ObjWriting/XAnim/FlatXAnimReader.cpp | 405 +++++++ src/ObjWriting/XAnim/FlatXAnimReader.h | 94 ++ 7 files changed, 1195 insertions(+), 848 deletions(-) create mode 100644 src/ObjWriting/XAnim/CompiledXAnimWriter.cpp create mode 100644 src/ObjWriting/XAnim/CompiledXAnimWriter.h create mode 100644 src/ObjWriting/XAnim/FlatXAnimReader.cpp create mode 100644 src/ObjWriting/XAnim/FlatXAnimReader.h diff --git a/src/ObjCommon/XAnim/XAnimCommon.cpp b/src/ObjCommon/XAnim/XAnimCommon.cpp index d305d264..a95e0877 100644 --- a/src/ObjCommon/XAnim/XAnimCommon.cpp +++ b/src/ObjCommon/XAnim/XAnimCommon.cpp @@ -8,6 +8,46 @@ namespace xanim { + CommonXQuat::CommonXQuat() + : value{} + { + } + + CommonXQuat::CommonXQuat(const int16_t v0, const int16_t v1, const int16_t v2, const int16_t v3) + : value{v0, v1, v2, v3} + { + } + + CommonXQuat2::CommonXQuat2() + : value{} + { + } + + CommonXQuat2::CommonXQuat2(const int16_t v0, const int16_t v1) + : value{v0, v1} + { + } + + CommonVec3U8::CommonVec3U8() + : value{} + { + } + + CommonVec3U8::CommonVec3U8(const uint8_t x, const uint8_t y, const uint8_t z) + : value{x, y, z} + { + } + + CommonVec3U16::CommonVec3U16() + : value{} + { + } + + CommonVec3U16::CommonVec3U16(const uint16_t x, const uint16_t y, const uint16_t z) + : value{x, y, z} + { + } + QuatTrack::QuatTrack() : m_type(QuatType::NO_QUAT) { diff --git a/src/ObjCommon/XAnim/XAnimCommon.h b/src/ObjCommon/XAnim/XAnimCommon.h index 6f401a31..c01960fe 100644 --- a/src/ObjCommon/XAnim/XAnimCommon.h +++ b/src/ObjCommon/XAnim/XAnimCommon.h @@ -34,21 +34,33 @@ namespace xanim struct CommonXQuat { + CommonXQuat(); + CommonXQuat(int16_t v0, int16_t v1, int16_t v2, int16_t v3); + int16_t value[4]; }; struct CommonXQuat2 { + CommonXQuat2(); + CommonXQuat2(int16_t v0, int16_t v1); + int16_t value[2]; }; struct CommonVec3U8 { + CommonVec3U8(); + CommonVec3U8(uint8_t x, uint8_t y, uint8_t z); + uint8_t value[3]; }; struct CommonVec3U16 { + CommonVec3U16(); + CommonVec3U16(uint16_t x, uint16_t y, uint16_t z); + uint16_t value[3]; }; diff --git a/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp b/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp index 2274940e..6afba940 100644 --- a/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp +++ b/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp @@ -1,122 +1,25 @@ #include "XAnimDumperIW3.h" -#include "Utils/Alignment.h" -#include "Utils/StreamUtils.h" +#include "XAnim/CompiledXAnimWriter.h" +#include "XAnim/FlatXAnimReader.h" #include "XAnim/XAnimCommon.h" #include #include -#include #include -#include #include #include #include #include -#include -#include #include #include +using namespace xanim; 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) + [[nodiscard]] const std::string& ResolveScriptString(const ScriptString value, const XAssetInfo& asset) { assert(asset.m_zone != nullptr && value < asset.m_zone->m_script_strings.Count()); return asset.m_zone->m_script_strings[value]; @@ -134,729 +37,139 @@ namespace return parts.numframes < 256; } - [[nodiscard]] float IntBitsToFloat(const int value) + std::vector ConvertNotifies(const XAnimParts& parts, const XAssetInfo& assetInfo) { - 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; + std::vector result; + if (!parts.notify || parts.notifyCount == 0) 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 = 0u; i < parts.notifyCount; i++) { - for (auto i = 0uz; i < count; i++) - result[i] = cursor.indices[i]; + const auto& notify = parts.notify[i]; + CommonXAnimNotifyInfo commonNotify; - cursor.indices += count; + commonNotify.m_name = ResolveScriptString(notify.name, assetInfo); + commonNotify.m_time = notify.time; - // 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; + result.emplace_back(std::move(commonNotify)); } - 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) + CommonDeltaQuatTrack ConvertDeltaQuatTrack(const XAnimDeltaPartQuat& deltaQuat, const bool useByteIndices, const uint16_t numLoopFrames) { - 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++) + CommonDeltaQuatTrack result; + if (deltaQuat.size > 0) { - const auto* frame = &values[frameIndex * componentCount]; - const auto omittedNegative = frame[storedComponentCount] < 0; + const auto frameCount = static_cast(deltaQuat.size) + 1uz; + result.m_frames2.reserve(frameCount); + result.m_indices.reserve(frameCount); - auto continuityNegated = false; - if (frameIndex > 0uz && omittedNegative != targetNegativeOmitted) + for (auto i = 0uz; i < frameCount; i++) + result.m_frames2.emplace_back(deltaQuat.u.frames.frames[i].value[0], deltaQuat.u.frames.frames[i].value[1]); + + if (useByteIndices) { - const auto* prevFrame = &values[(frameIndex - 1uz) * componentCount]; - continuityNegated = ComputeQuatDot(prevFrame, frame, componentCount) > 0; + for (auto i = 0uz; i < frameCount; i++) + result.m_indices.emplace_back(deltaQuat.u.frames.indices._1[i]); + } + else + { + for (auto i = 0uz; i < frameCount; i++) + result.m_indices.emplace_back(deltaQuat.u.frames.indices._2[i]); } - 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")); + assert(result.m_indices.size() <= numLoopFrames); } else { - assert(parts.indexCount == 0); + result.m_frames2.emplace_back(deltaQuat.u.frame0.value[0], deltaQuat.u.frame0.value[1]); } - return bones; + return result; } - [[nodiscard]] DeltaTrack ReconstructDeltaTrack(const XAnimParts& parts) + CommonDeltaTransTrack ConvertDeltaTransTrack(const XAnimPartTrans& deltaTrans, const bool useByteIndices) { - DeltaTrack result; + CommonDeltaTransTrack result; + if (deltaTrans.size > 0) + { + result.m_small_trans = deltaTrans.smallTrans; + result.m_mins = { + deltaTrans.u.frames.mins.x, + deltaTrans.u.frames.mins.y, + deltaTrans.u.frames.mins.z, + }; + result.m_size = { + deltaTrans.u.frames.size.x, + deltaTrans.u.frames.size.y, + deltaTrans.u.frames.size.z, + }; + + const auto frameCount = static_cast(deltaTrans.size) + 1uz; + result.m_indices.reserve(frameCount); + if (useByteIndices) + { + for (auto i = 0uz; i < frameCount; i++) + result.m_indices.emplace_back(static_cast(deltaTrans.u.frames.indices._1[i])); + } + else + { + for (auto i = 0uz; i < frameCount; i++) + result.m_indices.emplace_back(deltaTrans.u.frames.indices._2[i]); + } + + if (deltaTrans.smallTrans) + { + result.m_frames_u8.reserve(frameCount); + for (auto i = 0uz; i < frameCount; i++) + { + result.m_frames_u8.emplace_back( + deltaTrans.u.frames.frames._1[i][0], deltaTrans.u.frames.frames._1[i][1], deltaTrans.u.frames.frames._1[i][2]); + } + } + else + { + result.m_frames_u16.reserve(frameCount); + for (auto i = 0uz; i < frameCount; i++) + { + result.m_frames_u16.emplace_back( + deltaTrans.u.frames.frames._2[i][0], deltaTrans.u.frames.frames._2[i][1], deltaTrans.u.frames.frames._2[i][2]); + } + } + } + else + { + result.m_constant = std::array({ + deltaTrans.u.frame0.v[0], + deltaTrans.u.frame0.v[1], + deltaTrans.u.frame0.v[2], + }); + } + + return result; + } + + std::unique_ptr ConvertDeltaTrack(const XAnimParts& parts, const bool useByteIndices, const uint16_t numLoopFrames) + { + if (!parts.deltaPart) + return nullptr; + + auto result = std::make_unique(); 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 (parts.deltaPart->quat) + result->m_quat = ConvertDeltaQuatTrack(*parts.deltaPart->quat, useByteIndices, numLoopFrames); - 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]}; - } - } + if (parts.deltaPart->trans) + result->m_trans = ConvertDeltaTransTrack(*parts.deltaPart->trans, useByteIndices); 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 @@ -865,73 +178,62 @@ namespace xanim { const auto* parts = asset.Asset(); - auto maybeBoneTracks = ReconstructBoneTracks(asset); + const auto useByteIndices = UseByteIndices(*parts); + const auto numLoopFrames = GetNumLoopFrames(*parts); + + std::vector boneNames; + if (parts->names) + { + boneNames.reserve(parts->boneCount[PART_TYPE_ALL]); + for (auto i = 0u; i < parts->boneCount[PART_TYPE_ALL]; i++) + boneNames.emplace_back(asset.m_zone->m_script_strings.Value(parts->names[i])); + } + + const XAnimBoneCounts boneCounts(parts->boneCount[PART_TYPE_NO_QUAT], + parts->boneCount[PART_TYPE_HALF_QUAT], + parts->boneCount[PART_TYPE_FULL_QUAT], + parts->boneCount[PART_TYPE_HALF_QUAT_NO_SIZE], + parts->boneCount[PART_TYPE_FULL_QUAT_NO_SIZE], + parts->boneCount[PART_TYPE_SMALL_TRANS], + parts->boneCount[PART_TYPE_TRANS], + parts->boneCount[PART_TYPE_TRANS_NO_SIZE], + parts->boneCount[PART_TYPE_NO_TRANS]); + + // 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); + FlatXAnimReadCursor flatData(parts->dataByte, + parts->dataByteCount, + parts->dataShort, + parts->dataShortCount, + parts->dataInt, + parts->dataIntCount, + parts->randomDataByte, + parts->randomDataByteCount, + parts->randomDataShort, + parts->randomDataShortCount, + parts->indices._2, + parts->indexCount); + + auto maybeBoneTracks = CreateBoneTracksFromFlatData(std::move(boneNames), boneCounts, flatData, useByteIndices); if (!maybeBoneTracks.has_value()) { - con::error(maybeBoneTracks.error()); + con::error("Failed to reconstruct bone tracks from XAnim {}: {}", parts->name, 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); + CommonXAnimParts commonParts; + commonParts.m_num_frames = parts->numframes; + commonParts.m_looped = parts->bLoop; + commonParts.m_frame_rate = parts->framerate; + commonParts.m_asset_type = parts->assetType; + commonParts.m_bone_tracks = std::move(maybeBoneTracks).value(); + commonParts.m_notifies = ConvertNotifies(*parts, asset); + commonParts.m_delta_track = ConvertDeltaTrack(*parts, useByteIndices, numLoopFrames); - 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); + WriteCompiledXAnim(*assetFile, commonParts); } } // namespace xanim diff --git a/src/ObjWriting/XAnim/CompiledXAnimWriter.cpp b/src/ObjWriting/XAnim/CompiledXAnimWriter.cpp new file mode 100644 index 00000000..151f6f25 --- /dev/null +++ b/src/ObjWriting/XAnim/CompiledXAnimWriter.cpp @@ -0,0 +1,484 @@ +#include "CompiledXAnimWriter.h" + +#include "Utils/Alignment.h" +#include "Utils/Logging/Log.h" +#include "Utils/StreamUtils.h" + +#include +#include + +using namespace xanim; + +namespace +{ + 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; + + class EncodedQuatTrack + { + public: + bool m_flip_quat = false; + std::vector m_stored_values; + }; + + [[nodiscard]] uint16_t GetNumLoopFrames(const CommonXAnimParts& parts) + { + assert(parts.m_num_frames < std::numeric_limits::max()); + + // Raw non-looped xanims store numframes + 1 in keyed track counts/header fields. + return static_cast(parts.m_num_frames + 1u); + } + + [[nodiscard]] bool QuatTypeUsesHalf(const QuatType type) + { + return type == QuatType::NO_QUAT || type == QuatType::HALF_QUAT || type == QuatType::HALF_QUAT_NO_SIZE; + } + + [[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; + } + + template + concept XQuatOrXQuat2 = std::is_array_v && std::is_integral_v>; + + template [[nodiscard]] int64_t ComputeQuatDot(const T& lhs, const T& rhs) + { + int64_t result = 0; + for (auto i = 0uz; i < std::extent_v; i++) + result += static_cast(lhs.value[i]) * static_cast(rhs.value[i]); + + return result; + } + + template [[nodiscard]] EncodedQuatTrack EncodeQuatFrames(const std::vector& frames, const bool allowFlipQuat) + { + constexpr auto COMPONENT_COUNT = std::extent_v; + constexpr auto STORED_COMPONENT_COUNT = COMPONENT_COUNT - 1; + + EncodedQuatTrack result; + if (frames.empty()) + return result; + + const auto frameCount = frames.size(); + + // 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.m_stored_values.reserve(frameCount * STORED_COMPONENT_COUNT); + + result.m_flip_quat = allowFlipQuat && frames[0].value[COMPONENT_COUNT - 1] < 0; + const auto targetNegativeOmitted = result.m_flip_quat; + + for (size_t frameIndex = 0; frameIndex < frameCount; frameIndex++) + { + const auto& frame = frames[frameIndex]; + const auto omittedNegative = frame.value[COMPONENT_COUNT - 1] < 0; + + auto continuityNegated = false; + if (frameIndex > 0u && omittedNegative != targetNegativeOmitted) + { + const auto& prevFrame = frames[(frameIndex - 1u)]; + continuityNegated = ComputeQuatDot(prevFrame, frame) > 0; + } + + const auto rawNegated = result.m_flip_quat != continuityNegated; + const auto sign = rawNegated ? -1 : 1; + + for (size_t componentIndex = 0; componentIndex < STORED_COMPONENT_COUNT; componentIndex++) + { + const auto value = static_cast(frame.value[componentIndex]) * sign; + assert(value >= std::numeric_limits::min() && value <= std::numeric_limits::max()); + result.m_stored_values.emplace_back(static_cast(value)); + } + } + + return result; + } + + [[nodiscard]] EncodedQuatTrack EncodeQuatTrack(const QuatTrack& quat) + { + switch (quat.m_type) + { + case QuatType::NO_QUAT: + return {}; + + case QuatType::HALF_QUAT_NO_SIZE: + assert(quat.m_frames2.size() == 1); + return EncodeQuatFrames(quat.m_frames2, true); + + case QuatType::FULL_QUAT_NO_SIZE: + assert(quat.m_frames.size() == 1); + return EncodeQuatFrames(quat.m_frames, true); + + case QuatType::HALF_QUAT: + assert(quat.m_frames2.size() == quat.m_indices.size()); + return EncodeQuatFrames(quat.m_frames2, true); + + case QuatType::FULL_QUAT: + assert(quat.m_frames.size() == quat.m_indices.size()); + return EncodeQuatFrames(quat.m_frames, true); + } + + assert(false); + return {}; + } + + 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 WriteDeltaQuatTrack(std::ostream& stream, const CommonXAnimDeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices) + { + if (!delta.m_quat) + { + stream::WriteValue(stream, static_cast(0)); + return; + } + + const auto numQuatIndices = static_cast(delta.m_quat->m_frames2.size()); + assert(numQuatIndices > 0); + + stream::WriteValue(stream, numQuatIndices); + + const auto encodedDeltaQuatFrames = EncodeQuatFrames(delta.m_quat->m_frames2, false); + + if (numQuatIndices == 1) + { + assert(encodedDeltaQuatFrames.m_stored_values.size() == 1); + stream::WriteValue(stream, encodedDeltaQuatFrames.m_stored_values[0]); + } + else + { + assert(numQuatIndices > 1u); + assert(delta.m_quat->m_indices.size() == numQuatIndices); + assert(encodedDeltaQuatFrames.m_stored_values.size() == numQuatIndices); + + WriteIndicesIfNeeded(stream, delta.m_quat->m_indices, numLoopFrames, useByteIndices); + for (const auto value : encodedDeltaQuatFrames.m_stored_values) + stream::WriteValue(stream, value); + } + } + + [[nodiscard]] float EncodeRawTransSize(const float value, const bool smallTrans) + { + const auto scale = smallTrans ? HALF_TRANS_SIZE_SCALE : FULL_TRANS_SIZE_SCALE; + return value / scale; + } + + void WriteDeltaTransTrack(std::ostream& stream, const CommonXAnimDeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices) + { + if (!delta.m_trans) + { + stream::WriteValue(stream, static_cast(0)); + return; + } + + if (delta.m_trans->m_constant) + { + stream::WriteValue(stream, static_cast(1)); + for (const auto value : *delta.m_trans->m_constant) + stream::WriteValue(stream, value); + return; + } + + const auto numTransIndices = static_cast(delta.m_trans->m_indices.size()); + assert(numTransIndices > 1); + + stream::WriteValue(stream, numTransIndices); + WriteIndicesIfNeeded(stream, delta.m_trans->m_indices, numLoopFrames, useByteIndices); + + const auto smallTrans = !delta.m_trans->m_frames_u8.empty(); + stream::WriteValue(stream, static_cast(smallTrans ? 1 : 0)); + for (const auto value : delta.m_trans->m_mins) + stream::WriteValue(stream, value); + + if (smallTrans) + { + assert(delta.m_trans->m_frames_u8.size() == numTransIndices); + for (const auto value : delta.m_trans->m_size) + stream::WriteValue(stream, EncodeRawTransSize(value, true)); + + for (const auto vec3U8 : delta.m_trans->m_frames_u8) + { + stream::WriteValue(stream, vec3U8.value[0]); + stream::WriteValue(stream, vec3U8.value[1]); + stream::WriteValue(stream, vec3U8.value[2]); + } + } + else + { + assert(delta.m_trans->m_frames_u16.size() == numTransIndices); + for (const auto value : delta.m_trans->m_size) + stream::WriteValue(stream, EncodeRawTransSize(value, false)); + + for (const auto vec3U16 : delta.m_trans->m_frames_u16) + { + stream::WriteValue(stream, vec3U16.value[0]); + stream::WriteValue(stream, vec3U16.value[1]); + stream::WriteValue(stream, vec3U16.value[2]); + } + } + } + + void WriteDeltaTrack(std::ostream& stream, const CommonXAnimDeltaTrack& delta, const uint16_t numLoopFrames, const bool useByteIndices) + { + WriteDeltaQuatTrack(stream, delta, numLoopFrames, useByteIndices); + WriteDeltaTransTrack(stream, delta, numLoopFrames, useByteIndices); + } + + void WriteQuatTrack( + std::ostream& stream, const QuatTrack& quat, const EncodedQuatTrack& encodedQuat, const uint16_t numLoopFrames, const bool useByteIndices) + { + switch (quat.m_type) + { + case QuatType::NO_QUAT: + { + stream::WriteValue(stream, static_cast(0)); + break; + } + + case QuatType::HALF_QUAT_NO_SIZE: + { + assert(encodedQuat.m_stored_values.size() == 1uz); + stream::WriteValue(stream, static_cast(1)); + stream::WriteValue(stream, encodedQuat.m_stored_values[0]); + break; + } + + case QuatType::FULL_QUAT_NO_SIZE: + { + assert(encodedQuat.m_stored_values.size() == 3uz); + stream::WriteValue(stream, static_cast(1)); + for (const auto value : encodedQuat.m_stored_values) + stream::WriteValue(stream, value); + break; + } + + case QuatType::HALF_QUAT: + { + const auto frameCount = quat.m_indices.size(); + assert(frameCount > 0uz); + assert(quat.m_frames2.size() == frameCount); + assert(encodedQuat.m_stored_values.size() == frameCount); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, quat.m_indices, numLoopFrames, useByteIndices); + for (const auto value : encodedQuat.m_stored_values) + stream::WriteValue(stream, value); + break; + } + + case QuatType::FULL_QUAT: + { + const auto frameCount = quat.m_indices.size(); + assert(frameCount > 0uz); + assert(quat.m_frames.size() == frameCount); + assert(encodedQuat.m_stored_values.size() == frameCount * 3uz); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, quat.m_indices, numLoopFrames, useByteIndices); + for (const auto value : encodedQuat.m_stored_values) + stream::WriteValue(stream, value); + break; + } + } + } + + void WriteTransTrack(std::ostream& stream, const TransTrack& trans, const uint16_t numLoopFrames, const bool useByteIndices) + { + switch (trans.m_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.m_constant) + stream::WriteValue(stream, value); + break; + } + + case TransType::SMALL_TRANS: + { + const auto frameCount = trans.m_indices.size(); + assert(frameCount > 0uz); + assert(trans.m_byte_frames.size() == frameCount * 3uz); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, trans.m_indices, numLoopFrames, useByteIndices); + + constexpr auto smallTrans = static_cast(1); + stream::WriteValue(stream, smallTrans); + + for (const auto value : trans.m_mins) + stream::WriteValue(stream, value); + for (const auto value : trans.m_size) + stream::WriteValue(stream, EncodeRawTransSize(value, true)); + + stream::Write(stream, trans.m_byte_frames.data(), trans.m_byte_frames.size()); + break; + } + + case TransType::FULL_TRANS: + { + const auto frameCount = trans.m_indices.size(); + assert(frameCount > 0uz); + assert(trans.m_short_frames.size() == frameCount * 3uz); + + stream::WriteValue(stream, static_cast(frameCount)); + WriteIndicesIfNeeded(stream, trans.m_indices, numLoopFrames, useByteIndices); + + constexpr auto smallTrans = static_cast(0); + stream::WriteValue(stream, smallTrans); + + for (const auto value : trans.m_mins) + stream::WriteValue(stream, value); + for (const auto value : trans.m_size) + stream::WriteValue(stream, EncodeRawTransSize(value, false)); + + for (const auto value : trans.m_short_frames) + stream::WriteValue(stream, value); + break; + } + } + } + + void WriteNoteTracks(std::ostream& stream, const CommonXAnimParts& parts) + { + const auto notifyCount = parts.m_notifies.size(); + + auto rawNotifyCount = notifyCount; + if (notifyCount > 0uz) + { + const auto& lastNotify = parts.m_notifies[notifyCount - 1]; + + // The linker appends a synthetic "end" notify at 1.0f to the loaded asset state. + if (lastNotify.m_name == "end" && std::abs(lastNotify.m_time - 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++) + { + const auto& notify = parts.m_notifies[i]; + stream::WriteCString(stream, notify.m_name); + + uint16_t frame = 0; + if (parts.m_num_frames > 0) + { + const auto scaled = static_cast(std::lround(notify.m_time * static_cast(parts.m_num_frames))); + assert(scaled >= 0 && scaled <= std::numeric_limits::max()); + frame = static_cast(scaled); + } + + stream::WriteValue(stream, frame); + } + } +} // namespace + +namespace xanim +{ + void WriteCompiledXAnim(std::ostream& stream, const CommonXAnimParts& parts) + { + const auto numLoopFrames = GetNumLoopFrames(parts); + const auto useByteIndices = parts.m_num_frames < 256; + + std::vector encodedBoneQuats; + encodedBoneQuats.reserve(parts.m_bone_tracks.size()); + for (const auto& bone : parts.m_bone_tracks) + encodedBoneQuats.emplace_back(EncodeQuatTrack(bone.m_quat)); + + const auto flags = static_cast((parts.m_looped ? FLAG_LOOPED : 0u) | (parts.m_delta_track ? FLAG_DELTA : 0u)); + const auto boneCount = static_cast(parts.m_bone_tracks.size()); + const auto assetType = static_cast(parts.m_asset_type); + const auto framerate = static_cast(std::lround(parts.m_frame_rate)); + + stream::WriteValue(stream, static_cast(CompiledXAnimVersion::VERSION_17)); + // Looped raws store numframes directly; non-looped raws store numframes + 1. + stream::WriteValue(stream, static_cast(parts.m_looped ? parts.m_num_frames : numLoopFrames)); + stream::WriteValue(stream, boneCount); + stream::WriteValue(stream, flags); + stream::WriteValue(stream, assetType); + stream::WriteValue(stream, framerate); + + if (parts.m_delta_track) + WriteDeltaTrack(stream, *parts.m_delta_track, numLoopFrames, useByteIndices); + + if (!parts.m_bone_tracks.empty()) + { + const auto bitmaskSize = utils::Align(boneCount, 8u) / 8u; + std::vector flipQuat(bitmaskSize, 0); + std::vector halfQuat(bitmaskSize, 0); + + for (auto i = 0u; i < boneCount; i++) + { + if (encodedBoneQuats[i].m_flip_quat) + flipQuat[i / 8u] |= static_cast(1u << (i % 8u)); + + if (QuatTypeUsesHalf(parts.m_bone_tracks[i].m_quat.m_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 : parts.m_bone_tracks) + stream::WriteCString(stream, bone.m_name); + + for (auto i = 0u; i < boneCount; i++) + { + WriteQuatTrack(stream, parts.m_bone_tracks[i].m_quat, encodedBoneQuats[i], numLoopFrames, useByteIndices); + WriteTransTrack(stream, parts.m_bone_tracks[i].m_trans, numLoopFrames, useByteIndices); + } + } + + WriteNoteTracks(stream, parts); + } +} // namespace xanim diff --git a/src/ObjWriting/XAnim/CompiledXAnimWriter.h b/src/ObjWriting/XAnim/CompiledXAnimWriter.h new file mode 100644 index 00000000..61c8a268 --- /dev/null +++ b/src/ObjWriting/XAnim/CompiledXAnimWriter.h @@ -0,0 +1,10 @@ +#pragma once + +#include "XAnim/XAnimCommon.h" + +#include + +namespace xanim +{ + void WriteCompiledXAnim(std::ostream& stream, const CommonXAnimParts& parts); +} diff --git a/src/ObjWriting/XAnim/FlatXAnimReader.cpp b/src/ObjWriting/XAnim/FlatXAnimReader.cpp new file mode 100644 index 00000000..1c60e418 --- /dev/null +++ b/src/ObjWriting/XAnim/FlatXAnimReader.cpp @@ -0,0 +1,405 @@ +#include "FlatXAnimReader.h" + +#include +#include +#include +#include + +using namespace xanim; + +namespace +{ + + [[nodiscard]] std::vector ReadPackedIndices(FlatXAnimReadCursor& 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.PopDataByte(); + + 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) + { + cursor.ReadIndices(result.data(), 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.SkipDataShort(((count - 2uz) / 256u) + 2uz); + return result; + } + + cursor.ReadDataShort(result.data(), count); + return result; + } + + [[nodiscard]] float IntBitsToFloat(const int value) + { + union + { + int i; + float f; + }; + + i = value; + return f; + } + + [[nodiscard]] std::array ReadFloat3(FlatXAnimReadCursor& cursor) + { + std::array result{}; + for (float& i : result) + i = IntBitsToFloat(cursor.PopDataInt()); + return result; + } +} // namespace + +namespace xanim +{ + XAnimBoneCounts::XAnimBoneCounts(const size_t noQuatCount, + const size_t halfQuatCount, + const size_t fullQuatCount, + const size_t halfQuatNoSizeCount, + const size_t fullQuatNoSizeCount, + const size_t smallTransCount, + const size_t fullTransCount, + const size_t transNoSizeCount, + const size_t noTransCount) + : m_counts({ + noQuatCount, + halfQuatCount, + fullQuatCount, + halfQuatNoSizeCount, + fullQuatNoSizeCount, + smallTransCount, + fullTransCount, + transNoSizeCount, + noTransCount, + }) + { + assert(m_counts[std::to_underlying(QuatType::NO_QUAT)] == noQuatCount); + assert(m_counts[std::to_underlying(QuatType::HALF_QUAT)] == halfQuatCount); + assert(m_counts[std::to_underlying(QuatType::FULL_QUAT)] == fullQuatCount); + assert(m_counts[std::to_underlying(QuatType::HALF_QUAT_NO_SIZE)] == halfQuatNoSizeCount); + assert(m_counts[std::to_underlying(QuatType::FULL_QUAT_NO_SIZE)] == fullQuatNoSizeCount); + + assert(m_counts[std::to_underlying(TransType::SMALL_TRANS)] == smallTransCount); + assert(m_counts[std::to_underlying(TransType::FULL_TRANS)] == fullTransCount); + assert(m_counts[std::to_underlying(TransType::TRANS_NO_SIZE)] == transNoSizeCount); + assert(m_counts[std::to_underlying(TransType::NO_TRANS)] == noTransCount); + + assert(noQuatCount + halfQuatCount + fullQuatCount + halfQuatNoSizeCount + fullQuatNoSizeCount + == smallTransCount + fullTransCount + transNoSizeCount + noTransCount); + } + + size_t XAnimBoneCounts::GetCountForQuatType(const QuatType quatType) const + { + return m_counts[std::to_underlying(quatType)]; + } + + size_t XAnimBoneCounts::GetCountForTransType(const TransType transType) const + { + return m_counts[std::to_underlying(transType)]; + } + + FlatDataReadException::FlatDataReadException(std::string message) + : m_message(std::move(message)) + { + } + + const char* FlatDataReadException::what() const noexcept + { + return m_message.c_str(); + } + + const std::string& FlatDataReadException::message() const + { + return m_message; + } + + FlatXAnimReadCursor::FlatXAnimReadCursor(uint8_t* dataByte, + const size_t dataByteCount, + int16_t* dataShort, + const size_t dataShortCount, + int32_t* dataInt, + const size_t dataIntCount, + uint8_t* randomDataByte, + const size_t randomDataByteCount, + int16_t* randomDataShort, + const size_t randomDataShortCount, + uint16_t* indices, + const size_t indicesCount) + : m_data_byte(dataByte), + m_data_byte_count(dataByteCount), + m_data_short(dataShort), + m_data_short_count(dataShortCount), + m_data_int(dataInt), + m_data_int_count(dataIntCount), + m_random_data_byte(randomDataByte), + m_random_data_byte_count(randomDataByteCount), + m_random_data_short(randomDataShort), + m_random_data_short_count(randomDataShortCount), + m_indices(indices), + m_indices_count(indicesCount) + { + } + +#define DATA_EXHAUSTED_ERROR(name, readCount, remainingCount) \ + FlatDataReadException(std::format("Exhausted {} while trying to read {} entries ({} remaining)", name, readCount, remainingCount)) + + uint8_t FlatXAnimReadCursor::PopDataByte() + { + if (m_data_byte_count < 1) + throw DATA_EXHAUSTED_ERROR("dataByte", 1, m_data_byte_count); + + const auto result = m_data_byte[0]; + m_data_byte++; + m_data_byte_count--; + + return result; + } + + int16_t FlatXAnimReadCursor::PopDataShort() + { + if (m_data_short_count < 1) + throw DATA_EXHAUSTED_ERROR("dataShort", 1, m_data_short_count); + + const auto result = m_data_short[0]; + m_data_short++; + m_data_short_count--; + + return result; + } + + void FlatXAnimReadCursor::ReadDataShort(void* dst, const size_t count) + { + if (m_data_short_count < count) + throw DATA_EXHAUSTED_ERROR("dataShort", count, m_data_short_count); + + std::memcpy(dst, m_data_short, count * sizeof(int16_t)); + + m_data_short += count; + m_data_short_count -= count; + } + + void FlatXAnimReadCursor::SkipDataShort(const size_t count) + { + if (m_data_short_count < count) + throw DATA_EXHAUSTED_ERROR("dataShort", count, m_data_short_count); + + m_data_short += count; + m_data_short_count -= count; + } + + int32_t FlatXAnimReadCursor::PopDataInt() + { + if (m_data_int_count < 1) + throw DATA_EXHAUSTED_ERROR("dataInt", 1, m_data_int_count); + + const auto result = m_data_int[0]; + m_data_int++; + m_data_int_count--; + + return result; + } + + void FlatXAnimReadCursor::ReadRandomDataByte(void* dst, const size_t count) + { + if (m_random_data_byte_count < count) + throw DATA_EXHAUSTED_ERROR("randomDataByte", count, m_random_data_byte_count); + + std::memcpy(dst, m_random_data_byte, count * sizeof(uint8_t)); + + m_random_data_byte += count; + m_random_data_byte_count -= count; + } + + void FlatXAnimReadCursor::ReadRandomDataShort(void* dst, const size_t count) + { + if (m_random_data_short_count < count) + throw DATA_EXHAUSTED_ERROR("randomDataShort", count, m_random_data_short_count); + + std::memcpy(dst, m_random_data_short, count * sizeof(int16_t)); + + m_random_data_short += count; + m_random_data_short_count -= count; + } + + void FlatXAnimReadCursor::ReadIndices(void* dst, const size_t count) + { + if (m_indices_count < count) + throw DATA_EXHAUSTED_ERROR("indices", count, m_indices_count); + + std::memcpy(dst, m_indices, count * sizeof(uint16_t)); + + m_indices += count; + m_indices_count -= count; + } + + std::expected FlatXAnimReadCursor::ExpectEndOfData() const + { +#define END_OF_DATA_ERROR(name, size) std::unexpected(std::format("Expected {} to be exhausted but {} bytes remain", name, size)) +#define CHECK_END_OF_DATA_ERROR(name, size) \ + if ((size) > 0) \ + return END_OF_DATA_ERROR(name, size); + + CHECK_END_OF_DATA_ERROR("dataByte", m_data_byte_count) + CHECK_END_OF_DATA_ERROR("dataShort", m_data_short_count) + CHECK_END_OF_DATA_ERROR("dataInt", m_data_int_count) + CHECK_END_OF_DATA_ERROR("randomDataByte", m_random_data_byte_count) + CHECK_END_OF_DATA_ERROR("randomDataShort", m_random_data_short_count) + CHECK_END_OF_DATA_ERROR("indices", m_indices_count) + + return {}; + +#undef END_OF_DATA_ERROR +#undef CHECK_END_OF_DATA_ERROR + } + + std::expected, std::string> CreateBoneTracksFromFlatData(std::vector boneNames, + const XAnimBoneCounts& boneCounts, + FlatXAnimReadCursor& cursor, + const bool useByteIndices) + { + const auto boneCount = boneNames.size(); + std::vector boneTracks(boneCount); + for (size_t i = 0; i < boneCount; i++) + boneTracks[i].m_name = std::move(boneNames[i]); + + size_t boneIndex = 0; + + try + { + for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::NO_QUAT); i++, boneIndex++) + boneTracks[boneIndex].m_quat.m_type = QuatType::NO_QUAT; + + for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::HALF_QUAT); i++, boneIndex++) + { + auto& quat = boneTracks[boneIndex].m_quat; + quat.m_type = QuatType::HALF_QUAT; + const auto storedSize = static_cast(cursor.PopDataShort()); + const auto frameCount = static_cast(storedSize) + 1uz; + quat.m_indices = ReadPackedIndices(cursor, storedSize, useByteIndices); + + static_assert(sizeof(decltype(quat.m_frames2)::value_type) == sizeof(int16_t) * 2); + quat.m_frames2.resize(frameCount); + cursor.ReadRandomDataShort(quat.m_frames2.data(), frameCount * 2); + } + + for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::FULL_QUAT); i++, boneIndex++) + { + auto& quat = boneTracks[boneIndex].m_quat; + quat.m_type = QuatType::FULL_QUAT; + const auto storedSize = static_cast(cursor.PopDataShort()); + const auto frameCount = static_cast(storedSize) + 1uz; + quat.m_indices = ReadPackedIndices(cursor, storedSize, useByteIndices); + + static_assert(sizeof(decltype(quat.m_frames)::value_type) == sizeof(int16_t) * 4); + quat.m_frames.resize(frameCount); + cursor.ReadRandomDataShort(quat.m_frames.data(), frameCount * 4); + } + + for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::HALF_QUAT_NO_SIZE); i++, boneIndex++) + { + auto& quat = boneTracks[boneIndex].m_quat; + quat.m_type = QuatType::HALF_QUAT_NO_SIZE; + + static_assert(sizeof(decltype(quat.m_frames2)::value_type) == sizeof(int16_t) * 2); + quat.m_frames2.resize(1); + cursor.ReadDataShort(quat.m_frames2.data(), 2); + } + + for (auto i = 0u; i < boneCounts.GetCountForQuatType(QuatType::FULL_QUAT_NO_SIZE); i++, boneIndex++) + { + auto& quat = boneTracks[boneIndex].m_quat; + quat.m_type = QuatType::FULL_QUAT_NO_SIZE; + + static_assert(sizeof(decltype(quat.m_frames)::value_type) == sizeof(int16_t) * 4); + quat.m_frames.resize(1); + cursor.ReadDataShort(quat.m_frames.data(), 4); + } + + std::vector transAssigned(boneCount, false); + + for (auto i = 0u; i < boneCounts.GetCountForTransType(TransType::SMALL_TRANS); i++) + { + const auto bone = static_cast(cursor.PopDataByte()); + assert(bone < boneCount && !transAssigned[bone]); + + auto& trans = boneTracks[bone].m_trans; + transAssigned[bone] = true; + trans.m_type = TransType::SMALL_TRANS; + + const auto storedSize = static_cast(cursor.PopDataShort()); + const auto frameCount = static_cast(storedSize) + 1uz; + trans.m_mins = ReadFloat3(cursor); + trans.m_size = ReadFloat3(cursor); + trans.m_indices = ReadPackedIndices(cursor, storedSize, useByteIndices); + + static_assert(sizeof(decltype(trans.m_byte_frames)::value_type) == sizeof(uint8_t)); + trans.m_byte_frames.resize(frameCount * 3uz); + cursor.ReadRandomDataByte(trans.m_byte_frames.data(), frameCount * 3uz); + } + + for (auto i = 0u; i < boneCounts.GetCountForTransType(TransType::FULL_TRANS); i++) + { + const auto bone = static_cast(cursor.PopDataByte()); + assert(bone < boneCount && !transAssigned[bone]); + + auto& trans = boneTracks[bone].m_trans; + transAssigned[bone] = true; + trans.m_type = TransType::FULL_TRANS; + + const auto storedSize = static_cast(cursor.PopDataShort()); + const auto frameCount = static_cast(storedSize) + 1uz; + trans.m_mins = ReadFloat3(cursor); + trans.m_size = ReadFloat3(cursor); + trans.m_indices = ReadPackedIndices(cursor, storedSize, useByteIndices); + + static_assert(sizeof(decltype(trans.m_short_frames)::value_type) == sizeof(int16_t)); + trans.m_short_frames.resize(frameCount * 3uz); + cursor.ReadRandomDataShort(trans.m_short_frames.data(), frameCount * 3uz); + } + + for (auto i = 0u; i < boneCounts.GetCountForTransType(TransType::TRANS_NO_SIZE); i++) + { + const auto bone = static_cast(cursor.PopDataByte()); + assert(bone < boneCount && !transAssigned[bone]); + + auto& trans = boneTracks[bone].m_trans; + transAssigned[bone] = true; + trans.m_type = TransType::TRANS_NO_SIZE; + trans.m_constant = ReadFloat3(cursor); + } + + for (auto i = 0u; i < boneCounts.GetCountForTransType(TransType::NO_TRANS); i++) + { + const auto bone = static_cast(cursor.PopDataByte()); + assert(bone < boneCount && !transAssigned[bone]); + + boneTracks[bone].m_trans.m_type = TransType::NO_TRANS; + transAssigned[bone] = true; + } + + for (auto i = 0uz; i < boneCount; i++) + assert(transAssigned[i]); + } + catch (const FlatDataReadException& exception) + { + return std::unexpected(exception.message()); + } + + auto maybeError = cursor.ExpectEndOfData(); + if (!maybeError.has_value()) + return std::unexpected(std::move(maybeError).error()); + + return boneTracks; + } +} // namespace xanim diff --git a/src/ObjWriting/XAnim/FlatXAnimReader.h b/src/ObjWriting/XAnim/FlatXAnimReader.h new file mode 100644 index 00000000..3666d890 --- /dev/null +++ b/src/ObjWriting/XAnim/FlatXAnimReader.h @@ -0,0 +1,94 @@ +#pragma once + +#include "XAnim/XAnimCommon.h" + +#include +#include +#include +#include +#include + +namespace xanim +{ + class XAnimBoneCounts + { + public: + XAnimBoneCounts(size_t noQuatCount, + size_t halfQuatCount, + size_t fullQuatCount, + size_t halfQuatNoSizeCount, + size_t fullQuatNoSizeCount, + size_t smallTransCount, + size_t fullTransCount, + size_t transNoSizeCount, + size_t noTransCount); + + [[nodiscard]] size_t GetCountForQuatType(QuatType quatType) const; + [[nodiscard]] size_t GetCountForTransType(TransType transType) const; + + private: + std::array m_counts; + }; + + class FlatDataReadException : public std::exception + { + public: + explicit FlatDataReadException(std::string message); + + [[nodiscard]] const char* what() const noexcept override; + [[nodiscard]] const std::string& message() const; + + private: + std::string m_message; + }; + + class FlatXAnimReadCursor + { + public: + FlatXAnimReadCursor(uint8_t* dataByte, + size_t dataByteCount, + int16_t* dataShort, + size_t dataShortCount, + int32_t* dataInt, + size_t dataIntCount, + uint8_t* randomDataByte, + size_t randomDataByteCount, + int16_t* randomDataShort, + size_t randomDataShortCount, + uint16_t* indices, + size_t indicesCount); + + uint8_t PopDataByte(); + int16_t PopDataShort(); + void ReadDataShort(void* dst, size_t count); + void SkipDataShort(size_t count); + int32_t PopDataInt(); + void ReadRandomDataByte(void* dst, size_t count); + void ReadRandomDataShort(void* dst, size_t count); + void ReadIndices(void* dst, size_t count); + + [[nodiscard]] std::expected ExpectEndOfData() const; + + private: + uint8_t* m_data_byte; + size_t m_data_byte_count; + + int16_t* m_data_short; + size_t m_data_short_count; + + int32_t* m_data_int; + size_t m_data_int_count; + + uint8_t* m_random_data_byte; + size_t m_random_data_byte_count; + + int16_t* m_random_data_short; + size_t m_random_data_short_count; + + uint16_t* m_indices; + size_t m_indices_count; + }; + + std::expected, std::string> + CreateBoneTracksFromFlatData(std::vector boneNames, const XAnimBoneCounts& boneCounts, FlatXAnimReadCursor& cursor, bool useByteIndices); +} // namespace xanim