From 849542d01d21198705e853ccb2b8a80e80202e6b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sun, 29 Mar 2026 20:42:10 -0700 Subject: [PATCH] fix: doodad/mount animations synchronized due to unseeded rand() All 8 rand() calls for animation time offsets and variation timers in m2_renderer.cpp used C rand() which defaults to seed 1 without srand(), producing identical sequences every launch. Trees, torches, and grass all swayed in sync. Replaced with std::mt19937 seeded from random_device. Same fix for 4 mount idle fidget/sound timer sites in renderer.cpp which mixed rand() with the mt19937 already present. --- src/rendering/m2_renderer.cpp | 32 ++++++++++++++++++++++++-------- src/rendering/renderer.cpp | 18 ++++++++++-------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 8f134a75..ce57c489 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +32,21 @@ namespace rendering { namespace { +// Seeded RNG for animation time offsets and variation timers. Using rand() +// without srand() produces the same sequence every launch, causing all +// doodads (trees, torches, grass) to sway/flicker in sync. +std::mt19937& rng() { + static std::mt19937 gen(std::random_device{}()); + return gen; +} +uint32_t randRange(uint32_t maxExclusive) { + if (maxExclusive == 0) return 0; + return std::uniform_int_distribution(0, maxExclusive - 1)(rng()); +} +float randFloat(float lo, float hi) { + return std::uniform_real_distribution(lo, hi)(rng()); +} + // Shared lava UV scroll timer — ensures consistent animation across all render passes const auto kLavaAnimStart = std::chrono::steady_clock::now(); @@ -1596,8 +1612,8 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, instance.currentSequenceIndex = 0; instance.idleSequenceIndex = 0; instance.animDuration = static_cast(mdl.sequences[0].duration); - instance.animTime = static_cast(rand() % std::max(1u, mdl.sequences[0].duration)); - instance.variationTimer = 3000.0f + static_cast(rand() % 8000); + instance.animTime = static_cast(randRange(std::max(1u, mdl.sequences[0].duration))); + instance.variationTimer = randFloat(3000.0f, 11000.0f); // Seed bone matrices from an existing instance of the same model so the // new instance renders immediately instead of being invisible until the @@ -1703,8 +1719,8 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& instance.currentSequenceIndex = 0; instance.idleSequenceIndex = 0; instance.animDuration = static_cast(mdl2.sequences[0].duration); - instance.animTime = static_cast(rand() % std::max(1u, mdl2.sequences[0].duration)); - instance.variationTimer = 3000.0f + static_cast(rand() % 8000); + instance.animTime = static_cast(randRange(std::max(1u, mdl2.sequences[0].duration))); + instance.variationTimer = randFloat(3000.0f, 11000.0f); // Seed bone matrices from an existing sibling so the instance renders immediately for (const auto& existing : instances) { @@ -1718,7 +1734,7 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& computeBoneMatrices(mdl2, instance); } } else { - instance.animTime = static_cast(rand()) / RAND_MAX * 10000.0f; + instance.animTime = randFloat(0.0f, 10000.0f); } // Register in dedup map @@ -2021,7 +2037,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: instance.animDuration = static_cast(model.sequences[instance.idleSequenceIndex].duration); } instance.animTime = 0.0f; - instance.variationTimer = 4000.0f + static_cast(rand() % 6000); + instance.variationTimer = randFloat(4000.0f, 10000.0f); } else { // Use iterative subtraction instead of fmod() to preserve precision float duration = std::max(1.0f, instance.animDuration); @@ -2035,7 +2051,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: if (!instance.playingVariation && model.idleVariationIndices.size() > 1) { instance.variationTimer -= dtMs; if (instance.variationTimer <= 0.0f) { - int pick = rand() % static_cast(model.idleVariationIndices.size()); + int pick = static_cast(randRange(static_cast(model.idleVariationIndices.size()))); int newSeq = model.idleVariationIndices[pick]; if (newSeq != instance.currentSequenceIndex && newSeq < static_cast(model.sequences.size())) { instance.playingVariation = true; @@ -2043,7 +2059,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: instance.animDuration = static_cast(model.sequences[newSeq].duration); instance.animTime = 0.0f; } else { - instance.variationTimer = 2000.0f + static_cast(rand() % 4000); + instance.variationTimer = randFloat(2000.0f, 6000.0f); } } } diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 1d4a033e..4489da5c 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -2118,18 +2118,19 @@ void Renderer::updateCharacterAnimation() { // Idle fidgets: random one-shot animations when standing still if (!moving && mountAction_ == MountAction::None && mountActiveFidget_ == 0 && !mountAnims_.fidgets.empty()) { mountIdleFidgetTimer_ += lastDeltaTime_; - static float nextFidgetTime = 6.0f + (rand() % 7); // 6-12 seconds + // Use the seeded mt19937 for timing so fidgets aren't deterministic + // across launches (rand() without srand() always starts from seed 1). + static std::mt19937 idleRng(std::random_device{}()); + static float nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); if (mountIdleFidgetTimer_ >= nextFidgetTime) { - // Trigger random fidget animation - static std::mt19937 rng(std::random_device{}()); std::uniform_int_distribution dist(0, mountAnims_.fidgets.size() - 1); - uint32_t fidgetAnim = mountAnims_.fidgets[dist(rng)]; + uint32_t fidgetAnim = mountAnims_.fidgets[dist(idleRng)]; characterRenderer->playAnimation(mountInstanceId_, fidgetAnim, false); - mountActiveFidget_ = fidgetAnim; // Track active fidget + mountActiveFidget_ = fidgetAnim; mountIdleFidgetTimer_ = 0.0f; - nextFidgetTime = 6.0f + (rand() % 7); // Randomize next fidget time + nextFidgetTime = std::uniform_real_distribution(6.0f, 12.0f)(idleRng); LOG_DEBUG("Mount idle fidget: playing anim ", fidgetAnim); } @@ -2141,12 +2142,13 @@ void Renderer::updateCharacterAnimation() { // Idle ambient sounds: snorts and whinnies only, infrequent if (!moving && mountSoundManager) { mountIdleSoundTimer_ += lastDeltaTime_; - static float nextIdleSoundTime = 45.0f + (rand() % 46); // 45-90 seconds + static std::mt19937 soundRng(std::random_device{}()); + static float nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); if (mountIdleSoundTimer_ >= nextIdleSoundTime) { mountSoundManager->playIdleSound(); mountIdleSoundTimer_ = 0.0f; - nextIdleSoundTime = 45.0f + (rand() % 46); // Randomize next sound time + nextIdleSoundTime = std::uniform_real_distribution(45.0f, 90.0f)(soundRng); } } else if (moving) { mountIdleSoundTimer_ = 0.0f; // Reset timer when moving