From 0d0f92826771ddb7f18948ed24964e6ec6e5d190 Mon Sep 17 00:00:00 2001 From: Jan Date: Sat, 6 Jun 2026 16:47:51 +0200 Subject: [PATCH] feat: add binary xanim support for remaining games (#818) * refactor: use generic loader for iw3 xanims * refactor: use generic dumper for iw3 xanims * chore: use templating on XAnimDumper * chore: use templating on XAnimLoader * feat: dump xanims for T5 * feat: load binary t5 xanims * feat: load and dump t6 xanims * feat: load and dump iw4,iw5 xanims * chore: make sure iw3 and t5 notify about unsupported delta3D * chore: also use CommonVec3U8 and CommonVec3U16 for non delta trans track --- src/Common/Game/IW4/IW4_Assets.h | 9 +- src/Common/Game/IW5/IW5_Assets.h | 7 + src/Common/Game/T6/T6_Assets.h | 12 +- src/ObjCommon/XAnim/BinaryXAnimCommon.h | 50 + src/ObjCommon/XAnim/XAnimCommon.cpp | 137 +++ src/ObjCommon/XAnim/XAnimCommon.h | 157 ++- src/ObjLoading/Game/IW3/ObjLoaderIW3.cpp | 2 +- .../Game/IW3/XAnim/XAnimLoaderIW3.cpp | 781 --------------- .../Game/IW3/XAnim/XAnimLoaderIW3.h | 13 - src/ObjLoading/Game/IW4/ObjLoaderIW4.cpp | 4 +- src/ObjLoading/Game/IW5/ObjLoaderIW5.cpp | 4 +- src/ObjLoading/Game/T5/ObjLoaderT5.cpp | 3 +- src/ObjLoading/Game/T6/ObjLoaderT6.cpp | 3 +- src/ObjLoading/XAnim/CompiledXAnimLoader.cpp | 546 ++++++++++ src/ObjLoading/XAnim/CompiledXAnimLoader.h | 12 + src/ObjLoading/XAnim/FlatXAnimDataWriter.cpp | 188 ++++ src/ObjLoading/XAnim/FlatXAnimDataWriter.h | 22 + src/ObjLoading/XAnim/XAnimLoader.cpp.template | 490 +++++++++ src/ObjLoading/XAnim/XAnimLoader.h.template | 25 + src/ObjWriting/Game/IW3/ObjWriterIW3.cpp | 2 +- .../Game/IW3/XAnim/XAnimDumperIW3.cpp | 937 ------------------ .../Game/IW3/XAnim/XAnimDumperIW3.h | 13 - src/ObjWriting/Game/IW4/ObjWriterIW4.cpp | 3 +- src/ObjWriting/Game/IW5/ObjWriterIW5.cpp | 4 +- src/ObjWriting/Game/T5/ObjWriterT5.cpp | 3 +- src/ObjWriting/Game/T6/ObjWriterT6.cpp | 3 +- src/ObjWriting/XAnim/CompiledXAnimWriter.cpp | 555 +++++++++++ src/ObjWriting/XAnim/CompiledXAnimWriter.h | 11 + src/ObjWriting/XAnim/FlatXAnimReader.cpp | 405 ++++++++ src/ObjWriting/XAnim/FlatXAnimReader.h | 94 ++ src/ObjWriting/XAnim/XAnimDumper.cpp.template | 359 +++++++ src/ObjWriting/XAnim/XAnimDumper.h.template | 25 + test/SystemTests.lua | 2 + test/SystemTests/Game/IW4/XAnim/test_anim | Bin 0 -> 5000 bytes test/SystemTests/Game/IW4/XAnim/test_anim2 | Bin 0 -> 9292 bytes test/SystemTests/Game/IW4/XAnimIW4.cpp | 71 ++ test/SystemTests/Game/IW5/XAnim/test_anim | Bin 0 -> 7954 bytes test/SystemTests/Game/IW5/XAnim/test_anim2 | Bin 0 -> 9542 bytes test/SystemTests/Game/IW5/XAnimIW5.cpp | 71 ++ test/SystemTests/Game/T5/XAnim/test_anim | Bin 0 -> 16143 bytes test/SystemTests/Game/T5/XAnimT5.cpp | 62 ++ test/SystemTests/Game/T6/XAnim/test_anim | Bin 0 -> 6639 bytes test/SystemTests/Game/T6/XAnim/test_anim2 | Bin 0 -> 8126 bytes test/SystemTests/Game/T6/XAnimT6.cpp | 71 ++ 44 files changed, 3388 insertions(+), 1768 deletions(-) create mode 100644 src/ObjCommon/XAnim/BinaryXAnimCommon.h delete mode 100644 src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.cpp delete mode 100644 src/ObjLoading/Game/IW3/XAnim/XAnimLoaderIW3.h create mode 100644 src/ObjLoading/XAnim/CompiledXAnimLoader.cpp create mode 100644 src/ObjLoading/XAnim/CompiledXAnimLoader.h create mode 100644 src/ObjLoading/XAnim/FlatXAnimDataWriter.cpp create mode 100644 src/ObjLoading/XAnim/FlatXAnimDataWriter.h create mode 100644 src/ObjLoading/XAnim/XAnimLoader.cpp.template create mode 100644 src/ObjLoading/XAnim/XAnimLoader.h.template delete mode 100644 src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.cpp delete mode 100644 src/ObjWriting/Game/IW3/XAnim/XAnimDumperIW3.h create mode 100644 src/ObjWriting/XAnim/CompiledXAnimWriter.cpp create mode 100644 src/ObjWriting/XAnim/CompiledXAnimWriter.h create mode 100644 src/ObjWriting/XAnim/FlatXAnimReader.cpp create mode 100644 src/ObjWriting/XAnim/FlatXAnimReader.h create mode 100644 src/ObjWriting/XAnim/XAnimDumper.cpp.template create mode 100644 src/ObjWriting/XAnim/XAnimDumper.h.template create mode 100644 test/SystemTests/Game/IW4/XAnim/test_anim create mode 100644 test/SystemTests/Game/IW4/XAnim/test_anim2 create mode 100644 test/SystemTests/Game/IW4/XAnimIW4.cpp create mode 100644 test/SystemTests/Game/IW5/XAnim/test_anim create mode 100644 test/SystemTests/Game/IW5/XAnim/test_anim2 create mode 100644 test/SystemTests/Game/IW5/XAnimIW5.cpp create mode 100644 test/SystemTests/Game/T5/XAnim/test_anim create mode 100644 test/SystemTests/Game/T5/XAnimT5.cpp create mode 100644 test/SystemTests/Game/T6/XAnim/test_anim create mode 100644 test/SystemTests/Game/T6/XAnim/test_anim2 create mode 100644 test/SystemTests/Game/T6/XAnimT6.cpp 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 0000000000000000000000000000000000000000..44421d39db3fc08d0832fbf9250eb7345aef638d GIT binary patch literal 5000 zcmZ9QdmvTW9>>>mD2_auCJ|FnBa}Gr<8aR2XKv^*nVK4;7bUs9!yRVoPG%aD>Q<)o zrjinkM2epFK9QR8N>V~Ns76W;d7OiN*WS}A_xj^|*7^N@>$BGH_gnjM3#|KJj0vhdTUg_teGY$0X~F z1d}D0EWu<6CQC3`g2@t0mSVCLlcks}#bhZaOEFoB$x=*~VzLyIWtc3(WEm#QFj
4*#nb3Fxdl>Juuk= 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c26921a12cdb22dc8320f25fdbaf7f528df2b659 GIT binary patch literal 7954 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 IjfcPf5A8U!2mk;8 literal 0 HcmV?d00001 diff --git a/test/SystemTests/Game/IW5/XAnim/test_anim2 b/test/SystemTests/Game/IW5/XAnim/test_anim2 new file mode 100644 index 0000000000000000000000000000000000000000..bc77b6a5f1b52b8f7469b8d9559797cb3bcee633 GIT binary patch literal 9542 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..01c5b3e322e7a6bfd08369ba4fb0377249330aa1 GIT binary patch literal 16143 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 H{PuqUgRq$U literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..1849004b9c530f174c4e9b9264adf23244a52352 GIT binary patch literal 6639 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 literal 0 HcmV?d00001 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