From 86e726095a99eac2c76f59d90da3875cf50a1774 Mon Sep 17 00:00:00 2001 From: cueki Date: Wed, 15 Oct 2025 22:23:40 -0700 Subject: [PATCH 1/2] mdlpp: add animation, sequence, flex, and IK support with parsing improvements --- include/mdlpp/structs/Generic.h | 40 +- include/mdlpp/structs/MDL.h | 457 +++++++++++++++-- include/mdlpp/structs/VTX.h | 26 +- include/sourcepp/Math.h | 18 + src/mdlpp/structs/MDL.cpp | 880 +++++++++++++++++++++++++++++++- src/mdlpp/structs/VTX.cpp | 70 ++- 6 files changed, 1405 insertions(+), 86 deletions(-) diff --git a/include/mdlpp/structs/Generic.h b/include/mdlpp/structs/Generic.h index c8f5c127e..e52627109 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 = 0x00000001, + FLAG_Y = 0x00000002, + FLAG_Z = 0x00000004, + FLAG_XR = 0x00000008, + FLAG_YR = 0x00000010, + FLAG_ZR = 0x00000020, + FLAG_LX = 0x00000040, + FLAG_LY = 0x00000080, + FLAG_LZ = 0x00000100, + FLAG_LXR = 0x00000200, + FLAG_LYR = 0x00000400, + FLAG_LZR = 0x00000800, + FLAG_LINEAR = 0x00001000, + FLAG_TYPES = 0x0003FFFF, + FLAG_RLOOP = 0x00040000, }; 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 = std::array; + } // namespace mdlpp diff --git a/include/mdlpp/structs/MDL.h b/include/mdlpp/structs/MDL.h index cbad48d95..4760e2ec1 100644 --- a/include/mdlpp/structs/MDL.h +++ b/include/mdlpp/structs/MDL.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -12,8 +13,31 @@ namespace mdlpp::MDL { struct Bone { enum Flags : int32_t { - FLAG_NONE = 0, - // todo(flags): Bone + FLAG_NONE = 0, + FLAG_CALCULATE_MASK = 0x0000001F, + FLAG_PHYSICALLY_SIMULATED = 0x00000001, + FLAG_PHYSICS_PROCEDURAL = 0x00000002, + FLAG_ALWAYS_PROCEDURAL = 0x00000004, + FLAG_SCREEN_ALIGN_SPHERE = 0x00000008, + FLAG_SCREEN_ALIGN_CYLINDER = 0x00000010, + FLAG_USED_MASK = 0x0007FF00, + FLAG_USED_BY_ANYTHING = 0x0007FF00, + FLAG_USED_BY_HITBOX = 0x00000100, + FLAG_USED_BY_ATTACHMENT = 0x00000200, + FLAG_USED_BY_VERTEX_MASK = 0x0003FC00, + FLAG_USED_BY_VERTEX_LOD0 = 0x00000400, + FLAG_USED_BY_VERTEX_LOD1 = 0x00000800, + FLAG_USED_BY_VERTEX_LOD2 = 0x00001000, + FLAG_USED_BY_VERTEX_LOD3 = 0x00002000, + FLAG_USED_BY_VERTEX_LOD4 = 0x00004000, + FLAG_USED_BY_VERTEX_LOD5 = 0x00008000, + FLAG_USED_BY_VERTEX_LOD6 = 0x00010000, + FLAG_USED_BY_VERTEX_LOD7 = 0x00020000, + FLAG_USED_BY_BONE_MERGE = 0x00040000, + FLAG_TYPE_MASK = 0x00F00000, + FLAG_FIXED_ALIGNMENT = 0x00100000, + FLAG_HAS_SAVEFRAME_POS = 0x00200000, + FLAG_HAS_SAVEFRAME_ROT = 0x00400000, }; //int32_t nameIndex; @@ -62,7 +86,84 @@ struct HitboxSet { std::vector hitboxes; }; -/* +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::optional staticRotation48; + std::optional staticRotation64; + 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 { + float scale[6]; + int16_t offset[6]; + 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 AnimDesc { enum Flags : int32_t { FLAG_NONE = 0, @@ -86,29 +187,54 @@ 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 localHierarchyCount; + int32_t localHierarchyIndex; - //int32_t localHierarchyIndexCount; - //int32_t localHierarchyIndex; + int32_t sectionIndex; + int32_t sectionFrames; - //int32_t sectionIndex; - //int32_t sectionFrames; + int16_t zeroFrameSpan; + int16_t zeroFrameCount; + int32_t zeroFrameIndex; + float zeroFrameStallTime; + + // animation data + std::vector boneAnimations; +}; +SOURCEPP_BITFLAGS_ENUM(AnimDesc::Flags) + +struct Event { + float cycle; + int32_t event; + int32_t type; + std::array options; - //int16_t zeroFrameSpan; - //int16_t zeroFrameCount; - //int32_t zeroFrameIndex; - //float zeroFrameStallTime; + //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,18 +245,22 @@ 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; @@ -164,6 +294,7 @@ struct SequenceDesc { //int32_t autoLayerCount; //int32_t autoLayerIndex; + std::vector autoLayers; int32_t weightListIndex; @@ -171,21 +302,24 @@ struct SequenceDesc { //int32_t ikLockCount; //int32_t ikLockIndex; + std::vector ikLocks; //int32_t keyValueIndex; //int32_t keyValueSize; + std::string keyValues; int32_t cyclePoseIndex; //int32_t _unused0[7]; }; -SOURCEPP_BITWISE_ENUM(SequenceDesc::Flags) -*/ +SOURCEPP_BITFLAGS_ENUM(SequenceDesc::Flags) struct Material { enum Flags : int32_t { FLAG_NONE = 0, - // todo(flags): Material (Texture in MDL) + // Note: mstudiotexture_t.flags field exists in MDL binary format but is never set by studiomdl compiler. + // The field remains 0 in all compiled MDL files. RELATIVE_TEXTURE_PATH_SPECIFIED (0x1) exists in the + // compiler source but is not written to the file format. }; //int32_t nameIndex; @@ -198,6 +332,76 @@ struct Material { }; SOURCEPP_BITFLAGS_ENUM(Material::Flags) +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 iris_scale; + //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 m_bNonFACS; + //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 flexdesc; + + // 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 +413,7 @@ struct Mesh { //int32_t flexesCount; //int32_t flexesOffset; + std::vector flexes; int32_t materialType; int32_t materialParam; @@ -217,8 +422,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,17 +453,135 @@ 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]; }; +struct PoseParameter { + //int32_t nameIndex; + std::string name; + + int32_t flags; + float start; + float end; + float loop; +}; + +struct IncludeModel { + //int32_t labelIndex; + std::string label; + + //int32_t nameIndex; + std::string name; +}; + +struct FlexController { + //int32_t typeIndex; + std::string type; + + //int32_t nameIndex; + std::string name; + + int32_t localToGlobal; + float min; + float max; +}; + +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]; +}; + +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; +}; + +struct Mouth { + int32_t bone; + sourcepp::math::Vec3f forward; + int32_t flexdesc; +}; + +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; +}; + +struct AnimBlock { + // external offsets + int32_t dataStart; + int32_t dataEnd; +}; + struct BodyPart { //int32_t nameOffset; std::string name; @@ -313,9 +647,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 +667,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 +675,103 @@ 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 _unused1; //int32_t flexControllerUICount; //int32_t flexControllerUIIndex; + std::vector flexControllerUIs; + + float vertAnimFixedPointScale; + int32_t _unused2; - //float vertAnimFixedPointScale; - //int32_t _unused2; + int32_t studiohdr2index; - // todo: header 2 - //int32_t header2Offset; + int32_t _unused3; - //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; }; 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..a93614c6d 100644 --- a/include/sourcepp/Math.h +++ b/include/sourcepp/Math.h @@ -385,6 +385,24 @@ struct QuatCompressed64 { }; static_assert(std::is_trivially_copyable_v); +/// Lower precision Vec3 compressed to 6 bytes +struct Vec3Compressed48 { + uint16_t x : 16; + uint16_t y : 16; + uint16_t z : 16; + + [[nodiscard]] Vec3f decompress() const { + // Convert from 16-bit unsigned integers to floating point values in the range [-1, 1] + // Note: Actual positions need to be multiplied by bone's positionScale after decompression + const float fx = (static_cast(this->x) / 32767.5f) - 1.f; // x / ((2^16 - 1) / 2) - 1 + const float fy = (static_cast(this->y) / 32767.5f) - 1.f; // x / ((2^16 - 1) / 2) - 1 + const float fz = (static_cast(this->z) / 32767.5f) - 1.f; // x / ((2^16 - 1) / 2) - 1 + + 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..28dceeeb4 100644 --- a/src/mdlpp/structs/MDL.cpp +++ b/src/mdlpp/structs/MDL.cpp @@ -5,9 +5,39 @@ using namespace mdlpp::MDL; using namespace sourcepp; +using mdlpp::AnimValue; constexpr int32_t MDL_ID = parser::binary::makeFourCC("IDST"); +namespace { + +// TODO: Decompress RLE animation data into frame values +void readAnimValueRLE(BufferStreamReadOnly& stream, 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; + } + } +} + +} // anonymous namespace + bool MDL::open(const std::byte* data, std::size_t size) { BufferStreamReadOnly stream{data, size}; @@ -41,13 +71,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 +94,89 @@ 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.read(this->virtualModel); + + // do we want to parse these here? + const auto animationBlocksNameIndex = stream.read(); + const auto animationBlocksCount = stream.read(); + const auto animationBlocksIndex = stream.read(); + + stream.read(this->animationBlockModel); + + const auto boneTableNameIndex = stream.read(); + + stream + .read(this->vertexBase) + .read(this->offsetBase) + .read(this->directionalDotProduct) + .read(this->rootLOD) + .read(this->numAllowedRootLODs) + .read(this->_unused0) + .read(this->_unused1); + + const auto flexControllerUICount = stream.read(); + const auto flexControllerUIIndex = stream.read(); + + stream + .read(this->vertAnimFixedPointScale) + .read(this->_unused2) + .read(this->studiohdr2index) + .read(this->_unused3); + + // header of the second kind perchance + if (this->studiohdr2index != 0) { + stream.seek_u(this->studiohdr2index); + // TODO: Parse this + stream + .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); + // skip the 56 reserved int32 fields + stream.skip(56); + this->hasHeader2 = true; + } + // Done reading sequentially, start seeking to offsets stream.seek(boneOffset); @@ -87,6 +198,7 @@ bool MDL::open(const std::byte* data, std::size_t size) { .read(bone.procType) .read(bone.procIndex) .read(bone.physicsBone); + // this is awesome 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 +213,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); @@ -125,8 +236,8 @@ 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 - //readStringAtOffset(stream, hitbox.name, std::ios::cur, sizeof(int32_t) * 3 + sizeof(Vec3f) * 2); + // 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 +246,364 @@ 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(); + + // TODO: Parse all this strange stuff + stream + .read(animDesc.animBlockIKRuleIndex) + .read(animDesc.localHierarchyCount) + .read(animDesc.localHierarchyIndex) + .read(animDesc.sectionIndex) + .read(animDesc.sectionFrames) + .read(animDesc.zeroFrameSpan) + .read(animDesc.zeroFrameCount) + .read(animDesc.zeroFrameIndex) + .read(animDesc.zeroFrameStallTime); + + // TODO: Load external animations if animBlock != 0 + if (animDesc.animIndex != 0 && animDesc.animBlock == 0) { + const auto animDataPos = animDescPos + animDesc.animIndex; + const auto savedPos = stream.tell(); + stream.seek_u(animDataPos); + + // 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.staticRotation64 = stream.read(); + } else if (boneAnim.flags & AnimBoneData::FLAG_RAW_ROT) { + boneAnim.staticRotation48 = 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 (short comp : rotPtr) { + if (comp > 0) { + stream.seek_u(ptrPos - sizeof(AnimValuePtr) + comp); + 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 (short comp : posPtr) { + if (comp > 0) { + stream.seek_u(ptrPos - sizeof(AnimValuePtr) + comp); + readAnimValueRLE(stream, animDesc.frameCount, boneAnim.animPositionData); + } + } + + stream.seek_u(ptrPos); + } + + if (nextOffset == 0) { + break; + } + stream.seek_u(boneAnimPos + nextOffset); + } + + stream.seek_u(savedPos); + } + + if (movementCount > 0 && movementIndex != 0) { + const auto movementDataPos = animDescPos + movementIndex; + stream.seek_u(movementDataPos); + + 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 (ikRuleCount > 0 && ikRuleIndex != 0 && animDesc.animBlock == 0) { + const auto ikRuleDataPos = animDescPos + ikRuleIndex; + stream.seek_u(ikRuleDataPos); + + 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 (short k : ikRule.compressedIKError->offset) { + if (k > 0) { + stream.seek_u(compErrorDataPos + k); + 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); + } + } + } } 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(); + + // TODO: Parse animIndexIndex + // I think its an index to an array that maps the 2d blend to animation indices + // note: dont quote me on that + stream + .read(sequenceDesc.boundingBoxMin) + .read(sequenceDesc.boundingBoxMax) + .read(sequenceDesc.blendCount) + .read(sequenceDesc.animIndexIndex) + .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(); + + // TODO: Parse weightListIndex & poseKeyIndex + stream + .read(sequenceDesc.weightListIndex) + .read(sequenceDesc.poseKeyIndex); + + const auto ikLockCount = stream.read(); + const auto ikLockIndex = stream.read(); + + const auto seqKeyValueIndex = stream.read(); + const auto seqKeyValueSize = stream.read(); + + // TODO: Parse cyclePoseIndex + stream + .read(sequenceDesc.cyclePoseIndex); + + stream + .skip(2) // activitymodifierindex, numactivitymodifiers + .skip(5); // unused + + if (ikLockCount > 0 && ikLockIndex != 0) { + 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] + } + } + + if (eventCount > 0 && eventIndex != 0) { + 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); + + parser::binary::readStringAtOffset(stream, event.eventName, std::ios::cur, + sizeof(float) + sizeof(int32_t) * 2 + 64); + } + } + + if (autoLayerCount > 0 && autoLayerIndex != 0) { + 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); + stream.read(sequenceDesc.keyValues, seqKeyValueSize - 1, false); // seqKeyValueSize includes null terminator + } } - */ 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 +616,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,7 +634,6 @@ 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(); @@ -205,7 +656,14 @@ 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)); @@ -217,15 +675,397 @@ 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 (meshFlexCount > 0 && 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); + stream.seek_u(bodyPartPos + modelPos + meshPos + flexPos); + + auto& flex = mesh.flexes.emplace_back(); + + stream + .read(flex.flexdesc) + .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 (numverts > 0 && vertindex != 0) { + // vertindex is relative to the flex structure base + const auto vertAnimPos = bodyPartPos + modelPos + meshPos + flexPos + vertindex; + stream.seek_u(vertAnimPos); + + 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 (eyeballsCount > 0 && 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); + stream.seek_u(bodyPartPos + modelPos + eyeballPos); + + 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.iris_scale) + .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.m_bNonFACS); + stream.skip(3); // unused3[3] + stream.skip(7); // unused4[7] + } + } + + if (modelAttachmentsCount > 0 && 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); + stream.seek_u(bodyPartPos + modelPos + attachmentPos); + + 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); + + // ditto + 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 && localNodeIndex != 0 && localNodeNameIndex != 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 (localPoseParamCount > 0 && localPoseParamIndex != 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 (includeModelCount > 0 && includeModelIndex != 0) { + 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(); + stream.seek_u(includeModelPos + offset); + stream.read(target); + stream.seek_u(savedPos); + } + }; + + readStringAtRelativeOffset(includeModel.label); + readStringAtRelativeOffset(includeModel.name); + } + } + + if (flexDescCount > 0 && flexDescIndex != 0) { + stream.seek(flexDescIndex); + + for (int i = 0; i < flexDescCount; i++) { + auto& flexDescName = this->flexDescs.emplace_back(); + parser::binary::readStringAtOffset(stream, flexDescName); + } + } + + if (flexControllerCount > 0 && flexControllerIndex != 0) { + 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(); + + // ditto + auto readStringAtRelativeOffset = [&](std::string& target) { + if (const auto offset = stream.read(); offset != 0) { + const auto savedPos = stream.tell(); + stream.seek_u(flexControllerPos + offset); + stream.read(target); + stream.seek_u(savedPos); + } + }; + + readStringAtRelativeOffset(flexController.type); + readStringAtRelativeOffset(flexController.name); + + stream + .read(flexController.localToGlobal) + .read(flexController.min) + .read(flexController.max); + } + } + + if (ikChainCount > 0 && ikChainIndex != 0) { + 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(); + + if (const auto linkIndex = stream.read(); linkCount > 0 && linkIndex != 0) { + 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 (flexRulesCount > 0 && flexRulesIndex != 0) { + 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(); + + if (const auto opIndex = stream.read(); opCount > 0 && opIndex != 0) { + 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); + } + } + } + } + + if (mouthsCount > 0 && mouthsIndex != 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.flexdesc); + } + } + + if (keyValueCount > 0 && keyValueIndex != 0) { + stream.seek(keyValueIndex); + stream.read(this->keyValues, keyValueCount - 1, false); // keyValueCount includes null terminator + } + + if (surfacePropertyIndex != 0) { + stream.seek(surfacePropertyIndex); + stream.read(this->surfaceProperty); + } + + if (localIKAutoplayLockCount > 0 && localIKAutoplayLockIndex != 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] + } + } + + if (boneTableNameIndex != 0 && boneCount > 0) { + stream.seek(boneTableNameIndex); + this->boneTableByName.reserve(boneCount); + for (int i = 0; i < boneCount; i++) { + this->boneTableByName.push_back(stream.read()); + } + } + + if (animationBlocksNameIndex != 0) { + stream.seek(animationBlocksNameIndex); + stream.read(this->animationBlocksName); + } + + if (animationBlocksCount > 0 && animationBlocksIndex != 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 (flexControllerUICount > 0 && flexControllerUIIndex != 0) { + 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) { + stream.seek_u(flexControllerUIPos + szindex + controllerNameOffset); + 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..b89c01236 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); + // do: 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 From f6368b7fa886d6d4d9ef46bf9e29f1d7a977b58d Mon Sep 17 00:00:00 2001 From: cueki Date: Thu, 16 Oct 2025 11:59:37 -0700 Subject: [PATCH 2/2] mdlpp: forgot to comment out these unused fields --- include/mdlpp/structs/MDL.h | 8 ++++---- src/mdlpp/structs/MDL.cpp | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/include/mdlpp/structs/MDL.h b/include/mdlpp/structs/MDL.h index 4760e2ec1..c029b75ed 100644 --- a/include/mdlpp/structs/MDL.h +++ b/include/mdlpp/structs/MDL.h @@ -744,20 +744,20 @@ struct MDL { uint8_t directionalDotProduct; uint8_t rootLOD; uint8_t numAllowedRootLODs; - uint8_t _unused0; + //uint8_t _unused0; - int32_t _unused1; + //int32_t _unused1; //int32_t flexControllerUICount; //int32_t flexControllerUIIndex; std::vector flexControllerUIs; float vertAnimFixedPointScale; - int32_t _unused2; + //int32_t _unused2; int32_t studiohdr2index; - int32_t _unused3; + //int32_t _unused3; struct Header2 { int32_t srcBoneTransformCount; diff --git a/src/mdlpp/structs/MDL.cpp b/src/mdlpp/structs/MDL.cpp index 28dceeeb4..794c0637c 100644 --- a/src/mdlpp/structs/MDL.cpp +++ b/src/mdlpp/structs/MDL.cpp @@ -147,17 +147,17 @@ bool MDL::open(const std::byte* data, std::size_t size) { .read(this->directionalDotProduct) .read(this->rootLOD) .read(this->numAllowedRootLODs) - .read(this->_unused0) - .read(this->_unused1); + .skip() // _unused0 + .skip(); // _unused1 const auto flexControllerUICount = stream.read(); const auto flexControllerUIIndex = stream.read(); stream .read(this->vertAnimFixedPointScale) - .read(this->_unused2) + .skip() // _unused2 .read(this->studiohdr2index) - .read(this->_unused3); + .skip(); // _unused3 // header of the second kind perchance if (this->studiohdr2index != 0) {