From 71c4fb3ae634ac9b3e7edfa3e0b2e942d0c25e95 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Feb 2026 01:26:28 -0800 Subject: [PATCH] Fix crash in mount dust: add null check for camera pointer The mount dust code was calling camera->getForward() without checking if camera was null first, causing a crash that prevented mounting/dismounting. Added camera null check to the condition. --- include/audio/npc_voice_manager.hpp | 74 ++++++++++ src/audio/npc_voice_manager.cpp | 212 ++++++++++++++++++++++++++++ src/rendering/renderer.cpp | 2 +- 3 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 include/audio/npc_voice_manager.hpp create mode 100644 src/audio/npc_voice_manager.cpp diff --git a/include/audio/npc_voice_manager.hpp b/include/audio/npc_voice_manager.hpp new file mode 100644 index 00000000..4790dfa7 --- /dev/null +++ b/include/audio/npc_voice_manager.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { class AssetManager; } + +namespace audio { + +struct VoiceSample { + std::string path; + std::vector data; +}; + +// NPC voice types (based on creature model/gender) +enum class VoiceType { + HUMAN_MALE, + HUMAN_FEMALE, + DWARF_MALE, + DWARF_FEMALE, + NIGHTELF_MALE, + NIGHTELF_FEMALE, + ORC_MALE, + ORC_FEMALE, + TAUREN_MALE, + TAUREN_FEMALE, + TROLL_MALE, + TROLL_FEMALE, + UNDEAD_MALE, + UNDEAD_FEMALE, + GNOME_MALE, + GNOME_FEMALE, + GENERIC, // Fallback +}; + +class NpcVoiceManager { +public: + NpcVoiceManager(); + ~NpcVoiceManager(); + + bool initialize(pipeline::AssetManager* assets); + void shutdown(); + + // Play greeting sound for NPC at given position + void playGreeting(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position); + + void setVolumeScale(float scale) { volumeScale_ = scale; } + float getVolumeScale() const { return volumeScale_; } + +private: + void loadVoiceSounds(); + bool loadSound(const std::string& path, VoiceSample& sample); + VoiceType detectVoiceType(uint32_t creatureEntry) const; + + pipeline::AssetManager* assetManager_ = nullptr; + float volumeScale_ = 1.0f; + + // Voice samples grouped by type + std::unordered_map> voiceLibrary_; + + // Cooldown tracking (prevent spam clicking same NPC) + std::unordered_map lastPlayTime_; + static constexpr float GREETING_COOLDOWN = 2.0f; // seconds + + std::mt19937 rng_; +}; + +} // namespace audio +} // namespace wowee diff --git a/src/audio/npc_voice_manager.cpp b/src/audio/npc_voice_manager.cpp new file mode 100644 index 00000000..b7ec585c --- /dev/null +++ b/src/audio/npc_voice_manager.cpp @@ -0,0 +1,212 @@ +#include "audio/npc_voice_manager.hpp" +#include "audio/audio_engine.hpp" +#include "pipeline/asset_manager.hpp" +#include "core/logger.hpp" +#include + +namespace wowee { +namespace audio { + +NpcVoiceManager::NpcVoiceManager() : rng_(std::random_device{}()) {} + +NpcVoiceManager::~NpcVoiceManager() { + shutdown(); +} + +bool NpcVoiceManager::initialize(pipeline::AssetManager* assets) { + assetManager_ = assets; + if (!assetManager_) { + LOG_WARNING("NPC voice manager: no asset manager"); + return false; + } + + loadVoiceSounds(); + + int totalSamples = 0; + for (const auto& [type, samples] : voiceLibrary_) { + totalSamples += samples.size(); + } + LOG_INFO("NPC voice manager initialized (", totalSamples, " voice clips)"); + return true; +} + +void NpcVoiceManager::shutdown() { + voiceLibrary_.clear(); + lastPlayTime_.clear(); + assetManager_ = nullptr; +} + +void NpcVoiceManager::loadVoiceSounds() { + if (!assetManager_) return; + + // Generic NPC greetings (various creature sounds that work as greetings) + std::vector genericPaths = { + "Sound\\Character\\Human\\HumanMaleGreeting01.wav", + "Sound\\Character\\Human\\HumanMaleGreeting02.wav", + "Sound\\Character\\Human\\HumanMaleGreeting03.wav", + "Sound\\Character\\Human\\HumanFemaleGreeting01.wav", + "Sound\\Character\\Human\\HumanFemaleGreeting02.wav", + "Sound\\Character\\Dwarf\\DwarfMaleGreeting01.wav", + "Sound\\Character\\Dwarf\\DwarfMaleGreeting02.wav", + "Sound\\Character\\NightElf\\NightElfMaleGreeting01.wav", + "Sound\\Character\\NightElf\\NightElfFemaleGreeting01.wav", + }; + + auto& genericVoices = voiceLibrary_[VoiceType::GENERIC]; + for (const auto& path : genericPaths) { + VoiceSample sample; + if (loadSound(path, sample)) { + genericVoices.push_back(std::move(sample)); + } + } + + // Human male + std::vector humanMalePaths = { + "Sound\\Character\\Human\\HumanMaleGreeting01.wav", + "Sound\\Character\\Human\\HumanMaleGreeting02.wav", + "Sound\\Character\\Human\\HumanMaleGreeting03.wav", + "Sound\\Character\\Human\\HumanMaleYes01.wav", + "Sound\\Character\\Human\\HumanMaleYes02.wav", + }; + auto& humanMale = voiceLibrary_[VoiceType::HUMAN_MALE]; + for (const auto& path : humanMalePaths) { + VoiceSample sample; + if (loadSound(path, sample)) { + humanMale.push_back(std::move(sample)); + } + } + + // Human female + std::vector humanFemalePaths = { + "Sound\\Character\\Human\\HumanFemaleGreeting01.wav", + "Sound\\Character\\Human\\HumanFemaleGreeting02.wav", + "Sound\\Character\\Human\\HumanFemaleYes01.wav", + }; + auto& humanFemale = voiceLibrary_[VoiceType::HUMAN_FEMALE]; + for (const auto& path : humanFemalePaths) { + VoiceSample sample; + if (loadSound(path, sample)) { + humanFemale.push_back(std::move(sample)); + } + } + + // Dwarf male + std::vector dwarfMalePaths = { + "Sound\\Character\\Dwarf\\DwarfMaleGreeting01.wav", + "Sound\\Character\\Dwarf\\DwarfMaleGreeting02.wav", + "Sound\\Character\\Dwarf\\DwarfMaleYes01.wav", + }; + auto& dwarfMale = voiceLibrary_[VoiceType::DWARF_MALE]; + for (const auto& path : dwarfMalePaths) { + VoiceSample sample; + if (loadSound(path, sample)) { + dwarfMale.push_back(std::move(sample)); + } + } + + // Night elf male + std::vector nelfMalePaths = { + "Sound\\Character\\NightElf\\NightElfMaleGreeting01.wav", + "Sound\\Character\\NightElf\\NightElfMaleYes01.wav", + }; + auto& nelfMale = voiceLibrary_[VoiceType::NIGHTELF_MALE]; + for (const auto& path : nelfMalePaths) { + VoiceSample sample; + if (loadSound(path, sample)) { + nelfMale.push_back(std::move(sample)); + } + } + + // Night elf female + std::vector nelfFemalePaths = { + "Sound\\Character\\NightElf\\NightElfFemaleGreeting01.wav", + "Sound\\Character\\NightElf\\NightElfFemaleYes01.wav", + }; + auto& nelfFemale = voiceLibrary_[VoiceType::NIGHTELF_FEMALE]; + for (const auto& path : nelfFemalePaths) { + VoiceSample sample; + if (loadSound(path, sample)) { + nelfFemale.push_back(std::move(sample)); + } + } + + // Log loaded voice types + for (const auto& [type, samples] : voiceLibrary_) { + if (!samples.empty()) { + LOG_INFO("Loaded ", samples.size(), " voice samples for type ", static_cast(type)); + } + } +} + +bool NpcVoiceManager::loadSound(const std::string& path, VoiceSample& sample) { + if (!assetManager_ || !assetManager_->fileExists(path)) { + return false; + } + + auto data = assetManager_->readFile(path); + if (data.empty()) { + return false; + } + + sample.path = path; + sample.data = std::move(data); + return true; +} + +void NpcVoiceManager::playGreeting(uint64_t npcGuid, VoiceType voiceType, const glm::vec3& position) { + if (!AudioEngine::instance().isInitialized()) { + return; + } + + // Check cooldown + auto now = std::chrono::steady_clock::now(); + auto it = lastPlayTime_.find(npcGuid); + if (it != lastPlayTime_.end()) { + float elapsed = std::chrono::duration(now - it->second).count(); + if (elapsed < GREETING_COOLDOWN) { + return; // Still on cooldown + } + } + + // Find voice library for this type + auto libIt = voiceLibrary_.find(voiceType); + if (libIt == voiceLibrary_.end() || libIt->second.empty()) { + // Fall back to generic + libIt = voiceLibrary_.find(VoiceType::GENERIC); + if (libIt == voiceLibrary_.end() || libIt->second.empty()) { + return; // No voice samples available + } + } + + const auto& samples = libIt->second; + + // Pick random voice line + std::uniform_int_distribution dist(0, samples.size() - 1); + const auto& sample = samples[dist(rng_)]; + + // Play with 3D positioning + std::uniform_real_distribution volumeDist(0.85f, 1.0f); + std::uniform_real_distribution pitchDist(0.98f, 1.02f); + + bool success = AudioEngine::instance().playSound3D( + sample.data, + position, + volumeDist(rng_) * volumeScale_, + pitchDist(rng_), + 40.0f // Max distance for voice + ); + + if (success) { + lastPlayTime_[npcGuid] = now; + } +} + +VoiceType NpcVoiceManager::detectVoiceType(uint32_t creatureEntry) const { + // TODO: Use CreatureTemplate.dbc or other data to map creature entry to voice type + // For now, return generic + (void)creatureEntry; + return VoiceType::GENERIC; +} + +} // namespace audio +} // namespace wowee diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 77eccbe9..ade4cf64 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -1264,7 +1264,7 @@ void Renderer::update(float deltaTime) { mountDust->update(deltaTime); // Spawn dust when mounted and moving on ground - if (isMounted() && cameraController && !taxiFlight_) { + if (isMounted() && camera && cameraController && !taxiFlight_) { bool isMoving = cameraController->isMoving(); bool onGround = cameraController->isGrounded();