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
This commit is contained in:
Jan
2026-06-06 14:47:51 +00:00
committed by GitHub
parent e8d84c6d4d
commit 0d0f928267
44 changed files with 3388 additions and 1768 deletions
+1 -1
View File
@@ -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;
@@ -1,937 +0,0 @@
#include "XAnimDumperIW3.h"
#include "Utils/Alignment.h"
#include "Utils/StreamUtils.h"
#include "XAnim/XAnimCommon.h"
#include <array>
#include <cassert>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <expected>
#include <format>
#include <limits>
#include <optional>
#include <ostream>
#include <stdexcept>
#include <string>
#include <vector>
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<uint16_t> indices;
std::vector<int16_t> values;
};
struct TransTrack
{
TransType type = TransType::NO_TRANS;
std::vector<uint16_t> indices;
std::array<float, 3> mins{};
std::array<float, 3> size{};
std::vector<uint8_t> byteFrames;
std::vector<uint16_t> shortFrames;
std::array<float, 3> 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<uint16_t> indices;
std::vector<int16_t> values;
};
struct DeltaTransTrack
{
bool keyframed = false;
bool smallTrans = false;
std::vector<uint16_t> indices;
std::array<float, 3> mins{};
std::array<float, 3> size{};
std::vector<uint8_t> byteFrames;
std::vector<uint16_t> shortFrames;
std::array<float, 3> constant{};
};
struct DeltaTrack
{
std::optional<DeltaQuatTrack> quat;
std::optional<DeltaTransTrack> trans;
};
struct EncodedQuatTrack
{
bool flipQuat = false;
std::vector<int16_t> storedValues;
};
[[nodiscard]] const std::string& ResolveScriptString(const XAssetInfo<XAnimParts>& 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<uint16_t>::max());
// Raw non-looped xanims store numframes + 1 in keyed track counts/header fields.
return static_cast<uint16_t>(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<float, 3> ReadFloat3(const int*& dataInt)
{
std::array<float, 3> result{};
for (float& i : result)
i = IntBitsToFloat(*dataInt++);
return result;
}
template<typename T> [[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<uint16_t> ReadPackedIndices(FlatDataCursor& cursor, const uint16_t storedSize, const bool useByteIndices)
{
const auto count = static_cast<size_t>(storedSize) + 1uz;
std::vector<uint16_t> 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<uint16_t>(cursor.dataShort[i]);
cursor.dataShort += count;
return result;
}
[[nodiscard]] bool IsSequentialCoverage(const std::vector<uint16_t>& 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<int64_t>(lhs[i]) * static_cast<int64_t>(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<int>(frame[componentIndex]) * sign;
assert(value >= std::numeric_limits<int16_t>::min() && value <= std::numeric_limits<int16_t>::max());
result.storedValues.emplace_back(static_cast<int16_t>(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<XAnimParts>& asset, const char* field)
{
return std::format("IW3 xanim raw reconstruction cursor mismatch for asset \"{}\" in {}", asset.m_name, field);
}
[[nodiscard]] std::expected<std::vector<BoneTrack>, std::string> ReconstructBoneTracks(const XAssetInfo<XAnimParts>& asset)
{
const auto& parts = *asset.Asset();
const auto nameCount = static_cast<size_t>(parts.boneCount[PART_TYPE_ALL]);
const auto useByteIndices = UseByteIndices(parts);
std::vector<BoneTrack> 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<uint16_t>(*cursor.dataShort++);
const auto frameCount = static_cast<size_t>(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<uint16_t>(*cursor.dataShort++);
const auto frameCount = static_cast<size_t>(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<bool> transAssigned(nameCount, false);
for (auto i = 0u; i < parts.boneCount[PART_TYPE_SMALL_TRANS]; i++)
{
const auto bone = static_cast<size_t>(*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<uint16_t>(*cursor.dataShort++);
const auto frameCount = static_cast<size_t>(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<size_t>(*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<uint16_t>(*cursor.dataShort++);
const auto frameCount = static_cast<size_t>(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<uint16_t>(*cursor.randomDataShort++));
}
for (auto i = 0u; i < parts.boneCount[PART_TYPE_TRANS_NO_SIZE]; i++)
{
const auto bone = static_cast<size_t>(*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<size_t>(*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<bool>(parts.deltaPart) == static_cast<bool>(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<size_t>(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<uint8_t>(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<size_t>(trans->size) + 1uz;
result.trans->indices.reserve(frameCount);
if (useByteIndices)
{
for (auto i = 0uz; i < frameCount; i++)
result.trans->indices.emplace_back(static_cast<uint8_t>(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<uint16_t>& 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<uint8_t>::max());
const auto asByte = static_cast<uint8_t>(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<uint16_t>(0));
break;
}
case QuatType::HALF_QUAT_NO_SIZE:
{
assert(encodedQuat.storedValues.size() == 1uz);
stream::WriteValue(stream, static_cast<uint16_t>(1));
stream::WriteValue(stream, encodedQuat.storedValues[0]);
break;
}
case QuatType::FULL_QUAT_NO_SIZE:
{
assert(encodedQuat.storedValues.size() == 3uz);
stream::WriteValue(stream, static_cast<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(0));
break;
}
case TransType::TRANS_NO_SIZE:
{
stream::WriteValue(stream, static_cast<uint16_t>(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<uint16_t>(frameCount));
WriteIndicesIfNeeded(stream, trans.indices, numLoopFrames, useByteIndices);
constexpr auto smallTrans = static_cast<uint8_t>(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<uint16_t>(frameCount));
WriteIndicesIfNeeded(stream, trans.indices, numLoopFrames, useByteIndices);
constexpr auto smallTrans = static_cast<uint8_t>(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<uint16_t>(0));
}
else if (!delta.quat->keyframed)
{
assert(encodedDeltaQuat.storedValues.size() == 1uz);
stream::WriteValue(stream, static_cast<uint16_t>(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<uint16_t>(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<uint16_t>(0));
return;
}
if (!delta.trans->keyframed)
{
stream::WriteValue(stream, static_cast<uint16_t>(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<uint16_t>(frameCount));
WriteIndicesIfNeeded(stream, delta.trans->indices, numLoopFrames, useByteIndices);
const auto smallTrans = static_cast<uint8_t>(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<XAnimParts>& asset)
{
const auto& parts = *asset.Asset();
const auto notifyCount = static_cast<size_t>(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<uint8_t>(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<long>(std::lround(parts.notify[i].time * static_cast<float>(parts.numframes)));
assert(scaled >= 0 && scaled <= std::numeric_limits<uint16_t>::max());
frame = static_cast<uint16_t>(scaled);
}
stream::WriteValue(stream, frame);
}
}
} // namespace
namespace xanim
{
void DumperIW3::DumpAsset(AssetDumpingContext& context, const XAssetInfo<AssetXAnim::Type>& 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<EncodedQuatTrack> encodedBoneQuats;
encodedBoneQuats.reserve(boneTracks.size());
for (const auto& bone : boneTracks)
encodedBoneQuats.emplace_back(EncodeQuatTrack(bone.quat));
auto& stream = *assetFile;
const auto flags = static_cast<uint8_t>((parts->bLoop ? FLAG_LOOPED : 0u) | (parts->bDelta ? FLAG_DELTA : 0u));
const auto boneCount = static_cast<uint16_t>(parts->boneCount[PART_TYPE_ALL]);
const auto assetType = static_cast<uint8_t>(parts->assetType);
const auto framerate = static_cast<uint16_t>(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<uint16_t>(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<size_t>(boneTracks.size(), 8u) / 8u;
std::vector<uint8_t> flipQuat(bitmaskSize, 0);
std::vector<uint8_t> halfQuat(bitmaskSize, 0);
for (size_t i = 0u; i < boneTracks.size(); i++)
{
if (encodedBoneQuats[i].flipQuat)
flipQuat[i / 8u] |= static_cast<uint8_t>(1u << (i % 8u));
if (QuatTypeUsesHalf(boneTracks[i].quat.type))
halfQuat[i / 8u] |= static_cast<uint8_t>(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
@@ -1,13 +0,0 @@
#pragma once
#include "Dumping/AbstractAssetDumper.h"
#include "Game/IW3/IW3.h"
namespace xanim
{
class DumperIW3 final : public AbstractAssetDumper<IW3::AssetXAnim>
{
protected:
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<IW3::AssetXAnim::Type>& asset) override;
};
} // namespace xanim
+2 -1
View File
@@ -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<phys_preset::InfoStringDumperIW4>());
RegisterAssetDumper(std::make_unique<phys_collmap::DumperIW4>());
// REGISTER_DUMPER(AssetDumperXAnimParts)
RegisterAssetDumper(std::make_unique<xanim::DumperIW4>());
RegisterAssetDumper(std::make_unique<xmodel::DumperIW4>());
RegisterAssetDumper(std::make_unique<material::JsonDumperIW4>());
#ifdef EXPERIMENTAL_MATERIAL_COMPILATION
+2 -2
View File
@@ -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<phys_preset::InfoStringDumperIW5>());
// REGISTER_DUMPER(AssetDumperPhysCollmap)
// REGISTER_DUMPER(AssetDumperXAnimParts)
// REGISTER_DUMPER(AssetDumperXModelSurfs)
RegisterAssetDumper(std::make_unique<xanim::DumperIW4>());
RegisterAssetDumper(std::make_unique<xmodel::DumperIW5>());
RegisterAssetDumper(std::make_unique<material::JsonDumperIW5>());
RegisterAssetDumper(std::make_unique<techset::PixelShaderDumperIW5>());
+2 -1
View File
@@ -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<phys_preset::InfoStringDumperT5>());
// REGISTER_DUMPER(AssetDumperPhysConstraints, m_phys_constraints)
// REGISTER_DUMPER(AssetDumperDestructibleDef, m_destructible_def)
// REGISTER_DUMPER(AssetDumperXAnimParts, m_xanim_parts)
RegisterAssetDumper(std::make_unique<xanim::DumperT5>());
RegisterAssetDumper(std::make_unique<xmodel::DumperT5>());
RegisterAssetDumper(std::make_unique<material::JsonDumperT5>());
RegisterAssetDumper(std::make_unique<techset::DumperT5>(
+2 -1
View File
@@ -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<phys_preset::InfoStringDumperT6>());
RegisterAssetDumper(std::make_unique<phys_constraints::InfoStringDumperT6>());
// REGISTER_DUMPER(AssetDumperDestructibleDef, m_destructible_def)
// REGISTER_DUMPER(AssetDumperXAnimParts, m_xanim_parts)
RegisterAssetDumper(std::make_unique<xanim::DumperT6>());
RegisterAssetDumper(std::make_unique<xmodel::DumperT6>());
RegisterAssetDumper(std::make_unique<material::JsonDumperT6>());
RegisterAssetDumper(std::make_unique<techset::DumperT6>(
@@ -0,0 +1,555 @@
#include "CompiledXAnimWriter.h"
#include "Utils/Alignment.h"
#include "Utils/Logging/Log.h"
#include "Utils/StreamUtils.h"
#include <cassert>
#include <cmath>
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<int16_t> 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<uint16_t>::max());
// Raw non-looped xanims store numframes + 1 in keyed track counts/header fields.
return static_cast<uint16_t>(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<uint16_t>& 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<typename T>
concept XQuatOrXQuat2 = std::is_array_v<decltype(T::value)> && std::is_integral_v<std::remove_extent_t<decltype(T::value)>>;
template<XQuatOrXQuat2 T> [[nodiscard]] int64_t ComputeQuatDot(const T& lhs, const T& rhs)
{
int64_t result = 0;
for (auto i = 0uz; i < std::extent_v<decltype(lhs.value)>; i++)
result += static_cast<int64_t>(lhs.value[i]) * static_cast<int64_t>(rhs.value[i]);
return result;
}
template<XQuatOrXQuat2 T> [[nodiscard]] EncodedQuatTrack EncodeQuatFrames(const std::vector<T>& frames, const bool allowFlipQuat)
{
constexpr auto COMPONENT_COUNT = std::extent_v<decltype(T::value)>;
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<int>(frame.value[componentIndex]) * sign;
assert(value >= std::numeric_limits<int16_t>::min() && value <= std::numeric_limits<int16_t>::max());
result.m_stored_values.emplace_back(static_cast<int16_t>(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<uint16_t>& 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<uint8_t>::max());
const auto asByte = static_cast<uint8_t>(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<uint16_t>(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<uint16_t>(0));
return;
}
const auto numQuatIndices = static_cast<uint16_t>(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<uint16_t>(0));
return;
}
if (delta.m_trans->m_constant)
{
stream::WriteValue(stream, static_cast<uint16_t>(1));
for (const auto value : *delta.m_trans->m_constant)
stream::WriteValue(stream, value);
return;
}
const auto numTransIndices = static_cast<uint16_t>(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<uint8_t>(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<uint16_t>(0));
break;
}
case QuatType::HALF_QUAT_NO_SIZE:
{
assert(encodedQuat.m_stored_values.size() == 1uz);
stream::WriteValue(stream, static_cast<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(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<uint16_t>(0));
break;
}
case TransType::TRANS_NO_SIZE:
{
stream::WriteValue(stream, static_cast<uint16_t>(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<uint16_t>(frameCount));
WriteIndicesIfNeeded(stream, trans.m_indices, numLoopFrames, useByteIndices);
constexpr auto smallTrans = static_cast<uint8_t>(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<uint16_t>(frameCount));
WriteIndicesIfNeeded(stream, trans.m_indices, numLoopFrames, useByteIndices);
constexpr auto smallTrans = static_cast<uint8_t>(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<uint8_t>(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<long>(std::lround(notify.m_time * static_cast<float>(parts.m_num_frames)));
assert(scaled >= 0 && scaled <= std::numeric_limits<uint16_t>::max());
frame = static_cast<uint16_t>(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<EncodedQuatTrack> 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<uint16_t>(parts.m_bone_tracks.size());
const auto assetType = static_cast<uint8_t>(parts.m_asset_type);
const auto framerate = static_cast<uint16_t>(std::lround(parts.m_frame_rate));
stream::WriteValue(stream, static_cast<uint16_t>(version));
// Looped raws store numframes directly; non-looped raws store numframes + 1.
stream::WriteValue(stream, static_cast<uint16_t>(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<size_t>(boneCount, 8u) / 8u;
std::vector<uint8_t> flipQuat(bitmaskSize, 0);
std::vector<uint8_t> halfQuat(bitmaskSize, 0);
for (auto i = 0u; i < boneCount; i++)
{
if (encodedBoneQuats[i].m_flip_quat)
flipQuat[i / 8u] |= static_cast<uint8_t>(1u << (i % 8u));
if (QuatTypeUsesHalf(parts.m_bone_tracks[i].m_quat.m_type))
halfQuat[i / 8u] |= static_cast<uint8_t>(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
@@ -0,0 +1,11 @@
#pragma once
#include "XAnim/BinaryXAnimCommon.h"
#include "XAnim/XAnimCommon.h"
#include <ostream>
namespace xanim
{
void WriteCompiledXAnim(std::ostream& stream, const CommonXAnimParts& parts, CompiledXAnimVersion version);
}
+405
View File
@@ -0,0 +1,405 @@
#include "FlatXAnimReader.h"
#include <cassert>
#include <cstring>
#include <format>
#include <utility>
using namespace xanim;
namespace
{
[[nodiscard]] std::vector<uint16_t> ReadPackedIndices(FlatXAnimReadCursor& cursor, const uint16_t storedSize, const bool useByteIndices)
{
const auto count = static_cast<size_t>(storedSize) + 1uz;
std::vector<uint16_t> 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<float, 3> ReadFloat3(FlatXAnimReadCursor& cursor)
{
std::array<float, 3> 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<void, std::string> 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::vector<BoneTrack>, std::string> CreateBoneTracksFromFlatData(std::vector<std::string> boneNames,
const XAnimBoneCounts& boneCounts,
FlatXAnimReadCursor& cursor,
const bool useByteIndices)
{
const auto boneCount = boneNames.size();
std::vector<BoneTrack> 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<uint16_t>(cursor.PopDataShort());
const auto frameCount = static_cast<size_t>(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<uint16_t>(cursor.PopDataShort());
const auto frameCount = static_cast<size_t>(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<bool> transAssigned(boneCount, false);
for (auto i = 0u; i < boneCounts.GetCountForTransType(TransType::SMALL_TRANS); i++)
{
const auto bone = static_cast<size_t>(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<uint16_t>(cursor.PopDataShort());
const auto frameCount = static_cast<size_t>(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<size_t>(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<uint16_t>(cursor.PopDataShort());
const auto frameCount = static_cast<size_t>(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<size_t>(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<size_t>(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
+94
View File
@@ -0,0 +1,94 @@
#pragma once
#include "XAnim/XAnimCommon.h"
#include <cstdint>
#include <exception>
#include <expected>
#include <string>
#include <vector>
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<size_t, 9> 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<void, std::string> 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::vector<BoneTrack>, std::string>
CreateBoneTracksFromFlatData(std::vector<std::string> boneNames, const XAnimBoneCounts& boneCounts, FlatXAnimReadCursor& cursor, bool useByteIndices);
} // namespace xanim
@@ -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<bool>(parts.flags & ANIM_LOOP)
#define HAS_DELTA static_cast<bool>(parts.flags & ANIM_DELTA)
#define HAS_DELTA_3D static_cast<bool>(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 <array>
#include <cassert>
#include <cstdint>
#include <expected>
#include <format>
#include <limits>
#include <optional>
#include <string>
#include <vector>
using namespace xanim;
using namespace GAME;
namespace
{
[[nodiscard]] const std::string& ResolveScriptString(const ScriptString value, const XAssetInfo<XAnimParts>& 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<uint16_t>::max());
// Raw non-looped xanims store numframes + 1 in keyed track counts/header fields.
return static_cast<uint16_t>(parts.numframes + 1u);
}
[[nodiscard]] bool UseByteIndices(const XAnimParts& parts)
{
return parts.numframes < 256;
}
std::vector<CommonXAnimNotifyInfo> ConvertNotifies(const XAnimParts& parts, const XAssetInfo<AssetXAnim::Type>& assetInfo)
{
std::vector<CommonXAnimNotifyInfo> 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<size_t>(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<size_t>(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<size_t>(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<uint8_t>(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<float, 3>({
deltaTrans.u.frame0.v[0],
deltaTrans.u.frame0.v[1],
deltaTrans.u.frame0.v[2],
});
}
return result;
}
std::unique_ptr<CommonXAnimDeltaTrack> ConvertDeltaTrack(const XAnimParts& parts, const bool useByteIndices, const uint16_t numLoopFrames)
{
if (!parts.deltaPart)
return nullptr;
auto result = std::make_unique<CommonXAnimDeltaTrack>();
#ifdef HAS_DELTA_QUAT_3D
assert(static_cast<bool>(parts.deltaPart) == HAS_DELTA || HAS_DELTA_3D);
#else
assert(static_cast<bool>(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<AssetXAnim::Type>& asset)
{
const auto& parts = *asset.Asset();
const auto useByteIndices = UseByteIndices(parts);
const auto numLoopFrames = GetNumLoopFrames(parts);
std::vector<std::string> 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
@@ -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<GAME::AssetXAnim>
{
protected:
void DumpAsset(AssetDumpingContext& context, const XAssetInfo<GAME::AssetXAnim::Type>& asset) override;
};
} // namespace xanim