From 241722feaac71efa6c5cdf89301dd0a512e3573c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 6 May 2026 09:06:46 -0700 Subject: [PATCH] fix(wom): cap bone/animation counts at save (matches load limits) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WOM load caps bones at 512 and animations at 1024. Save previously wrote raw size() and iterated all entries — a model with >512 bones would write fine but truncate on round-trip, and the post-truncation keyframe data would be misread as the next animation. Cap both counts at save and iterate using the capped value so the per-bone keyframe block stays aligned with what load expects. --- src/pipeline/wowee_model.cpp | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/pipeline/wowee_model.cpp b/src/pipeline/wowee_model.cpp index 2fec112e..a609a946 100644 --- a/src/pipeline/wowee_model.cpp +++ b/src/pipeline/wowee_model.cpp @@ -312,9 +312,15 @@ bool WoweeModelLoader::save(const WoweeModel& model, const std::string& basePath // WOM2/WOM3: write bones and animations (always, even if empty for WOM3) if (hasAnim || hasBatches) { - uint32_t boneCount = static_cast(model.bones.size()); + // Cap counts at the load-side limits (512 bones, 1024 anims). Raw + // size() would let a pathological in-memory model write a file the + // loader silently rejects, leaving the post-truncation bytes to be + // misread as the next section. + uint32_t boneCount = static_cast( + std::min(model.bones.size(), 512)); f.write(reinterpret_cast(&boneCount), 4); - for (const auto& bone : model.bones) { + for (uint32_t bi = 0; bi < boneCount; bi++) { + const auto& bone = model.bones[bi]; // Symmetric scrub with load — pivot NaN propagates through // skeleton matrices to every child bone; parent indices outside // bone array would walk off the end during matrix evaluation. @@ -331,7 +337,8 @@ bool WoweeModelLoader::save(const WoweeModel& model, const std::string& basePath f.write(reinterpret_cast(&bone.flags), 4); } - uint32_t animCount = static_cast(model.animations.size()); + uint32_t animCount = static_cast( + std::min(model.animations.size(), 1024)); f.write(reinterpret_cast(&animCount), 4); // Same NaN scrub as load — keyframes can carry corrupt source data // straight through fromM2 without ever round-tripping a load, so the @@ -342,13 +349,16 @@ bool WoweeModelLoader::save(const WoweeModel& model, const std::string& basePath if (!std::isfinite(v.z)) v.z = def; return v; }; - for (const auto& anim : model.animations) { + for (uint32_t ai = 0; ai < animCount; ai++) { + const auto& anim = model.animations[ai]; f.write(reinterpret_cast(&anim.id), 4); f.write(reinterpret_cast(&anim.durationMs), 4); float movingSpeed = std::isfinite(anim.movingSpeed) ? anim.movingSpeed : 0.0f; f.write(reinterpret_cast(&movingSpeed), 4); - for (size_t bi = 0; bi < model.bones.size(); bi++) { + // Iterate bones using the *capped* boneCount so the per-bone + // keyframe block stays aligned with what load expects to read. + for (size_t bi = 0; bi < boneCount; bi++) { uint32_t kfCount = (bi < anim.boneKeyframes.size()) ? static_cast(anim.boneKeyframes[bi].size()) : 0; f.write(reinterpret_cast(&kfCount), 4);