From f2f6ffd2cdd93ae1b397e8a85fbad7680f13cebe Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 23 Feb 2026 06:22:30 -0800 Subject: [PATCH] Fix voice gender using server data and update loading screen UI - Use authoritative playerRace/playerGender at spawn for voice profiles instead of unreliable model name parsing - Support nonbinary gender with useFemaleModel body type fallback - Move voice setup into spawnPlayerCharacter() for all spawn paths - Remove legacy single-player default Human Male clip preloading - Make loading screen text black and move progress bar to top --- include/audio/activity_sound_manager.hpp | 1 + src/audio/activity_sound_manager.cpp | 28 +++++++++++++++++++++--- src/core/application.cpp | 26 ++++++++++++++++++++++ src/rendering/loading_screen.cpp | 12 +++++----- src/rendering/renderer.cpp | 6 ----- 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/include/audio/activity_sound_manager.hpp b/include/audio/activity_sound_manager.hpp index 471f785c..764565ea 100644 --- a/include/audio/activity_sound_manager.hpp +++ b/include/audio/activity_sound_manager.hpp @@ -27,6 +27,7 @@ public: void playLanding(FootstepSurface surface, bool hardLanding); void setSwimmingState(bool swimming, bool moving); void setCharacterVoiceProfile(const std::string& modelName); + void setCharacterVoiceProfile(const std::string& raceFolder, const std::string& raceBase, bool male); void playWaterEnter(); void playWaterExit(); void playMeleeSwing(); diff --git a/src/audio/activity_sound_manager.cpp b/src/audio/activity_sound_manager.cpp index 2c43b6c5..9ab37a2d 100644 --- a/src/audio/activity_sound_manager.cpp +++ b/src/audio/activity_sound_manager.cpp @@ -28,9 +28,8 @@ bool ActivitySoundManager::initialize(pipeline::AssetManager* assets) { assetManager = assets; if (!assetManager) return false; - rebuildJumpClipsForProfile("Human", "Human", true); - rebuildSwimLoopClipsForProfile("Human", "Human", true); - rebuildHardLandClipsForProfile("Human", "Human", true); + // Voice profile clips (jump, swim, hardLand, combat vocals) are set at + // character spawn via setCharacterVoiceProfile() with the correct race/gender. preloadCandidates(splashEnterClips, { "Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterSmallA.wav", @@ -162,6 +161,11 @@ void ActivitySoundManager::rebuildJumpClipsForProfile(const std::string& raceFol prefix + stem + "\\" + stem + "Jump01.wav", prefix + stem + "\\" + stem + "Jump02.wav", }); + if (jumpClips.empty()) { + LOG_WARNING("No jump clips found for ", stem, " (tried exert prefix: ", exertPrefix, ")"); + } else { + LOG_INFO("Loaded ", jumpClips.size(), " jump clips for ", stem); + } } void ActivitySoundManager::rebuildSwimLoopClipsForProfile(const std::string& raceFolder, const std::string& raceBase, bool male) { @@ -393,6 +397,24 @@ void ActivitySoundManager::setCharacterVoiceProfile(const std::string& modelName " death clips=", deathClips.size()); } +void ActivitySoundManager::setCharacterVoiceProfile(const std::string& raceFolder, const std::string& raceBase, bool male) { + if (!assetManager) return; + std::string key = raceFolder + "|" + raceBase + "|" + (male ? "M" : "F"); + if (key == voiceProfileKey) return; + voiceProfileKey = key; + rebuildJumpClipsForProfile(raceFolder, raceBase, male); + rebuildSwimLoopClipsForProfile(raceFolder, raceBase, male); + rebuildHardLandClipsForProfile(raceFolder, raceBase, male); + rebuildCombatVocalClipsForProfile(raceFolder, raceBase, male); + core::Logger::getInstance().info("Activity SFX voice profile (explicit): ", voiceProfileKey, + " jump clips=", jumpClips.size(), + " swim clips=", swimLoopClips.size(), + " hardLand clips=", hardLandClips.size(), + " attackGrunt clips=", attackGruntClips.size(), + " wound clips=", woundClips.size(), + " death clips=", deathClips.size()); +} + void ActivitySoundManager::playWaterEnter() { LOG_INFO("Water entry detected - attempting to play splash sound"); auto now = std::chrono::steady_clock::now(); diff --git a/src/core/application.cpp b/src/core/application.cpp index d40cc6b6..e5ca6e54 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -2545,6 +2545,32 @@ void Application::spawnPlayerCharacter() { static_cast(spawnPos.z), ")"); playerCharacterSpawned = true; + // Set voice profile to match character race/gender + if (auto* asm_ = renderer->getActivitySoundManager()) { + const char* raceFolder = "Human"; + const char* raceBase = "Human"; + switch (playerRace_) { + case game::Race::HUMAN: raceFolder = "Human"; raceBase = "Human"; break; + case game::Race::ORC: raceFolder = "Orc"; raceBase = "Orc"; break; + case game::Race::DWARF: raceFolder = "Dwarf"; raceBase = "Dwarf"; break; + case game::Race::NIGHT_ELF: raceFolder = "NightElf"; raceBase = "NightElf"; break; + case game::Race::UNDEAD: raceFolder = "Scourge"; raceBase = "Scourge"; break; + case game::Race::TAUREN: raceFolder = "Tauren"; raceBase = "Tauren"; break; + case game::Race::GNOME: raceFolder = "Gnome"; raceBase = "Gnome"; break; + case game::Race::TROLL: raceFolder = "Troll"; raceBase = "Troll"; break; + case game::Race::BLOOD_ELF: raceFolder = "BloodElf"; raceBase = "BloodElf"; break; + case game::Race::DRAENEI: raceFolder = "Draenei"; raceBase = "Draenei"; break; + default: break; + } + bool useFemaleVoice = (playerGender_ == game::Gender::FEMALE); + if (playerGender_ == game::Gender::NONBINARY && gameHandler) { + if (const game::Character* ch = gameHandler->getActiveCharacter()) { + useFemaleVoice = ch->useFemaleModel; + } + } + asm_->setCharacterVoiceProfile(std::string(raceFolder), std::string(raceBase), !useFemaleVoice); + } + // Track which character's appearance this instance represents so we can // respawn if the user logs into a different character without restarting. spawnedPlayerGuid_ = gameHandler ? gameHandler->getActiveCharacterGuid() : 0; diff --git a/src/rendering/loading_screen.cpp b/src/rendering/loading_screen.cpp index b60673e0..3511d8e3 100644 --- a/src/rendering/loading_screen.cpp +++ b/src/rendering/loading_screen.cpp @@ -271,11 +271,11 @@ void LoadingScreen::render() { ImVec2(0, 0), ImVec2(screenW, screenH)); } - // Progress bar + // Progress bar (top of screen) { const float barWidthFrac = 0.6f; const float barHeight = 6.0f; - const float barY = screenH * 0.91f; + const float barY = screenH * 0.06f; float barX = screenW * (0.5f - barWidthFrac * 0.5f); float barW = screenW * barWidthFrac; @@ -306,20 +306,20 @@ void LoadingScreen::render() { { char pctBuf[32]; snprintf(pctBuf, sizeof(pctBuf), "%d%%", static_cast(loadProgress * 100.0f)); - float barCenterY = screenH * 0.91f; + float barCenterY = screenH * 0.06f; float textY = barCenterY - 20.0f; ImVec2 pctSize = ImGui::CalcTextSize(pctBuf); ImGui::SetCursorPos(ImVec2((screenW - pctSize.x) * 0.5f, textY)); - ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), "%s", pctBuf); + ImGui::TextColored(ImVec4(0.0f, 0.0f, 0.0f, 1.0f), "%s", pctBuf); } // Status text below bar { - float statusY = screenH * 0.91f + 14.0f; + float statusY = screenH * 0.06f + 14.0f; ImVec2 statusSize = ImGui::CalcTextSize(statusText.c_str()); ImGui::SetCursorPos(ImVec2((screenW - statusSize.x) * 0.5f, statusY)); - ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.8f, 1.0f), "%s", statusText.c_str()); + ImGui::TextColored(ImVec4(0.0f, 0.0f, 0.0f, 1.0f), "%s", statusText.c_str()); } ImGui::End(); diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 4851f254..619d0796 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -2452,12 +2452,6 @@ void Renderer::update(float deltaTime) { } characterRenderer->setInstancePosition(characterInstanceId, characterPosition); - if (activitySoundManager) { - std::string modelName; - if (characterRenderer->getInstanceModelName(characterInstanceId, modelName)) { - activitySoundManager->setCharacterVoiceProfile(modelName); - } - } // Movement-facing comes from camera controller and is decoupled from LMB orbit. // During taxi flights, orientation is controlled by the flight path (not player input)