diff --git a/include/mdlpp/structs/Generic.h b/include/mdlpp/structs/Generic.h index c8f5c127e..bf30c7dc2 100644 --- a/include/mdlpp/structs/Generic.h +++ b/include/mdlpp/structs/Generic.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -25,8 +26,22 @@ struct BBox { struct Movement { enum Flags : int32_t { - FLAG_NONE = 0, - // todo(flags): Movement + FLAG_NONE = 0, + FLAG_X = 1 << 0, + FLAG_Y = 1 << 1, + FLAG_Z = 1 << 2, + FLAG_XR = 1 << 3, + FLAG_YR = 1 << 4, + FLAG_ZR = 1 << 5, + FLAG_LX = 1 << 6, + FLAG_LY = 1 << 7, + FLAG_LZ = 1 << 8, + FLAG_LXR = 1 << 9, + FLAG_LYR = 1 << 10, + FLAG_LZR = 1 << 11, + FLAG_LINEAR = 1 << 12, + FLAG_TYPES = FLAG_X | FLAG_Y | FLAG_Z | FLAG_XR | FLAG_YR | FLAG_ZR | FLAG_LX | FLAG_LY | FLAG_LZ | FLAG_LX | FLAG_LYR | FLAG_LZR | FLAG_LINEAR, + FLAG_RLOOP = 1 << 18, }; int32_t endFrame; @@ -39,4 +54,25 @@ struct Movement { }; SOURCEPP_BITFLAGS_ENUM(Movement::Flags) +struct IKLock { + int32_t chain; + float posWeight; + float localQWeight; + int32_t flags; + + //int32_t unused[4]; +}; + +union AnimValue { + struct { + uint8_t valid; + uint8_t total; + } num; + int16_t value; +}; +static_assert(sizeof(AnimValue) == 2); + +// x/y/z or pitch/yaw/roll +using AnimValuePtr = sourcepp::math::Vec3i16; + } // namespace mdlpp diff --git a/include/mdlpp/structs/MDL.h b/include/mdlpp/structs/MDL.h index cbad48d95..f1a0f01b5 100644 --- a/include/mdlpp/structs/MDL.h +++ b/include/mdlpp/structs/MDL.h @@ -3,17 +3,84 @@ #include #include #include +#include #include +#include #include #include "Generic.h" namespace mdlpp::MDL { +// Header2 structs +struct SrcBoneTransform { + //int32_t nameIndex; + std::string name; + + sourcepp::math::Mat3x4f pretransform; + sourcepp::math::Mat3x4f posttransform; +}; + +struct LinearBone { + int32_t boneCount; + + std::vector flags; + std::vector parent; + std::vector position; + std::vector quaternion; + std::vector rotation; + std::vector poseToBone; + std::vector positionScale; + std::vector rotationScale; + std::vector quaternionAlignment; +}; + +struct BoneFlexDriverControl { + int32_t boneComponent; + int32_t flexControllerIndex; + float min; + float max; +}; + +struct BoneFlexDriver { + int32_t boneIndex; + + //int32_t controlCount; + //int32_t controlIndex; + std::vector controls; + + //int32_t unused[3]; +}; + +// Bone structs struct Bone { enum Flags : int32_t { - FLAG_NONE = 0, - // todo(flags): Bone + FLAG_NONE = 0, + FLAG_PHYSICALLY_SIMULATED = 1 << 0, + FLAG_PHYSICS_PROCEDURAL = 1 << 1, + FLAG_ALWAYS_PROCEDURAL = 1 << 2, + FLAG_SCREEN_ALIGN_SPHERE = 1 << 3, + FLAG_SCREEN_ALIGN_CYLINDER = 1 << 4, + FLAG_CALCULATE_MASK = FLAG_PHYSICALLY_SIMULATED | FLAG_PHYSICS_PROCEDURAL | FLAG_ALWAYS_PROCEDURAL | FLAG_SCREEN_ALIGN_SPHERE | FLAG_SCREEN_ALIGN_CYLINDER, + FLAG_USED_BY_HITBOX = 1 << 8, + FLAG_USED_BY_ATTACHMENT = 1 << 9, + FLAG_USED_BY_VERTEX_LOD0 = 1 << 10, + FLAG_USED_BY_VERTEX_LOD1 = 1 << 11, + FLAG_USED_BY_VERTEX_LOD2 = 1 << 12, + FLAG_USED_BY_VERTEX_LOD3 = 1 << 13, + FLAG_USED_BY_VERTEX_LOD4 = 1 << 14, + FLAG_USED_BY_VERTEX_LOD5 = 1 << 15, + FLAG_USED_BY_VERTEX_LOD6 = 1 << 16, + FLAG_USED_BY_VERTEX_LOD7 = 1 << 17, + FLAG_USED_BY_BONE_MERGE = 1 << 18, + FLAG_USED_BY_VERTEX_MASK = FLAG_USED_BY_VERTEX_LOD0 | FLAG_USED_BY_VERTEX_LOD1 | FLAG_USED_BY_VERTEX_LOD2 | FLAG_USED_BY_VERTEX_LOD3 | FLAG_USED_BY_VERTEX_LOD4 | FLAG_USED_BY_VERTEX_LOD5 | FLAG_USED_BY_VERTEX_LOD6 | FLAG_USED_BY_VERTEX_LOD7, + FLAG_USED_MASK = FLAG_USED_BY_HITBOX | FLAG_USED_BY_ATTACHMENT | FLAG_USED_BY_VERTEX_MASK | FLAG_USED_BY_BONE_MERGE, + FLAG_USED_BY_ANYTHING = FLAG_USED_MASK, + FLAG_FIXED_ALIGNMENT = 1 << 20, + FLAG_HAS_SAVEFRAME_POS = 1 << 21, + FLAG_HAS_SAVEFRAME_ROT = 1 << 22, + FLAG_EMPTY_SLOT = 1 << 23, + FLAG_TYPE_MASK = FLAG_FIXED_ALIGNMENT | FLAG_HAS_SAVEFRAME_POS | FLAG_HAS_SAVEFRAME_ROT | FLAG_EMPTY_SLOT, }; //int32_t nameIndex; @@ -53,6 +120,7 @@ struct BoneController { //int32_t _unused0[8]; }; +// Hitbox structs struct HitboxSet { //int32_t nameIndex; std::string name; @@ -62,7 +130,104 @@ struct HitboxSet { std::vector hitboxes; }; -/* +// Animation structs +struct AnimBoneData { + enum Flags : uint8_t { + FLAG_NONE = 0, + FLAG_RAW_POS = 1 << 0, + FLAG_RAW_ROT = 1 << 1, + FLAG_ANIM_POS = 1 << 2, + FLAG_ANIM_ROT = 1 << 3, + FLAG_DELTA = 1 << 4, + FLAG_RAW_ROT2 = 1 << 5, + }; + + uint8_t bone; + Flags flags; + + // static data when RAW flags are set + std::variant staticRotation; + std::optional staticPosition; + + // keyframe data when ANIM flags are set + std::optional animRotationPtr; + std::optional animPositionPtr; + std::vector animRotationData; // Keyframe arrays + std::vector animPositionData; // Keyframe arrays +}; +SOURCEPP_BITFLAGS_ENUM(AnimBoneData::Flags) + +struct IKError { + sourcepp::math::Vec3f position; + sourcepp::math::Quat rotation; +}; + +struct CompressedIKError { + sourcepp::math::Vec<6, float> scale; + sourcepp::math::Vec<6, int16_t> offset; + std::vector animValues; +}; + +struct IKRule { + int32_t index; + int32_t type; + int32_t chain; + int32_t bone; + int32_t slot; + float height; + float radius; + float floor; + sourcepp::math::Vec3f pos; + sourcepp::math::Quat q; + + //int32_t compressedIKErrorIndex; + std::optional compressedIKError; + + //int32_t unused2; + + int32_t iStart; + //int32_t ikErrorIndex; + std::vector ikErrors; + + float start; + float peak; + float tail; + float end; + //float unused3; + float contact; + float drop; + float top; + + //int32_t unused6; + //int32_t unused7; + //int32_t unused8; + + //int32_t attachmentIndex; + std::string attachment; // just the name + + //int32_t unused[7]; +}; + +struct LocalHierarchy { + int32_t bone{}; + int32_t newParent{}; + float start{}; + float peak{}; + float tail{}; + float end{}; + int32_t startFrame{}; + + //int32_t localAnimIndex; + std::optional compressedIKError; + + //int32_t unused[4]; +}; + +struct AnimSection { + int32_t animBlock; + int32_t animIndex; +}; + struct AnimDesc { enum Flags : int32_t { FLAG_NONE = 0, @@ -86,29 +251,59 @@ struct AnimDesc { //int32_t movementCount; //int32_t movementIndex; + std::vector movements; //int32_t _unused0[6]; - //int32_t animBlock; - //int32_t animIndex; + int32_t animBlock; + int32_t animIndex; //int32_t ikRuleCount; //int32_t ikRuleIndex; + std::vector ikRules; - //int32_t animBlockIKRuleIndex; + int32_t animBlockIKRuleIndex; - //int32_t localHierarchyIndexCount; + //int32_t localHierarchyCount; //int32_t localHierarchyIndex; + std::vector localHierarchies; //int32_t sectionIndex; - //int32_t sectionFrames; + int32_t sectionFrames; + std::vector sections; - //int16_t zeroFrameSpan; - //int16_t zeroFrameCount; - //int32_t zeroFrameIndex; - //float zeroFrameStallTime; + int16_t zeroFrameSpan; + int16_t zeroFrameCount; + int32_t zeroFrameIndex; + float zeroFrameStallTime; + + // animation data + std::vector boneAnimations; +}; +SOURCEPP_BITFLAGS_ENUM(AnimDesc::Flags) + +// Sequence structs +struct Event { + float cycle; + int32_t event; + int32_t type; + + //char options[64]; + std::string options; + + //int32_t eventNameIndex; + std::string eventName; +}; + +struct AutoLayer { + int16_t sequence; + int16_t pose; + int32_t flags; + float start; + float peak; + float tail; + float end; }; -SOURCEPP_BITWISE_ENUM(AnimDesc::Flags) struct SequenceDesc { enum Flags : int32_t { @@ -119,22 +314,27 @@ struct SequenceDesc { //int32_t basePointer; //int32_t labelIndex; + std::string label; + //int32_t activityLabelIndex; + std::string activityName; Flags flags; - //int32_t activity; - //int32_t activityWeight; + int32_t activity; + int32_t activityWeight; //int32_t eventCount; //int32_t eventIndex; + std::vector events; - sourcepp::Vec3f boundingBoxMin; - sourcepp::Vec3f boundingBoxMax; + sourcepp::math::Vec3f boundingBoxMin; + sourcepp::math::Vec3f boundingBoxMax; int32_t blendCount; - int32_t animIndexIndex; + //int32_t animIndexIndex; + std::vector animIndices; // groupSize[0] * groupSize[1] (for animation blending) int32_t movementIndex; @@ -164,28 +364,39 @@ struct SequenceDesc { //int32_t autoLayerCount; //int32_t autoLayerIndex; + std::vector autoLayers; - int32_t weightListIndex; + //int32_t weightListIndex; + std::vector boneWeights; - int32_t poseKeyIndex; + //int32_t poseKeyIndex; + std::vector poseKeys; //int32_t ikLockCount; //int32_t ikLockIndex; + std::vector ikLocks; //int32_t keyValueIndex; //int32_t keyValueSize; + std::string keyValues; - int32_t cyclePoseIndex; + int32_t cyclePoseIndex; // index into poseParameters - //int32_t _unused0[7]; + //int32_t activityModifierCount; + //int32_t activityModifierIndex; + std::vector activityModifiers; + + //int32_t _unused0[5]; }; -SOURCEPP_BITWISE_ENUM(SequenceDesc::Flags) -*/ +SOURCEPP_BITFLAGS_ENUM(SequenceDesc::Flags) +// Material structs struct Material { enum Flags : int32_t { - FLAG_NONE = 0, - // todo(flags): Material (Texture in MDL) + FLAG_NONE = 0, + FLAG_RELATIVE_TEXTURE_PATH_SPECIFIED = 1 << 0, + // Note: mstudiotexture_t.flags field exists in the format but is never set by the studiomdl compiler. + // The engine might still check this flag, so it is exposed here anyway. }; //int32_t nameIndex; @@ -198,6 +409,77 @@ struct Material { }; SOURCEPP_BITFLAGS_ENUM(Material::Flags) +// BodyPart structs (Model, Mesh, Flex, Eyeball, Attachment) +struct Eyeball { + //int32_t sznameindex; + std::string name; + + int32_t bone; + sourcepp::math::Vec3f org; + float zOffset; + float radius; + sourcepp::math::Vec3f up; + sourcepp::math::Vec3f forward; + int32_t texture; + + //int32_t unused1; + float irisScale; + //int32_t unused2; + + std::array upperFlexDesc; + std::array lowerFlexDesc; + std::array upperTarget; + std::array lowerTarget; + + int32_t upperLidFlexDesc; + int32_t lowerLidFlexDesc; + //int32_t unused[4]; + bool nonFACS; + //char unused3[3]; + //int32_t unused4[7]; +}; + +struct VertexAnim { + uint16_t index; + uint8_t speed; // 255/max_length_in_flex + uint8_t side; // 255/left_right + + // pos delta + std::array delta; + + // normal delta + std::array ndelta; +}; + +struct VertexAnimWrinkle { + uint16_t index; + uint8_t speed; + uint8_t side; + std::array delta; + std::array ndelta; + int16_t wrinkleDelta; +}; + +struct MeshFlex { + int32_t flexDescIndex; + + // control curve + float target0; + float target1; + float target2; + float target3; + + //int32_t numverts; + //int32_t vertindex; + std::vector vertAnims; + std::vector vertAnimsWrinkle; + + int32_t flexPair; + uint8_t vertAnimType; // 0=normal, 1=wrinkle + //uint8_t unusedchar[3]; + //int32_t unused[6]; +}; + struct Mesh { int32_t material; @@ -209,6 +491,7 @@ struct Mesh { //int32_t flexesCount; //int32_t flexesOffset; + std::vector flexes; int32_t materialType; int32_t materialParam; @@ -217,8 +500,19 @@ struct Mesh { sourcepp::math::Vec3f center; - //int32_t modelVertexData; - //int32_t numLODVertexes[MAX_LOD_COUNT]; + int32_t modelVertexData; + std::array numLODVertexes; + //int32_t _unused[8]; +}; + +struct Attachment { + //int32_t nameIndex; + std::string name; + + int32_t bone; + sourcepp::math::Vec3f position; + + sourcepp::math::Mat3x4f localMatrix; //int32_t _unused[8]; }; @@ -237,13 +531,15 @@ struct Model { // These do not map to raw memory int32_t verticesCount; int32_t verticesOffset; - //int32_t tangentsOffset; + int32_t tangentsOffset; //int32_t attachmentsCount; //int32_t attachmentsOffset; + std::vector attachments; //int32_t eyeballsCount; //int32_t eyeballsOffset; + std::vector eyeballs; //int32_t _unused0[10]; }; @@ -253,11 +549,135 @@ struct BodyPart { std::string name; //int32_t modelsCount; - int32_t base; // No idea what this is, might as well expose it + int32_t base; // multiplier for bodygroup skin index calculation? //int32_t modelsOffset; std::vector models; }; +// Flex structs +struct FlexController { + //int32_t typeIndex; + std::string type; + + //int32_t nameIndex; + std::string name; + + int32_t localToGlobal; + float min; + float max; +}; + +enum FlexOpType : int32_t { + FLEX_OP_CONST = 1, + FLEX_OP_FETCH1 = 2, + FLEX_OP_FETCH2 = 3, + FLEX_OP_ADD = 4, + FLEX_OP_SUB = 5, + FLEX_OP_MUL = 6, + FLEX_OP_DIV = 7, + FLEX_OP_NEG = 8, + FLEX_OP_EXP = 9, + FLEX_OP_OPEN = 10, + FLEX_OP_CLOSE = 11, + FLEX_OP_COMMA = 12, + FLEX_OP_MAX = 13, + FLEX_OP_MIN = 14, + FLEX_OP_2WAY_0 = 15, + FLEX_OP_2WAY_1 = 16, + FLEX_OP_NWAY = 17, + FLEX_OP_COMBO = 18, + FLEX_OP_DOMINATE = 19, + FLEX_OP_DME_LOWER_EYELID = 20, + FLEX_OP_DME_UPPER_EYELID = 21, +}; + +struct FlexOp { + int32_t op; + union { + int32_t index; + float value; + } d; +}; + +struct FlexRule { + int32_t flex; + + //int32_t opCount; + //int32_t opIndex; + std::vector ops; +}; + +// IK structs +struct IKLink { + int32_t bone; + sourcepp::math::Vec3f kneeDir; + //sourcepp::math::Vec3f unused; +}; + +struct IKChain { + //int32_t nameIndex; + std::string name; + + int32_t linkType; + + //int32_t linkCount; + //int32_t linkIndex; + std::vector links; +}; + +// Mouth struct +struct Mouth { + int32_t bone; + sourcepp::math::Vec3f forward; + int32_t flexDescIndex; +}; + +// Pose parameter struct +struct PoseParameter { + //int32_t nameIndex; + std::string name; + + int32_t flags; + float start; + float end; + float loop; +}; + +// Include model struct +struct IncludeModel { + //int32_t labelIndex; + std::string label; + + //int32_t nameIndex; + std::string name; +}; + +// Animation block struct +struct AnimBlock { + // external offsets + int32_t dataStart; + int32_t dataEnd; +}; + +// Flex controller UI struct +struct FlexControllerUI { + //int32_t nameIndex; + std::string name; + + // SIMPLE/STEREO/NWAY + //int32_t szindex0; + //int32_t szindex1; + //int32_t szindex2; + std::string controllerName0; // non-stereo || left controller (stereo) + std::string controllerName1; // right controller (stereo) + std::string controllerName2; // value controller (NWAY only) + + uint8_t remapType; + bool stereo; + //uint8_t unused[2]; +}; + +// Main struct struct MDL { [[nodiscard]] bool open(const std::byte* data, std::size_t size); @@ -313,9 +733,11 @@ struct MDL { //int32_t localAnimationCount; //int32_t localAnimationOffset; + std::vector animations; //int32_t localSequenceCount; //int32_t localSequenceOffset; + std::vector sequences; int32_t activityListVersion; int32_t eventsIndexed; @@ -331,7 +753,6 @@ struct MDL { //int32_t skinReferenceCount; //int32_t skinReferenceFamilyCount; //int32_t skinReferenceIndex; - // Each vector is an individual skin, which holds indices into the materials vector std::vector> skins; //int32_t bodyPartCount; @@ -340,75 +761,107 @@ struct MDL { //int32_t attachmentCount; //int32_t attachmentOffset; + std::vector attachments; //int32_t localNodeCount; //int32_t localNodeIndex; //int32_t localNodeNameIndex; + std::vector localNodeNames; + std::vector localNodeTransitions; //int32_t flexDescCount; //int32_t flexDescIndex; + std::vector flexDescs; //int32_t flexControllerCount; //int32_t flexControllerIndex; + std::vector flexControllers; //int32_t flexRulesCount; //int32_t flexRulesIndex; + std::vector flexRules; //int32_t ikChainCount; //int32_t ikChainIndex; + std::vector ikChains; //int32_t mouthsCount; //int32_t mouthsIndex; + std::vector mouths; //int32_t localPoseParamCount; //int32_t localPoseParamIndex; + std::vector poseParameters; //int32_t surfacePropertyIndex; + std::string surfaceProperty; //int32_t keyValueIndex; //int32_t keyValueCount; + std::string keyValues; - //int32_t ikLockCount; - //int32_t ikLockIndex; + //int32_t localIKAutoplayLockCount; + //int32_t localIKAutoplayLockIndex; + std::vector ikAutoplayLocks; - //float mass; - //int32_t contentsFlags; + float mass; + int32_t contentsFlags; //int32_t includeModelCount; //int32_t includeModelIndex; + std::vector includeModels; - //int32_t virtualModel; + // int32_t virtualModel; //int32_t animationBlocksNameIndex; - //int32_t animationBlocksCount; //int32_t animationBlocksIndex; + std::string animationBlocksName; + std::vector animationBlocks; - //int32_t animationBlockModel; + // int32_t animationBlockModel; - //int32_t boneTableNameIndex; + //int32_t boneTableByNameIndex; + std::vector boneTableByName; - //int32_t vertexBase; - //int32_t offsetBase; + // int32_t vertexBase; + // int32_t offsetBase; - //std::byte directionalDotProduct; + uint8_t directionalDotProduct; + uint8_t rootLOD; + uint8_t numAllowedRootLODs; + //uint8_t _unused0; - //uint8_t rootLOD; - //uint8_t numAllowedRootLODs; - - //std::byte _unused0; //int32_t _unused1; //int32_t flexControllerUICount; //int32_t flexControllerUIIndex; + std::vector flexControllerUIs; - //float vertAnimFixedPointScale; + float vertAnimFixedPointScale; //int32_t _unused2; - // todo: header 2 - //int32_t header2Offset; + int32_t studioHdr2Index; //int32_t _unused3; + + struct Header2 { + int32_t srcBoneTransformCount; + int32_t srcBoneTransformIndex; + int32_t illumPositionAttachmentIndex; + float maxEyeDeflection; + int32_t linearBoneIndex; + int32_t nameIndex; + int32_t boneFlexDriverCount; + int32_t boneFlexDriverIndex; + //int32_t reserved[56]; + }; + Header2 header2{}; + bool hasHeader2 = false; + + std::vector srcBoneTransforms; + std::optional linearBone; + std::vector boneFlexDrivers; }; SOURCEPP_BITFLAGS_ENUM(MDL::Flags) diff --git a/include/mdlpp/structs/VTX.h b/include/mdlpp/structs/VTX.h index 1df72db79..b4edfd4ef 100644 --- a/include/mdlpp/structs/VTX.h +++ b/include/mdlpp/structs/VTX.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -9,11 +10,28 @@ namespace mdlpp::VTX { +struct MaterialReplacement { + int16_t materialID; + //int32_t replacementMaterialNameOffset; + std::string replacementMaterialName; +}; + +struct MaterialReplacementList { + //int32_t numReplacements; + //int32_t replacementOffset; + std::vector replacements; +}; + +struct BoneStateChange { + int32_t hardwareID; + int32_t newBoneID; +}; + struct Vertex { - //uint8_t boneWeightIndex[3]; - //uint8_t boneCount; + std::array boneWeightIndex; + uint8_t boneCount; uint16_t meshVertexID; - //int8_t boneID[3]; + std::array boneID; }; struct Strip { @@ -36,6 +54,7 @@ struct Strip { //int32_t boneStateChangeCount; //int32_t boneStateChangeOffset; + std::vector boneStateChanges; // On MDL version >= 49: //int32_t numTopologyIndices; @@ -119,6 +138,7 @@ struct VTX { int32_t numLODs; //int32_t materialReplacementListOffset; + std::vector materialReplacementLists; // One per LOD //int32_t bodyPartCount; //int32_t bodyPartOffset; diff --git a/include/sourcepp/Math.h b/include/sourcepp/Math.h index 5bda3a39c..6bcc66840 100644 --- a/include/sourcepp/Math.h +++ b/include/sourcepp/Math.h @@ -385,6 +385,19 @@ struct QuatCompressed64 { }; static_assert(std::is_trivially_copyable_v); +/// Lower precision Vec3 compressed to 6 bytes +struct Vec3Compressed48 : Vec3ui16 { + using Vec3ui16::Vec; + + [[nodiscard]] Vec3f decompress() const { + const float fx = (static_cast((*this)[0]) / 32767.5f) - 1.f; + const float fy = (static_cast((*this)[1]) / 32767.5f) - 1.f; + const float fz = (static_cast((*this)[2]) / 32767.5f) - 1.f; + return {fx, fy, fz}; + } +}; +static_assert(std::is_trivially_copyable_v); + template class Mat { static_assert(M >= 2, "Matrices must have at least two rows!"); diff --git a/src/mdlpp/structs/MDL.cpp b/src/mdlpp/structs/MDL.cpp index 3df87eba6..85532ca63 100644 --- a/src/mdlpp/structs/MDL.cpp +++ b/src/mdlpp/structs/MDL.cpp @@ -5,9 +5,49 @@ using namespace mdlpp::MDL; using namespace sourcepp; +using mdlpp::AnimValue; constexpr int32_t MDL_ID = parser::binary::makeFourCC("IDST"); +namespace { + +// boundary check before seeking +bool seekAndValidate(BufferStreamReadOnly& stream, const uint64_t offset) { + if (offset >= stream.size()) { + return false; + } + stream.seek_u(offset); + return true; +} + +// TODO: Decompress RLE animation data into frame values +// TODO: Make lambda return bool for the boundary check? +void readAnimValueRLE(BufferStreamReadOnly& stream, const int frameCount, std::vector& outData) { + AnimValue val{}; + int framesCovered = 0; + while (framesCovered < frameCount) { + stream.read(val.value); + outData.push_back(val); + + if (val.num.valid == 0) { + framesCovered += val.num.total; + stream.read(val.value); + outData.push_back(val); + } else { + // val.num.valid values, covering val.num.total frames + const int numValues = val.num.valid; + const int totalFrames = val.num.total; + for (int v = 0; v < numValues; v++) { + stream.read(val.value); + outData.push_back(val); + } + framesCovered += totalFrames; + } + } +} + +} // namespace + bool MDL::open(const std::byte* data, std::size_t size) { BufferStreamReadOnly stream{data, size}; @@ -41,13 +81,11 @@ bool MDL::open(const std::byte* data, std::size_t size) { const auto hitboxSetCount = stream.read(); const auto hitboxSetOffset = stream.read(); - //auto animDescCount = stream.read(); - //auto animDescOffset = stream.read(); - stream.skip(2); + const auto animDescCount = stream.read(); + const auto animDescOffset = stream.read(); - //auto sequenceDescCount = stream.read(); - //auto sequenceDescOffset = stream.read(); - stream.skip(2); + const auto sequenceDescCount = stream.read(); + const auto sequenceDescOffset = stream.read(); stream .read(this->activityListVersion) @@ -66,6 +104,203 @@ bool MDL::open(const std::byte* data, std::size_t size) { const auto bodyPartCount = stream.read(); const auto bodyPartOffset = stream.read(); + const auto attachmentCount = stream.read(); + const auto attachmentOffset = stream.read(); + + const auto localNodeCount = stream.read(); + const auto localNodeIndex = stream.read(); + const auto localNodeNameIndex = stream.read(); + + const auto flexDescCount = stream.read(); + const auto flexDescIndex = stream.read(); + const auto flexControllerCount = stream.read(); + const auto flexControllerIndex = stream.read(); + const auto flexRulesCount = stream.read(); + const auto flexRulesIndex = stream.read(); + const auto ikChainCount = stream.read(); + const auto ikChainIndex = stream.read(); + + const auto mouthsCount = stream.read(); + const auto mouthsIndex = stream.read(); + + const auto localPoseParamCount = stream.read(); + const auto localPoseParamIndex = stream.read(); + + const auto surfacePropertyIndex = stream.read(); + const auto keyValueIndex = stream.read(); + const auto keyValueCount = stream.read(); + + const auto localIKAutoplayLockCount = stream.read(); + const auto localIKAutoplayLockIndex = stream.read(); + + stream + .read(this->mass) + .read(this->contentsFlags); + + const auto includeModelCount = stream.read(); + const auto includeModelIndex = stream.read(); + stream.skip(); // virtualModel + + // do we want to parse these here? + const auto animationBlocksNameIndex = stream.read(); + const auto animationBlocksCount = stream.read(); + const auto animationBlocksIndex = stream.read(); + stream.skip(); // animationBlockModel + + const auto boneTableNameIndex = stream.read(); + + stream + .skip() // vertexBase + .skip() // offsetBase + .read(this->directionalDotProduct) + .read(this->rootLOD) + .read(this->numAllowedRootLODs) + .skip() // _unused0 + .skip(); // _unused1 + + const auto flexControllerUICount = stream.read(); + const auto flexControllerUIIndex = stream.read(); + + stream + .read(this->vertAnimFixedPointScale) + .skip() // _unused2 + .read(this->studioHdr2Index) + .skip(); // _unused3 + + // header 2 + // Note: some models have weird (dynamic??) header2 data with offsets (and size) pointing outside the file. + // Unlike the rest of this parser, I validate offset AND size, then skip invalid sections + // rather than failing the whole file. (Might want to change this). - cueki Nov 27, 2025 + if (this->studioHdr2Index != 0) { + stream.seek_u(this->studioHdr2Index) + .read(this->header2.srcBoneTransformCount) + .read(this->header2.srcBoneTransformIndex) + .read(this->header2.illumPositionAttachmentIndex) + .read(this->header2.maxEyeDeflection) + .read(this->header2.linearBoneIndex) + .read(this->header2.nameIndex) + .read(this->header2.boneFlexDriverCount) + .read(this->header2.boneFlexDriverIndex); + stream.skip(56); // reserved + this->hasHeader2 = true; + + for (int i = 0; i < this->header2.srcBoneTransformCount; i++) { + const auto srcBoneTransformPos = this->studioHdr2Index + this->header2.srcBoneTransformIndex + i * (sizeof(int32_t) + sizeof(math::Mat3x4f) * 2); + if (srcBoneTransformPos + (sizeof(int32_t) + sizeof(math::Mat3x4f) * 2) > stream.size()) { + break; + } + auto& srcBoneTransform = this->srcBoneTransforms.emplace_back(); + + stream.seek_u(srcBoneTransformPos); + const auto nameOffset = stream.read(); + const auto nameAbsOffset = srcBoneTransformPos + nameOffset; + if (nameOffset != 0 && nameAbsOffset > 0 && nameAbsOffset < stream.size()) { + stream.seek_u(nameAbsOffset) + .read(srcBoneTransform.name) + .seek_u(srcBoneTransformPos + sizeof(int32_t)); + } + + stream + .read(srcBoneTransform.pretransform) + .read(srcBoneTransform.posttransform); + } + + if (this->header2.linearBoneIndex != 0) { + const auto linearBoneOffset = this->studioHdr2Index + this->header2.linearBoneIndex; + if (linearBoneOffset + sizeof(int32_t) * 16 <= stream.size()) { + this->linearBone = LinearBone{}; + auto& lb = *this->linearBone; + + stream.seek_u(linearBoneOffset) + .read(lb.boneCount); + + const auto flagsIndex = stream.read(); + const auto parentIndex = stream.read(); + const auto posIndex = stream.read(); + const auto quatIndex = stream.read(); + const auto rotIndex = stream.read(); + const auto poseToBoneIndex = stream.read(); + const auto posScaleIndex = stream.read(); + const auto rotScaleIndex = stream.read(); + const auto qAlignmentIndex = stream.read(); + stream.skip(6); // unused[6] + + // we are reading from a struct of arrays instead of an array of structs + stream.seek_u(linearBoneOffset + flagsIndex); + for (int i = 0; i < lb.boneCount; i++) { + lb.flags.push_back(stream.read()); + } + + stream.seek_u(linearBoneOffset + parentIndex); + for (int i = 0; i < lb.boneCount; i++) { + lb.parent.push_back(stream.read()); + } + + stream.seek_u(linearBoneOffset + posIndex); + for (int i = 0; i < lb.boneCount; i++) { + lb.position.push_back(stream.read()); + } + + stream.seek_u(linearBoneOffset + quatIndex); + for (int i = 0; i < lb.boneCount; i++) { + lb.quaternion.push_back(stream.read()); + } + + stream.seek_u(linearBoneOffset + rotIndex); + for (int i = 0; i < lb.boneCount; i++) { + lb.rotation.push_back(stream.read()); + } + + stream.seek_u(linearBoneOffset + poseToBoneIndex); + for (int i = 0; i < lb.boneCount; i++) { + lb.poseToBone.push_back(stream.read()); + } + + stream.seek_u(linearBoneOffset + posScaleIndex); + for (int i = 0; i < lb.boneCount; i++) { + lb.positionScale.push_back(stream.read()); + } + + stream.seek_u(linearBoneOffset + rotScaleIndex); + for (int i = 0; i < lb.boneCount; i++) { + lb.rotationScale.push_back(stream.read()); + } + + stream.seek_u(linearBoneOffset + qAlignmentIndex); + for (int i = 0; i < lb.boneCount; i++) { + lb.quaternionAlignment.push_back(stream.read()); + } + } + } + + for (int i = 0; i < this->header2.boneFlexDriverCount; i++) { + const auto boneFlexDriverPos = this->studioHdr2Index + this->header2.boneFlexDriverIndex + i * (sizeof(int32_t) * 6); + if (boneFlexDriverPos + (sizeof(int32_t) * 6) > stream.size()) { + break; + } + auto& boneFlexDriver = this->boneFlexDrivers.emplace_back(); + stream.seek_u(boneFlexDriverPos) + .read(boneFlexDriver.boneIndex); + + const auto controlCount = stream.read(); + const auto controlIndex = stream.read(); + stream.skip(3); // unused[3] + + for (int j = 0; j < controlCount; j++) { + const auto controlPos = boneFlexDriverPos + controlIndex + j * (sizeof(int32_t) * 2 + sizeof(float) * 2); + if (controlPos + (sizeof(int32_t) * 2 + sizeof(float) * 2) > stream.size()) { + break; + } + auto& control = boneFlexDriver.controls.emplace_back(); + stream.seek_u(controlPos) + .read(control.boneComponent) + .read(control.flexControllerIndex) + .read(control.min) + .read(control.max); + } + } + } + // Done reading sequentially, start seeking to offsets stream.seek(boneOffset); @@ -87,6 +322,14 @@ bool MDL::open(const std::byte* data, std::size_t size) { .read(bone.procType) .read(bone.procIndex) .read(bone.physicsBone); + + // TODO: Parse procedural bone data at procIndex + // - STUDIO_PROC_AXISINTERP (1): mstudioaxisinterpbone_t - 152 bytes + // - STUDIO_PROC_QUATINTERP (2): mstudioquatinterpbone_t - 12 bytes + trigger array + // - STUDIO_PROC_AIMATBONE (3): mstudioaimatbone_t - 44 bytes + // - STUDIO_PROC_AIMATATTACH (4): mstudioaimatbone_t - 44 bytes + // - STUDIO_PROC_JIGGLE (5): mstudiojigglebone_t - 140 bytes (lol) + parser::binary::readStringAtOffset(stream, bone.surfacePropName, std::ios::cur, sizeof(int32_t) * 12 + sizeof(math::Vec3f) * 4 + sizeof(math::Quat) * 2 + sizeof(math::Mat3x4f) + sizeof(Bone::Flags)); stream.read(bone.contents); @@ -101,7 +344,6 @@ bool MDL::open(const std::byte* data, std::size_t size) { // _unused0 stream.skip(8); } - for (int i = 0; i < hitboxSetCount; i++) { const auto hitboxSetPos = hitboxSetOffset + i * (sizeof(int32_t) * 3); stream.seek_u(hitboxSetPos); @@ -114,7 +356,9 @@ bool MDL::open(const std::byte* data, std::size_t size) { for (int j = 0; j < hitboxCount; j++) { const auto hitboxPos = hitboxOffset + j * (sizeof(int32_t) * 11 + sizeof(math::Vec3f) * 2); - stream.seek_u(hitboxSetPos + hitboxPos); + if (!seekAndValidate(stream, hitboxSetPos + hitboxPos)) { + return false; + } auto& hitbox = hitboxSet.hitboxes.emplace_back(); @@ -125,7 +369,7 @@ bool MDL::open(const std::byte* data, std::size_t size) { .read(hitbox.bboxMax); // note: we don't know what model versions use absolute vs. relative offsets here - // and this is unimportant, so skip parsing the bbox name here + // and this is unimportant, so skip parsing the bbox name here //readStringAtOffset(stream, hitbox.name, std::ios::cur, sizeof(int32_t) * 3 + sizeof(Vec3f) * 2); stream.skip(); hitbox.name = ""; @@ -135,22 +379,466 @@ bool MDL::open(const std::byte* data, std::size_t size) { } } - /* stream.seek(animDescOffset); for (int i = 0; i < animDescCount; i++) { - // todo(wrapper) + const auto animDescPos = animDescOffset + i * (sizeof(int32_t) * 22 + sizeof(int16_t) * 2 + sizeof(float) * 2); + stream.seek_u(animDescPos); + + auto& animDesc = this->animations.emplace_back(); + + stream.skip(); // basePointer + // animation name offsets are relative to the struct base + // after the offset int32, we are 8 bytes past the structure base (4 for basePointer + 4 for offset) + // so we subtract 8 instead of the default 4 + parser::binary::readStringAtOffset(stream, animDesc.name, std::ios::cur, 8); + stream + .read(animDesc.fps) + .read(animDesc.flags) + .read(animDesc.frameCount); + + const auto movementCount = stream.read(); + const auto movementIndex = stream.read(); + stream.skip(6); // unused + + stream + .read(animDesc.animBlock) + .read(animDesc.animIndex); + + const auto ikRuleCount = stream.read(); + const auto ikRuleIndex = stream.read(); + + stream.read(animDesc.animBlockIKRuleIndex); + + const auto localHierarchyCount = stream.read(); + const auto localHierarchyIndex = stream.read(); + + const auto sectionIndex = stream.read(); + stream.read(animDesc.sectionFrames); + + // TODO: Parse zeroframe bone data at zeroFrameIndex when animBlock != 0 + // For each bone with BONE_HAS_SAVEFRAME_POS: read zeroFrameCount * Vector48 + // For each bone with BONE_HAS_SAVEFRAME_ROT: read zeroFrameCount * Quaternion64 + stream.read(animDesc.zeroFrameSpan) + .read(animDesc.zeroFrameCount) + .read(animDesc.zeroFrameIndex) + .read(animDesc.zeroFrameStallTime); + + // TODO: Load external animations from external file when animBlock != 0 + // Animation data, IK rules (animBlockIKRuleIndex), and local hierarchy are in external blocks + if (animDesc.animIndex != 0 && animDesc.animBlock == 0) { + const auto animDataPos = animDescPos + animDesc.animIndex; + if (!seekAndValidate(stream, animDataPos)) { + return false; + } + const auto savedPos = stream.tell(); + + // LL of mstudioanim_t structures + // bone, flags, nextoffset (2 bytes), then var data + while (true) { + const auto boneAnimPos = stream.tell(); + + auto& boneAnim = animDesc.boneAnimations.emplace_back(); + stream + .read(boneAnim.bone) + .read(boneAnim.flags); + + if (boneAnim.bone == 255) { + animDesc.boneAnimations.pop_back(); + break; + } + + const auto nextOffset = stream.read(); + + // flags + if (boneAnim.flags & AnimBoneData::FLAG_RAW_ROT2) { + boneAnim.staticRotation = stream.read(); + } else if (boneAnim.flags & AnimBoneData::FLAG_RAW_ROT) { + boneAnim.staticRotation = stream.read(); + } + + if (boneAnim.flags & AnimBoneData::FLAG_RAW_POS) { + boneAnim.staticPosition = stream.read(); + } + + // rotation + if (boneAnim.flags & AnimBoneData::FLAG_ANIM_ROT) { + AnimValuePtr rotPtr{}; + stream.read(rotPtr); + boneAnim.animRotationPtr = rotPtr; + + const auto ptrPos = stream.tell(); + + // pitch/yaw/roll + for (uint8_t idx = 0; idx < rotPtr.size(); idx++) { + const auto comp = rotPtr[idx]; + if (comp > 0) { + if (!seekAndValidate(stream, ptrPos - sizeof(AnimValuePtr) + comp)) { + return false; + } + readAnimValueRLE(stream, animDesc.frameCount, boneAnim.animRotationData); + } + } + + stream.seek_u(ptrPos); + } + + // position + if (boneAnim.flags & AnimBoneData::FLAG_ANIM_POS) { + AnimValuePtr posPtr{}; + stream.read(posPtr); + boneAnim.animPositionPtr = posPtr; + + const auto ptrPos = stream.tell(); + + // x/y/z + for (uint8_t idx = 0; idx < posPtr.size(); idx++) { + const auto comp = posPtr[idx]; + if (comp > 0) { + if (!seekAndValidate(stream, ptrPos - sizeof(AnimValuePtr) + comp)) { + return false; + } + readAnimValueRLE(stream, animDesc.frameCount, boneAnim.animPositionData); + } + } + + stream.seek_u(ptrPos); + } + + if (nextOffset == 0) { + break; + } + if (!seekAndValidate(stream, boneAnimPos + nextOffset)) { + return false; + } + } + + stream.seek_u(savedPos); + } + + const auto movementDataPos = animDescPos + movementIndex; + if (!seekAndValidate(stream, movementDataPos)) { + return false; + } + + for (int j = 0; j < movementCount; j++) { + auto& movement = animDesc.movements.emplace_back(); + stream.read(movement.endFrame); + + movement.flags = static_cast(stream.read()); + + stream + .read(movement.velocityStart) + .read(movement.velocityEnd) + .read(movement.yawEnd) + .read(movement.movement) + .read(movement.relativePosition); + } + + if (ikRuleIndex != 0 && animDesc.animBlock == 0) { + const auto ikRuleDataPos = animDescPos + ikRuleIndex; + if (!seekAndValidate(stream, ikRuleDataPos)) { + return false; + } + + for (int j = 0; j < ikRuleCount; j++) { + const auto ikRulePos = ikRuleDataPos + j * (sizeof(int32_t) * 20 + sizeof(float) * 11 + sizeof(math::Vec3f) + sizeof(math::Quat)); + stream.seek_u(ikRulePos); + + auto& ikRule = animDesc.ikRules.emplace_back(); + + stream + .read(ikRule.index) + .read(ikRule.type) + .read(ikRule.chain) + .read(ikRule.bone) + .read(ikRule.slot) + .read(ikRule.height) + .read(ikRule.radius) + .read(ikRule.floor) + .read(ikRule.pos) + .read(ikRule.q); + + const auto compressedIKErrorIndex = stream.read(); + stream.skip(); // unused2 + + stream + .read(ikRule.iStart); + + const auto ikErrorIndex = stream.read(); + // note: afaik crowbar has a bug when decompiling 1 frame anims ?? + // [0.0, 0.0, 1.0, 1.0] should be 0 0 1 1 but crowbar gives 0 0 0 0 (for the model I tested: bot_heavy from TF2) + stream + .read(ikRule.start) + .read(ikRule.peak) + .read(ikRule.tail) + .read(ikRule.end) + .skip() // unused3 + .read(ikRule.contact) + .read(ikRule.drop) + .read(ikRule.top) + .skip() // unused6 + .skip() // unused7 + .skip(); // unused8 + + parser::binary::readStringAtOffset(stream, ikRule.attachment); + + stream.skip(7); // unused[7] + + // compressed IK error + if (compressedIKErrorIndex != 0) { + const auto compErrorPos = ikRulePos + compressedIKErrorIndex; + const auto savedPos = stream.tell(); + stream.seek_u(compErrorPos); + + ikRule.compressedIKError = CompressedIKError{}; + stream + .read(ikRule.compressedIKError->scale) + .read(ikRule.compressedIKError->offset); + + const auto compErrorDataPos = stream.tell(); + for (uint8_t idx = 0; idx < ikRule.compressedIKError->offset.size(); idx++) { + const auto k = ikRule.compressedIKError->offset[idx]; + if (k > 0) { + if (!seekAndValidate(stream, compErrorDataPos + k)) { + return false; + } + readAnimValueRLE(stream, animDesc.frameCount, ikRule.compressedIKError->animValues); + } + } + + stream.seek_u(savedPos); + } + + // uncompressed IK error + if (ikErrorIndex != 0) { + const auto ikErrorPos = ikRulePos + ikErrorIndex; + const auto savedPos = stream.tell(); + stream.seek_u(ikErrorPos); + + const int errorFrameCount = animDesc.frameCount - ikRule.iStart; + ikRule.ikErrors.reserve(errorFrameCount); + + for (int k = 0; k < errorFrameCount; k++) { + auto& ikError = ikRule.ikErrors.emplace_back(); + stream + .read(ikError.position) + .read(ikError.rotation); + } + + stream.seek_u(savedPos); + } + } + } + + if (localHierarchyIndex != 0 && animDesc.animBlock == 0) { + for (int j = 0; j < localHierarchyCount; j++) { + const auto localHierarchyPos = animDescPos + localHierarchyIndex + j * (sizeof(int32_t) * 2 + sizeof(float) * 4 + sizeof(int32_t) * 6); + stream.seek_u(localHierarchyPos); + + auto& localHierarchy = animDesc.localHierarchies.emplace_back(); + stream + .read(localHierarchy.bone) + .read(localHierarchy.newParent) + .read(localHierarchy.start) + .read(localHierarchy.peak) + .read(localHierarchy.tail) + .read(localHierarchy.end) + .read(localHierarchy.startFrame); + + const auto localAnimIndex = stream.read(); + stream.skip(4); // unused[4] + + if (localAnimIndex != 0) { + const auto compErrorPos = localHierarchyPos + localAnimIndex; + stream.seek_u(compErrorPos); + + localHierarchy.compressedIKError = CompressedIKError{}; + stream + .read(localHierarchy.compressedIKError->scale) + .read(localHierarchy.compressedIKError->offset); + + const auto compErrorDataPos = stream.tell(); + for (uint8_t idx = 0; idx < localHierarchy.compressedIKError->offset.size(); idx++) { + const auto k = localHierarchy.compressedIKError->offset[idx]; + if (k > 0) { + if (!seekAndValidate(stream, compErrorDataPos + k)) { + return false; + } + readAnimValueRLE(stream, animDesc.frameCount, localHierarchy.compressedIKError->animValues); + } + } + } + } + } + + if (sectionIndex != 0 && animDesc.sectionFrames > 0) { + // NOTE: numsections is not stored in the file, only in studiomdl's internal s_animation_t. + // So I cannot guarantee this formula is correct - cueki + const int sectionCount = animDesc.frameCount / animDesc.sectionFrames + 2; + for (int j = 0; j < sectionCount; j++) { + const auto sectionPos = animDescPos + sectionIndex + j * sizeof(AnimSection); + stream.seek_u(sectionPos); + + auto& section = animDesc.sections.emplace_back(); + stream + .read(section.animBlock) + .read(section.animIndex); + } + } } stream.seek(sequenceDescOffset); for (int i = 0; i < sequenceDescCount; i++) { - // todo(wrapper) + const auto sequenceDescPos = sequenceDescOffset + i * (sizeof(int32_t) * 38 + sizeof(float) * 9 + sizeof(math::Vec3f) * 2); + stream.seek_u(sequenceDescPos); + + auto& sequenceDesc = this->sequences.emplace_back(); + + stream.skip(); // basePointer + parser::binary::readStringAtOffset(stream, sequenceDesc.label, std::ios::cur, 8); + parser::binary::readStringAtOffset(stream, sequenceDesc.activityName, std::ios::cur, 12); + stream + .read(sequenceDesc.flags) + .read(sequenceDesc.activity) + .read(sequenceDesc.activityWeight); + + const auto eventCount = stream.read(); + const auto eventIndex = stream.read(); + + stream + .read(sequenceDesc.boundingBoxMin) + .read(sequenceDesc.boundingBoxMax) + .read(sequenceDesc.blendCount); + + const auto animIndexIndex = stream.read(); + + stream + .read(sequenceDesc.movementIndex) + .read(sequenceDesc.groupSize) + .read(sequenceDesc.paramIndex) + .read(sequenceDesc.paramStart) + .read(sequenceDesc.paramEnd) + .read(sequenceDesc.paramParent) + .read(sequenceDesc.fadeInTime) + .read(sequenceDesc.fadeOutTime) + .read(sequenceDesc.localEntryNode) + .read(sequenceDesc.localExitNode) + .read(sequenceDesc.nodeFlags) + .read(sequenceDesc.entryPhase) + .read(sequenceDesc.exitPhase) + .read(sequenceDesc.lastFrame) + .read(sequenceDesc.nextSequence) + .read(sequenceDesc.pose) + .read(sequenceDesc.ikRuleCount); + + const auto autoLayerCount = stream.read(); + const auto autoLayerIndex = stream.read(); + + const auto weightListIndex = stream.read(); + const auto poseKeyIndex = stream.read(); + + const auto ikLockCount = stream.read(); + const auto ikLockIndex = stream.read(); + + const auto seqKeyValueIndex = stream.read(); + const auto seqKeyValueSize = stream.read(); + + stream.read(sequenceDesc.cyclePoseIndex); + + const auto activityModifierIndex = stream.read(); + const auto activityModifierCount = stream.read(); + + stream.skip(5); // unused[5] + + if (animIndexIndex != 0 && sequenceDesc.groupSize[0] > 0 && sequenceDesc.groupSize[1] > 0) { + stream.seek_u(sequenceDescPos + animIndexIndex); + const int blendCount = sequenceDesc.groupSize[0] * sequenceDesc.groupSize[1]; + for (int j = 0; j < blendCount; j++) { + sequenceDesc.animIndices.push_back(stream.read()); + } + } + + // if boneCount is not set this will be garbage data so we check for it + if (weightListIndex != 0 && boneCount > 0) { + stream.seek_u(sequenceDescPos + weightListIndex); + for (int j = 0; j < boneCount; j++) { + sequenceDesc.boneWeights.push_back(stream.read()); + } + } + + if (poseKeyIndex != 0) { + stream.seek_u(sequenceDescPos + poseKeyIndex); + // pPoseKey(iParam, iAnim) indexes as iParam * groupsize[0] + iAnim + // iParam is 0 or 1 (2 params), iAnim is 0 to groupsize[0]-1 + // So total keys = 2 * groupSize[0] + const int poseKeyCount = 2 * sequenceDesc.groupSize[0]; + for (int j = 0; j < poseKeyCount; j++) { + sequenceDesc.poseKeys.push_back(stream.read()); + } + } + + if (activityModifierIndex != 0) { + for (int j = 0; j < activityModifierCount; j++) { + const auto modifierPos = sequenceDescPos + activityModifierIndex + j * sizeof(int32_t); + stream.seek_u(modifierPos); + auto& modifier = sequenceDesc.activityModifiers.emplace_back(); + parser::binary::readStringAtOffset(stream, modifier); + } + } + + const auto ikLockDataPos = sequenceDescPos + ikLockIndex; + stream.seek_u(ikLockDataPos); + + for (int j = 0; j < ikLockCount; j++) { + auto& ikLock = sequenceDesc.ikLocks.emplace_back(); + stream + .read(ikLock.chain) + .read(ikLock.posWeight) + .read(ikLock.localQWeight) + .read(ikLock.flags); + stream.skip(4); // unused[4] + } + + const auto eventDataPos = sequenceDescPos + eventIndex; + stream.seek_u(eventDataPos); + + for (int j = 0; j < eventCount; j++) { + auto& event = sequenceDesc.events.emplace_back(); + + stream + .read(event.cycle) + .read(event.event) + .read(event.type) + .read(event.options, 64); + + parser::binary::readStringAtOffset(stream, event.eventName, std::ios::cur, + sizeof(float) + sizeof(int32_t) * 2 + 64); + } + + const auto autoLayerDataPos = sequenceDescPos + autoLayerIndex; + stream.seek_u(autoLayerDataPos); + + for (int j = 0; j < autoLayerCount; j++) { + auto& autoLayer = sequenceDesc.autoLayers.emplace_back(); + stream + .read(autoLayer.sequence) + .read(autoLayer.pose) + .read(autoLayer.flags) + .read(autoLayer.start) + .read(autoLayer.peak) + .read(autoLayer.tail) + .read(autoLayer.end); + } + + if (seqKeyValueSize > 0 && seqKeyValueIndex != 0) { + const auto seqKeyValueDataPos = sequenceDescPos + seqKeyValueIndex; + stream.seek_u(seqKeyValueDataPos).read(sequenceDesc.keyValues, seqKeyValueSize); + } } - */ stream.seek(materialOffset); for (int i = 0; i < materialCount; i++) { auto& material = this->materials.emplace_back(); - parser::binary::readStringAtOffset(stream, material.name); stream.read(material.flags); @@ -163,7 +851,6 @@ bool MDL::open(const std::byte* data, std::size_t size) { stream.seek(materialDirOffset); for (int i = 0; i < materialDirCount; i++) { auto& materialDir = this->materialDirectories.emplace_back(); - parser::binary::readStringAtOffset(stream, materialDir, std::ios::beg, 0); } @@ -182,16 +869,17 @@ bool MDL::open(const std::byte* data, std::size_t size) { stream.seek_u(bodyPartPos); auto& bodyPart = this->bodyParts.emplace_back(); - parser::binary::readStringAtOffset(stream, bodyPart.name); const auto modelsCount = stream.read(); - stream.skip(); // base + stream.read(bodyPart.base); const auto modelsOffset = stream.read(); for (int j = 0; j < modelsCount; j++) { auto modelPos = modelsOffset + j * (64 + sizeof(float) + sizeof(int32_t) * 20); - stream.seek_u(bodyPartPos + modelPos); + if (!seekAndValidate(stream, bodyPartPos + modelPos)) { + return false; + } auto& model = bodyPart.models.emplace_back(); @@ -205,11 +893,20 @@ bool MDL::open(const std::byte* data, std::size_t size) { stream .read(model.verticesCount) - .read(model.verticesOffset); + .read(model.verticesOffset) + .read(model.tangentsOffset); + + const auto modelAttachmentsCount = stream.read(); + const auto modelAttachmentsOffset = stream.read(); + + const auto eyeballsCount = stream.read(); + const auto eyeballsOffset = stream.read(); for (int k = 0; k < meshesCount; k++) { const auto meshPos = meshesOffset + k * (sizeof(int32_t) * (18 + MAX_LOD_COUNT) + sizeof(math::Vec3f)); - stream.seek_u(bodyPartPos + modelPos + meshPos); + if (!seekAndValidate(stream, bodyPartPos + modelPos + meshPos)) { + return false; + } auto& mesh = model.meshes.emplace_back(); @@ -217,15 +914,390 @@ bool MDL::open(const std::byte* data, std::size_t size) { .read(mesh.material) .skip() .read(mesh.verticesCount) - .read(mesh.verticesOffset) - .skip(2) + .read(mesh.verticesOffset); + + const auto meshFlexCount = stream.read(); + const auto meshFlexOffset = stream.read(); + + stream .read(mesh.materialType) .read(mesh.materialParam) .read(mesh.meshID) - .read(mesh.center); + .read(mesh.center) + .read(mesh.modelVertexData) + .read(mesh.numLODVertexes); + + stream.skip(8); // unused + + if (meshFlexOffset != 0) { + + for (int m = 0; m < meshFlexCount; m++) { + const auto flexPos = meshFlexOffset + m * (sizeof(int32_t) * 10 + sizeof(float) * 4 + sizeof(uint8_t) * 4); + if (!seekAndValidate(stream, bodyPartPos + modelPos + meshPos + flexPos)) { + return false; + } + + auto& flex = mesh.flexes.emplace_back(); + + stream + .read(flex.flexDescIndex) + .read(flex.target0) + .read(flex.target1) + .read(flex.target2) + .read(flex.target3); + + const auto numverts = stream.read(); + const auto vertindex = stream.read(); + + stream + .read(flex.flexPair) + .read(flex.vertAnimType); + + if (vertindex != 0) { + // vertindex is relative to the flex structure base + const auto vertAnimPos = bodyPartPos + modelPos + meshPos + flexPos + vertindex; + if (!seekAndValidate(stream, vertAnimPos)) { + return false; + } + + if (flex.vertAnimType == 0) { + // normal vertex animations + flex.vertAnims.reserve(numverts); + for (int n = 0; n < numverts; n++) { + auto& vertAnim = flex.vertAnims.emplace_back(); + stream + .read(vertAnim.index) + .read(vertAnim.speed) + .read(vertAnim.side) + .read(vertAnim.delta) + .read(vertAnim.ndelta); + } + } else if (flex.vertAnimType == 1) { + // wrinkle vertex animations + flex.vertAnimsWrinkle.reserve(numverts); + for (int n = 0; n < numverts; n++) { + auto& vertAnim = flex.vertAnimsWrinkle.emplace_back(); + stream + .read(vertAnim.index) + .read(vertAnim.speed) + .read(vertAnim.side) + .read(vertAnim.delta) + .read(vertAnim.ndelta) + .read(vertAnim.wrinkleDelta); + } + } + } + } + } + } + + if (eyeballsOffset != 0) { + for (int k = 0; k < eyeballsCount; k++) { + const auto eyeballPos = eyeballsOffset + k * (sizeof(int32_t) * 24 + sizeof(float) * 9 + sizeof(math::Vec3f) * 3 + sizeof(uint8_t) * 4); + if (!seekAndValidate(stream, bodyPartPos + modelPos + eyeballPos)) { + return false; + } + + auto& eyeball = model.eyeballs.emplace_back(); + + // note: eyeball names are not stored in compiled MDL files ? + // sznameindex exists in the struct but is never populated by studiomdl + stream.skip(); // sznameindex + + stream + .read(eyeball.bone) + .read(eyeball.org) + .read(eyeball.zOffset) + .read(eyeball.radius) + .read(eyeball.up) + .read(eyeball.forward) + .read(eyeball.texture) + .skip() // unused1 + .read(eyeball.irisScale) + .skip() // unused2 + .read(eyeball.upperFlexDesc) + .read(eyeball.lowerFlexDesc) + .read(eyeball.upperTarget) + .read(eyeball.lowerTarget) + .read(eyeball.upperLidFlexDesc) + .read(eyeball.lowerLidFlexDesc); + + stream.skip(4); // unused[4] + stream.read(eyeball.nonFACS); + stream.skip(3); // unused3[3] + stream.skip(7); // unused4[7] + } + } + + if (modelAttachmentsOffset != 0) { + for (int k = 0; k < modelAttachmentsCount; k++) { + const auto attachmentPos = modelAttachmentsOffset + k * (sizeof(int32_t) + sizeof(uint32_t) + sizeof(int32_t) + sizeof(math::Mat3x4f) + sizeof(int32_t) * 8); + if (!seekAndValidate(stream, bodyPartPos + modelPos + attachmentPos)) { + return false; + } + + auto& attachment = model.attachments.emplace_back(); + parser::binary::readStringAtOffset(stream, attachment.name); + + stream + .skip() // flags + .read(attachment.bone) + .read(attachment.localMatrix); + + // extract position from the last column of the matrix + attachment.position[0] = attachment.localMatrix[0][3]; + attachment.position[1] = attachment.localMatrix[1][3]; + attachment.position[2] = attachment.localMatrix[2][3]; + + stream.skip(8); // unused + } + } + } + } + + stream.seek(attachmentOffset); + for (int i = 0; i < attachmentCount; i++) { + auto& attachment = this->attachments.emplace_back(); + parser::binary::readStringAtOffset(stream, attachment.name); + + stream + .skip() // flags + .read(attachment.bone) + .read(attachment.localMatrix); + + // extract position from the last column of the matrix + attachment.position[0] = attachment.localMatrix[0][3]; + attachment.position[1] = attachment.localMatrix[1][3]; + attachment.position[2] = attachment.localMatrix[2][3]; + + stream.skip(8); // unused + } + + if (localNodeCount > 0) { + stream.seek(localNodeNameIndex); + this->localNodeNames.reserve(localNodeCount); + for (int i = 0; i < localNodeCount; i++) { + auto& nodeName = this->localNodeNames.emplace_back(); + parser::binary::readStringAtOffset(stream, nodeName, std::ios::beg, 0); + } + + // transition matrix (localNodeCount × localNodeCount bytes) + stream.seek(localNodeIndex); + const auto transitionMatrixSize = localNodeCount * localNodeCount; + this->localNodeTransitions.resize(transitionMatrixSize); + for (int i = 0; i < transitionMatrixSize; i++) { + this->localNodeTransitions[i] = stream.read(); + } + } + + if (flexDescCount > 0) { + stream.seek(flexDescIndex); + for (int i = 0; i < flexDescCount; i++) { + auto& flexDescName = this->flexDescs.emplace_back(); + parser::binary::readStringAtOffset(stream, flexDescName); + } + } + + for (int i = 0; i < flexControllerCount; i++) { + const auto flexControllerPos = flexControllerIndex + i * (sizeof(int32_t) * 3 + sizeof(float) * 2); + stream.seek_u(flexControllerPos); + + auto& flexController = this->flexControllers.emplace_back(); + + // lambda to read at relative offset + auto readStringAtRelativeOffset = [&](std::string& target) { + if (const auto offset = stream.read(); offset != 0) { + const auto savedPos = stream.tell(); + // skips if fail + if (!seekAndValidate(stream, flexControllerPos + offset)) { + return; + } + stream.read(target); + stream.seek_u(savedPos); } + }; + + readStringAtRelativeOffset(flexController.type); + readStringAtRelativeOffset(flexController.name); + + stream + .read(flexController.localToGlobal) + .read(flexController.min) + .read(flexController.max); + } + + for (int i = 0; i < flexRulesCount; i++) { + const auto flexRulePos = flexRulesIndex + i * (sizeof(int32_t) * 3); + stream.seek_u(flexRulePos); + + auto& flexRule = this->flexRules.emplace_back(); + stream.read(flexRule.flex); + + const auto opCount = stream.read(); + const auto opIndex = stream.read(); + + const auto opDataPos = flexRulePos + opIndex; + stream.seek_u(opDataPos); + + for (int j = 0; j < opCount; j++) { + auto& op = flexRule.ops.emplace_back(); + stream + .read(op.op) + .read(op.d.index); + } + } + + for (int i = 0; i < ikChainCount; i++) { + const auto ikChainPos = ikChainIndex + i * (sizeof(int32_t) * 4); + stream.seek_u(ikChainPos); + + auto& ikChain = this->ikChains.emplace_back(); + parser::binary::readStringAtOffset(stream, ikChain.name); + stream.read(ikChain.linkType); + + const auto linkCount = stream.read(); + const auto linkIndex = stream.read(); + + const auto linkDataPos = ikChainPos + linkIndex; + stream.seek_u(linkDataPos); + + for (int j = 0; j < linkCount; j++) { + auto& link = ikChain.links.emplace_back(); + stream + .read(link.bone) + .read(link.kneeDir) + .skip(); // unused } } + if (mouthsCount > 0) { + stream.seek(mouthsIndex); + for (int i = 0; i < mouthsCount; i++) { + auto& mouth = this->mouths.emplace_back(); + stream + .read(mouth.bone) + .read(mouth.forward) + .read(mouth.flexDescIndex); + } + } + + if (localPoseParamCount > 0) { + stream.seek(localPoseParamIndex); + + for (int i = 0; i < localPoseParamCount; i++) { + auto& poseParam = this->poseParameters.emplace_back(); + parser::binary::readStringAtOffset(stream, poseParam.name); + stream + .read(poseParam.flags) + .read(poseParam.start) + .read(poseParam.end) + .read(poseParam.loop); + } + } + + if (surfacePropertyIndex != 0) { + stream.seek(surfacePropertyIndex).read(this->surfaceProperty); + } + + if (keyValueCount > 0) { + stream.seek(keyValueIndex).read(this->keyValues, keyValueCount); + } + + if (localIKAutoplayLockCount > 0) { + stream.seek(localIKAutoplayLockIndex); + for (int i = 0; i < localIKAutoplayLockCount; i++) { + auto& ikLock = this->ikAutoplayLocks.emplace_back(); + stream + .read(ikLock.chain) + .read(ikLock.posWeight) + .read(ikLock.localQWeight) + .read(ikLock.flags); + stream.skip(4); // unused[4] + } + } + + for (int i = 0; i < includeModelCount; i++) { + const auto includeModelPos = includeModelIndex + i * (sizeof(int32_t) * 2); + stream.seek_u(includeModelPos); + auto& includeModel = this->includeModels.emplace_back(); + + // lambda to read at relative offset + auto readStringAtRelativeOffset = [&](std::string& target) { + if (const auto offset = stream.read(); offset != 0) { + const auto savedPos = stream.tell(); + // skips if fail + if (!seekAndValidate(stream, includeModelPos + offset)) { + return; + } + stream.read(target); + stream.seek_u(savedPos); + } + }; + + readStringAtRelativeOffset(includeModel.label); + readStringAtRelativeOffset(includeModel.name); + } + + if (animationBlocksNameIndex != 0) { + stream.seek(animationBlocksNameIndex).read(this->animationBlocksName); + } + + if (animationBlocksCount > 0) { + stream.seek(animationBlocksIndex); + for (int i = 0; i < animationBlocksCount; i++) { + auto& animBlock = this->animationBlocks.emplace_back(); + stream + .read(animBlock.dataStart) + .read(animBlock.dataEnd); + } + } + + if (boneTableNameIndex != 0) { + stream.seek(boneTableNameIndex); + this->boneTableByName.reserve(boneCount); + for (int i = 0; i < boneCount; i++) { + this->boneTableByName.push_back(stream.read()); + } + } + + for (int i = 0; i < flexControllerUICount; i++) { + const auto flexControllerUIPos = flexControllerUIIndex + i * (sizeof(int32_t) * 4 + sizeof(uint8_t) * 4); + stream.seek_u(flexControllerUIPos); + + auto& flexControllerUI = this->flexControllerUIs.emplace_back(); + parser::binary::readStringAtOffset(stream, flexControllerUI.name); + + const auto szIndex0 = stream.read(); + const auto szIndex1 = stream.read(); + const auto szIndex2 = stream.read(); + + stream + .read(flexControllerUI.remapType) + .read(flexControllerUI.stereo); + + stream.skip(2); // unused + + // lambda to resolve controller name from szIndex offset + auto resolveControllerName = [&](int32_t szIndex, std::string& targetName) { + if (szIndex != 0) { + const auto savedPos = stream.tell(); + stream.seek_u(flexControllerUIPos + szIndex); + stream.skip(); + if (const auto controllerNameOffset = stream.read(); controllerNameOffset != 0) { + // skips if fail + if (!seekAndValidate(stream, flexControllerUIPos + szIndex + controllerNameOffset)) { + return; + } + stream.read(targetName); + } + stream.seek_u(savedPos); + } + }; + + resolveControllerName(szIndex0, flexControllerUI.controllerName0); + resolveControllerName(szIndex1, flexControllerUI.controllerName1); + resolveControllerName(szIndex2, flexControllerUI.controllerName2); + } + return true; } diff --git a/src/mdlpp/structs/VTX.cpp b/src/mdlpp/structs/VTX.cpp index 13041d95f..aae3cb0cb 100644 --- a/src/mdlpp/structs/VTX.cpp +++ b/src/mdlpp/structs/VTX.cpp @@ -1,8 +1,10 @@ #include #include +#include using namespace mdlpp::VTX; +using namespace sourcepp; bool VTX::open(const std::byte* data, std::size_t size, const MDL::MDL& mdl) { BufferStreamReadOnly stream{data, size}; @@ -23,12 +25,37 @@ bool VTX::open(const std::byte* data, std::size_t size, const MDL::MDL& mdl) { stream.read(this->numLODs); - // todo: read material replacement list - stream.skip(); + const auto materialReplacementListOffset = stream.read(); const auto bodyPartCount = stream.read(); const auto bodyPartOffset = stream.read(); + if (materialReplacementListOffset > 0) { + stream.seek(materialReplacementListOffset); + + for (int i = 0; i < this->numLODs; i++) { + const auto replacementListPos = materialReplacementListOffset + i * (sizeof(int32_t) * 2); + stream.seek_u(replacementListPos); + + auto& replacementList = this->materialReplacementLists.emplace_back(); + + const auto numReplacements = stream.read(); + const auto replacementOffset = stream.read(); + + if (numReplacements > 0 && replacementOffset > 0) { + for (int j = 0; j < numReplacements; j++) { + const auto replacementPos = replacementOffset + j * (sizeof(int16_t) + sizeof(int32_t)); + stream.seek_u(replacementListPos + replacementPos); + + auto& replacement = replacementList.replacements.emplace_back(); + stream.read(replacement.materialID); + + parser::binary::readStringAtOffset(stream, replacement.replacementMaterialName, std::ios::cur, 6); + } + } + } + } + for (int i = 0; i < bodyPartCount; i++) { const auto bodyPartPos = bodyPartOffset + i * ((sizeof(int32_t) * 2)); stream.seek_u(bodyPartPos); @@ -85,15 +112,13 @@ bool VTX::open(const std::byte* data, std::size_t size, const MDL::MDL& mdl) { auto stripGroupCurrentPos = stream.tell(); stream.seek_u(bodyPartPos + modelPos + modelLODPos + meshPos + stripGroupPos + vertexOffset); for (int n = 0; n < vertexCount; n++) { - auto& [meshVertexID] = stripGroup.vertices.emplace_back(); - - // todo: process bone data - stream.skip(4); - - stream.read(meshVertexID); + auto& vertex = stripGroup.vertices.emplace_back(); - // ditto - stream.skip(3); + stream + .read(vertex.boneWeightIndex) + .read(vertex.boneCount) + .read(vertex.meshVertexID) + .read(vertex.boneID); } stream.seek_u(stripGroupCurrentPos); @@ -124,20 +149,37 @@ bool VTX::open(const std::byte* data, std::size_t size, const MDL::MDL& mdl) { const auto indicesCount = stream.read(); stream.read(strip.indicesOffset); - // todo: check if offset is in bytes + // Note: offset is in elements, not bytes strip.indices = std::span(stripGroup.indices.begin() + strip.indicesOffset, indicesCount); const auto verticesCount = stream.read(); stream.read(strip.verticesOffset); - // todo: check if offset is in bytes + // Note: offset is in elements, not bytes strip.vertices = std::span(stripGroup.vertices.begin() + strip.verticesOffset, verticesCount); stream .read(strip.boneCount) .read(strip.flags); - // todo: bone stuff - stream.skip(2); + // bone stuff + const auto boneStateChangeCount = stream.read(); + const auto boneStateChangeOffset = stream.read(); + + if (boneStateChangeCount > 0 && boneStateChangeOffset > 0) { + const auto savedPos = stream.tell(); + constexpr auto stripHeaderSize = sizeof(int32_t) * 6 + sizeof(int16_t) + sizeof(Strip::Flags); + const auto stripBasePos = bodyPartPos + modelPos + modelLODPos + meshPos + stripGroupPos + stripOffset + (n * stripHeaderSize); + stream.seek_u(stripBasePos + boneStateChangeOffset); + + for (int p = 0; p < boneStateChangeCount; p++) { + auto& boneStateChange = strip.boneStateChanges.emplace_back(); + stream + .read(boneStateChange.hardwareID) + .read(boneStateChange.newBoneID); + } + + stream.seek_u(savedPos); + } if (mdl.version >= 49) { // mesh topology