2026-02-09 01:26:28 -08:00
|
|
|
#include "audio/npc_voice_manager.hpp"
|
|
|
|
|
#include "audio/audio_engine.hpp"
|
|
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <glm/glm.hpp>
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-09 02:22:20 -08:00
|
|
|
// Files are .WAV not .OGG in WotLK 3.3.5a!
|
|
|
|
|
LOG_INFO("=== Probing for NPC voice files (.wav format) ===");
|
2026-02-09 01:44:58 -08:00
|
|
|
std::vector<std::string> testPaths = {
|
2026-02-09 02:22:20 -08:00
|
|
|
"Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreeting01.wav",
|
|
|
|
|
"Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.wav",
|
|
|
|
|
"Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.wav",
|
|
|
|
|
"Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.wav",
|
2026-02-09 01:44:58 -08:00
|
|
|
};
|
|
|
|
|
for (const auto& path : testPaths) {
|
|
|
|
|
bool exists = assetManager_->fileExists(path);
|
|
|
|
|
LOG_INFO(" ", path, ": ", (exists ? "EXISTS" : "NOT FOUND"));
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("=== Probing for tavern music files ===");
|
|
|
|
|
std::vector<std::string> musicPaths = {
|
|
|
|
|
"Sound\\Music\\GlueScreenMusic\\tavern_01.mp3",
|
|
|
|
|
"Sound\\Music\\GlueScreenMusic\\BC_Alehouse.mp3",
|
|
|
|
|
"Sound\\Music\\ZoneMusic\\Tavern\\tavernAlliance01.mp3",
|
|
|
|
|
};
|
|
|
|
|
for (const auto& path : musicPaths) {
|
|
|
|
|
bool exists = assetManager_->fileExists(path);
|
|
|
|
|
LOG_INFO(" ", path, ": ", (exists ? "EXISTS" : "NOT FOUND"));
|
|
|
|
|
}
|
|
|
|
|
LOG_INFO("===================================");
|
|
|
|
|
|
2026-02-09 01:26:28 -08:00
|
|
|
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;
|
|
|
|
|
|
2026-02-09 02:22:20 -08:00
|
|
|
// WotLK 3.3.5a uses .WAV files, not .OGG!
|
|
|
|
|
// Files use "Greeting" (singular) not "Greetings"
|
2026-02-09 01:26:28 -08:00
|
|
|
|
2026-02-09 01:54:21 -08:00
|
|
|
// Generic - mix of all races for variety
|
2026-02-09 01:26:28 -08:00
|
|
|
auto& genericVoices = voiceLibrary_[VoiceType::GENERIC];
|
2026-02-09 01:54:21 -08:00
|
|
|
for (const auto& path : {
|
2026-02-09 02:22:20 -08:00
|
|
|
"Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreeting01.wav",
|
|
|
|
|
"Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.wav",
|
|
|
|
|
"Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.wav",
|
|
|
|
|
"Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting01.wav",
|
|
|
|
|
"Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting01.wav",
|
|
|
|
|
"Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.wav",
|
2026-02-09 01:54:21 -08:00
|
|
|
}) {
|
2026-02-09 01:26:28 -08:00
|
|
|
VoiceSample sample;
|
2026-02-09 01:54:21 -08:00
|
|
|
if (loadSound(path, sample)) genericVoices.push_back(std::move(sample));
|
2026-02-09 01:26:28 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 01:54:21 -08:00
|
|
|
// Human Male
|
2026-02-09 01:26:28 -08:00
|
|
|
auto& humanMale = voiceLibrary_[VoiceType::HUMAN_MALE];
|
2026-02-09 01:54:21 -08:00
|
|
|
for (int i = 1; i <= 6; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:26:28 -08:00
|
|
|
VoiceSample sample;
|
2026-02-09 01:54:21 -08:00
|
|
|
if (loadSound(path, sample)) humanMale.push_back(std::move(sample));
|
2026-02-09 01:26:28 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 01:54:21 -08:00
|
|
|
// Human Female
|
2026-02-09 01:26:28 -08:00
|
|
|
auto& humanFemale = voiceLibrary_[VoiceType::HUMAN_FEMALE];
|
2026-02-09 01:54:21 -08:00
|
|
|
for (int i = 1; i <= 5; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) humanFemale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Dwarf Male
|
|
|
|
|
auto& dwarfMale = voiceLibrary_[VoiceType::DWARF_MALE];
|
|
|
|
|
for (int i = 1; i <= 6; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) dwarfMale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Gnome Male
|
|
|
|
|
auto& gnomeMale = voiceLibrary_[VoiceType::GNOME_MALE];
|
|
|
|
|
for (int i = 1; i <= 6; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) gnomeMale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Gnome Female
|
|
|
|
|
auto& gnomeFemale = voiceLibrary_[VoiceType::GNOME_FEMALE];
|
|
|
|
|
for (int i = 1; i <= 6; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\GnomeFemaleStandardNPC\\GnomeFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) gnomeFemale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Night Elf Male
|
|
|
|
|
auto& nelfMale = voiceLibrary_[VoiceType::NIGHTELF_MALE];
|
|
|
|
|
for (int i = 1; i <= 8; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) nelfMale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Night Elf Female
|
|
|
|
|
auto& nelfFemale = voiceLibrary_[VoiceType::NIGHTELF_FEMALE];
|
|
|
|
|
for (int i = 1; i <= 6; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\NightElfFemaleStandardNPC\\NightElfFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) nelfFemale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Orc Male
|
|
|
|
|
auto& orcMale = voiceLibrary_[VoiceType::ORC_MALE];
|
|
|
|
|
for (int i = 1; i <= 5; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:26:28 -08:00
|
|
|
VoiceSample sample;
|
2026-02-09 01:54:21 -08:00
|
|
|
if (loadSound(path, sample)) orcMale.push_back(std::move(sample));
|
2026-02-09 01:26:28 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-09 01:54:21 -08:00
|
|
|
// Orc Female
|
|
|
|
|
auto& orcFemale = voiceLibrary_[VoiceType::ORC_FEMALE];
|
|
|
|
|
for (int i = 1; i <= 6; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\OrcFemaleStandardNPC\\OrcFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) orcFemale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tauren Male
|
|
|
|
|
auto& taurenMale = voiceLibrary_[VoiceType::TAUREN_MALE];
|
|
|
|
|
for (int i = 1; i <= 5; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\TaurenMaleStandardNPC\\TaurenMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) taurenMale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tauren Female
|
|
|
|
|
auto& taurenFemale = voiceLibrary_[VoiceType::TAUREN_FEMALE];
|
|
|
|
|
for (int i = 1; i <= 5; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\TaurenFemaleStandardNPC\\TaurenFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) taurenFemale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Troll Male
|
|
|
|
|
auto& trollMale = voiceLibrary_[VoiceType::TROLL_MALE];
|
|
|
|
|
for (int i = 1; i <= 6; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\TrollMaleStandardNPC\\TrollMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) trollMale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Troll Female
|
|
|
|
|
auto& trollFemale = voiceLibrary_[VoiceType::TROLL_FEMALE];
|
|
|
|
|
for (int i = 1; i <= 5; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\TrollFemaleStandardNPC\\TrollFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) trollFemale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Undead Male
|
|
|
|
|
auto& undeadMale = voiceLibrary_[VoiceType::UNDEAD_MALE];
|
|
|
|
|
for (int i = 1; i <= 6; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\UndeadMaleStandardNPC\\UndeadMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) undeadMale.push_back(std::move(sample));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Undead Female
|
|
|
|
|
auto& undeadFemale = voiceLibrary_[VoiceType::UNDEAD_FEMALE];
|
|
|
|
|
for (int i = 1; i <= 6; ++i) {
|
2026-02-09 02:22:20 -08:00
|
|
|
std::string path = "Sound\\Creature\\UndeadFemaleStandardNPC\\UndeadFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
|
2026-02-09 01:54:21 -08:00
|
|
|
VoiceSample sample;
|
|
|
|
|
if (loadSound(path, sample)) undeadFemale.push_back(std::move(sample));
|
|
|
|
|
}
|
2026-02-09 01:26:28 -08:00
|
|
|
|
|
|
|
|
// Log loaded voice types
|
2026-02-09 01:39:12 -08:00
|
|
|
int totalLoaded = 0;
|
2026-02-09 01:26:28 -08:00
|
|
|
for (const auto& [type, samples] : voiceLibrary_) {
|
|
|
|
|
if (!samples.empty()) {
|
|
|
|
|
LOG_INFO("Loaded ", samples.size(), " voice samples for type ", static_cast<int>(type));
|
2026-02-09 01:39:12 -08:00
|
|
|
totalLoaded += samples.size();
|
2026-02-09 01:26:28 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-09 01:39:12 -08:00
|
|
|
|
|
|
|
|
if (totalLoaded == 0) {
|
|
|
|
|
LOG_WARNING("NPC voice manager: no voice samples loaded (files may not exist in MPQ)");
|
|
|
|
|
}
|
2026-02-09 01:26:28 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-02-09 01:43:20 -08:00
|
|
|
LOG_INFO("NPC voice: playGreeting called for GUID ", npcGuid);
|
|
|
|
|
|
2026-02-09 01:26:28 -08:00
|
|
|
if (!AudioEngine::instance().isInitialized()) {
|
2026-02-09 01:43:20 -08:00
|
|
|
LOG_WARNING("NPC voice: AudioEngine not initialized");
|
2026-02-09 01:26:28 -08:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check cooldown
|
|
|
|
|
auto now = std::chrono::steady_clock::now();
|
|
|
|
|
auto it = lastPlayTime_.find(npcGuid);
|
|
|
|
|
if (it != lastPlayTime_.end()) {
|
|
|
|
|
float elapsed = std::chrono::duration<float>(now - it->second).count();
|
|
|
|
|
if (elapsed < GREETING_COOLDOWN) {
|
2026-02-09 01:43:20 -08:00
|
|
|
LOG_INFO("NPC voice: on cooldown (", elapsed, "s elapsed)");
|
2026-02-09 01:26:28 -08:00
|
|
|
return; // Still on cooldown
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find voice library for this type
|
|
|
|
|
auto libIt = voiceLibrary_.find(voiceType);
|
|
|
|
|
if (libIt == voiceLibrary_.end() || libIt->second.empty()) {
|
2026-02-09 01:43:20 -08:00
|
|
|
LOG_INFO("NPC voice: No samples for type ", static_cast<int>(voiceType), ", falling back to GENERIC");
|
2026-02-09 01:26:28 -08:00
|
|
|
// Fall back to generic
|
|
|
|
|
libIt = voiceLibrary_.find(VoiceType::GENERIC);
|
|
|
|
|
if (libIt == voiceLibrary_.end() || libIt->second.empty()) {
|
2026-02-09 01:43:20 -08:00
|
|
|
LOG_WARNING("NPC voice: No voice samples available (library empty)");
|
2026-02-09 01:26:28 -08:00
|
|
|
return; // No voice samples available
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto& samples = libIt->second;
|
|
|
|
|
|
|
|
|
|
// Pick random voice line
|
|
|
|
|
std::uniform_int_distribution<size_t> dist(0, samples.size() - 1);
|
|
|
|
|
const auto& sample = samples[dist(rng_)];
|
|
|
|
|
|
2026-02-09 01:43:20 -08:00
|
|
|
LOG_INFO("NPC voice: Playing sound from: ", sample.path);
|
|
|
|
|
|
2026-02-09 01:26:28 -08:00
|
|
|
// Play with 3D positioning
|
|
|
|
|
std::uniform_real_distribution<float> volumeDist(0.85f, 1.0f);
|
|
|
|
|
std::uniform_real_distribution<float> 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) {
|
2026-02-09 01:43:20 -08:00
|
|
|
LOG_INFO("NPC voice: Sound played successfully");
|
2026-02-09 01:26:28 -08:00
|
|
|
lastPlayTime_[npcGuid] = now;
|
2026-02-09 01:43:20 -08:00
|
|
|
} else {
|
|
|
|
|
LOG_WARNING("NPC voice: Failed to play sound");
|
2026-02-09 01:26:28 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|