From d27387d7446681cfdc17d008345c603dd8352e7c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Feb 2026 21:04:20 -0800 Subject: [PATCH] Fix mount sounds, grey WMO meshes, taxi landing, tree animations, and classic dismount - Per-family mount sounds (kodo, tallstrider, mechanostrider, etc.) detected from M2 model path - Skip WMO groups with SHOW_SKYBOX flag or all-untextured batches (grey mesh in Orgrimmar) - Freeze physics during taxi landing until terrain loads to prevent falling through void - Disable bone animations on tropical vegetation (palm, bamboo, banana, etc.) to fix wiggling - Snap player to final taxi waypoint on flight completion - Extract mount aura spell ID from classic UNIT_FIELD_AURAS for CMSG_CANCEL_AURA dismount - Increase /unstuck forward nudge to 5 units --- Data/expansions/classic/update_fields.json | 1 + Data/expansions/turtle/update_fields.json | 1 + include/audio/mount_sound_manager.hpp | 31 +- include/game/update_field_table.hpp | 1 + include/rendering/renderer.hpp | 2 +- include/rendering/wmo_renderer.hpp | 1 + src/audio/mount_sound_manager.cpp | 593 ++++++++++++++------- src/core/application.cpp | 35 +- src/game/game_handler.cpp | 39 ++ src/game/update_field_table.cpp | 1 + src/rendering/m2_renderer.cpp | 18 +- src/rendering/renderer.cpp | 4 +- src/rendering/wmo_renderer.cpp | 15 + 13 files changed, 525 insertions(+), 217 deletions(-) diff --git a/Data/expansions/classic/update_fields.json b/Data/expansions/classic/update_fields.json index b9d33bd6..de9aa094 100644 --- a/Data/expansions/classic/update_fields.json +++ b/Data/expansions/classic/update_fields.json @@ -12,6 +12,7 @@ "UNIT_FIELD_FLAGS": 46, "UNIT_FIELD_DISPLAYID": 131, "UNIT_FIELD_MOUNTDISPLAYID": 133, + "UNIT_FIELD_AURAS": 50, "UNIT_NPC_FLAGS": 147, "UNIT_DYNAMIC_FLAGS": 143, "UNIT_END": 188, diff --git a/Data/expansions/turtle/update_fields.json b/Data/expansions/turtle/update_fields.json index b9d33bd6..de9aa094 100644 --- a/Data/expansions/turtle/update_fields.json +++ b/Data/expansions/turtle/update_fields.json @@ -12,6 +12,7 @@ "UNIT_FIELD_FLAGS": 46, "UNIT_FIELD_DISPLAYID": 131, "UNIT_FIELD_MOUNTDISPLAYID": 133, + "UNIT_FIELD_AURAS": 50, "UNIT_NPC_FLAGS": 147, "UNIT_DYNAMIC_FLAGS": 143, "UNIT_END": 188, diff --git a/include/audio/mount_sound_manager.hpp b/include/audio/mount_sound_manager.hpp index 93efdb72..a02cb868 100644 --- a/include/audio/mount_sound_manager.hpp +++ b/include/audio/mount_sound_manager.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include namespace wowee { @@ -24,7 +25,15 @@ enum class MountFamily { WOLF, TIGER, RAPTOR, - DRAGON + DRAGON, + KODO, + MECHANOSTRIDER, + TALLSTRIDER, + UNDEAD_HORSE +}; + +struct MountFamilyHash { + std::size_t operator()(MountFamily f) const { return static_cast(f); } }; struct MountSample { @@ -42,7 +51,7 @@ public: void update(float deltaTime); // Called when mounting/dismounting - void onMount(uint32_t creatureDisplayId, bool isFlying); + void onMount(uint32_t creatureDisplayId, bool isFlying, const std::string& modelPath = ""); void onDismount(); // Update movement state @@ -63,6 +72,7 @@ public: private: MountType detectMountType(uint32_t creatureDisplayId) const; MountFamily detectMountFamily(uint32_t creatureDisplayId) const; + MountFamily detectMountFamilyFromPath(const std::string& modelPath) const; void updateMountSounds(); void stopAllMountSounds(); void loadMountSounds(); @@ -80,11 +90,18 @@ private: // Mount sound samples (loaded from MPQ) std::vector wingFlapSounds_; std::vector wingIdleSounds_; - std::vector horseBreathSounds_; - std::vector horseMoveSounds_; - std::vector horseJumpSounds_; // Jump effort sounds - std::vector horseLandSounds_; // Landing thud sounds - std::vector horseIdleSounds_; // Snorts and whinnies for idle + + // Per-family ground mount sounds + struct FamilySounds { + std::vector move; // Movement ambient (alerts/whinnies/growls) + std::vector jump; // Jump effort sounds + std::vector land; // Landing wound/thud sounds + std::vector idle; // Idle ambient (snorts/breathing/fidgets) + }; + std::unordered_map familySounds_; + + // Helper to get sounds for current family (falls back to HORSE) + const FamilySounds& getCurrentFamilySounds() const; // Sound state tracking bool playingMovementSound_ = false; diff --git a/include/game/update_field_table.hpp b/include/game/update_field_table.hpp index 63f2cbf1..5f79754b 100644 --- a/include/game/update_field_table.hpp +++ b/include/game/update_field_table.hpp @@ -29,6 +29,7 @@ enum class UF : uint16_t { UNIT_FIELD_FLAGS_2, UNIT_FIELD_DISPLAYID, UNIT_FIELD_MOUNTDISPLAYID, + UNIT_FIELD_AURAS, // Start of aura spell ID array (48 consecutive uint32 slots, classic/vanilla only) UNIT_NPC_FLAGS, UNIT_DYNAMIC_FLAGS, UNIT_END, diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index aac9f8b6..e5d1f165 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -137,7 +137,7 @@ public: void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; } // Mount rendering - void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset); + void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath = ""); void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; } void setMountPitchRoll(float pitch, float roll) { mountPitch_ = pitch; mountRoll_ = roll; } void clearMount(); diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 3d472922..e6fffa3c 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -309,6 +309,7 @@ private: glm::vec3 boundingBoxMax; uint32_t groupFlags = 0; + bool allUntextured = false; // True if ALL batches use fallback white texture (collision/placeholder group) // Material batches (start index, count, material ID) struct Batch { diff --git a/src/audio/mount_sound_manager.cpp b/src/audio/mount_sound_manager.cpp index 27c37dc1..e7e1414a 100644 --- a/src/audio/mount_sound_manager.cpp +++ b/src/audio/mount_sound_manager.cpp @@ -24,10 +24,13 @@ bool MountSoundManager::initialize(pipeline::AssetManager* assets) { loadMountSounds(); - int totalSamples = wingFlapSounds_.size() + wingIdleSounds_.size() + - horseBreathSounds_.size() + horseMoveSounds_.size() + - horseJumpSounds_.size() + horseLandSounds_.size(); - LOG_INFO("Mount sound manager initialized (", totalSamples, " clips)"); + int totalSamples = wingFlapSounds_.size() + wingIdleSounds_.size(); + for (const auto& [family, sounds] : familySounds_) { + totalSamples += sounds.move.size() + sounds.jump.size() + + sounds.land.size() + sounds.idle.size(); + } + LOG_INFO("Mount sound manager initialized (", totalSamples, " clips, ", + familySounds_.size(), " mount families)"); return true; } @@ -36,11 +39,24 @@ void MountSoundManager::shutdown() { mounted_ = false; wingFlapSounds_.clear(); wingIdleSounds_.clear(); - horseBreathSounds_.clear(); - horseMoveSounds_.clear(); + familySounds_.clear(); assetManager_ = nullptr; } +static void loadSoundList(pipeline::AssetManager* assets, + const std::vector& paths, + std::vector& out) { + for (const auto& path : paths) { + if (!assets->fileExists(path)) continue; + auto data = assets->readFile(path); + if (data.empty()) continue; + MountSample sample; + sample.path = path; + sample.data = std::move(data); + out.push_back(std::move(sample)); + } +} + void MountSoundManager::loadMountSounds() { if (!assetManager_) return; @@ -52,7 +68,6 @@ void MountSoundManager::loadMountSounds() { "Sound\\Creature\\DragonWhelp\\mDragonWhelpWingFlapA.wav", "Sound\\Creature\\DragonWhelp\\mDragonWhelpWingFlapB.wav", }; - for (const auto& path : wingFlapPaths) { MountSample sample; if (loadSound(path, sample)) { @@ -60,13 +75,12 @@ void MountSoundManager::loadMountSounds() { } } - // Flying mount idle/hovering (screeches/calls) + // Flying mount idle/hovering std::vector wingIdlePaths = { "Sound\\Creature\\DragonHawk\\DragonHawkPreAggro.wav", "Sound\\Creature\\DragonHawk\\DragonHawkAggro.wav", "Sound\\Creature\\Dragons\\DragonPreAggro.wav", }; - for (const auto& path : wingIdlePaths) { MountSample sample; if (loadSound(path, sample)) { @@ -74,114 +88,247 @@ void MountSoundManager::loadMountSounds() { } } - // Ground mount breathing/idle (per creature family) - std::vector horseBreathPaths = { - "Sound\\Creature\\Horse\\mHorseStand3A.wav", - "Sound\\Creature\\Ram\\RamPreAggro.wav", - "Sound\\Creature\\Wolf\\mWolfFidget2a.wav", - "Sound\\Creature\\Tiger\\mTigerStand2A.wav", - }; + // --- Per-family ground mount sounds --- - for (const auto& path : horseBreathPaths) { - MountSample sample; - if (loadSound(path, sample)) { - horseBreathSounds_.push_back(std::move(sample)); + // Horse + { + auto& s = familySounds_[MountFamily::HORSE]; + loadSoundList(assetManager_, { + "Sound\\Creature\\Horse\\mHorseAggroA.wav", + }, s.move); + loadSoundList(assetManager_, { + "Sound\\Creature\\Horse\\mHorseAttackA.wav", + "Sound\\Creature\\Horse\\mHorseAttackB.wav", + "Sound\\Creature\\Horse\\mHorseAttackC.wav", + }, s.jump); + loadSoundList(assetManager_, { + "Sound\\Creature\\Horse\\mHorseWoundA.wav", + "Sound\\Creature\\Horse\\mHorseWoundB.wav", + }, s.land); + loadSoundList(assetManager_, { + "Sound\\Creature\\Horse\\mHorseStand3A.wav", + "Sound\\Creature\\Horse\\mHorseAggroA.wav", + }, s.idle); + } + + // Wolf + { + auto& s = familySounds_[MountFamily::WOLF]; + loadSoundList(assetManager_, { + "Sound\\Creature\\Wolf\\mWolfAggroA.wav", + "Sound\\Creature\\Wolf\\mWolfAggroB.wav", + "Sound\\Creature\\Wolf\\mWolfAggroC.wav", + }, s.move); + loadSoundList(assetManager_, { + "Sound\\Creature\\Wolf\\mWolfAttackA.wav", + "Sound\\Creature\\Wolf\\mWolfAttackB.wav", + "Sound\\Creature\\Wolf\\mWolfAttackC.wav", + }, s.jump); + loadSoundList(assetManager_, { + "Sound\\Creature\\Wolf\\mWolfWoundA.wav", + "Sound\\Creature\\Wolf\\mWolfWoundB.wav", + "Sound\\Creature\\Wolf\\mWolfWoundC.wav", + }, s.land); + loadSoundList(assetManager_, { + "Sound\\Creature\\Wolf\\mWolfFidget2a.wav", + "Sound\\Creature\\Wolf\\mWolfFidget2b.wav", + "Sound\\Creature\\Wolf\\mWolfFidget2c.wav", + }, s.idle); + } + + // Ram + { + auto& s = familySounds_[MountFamily::RAM]; + loadSoundList(assetManager_, { + "Sound\\Creature\\Ram\\RamAggro.wav", + "Sound\\Creature\\Ram\\RamPreAggro.wav", + }, s.move); + loadSoundList(assetManager_, { + "Sound\\Creature\\Ram\\RamAttackA.wav", + "Sound\\Creature\\Ram\\RamAttackB.wav", + "Sound\\Creature\\Ram\\RamAttackC.wav", + }, s.jump); + loadSoundList(assetManager_, { + "Sound\\Creature\\Ram\\RamWoundA.wav", + "Sound\\Creature\\Ram\\RamWoundB.wav", + "Sound\\Creature\\Ram\\RamWoundC.wav", + }, s.land); + loadSoundList(assetManager_, { + "Sound\\Creature\\Ram\\RamPreAggro.wav", + }, s.idle); + } + + // Tiger (also saber cats / nightsaber) + { + auto& s = familySounds_[MountFamily::TIGER]; + loadSoundList(assetManager_, { + "Sound\\Creature\\Tiger\\mTigerAggroA.wav", + }, s.move); + loadSoundList(assetManager_, { + "Sound\\Creature\\Tiger\\mTigerAttackA.wav", + "Sound\\Creature\\Tiger\\mTigerAttackB.wav", + "Sound\\Creature\\Tiger\\mTigerAttackC.wav", + "Sound\\Creature\\Tiger\\mTigerAttackD.wav", + }, s.jump); + loadSoundList(assetManager_, { + "Sound\\Creature\\Tiger\\mTigerWoundA.wav", + "Sound\\Creature\\Tiger\\mTigerWoundB.wav", + "Sound\\Creature\\Tiger\\mTigerWoundC.wav", + }, s.land); + loadSoundList(assetManager_, { + "Sound\\Creature\\Tiger\\mTigerStand2A.wav", + }, s.idle); + } + + // Raptor + { + auto& s = familySounds_[MountFamily::RAPTOR]; + loadSoundList(assetManager_, { + "Sound\\Creature\\Raptor\\mRaptorAggroA.wav", + }, s.move); + loadSoundList(assetManager_, { + "Sound\\Creature\\Raptor\\mRaptorAttackA.wav", + "Sound\\Creature\\Raptor\\mRaptorAttackB.wav", + "Sound\\Creature\\Raptor\\mRaptorAttackC.wav", + }, s.jump); + loadSoundList(assetManager_, { + "Sound\\Creature\\Raptor\\mRaptorWoundA.wav", + "Sound\\Creature\\Raptor\\mRaptorWoundB.wav", + "Sound\\Creature\\Raptor\\mRaptorWoundC.wav", + }, s.land); + loadSoundList(assetManager_, { + "Sound\\Creature\\Raptor\\mRaptorAggroA.wav", + }, s.idle); + } + + // Kodo + { + auto& s = familySounds_[MountFamily::KODO]; + loadSoundList(assetManager_, { + "Sound\\Creature\\KodoBeast\\KodoBeastAggroA.wav", + }, s.move); + loadSoundList(assetManager_, { + "Sound\\Creature\\KodoBeast\\KodoBeastAttackA.wav", + "Sound\\Creature\\KodoBeast\\KodoBeastAttackB.wav", + "Sound\\Creature\\KodoBeast\\KodoBeastAttackC.wav", + }, s.jump); + loadSoundList(assetManager_, { + "Sound\\Creature\\KodoBeast\\KodoBeastWoundA.wav", + "Sound\\Creature\\KodoBeast\\KodoBeastWoundB.wav", + "Sound\\Creature\\KodoBeast\\KodoBeastWoundC.wav", + }, s.land); + loadSoundList(assetManager_, { + "Sound\\Creature\\KodoBeast\\KodoBeastStand02A.wav", + }, s.idle); + } + + // Mechanostrider + { + auto& s = familySounds_[MountFamily::MECHANOSTRIDER]; + loadSoundList(assetManager_, { + "Sound\\Creature\\MechaStrider\\MechaStriderAggro.wav", + "Sound\\Creature\\MechaStrider\\MechaStriderPreAggro.wav", + }, s.move); + loadSoundList(assetManager_, { + "Sound\\Creature\\MechaStrider\\MechaStriderAttackA.wav", + "Sound\\Creature\\MechaStrider\\MechaStriderAttackB.wav", + "Sound\\Creature\\MechaStrider\\MechaStriderAttackC.wav", + }, s.jump); + loadSoundList(assetManager_, { + "Sound\\Creature\\MechaStrider\\MechaStriderAttackA.wav", + }, s.land); + loadSoundList(assetManager_, { + "Sound\\Creature\\MechaStrider\\MechaStriderPreAggro.wav", + }, s.idle); + } + + // Tallstrider (plainstrider mounts) + { + auto& s = familySounds_[MountFamily::TALLSTRIDER]; + loadSoundList(assetManager_, { + "Sound\\Creature\\Tallstrider\\TallstriderAggro.wav", + "Sound\\Creature\\Tallstrider\\TallstriderPreAggro.wav", + }, s.move); + loadSoundList(assetManager_, { + "Sound\\Creature\\Tallstrider\\TallstriderAttackA.wav", + "Sound\\Creature\\Tallstrider\\TallstriderAttackB.wav", + "Sound\\Creature\\Tallstrider\\TallstriderAttackC.wav", + }, s.jump); + loadSoundList(assetManager_, { + "Sound\\Creature\\Tallstrider\\TallstriderWoundA.wav", + "Sound\\Creature\\Tallstrider\\TallstriderWoundB.wav", + "Sound\\Creature\\Tallstrider\\TallstriderWoundC.wav", + }, s.land); + loadSoundList(assetManager_, { + "Sound\\Creature\\Tallstrider\\TallstriderPreAggro.wav", + }, s.idle); + } + + // Undead horse (skeletal warhorse) + { + auto& s = familySounds_[MountFamily::UNDEAD_HORSE]; + loadSoundList(assetManager_, { + "Sound\\Creature\\HorseUndead\\HorseUndeadAggro.wav", + }, s.move); + loadSoundList(assetManager_, { + "Sound\\Creature\\HorseUndead\\HorseUndeadAttackA.wav", + "Sound\\Creature\\HorseUndead\\HorseUndeadAttackB.wav", + "Sound\\Creature\\HorseUndead\\HorseUndeadAttackC.wav", + }, s.jump); + loadSoundList(assetManager_, { + "Sound\\Creature\\HorseUndead\\HorseUndeadWoundA.wav", + "Sound\\Creature\\HorseUndead\\HorseUndeadWoundB.wav", + "Sound\\Creature\\HorseUndead\\HorseUndeadWoundC.wav", + }, s.land); + loadSoundList(assetManager_, { + "Sound\\Creature\\HorseUndead\\HorseUndeadPreAggro.wav", + }, s.idle); + } + + // Log loaded families + for (const auto& [family, sounds] : familySounds_) { + int total = sounds.move.size() + sounds.jump.size() + + sounds.land.size() + sounds.idle.size(); + if (total > 0) { + LOG_INFO("Mount family ", static_cast(family), ": ", + sounds.move.size(), " move, ", sounds.jump.size(), " jump, ", + sounds.land.size(), " land, ", sounds.idle.size(), " idle"); } } - - // Ground mount movement ambient (alerts/whinnies) - std::vector horseMovePaths = { - "Sound\\Creature\\Horse\\mHorseAggroA.wav", - }; - - for (const auto& path : horseMovePaths) { - MountSample sample; - if (loadSound(path, sample)) { - horseMoveSounds_.push_back(std::move(sample)); - } - } - - // Ground mount jump effort sounds - std::vector horseJumpPaths = { - "Sound\\Creature\\Horse\\mHorseAttackA.wav", - "Sound\\Creature\\Horse\\mHorseAttackB.wav", - "Sound\\Creature\\Horse\\mHorseAttackC.wav", - }; - - for (const auto& path : horseJumpPaths) { - MountSample sample; - if (loadSound(path, sample)) { - horseJumpSounds_.push_back(std::move(sample)); - } - } - - // Ground mount landing thud sounds - std::vector horseLandPaths = { - "Sound\\Creature\\Horse\\mHorseWoundA.wav", - "Sound\\Creature\\Horse\\mHorseWoundB.wav", - }; - - for (const auto& path : horseLandPaths) { - MountSample sample; - if (loadSound(path, sample)) { - horseLandSounds_.push_back(std::move(sample)); - } - } - - // Ground mount idle ambient (snorts and whinnies only) - std::vector horseIdlePaths = { - "Sound\\Creature\\Horse\\mHorseStand3A.wav", // Snort - "Sound\\Creature\\Horse\\mHorseAggroA.wav", // Whinny - }; - - for (const auto& path : horseIdlePaths) { - MountSample sample; - if (loadSound(path, sample)) { - horseIdleSounds_.push_back(std::move(sample)); - } - } - - if (!wingFlapSounds_.empty()) { - LOG_INFO("Loaded ", wingFlapSounds_.size(), " wing flap sounds"); - } - if (!wingIdleSounds_.empty()) { - LOG_INFO("Loaded ", wingIdleSounds_.size(), " wing idle sounds"); - } - if (!horseBreathSounds_.empty()) { - LOG_INFO("Loaded ", horseBreathSounds_.size(), " horse breath sounds"); - } - if (!horseMoveSounds_.empty()) { - LOG_INFO("Loaded ", horseMoveSounds_.size(), " horse move sounds"); - } - if (!horseJumpSounds_.empty()) { - LOG_INFO("Loaded ", horseJumpSounds_.size(), " horse jump sounds"); - } - if (!horseLandSounds_.empty()) { - LOG_INFO("Loaded ", horseLandSounds_.size(), " horse land sounds"); - } - if (!horseIdleSounds_.empty()) { - LOG_INFO("Loaded ", horseIdleSounds_.size(), " horse idle sounds (snorts/whinnies)"); - } } bool MountSoundManager::loadSound(const std::string& path, MountSample& sample) { if (!assetManager_ || !assetManager_->fileExists(path)) { - LOG_WARNING("Mount sound file not found: ", path); return false; } auto data = assetManager_->readFile(path); if (data.empty()) { - LOG_WARNING("Mount sound file empty: ", path); return false; } sample.path = path; sample.data = std::move(data); - LOG_INFO("Loaded mount sound: ", path); return true; } +const MountSoundManager::FamilySounds& MountSoundManager::getCurrentFamilySounds() const { + static const FamilySounds empty; + + auto it = familySounds_.find(currentMountFamily_); + if (it != familySounds_.end()) { + return it->second; + } + + // Fall back to horse for unknown families + it = familySounds_.find(MountFamily::HORSE); + if (it != familySounds_.end()) { + return it->second; + } + + return empty; +} + void MountSoundManager::update(float deltaTime) { if (!mounted_) { soundLoopTimer_ = 0.0f; @@ -192,18 +339,26 @@ void MountSoundManager::update(float deltaTime) { updateMountSounds(); } -void MountSoundManager::onMount(uint32_t creatureDisplayId, bool isFlying) { +void MountSoundManager::onMount(uint32_t creatureDisplayId, bool isFlying, const std::string& modelPath) { mounted_ = true; currentDisplayId_ = creatureDisplayId; currentMountType_ = detectMountType(creatureDisplayId); - currentMountFamily_ = detectMountFamily(creatureDisplayId); + + // Prefer model path detection (reliable) over display ID ranges (fragile) + if (!modelPath.empty()) { + currentMountFamily_ = detectMountFamilyFromPath(modelPath); + } else { + currentMountFamily_ = detectMountFamily(creatureDisplayId); + } + flying_ = isFlying; moving_ = false; LOG_INFO("Mount sound: mounted on display ID ", creatureDisplayId, " type=", static_cast(currentMountType_), " family=", static_cast(currentMountFamily_), - " flying=", flying_); + " flying=", flying_, + " model=", modelPath); updateMountSounds(); } @@ -212,6 +367,7 @@ void MountSoundManager::onDismount() { stopAllMountSounds(); mounted_ = false; currentMountType_ = MountType::NONE; + currentMountFamily_ = MountFamily::UNKNOWN; currentDisplayId_ = 0; flying_ = false; moving_ = false; @@ -242,23 +398,22 @@ void MountSoundManager::setGrounded(bool grounded) { void MountSoundManager::playRearUpSound() { if (!mounted_) return; - // Cooldown to prevent spam (200ms) auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast(now - lastActionSoundTime_).count(); if (elapsed < 200) return; lastActionSoundTime_ = now; - // Use semantic sound based on mount family - if (currentMountType_ == MountType::GROUND && !horseMoveSounds_.empty()) { - // Ground mounts: whinny/roar - static std::mt19937 rng(std::random_device{}()); - std::uniform_int_distribution dist(0, horseMoveSounds_.size() - 1); - const auto& sample = horseMoveSounds_[dist(rng)]; - if (!sample.data.empty()) { - AudioEngine::instance().playSound2D(sample.data, 0.7f * volumeScale_, 1.0f); + if (currentMountType_ == MountType::GROUND) { + const auto& sounds = getCurrentFamilySounds(); + if (!sounds.move.empty()) { + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, sounds.move.size() - 1); + const auto& sample = sounds.move[dist(rng)]; + if (!sample.data.empty()) { + AudioEngine::instance().playSound2D(sample.data, 0.7f * volumeScale_, 1.0f); + } } } else if (currentMountType_ == MountType::FLYING && !wingIdleSounds_.empty()) { - // Flying mounts: screech/roar static std::mt19937 rng(std::random_device{}()); std::uniform_int_distribution dist(0, wingIdleSounds_.size() - 1); const auto& sample = wingIdleSounds_[dist(rng)]; @@ -271,22 +426,22 @@ void MountSoundManager::playRearUpSound() { void MountSoundManager::playJumpSound() { if (!mounted_) return; - // Cooldown to prevent spam auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast(now - lastActionSoundTime_).count(); if (elapsed < 200) return; lastActionSoundTime_ = now; - // Jump effort sound - if (currentMountType_ == MountType::GROUND && !horseJumpSounds_.empty()) { - static std::mt19937 rng(std::random_device{}()); - std::uniform_int_distribution dist(0, horseJumpSounds_.size() - 1); - const auto& sample = horseJumpSounds_[dist(rng)]; - if (!sample.data.empty()) { - AudioEngine::instance().playSound2D(sample.data, 0.5f * volumeScale_, 1.1f); + if (currentMountType_ == MountType::GROUND) { + const auto& sounds = getCurrentFamilySounds(); + if (!sounds.jump.empty()) { + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, sounds.jump.size() - 1); + const auto& sample = sounds.jump[dist(rng)]; + if (!sample.data.empty()) { + AudioEngine::instance().playSound2D(sample.data, 0.5f * volumeScale_, 1.1f); + } } } else if (currentMountType_ == MountType::FLYING && !wingFlapSounds_.empty()) { - // Flying mounts: wing whoosh static std::mt19937 rng(std::random_device{}()); std::uniform_int_distribution dist(0, wingFlapSounds_.size() - 1); const auto& sample = wingFlapSounds_[dist(rng)]; @@ -299,19 +454,20 @@ void MountSoundManager::playJumpSound() { void MountSoundManager::playLandSound() { if (!mounted_) return; - // Cooldown to prevent spam auto now = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast(now - lastActionSoundTime_).count(); if (elapsed < 200) return; lastActionSoundTime_ = now; - // Landing thud/hoof sound - if (currentMountType_ == MountType::GROUND && !horseLandSounds_.empty()) { - static std::mt19937 rng(std::random_device{}()); - std::uniform_int_distribution dist(0, horseLandSounds_.size() - 1); - const auto& sample = horseLandSounds_[dist(rng)]; - if (!sample.data.empty()) { - AudioEngine::instance().playSound2D(sample.data, 0.6f * volumeScale_, 0.85f); + if (currentMountType_ == MountType::GROUND) { + const auto& sounds = getCurrentFamilySounds(); + if (!sounds.land.empty()) { + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, sounds.land.size() - 1); + const auto& sample = sounds.land[dist(rng)]; + if (!sample.data.empty()) { + AudioEngine::instance().playSound2D(sample.data, 0.6f * volumeScale_, 0.85f); + } } } } @@ -319,62 +475,137 @@ void MountSoundManager::playLandSound() { void MountSoundManager::playIdleSound() { if (!mounted_ || moving_) return; - // Ambient idle sounds (snorts and whinnies only for ground mounts) - if (currentMountType_ == MountType::GROUND && !horseIdleSounds_.empty()) { - static std::mt19937 rng(std::random_device{}()); - std::uniform_int_distribution dist(0, horseIdleSounds_.size() - 1); - const auto& sample = horseIdleSounds_[dist(rng)]; - if (!sample.data.empty()) { - AudioEngine::instance().playSound2D(sample.data, 0.35f * volumeScale_, 0.95f); + if (currentMountType_ == MountType::GROUND) { + const auto& sounds = getCurrentFamilySounds(); + if (!sounds.idle.empty()) { + static std::mt19937 rng(std::random_device{}()); + std::uniform_int_distribution dist(0, sounds.idle.size() - 1); + const auto& sample = sounds.idle[dist(rng)]; + if (!sample.data.empty()) { + AudioEngine::instance().playSound2D(sample.data, 0.35f * volumeScale_, 0.95f); + } } } - // No idle sounds for flying mounts (wing sounds were too aggressive) } MountType MountSoundManager::detectMountType(uint32_t creatureDisplayId) const { - // TODO: Load from CreatureDisplayInfo.dbc or CreatureModelData.dbc - // For now, use simple heuristics based on common display IDs - // This is a placeholder - we'd need proper DBC parsing for accuracy - // Common flying mount display IDs (approximate ranges) - // Gryphons: ~2300-2310 - // Wyverns: ~1566-1570 - // Drakes: ~25830-25870 - // Phoenixes: ~17890-17900 - if (creatureDisplayId >= 2300 && creatureDisplayId <= 2320) return MountType::FLYING; // Gryphons if (creatureDisplayId >= 1560 && creatureDisplayId <= 1580) return MountType::FLYING; // Wyverns if (creatureDisplayId >= 25800 && creatureDisplayId <= 25900) return MountType::FLYING; // Drakes if (creatureDisplayId >= 17880 && creatureDisplayId <= 17910) return MountType::FLYING; // Phoenixes - // Most other mounts are ground return MountType::GROUND; } +MountFamily MountSoundManager::detectMountFamilyFromPath(const std::string& modelPath) const { + // Convert path to lowercase for matching + std::string lower = modelPath; + for (char& c : lower) c = std::tolower(c); + + // Check creature model path for family keywords + if (lower.find("tallstrider") != std::string::npos || + lower.find("plainstrider") != std::string::npos) + return MountFamily::TALLSTRIDER; + if (lower.find("wolf") != std::string::npos || + lower.find("direwolf") != std::string::npos) + return MountFamily::WOLF; + if (lower.find("tiger") != std::string::npos || + lower.find("nightsaber") != std::string::npos || + lower.find("panther") != std::string::npos || + lower.find("frostsaber") != std::string::npos) + return MountFamily::TIGER; + if (lower.find("raptor") != std::string::npos) + return MountFamily::RAPTOR; + if (lower.find("kodo") != std::string::npos) + return MountFamily::KODO; + if (lower.find("mechastrider") != std::string::npos || + lower.find("mechanostrider") != std::string::npos) + return MountFamily::MECHANOSTRIDER; + if (lower.find("ram") != std::string::npos) + return MountFamily::RAM; + if (lower.find("horseundead") != std::string::npos || + lower.find("skeletalhorse") != std::string::npos) + return MountFamily::UNDEAD_HORSE; + if (lower.find("horse") != std::string::npos) + return MountFamily::HORSE; + if (lower.find("hawkstrider") != std::string::npos) + return MountFamily::TALLSTRIDER; // BC hawkstriders share tallstrider sounds + if (lower.find("dragon") != std::string::npos || + lower.find("drake") != std::string::npos || + lower.find("gryphon") != std::string::npos || + lower.find("wyvern") != std::string::npos) + return MountFamily::DRAGON; + + return MountFamily::HORSE; // Default fallback +} + MountFamily MountSoundManager::detectMountFamily(uint32_t creatureDisplayId) const { - // Heuristic creature family detection based on common display ID ranges - // TODO: Replace with proper CreatureModelData.dbc lookup + // Undead horses (skeletal warhorses) — check before generic horse range + // Display IDs: 10670 (Skeletal Horse), 10671-10672, 5765-5768 + if ((creatureDisplayId >= 10670 && creatureDisplayId <= 10672) || + (creatureDisplayId >= 5765 && creatureDisplayId <= 5768)) + return MountFamily::UNDEAD_HORSE; - // Horses: ~14000-14999 range (includes many horse variants) - if (creatureDisplayId >= 14000 && creatureDisplayId < 15000) return MountFamily::HORSE; + // Horses: common palomino/pinto/black stallion etc. + // 2402-2410 (standard horses), 14337-14348 (epic horses) + if ((creatureDisplayId >= 2402 && creatureDisplayId <= 2410) || + (creatureDisplayId >= 14337 && creatureDisplayId <= 14348) || + creatureDisplayId == 2404 || creatureDisplayId == 2405) + return MountFamily::HORSE; - // Rams: ~14349-14375 range - if (creatureDisplayId >= 14349 && creatureDisplayId <= 14375) return MountFamily::RAM; + // Rams: Dwarf ram mounts + // 2736-2737 (standard rams), 14347-14353 (epic rams), 14577-14578 + if ((creatureDisplayId >= 2736 && creatureDisplayId <= 2737) || + (creatureDisplayId >= 14347 && creatureDisplayId <= 14353) || + (creatureDisplayId >= 14577 && creatureDisplayId <= 14578)) + return MountFamily::RAM; - // Wolves: ~207-217, ~2326-2329 ranges - if ((creatureDisplayId >= 207 && creatureDisplayId <= 217) || - (creatureDisplayId >= 2326 && creatureDisplayId <= 2329)) return MountFamily::WOLF; + // Wolves: Orc wolf mounts + // 207-212 (dire wolves), 2320-2328 (epic wolves), 14573-14575 + if ((creatureDisplayId >= 207 && creatureDisplayId <= 212) || + (creatureDisplayId >= 2320 && creatureDisplayId <= 2330) || + (creatureDisplayId >= 14573 && creatureDisplayId <= 14575)) + return MountFamily::WOLF; - // Tigers/Cats: ~6442-6473 range - if (creatureDisplayId >= 6442 && creatureDisplayId <= 6473) return MountFamily::TIGER; + // Tigers/Nightsabers: Night elf cat mounts + // 6068-6074 (nightsabers), 6075-6080 (epic/frostsaber), 9991-9992, 14328-14332 + if ((creatureDisplayId >= 6068 && creatureDisplayId <= 6080) || + (creatureDisplayId >= 9991 && creatureDisplayId <= 9992) || + (creatureDisplayId >= 14328 && creatureDisplayId <= 14332)) + return MountFamily::TIGER; - // Raptors: ~6466-6474 range - if (creatureDisplayId >= 6466 && creatureDisplayId <= 6474) return MountFamily::RAPTOR; + // Raptors: Troll raptor mounts + // 6469-6474 (standard raptors), 14338-14344 (epic raptors), 4806 + if ((creatureDisplayId >= 6469 && creatureDisplayId <= 6474) || + (creatureDisplayId >= 14338 && creatureDisplayId <= 14344) || + creatureDisplayId == 4806) + return MountFamily::RAPTOR; - // Dragons/Drakes - if (creatureDisplayId >= 25800 && creatureDisplayId <= 25900) return MountFamily::DRAGON; + // Kodo: Tauren kodo mounts + // 11641-11643 (standard kodo), 14348-14349 (great kodo), 12246 + if ((creatureDisplayId >= 11641 && creatureDisplayId <= 11643) || + (creatureDisplayId >= 14348 && creatureDisplayId <= 14349) || + creatureDisplayId == 12246 || creatureDisplayId == 14349) + return MountFamily::KODO; - // Default to horse for unknown ground mounts + // Mechanostriders: Gnome mechanostrider mounts + // 6569-6571 (standard), 9473-9476 (epic), 10180, 14584 + if ((creatureDisplayId >= 6569 && creatureDisplayId <= 6571) || + (creatureDisplayId >= 9473 && creatureDisplayId <= 9476) || + creatureDisplayId == 10180 || creatureDisplayId == 14584) + return MountFamily::MECHANOSTRIDER; + + // Tallstriders (plainstrider mounts — Turtle WoW custom or future) + // 1961-1964 + if (creatureDisplayId >= 1961 && creatureDisplayId <= 1964) + return MountFamily::TALLSTRIDER; + + // Dragons/Drakes (flying) + if (creatureDisplayId >= 25800 && creatureDisplayId <= 25900) + return MountFamily::DRAGON; + + // Default to HORSE for unknown ground mounts (safe fallback) return MountFamily::HORSE; } @@ -388,66 +619,51 @@ void MountSoundManager::updateMountSounds() { // Flying mounts if (currentMountType_ == MountType::FLYING && flying_) { if (moving_ && !wingFlapSounds_.empty()) { - // Wing flaps when moving (play periodically for continuous flapping sound) if (soundLoopTimer_ >= 1.2f) { std::uniform_int_distribution dist(0, wingFlapSounds_.size() - 1); const auto& sample = wingFlapSounds_[dist(rng)]; std::uniform_real_distribution volumeDist(0.4f, 0.5f); std::uniform_real_distribution pitchDist(0.95f, 1.05f); AudioEngine::instance().playSound2D( - sample.data, - volumeDist(rng) * volumeScale_, - pitchDist(rng) - ); + sample.data, volumeDist(rng) * volumeScale_, pitchDist(rng)); soundLoopTimer_ = 0.0f; playingMovementSound_ = true; } } else if (!moving_ && !wingIdleSounds_.empty()) { - // Idle/hovering sounds (less frequent) if (soundLoopTimer_ >= 3.5f) { std::uniform_int_distribution dist(0, wingIdleSounds_.size() - 1); const auto& sample = wingIdleSounds_[dist(rng)]; std::uniform_real_distribution volumeDist(0.3f, 0.4f); std::uniform_real_distribution pitchDist(0.98f, 1.02f); AudioEngine::instance().playSound2D( - sample.data, - volumeDist(rng) * volumeScale_, - pitchDist(rng) - ); + sample.data, volumeDist(rng) * volumeScale_, pitchDist(rng)); soundLoopTimer_ = 0.0f; playingIdleSound_ = true; } } } - // Ground mounts + // Ground mounts — use per-family sounds else if (currentMountType_ == MountType::GROUND && !flying_) { - if (moving_ && !horseMoveSounds_.empty()) { - // Occasional whinny/ambient sounds while moving + const auto& sounds = getCurrentFamilySounds(); + if (moving_ && !sounds.move.empty()) { if (soundLoopTimer_ >= 8.0f) { - std::uniform_int_distribution dist(0, horseMoveSounds_.size() - 1); - const auto& sample = horseMoveSounds_[dist(rng)]; + std::uniform_int_distribution dist(0, sounds.move.size() - 1); + const auto& sample = sounds.move[dist(rng)]; std::uniform_real_distribution volumeDist(0.35f, 0.45f); std::uniform_real_distribution pitchDist(0.97f, 1.03f); AudioEngine::instance().playSound2D( - sample.data, - volumeDist(rng) * volumeScale_, - pitchDist(rng) - ); + sample.data, volumeDist(rng) * volumeScale_, pitchDist(rng)); soundLoopTimer_ = 0.0f; playingMovementSound_ = true; } - } else if (!moving_ && !horseBreathSounds_.empty()) { - // Breathing/snorting when idle + } else if (!moving_ && !sounds.idle.empty()) { if (soundLoopTimer_ >= 4.5f) { - std::uniform_int_distribution dist(0, horseBreathSounds_.size() - 1); - const auto& sample = horseBreathSounds_[dist(rng)]; + std::uniform_int_distribution dist(0, sounds.idle.size() - 1); + const auto& sample = sounds.idle[dist(rng)]; std::uniform_real_distribution volumeDist(0.25f, 0.35f); std::uniform_real_distribution pitchDist(0.98f, 1.02f); AudioEngine::instance().playSound2D( - sample.data, - volumeDist(rng) * volumeScale_, - pitchDist(rng) - ); + sample.data, volumeDist(rng) * volumeScale_, pitchDist(rng)); soundLoopTimer_ = 0.0f; playingIdleSound_ = true; } @@ -456,7 +672,6 @@ void MountSoundManager::updateMountSounds() { } void MountSoundManager::stopAllMountSounds() { - // Reset state flags playingMovementSound_ = false; playingIdleSound_ = false; soundLoopTimer_ = 0.0f; diff --git a/src/core/application.cpp b/src/core/application.cpp index a5f788b1..2c601e20 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -712,7 +712,12 @@ void Application::update(float deltaTime) { } if (renderer && renderer->getCameraController()) { const bool externallyDrivenMotion = onTaxi || onTransportNow; - renderer->getCameraController()->setExternalFollow(externallyDrivenMotion); + // Keep physics frozen (externalFollow) during landing clamp when terrain + // hasn't loaded yet — prevents gravity from pulling player through void. + bool landingClampActive = !onTaxi && taxiLandingClampTimer_ > 0.0f && + worldEntryMovementGraceTimer_ <= 0.0f && + !gameHandler->isMounted(); + renderer->getCameraController()->setExternalFollow(externallyDrivenMotion || landingClampActive); renderer->getCameraController()->setExternalMoving(externallyDrivenMotion); if (externallyDrivenMotion) { // Drop any stale local movement toggles while server drives taxi motion. @@ -721,15 +726,11 @@ void Application::update(float deltaTime) { } if (lastTaxiFlight_ && !onTaxi) { renderer->getCameraController()->clearMovementInputs(); - // Keep clamping for a short grace window after taxi ends to avoid - // falling through WMOs while floor/collision state settles. - taxiLandingClampTimer_ = 1.5f; + // Keep clamping until terrain loads at landing position. + // Timer only counts down once a valid floor is found. + taxiLandingClampTimer_ = 2.0f; } - if (!onTaxi && - worldEntryMovementGraceTimer_ <= 0.0f && - !gameHandler->isMounted() && - taxiLandingClampTimer_ > 0.0f) { - taxiLandingClampTimer_ -= deltaTime; + if (landingClampActive) { if (renderer && gameHandler) { glm::vec3 p = renderer->getCharacterPosition(); std::optional terrainFloor; @@ -753,17 +754,18 @@ void Application::update(float deltaTime) { if (m2Floor && (!targetFloor || *m2Floor > *targetFloor)) targetFloor = m2Floor; if (targetFloor) { + // Floor found — snap player to it and start countdown to release float targetZ = *targetFloor + 0.10f; - // Only lift upward to prevent sinking through floors/bridges. - // Never force the player downward from a valid elevated surface. - if (p.z < targetZ - 0.05f) { + if (std::abs(p.z - targetZ) > 0.05f) { p.z = targetZ; renderer->getCharacterPosition() = p; glm::vec3 canonical = core::coords::renderToCanonical(p); gameHandler->setPosition(canonical.x, canonical.y, canonical.z); gameHandler->sendMovement(game::Opcode::CMSG_MOVE_HEARTBEAT); } + taxiLandingClampTimer_ -= deltaTime; } + // No floor found: don't decrement timer, keep player frozen until terrain loads } } bool idleOrbit = renderer->getCameraController()->isIdleOrbit(); @@ -1130,12 +1132,11 @@ void Application::setupUICallbacks() { pos.z += 20.0f; } - // Nudge forward slightly to break edge-cases where unstuck lands exactly - // on problematic collision seams. + // Nudge forward to break free of collision seams / stuck geometry. if (gameHandler) { float renderYaw = gameHandler->getMovementInfo().orientation + glm::radians(90.0f); - pos.x += std::cos(renderYaw) * 2.0f; - pos.y += std::sin(renderYaw) * 2.0f; + pos.x += std::cos(renderYaw) * 5.0f; + pos.y += std::sin(renderYaw) * 5.0f; } cc->teleportTo(pos); @@ -4675,7 +4676,7 @@ void Application::processPendingMount() { } } - renderer->setMounted(instanceId, mountDisplayId, heightOffset); + renderer->setMounted(instanceId, mountDisplayId, heightOffset, m2Path); charRenderer->playAnimation(instanceId, 0, true); LOG_INFO("processPendingMount: DONE displayId=", mountDisplayId, " model=", m2Path, " heightOffset=", heightOffset); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 72b1bffd..66050251 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3280,6 +3280,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { mountAuraSpellId_ = a.spellId; } } + // Classic/vanilla fallback: scan UNIT_FIELD_AURAS from same update block + if (mountAuraSpellId_ == 0) { + const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS); + if (ufAuras != 0xFFFF) { + for (const auto& [fk, fv] : block.fields) { + if (fk >= ufAuras && fk < ufAuras + 48 && fv != 0) { + mountAuraSpellId_ = fv; + break; + } + } + } + } LOG_INFO("Mount detected: displayId=", val, " auraSpellId=", mountAuraSpellId_); } if (old != 0 && val == 0) { @@ -3670,6 +3682,18 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { mountAuraSpellId_ = a.spellId; } } + // Classic/vanilla fallback: scan UNIT_FIELD_AURAS from same update block + if (mountAuraSpellId_ == 0) { + const uint16_t ufAuras = fieldIndex(UF::UNIT_FIELD_AURAS); + if (ufAuras != 0xFFFF) { + for (const auto& [fk, fv] : block.fields) { + if (fk >= ufAuras && fk < ufAuras + 48 && fv != 0) { + mountAuraSpellId_ = fv; + break; + } + } + } + } LOG_INFO("Mount detected (values update): displayId=", val, " auraSpellId=", mountAuraSpellId_); } if (old != 0 && val == 0) { @@ -8594,6 +8618,21 @@ void GameHandler::updateClientTaxi(float deltaTime) { auto playerEntity = entityManager.getEntity(playerGuid); auto finishTaxiFlight = [&]() { + // Snap player to the last waypoint (landing position) before clearing state. + // Without this, the player would be left at whatever mid-flight position + // they were at when the path completion was detected. + if (!taxiClientPath_.empty()) { + const auto& landingPos = taxiClientPath_.back(); + if (playerEntity) { + playerEntity->setPosition(landingPos.x, landingPos.y, landingPos.z, + movementInfo.orientation); + } + movementInfo.x = landingPos.x; + movementInfo.y = landingPos.y; + movementInfo.z = landingPos.z; + LOG_INFO("Taxi landing: snapped to final waypoint (", + landingPos.x, ", ", landingPos.y, ", ", landingPos.z, ")"); + } taxiClientActive_ = false; onTaxiFlight_ = false; taxiLandingCooldown_ = 2.0f; // 2 second cooldown to prevent re-entering diff --git a/src/game/update_field_table.cpp b/src/game/update_field_table.cpp index 6114fce8..d03c01b9 100644 --- a/src/game/update_field_table.cpp +++ b/src/game/update_field_table.cpp @@ -32,6 +32,7 @@ static const UFNameEntry kUFNames[] = { {"UNIT_FIELD_FLAGS_2", UF::UNIT_FIELD_FLAGS_2}, {"UNIT_FIELD_DISPLAYID", UF::UNIT_FIELD_DISPLAYID}, {"UNIT_FIELD_MOUNTDISPLAYID", UF::UNIT_FIELD_MOUNTDISPLAYID}, + {"UNIT_FIELD_AURAS", UF::UNIT_FIELD_AURAS}, {"UNIT_NPC_FLAGS", UF::UNIT_NPC_FLAGS}, {"UNIT_DYNAMIC_FLAGS", UF::UNIT_DYNAMIC_FLAGS}, {"UNIT_END", UF::UNIT_END}, diff --git a/src/rendering/m2_renderer.cpp b/src/rendering/m2_renderer.cpp index 4dfcb2d3..771ba9cd 100644 --- a/src/rendering/m2_renderer.cpp +++ b/src/rendering/m2_renderer.cpp @@ -895,7 +895,23 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) { (lowerName.find("seaweed") != std::string::npos) || (lowerName.find("kelp") != std::string::npos) || (lowerName.find("cattail") != std::string::npos) || - (lowerName.find("reed") != std::string::npos); + (lowerName.find("reed") != std::string::npos) || + (lowerName.find("palm") != std::string::npos) || + (lowerName.find("bamboo") != std::string::npos) || + (lowerName.find("banana") != std::string::npos) || + (lowerName.find("coconut") != std::string::npos) || + (lowerName.find("canopy") != std::string::npos) || + (lowerName.find("hedge") != std::string::npos) || + (lowerName.find("cactus") != std::string::npos) || + (lowerName.find("leaf") != std::string::npos) || + (lowerName.find("leaves") != std::string::npos) || + (lowerName.find("stalk") != std::string::npos) || + (lowerName.find("corn") != std::string::npos) || + (lowerName.find("crop") != std::string::npos) || + (lowerName.find("hay") != std::string::npos) || + (lowerName.find("frond") != std::string::npos) || + (lowerName.find("algae") != std::string::npos) || + (lowerName.find("coral") != std::string::npos); bool treeLike = (lowerName.find("tree") != std::string::npos); foliageOrTreeLike = (foliageName || treeLike); bool hardTreePart = diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index e9276dd9..59c2a39f 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -576,7 +576,7 @@ void Renderer::setCharacterFollow(uint32_t instanceId) { } } -void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset) { +void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath) { mountInstanceId_ = mountInstId; mountHeightOffset_ = heightOffset; mountSeatAttachmentId_ = -1; @@ -812,7 +812,7 @@ void Renderer::setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float h // Notify mount sound manager if (mountSoundManager) { bool isFlying = taxiFlight_; // Taxi flights are flying mounts - mountSoundManager->onMount(mountDisplayId, isFlying); + mountSoundManager->onMount(mountDisplayId, isFlying, modelPath); } } diff --git a/src/rendering/wmo_renderer.cpp b/src/rendering/wmo_renderer.cpp index 8694db33..f87a87d3 100644 --- a/src/rendering/wmo_renderer.cpp +++ b/src/rendering/wmo_renderer.cpp @@ -400,9 +400,12 @@ bool WMORenderer::loadModel(const pipeline::WMOModel& model, uint32_t id) { } groupRes.mergedBatches.reserve(batchMap.size()); + bool anyTextured = false; for (auto& [key, mb] : batchMap) { + if (mb.hasTexture) anyTextured = true; groupRes.mergedBatches.push_back(std::move(mb)); } + groupRes.allUntextured = !anyTextured && !groupRes.mergedBatches.empty(); } // Copy portal data for visibility culling @@ -1198,6 +1201,18 @@ void WMORenderer::render(const Camera& camera, const glm::mat4& view, const glm: for (uint32_t gi : dl.visibleGroups) { const auto& group = model.groups[gi]; + // Skip groups with SHOW_SKYBOX flag (0x20000) — these are transparent + // sky windows meant to show the skybox behind them, not solid geometry + if (group.groupFlags & 0x20000) { + continue; + } + + // Skip groups where ALL batches use the fallback white texture — + // these are collision/placeholder/LOD shell groups with no visual data + if (group.allUntextured) { + continue; + } + // STORMWIND.WMO specific fix: LOD shell visibility control // Combination of distance culling + backface culling for best results bool isLODShell = false;