diff --git a/src/Common/Game/IW4/IW4_Assets.h b/src/Common/Game/IW4/IW4_Assets.h index a378ded6..b7f5b409 100644 --- a/src/Common/Game/IW4/IW4_Assets.h +++ b/src/Common/Game/IW4/IW4_Assets.h @@ -395,6 +395,13 @@ namespace IW4 PART_TYPE_COUNT }; + enum XAnimFlags + { + ANIM_LOOP = 0x1, + ANIM_DELTA = 0x2, + ANIM_DELTA_3D = 0x4, + }; + struct XAnimParts { const char* name; @@ -404,7 +411,7 @@ namespace IW4 uint16_t randomDataByteCount; uint16_t randomDataIntCount; uint16_t numframes; - char flags; + unsigned char flags; unsigned char boneCount[PART_TYPE_COUNT]; unsigned char notifyCount; unsigned char assetType; diff --git a/src/Common/Game/IW5/IW5_Assets.h b/src/Common/Game/IW5/IW5_Assets.h index d6a436ff..2b436c10 100644 --- a/src/Common/Game/IW5/IW5_Assets.h +++ b/src/Common/Game/IW5/IW5_Assets.h @@ -419,6 +419,13 @@ namespace IW5 PART_TYPE_COUNT }; + enum XAnimFlags + { + ANIM_LOOP = 0x1, + ANIM_DELTA = 0x2, + ANIM_DELTA_3D = 0x4, + }; + struct XAnimParts { const char* name; diff --git a/src/Common/Game/T6/T6_Assets.h b/src/Common/Game/T6/T6_Assets.h index b0b631bf..e36c22b2 100644 --- a/src/Common/Game/T6/T6_Assets.h +++ b/src/Common/Game/T6/T6_Assets.h @@ -5668,7 +5668,7 @@ namespace T6 XAnimPartTransData u; }; - union XAnimDynamicIndicesDeltaQuat2 + union XAnimDynamicIndicesQuat { unsigned char _1[1]; uint16_t _2[1]; @@ -5682,7 +5682,7 @@ namespace T6 struct type_align32(4) XAnimDeltaPartQuatDataFrames2 { XQuat2* frames; - XAnimDynamicIndicesDeltaQuat2 indices; + XAnimDynamicIndicesQuat indices; }; union XAnimDeltaPartQuatData2 @@ -5697,12 +5697,6 @@ namespace T6 XAnimDeltaPartQuatData2 u; }; - union XAnimDynamicIndicesDeltaQuat - { - unsigned char _1[1]; - uint16_t _2[1]; - }; - struct type_align(4) XQuat { int16_t value[4]; @@ -5711,7 +5705,7 @@ namespace T6 struct type_align32(4) XAnimDeltaPartQuatDataFrames { XQuat* frames; - XAnimDynamicIndicesDeltaQuat indices; + XAnimDynamicIndicesQuat indices; }; union XAnimDeltaPartQuatData diff --git a/src/ObjCommon/XAnim/BinaryXAnimCommon.h b/src/ObjCommon/XAnim/BinaryXAnimCommon.h new file mode 100644 index 00000000..092d00f3 --- /dev/null +++ b/src/ObjCommon/XAnim/BinaryXAnimCommon.h @@ -0,0 +1,50 @@ +#pragma once + +#include + +namespace xanim +{ + enum class CompiledXAnimVersion : uint8_t + { + // IW3, T4 + VERSION_17 = 17, + // IW4, IW5 + VERSION_18 = 18, + // T5, T6 (slightly different format however) + VERSION_19 = 19 + }; + + namespace binary17 + { + constexpr uint8_t FLAG_LOOPED = 0x1; + constexpr uint8_t FLAG_DELTA = 0x2; + } // namespace binary17 + + namespace binary18 + { + constexpr uint8_t FLAG_LOOPED = 0x1; + constexpr uint8_t FLAG_DELTA = 0x2; + constexpr uint8_t FLAG_DELTA_3D = 0x4; + } // namespace binary18 + + namespace binary19 + { + constexpr uint8_t FLAG_LOOPED = 0x1; + constexpr uint8_t FLAG_DELTA = 0x2; + + constexpr uint8_t FLAG_T5_LEFT_HAND_GRIP_IK = 0x4; + constexpr uint8_t FLAG_T5_STREAMABLE = 0x8; + + constexpr uint8_t FLAG_T6_DELTA_3D = 0x4; + constexpr uint8_t FLAG_T6_LEFT_HAND_GRIP_IK = 0x8; + + // This flag is not part of the official format. + // T5 and T6 use the same XAnim version, even though the format is different: + // * Flags have slightly different values + // * T6 does not support "streamable" + // * T5 does not support delta3D + // So this flag value is added to be able to identify whether the file should use + // T5 or T6 parsing behavior. + constexpr uint8_t FLAG_T6_COMPATIBILITY = 0x80; + } // namespace binary19 +} // namespace xanim diff --git a/src/ObjCommon/XAnim/XAnimCommon.cpp b/src/ObjCommon/XAnim/XAnimCommon.cpp index 539ff141..2f803c7e 100644 --- a/src/ObjCommon/XAnim/XAnimCommon.cpp +++ b/src/ObjCommon/XAnim/XAnimCommon.cpp @@ -1,9 +1,146 @@ #include "XAnimCommon.h" +#include #include +#include +#include +#include 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) + { + } + + TransTrack::TransTrack() + : m_type(TransType::NO_TRANS), + m_mins({}), + m_size({}), + m_constant({}) + { + } + + CommonXAnimNotifyInfo::CommonXAnimNotifyInfo() + : m_time(0) + { + } + + CommonXAnimNotifyInfo::CommonXAnimNotifyInfo(std::string name, const float time) + : m_name(std::move(name)), + m_time(time) + { + } + + bool CommonDeltaQuatTrack::Is3DTrack() const + { + return !m_frames.empty(); + } + + CommonDeltaTransTrack::CommonDeltaTransTrack() + : m_constant(std::nullopt), + m_small_trans(false), + m_mins({}), + m_size({}) + { + } + + CommonXAnimParts::CommonXAnimParts() + : m_num_frames(0), + m_looped(false), + m_left_hand_grip_ik(false), + m_streamable(false), + m_frame_rate(0), + m_primed_length(0), + m_asset_type(0) + { + } + + void CommonXAnimParts::SortBoneTracksForQuats() + { + std::vector boneOrder(m_bone_tracks.size()); + std::ranges::iota(boneOrder, 0); + + std::ranges::sort(boneOrder, + [this](const size_t i0, const size_t i1) + { + const auto type0 = std::to_underlying(m_bone_tracks[i0].m_quat.m_type); + const auto type1 = std::to_underlying(m_bone_tracks[i1].m_quat.m_type); + if (type0 != type1) + return type0 < type1; + + return i0 < i1; + }); + + std::vector boneTrackCopies(m_bone_tracks.size()); + for (size_t i = 0u; i < boneOrder.size(); ++i) + { + boneTrackCopies[i] = std::move(m_bone_tracks[boneOrder[i]]); + } + + m_bone_tracks = std::move(boneTrackCopies); + } + + std::vector CommonXAnimParts::GetBoneTrackOrderForTrans() const + { + // This assumes the bone tracks were already sorted for quats + std::vector boneOrder(m_bone_tracks.size()); + std::ranges::iota(boneOrder, 0); + + std::ranges::sort(boneOrder, + [this](const size_t i0, const size_t i1) + { + const auto type0 = std::to_underlying(m_bone_tracks[i0].m_trans.m_type); + const auto type1 = std::to_underlying(m_bone_tracks[i1].m_trans.m_type); + if (type0 != type1) + return type0 < type1; + + return i0 < i1; + }); + + return boneOrder; + } + std::string GetCompiledFileNameForAssetName(const std::string& assetName) { return std::format("xanim/{}", assetName); diff --git a/src/ObjCommon/XAnim/XAnimCommon.h b/src/ObjCommon/XAnim/XAnimCommon.h index efa0fb18..6fa2c4c3 100644 --- a/src/ObjCommon/XAnim/XAnimCommon.h +++ b/src/ObjCommon/XAnim/XAnimCommon.h @@ -1,8 +1,163 @@ #pragma once +#include +#include +#include +#include #include +#include namespace xanim { + 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 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]; + }; + + class QuatTrack + { + public: + QuatTrack(); + + QuatType m_type; + std::vector m_indices; + std::vector m_frames; + std::vector m_frames2; + }; + + class TransTrack + { + public: + TransTrack(); + + TransType m_type; + std::vector m_indices; + std::array m_mins; + std::array m_size; + std::vector m_frames_u8; + std::vector m_frames_u16; + std::array m_constant; + }; + + class BoneTrack + { + public: + BoneTrack() = default; + + std::string m_name; + QuatTrack m_quat; + TransTrack m_trans; + }; + + class CommonXAnimNotifyInfo + { + public: + CommonXAnimNotifyInfo(); + CommonXAnimNotifyInfo(std::string name, float time); + + std::string m_name; + float m_time; + }; + + class CommonDeltaQuatTrack + { + public: + CommonDeltaQuatTrack() = default; + + [[nodiscard]] bool Is3DTrack() const; + + std::vector m_indices; + std::vector m_frames; + std::vector m_frames2; + }; + + class CommonDeltaTransTrack + { + public: + CommonDeltaTransTrack(); + + std::optional> m_constant; + + bool m_small_trans; + std::vector m_indices; + std::array m_mins; + std::array m_size; + std::vector m_frames_u8; + std::vector m_frames_u16; + }; + + class CommonXAnimDeltaTrack + { + public: + CommonXAnimDeltaTrack() = default; + + std::optional m_quat; + std::optional m_trans; + }; + + class CommonXAnimParts + { + public: + CommonXAnimParts(); + + void SortBoneTracksForQuats(); + [[nodiscard]] std::vector GetBoneTrackOrderForTrans() const; + + size_t m_num_frames; + bool m_looped; + bool m_left_hand_grip_ik; + bool m_streamable; + float m_frame_rate; + float m_primed_length; + uint8_t m_asset_type; + std::vector m_bone_tracks; + std::vector m_notifies; + std::unique_ptr m_delta_track; + }; + [[nodiscard]] std::string GetCompiledFileNameForAssetName(const std::string& assetName); -} +} // namespace xanim diff --git a/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp b/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp index d0e648b4..2af4cc84 100644 --- a/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp +++ b/src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp @@ -8,6 +8,7 @@ #include "Game/IW3/Image/ImageLoaderExternalIW3.h" #include "Game/IW3/Techset/PixelShaderLoaderIW3.h" #include "Game/IW3/Techset/VertexShaderLoaderIW3.h" +#include "Game/IW3/XAnim/XAnimLoaderIW3.h" #include "Game/IW3/XModel/LoaderXModelIW3.h" #include "LightDef/LightDefLoaderIW3.h" #include "Localize/AssetLoaderLocalizeIW3.h" @@ -18,7 +19,6 @@ #include "RawFile/AssetLoaderRawFileIW3.h" #include "Sound/LoaderSoundCurveIW3.h" #include "StringTable/AssetLoaderStringTableIW3.h" -#include "XAnim/XAnimLoaderIW3.h" #include diff --git a/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp b/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp deleted file mode 100644 index e988fecc..00000000 --- a/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp +++ /dev/null @@ -1,781 +0,0 @@ -#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 deleted file mode 100644 index 92dfab55..00000000 --- a/src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.h +++ /dev/null @@ -1,13 +0,0 @@ -#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/ObjLoading/Game/IW4/ObjLoaderIW4.cpp b/src/ObjLoading/Game/IW4/ObjLoaderIW4.cpp index 1bc7f913..d183bc61 100644 --- a/src/ObjLoading/Game/IW4/ObjLoaderIW4.cpp +++ b/src/ObjLoading/Game/IW4/ObjLoaderIW4.cpp @@ -8,6 +8,7 @@ #include "Game/IW4/Image/ImageLoaderExternalIW4.h" #include "Game/IW4/Techset/PixelShaderLoaderIW4.h" #include "Game/IW4/Techset/VertexShaderLoaderIW4.h" +#include "Game/IW4/XAnim/XAnimLoaderIW4.h" #include "Game/IW4/XModel/LoaderXModelIW4.h" #include "Leaderboard/LoaderLeaderboardIW4.h" #include "LightDef/LightDefLoaderIW4.h" @@ -125,8 +126,7 @@ namespace collection.AddAssetCreator(phys_preset::CreateRawLoaderIW4(memory, searchPath, zone)); collection.AddAssetCreator(phys_preset::CreateGdtLoaderIW4(memory, gdt, zone)); // collection.AddAssetCreator(std::make_unique(memory)); - // collection.AddAssetCreator(std::make_unique(memory)); - // collection.AddAssetCreator(std::make_unique(memory)); + collection.AddAssetCreator(xanim::CreateLoaderIW4(memory, searchPath, zone)); collection.AddAssetCreator(xmodel::CreateLoaderIW4(memory, searchPath, zone)); collection.AddAssetCreator(material::CreateLoaderIW4(memory, searchPath)); collection.AddAssetCreator(techset::CreateVertexShaderLoaderIW4(memory, searchPath)); diff --git a/src/ObjLoading/Game/IW5/ObjLoaderIW5.cpp b/src/ObjLoading/Game/IW5/ObjLoaderIW5.cpp index 2b5ada65..f9084f30 100644 --- a/src/ObjLoading/Game/IW5/ObjLoaderIW5.cpp +++ b/src/ObjLoading/Game/IW5/ObjLoaderIW5.cpp @@ -8,6 +8,7 @@ #include "Game/IW5/Image/ImageLoaderExternalIW5.h" #include "Game/IW5/Techset/PixelShaderLoaderIW5.h" #include "Game/IW5/Techset/VertexShaderLoaderIW5.h" +#include "Game/IW5/XAnim/XAnimLoaderIW5.h" #include "Game/IW5/XModel/LoaderXModelIW5.h" #include "Leaderboard/LoaderLeaderboardIW5.h" #include "LightDef/LightDefLoaderIW5.h" @@ -132,8 +133,7 @@ namespace collection.AddAssetCreator(phys_preset::CreateRawLoaderIW5(memory, searchPath, zone)); collection.AddAssetCreator(phys_preset::CreateGdtLoaderIW5(memory, gdt, zone)); // collection.AddAssetCreator(std::make_unique(memory)); - // collection.AddAssetCreator(std::make_unique(memory)); - // collection.AddAssetCreator(std::make_unique(memory)); + collection.AddAssetCreator(xanim::CreateLoaderIW5(memory, searchPath, zone)); collection.AddAssetCreator(xmodel::CreateLoaderIW5(memory, searchPath, zone)); collection.AddAssetCreator(material::CreateLoaderIW5(memory, searchPath)); collection.AddAssetCreator(techset::CreateVertexShaderLoaderIW5(memory, searchPath)); diff --git a/src/ObjLoading/Game/T5/ObjLoaderT5.cpp b/src/ObjLoading/Game/T5/ObjLoaderT5.cpp index e39247d5..31dbaec0 100644 --- a/src/ObjLoading/Game/T5/ObjLoaderT5.cpp +++ b/src/ObjLoading/Game/T5/ObjLoaderT5.cpp @@ -8,6 +8,7 @@ #include "Game/T5/T5.h" #include "Game/T5/Techset/PixelShaderLoaderT5.h" #include "Game/T5/Techset/VertexShaderLoaderT5.h" +#include "Game/T5/XAnim/XAnimLoaderT5.h" #include "Game/T5/XModel/LoaderXModelT5.h" #include "LightDef/LightDefLoaderT5.h" #include "Localize/LoaderLocalizeT5.h" @@ -112,7 +113,7 @@ namespace collection.AddAssetCreator(phys_preset::CreateGdtLoaderT5(memory, gdt, zone)); // collection.AddAssetCreator(std::make_unique(memory)); // collection.AddAssetCreator(std::make_unique(memory)); - // collection.AddAssetCreator(std::make_unique(memory)); + collection.AddAssetCreator(xanim::CreateLoaderT5(memory, searchPath, zone)); collection.AddAssetCreator(xmodel::CreateLoaderT5(memory, searchPath, zone)); collection.AddAssetCreator(material::CreateLoaderT5(memory, searchPath)); // collection.AddAssetCreator(std::make_unique(memory)); diff --git a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp index 0721c573..51808774 100644 --- a/src/ObjLoading/Game/T6/ObjLoaderT6.cpp +++ b/src/ObjLoading/Game/T6/ObjLoaderT6.cpp @@ -11,6 +11,7 @@ #include "Game/T6/T6.h" #include "Game/T6/Techset/PixelShaderLoaderT6.h" #include "Game/T6/Techset/VertexShaderLoaderT6.h" +#include "Game/T6/XAnim/XAnimLoaderT6.h" #include "Game/T6/XModel/LoaderXModelT6.h" #include "Image/Dx12TextureLoader.h" #include "Image/IwiLoader.h" @@ -386,7 +387,7 @@ namespace T6 collection.AddAssetCreator(phys_constraints::CreateRawLoaderT6(memory, searchPath, zone)); collection.AddAssetCreator(phys_constraints::CreateGdtLoaderT6(memory, searchPath, gdt, zone)); // collection.AddAssetCreator(std::make_unique(memory)); - // collection.AddAssetCreator(std::make_unique(memory)); + collection.AddAssetCreator(xanim::CreateLoaderT6(memory, searchPath, zone)); collection.AddAssetCreator(xmodel::CreateLoaderT6(memory, searchPath, zone)); collection.AddAssetCreator(material::CreateLoaderT6(memory, searchPath)); // collection.AddAssetCreator(std::make_unique(memory)); diff --git a/src/ObjLoading/XAnim/CompiledXAnimLoader.cpp b/src/ObjLoading/XAnim/CompiledXAnimLoader.cpp new file mode 100644 index 00000000..4ce786b8 --- /dev/null +++ b/src/ObjLoading/XAnim/CompiledXAnimLoader.cpp @@ -0,0 +1,546 @@ +#include "CompiledXAnimLoader.h" + +#include "Utils/Alignment.h" +#include "Utils/StreamUtils.h" +#include "XAnim/BinaryXAnimCommon.h" + +#include +#include +#include +#include +#include + +using namespace xanim; + +namespace +{ + // 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; + + std::expected IdentifyVersion(std::istream& stream) + { + const auto fileVersion = stream::ReadValue(stream); + switch (static_cast(fileVersion)) + { + case CompiledXAnimVersion::VERSION_17: + return CompiledXAnimVersion::VERSION_17; + case CompiledXAnimVersion::VERSION_18: + return CompiledXAnimVersion::VERSION_18; + case CompiledXAnimVersion::VERSION_19: + return CompiledXAnimVersion::VERSION_19; + } + + return std::unexpected(std::format("Version {} is not supported", fileVersion)); + } + + CommonXQuat ConsumeQuat(std::istream& stream) + { + CommonXQuat 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); + + return quat; + } + + CommonXQuat2 ConsumeQuat2(std::istream& stream) + { + CommonXQuat2 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); + + return quat2; + } + + void FlipQuat(CommonXQuat& 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(CommonXQuat2& quat) + { + quat.value[0] = static_cast(-quat.value[0]); + quat.value[1] = static_cast(-quat.value[1]); + } + + [[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 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)); + } + } + + std::expected, std::string> + LoadDeltaQuatTrack(std::istream& stream, const bool useByteIndices, const uint16_t numLoopFrames) + { + const auto numQuatIndices = stream::ReadValue(stream); + if (numQuatIndices == 0) + return std::nullopt; + + CommonDeltaQuatTrack deltaQuatTrack; + if (numQuatIndices == 1) + { + deltaQuatTrack.m_frames.emplace_back(ConsumeQuat(stream)); + return deltaQuatTrack; + } + + LoadIndicesIfNeeded(stream, deltaQuatTrack.m_indices, numQuatIndices, useByteIndices, numLoopFrames); + + deltaQuatTrack.m_frames.reserve(numQuatIndices); + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum) + { + auto& curFrame = deltaQuatTrack.m_frames.emplace_back(ConsumeQuat(stream)); + + if (quatIndexNum > 0) + { + const auto& prevFrame = deltaQuatTrack.m_frames[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); + } + } + + return deltaQuatTrack; + } + + std::expected, std::string> + LoadDeltaQuat2Track(std::istream& stream, const bool useByteIndices, const uint16_t numLoopFrames) + { + const auto numQuatIndices = stream::ReadValue(stream); + if (numQuatIndices == 0) + return std::nullopt; + + CommonDeltaQuatTrack deltaQuatTrack; + if (numQuatIndices == 1) + { + deltaQuatTrack.m_frames2.emplace_back(ConsumeQuat2(stream)); + return deltaQuatTrack; + } + + LoadIndicesIfNeeded(stream, deltaQuatTrack.m_indices, numQuatIndices, useByteIndices, numLoopFrames); + + deltaQuatTrack.m_frames2.reserve(numQuatIndices); + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum) + { + auto& curFrame = deltaQuatTrack.m_frames2.emplace_back(ConsumeQuat2(stream)); + + if (quatIndexNum > 0) + { + const auto& prevFrame = deltaQuatTrack.m_frames2[quatIndexNum - 1]; + if (prevFrame.value[0] * curFrame.value[0] + prevFrame.value[1] * curFrame.value[1] < 0) + FlipQuat2(curFrame); + } + } + + return deltaQuatTrack; + } + + std::expected, std::string> + LoadDeltaTransTrack(std::istream& stream, const bool useByteIndices, const uint16_t numLoopFrames) + { + const auto numTransIndices = stream::ReadValue(stream); + if (numTransIndices == 0) + return std::nullopt; + + CommonDeltaTransTrack deltaTransTrack; + if (numTransIndices == 1) + { + deltaTransTrack.m_constant.emplace(std::array({ + stream::ReadValue(stream), + stream::ReadValue(stream), + stream::ReadValue(stream), + })); + return deltaTransTrack; + } + + LoadIndicesIfNeeded(stream, deltaTransTrack.m_indices, numTransIndices, useByteIndices, numLoopFrames); + + const auto smallTrans = stream::ReadValue(stream); + + deltaTransTrack.m_mins[0] = stream::ReadValue(stream); + deltaTransTrack.m_mins[1] = stream::ReadValue(stream); + deltaTransTrack.m_mins[2] = stream::ReadValue(stream); + + deltaTransTrack.m_size[0] = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + deltaTransTrack.m_size[1] = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + deltaTransTrack.m_size[2] = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + + if (smallTrans) + { + deltaTransTrack.m_frames_u8.resize(numTransIndices); + stream::Read(stream, deltaTransTrack.m_frames_u8.data(), numTransIndices * sizeof(CommonVec3U8)); + } + else + { + deltaTransTrack.m_frames_u16.resize(numTransIndices); + stream::Read(stream, deltaTransTrack.m_frames_u16.data(), numTransIndices * sizeof(CommonVec3U16)); + } + + return deltaTransTrack; + } + + std::expected, std::string> + LoadDeltaTrack(std::istream& stream, const bool hasDelta3D, const bool useByteIndices, const uint16_t numLoopFrames) + { + auto delta = std::make_unique(); + + auto maybeLoadedDeltaQuat = + hasDelta3D ? LoadDeltaQuatTrack(stream, useByteIndices, numLoopFrames) : LoadDeltaQuat2Track(stream, useByteIndices, numLoopFrames); + if (!maybeLoadedDeltaQuat.has_value()) + return std::unexpected(std::move(maybeLoadedDeltaQuat).error()); + delta->m_quat = std::move(maybeLoadedDeltaQuat).value(); + + auto maybeLoadedDeltaTrans = LoadDeltaTransTrack(stream, useByteIndices, numLoopFrames); + if (!maybeLoadedDeltaTrans.has_value()) + return std::unexpected(std::move(maybeLoadedDeltaTrans).error()); + delta->m_trans = std::move(maybeLoadedDeltaTrans).value(); + + return delta; + } + + QuatTrack ReadQuatTrack(std::istream& stream, const uint16_t numLoopFrames, const bool useByteIndices, const bool flipQuat, const bool halfQuat) + { + QuatTrack quatTrack; + + const auto numQuatIndices = stream::ReadValue(stream); + if (numQuatIndices == 0) + { + assert(halfQuat); + quatTrack.m_type = QuatType::NO_QUAT; + return quatTrack; + } + + if (numQuatIndices == 1) + { + quatTrack.m_type = halfQuat ? QuatType::HALF_QUAT_NO_SIZE : QuatType::FULL_QUAT_NO_SIZE; + if (halfQuat) + { + auto quat2 = ConsumeQuat2(stream); + if (flipQuat) + FlipQuat2(quat2); + + quatTrack.m_frames2.emplace_back(quat2); + } + else + { + auto quat = ConsumeQuat(stream); + if (flipQuat) + FlipQuat(quat); + + quatTrack.m_frames.emplace_back(quat); + } + + return quatTrack; + } + + LoadIndicesIfNeeded(stream, quatTrack.m_indices, numQuatIndices, useByteIndices, numLoopFrames); + + if (halfQuat) + { + quatTrack.m_type = QuatType::HALF_QUAT; + quatTrack.m_frames2.reserve(numQuatIndices); + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; quatIndexNum++) + { + auto& curFrame = quatTrack.m_frames2.emplace_back(ConsumeQuat2(stream)); + + if (quatIndexNum > 0) + { + const auto& prevFrame = quatTrack.m_frames2[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.m_type = QuatType::FULL_QUAT; + quatTrack.m_frames.reserve(numQuatIndices); + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; quatIndexNum++) + { + auto& curFrame = quatTrack.m_frames.emplace_back(ConsumeQuat(stream)); + + if (quatIndexNum > 0) + { + const auto& prevFrame = quatTrack.m_frames[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); + } + } + + return quatTrack; + } + + TransTrack ReadTransTrack(std::istream& stream, const uint16_t numLoopFrames, const bool useByteIndices) + { + TransTrack transTrack; + + const auto numTransIndices = stream::ReadValue(stream); + if (numTransIndices == 0) + { + transTrack.m_type = TransType::NO_TRANS; + return transTrack; + } + + if (numTransIndices == 1) + { + transTrack.m_type = TransType::TRANS_NO_SIZE; + for (auto& value : transTrack.m_constant) + value = stream::ReadValue(stream); + return transTrack; + } + + LoadIndicesIfNeeded(stream, transTrack.m_indices, numTransIndices, useByteIndices, numLoopFrames); + + const auto smallTrans = stream::ReadValue(stream); + transTrack.m_type = smallTrans ? TransType::SMALL_TRANS : TransType::FULL_TRANS; + + for (auto& value : transTrack.m_mins) + value = stream::ReadValue(stream); + for (auto& value : transTrack.m_size) + value = DecodeRawTransSize(stream::ReadValue(stream), smallTrans); + + if (smallTrans) + { + static_assert(sizeof(decltype(transTrack.m_frames_u8)::value_type) == sizeof(uint8_t) * 3u); + transTrack.m_frames_u8.resize(numTransIndices); + stream::Read(stream, transTrack.m_frames_u8.data(), numTransIndices * sizeof(uint8_t) * 3); + } + else + { + static_assert(sizeof(decltype(transTrack.m_frames_u16)::value_type) == sizeof(int16_t) * 3u); + transTrack.m_frames_u16.resize(numTransIndices); + stream::Read(stream, transTrack.m_frames_u16.data(), numTransIndices * sizeof(uint16_t) * 3); + } + + return transTrack; + } + + void ReadNoteTracks(std::istream& stream, CommonXAnimParts& parts) + { + const auto numNoteTracks = stream::ReadValue(stream); + + parts.m_notifies.reserve(numNoteTracks + 1); + for (auto notifyIndex = 0u; notifyIndex < numNoteTracks; notifyIndex++) + { + const auto notifyName = stream::ReadCString(stream); + + const auto frame = stream::ReadValue(stream); + const auto time = parts.m_num_frames > 0 ? static_cast(frame) / static_cast(parts.m_num_frames) : 0; + assert(time >= 0.0f && time <= 1.0f); + parts.m_notifies.emplace_back(notifyName, time); + } + + // This notify is always automatically added + parts.m_notifies.emplace_back("end", 1.0f); + } + + bool IsLooped(const uint8_t flags, const CompiledXAnimVersion version) + { + switch (version) + { + case CompiledXAnimVersion::VERSION_17: + return (flags & binary17::FLAG_LOOPED) > 0; + case CompiledXAnimVersion::VERSION_18: + return (flags & binary18::FLAG_LOOPED) > 0; + case CompiledXAnimVersion::VERSION_19: + return (flags & binary19::FLAG_LOOPED) > 0; + } + + return false; + } + + bool HasDelta(const uint8_t flags, const CompiledXAnimVersion version) + { + switch (version) + { + case CompiledXAnimVersion::VERSION_17: + return (flags & binary17::FLAG_DELTA) > 0; + case CompiledXAnimVersion::VERSION_18: + return (flags & binary18::FLAG_DELTA) > 0; + case CompiledXAnimVersion::VERSION_19: + return (flags & binary19::FLAG_DELTA) > 0; + } + + return false; + } + + bool HasDelta3D(const uint8_t flags, const CompiledXAnimVersion version) + { + switch (version) + { + case CompiledXAnimVersion::VERSION_18: + return (flags & binary18::FLAG_DELTA_3D) > 0; + case CompiledXAnimVersion::VERSION_19: + return (flags & binary19::FLAG_T6_COMPATIBILITY) > 0 && (flags & binary19::FLAG_T6_DELTA_3D) > 0; + case CompiledXAnimVersion::VERSION_17: + return false; + } + + return false; + } + + bool IsLeftHandGripIk(const uint8_t flags, const CompiledXAnimVersion version) + { + switch (version) + { + case CompiledXAnimVersion::VERSION_19: + if (flags & binary19::FLAG_T6_COMPATIBILITY) + return (flags & binary19::FLAG_T6_LEFT_HAND_GRIP_IK) > 0; + return (flags & binary19::FLAG_T5_LEFT_HAND_GRIP_IK) > 0; + case CompiledXAnimVersion::VERSION_17: + case CompiledXAnimVersion::VERSION_18: + return false; + } + + return false; + } + + bool IsStreamable(const uint8_t flags, const CompiledXAnimVersion version) + { + switch (version) + { + case CompiledXAnimVersion::VERSION_19: + if (flags & binary19::FLAG_T6_COMPATIBILITY) + return false; + return (flags & binary19::FLAG_T5_STREAMABLE) > 0; + + case CompiledXAnimVersion::VERSION_17: + case CompiledXAnimVersion::VERSION_18: + return false; + } + + return false; + } +} // namespace + +namespace xanim +{ + std::expected, std::string> LoadCompiledXAnim(std::istream& stream) + { + auto maybeVersion = IdentifyVersion(stream); + if (!maybeVersion) + return std::unexpected(std::move(maybeVersion).error()); + + auto parts = std::make_unique(); + + const auto version = maybeVersion.value(); + + 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()) + return std::unexpected("Truncated file"); + + const bool isLooped = IsLooped(flags, version); + const bool hasDelta = HasDelta(flags, version); + const bool hasDelta3D = HasDelta3D(flags, version); + const bool leftHandGripIk = IsLeftHandGripIk(flags, version); + const bool streamable = IsStreamable(flags, version); + const uint16_t numLoopFrames = isLooped ? numFrames + 1u : numFrames; + + parts->m_num_frames = numLoopFrames - 1; + parts->m_looped = isLooped; + parts->m_left_hand_grip_ik = leftHandGripIk; + parts->m_streamable = streamable; + parts->m_asset_type = assetType; + parts->m_frame_rate = static_cast(framerate); + + if (version == CompiledXAnimVersion::VERSION_19 && streamable) + parts->m_primed_length = stream::ReadValue(stream); + + const auto useByteIndices = parts->m_num_frames < 256; + + if (hasDelta || hasDelta3D) + { + auto maybeBoneTrack = LoadDeltaTrack(stream, hasDelta3D, useByteIndices, numLoopFrames); + if (!maybeBoneTrack.has_value()) + return std::unexpected(std::move(maybeBoneTrack).error()); + + parts->m_delta_track = std::move(maybeBoneTrack).value(); + } + + 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); + + parts->m_bone_tracks.resize(boneCount); + for (size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex) + parts->m_bone_tracks[boneIndex].m_name = stream::ReadCString(stream); + + for (size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex) + { + auto& boneTrack = parts->m_bone_tracks[boneIndex]; + + const bool flipQuat = flipQuatBits[boneIndex / 8u] & static_cast(1u << (boneIndex % 8u)); + const bool halfQuat = halfQuatBits[boneIndex / 8u] & static_cast(1u << (boneIndex % 8u)); + + boneTrack.m_quat = ReadQuatTrack(stream, numLoopFrames, useByteIndices, flipQuat, halfQuat); + boneTrack.m_trans = ReadTransTrack(stream, numLoopFrames, useByteIndices); + } + } + parts->SortBoneTracksForQuats(); + + ReadNoteTracks(stream, *parts); + + assert(stream.peek() == std::char_traits::eof()); + return parts; + } +} // namespace xanim diff --git a/src/ObjLoading/XAnim/CompiledXAnimLoader.h b/src/ObjLoading/XAnim/CompiledXAnimLoader.h new file mode 100644 index 00000000..add45f75 --- /dev/null +++ b/src/ObjLoading/XAnim/CompiledXAnimLoader.h @@ -0,0 +1,12 @@ +#pragma once + +#include "XAnim/XAnimCommon.h" + +#include +#include +#include + +namespace xanim +{ + std::expected, std::string> LoadCompiledXAnim(std::istream& stream); +} diff --git a/src/ObjLoading/XAnim/FlatXAnimDataWriter.cpp b/src/ObjLoading/XAnim/FlatXAnimDataWriter.cpp new file mode 100644 index 00000000..947ee92b --- /dev/null +++ b/src/ObjLoading/XAnim/FlatXAnimDataWriter.cpp @@ -0,0 +1,188 @@ +#include "FlatXAnimDataWriter.h" + +#include +#include + +using namespace xanim; + +namespace +{ + [[nodiscard]] int FloatBitsToInt(const float value) + { + union + { + int i; + float f; + }; + + f = value; + return i; + } + + void WriteFloat3(FlatData& writeCursor, const std::array& value) + { + for (const float f : value) + writeCursor.m_data_int.emplace_back(FloatBitsToInt(f)); + } + + void WritePackedIndices(FlatData& writeCursor, const std::vector& indices, const bool useByteIndices) + { + const auto indexCount = indices.size(); + writeCursor.m_data_short.emplace_back(static_cast(indexCount - 1)); // storedSize + + if (useByteIndices) + { + for (const auto index : indices) + { + assert(index <= std::numeric_limits::max()); + writeCursor.m_data_byte.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.m_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.m_data_short.emplace_back(indices[256 * i]); + writeCursor.m_data_short.emplace_back(indices[indices.size() - 1]); + } + else + { + std::ranges::copy(indices, std::back_inserter(writeCursor.m_data_short)); + } + } + + void ProcessQuatTrack(FlatData& writeCursor, const QuatTrack& quatTrack, const bool useByteIndices) + { + switch (quatTrack.m_type) + { + case QuatType::NO_QUAT: + break; + + case QuatType::HALF_QUAT: + WritePackedIndices(writeCursor, quatTrack.m_indices, useByteIndices); + assert(quatTrack.m_frames2.size() == quatTrack.m_indices.size()); + + writeCursor.m_random_data_short.reserve(writeCursor.m_random_data_short.size() + quatTrack.m_frames2.size() * 2); + for (const auto& quat2 : quatTrack.m_frames2) + { + writeCursor.m_random_data_short.emplace_back(quat2.value[0]); + writeCursor.m_random_data_short.emplace_back(quat2.value[1]); + } + break; + + case QuatType::FULL_QUAT: + WritePackedIndices(writeCursor, quatTrack.m_indices, useByteIndices); + assert(quatTrack.m_frames.size() == quatTrack.m_indices.size()); + + writeCursor.m_random_data_short.reserve(writeCursor.m_random_data_short.size() + quatTrack.m_frames.size() * 4); + for (const auto& quat : quatTrack.m_frames) + { + writeCursor.m_random_data_short.emplace_back(quat.value[0]); + writeCursor.m_random_data_short.emplace_back(quat.value[1]); + writeCursor.m_random_data_short.emplace_back(quat.value[2]); + writeCursor.m_random_data_short.emplace_back(quat.value[3]); + } + break; + + case QuatType::HALF_QUAT_NO_SIZE: + { + assert(quatTrack.m_frames2.size() == 1); + writeCursor.m_data_short.reserve(writeCursor.m_data_short.size() + 2); + + const auto& quat2 = quatTrack.m_frames2[0]; + writeCursor.m_data_short.emplace_back(quat2.value[0]); + writeCursor.m_data_short.emplace_back(quat2.value[1]); + break; + } + + case QuatType::FULL_QUAT_NO_SIZE: + { + assert(quatTrack.m_frames.size() == 1); + writeCursor.m_data_short.reserve(writeCursor.m_data_short.size() + 4); + + const auto& quat = quatTrack.m_frames[0]; + writeCursor.m_data_short.emplace_back(quat.value[0]); + writeCursor.m_data_short.emplace_back(quat.value[1]); + writeCursor.m_data_short.emplace_back(quat.value[2]); + writeCursor.m_data_short.emplace_back(quat.value[3]); + break; + } + } + } + + void ProcessTransTrack(FlatData& writeCursor, const TransTrack& transTrack, const size_t boneIndex, const bool useByteIndices) + { + assert(boneIndex <= std::numeric_limits::max()); + writeCursor.m_data_byte.emplace_back(static_cast(boneIndex)); + + switch (transTrack.m_type) + { + case TransType::SMALL_TRANS: + WritePackedIndices(writeCursor, transTrack.m_indices, useByteIndices); + WriteFloat3(writeCursor, transTrack.m_mins); + WriteFloat3(writeCursor, transTrack.m_size); + assert(transTrack.m_frames_u8.size() == transTrack.m_indices.size()); + + writeCursor.m_random_data_byte.reserve(writeCursor.m_random_data_byte.size() + transTrack.m_frames_u8.size() * 3); + for (const auto& vec : transTrack.m_frames_u8) + { + writeCursor.m_random_data_byte.emplace_back(vec.value[0]); + writeCursor.m_random_data_byte.emplace_back(vec.value[1]); + writeCursor.m_random_data_byte.emplace_back(vec.value[2]); + } + break; + + case TransType::FULL_TRANS: + WritePackedIndices(writeCursor, transTrack.m_indices, useByteIndices); + WriteFloat3(writeCursor, transTrack.m_mins); + WriteFloat3(writeCursor, transTrack.m_size); + assert(transTrack.m_frames_u16.size() == transTrack.m_indices.size()); + + writeCursor.m_random_data_short.reserve(writeCursor.m_random_data_short.size() + transTrack.m_frames_u16.size() * 3); + for (const auto& vec : transTrack.m_frames_u16) + { + writeCursor.m_random_data_short.emplace_back(vec.value[0]); + writeCursor.m_random_data_short.emplace_back(vec.value[1]); + writeCursor.m_random_data_short.emplace_back(vec.value[2]); + } + break; + + case TransType::TRANS_NO_SIZE: + WriteFloat3(writeCursor, transTrack.m_constant); + break; + + case TransType::NO_TRANS: + break; + } + } +} // namespace + +namespace xanim +{ + FlatData CreateFlatDataFromCommonXAnim(const CommonXAnimParts& parts) + { + FlatData writeCursor; + + const auto useByteIndices = parts.m_num_frames < 256; + + for (const auto& boneTrack : parts.m_bone_tracks) + ProcessQuatTrack(writeCursor, boneTrack.m_quat, useByteIndices); + + const auto transBoneOrder = parts.GetBoneTrackOrderForTrans(); + const auto boneCount = transBoneOrder.size(); + for (size_t i = 0; i < boneCount; ++i) + { + const auto boneIndex = transBoneOrder[i]; + ProcessTransTrack(writeCursor, parts.m_bone_tracks[boneIndex].m_trans, boneIndex, useByteIndices); + } + + return writeCursor; + } +} // namespace xanim diff --git a/src/ObjLoading/XAnim/FlatXAnimDataWriter.h b/src/ObjLoading/XAnim/FlatXAnimDataWriter.h new file mode 100644 index 00000000..447ac79e --- /dev/null +++ b/src/ObjLoading/XAnim/FlatXAnimDataWriter.h @@ -0,0 +1,22 @@ +#pragma once + +#include "XAnim/XAnimCommon.h" + +#include +#include + +namespace xanim +{ + class FlatData + { + public: + std::vector m_data_byte; + std::vector m_data_short; + std::vector m_data_int; + std::vector m_random_data_byte; + std::vector m_random_data_short; + std::vector m_indices; + }; + + FlatData CreateFlatDataFromCommonXAnim(const CommonXAnimParts& parts); +} // namespace xanim diff --git a/src/ObjLoading/XAnim/XAnimLoader.cpp.template b/src/ObjLoading/XAnim/XAnimLoader.cpp.template new file mode 100644 index 00000000..11fc9c84 --- /dev/null +++ b/src/ObjLoading/XAnim/XAnimLoader.cpp.template @@ -0,0 +1,490 @@ +#options GAME(IW3, IW4, IW5, T5, T6) + +#filename "Game/" + GAME + "/XAnim/XAnimLoader" + GAME + ".cpp" + +#set LOADER_HEADER "\"XAnimLoader" + GAME + ".h\"" + +#if GAME == "IW3" +#define FEATURE_IW3 +#elif GAME == "IW4" +#define FEATURE_IW4 +#define HAS_DELTA_QUAT_3D +#elif GAME == "IW5" +#define FEATURE_IW5 +#define HAS_DELTA_QUAT_3D +#elif GAME == "T5" +#define FEATURE_T5 +#elif GAME == "T6" +#define FEATURE_T6 +#define HAS_DELTA_QUAT_3D +#endif + +#if defined(FEATURE_IW4) || defined(FEATURE_IW5) +#define SET_IS_LOOPED() parts.flags |= ANIM_LOOP +#define SET_HAS_DELTA() parts.flags |= ANIM_DELTA +#define SET_HAS_DELTA_3D() parts.flags |= ANIM_DELTA_3D +#else +#define SET_IS_LOOPED() parts.bLoop = true +#define SET_HAS_DELTA() parts.bDelta = true +#define SET_HAS_DELTA_3D() parts.bDelta3D = true +#endif + +// This file was templated. +// See XAnimLoader.cpp.template. +// Do not modify, changes will be lost. + +#include LOADER_HEADER + +#include "Utils/Logging/Log.h" +#include "XAnim/CompiledXAnimLoader.h" +#include "XAnim/FlatXAnimDataWriter.h" +#include "XAnim/XAnimCommon.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace GAME; + +namespace +{ + void ConvertNoteTracks(XAnimParts& parts, + const xanim::CommonXAnimParts& commonParts, + AssetRegistration& registration, + MemoryManager& memory, + ZoneScriptStrings& scriptStrings) + { + if (commonParts.m_notifies.empty()) + return; + + const auto numCommonNoteTracks = commonParts.m_notifies.size(); + const auto numNoteTracks = static_cast(std::min(numCommonNoteTracks, std::numeric_limits::max())); + + if (numNoteTracks < numCommonNoteTracks) + con::error("XAnim {}: Could only fit {} of {} notetracks", parts.name, numNoteTracks, numCommonNoteTracks); + + parts.notifyCount = numNoteTracks; + parts.notify = memory.Alloc(numNoteTracks); + + for (auto notifyIndex = 0u; notifyIndex < numCommonNoteTracks; notifyIndex++) + { + const auto& commonNotify = commonParts.m_notifies[notifyIndex]; + auto& notify = parts.notify[notifyIndex]; + + notify.name = scriptStrings.AddOrGetScriptString(commonNotify.m_name); + registration.AddScriptString(notify.name); + + notify.time = commonNotify.m_time; + } + +#if defined(FEATURE_T5) || defined(FEATURE_T6) + const auto loopBegin = std::ranges::find_if(commonParts.m_notifies, [](const xanim::CommonXAnimNotifyInfo& notify) + { + return notify.m_name == "loop_begin"; + }); + + if (loopBegin != commonParts.m_notifies.end()) + parts.loopEntryTime = loopBegin->m_time; + else + parts.loopEntryTime = 0; +#endif + } + + template void ConvertIndices(T& indices, const std::vector& commonIndices, const bool useByteIndices) + { + if (useByteIndices) + { + const auto numIndices = commonIndices.size(); + for (size_t i = 0u; i < numIndices; i++) + { + assert(commonIndices[i] <= std::numeric_limits::max()); + indices._1[i] = static_cast(commonIndices[i]); + } + } + else + { + std::memcpy(indices._2, commonIndices.data(), commonIndices.size() * sizeof(uint16_t)); + } + } + + void CountBoneTrackTypes(XAnimParts& parts, const xanim::BoneTrack& boneTrack) + { + switch (boneTrack.m_quat.m_type) + { + case xanim::QuatType::NO_QUAT: + parts.boneCount[PART_TYPE_NO_QUAT]++; + break; + case xanim::QuatType::HALF_QUAT: + parts.boneCount[PART_TYPE_HALF_QUAT]++; + break; + case xanim::QuatType::FULL_QUAT: + parts.boneCount[PART_TYPE_FULL_QUAT]++; + break; + case xanim::QuatType::HALF_QUAT_NO_SIZE: + parts.boneCount[PART_TYPE_HALF_QUAT_NO_SIZE]++; + break; + case xanim::QuatType::FULL_QUAT_NO_SIZE: + parts.boneCount[PART_TYPE_FULL_QUAT_NO_SIZE]++; + break; + } + + switch (boneTrack.m_trans.m_type) + { + case xanim::TransType::SMALL_TRANS: + parts.boneCount[PART_TYPE_SMALL_TRANS]++; + break; + case xanim::TransType::FULL_TRANS: + parts.boneCount[PART_TYPE_TRANS]++; + break; + case xanim::TransType::TRANS_NO_SIZE: + parts.boneCount[PART_TYPE_TRANS_NO_SIZE]++; + break; + case xanim::TransType::NO_TRANS: + parts.boneCount[PART_TYPE_NO_TRANS]++; + break; + } + } + + void ConvertFlatData(XAnimParts& parts, const xanim::FlatData& flatData, MemoryManager& memory) + { + if (!flatData.m_data_byte.empty()) + { + parts.dataByteCount = static_cast(flatData.m_data_byte.size()); + parts.dataByte = memory.Alloc(parts.dataByteCount); + std::memcpy(parts.dataByte, flatData.m_data_byte.data(), parts.dataByteCount * sizeof(uint8_t)); + } + + if (!flatData.m_data_short.empty()) + { + parts.dataShortCount = static_cast(flatData.m_data_short.size()); + parts.dataShort = memory.Alloc(parts.dataShortCount); + std::memcpy(parts.dataShort, flatData.m_data_short.data(), parts.dataShortCount * sizeof(int16_t)); + } + + if (!flatData.m_data_int.empty()) + { + parts.dataIntCount = static_cast(flatData.m_data_int.size()); + parts.dataInt = memory.Alloc(parts.dataIntCount); + std::memcpy(parts.dataInt, flatData.m_data_int.data(), parts.dataIntCount * sizeof(int32_t)); + } + + if (!flatData.m_random_data_byte.empty()) + { + parts.randomDataByteCount = static_cast(flatData.m_random_data_byte.size()); + parts.randomDataByte = memory.Alloc(parts.randomDataByteCount); + std::memcpy(parts.randomDataByte, flatData.m_random_data_byte.data(), parts.randomDataByteCount * sizeof(uint8_t)); + } + + if (!flatData.m_random_data_short.empty()) + { + parts.randomDataShortCount = static_cast(flatData.m_random_data_short.size()); + parts.randomDataShort = memory.Alloc(parts.randomDataShortCount); + std::memcpy(parts.randomDataShort, flatData.m_random_data_short.data(), parts.randomDataShortCount * sizeof(int16_t)); + } + + if (!flatData.m_indices.empty()) + { + parts.indexCount = static_cast(flatData.m_indices.size()); + parts.indices._2 = memory.Alloc(parts.indexCount); + std::memcpy(parts.indices._2, flatData.m_indices.data(), parts.indexCount * sizeof(uint16_t)); + } + } + +#ifdef HAS_DELTA_QUAT_3D +#define DELTA_QUAT_2D_MEMBER quat2 +#define DELTA_QUAT_2D_STRUCT XAnimDeltaPartQuat2 +#define DELTA_QUAT_2D_DATA_STRUCT XAnimDeltaPartQuatData2 +#define DELTA_QUAT_2D_FRAMES_STRUCT XAnimDeltaPartQuatDataFrames2 +#else +#define DELTA_QUAT_2D_MEMBER quat +#define DELTA_QUAT_2D_STRUCT XAnimDeltaPartQuat +#define DELTA_QUAT_2D_DATA_STRUCT XAnimDeltaPartQuatData +#define DELTA_QUAT_2D_FRAMES_STRUCT XAnimDeltaPartQuatDataFrames +#endif + +#ifdef HAS_DELTA_QUAT_3D + void ConvertCommonDeltaQuatPart(XAnimDeltaPart& deltaPart, + const xanim::CommonDeltaQuatTrack& commonDeltaQuatTrack, + MemoryManager& memory, + const bool useByteIndices) + { + if (commonDeltaQuatTrack.m_frames.size() == 1) + { + deltaPart.quat = static_cast(memory.AllocRaw(offsetof(XAnimDeltaPartQuat, u) + sizeof(XAnimDeltaPartQuatData::frame0))); + deltaPart.quat->size = 0; + + const auto& commonFrame = commonDeltaQuatTrack.m_frames[0]; + auto& frame = deltaPart.quat->u.frame0; + + frame.value[0] = commonFrame.value[0]; + frame.value[1] = commonFrame.value[1]; + frame.value[2] = commonFrame.value[2]; + frame.value[3] = commonFrame.value[3]; + return; + } + + const auto numQuatIndices = commonDeltaQuatTrack.m_indices.size(); + const auto indicesArraySize = + useByteIndices ? numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_1) : numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_2); + + deltaPart.quat = static_cast( + memory.AllocRaw(offsetof(XAnimDeltaPartQuat, u) + offsetof(XAnimDeltaPartQuatDataFrames, indices) + indicesArraySize)); + + auto& frames = deltaPart.quat->u.frames; + ConvertIndices(frames.indices, commonDeltaQuatTrack.m_indices, useByteIndices); + + deltaPart.quat->size = static_cast(numQuatIndices - 1); + deltaPart.quat->u.frames.frames = memory.Alloc(numQuatIndices); + + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum) + { + const auto& commonFrame = commonDeltaQuatTrack.m_frames[quatIndexNum]; + auto& curFrame = deltaPart.quat->u.frames.frames[quatIndexNum]; + + curFrame.value[0] = commonFrame.value[0]; + curFrame.value[1] = commonFrame.value[1]; + curFrame.value[2] = commonFrame.value[2]; + curFrame.value[3] = commonFrame.value[3]; + } + } +#endif + + void ConvertCommonDeltaQuat2Part(XAnimDeltaPart& deltaPart, + const xanim::CommonDeltaQuatTrack& commonDeltaQuatTrack, + MemoryManager& memory, + const bool useByteIndices) + { + if (commonDeltaQuatTrack.m_frames2.size() == 1) + { + deltaPart.DELTA_QUAT_2D_MEMBER = static_cast(memory.AllocRaw(offsetof(DELTA_QUAT_2D_STRUCT, u) + sizeof(DELTA_QUAT_2D_DATA_STRUCT::frame0))); + deltaPart.DELTA_QUAT_2D_MEMBER->size = 0; + + const auto& commonFrame = commonDeltaQuatTrack.m_frames2[0]; + auto& frame = deltaPart.DELTA_QUAT_2D_MEMBER->u.frame0; + + frame.value[0] = commonFrame.value[0]; + frame.value[1] = commonFrame.value[1]; + return; + } + + const auto numQuatIndices = commonDeltaQuatTrack.m_indices.size(); + const auto indicesArraySize = + useByteIndices ? numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_1) : numQuatIndices * sizeof(XAnimDynamicIndicesQuat::_2); + + deltaPart.DELTA_QUAT_2D_MEMBER = static_cast( + memory.AllocRaw(offsetof(DELTA_QUAT_2D_STRUCT, u) + offsetof(DELTA_QUAT_2D_FRAMES_STRUCT, indices) + indicesArraySize)); + + auto& frames = deltaPart.DELTA_QUAT_2D_MEMBER->u.frames; + ConvertIndices(frames.indices, commonDeltaQuatTrack.m_indices, useByteIndices); + + deltaPart.DELTA_QUAT_2D_MEMBER->size = static_cast(numQuatIndices - 1); + deltaPart.DELTA_QUAT_2D_MEMBER->u.frames.frames = memory.Alloc(numQuatIndices); + + for (auto quatIndexNum = 0u; quatIndexNum < numQuatIndices; ++quatIndexNum) + { + const auto& commonFrame = commonDeltaQuatTrack.m_frames2[quatIndexNum]; + auto& curFrame = deltaPart.DELTA_QUAT_2D_MEMBER->u.frames.frames[quatIndexNum]; + + curFrame.value[0] = commonFrame.value[0]; + curFrame.value[1] = commonFrame.value[1]; + } + } + + void ConvertCommonDeltaTransPart(XAnimDeltaPart& deltaPart, + const xanim::CommonDeltaTransTrack& commonDeltaTransTrack, + MemoryManager& memory, + const bool useByteIndices) + { + if (commonDeltaTransTrack.m_constant) + { + deltaPart.trans = static_cast(memory.AllocRaw(offsetof(XAnimPartTrans, u) + sizeof(XAnimPartTransData::frame0))); + deltaPart.trans->size = 0; + deltaPart.trans->u.frame0.x = (*commonDeltaTransTrack.m_constant)[0]; + deltaPart.trans->u.frame0.y = (*commonDeltaTransTrack.m_constant)[1]; + deltaPart.trans->u.frame0.z = (*commonDeltaTransTrack.m_constant)[2]; + return; + } + + const auto numTransIndices = commonDeltaTransTrack.m_indices.size(); + const auto indicesArraySize = + useByteIndices ? numTransIndices * sizeof(XAnimDynamicIndicesTrans::_1) : numTransIndices * sizeof(XAnimDynamicIndicesTrans::_2); + + deltaPart.trans = + static_cast(memory.AllocRaw(offsetof(XAnimPartTrans, u) + offsetof(XAnimPartTransFrames, indices) + indicesArraySize)); + + auto& frames = deltaPart.trans->u.frames; + ConvertIndices(frames.indices, commonDeltaTransTrack.m_indices, useByteIndices); + + deltaPart.trans->size = static_cast(numTransIndices - 1); + frames.mins.x = commonDeltaTransTrack.m_mins[0]; + frames.mins.y = commonDeltaTransTrack.m_mins[1]; + frames.mins.z = commonDeltaTransTrack.m_mins[2]; + frames.size.x = commonDeltaTransTrack.m_size[0]; + frames.size.y = commonDeltaTransTrack.m_size[1]; + frames.size.z = commonDeltaTransTrack.m_size[2]; + + if (commonDeltaTransTrack.m_frames_u16.empty()) + { + deltaPart.trans->smallTrans = 1; + + static_assert(sizeof(ByteVec) == sizeof(xanim::CommonVec3U8)); + frames.frames._1 = memory.Alloc(numTransIndices); + std::memcpy(frames.frames._1, commonDeltaTransTrack.m_frames_u8.data(), numTransIndices * sizeof(ByteVec)); + } + else + { + deltaPart.trans->smallTrans = 0; + + static_assert(sizeof(UShortVec) == sizeof(xanim::CommonVec3U16)); + frames.frames._2 = memory.Alloc(numTransIndices); + std::memcpy(frames.frames._2, commonDeltaTransTrack.m_frames_u16.data(), numTransIndices * sizeof(UShortVec)); + } + } + + std::expected ConvertCommonDeltaPart(XAnimDeltaPart& deltaPart, + const xanim::CommonXAnimDeltaTrack& commonXAnimDeltaTrack, + MemoryManager& memory, + const bool useByteIndices) + { +#ifdef HAS_DELTA_QUAT_3D + if (commonXAnimDeltaTrack.m_quat) + { + if (commonXAnimDeltaTrack.m_quat->Is3DTrack()) + ConvertCommonDeltaQuatPart(deltaPart, *commonXAnimDeltaTrack.m_quat, memory, useByteIndices); + else + ConvertCommonDeltaQuat2Part(deltaPart, *commonXAnimDeltaTrack.m_quat, memory, useByteIndices); + } +#else + if (commonXAnimDeltaTrack.m_quat) + { + if (commonXAnimDeltaTrack.m_quat->Is3DTrack()) + return std::unexpected("XAnim uses delta3D which is unsupported in this game"); + + ConvertCommonDeltaQuat2Part(deltaPart, *commonXAnimDeltaTrack.m_quat, memory, useByteIndices); + } +#endif + if (commonXAnimDeltaTrack.m_trans) + ConvertCommonDeltaTransPart(deltaPart, *commonXAnimDeltaTrack.m_trans, memory, useByteIndices); + + return {}; + } + + std::expected ConvertCommonXAnim(XAnimParts& parts, + const xanim::CommonXAnimParts& commonParts, + AssetRegistration& registration, + MemoryManager& memory, + ZoneScriptStrings& scriptStrings) + { + parts.numframes = static_cast(commonParts.m_num_frames); + if (commonParts.m_looped) + SET_IS_LOOPED(); +#if defined(FEATURE_T5) || defined(FEATURE_T6) + parts.bLeftHandGripIK = commonParts.m_left_hand_grip_ik; +#endif +#if defined(FEATURE_T5) + parts.bStreamable = commonParts.m_streamable; +#endif + parts.assetType = commonParts.m_asset_type; + parts.framerate = commonParts.m_frame_rate; + parts.frequency = parts.numframes > 0 ? parts.framerate / static_cast(parts.numframes) : 0; +#if defined(FEATURE_T5) || defined(FEATURE_T6) + parts.primedLength = commonParts.m_primed_length; +#endif + + const auto useByteIndices = parts.numframes < 256; + + if (commonParts.m_delta_track) + { + parts.deltaPart = memory.Alloc(); +#ifdef HAS_DELTA_QUAT_3D + if (commonParts.m_delta_track->m_quat && commonParts.m_delta_track->m_quat->Is3DTrack()) + SET_HAS_DELTA_3D(); + else + SET_HAS_DELTA(); +#else + SET_HAS_DELTA(); +#endif + auto result = ConvertCommonDeltaPart(*parts.deltaPart, *commonParts.m_delta_track, memory, useByteIndices); + if (!result.has_value()) + return std::unexpected(std::move(result).error()); + } + + parts.names = memory.Alloc(commonParts.m_bone_tracks.size()); + for (size_t boneIndex = 0; boneIndex < commonParts.m_bone_tracks.size(); ++boneIndex) + { + const auto nameScrString = scriptStrings.AddOrGetScriptString(commonParts.m_bone_tracks[boneIndex].m_name); + parts.names[boneIndex] = nameScrString; + registration.AddScriptString(nameScrString); + CountBoneTrackTypes(parts, commonParts.m_bone_tracks[boneIndex]); + } + parts.boneCount[PART_TYPE_ALL] = static_cast(commonParts.m_bone_tracks.size()); + + ConvertNoteTracks(parts, commonParts, registration, memory, scriptStrings); + + const auto flatData = xanim::CreateFlatDataFromCommonXAnim(commonParts); + ConvertFlatData(parts, flatData, memory); + + return {}; + } + + 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 maybeCommonParts = xanim::LoadCompiledXAnim(*file.m_stream); + if (!maybeCommonParts.has_value()) + { + con::error("Failed to load xanim \"{}\": {}", assetName, maybeCommonParts.error()); + return AssetCreationResult::Failure(); + } + const auto commonParts = std::move(maybeCommonParts).value(); + + auto* parts = m_memory.Alloc(); + parts->name = m_memory.Dup(assetName.c_str()); + + AssetRegistration registration(assetName, parts); + const auto conversionResult = ConvertCommonXAnim(*parts, *commonParts, registration, m_memory, m_script_strings); + if (!conversionResult.has_value()) + { + con::error("Failed to load xanim \"{}\": {}", assetName, conversionResult.error()); + return AssetCreationResult::Failure(); + } + + return AssetCreationResult::Success(context.AddAsset(std::move(registration))); + } + + MemoryManager& m_memory; + ISearchPath& m_search_path; + ZoneScriptStrings& m_script_strings; + }; +} // namespace + +#set METHOD_NAME "CreateLoader" + GAME + +namespace xanim +{ + std::unique_ptr> METHOD_NAME(MemoryManager& memory, ISearchPath& searchPath, Zone& zone) + { + return std::make_unique(memory, searchPath, zone.m_script_strings); + } +} // namespace xanim diff --git a/src/ObjLoading/XAnim/XAnimLoader.h.template b/src/ObjLoading/XAnim/XAnimLoader.h.template new file mode 100644 index 00000000..0d4073a7 --- /dev/null +++ b/src/ObjLoading/XAnim/XAnimLoader.h.template @@ -0,0 +1,25 @@ +#options GAME(IW3, IW4, IW5, T5, T6) + +#filename "Game/" + GAME + "/XAnim/XAnimLoader" + GAME + ".h" + +#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\"" + +// This file was templated. +// See XAnimLoader.h.template. +// Do not modify, changes will be lost. + +#pragma once + +#include "Asset/IAssetCreator.h" +#include GAME_HEADER +#include "SearchPath/ISearchPath.h" +#include "Utils/MemoryManager.h" + +#include + +#set METHOD_NAME "CreateLoader" + GAME + +namespace xanim +{ + std::unique_ptr> METHOD_NAME(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 b0b828c2..ee2bc480 100644 --- a/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp +++ b/src/ObjWriting/Game/IW3/ObjWriterIW3.cpp @@ -3,6 +3,7 @@ #include "Game/IW3/Image/ImageDumperIW3.h" #include "Game/IW3/Material/MaterialJsonDumperIW3.h" #include "Game/IW3/Techset/TechsetDumperIW3.h" +#include "Game/IW3/XAnim/XAnimDumperIW3.h" #include "Game/IW3/XModel/XModelDumperIW3.h" #include "LightDef/LightDefDumperIW3.h" #include "Localize/LocalizeDumperIW3.h" @@ -12,7 +13,6 @@ #include "Sound/LoadedSoundDumperIW3.h" #include "Sound/SndCurveDumperIW3.h" #include "StringTable/StringTableDumperIW3.h" -#include "XAnim/XAnimDumperIW3.h" using namespace IW3; diff --git a/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp b/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp deleted file mode 100644 index 2274940e..00000000 --- a/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp +++ /dev/null @@ -1,937 +0,0 @@ -#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 deleted file mode 100644 index 7b6b78d9..00000000 --- a/src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.h +++ /dev/null @@ -1,13 +0,0 @@ -#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/ObjWriting/Game/IW4/ObjWriterIW4.cpp b/src/ObjWriting/Game/IW4/ObjWriterIW4.cpp index de74377c..b31fab57 100644 --- a/src/ObjWriting/Game/IW4/ObjWriterIW4.cpp +++ b/src/ObjWriting/Game/IW4/ObjWriterIW4.cpp @@ -5,6 +5,7 @@ #include "Game/IW4/Techset/PixelShaderDumperIW4.h" #include "Game/IW4/Techset/TechsetDumperIW4.h" #include "Game/IW4/Techset/VertexShaderDumperIW4.h" +#include "Game/IW4/XAnim/XAnimDumperIW4.h" #include "Game/IW4/XModel/XModelDumperIW4.h" #include "Leaderboard/LeaderboardJsonDumperIW4.h" #include "LightDef/LightDefDumperIW4.h" @@ -30,7 +31,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context) { RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); - // REGISTER_DUMPER(AssetDumperXAnimParts) + RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); #ifdef EXPERIMENTAL_MATERIAL_COMPILATION diff --git a/src/ObjWriting/Game/IW5/ObjWriterIW5.cpp b/src/ObjWriting/Game/IW5/ObjWriterIW5.cpp index 544b75bb..9027f820 100644 --- a/src/ObjWriting/Game/IW5/ObjWriterIW5.cpp +++ b/src/ObjWriting/Game/IW5/ObjWriterIW5.cpp @@ -1,5 +1,6 @@ #include "ObjWriterIW5.h" +#include "Game/IW4/XAnim/XAnimDumperIW4.h" #include "Game/IW5/Image/ImageDumperIW5.h" #include "Game/IW5/Material/MaterialJsonDumperIW5.h" #include "Game/IW5/Techset/PixelShaderDumperIW5.h" @@ -27,8 +28,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context) { RegisterAssetDumper(std::make_unique()); // REGISTER_DUMPER(AssetDumperPhysCollmap) - // REGISTER_DUMPER(AssetDumperXAnimParts) - // REGISTER_DUMPER(AssetDumperXModelSurfs) + RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); diff --git a/src/ObjWriting/Game/T5/ObjWriterT5.cpp b/src/ObjWriting/Game/T5/ObjWriterT5.cpp index b0d348dc..4a3be588 100644 --- a/src/ObjWriting/Game/T5/ObjWriterT5.cpp +++ b/src/ObjWriting/Game/T5/ObjWriterT5.cpp @@ -3,6 +3,7 @@ #include "Game/T5/Image/ImageDumperT5.h" #include "Game/T5/Material/MaterialJsonDumperT5.h" #include "Game/T5/Techset/TechsetDumperT5.h" +#include "Game/T5/XAnim/XAnimDumperT5.h" #include "Game/T5/XModel/XModelDumperT5.h" #include "LightDef/LightDefDumperT5.h" #include "Localize/LocalizeDumperT5.h" @@ -17,7 +18,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context) RegisterAssetDumper(std::make_unique()); // REGISTER_DUMPER(AssetDumperPhysConstraints, m_phys_constraints) // REGISTER_DUMPER(AssetDumperDestructibleDef, m_destructible_def) - // REGISTER_DUMPER(AssetDumperXAnimParts, m_xanim_parts) + RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique( diff --git a/src/ObjWriting/Game/T6/ObjWriterT6.cpp b/src/ObjWriting/Game/T6/ObjWriterT6.cpp index f99ef780..52a30367 100644 --- a/src/ObjWriting/Game/T6/ObjWriterT6.cpp +++ b/src/ObjWriting/Game/T6/ObjWriterT6.cpp @@ -4,6 +4,7 @@ #include "Game/T6/Image/ImageDumperT6.h" #include "Game/T6/Material/MaterialJsonDumperT6.h" #include "Game/T6/Techset/TechsetDumperT6.h" +#include "Game/T6/XAnim/XAnimDumperT6.h" #include "Game/T6/XModel/XModelDumperT6.h" #include "Leaderboard/LeaderboardJsonDumperT6.h" #include "LightDef/LightDefDumperT6.h" @@ -33,7 +34,7 @@ void ObjWriter::RegisterAssetDumpers(AssetDumpingContext& context) RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); // REGISTER_DUMPER(AssetDumperDestructibleDef, m_destructible_def) - // REGISTER_DUMPER(AssetDumperXAnimParts, m_xanim_parts) + RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique()); RegisterAssetDumper(std::make_unique( diff --git a/src/ObjWriting/XAnim/CompiledXAnimWriter.cpp b/src/ObjWriting/XAnim/CompiledXAnimWriter.cpp new file mode 100644 index 00000000..44b0d939 --- /dev/null +++ b/src/ObjWriting/XAnim/CompiledXAnimWriter.cpp @@ -0,0 +1,555 @@ +#include "CompiledXAnimWriter.h" + +#include "Utils/Alignment.h" +#include "Utils/Logging/Log.h" +#include "Utils/StreamUtils.h" + +#include +#include + +using namespace xanim; + +namespace +{ + // 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; + }; + + uint8_t GetFlagsForVersion(const CompiledXAnimVersion version, const CommonXAnimParts& parts) + { + uint8_t flags = 0; + + const auto hasDelta3D = parts.m_delta_track && parts.m_delta_track->m_quat && parts.m_delta_track->m_quat->Is3DTrack(); + switch (version) + { + case CompiledXAnimVersion::VERSION_17: + if (parts.m_looped) + flags |= binary17::FLAG_LOOPED; + if (parts.m_delta_track) + flags |= binary17::FLAG_DELTA; + break; + + case CompiledXAnimVersion::VERSION_18: + if (parts.m_looped) + flags |= binary18::FLAG_LOOPED; + if (parts.m_delta_track) + flags |= hasDelta3D ? binary18::FLAG_DELTA_3D : binary18::FLAG_DELTA; + break; + + case CompiledXAnimVersion::VERSION_19: + { + const auto requiresT6Compatibility = hasDelta3D; + + if (requiresT6Compatibility) + flags |= binary19::FLAG_T6_COMPATIBILITY; + if (parts.m_looped) + flags |= binary19::FLAG_LOOPED; + if (parts.m_delta_track) + flags |= hasDelta3D ? binary19::FLAG_T6_DELTA_3D : binary19::FLAG_DELTA; + if (parts.m_left_hand_grip_ik) + flags |= requiresT6Compatibility ? binary19::FLAG_T6_LEFT_HAND_GRIP_IK : binary19::FLAG_T5_LEFT_HAND_GRIP_IK; + if (parts.m_streamable && !requiresT6Compatibility) + flags |= binary19::FLAG_T5_STREAMABLE; + } + break; + } + + return flags; + } + + [[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 CommonDeltaQuatTrack& quat, const uint16_t numLoopFrames, const bool useByteIndices) + { + const auto numQuatIndices = static_cast(quat.m_frames.size()); + assert(numQuatIndices > 0); + + stream::WriteValue(stream, numQuatIndices); + + const auto encodedDeltaQuatFrames = EncodeQuatFrames(quat.m_frames, false); + + if (numQuatIndices == 1) + { + assert(encodedDeltaQuatFrames.m_stored_values.size() == 3); + stream::WriteValue(stream, encodedDeltaQuatFrames.m_stored_values[0]); + stream::WriteValue(stream, encodedDeltaQuatFrames.m_stored_values[1]); + stream::WriteValue(stream, encodedDeltaQuatFrames.m_stored_values[2]); + } + else + { + assert(numQuatIndices > 1u); + assert(quat.m_indices.size() == numQuatIndices); + assert(encodedDeltaQuatFrames.m_stored_values.size() == numQuatIndices * 3); + + WriteIndicesIfNeeded(stream, quat.m_indices, numLoopFrames, useByteIndices); + for (const auto value : encodedDeltaQuatFrames.m_stored_values) + stream::WriteValue(stream, value); + } + } + + void WriteDeltaQuat2Track(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) + { + if (delta.m_quat && delta.m_quat->Is3DTrack()) + WriteDeltaQuatTrack(stream, *delta.m_quat, numLoopFrames, useByteIndices); + else + WriteDeltaQuat2Track(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_frames_u8.size() == frameCount); + + 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_frames_u8.data(), trans.m_frames_u8.size() * sizeof(CommonVec3U8)); + break; + } + + case TransType::FULL_TRANS: + { + const auto frameCount = trans.m_indices.size(); + assert(frameCount > 0uz); + assert(trans.m_frames_u16.size() == frameCount); + + 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)); + + stream::Write(stream, trans.m_frames_u16.data(), trans.m_frames_u16.size() * sizeof(CommonVec3U16)); + 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, CompiledXAnimVersion version) + { + 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 = GetFlagsForVersion(version, parts); + 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(version)); + // 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 (version == CompiledXAnimVersion::VERSION_19 && parts.m_streamable && (flags & binary19::FLAG_T6_COMPATIBILITY) == 0) + stream::WriteValue(stream, parts.m_primed_length); + + 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..7b985823 --- /dev/null +++ b/src/ObjWriting/XAnim/CompiledXAnimWriter.h @@ -0,0 +1,11 @@ +#pragma once + +#include "XAnim/BinaryXAnimCommon.h" +#include "XAnim/XAnimCommon.h" + +#include + +namespace xanim +{ + void WriteCompiledXAnim(std::ostream& stream, const CommonXAnimParts& parts, CompiledXAnimVersion version); +} diff --git a/src/ObjWriting/XAnim/FlatXAnimReader.cpp b/src/ObjWriting/XAnim/FlatXAnimReader.cpp new file mode 100644 index 00000000..8f638188 --- /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_frames_u8)::value_type) == sizeof(uint8_t) * 3u); + trans.m_frames_u8.resize(frameCount); + cursor.ReadRandomDataByte(trans.m_frames_u8.data(), frameCount * 3u); + } + + 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_frames_u16)::value_type) == sizeof(int16_t) * 3u); + trans.m_frames_u16.resize(frameCount); + cursor.ReadRandomDataShort(trans.m_frames_u16.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 diff --git a/src/ObjWriting/XAnim/XAnimDumper.cpp.template b/src/ObjWriting/XAnim/XAnimDumper.cpp.template new file mode 100644 index 00000000..f6f2cd55 --- /dev/null +++ b/src/ObjWriting/XAnim/XAnimDumper.cpp.template @@ -0,0 +1,359 @@ +#options GAME(IW3, IW4, IW5, T5, T6) + +#filename "Game/" + GAME + "/XAnim/XAnimDumper" + GAME + ".cpp" + +#set DUMPER_HEADER "\"XAnimDumper" + GAME + ".h\"" + +#if GAME == "IW3" +#define FEATURE_IW3 +#elif GAME == "IW4" +#define FEATURE_IW4 +#define HAS_DELTA_QUAT_3D +#elif GAME == "IW5" +#define FEATURE_IW5 +#define HAS_DELTA_QUAT_3D +#elif GAME == "T5" +#define FEATURE_T5 +#elif GAME == "T6" +#define FEATURE_T6 +#define HAS_DELTA_QUAT_3D +#endif + +#if defined(FEATURE_IW4) || defined(FEATURE_IW5) +#define IS_LOOPED static_cast(parts.flags & ANIM_LOOP) +#define HAS_DELTA static_cast(parts.flags & ANIM_DELTA) +#define HAS_DELTA_3D static_cast(parts.flags & ANIM_DELTA_3D) +#else +#define IS_LOOPED parts.bLoop +#define HAS_DELTA parts.bDelta +#define HAS_DELTA_3D parts.bDelta3D +#endif + +// This file was templated. +// See XAnimDumper.cpp.template. +// Do not modify, changes will be lost. + +#include DUMPER_HEADER + +#include "XAnim/CompiledXAnimWriter.h" +#include "XAnim/FlatXAnimReader.h" +#include "XAnim/XAnimCommon.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace xanim; +using namespace GAME; + +namespace +{ + [[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]; + } + + [[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; + } + + std::vector ConvertNotifies(const XAnimParts& parts, const XAssetInfo& assetInfo) + { + std::vector result; + if (!parts.notify || parts.notifyCount == 0) + return result; + + for (auto i = 0u; i < parts.notifyCount; i++) + { + const auto& notify = parts.notify[i]; + CommonXAnimNotifyInfo commonNotify; + + commonNotify.m_name = ResolveScriptString(notify.name, assetInfo); + commonNotify.m_time = notify.time; + + result.emplace_back(std::move(commonNotify)); + } + + return result; + } + +#ifdef HAS_DELTA_QUAT_3D +#define DELTA_QUAT_2D_STRUCT XAnimDeltaPartQuat2 +#else +#define DELTA_QUAT_2D_STRUCT XAnimDeltaPartQuat +#endif + +#ifdef HAS_DELTA_QUAT_3D + CommonDeltaQuatTrack ConvertDeltaQuatTrack(const XAnimDeltaPartQuat& deltaQuat, const bool useByteIndices, const uint16_t numLoopFrames) + { + CommonDeltaQuatTrack result; + if (deltaQuat.size > 0) + { + const auto frameCount = static_cast(deltaQuat.size) + 1u; + result.m_frames.reserve(frameCount); + result.m_indices.reserve(frameCount); + + for (size_t i = 0u; i < frameCount; i++) + { + result.m_frames.emplace_back(deltaQuat.u.frames.frames[i].value[0], + deltaQuat.u.frames.frames[i].value[1], + deltaQuat.u.frames.frames[i].value[2], + deltaQuat.u.frames.frames[i].value[3]); + } + + if (useByteIndices) + { + for (size_t i = 0u; i < frameCount; i++) + result.m_indices.emplace_back(deltaQuat.u.frames.indices._1[i]); + } + else + { + for (size_t i = 0u; i < frameCount; i++) + result.m_indices.emplace_back(deltaQuat.u.frames.indices._2[i]); + } + + assert(result.m_indices.size() <= numLoopFrames); + } + else + { + result.m_frames.emplace_back(deltaQuat.u.frame0.value[0], deltaQuat.u.frame0.value[1], deltaQuat.u.frame0.value[2], deltaQuat.u.frame0.value[3]); + } + + return result; + } +#endif + + CommonDeltaQuatTrack ConvertDeltaQuat2Track(const DELTA_QUAT_2D_STRUCT& deltaQuat, const bool useByteIndices, const uint16_t numLoopFrames) + { + CommonDeltaQuatTrack result; + if (deltaQuat.size > 0) + { + const auto frameCount = static_cast(deltaQuat.size) + 1u; + result.m_frames2.reserve(frameCount); + result.m_indices.reserve(frameCount); + + for (size_t i = 0u; i < frameCount; i++) + result.m_frames2.emplace_back(deltaQuat.u.frames.frames[i].value[0], deltaQuat.u.frames.frames[i].value[1]); + + if (useByteIndices) + { + for (size_t i = 0u; i < frameCount; i++) + result.m_indices.emplace_back(deltaQuat.u.frames.indices._1[i]); + } + else + { + for (size_t i = 0u; i < frameCount; i++) + result.m_indices.emplace_back(deltaQuat.u.frames.indices._2[i]); + } + + assert(result.m_indices.size() <= numLoopFrames); + } + else + { + result.m_frames2.emplace_back(deltaQuat.u.frame0.value[0], deltaQuat.u.frame0.value[1]); + } + + return result; + } + + CommonDeltaTransTrack ConvertDeltaTransTrack(const XAnimPartTrans& deltaTrans, const bool useByteIndices) + { + 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) + 1u; + result.m_indices.reserve(frameCount); + if (useByteIndices) + { + for (size_t i = 0u; i < frameCount; i++) + result.m_indices.emplace_back(static_cast(deltaTrans.u.frames.indices._1[i])); + } + else + { + for (size_t i = 0u; i < frameCount; i++) + result.m_indices.emplace_back(deltaTrans.u.frames.indices._2[i]); + } + + if (deltaTrans.smallTrans) + { + result.m_frames_u8.reserve(frameCount); + for (size_t i = 0u; 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 (size_t i = 0u; 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(); + +#ifdef HAS_DELTA_QUAT_3D + assert(static_cast(parts.deltaPart) == HAS_DELTA || HAS_DELTA_3D); +#else + assert(static_cast(parts.deltaPart) == HAS_DELTA); +#endif + + if (!parts.deltaPart) + return result; + +#ifdef HAS_DELTA_QUAT_3D + if (parts.deltaPart->quat2) + { + result->m_quat = ConvertDeltaQuat2Track(*parts.deltaPart->quat2, useByteIndices, numLoopFrames); + assert(!parts.deltaPart->quat); + } + else if (parts.deltaPart->quat) + { + result->m_quat = ConvertDeltaQuatTrack(*parts.deltaPart->quat, useByteIndices, numLoopFrames); + } +#else + if (parts.deltaPart->quat) + result->m_quat = ConvertDeltaQuat2Track(*parts.deltaPart->quat, useByteIndices, numLoopFrames); +#endif + + if (parts.deltaPart->trans) + result->m_trans = ConvertDeltaTransTrack(*parts.deltaPart->trans, useByteIndices); + + return result; + } +} // namespace + +#set CLASS_NAME "Dumper" + GAME + +namespace xanim +{ + void CLASS_NAME::DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) + { + const auto& parts = *asset.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("Failed to reconstruct bone tracks from XAnim {}: {}", parts.name, maybeBoneTracks.error()); + return; + } + + const auto assetFile = context.OpenAssetFile(GetCompiledFileNameForAssetName(asset.m_name)); + if (!assetFile) + return; + + CommonXAnimParts commonParts; + commonParts.m_num_frames = parts.numframes; + commonParts.m_looped = IS_LOOPED; +#if defined(FEATURE_T5) || defined(FEATURE_T6) + commonParts.m_left_hand_grip_ik = parts.bLeftHandGripIK; +#endif +#if defined(FEATURE_T5) + commonParts.m_streamable = parts.bStreamable; +#endif + commonParts.m_frame_rate = parts.framerate; +#if defined(FEATURE_T5) || defined(FEATURE_T6) + commonParts.m_primed_length = parts.primedLength; +#endif + 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); + + WriteCompiledXAnim( + *assetFile, + commonParts, +#if defined(FEATURE_IW3) + CompiledXAnimVersion::VERSION_17 +#elif defined(FEATURE_IW4) || defined(FEATURE_IW5) + // Make sure we dump as many anims as possible in an IW3 modtools compatible way + HAS_DELTA_3D ? CompiledXAnimVersion::VERSION_18 : CompiledXAnimVersion::VERSION_17 +#elif defined(FEATURE_T5) || defined(FEATURE_T6) + CompiledXAnimVersion::VERSION_19 +#endif + ); + } +} // namespace xanim diff --git a/src/ObjWriting/XAnim/XAnimDumper.h.template b/src/ObjWriting/XAnim/XAnimDumper.h.template new file mode 100644 index 00000000..40fb47dd --- /dev/null +++ b/src/ObjWriting/XAnim/XAnimDumper.h.template @@ -0,0 +1,25 @@ +#options GAME(IW3, IW4, IW5, T5, T6) + +#filename "Game/" + GAME + "/XAnim/XAnimDumper" + GAME + ".h" + +#set GAME_HEADER "\"Game/" + GAME + "/" + GAME + ".h\"" + +// This file was templated. +// See XAnimDumper.h.template. +// Do not modify, changes will be lost. + +#pragma once + +#include "Dumping/AbstractAssetDumper.h" +#include GAME_HEADER + +#set CLASS_NAME "Dumper" + GAME + +namespace xanim +{ + class CLASS_NAME final : public AbstractAssetDumper + { + protected: + void DumpAsset(AssetDumpingContext& context, const XAssetInfo& asset) override; + }; +} // namespace xanim diff --git a/test/SystemTests.lua b/test/SystemTests.lua index d7ebe7a3..36ec875d 100644 --- a/test/SystemTests.lua +++ b/test/SystemTests.lua @@ -3,6 +3,8 @@ SystemTests = {} function SystemTests:include(includes) if includes:handle(self:name()) then includedirs { + "%{wks.location}/src/ObjLoading", + "%{wks.location}/src/ObjWriting", path.join(TestFolder(), "SystemTests") } end diff --git a/test/SystemTests/Game/IW4/XAnim/test_anim b/test/SystemTests/Game/IW4/XAnim/test_anim new file mode 100644 index 00000000..44421d39 Binary files /dev/null and b/test/SystemTests/Game/IW4/XAnim/test_anim differ diff --git a/test/SystemTests/Game/IW4/XAnim/test_anim2 b/test/SystemTests/Game/IW4/XAnim/test_anim2 new file mode 100644 index 00000000..cf39355b Binary files /dev/null and b/test/SystemTests/Game/IW4/XAnim/test_anim2 differ diff --git a/test/SystemTests/Game/IW4/XAnimIW4.cpp b/test/SystemTests/Game/IW4/XAnimIW4.cpp new file mode 100644 index 00000000..e8ea1338 --- /dev/null +++ b/test/SystemTests/Game/IW4/XAnimIW4.cpp @@ -0,0 +1,71 @@ +#include "Game/IW4/XAnim/XAnimDumperIW4.h" +#include "Game/IW4/XAnim/XAnimLoaderIW4.h" +#include "OatTestPaths.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" +#include "ZoneLoading.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals; +namespace fs = std::filesystem; + +namespace +{ + TEST_CASE("XAnim Loading/Dumping (IW4)", "[iw4][system]") + { + MockSearchPath searchPath; + + const auto [animName] = GENERATE(Catch::Generators::table({ + {"test_anim"}, + {"test_anim2"}, + })); + + CAPTURE(animName); + + const auto filePath = oat::paths::GetTestDirectory() / std::format("SystemTests/Game/IW4/XAnim/{}", animName); + 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(std::format("xanim/{}", animName), std::string(data.get(), fileSize)); + + Zone zone("MockZone", 0, GameId::IW4, GamePlatform::PC); + AssetCreatorCollection creatorCollection(zone); + IgnoredAssetLookup ignoredAssetLookup; + MemoryManager memoryManager; + const auto loader = xanim::CreateLoaderIW4(memoryManager, searchPath, zone); + AssetCreationContext context(zone, &creatorCollection, &ignoredAssetLookup); + + const auto result = loader->CreateAsset(animName, context); + + REQUIRE(result.HasBeenSuccessful()); + const auto* assetInfo = reinterpret_cast*>(result.GetAssetInfo()); + const auto* parts = assetInfo->Asset(); + + REQUIRE(parts->name == animName); + REQUIRE(parts->numframes > 0); + + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + xanim::DumperIW4 dumper; + AssetDumpingContext dumpingContext(zone, "", mockOutput, mockObjPath, std::nullopt); + dumper.Dump(dumpingContext); + + const auto* outAnimFile = mockOutput.GetMockedFile(std::format("xanim/{}", animName)); + REQUIRE(outAnimFile != nullptr); + + REQUIRE(outAnimFile->m_data.size() == fileSize); + REQUIRE(memcmp(outAnimFile->m_data.data(), data.get(), fileSize) == 0); + } +} // namespace diff --git a/test/SystemTests/Game/IW5/XAnim/test_anim b/test/SystemTests/Game/IW5/XAnim/test_anim new file mode 100644 index 00000000..c26921a1 Binary files /dev/null and b/test/SystemTests/Game/IW5/XAnim/test_anim differ diff --git a/test/SystemTests/Game/IW5/XAnim/test_anim2 b/test/SystemTests/Game/IW5/XAnim/test_anim2 new file mode 100644 index 00000000..bc77b6a5 Binary files /dev/null and b/test/SystemTests/Game/IW5/XAnim/test_anim2 differ diff --git a/test/SystemTests/Game/IW5/XAnimIW5.cpp b/test/SystemTests/Game/IW5/XAnimIW5.cpp new file mode 100644 index 00000000..dce5786f --- /dev/null +++ b/test/SystemTests/Game/IW5/XAnimIW5.cpp @@ -0,0 +1,71 @@ +#include "Game/IW5/XAnim/XAnimDumperIW5.h" +#include "Game/IW5/XAnim/XAnimLoaderIW5.h" +#include "OatTestPaths.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" +#include "ZoneLoading.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals; +namespace fs = std::filesystem; + +namespace +{ + TEST_CASE("XAnim Loading/Dumping (IW5)", "[iw5][system]") + { + MockSearchPath searchPath; + + const auto [animName] = GENERATE(Catch::Generators::table({ + {"test_anim"}, + {"test_anim2"}, + })); + + CAPTURE(animName); + + const auto filePath = oat::paths::GetTestDirectory() / std::format("SystemTests/Game/IW5/XAnim/{}", animName); + 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(std::format("xanim/{}", animName), std::string(data.get(), fileSize)); + + Zone zone("MockZone", 0, GameId::IW5, GamePlatform::PC); + AssetCreatorCollection creatorCollection(zone); + IgnoredAssetLookup ignoredAssetLookup; + MemoryManager memoryManager; + const auto loader = xanim::CreateLoaderIW5(memoryManager, searchPath, zone); + AssetCreationContext context(zone, &creatorCollection, &ignoredAssetLookup); + + const auto result = loader->CreateAsset(animName, context); + + REQUIRE(result.HasBeenSuccessful()); + const auto* assetInfo = reinterpret_cast*>(result.GetAssetInfo()); + const auto* parts = assetInfo->Asset(); + + REQUIRE(parts->name == animName); + REQUIRE(parts->numframes > 0); + + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + xanim::DumperIW5 dumper; + AssetDumpingContext dumpingContext(zone, "", mockOutput, mockObjPath, std::nullopt); + dumper.Dump(dumpingContext); + + const auto* outAnimFile = mockOutput.GetMockedFile(std::format("xanim/{}", animName)); + REQUIRE(outAnimFile != nullptr); + + REQUIRE(outAnimFile->m_data.size() == fileSize); + REQUIRE(memcmp(outAnimFile->m_data.data(), data.get(), fileSize) == 0); + } +} // namespace diff --git a/test/SystemTests/Game/T5/XAnim/test_anim b/test/SystemTests/Game/T5/XAnim/test_anim new file mode 100644 index 00000000..01c5b3e3 Binary files /dev/null and b/test/SystemTests/Game/T5/XAnim/test_anim differ diff --git a/test/SystemTests/Game/T5/XAnimT5.cpp b/test/SystemTests/Game/T5/XAnimT5.cpp new file mode 100644 index 00000000..ce4e0dbb --- /dev/null +++ b/test/SystemTests/Game/T5/XAnimT5.cpp @@ -0,0 +1,62 @@ +#include "Game/T5/XAnim/XAnimDumperT5.h" +#include "Game/T5/XAnim/XAnimLoaderT5.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 (T5)", "[t5][system]") + { + MockSearchPath searchPath; + + const auto filePath = oat::paths::GetTestDirectory() / "SystemTests/Game/T5/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::T5, GamePlatform::PC); + AssetCreatorCollection creatorCollection(zone); + IgnoredAssetLookup ignoredAssetLookup; + MemoryManager memoryManager; + const auto loader = xanim::CreateLoaderT5(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::DumperT5 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 diff --git a/test/SystemTests/Game/T6/XAnim/test_anim b/test/SystemTests/Game/T6/XAnim/test_anim new file mode 100644 index 00000000..1849004b Binary files /dev/null and b/test/SystemTests/Game/T6/XAnim/test_anim differ diff --git a/test/SystemTests/Game/T6/XAnim/test_anim2 b/test/SystemTests/Game/T6/XAnim/test_anim2 new file mode 100644 index 00000000..5950178f Binary files /dev/null and b/test/SystemTests/Game/T6/XAnim/test_anim2 differ diff --git a/test/SystemTests/Game/T6/XAnimT6.cpp b/test/SystemTests/Game/T6/XAnimT6.cpp new file mode 100644 index 00000000..06ce4bad --- /dev/null +++ b/test/SystemTests/Game/T6/XAnimT6.cpp @@ -0,0 +1,71 @@ +#include "Game/T6/XAnim/XAnimDumperT6.h" +#include "Game/T6/XAnim/XAnimLoaderT6.h" +#include "OatTestPaths.h" +#include "SearchPath/MockOutputPath.h" +#include "SearchPath/MockSearchPath.h" +#include "ZoneLoading.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::literals; +namespace fs = std::filesystem; + +namespace +{ + TEST_CASE("XAnim Loading/Dumping (T6)", "[t6][system]") + { + MockSearchPath searchPath; + + const auto [animName] = GENERATE(Catch::Generators::table({ + {"test_anim"}, + {"test_anim2"}, + })); + + CAPTURE(animName); + + const auto filePath = oat::paths::GetTestDirectory() / std::format("SystemTests/Game/T6/XAnim/{}", animName); + 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(std::format("xanim/{}", animName), std::string(data.get(), fileSize)); + + Zone zone("MockZone", 0, GameId::T6, GamePlatform::PC); + AssetCreatorCollection creatorCollection(zone); + IgnoredAssetLookup ignoredAssetLookup; + MemoryManager memoryManager; + const auto loader = xanim::CreateLoaderT6(memoryManager, searchPath, zone); + AssetCreationContext context(zone, &creatorCollection, &ignoredAssetLookup); + + const auto result = loader->CreateAsset(animName, context); + + REQUIRE(result.HasBeenSuccessful()); + const auto* assetInfo = reinterpret_cast*>(result.GetAssetInfo()); + const auto* parts = assetInfo->Asset(); + + REQUIRE(parts->name == animName); + REQUIRE(parts->numframes > 0); + + MockSearchPath mockObjPath; + MockOutputPath mockOutput; + xanim::DumperT6 dumper; + AssetDumpingContext dumpingContext(zone, "", mockOutput, mockObjPath, std::nullopt); + dumper.Dump(dumpingContext); + + const auto* outAnimFile = mockOutput.GetMockedFile(std::format("xanim/{}", animName)); + REQUIRE(outAnimFile != nullptr); + + REQUIRE(outAnimFile->m_data.size() == fileSize); + REQUIRE(memcmp(outAnimFile->m_data.data(), data.get(), fileSize) == 0); + } +} // namespace