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/XAnim/CompiledXAnimLoader.cpp b/src/ObjLoading/XAnim/CompiledXAnimLoader.cpp index 822adb79..ea823eba 100644 --- a/src/ObjLoading/XAnim/CompiledXAnimLoader.cpp +++ b/src/ObjLoading/XAnim/CompiledXAnimLoader.cpp @@ -14,9 +14,6 @@ using namespace xanim; namespace { - constexpr uint8_t FLAG_LOOPED = 1u; - constexpr uint8_t FLAG_DELTA = 2u; - // The linker decodes raw trans size[] with these exact float literals. // They correspond to 1.0f / 255.0f and 1.0f / 65535.0f, but we keep the // decompiled values to preserve binary-stable round trips. @@ -30,6 +27,8 @@ namespace { case CompiledXAnimVersion::VERSION_17: return CompiledXAnimVersion::VERSION_17; + case CompiledXAnimVersion::VERSION_19: + return CompiledXAnimVersion::VERSION_19; default: return std::unexpected(std::format("Version {} is not supported", fileVersion)); } @@ -347,6 +346,54 @@ namespace // 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_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_19: + return (flags & binary19::FLAG_DELTA) > 0; + } + + return false; + } + + bool IsLeftHandGripIk(const uint8_t flags, const CompiledXAnimVersion version) + { + switch (version) + { + case CompiledXAnimVersion::VERSION_19: + return (flags & binary19::FLAG_LEFT_HAND_GRIP_IK) > 0; + default: + return false; + } + } + + bool IsStreamable(const uint8_t flags, const CompiledXAnimVersion version) + { + switch (version) + { + case CompiledXAnimVersion::VERSION_19: + return (flags & binary19::FLAG_STREAMABLE) > 0; + default: + return false; + } + } } // namespace namespace xanim @@ -360,7 +407,6 @@ namespace xanim auto parts = std::make_unique(); const auto version = maybeVersion.value(); - assert(version == CompiledXAnimVersion::VERSION_17); const auto numFrames = stream::ReadValue(stream); const auto boneCount = stream::ReadValue(stream); @@ -370,15 +416,22 @@ namespace xanim if (stream.fail()) return std::unexpected("Truncated file"); - const bool isLooped = flags & FLAG_LOOPED; - const bool hasDelta = flags & FLAG_DELTA; + const bool isLooped = IsLooped(flags, version); + const bool hasDelta = HasDelta(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) diff --git a/src/ObjLoading/XAnim/XAnimLoader.cpp.template b/src/ObjLoading/XAnim/XAnimLoader.cpp.template index 76ddc0ea..463e2071 100644 --- a/src/ObjLoading/XAnim/XAnimLoader.cpp.template +++ b/src/ObjLoading/XAnim/XAnimLoader.cpp.template @@ -1,4 +1,4 @@ -#options GAME(IW3) +#options GAME(IW3, T5) #filename "Game/" + GAME + "/XAnim/XAnimLoader" + GAME + ".cpp" @@ -70,6 +70,18 @@ namespace notify.time = commonNotify.m_time; } + +#ifdef FEATURE_T5 + 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) @@ -279,9 +291,16 @@ namespace { parts.numframes = static_cast(commonParts.m_num_frames); parts.bLoop = commonParts.m_looped; +#ifdef FEATURE_T5 + parts.bLeftHandGripIK = commonParts.m_left_hand_grip_ik; + 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; +#ifdef FEATURE_T5 + parts.primedLength = commonParts.m_primed_length; +#endif const auto useByteIndices = parts.numframes < 256; diff --git a/src/ObjLoading/XAnim/XAnimLoader.h.template b/src/ObjLoading/XAnim/XAnimLoader.h.template index 27df1bb4..79c6230e 100644 --- a/src/ObjLoading/XAnim/XAnimLoader.h.template +++ b/src/ObjLoading/XAnim/XAnimLoader.h.template @@ -1,4 +1,4 @@ -#options GAME(IW3) +#options GAME(IW3, T5) #filename "Game/" + GAME + "/XAnim/XAnimLoader" + GAME + ".h" diff --git a/test/SystemTests/Game/T5/XAnim/test_anim b/test/SystemTests/Game/T5/XAnim/test_anim new file mode 100644 index 00000000..01c5b3e3 Binary files /dev/null and b/test/SystemTests/Game/T5/XAnim/test_anim differ diff --git a/test/SystemTests/Game/T5/XAnimT5.cpp b/test/SystemTests/Game/T5/XAnimT5.cpp new file mode 100644 index 00000000..2f5c5763 --- /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::IW3, 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