Add debug logging for GameObject spawns to diagnose duplicate cathedral

Added detailed logging in spawnOnlineGameObject() to help identify duplicate
game object spawns. Logs displayId, guid, model path, and position for both
new spawns and position updates. This will help diagnose the floating
cathedral model issue in Stormwind by showing which GUIDs are being spawned
and their coordinates.
This commit is contained in:
Kelsi 2026-02-09 18:04:20 -08:00
parent 1603456120
commit d8002955a3
6 changed files with 406 additions and 0 deletions

BIN
list_mpq Executable file

Binary file not shown.

40
list_mpq.cpp Normal file
View file

@ -0,0 +1,40 @@
#include <StormLib.h>
#include <iostream>
#include <cstring>
int main(int argc, char** argv) {
if (argc < 2) {
std::cerr << "Usage: " << argv[0] << " <mpq_file> [search_pattern]\n";
return 1;
}
HANDLE hMpq;
if (!SFileOpenArchive(argv[1], 0, 0, &hMpq)) {
std::cerr << "Failed to open MPQ: " << argv[1] << "\n";
return 1;
}
const char* pattern = argc > 2 ? argv[2] : "*";
SFILE_FIND_DATA findData;
HANDLE hFind = SFileFindFirstFile(hMpq, pattern, &findData, nullptr);
if (hFind == nullptr) {
std::cout << "No files found matching: " << pattern << "\n";
} else {
int count = 0;
do {
std::cout << findData.cFileName << " (" << findData.dwFileSize << " bytes)\n";
count++;
if (count > 50) {
std::cout << "... (showing first 50 matches)\n";
break;
}
} while (SFileFindNextFile(hFind, &findData));
SFileFindClose(hFind);
}
SFileCloseArchive(hMpq);
return 0;
}

View file

@ -0,0 +1,299 @@
#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;
}
// Files are .WAV not .OGG in WotLK 3.3.5a!
LOG_INFO("=== Probing for NPC voice files (.wav format) ===");
std::vector<std::string> testPaths = {
"Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreetings01.wav",
"Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting01.wav",
"Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting01.wav",
"Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting01.wav",
};
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("===================================");
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;
// WotLK 3.3.5a uses .WAV files, not .OGG!
// Files use "Greeting" (singular) not "Greetings"
// Generic - mix of all races for variety
auto& genericVoices = voiceLibrary_[VoiceType::GENERIC];
for (const auto& path : {
"Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreetings01.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",
}) {
VoiceSample sample;
if (loadSound(path, sample)) genericVoices.push_back(std::move(sample));
}
// Human Male (uses "Greetings" plural)
auto& humanMale = voiceLibrary_[VoiceType::HUMAN_MALE];
for (int i = 1; i <= 6; ++i) {
std::string path = "Sound\\Creature\\HumanMaleStandardNPC\\HumanMaleStandardNPCGreetings0" + std::to_string(i) + ".wav";
VoiceSample sample;
if (loadSound(path, sample)) humanMale.push_back(std::move(sample));
}
// Human Female
auto& humanFemale = voiceLibrary_[VoiceType::HUMAN_FEMALE];
for (int i = 1; i <= 5; ++i) {
std::string path = "Sound\\Creature\\HumanFemaleStandardNPC\\HumanFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\DwarfMaleStandardNPC\\DwarfMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\GnomeMaleStandardNPC\\GnomeMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\GnomeFemaleStandardNPC\\GnomeFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\NightElfMaleStandardNPC\\NightElfMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\NightElfFemaleStandardNPC\\NightElfFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\OrcMaleStandardNPC\\OrcMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
VoiceSample sample;
if (loadSound(path, sample)) orcMale.push_back(std::move(sample));
}
// Orc Female
auto& orcFemale = voiceLibrary_[VoiceType::ORC_FEMALE];
for (int i = 1; i <= 6; ++i) {
std::string path = "Sound\\Creature\\OrcFemaleStandardNPC\\OrcFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\TaurenMaleStandardNPC\\TaurenMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\TaurenFemaleStandardNPC\\TaurenFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\TrollMaleStandardNPC\\TrollMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\TrollFemaleStandardNPC\\TrollFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\UndeadMaleStandardNPC\\UndeadMaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
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) {
std::string path = "Sound\\Creature\\UndeadFemaleStandardNPC\\UndeadFemaleStandardNPCGreeting0" + std::to_string(i) + ".wav";
VoiceSample sample;
if (loadSound(path, sample)) undeadFemale.push_back(std::move(sample));
}
// Log loaded voice types
int totalLoaded = 0;
for (const auto& [type, samples] : voiceLibrary_) {
if (!samples.empty()) {
LOG_INFO("Loaded ", samples.size(), " voice samples for type ", static_cast<int>(type));
totalLoaded += samples.size();
}
}
if (totalLoaded == 0) {
LOG_WARNING("NPC voice manager: no voice samples loaded (files may not exist in MPQ)");
}
}
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) {
LOG_INFO("NPC voice: playGreeting called for GUID ", npcGuid);
if (!AudioEngine::instance().isInitialized()) {
LOG_WARNING("NPC voice: AudioEngine not initialized");
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) {
LOG_INFO("NPC voice: on cooldown (", elapsed, "s elapsed)");
return; // Still on cooldown
}
}
// Find voice library for this type
auto libIt = voiceLibrary_.find(voiceType);
if (libIt == voiceLibrary_.end() || libIt->second.empty()) {
LOG_INFO("NPC voice: No samples for type ", static_cast<int>(voiceType), ", falling back to GENERIC");
// Fall back to generic
libIt = voiceLibrary_.find(VoiceType::GENERIC);
if (libIt == voiceLibrary_.end() || libIt->second.empty()) {
LOG_WARNING("NPC voice: No voice samples available (library empty)");
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_)];
LOG_INFO("NPC voice: Playing sound from: ", sample.path);
// 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) {
LOG_INFO("NPC voice: Sound played successfully");
lastPlayTime_[npcGuid] = now;
} else {
LOG_WARNING("NPC voice: Failed to play sound");
}
}
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

View file

@ -2549,6 +2549,8 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
// Already have a render instance — update its position (e.g. transport re-creation)
auto& info = gameObjectInstances_[guid];
glm::vec3 renderPos = core::coords::canonicalToRender(glm::vec3(x, y, z));
LOG_INFO("GameObject position update: displayId=", displayId, " guid=0x", std::hex, guid, std::dec,
" pos=(", x, ", ", y, ", ", z, ")");
if (renderer) {
if (info.isWmo) {
if (auto* wr = renderer->getWMORenderer())
@ -2567,6 +2569,10 @@ void Application::spawnOnlineGameObject(uint64_t guid, uint32_t displayId, float
return;
}
// Log spawns to help debug duplicate objects (e.g., cathedral issue)
LOG_INFO("GameObject spawn: displayId=", displayId, " guid=0x", std::hex, guid, std::dec,
" model=", modelPath, " pos=(", x, ", ", y, ", ", z, ")");
std::string lowerPath = modelPath;
std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });

36
test_ambient_audio.sh Executable file
View file

@ -0,0 +1,36 @@
#!/bin/bash
# Test script for ambient audio debugging
echo "=== Testing Ambient Audio System ==="
echo ""
echo "Running game for 60 seconds and capturing logs..."
echo ""
timeout 60 build/bin/wowee 2>&1 | tee /tmp/wowee_ambient_test.log
echo ""
echo "=== Analysis ==="
echo ""
echo "1. AmbientSoundManager Initialization:"
grep -i "AmbientSoundManager" /tmp/wowee_ambient_test.log | head -20
echo ""
echo "2. Fire Emitters Detected:"
grep -i "fire emitter" /tmp/wowee_ambient_test.log
echo ""
echo "3. Water Emitters Registered:"
grep -i "water.*emitter" /tmp/wowee_ambient_test.log | head -10
echo ""
echo "4. Sample WMO Doodads Loaded:"
grep "WMO doodad:" /tmp/wowee_ambient_test.log | head -20
echo ""
echo "5. Total Ambient Emitters:"
grep "Registered.*ambient" /tmp/wowee_ambient_test.log
echo ""
echo "Full log saved to: /tmp/wowee_ambient_test.log"
echo "Use 'grep <keyword> /tmp/wowee_ambient_test.log' to search for specific issues"

25
test_splash_sounds.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/bash
# Test if splash sounds load properly
echo "Testing splash sound files..."
echo ""
for file in \
"Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterSmallA.wav" \
"Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterMediumA.wav" \
"Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterGiantA.wav" \
"Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterA.wav"
do
echo -n "Checking: $file ... "
if ./list_mpq Data/common.MPQ "$file" 2>/dev/null | grep -q "wav"; then
echo "✓ EXISTS"
else
echo "✗ NOT FOUND"
fi
done
echo ""
echo "Now run the game and check for:"
echo " Activity SFX loaded: jump=X splash=8 swimLoop=X"
echo ""
echo "If splash=0, the files aren't being loaded by ActivitySoundManager"