From dab23f189593870b2d6daaac34b6433d9cbebb46 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Feb 2026 14:50:14 -0800 Subject: [PATCH] Add ambient sound system and eliminate log spam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement AmbientSoundManager with tavern/outdoor ambience - Fix audio buffer limit (5s → 60s) for long ambient loops - Set log level to INFO to eliminate DEBUG spam (130MB → 3.2MB logs) - Remove excessive terrain/model/network logging - Fix ambient sound timer sharing and pitch parameter bugs --- CMakeLists.txt | 1 + include/audio/activity_sound_manager.hpp | 1 + include/audio/ambient_sound_manager.hpp | 111 ++++++++ include/audio/music_manager.hpp | 2 + include/core/logger.hpp | 2 +- include/rendering/renderer.hpp | 4 +- include/rendering/terrain_manager.hpp | 10 + src/audio/activity_sound_manager.cpp | 106 +++++--- src/audio/ambient_sound_manager.cpp | 329 +++++++++++++++++++++++ src/audio/audio_engine.cpp | 16 +- src/audio/music_manager.cpp | 15 ++ src/core/application.cpp | 4 - src/game/game_handler.cpp | 11 +- src/game/npc_manager.cpp | 3 - src/game/world_packets.cpp | 4 - src/main.cpp | 2 +- src/network/world_socket.cpp | 43 +-- src/pipeline/adt_loader.cpp | 7 - src/pipeline/m2_loader.cpp | 5 - src/pipeline/terrain_mesh.cpp | 2 - src/pipeline/wmo_loader.cpp | 12 +- src/rendering/character_renderer.cpp | 6 - src/rendering/renderer.cpp | 36 +++ src/rendering/terrain_manager.cpp | 107 ++++++++ 24 files changed, 701 insertions(+), 138 deletions(-) create mode 100644 include/audio/ambient_sound_manager.hpp create mode 100644 src/audio/ambient_sound_manager.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c9626ec2..1c69e2e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,7 @@ set(WOWEE_SOURCES src/audio/activity_sound_manager.cpp src/audio/mount_sound_manager.cpp src/audio/npc_voice_manager.cpp + src/audio/ambient_sound_manager.cpp # Pipeline (asset loaders) src/pipeline/mpq_manager.cpp diff --git a/include/audio/activity_sound_manager.hpp b/include/audio/activity_sound_manager.hpp index 360f34fd..6e49156c 100644 --- a/include/audio/activity_sound_manager.hpp +++ b/include/audio/activity_sound_manager.hpp @@ -66,6 +66,7 @@ private: std::chrono::steady_clock::time_point lastLandAt{}; std::chrono::steady_clock::time_point lastSplashAt{}; std::chrono::steady_clock::time_point lastMeleeSwingAt{}; + std::chrono::steady_clock::time_point lastSwimStrokeAt{}; bool meleeSwingWarned = false; std::string voiceProfileKey; float volumeScale = 1.0f; diff --git a/include/audio/ambient_sound_manager.hpp b/include/audio/ambient_sound_manager.hpp new file mode 100644 index 00000000..84134e8f --- /dev/null +++ b/include/audio/ambient_sound_manager.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace wowee { +namespace pipeline { +class AssetManager; +} + +namespace audio { + +class AmbientSoundManager { +public: + AmbientSoundManager() = default; + ~AmbientSoundManager() = default; + + // Initialization + bool initialize(pipeline::AssetManager* assets); + void shutdown(); + + // Main update loop - called from renderer + void update(float deltaTime, const glm::vec3& cameraPos, bool isIndoor, bool isSwimming = false); + + // Emitter management + enum class AmbientType { + FIREPLACE_SMALL, + FIREPLACE_LARGE, + TORCH, + FOUNTAIN, + WATER_SURFACE, + RIVER, + WATERFALL, + WIND, + BIRD_DAY, + CRICKET_NIGHT, + OWL_NIGHT + }; + + uint64_t addEmitter(const glm::vec3& position, AmbientType type); + void removeEmitter(uint64_t id); + void clearEmitters(); + + // Time of day control (0-24 hours) + void setGameTime(float hours); + + // Volume control + void setVolumeScale(float scale); + +private: + struct AmbientEmitter { + uint64_t id; + AmbientType type; + glm::vec3 position; + bool active; + float lastPlayTime; + float loopInterval; // For periodic/looping sounds + }; + + struct AmbientSample { + std::string path; + std::vector data; + bool loaded; + }; + + // Sound libraries + std::vector fireSoundsSmall_; + std::vector fireSoundsLarge_; + std::vector torchSounds_; + std::vector waterSounds_; + std::vector riverSounds_; + std::vector waterfallSounds_; + std::vector windSounds_; + std::vector tavernSounds_; + + // Active emitters + std::vector emitters_; + uint64_t nextEmitterId_ = 1; + + // State tracking + float gameTimeHours_ = 12.0f; // Default noon + float volumeScale_ = 1.0f; + float birdTimer_ = 0.0f; + float cricketTimer_ = 0.0f; + float windLoopTime_ = 0.0f; + bool wasIndoor_ = false; + bool initialized_ = false; + + // Active audio tracking + struct ActiveSound { + uint64_t emitterId; + float startTime; + }; + std::vector activeSounds_; + + // Helper methods + void updatePositionalEmitters(float deltaTime, const glm::vec3& cameraPos); + void updatePeriodicSounds(float deltaTime, bool isIndoor, bool isSwimming); + void updateWindAmbience(float deltaTime, bool isIndoor); + bool loadSound(const std::string& path, AmbientSample& sample, pipeline::AssetManager* assets); + + // Time of day helpers + bool isDaytime() const { return gameTimeHours_ >= 6.0f && gameTimeHours_ < 20.0f; } + bool isNighttime() const { return !isDaytime(); } +}; + +} // namespace audio +} // namespace wowee diff --git a/include/audio/music_manager.hpp b/include/audio/music_manager.hpp index 0506dfe7..0d260e36 100644 --- a/include/audio/music_manager.hpp +++ b/include/audio/music_manager.hpp @@ -22,6 +22,7 @@ public: void update(float deltaTime); void setVolume(int volume); int getVolume() const { return volumePercent; } + void setUnderwaterMode(bool underwater); bool isPlaying() const { return playing; } bool isInitialized() const { return assetManager != nullptr; } @@ -33,6 +34,7 @@ private: bool currentTrackIsFile = false; bool playing = false; int volumePercent = 30; + bool underwaterMode = false; // Crossfade state bool crossfading = false; diff --git a/include/core/logger.hpp b/include/core/logger.hpp index 7776f53f..ed2bf93e 100644 --- a/include/core/logger.hpp +++ b/include/core/logger.hpp @@ -62,7 +62,7 @@ private: return oss.str(); } - LogLevel minLevel = LogLevel::DEBUG; + LogLevel minLevel = LogLevel::INFO; // Changed from DEBUG to reduce log spam std::mutex mutex; std::ofstream fileStream; bool fileReady = false; diff --git a/include/rendering/renderer.hpp b/include/rendering/renderer.hpp index b35b6cfa..442d8025 100644 --- a/include/rendering/renderer.hpp +++ b/include/rendering/renderer.hpp @@ -8,7 +8,7 @@ namespace wowee { namespace core { class Window; } namespace game { class World; class ZoneManager; } -namespace audio { class MusicManager; class FootstepManager; class ActivitySoundManager; class MountSoundManager; class NpcVoiceManager; enum class FootstepSurface : uint8_t; enum class VoiceType; } +namespace audio { class MusicManager; class FootstepManager; class ActivitySoundManager; class MountSoundManager; class NpcVoiceManager; class AmbientSoundManager; enum class FootstepSurface : uint8_t; enum class VoiceType; } namespace pipeline { class AssetManager; } namespace rendering { @@ -150,6 +150,7 @@ public: audio::ActivitySoundManager* getActivitySoundManager() { return activitySoundManager.get(); } audio::MountSoundManager* getMountSoundManager() { return mountSoundManager.get(); } audio::NpcVoiceManager* getNpcVoiceManager() { return npcVoiceManager.get(); } + audio::AmbientSoundManager* getAmbientSoundManager() { return ambientSoundManager.get(); } private: core::Window* window = nullptr; @@ -177,6 +178,7 @@ private: std::unique_ptr activitySoundManager; std::unique_ptr mountSoundManager; std::unique_ptr npcVoiceManager; + std::unique_ptr ambientSoundManager; std::unique_ptr zoneManager; std::unique_ptr underwaterOverlayShader; uint32_t underwaterOverlayVAO = 0; diff --git a/include/rendering/terrain_manager.hpp b/include/rendering/terrain_manager.hpp index f0919031..e4562537 100644 --- a/include/rendering/terrain_manager.hpp +++ b/include/rendering/terrain_manager.hpp @@ -22,6 +22,7 @@ namespace wowee { namespace pipeline { class AssetManager; } +namespace audio { class AmbientSoundManager; } namespace rendering { class TerrainRenderer; class Camera; class WaterRenderer; class M2Renderer; class WMORenderer; } namespace rendering { @@ -106,6 +107,13 @@ struct PendingTile { }; std::vector wmoDoodads; + // Ambient sound emitters (detected from doodads) + struct AmbientEmitter { + glm::vec3 position; + uint32_t type; // Maps to AmbientSoundManager::AmbientType + }; + std::vector ambientEmitters; + // Pre-loaded terrain texture BLP data (loaded on background thread to avoid // blocking file I/O on the main thread during finalizeTile) std::unordered_map preloadedTextures; @@ -182,6 +190,7 @@ public: void setWaterRenderer(WaterRenderer* renderer) { waterRenderer = renderer; } void setM2Renderer(M2Renderer* renderer) { m2Renderer = renderer; } void setWMORenderer(WMORenderer* renderer) { wmoRenderer = renderer; } + void setAmbientSoundManager(audio::AmbientSoundManager* manager) { ambientSoundManager = manager; } /** * Get terrain height at GL coordinates @@ -257,6 +266,7 @@ private: WaterRenderer* waterRenderer = nullptr; M2Renderer* m2Renderer = nullptr; WMORenderer* wmoRenderer = nullptr; + audio::AmbientSoundManager* ambientSoundManager = nullptr; std::string mapName = "Azeroth"; diff --git a/src/audio/activity_sound_manager.cpp b/src/audio/activity_sound_manager.cpp index 6f2041da..f05f2ff3 100644 --- a/src/audio/activity_sound_manager.cpp +++ b/src/audio/activity_sound_manager.cpp @@ -33,13 +33,14 @@ bool ActivitySoundManager::initialize(pipeline::AssetManager* assets) { rebuildHardLandClipsForProfile("Human", "Human", true); preloadCandidates(splashEnterClips, { - "Sound\\Character\\General\\Water\\WaterSplashSmall.wav", - "Sound\\Character\\General\\Water\\WaterSplashMedium.wav", - "Sound\\Character\\General\\Water\\WaterSplashLarge.wav", - "Sound\\Character\\Footsteps\\mFootMediumLargeWaterA.wav", - "Sound\\Character\\Footsteps\\mFootMediumLargeWaterB.wav", - "Sound\\Character\\Footsteps\\mFootMediumLargeWaterC.wav", - "Sound\\Character\\Footsteps\\mFootMediumLargeWaterD.wav" + "Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterSmallA.wav", + "Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterMediumA.wav", + "Sound\\Character\\Footsteps\\EnterWaterSplash\\EnterWaterGiantA.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterA.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterB.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterC.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterD.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterE.wav" }); splashExitClips = splashEnterClips; @@ -91,8 +92,29 @@ void ActivitySoundManager::shutdown() { assetManager = nullptr; } -void ActivitySoundManager::update(float) { +void ActivitySoundManager::update(float deltaTime) { reapProcesses(); + + // Play swimming stroke sounds periodically when swimming and moving + if (swimmingActive && swimMoving && !swimLoopClips.empty()) { + auto now = std::chrono::steady_clock::now(); + float elapsed = std::chrono::duration(now - lastSwimStrokeAt).count(); + + // Play swimming stroke sound every 0.8 seconds (swim stroke rhythm) + if (lastSwimStrokeAt.time_since_epoch().count() == 0 || elapsed >= 0.8f) { + std::uniform_int_distribution clipDist(0, swimLoopClips.size() - 1); + const Sample& sample = swimLoopClips[clipDist(rng)]; + + // Play as one-shot 2D sound + float volume = 0.6f * volumeScale; + AudioEngine::instance().playSound2D(sample.data, volume, false); + + lastSwimStrokeAt = now; + } + } else if (!swimmingActive) { + // Reset timer when not swimming + lastSwimStrokeAt = std::chrono::steady_clock::time_point{}; + } } void ActivitySoundManager::preloadCandidates(std::vector& out, const std::vector& candidates) { @@ -169,24 +191,21 @@ void ActivitySoundManager::rebuildJumpClipsForProfile(const std::string& raceFol void ActivitySoundManager::rebuildSwimLoopClipsForProfile(const std::string& raceFolder, const std::string& raceBase, bool male) { swimLoopClips.clear(); - const std::string gender = male ? "Male" : "Female"; - const std::string prefix = "Sound\\Character\\" + raceFolder + "\\"; - const std::string stem = raceBase + gender; + + // WoW 3.3.5a doesn't have dedicated swim loop sounds + // Use water splash/footstep sounds as swimming stroke sounds preloadCandidates(swimLoopClips, { - prefix + stem + "\\" + stem + "SwimLoop.wav", - prefix + stem + "\\" + stem + "Swim01.wav", - prefix + stem + "\\" + stem + "Swim02.wav", - prefix + stem + "SwimLoop.wav", - prefix + stem + "Swim01.wav", - prefix + stem + "Swim02.wav", - prefix + (male ? "Male" : "Female") + "\\" + stem + "SwimLoop.wav", - "Sound\\Character\\Swim\\SwimMoveLoop.wav", - "Sound\\Character\\Swim\\SwimLoop.wav", - "Sound\\Character\\Swim\\SwimSlowLoop.wav" + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterA.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterB.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterC.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterD.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsMediumWaterE.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterA.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterB.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterC.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterD.wav", + "Sound\\Character\\Footsteps\\WaterSplash\\FootStepsSmallWaterE.wav" }); - if (swimLoopClips.empty()) { - preloadCandidates(swimLoopClips, buildClassicSet("Water")); - } } void ActivitySoundManager::rebuildHardLandClipsForProfile(const std::string& raceFolder, const std::string& raceBase, bool male) { @@ -231,22 +250,9 @@ bool ActivitySoundManager::playOneShot(const std::vector& clips, float v } void ActivitySoundManager::startSwimLoop() { - if (swimLoopPid != INVALID_PROCESS || swimLoopClips.empty()) return; - std::uniform_int_distribution clipDist(0, swimLoopClips.size() - 1); - const Sample& sample = swimLoopClips[clipDist(rng)]; - - std::ofstream out(loopTempPath, std::ios::binary); - if (!out) return; - out.write(reinterpret_cast(sample.data.data()), static_cast(sample.data.size())); - out.close(); - - float volume = (swimMoving ? 0.85f : 0.65f) * volumeScale; - std::string filter = "volume=" + std::to_string(volume); - - swimLoopPid = platform::spawnProcess({ - "-nodisp", "-autoexit", "-loop", "0", "-loglevel", "quiet", - "-af", filter, loopTempPath - }); + // Swimming sounds now handled by periodic playback in update() method + // This method kept for API compatibility but does nothing + return; } void ActivitySoundManager::stopSwimLoop() { @@ -353,8 +359,10 @@ void ActivitySoundManager::setSwimmingState(bool swimming, bool moving) { if (swimming == swimmingActive) return; swimmingActive = swimming; if (swimmingActive) { + LOG_INFO("Swimming started - playing swim loop"); startSwimLoop(); } else { + LOG_INFO("Swimming stopped - stopping swim loop"); stopSwimLoop(); } } @@ -406,22 +414,36 @@ void ActivitySoundManager::setCharacterVoiceProfile(const std::string& modelName } void ActivitySoundManager::playWaterEnter() { + LOG_INFO("Water entry detected - attempting to play splash sound"); auto now = std::chrono::steady_clock::now(); if (lastSplashAt.time_since_epoch().count() != 0) { - if (std::chrono::duration(now - lastSplashAt).count() < 0.20f) return; + if (std::chrono::duration(now - lastSplashAt).count() < 0.20f) { + LOG_DEBUG("Water splash throttled (too soon)"); + return; + } } if (playOneShot(splashEnterClips, 0.95f, 0.95f, 1.05f)) { + LOG_INFO("Water splash enter sound played"); lastSplashAt = now; + } else { + LOG_ERROR("Failed to play water splash enter sound"); } } void ActivitySoundManager::playWaterExit() { + LOG_INFO("Water exit detected - attempting to play splash sound"); auto now = std::chrono::steady_clock::now(); if (lastSplashAt.time_since_epoch().count() != 0) { - if (std::chrono::duration(now - lastSplashAt).count() < 0.20f) return; + if (std::chrono::duration(now - lastSplashAt).count() < 0.20f) { + LOG_DEBUG("Water splash throttled (too soon)"); + return; + } } if (playOneShot(splashExitClips, 0.95f, 0.95f, 1.05f)) { + LOG_INFO("Water splash exit sound played"); lastSplashAt = now; + } else { + LOG_ERROR("Failed to play water splash exit sound"); } } diff --git a/src/audio/ambient_sound_manager.cpp b/src/audio/ambient_sound_manager.cpp new file mode 100644 index 00000000..139c91e9 --- /dev/null +++ b/src/audio/ambient_sound_manager.cpp @@ -0,0 +1,329 @@ +#include "audio/ambient_sound_manager.hpp" +#include "audio/audio_engine.hpp" +#include "pipeline/asset_manager.hpp" +#include "core/logger.hpp" +#include +#include +#include + +namespace wowee { +namespace audio { + +namespace { + // Distance thresholds (in game units) + constexpr float MAX_FIRE_DISTANCE = 20.0f; + constexpr float MAX_WATER_DISTANCE = 35.0f; + constexpr float MAX_AMBIENT_DISTANCE = 50.0f; + + // Volume settings + constexpr float FIRE_VOLUME = 0.7f; + constexpr float WATER_VOLUME = 0.5f; + constexpr float WIND_VOLUME = 0.35f; + constexpr float BIRD_VOLUME = 0.6f; + constexpr float CRICKET_VOLUME = 0.5f; + + // Timing settings (seconds) + constexpr float BIRD_MIN_INTERVAL = 8.0f; + constexpr float BIRD_MAX_INTERVAL = 20.0f; + constexpr float CRICKET_MIN_INTERVAL = 6.0f; + constexpr float CRICKET_MAX_INTERVAL = 15.0f; + constexpr float FIRE_LOOP_INTERVAL = 3.0f; // Fire crackling loop length + + std::random_device rd; + std::mt19937 gen(rd()); + + float randomFloat(float min, float max) { + std::uniform_real_distribution dist(min, max); + return dist(gen); + } +} + +bool AmbientSoundManager::initialize(pipeline::AssetManager* assets) { + if (!assets) { + LOG_ERROR("AmbientSoundManager: AssetManager is null"); + return false; + } + + LOG_INFO("AmbientSoundManager: Initializing..."); + + // Load fire sounds + fireSoundsSmall_.resize(1); + loadSound("Sound\\Doodad\\CampFireSmallLoop.wav", fireSoundsSmall_[0], assets); + + fireSoundsLarge_.resize(1); + loadSound("Sound\\Doodad\\CampFireLargeLoop.wav", fireSoundsLarge_[0], assets); + + torchSounds_.resize(1); + loadSound("Sound\\Doodad\\TorchFireLoop.wav", torchSounds_[0], assets); + + // Load water sounds + waterSounds_.resize(1); + loadSound("Sound\\Ambience\\Water\\River_LakeStillA.wav", waterSounds_[0], assets); + + riverSounds_.resize(1); + loadSound("Sound\\Ambience\\Water\\RiverSlowA.wav", riverSounds_[0], assets); + + waterfallSounds_.resize(1); + loadSound("Sound\\Doodad\\WaterFallSmall.wav", waterfallSounds_[0], assets); + + // Load wind/ambience sounds + windSounds_.resize(1); + bool windLoaded = loadSound("Sound\\Ambience\\ZoneAmbience\\ForestNormalDay.wav", windSounds_[0], assets); + + tavernSounds_.resize(1); + bool tavernLoaded = loadSound("Sound\\Ambience\\WMOAmbience\\Tavern.wav", tavernSounds_[0], assets); + + LOG_INFO("AmbientSoundManager: Wind loaded: ", windLoaded ? "YES" : "NO", + ", Tavern loaded: ", tavernLoaded ? "YES" : "NO"); + + // Initialize timers with random offsets + birdTimer_ = randomFloat(0.0f, 5.0f); + cricketTimer_ = randomFloat(0.0f, 5.0f); + + initialized_ = true; + LOG_INFO("AmbientSoundManager: Initialization complete"); + return true; +} + +void AmbientSoundManager::shutdown() { + emitters_.clear(); + activeSounds_.clear(); + initialized_ = false; +} + +bool AmbientSoundManager::loadSound(const std::string& path, AmbientSample& sample, pipeline::AssetManager* assets) { + sample.path = path; + sample.loaded = false; + + try { + sample.data = assets->readFile(path); + if (!sample.data.empty()) { + sample.loaded = true; + return true; + } + } catch (const std::exception& e) { + LOG_ERROR("AmbientSoundManager: Failed to load ", path, ": ", e.what()); + } + + return false; +} + +void AmbientSoundManager::update(float deltaTime, const glm::vec3& cameraPos, bool isIndoor, bool isSwimming) { + if (!initialized_) return; + + // Update all emitter systems + updatePositionalEmitters(deltaTime, cameraPos); + updatePeriodicSounds(deltaTime, isIndoor, isSwimming); + updateWindAmbience(deltaTime, isIndoor); + + // Track indoor state changes + wasIndoor_ = isIndoor; +} + +void AmbientSoundManager::updatePositionalEmitters(float deltaTime, const glm::vec3& cameraPos) { + // First pass: mark emitters as active/inactive based on distance + int activeFireCount = 0; + int activeWaterCount = 0; + const int MAX_ACTIVE_FIRE = 5; // Max 5 fire sounds at once + const int MAX_ACTIVE_WATER = 3; // Max 3 water sounds at once + + for (auto& emitter : emitters_) { + float distance = glm::distance(emitter.position, cameraPos); + + // Determine max distance based on type + float maxDist = MAX_AMBIENT_DISTANCE; + bool isFire = false; + bool isWater = false; + + if (emitter.type == AmbientType::FIREPLACE_SMALL || + emitter.type == AmbientType::FIREPLACE_LARGE || + emitter.type == AmbientType::TORCH) { + maxDist = MAX_FIRE_DISTANCE; + isFire = true; + } else if (emitter.type == AmbientType::WATER_SURFACE || + emitter.type == AmbientType::RIVER || + emitter.type == AmbientType::WATERFALL) { + maxDist = MAX_WATER_DISTANCE; + isWater = true; + } + + // Update active state based on distance AND limits + bool withinRange = (distance < maxDist); + + if (isFire && withinRange && activeFireCount < MAX_ACTIVE_FIRE) { + emitter.active = true; + activeFireCount++; + } else if (isWater && withinRange && activeWaterCount < MAX_ACTIVE_WATER) { + emitter.active = true; + activeWaterCount++; + } else if (!isFire && !isWater && withinRange) { + emitter.active = true; // Other types (fountain, etc) + } else { + emitter.active = false; + } + + if (!emitter.active) continue; + + // Update play timer + emitter.lastPlayTime += deltaTime; + + // Handle different emitter types + switch (emitter.type) { + case AmbientType::FIREPLACE_SMALL: + if (emitter.lastPlayTime >= FIRE_LOOP_INTERVAL && !fireSoundsSmall_.empty() && fireSoundsSmall_[0].loaded) { + float volume = FIRE_VOLUME * volumeScale_ * (1.0f - (distance / maxDist)); + AudioEngine::instance().playSound3D(fireSoundsSmall_[0].data, emitter.position, volume); + emitter.lastPlayTime = 0.0f; + } + break; + + case AmbientType::FIREPLACE_LARGE: + if (emitter.lastPlayTime >= FIRE_LOOP_INTERVAL && !fireSoundsLarge_.empty() && fireSoundsLarge_[0].loaded) { + float volume = FIRE_VOLUME * volumeScale_ * (1.0f - (distance / maxDist)); + AudioEngine::instance().playSound3D(fireSoundsLarge_[0].data, emitter.position, volume); + emitter.lastPlayTime = 0.0f; + } + break; + + case AmbientType::TORCH: + if (emitter.lastPlayTime >= FIRE_LOOP_INTERVAL && !torchSounds_.empty() && torchSounds_[0].loaded) { + float volume = FIRE_VOLUME * 0.7f * volumeScale_ * (1.0f - (distance / maxDist)); + AudioEngine::instance().playSound3D(torchSounds_[0].data, emitter.position, volume); + emitter.lastPlayTime = 0.0f; + } + break; + + case AmbientType::WATER_SURFACE: + if (emitter.lastPlayTime >= 5.0f && !waterSounds_.empty() && waterSounds_[0].loaded) { + float volume = WATER_VOLUME * volumeScale_ * (1.0f - (distance / maxDist)); + AudioEngine::instance().playSound3D(waterSounds_[0].data, emitter.position, volume); + emitter.lastPlayTime = 0.0f; + } + break; + + case AmbientType::RIVER: + if (emitter.lastPlayTime >= 5.0f && !riverSounds_.empty() && riverSounds_[0].loaded) { + float volume = WATER_VOLUME * volumeScale_ * (1.0f - (distance / maxDist)); + AudioEngine::instance().playSound3D(riverSounds_[0].data, emitter.position, volume); + emitter.lastPlayTime = 0.0f; + } + break; + + case AmbientType::WATERFALL: + if (emitter.lastPlayTime >= 4.0f && !waterfallSounds_.empty() && waterfallSounds_[0].loaded) { + float volume = WATER_VOLUME * 1.2f * volumeScale_ * (1.0f - (distance / maxDist)); + AudioEngine::instance().playSound3D(waterfallSounds_[0].data, emitter.position, volume); + emitter.lastPlayTime = 0.0f; + } + break; + + default: + break; + } + } +} + +void AmbientSoundManager::updatePeriodicSounds(float deltaTime, bool isIndoor, bool isSwimming) { + // Only play outdoor periodic sounds when outdoors and not swimming/underwater + if (isIndoor || isSwimming) return; + + // Bird sounds during daytime + if (isDaytime()) { + birdTimer_ += deltaTime; + if (birdTimer_ >= randomFloat(BIRD_MIN_INTERVAL, BIRD_MAX_INTERVAL)) { + // Play a random bird chirp (we'll use wind sound as placeholder for now) + // TODO: Add actual bird sound files when available + birdTimer_ = 0.0f; + } + } + + // Cricket sounds during nighttime + if (isNighttime()) { + cricketTimer_ += deltaTime; + if (cricketTimer_ >= randomFloat(CRICKET_MIN_INTERVAL, CRICKET_MAX_INTERVAL)) { + // Play cricket sounds + // TODO: Add actual cricket sound files when available + cricketTimer_ = 0.0f; + } + } +} + +void AmbientSoundManager::updateWindAmbience(float deltaTime, bool isIndoor) { + // Always track indoor state for next frame + bool stateChanged = (wasIndoor_ != isIndoor); + + if (stateChanged) { + LOG_INFO("Ambient: ", isIndoor ? "ENTERED BUILDING" : "EXITED TO OUTDOORS"); + windLoopTime_ = 99.0f; // Force immediate playback on next update + } + + wasIndoor_ = isIndoor; + + // Indoor ambience (tavern sounds) + if (isIndoor) { + if (!tavernSounds_.empty() && tavernSounds_[0].loaded) { + windLoopTime_ += deltaTime; + if (windLoopTime_ >= 8.0f) { + float volume = 0.8f * volumeScale_; + bool success = AudioEngine::instance().playSound2D(tavernSounds_[0].data, volume, 1.0f); + LOG_INFO("Playing tavern ambience: ", success ? "OK" : "FAILED", " (vol=", volume, ")"); + windLoopTime_ = 0.0f; + } + } else { + LOG_WARNING("Cannot play tavern: empty=", tavernSounds_.empty(), + " loaded=", (!tavernSounds_.empty() && tavernSounds_[0].loaded)); + } + } + // Outdoor wind ambience + else { + if (!windSounds_.empty() && windSounds_[0].loaded) { + windLoopTime_ += deltaTime; + if (windLoopTime_ >= 30.0f) { + float volume = 0.2f * volumeScale_; + bool success = AudioEngine::instance().playSound2D(windSounds_[0].data, volume, 1.0f); + LOG_INFO("Playing outdoor ambience: ", success ? "OK" : "FAILED", " (vol=", volume, ")"); + windLoopTime_ = 0.0f; + } + } else { + LOG_WARNING("Cannot play outdoor: empty=", windSounds_.empty(), + " loaded=", (!windSounds_.empty() && windSounds_[0].loaded)); + } + } +} + +uint64_t AmbientSoundManager::addEmitter(const glm::vec3& position, AmbientType type) { + AmbientEmitter emitter; + emitter.id = nextEmitterId_++; + emitter.type = type; + emitter.position = position; + emitter.active = false; + emitter.lastPlayTime = randomFloat(0.0f, 2.0f); // Random initial offset + emitter.loopInterval = FIRE_LOOP_INTERVAL; + + emitters_.push_back(emitter); + return emitter.id; +} + +void AmbientSoundManager::removeEmitter(uint64_t id) { + emitters_.erase( + std::remove_if(emitters_.begin(), emitters_.end(), + [id](const AmbientEmitter& e) { return e.id == id; }), + emitters_.end() + ); +} + +void AmbientSoundManager::clearEmitters() { + emitters_.clear(); +} + +void AmbientSoundManager::setGameTime(float hours) { + gameTimeHours_ = std::fmod(hours, 24.0f); + if (gameTimeHours_ < 0.0f) gameTimeHours_ += 24.0f; +} + +void AmbientSoundManager::setVolumeScale(float scale) { + volumeScale_ = std::max(0.0f, std::min(1.0f, scale)); +} + +} // namespace audio +} // namespace wowee diff --git a/src/audio/audio_engine.cpp b/src/audio/audio_engine.cpp index 93a8a3b6..85694990 100644 --- a/src/audio/audio_engine.cpp +++ b/src/audio/audio_engine.cpp @@ -136,7 +136,7 @@ bool AudioEngine::playSound2D(const std::vector& wavData, float volume, ); if (result != MA_SUCCESS) { - LOG_WARNING("Failed to decode WAV data: ", result); + LOG_ERROR("AudioEngine: Failed to decode WAV data (", wavData.size(), " bytes): error ", result); return false; } @@ -152,8 +152,8 @@ bool AudioEngine::playSound2D(const std::vector& wavData, float volume, totalFrames = 0; // Unknown length, will decode what we can } - // Allocate buffer for decoded PCM data (limit to 5 seconds max to prevent huge allocations) - ma_uint64 maxFrames = sampleRate * 5; + // Allocate buffer for decoded PCM data (limit to 60 seconds max for ambient loops) + ma_uint64 maxFrames = sampleRate * 60; if (totalFrames == 0 || totalFrames > maxFrames) { totalFrames = maxFrames; } @@ -167,10 +167,15 @@ bool AudioEngine::playSound2D(const std::vector& wavData, float volume, ma_decoder_uninit(&decoder); if (result != MA_SUCCESS || framesRead == 0) { - LOG_WARNING("Failed to read any frames from WAV: ", result); + LOG_ERROR("AudioEngine: Failed to read frames from WAV: error ", result, ", framesRead=", framesRead); return false; } + // Only log for large files (>1MB) + if (wavData.size() > 1000000) { + LOG_INFO("AudioEngine: Decoded ", framesRead, " frames (", framesRead / (float)sampleRate, "s) from ", wavData.size(), " byte WAV"); + } + // Resize pcmData to actual size used pcmData.resize(framesRead * channels * ma_get_bytes_per_sample(format)); @@ -270,7 +275,8 @@ bool AudioEngine::playSound3D(const std::vector& wavData, const glm::ve totalFrames = 0; } - ma_uint64 maxFrames = sampleRate * 5; + // Limit to 60 seconds max for ambient loops (same as 2D) + ma_uint64 maxFrames = sampleRate * 60; if (totalFrames == 0 || totalFrames > maxFrames) { totalFrames = maxFrames; } diff --git a/src/audio/music_manager.cpp b/src/audio/music_manager.cpp index d49268c5..eab07702 100644 --- a/src/audio/music_manager.cpp +++ b/src/audio/music_manager.cpp @@ -119,6 +119,21 @@ void MusicManager::setVolume(int volume) { // Update AudioEngine music volume directly (no restart needed!) float vol = volumePercent / 100.0f; + if (underwaterMode) { + vol *= 0.3f; // 30% volume underwater + } + AudioEngine::instance().setMusicVolume(vol); +} + +void MusicManager::setUnderwaterMode(bool underwater) { + if (underwaterMode == underwater) return; + underwaterMode = underwater; + + // Apply volume change immediately + float vol = volumePercent / 100.0f; + if (underwaterMode) { + vol *= 0.3f; // Fade to 30% underwater + } AudioEngine::instance().setMusicVolume(vol); } diff --git a/src/core/application.cpp b/src/core/application.cpp index 56c4f087..8ee3d5ab 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -1121,9 +1121,6 @@ void Application::spawnPlayerCharacter() { model.indices.size(), " indices, ", model.batches.size(), " batches"); // Log all animation sequence IDs for (size_t i = 0; i < model.sequences.size(); i++) { - LOG_INFO(" Anim[", i, "]: id=", model.sequences[i].id, - " duration=", model.sequences[i].duration, "ms", - " speed=", model.sequences[i].movingSpeed); } } } @@ -1889,7 +1886,6 @@ void Application::buildCreatureDisplayLookups() { uint32_t k = (1u << 16) | (0u << 8) | v; auto it = hairGeosetMap_.find(k); if (it != hairGeosetMap_.end()) { - LOG_INFO(" HairGeoset Human Male style ", v, " → geosetId ", it->second); } } } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 95fc5ce0..d4be7063 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1522,7 +1522,6 @@ void GameHandler::setOrientation(float orientation) { } void GameHandler::handleUpdateObject(network::Packet& packet) { - LOG_INFO("Handling SMSG_UPDATE_OBJECT"); UpdateObjectData data; if (!UpdateObjectParser::parse(packet, data)) { @@ -1563,24 +1562,19 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { switch (block.objectType) { case ObjectType::PLAYER: entity = std::make_shared(block.guid); - LOG_INFO("Created player entity: 0x", std::hex, block.guid, std::dec); break; case ObjectType::UNIT: entity = std::make_shared(block.guid); - LOG_INFO("Created unit entity: 0x", std::hex, block.guid, std::dec); break; case ObjectType::GAMEOBJECT: entity = std::make_shared(block.guid); - LOG_INFO("Created gameobject entity: 0x", std::hex, block.guid, std::dec); break; default: entity = std::make_shared(block.guid); entity->setType(block.objectType); - LOG_INFO("Created generic entity: 0x", std::hex, block.guid, std::dec, - ", type=", static_cast(block.objectType)); break; } @@ -1948,7 +1942,6 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { LOG_DEBUG("Updated entity fields: 0x", std::hex, block.guid, std::dec); } else { - LOG_WARNING("VALUES update for unknown entity: 0x", std::hex, block.guid, std::dec); } break; } @@ -1983,7 +1976,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } tabCycleStale = true; - LOG_INFO("Entity count: ", entityManager.getEntityCount()); + // Entity count logging disabled // Late inventory base detection once items are known if (playerGuid != 0 && invSlotBase_ < 0 && !lastPlayerFields_.empty() && !onlineItems_.empty()) { @@ -2070,7 +2063,7 @@ void GameHandler::handleDestroyObject(network::Packet& packet) { npcQuestStatus_.erase(data.guid); tabCycleStale = true; - LOG_INFO("Entity count: ", entityManager.getEntityCount()); + // Entity count logging disabled } void GameHandler::sendChatMessage(ChatType type, const std::string& message, const std::string& target) { diff --git a/src/game/npc_manager.cpp b/src/game/npc_manager.cpp index 036f5b2b..2aa2be24 100644 --- a/src/game/npc_manager.cpp +++ b/src/game/npc_manager.cpp @@ -843,9 +843,6 @@ void NpcManager::initialize(pipeline::AssetManager* am, npc.isEmoting = false; npc.isCritter = s.isCritter; npcs.push_back(npc); - - LOG_INFO("NpcManager: spawned '", s.name, "' guid=0x", std::hex, guid, std::dec, - " at GL(", glPos.x, ",", glPos.y, ",", glPos.z, ")"); } LOG_INFO("NpcManager: initialized ", npcs.size(), " NPCs with ", diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 438f054a..67cb593c 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -924,11 +924,9 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& } bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) { - LOG_INFO("Parsing SMSG_UPDATE_OBJECT"); // Read block count data.blockCount = packet.readUInt32(); - LOG_INFO(" Block count: ", data.blockCount); // Check for out-of-range objects first if (packet.getReadPos() + 1 <= packet.getSize()) { @@ -937,7 +935,6 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) if (firstByte == static_cast(UpdateType::OUT_OF_RANGE_OBJECTS)) { // Read out-of-range GUID count uint32_t count = packet.readUInt32(); - LOG_INFO(" Out-of-range objects: ", count); for (uint32_t i = 0; i < count; ++i) { uint64_t guid = readPackedGuid(packet); @@ -968,7 +965,6 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) data.blocks.push_back(block); } - LOG_INFO("Successfully parsed ", data.blocks.size(), " update blocks"); return true; } diff --git a/src/main.cpp b/src/main.cpp index 66e398ac..52757c81 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,7 +31,7 @@ int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { std::signal(SIGTERM, crashHandler); std::signal(SIGINT, crashHandler); try { - wowee::core::Logger::getInstance().setLogLevel(wowee::core::LogLevel::DEBUG); + wowee::core::Logger::getInstance().setLogLevel(wowee::core::LogLevel::INFO); LOG_INFO("=== Wowee Native Client ==="); LOG_INFO("Starting application..."); diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 382d56d3..df6a6c4d 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -115,34 +115,16 @@ void WorldSocket::send(const Packet& packet) { sendData.push_back(0); // High bytes are 0 for all WoW opcodes sendData.push_back(0); - // Debug: log encryption state and header - LOG_DEBUG("SEND opcode=0x", std::hex, opcode, std::dec, - " encryptionEnabled=", encryptionEnabled, - " header=[", std::hex, - (int)sendData[0], " ", (int)sendData[1], " ", - (int)sendData[2], " ", (int)sendData[3], " ", - (int)sendData[4], " ", (int)sendData[5], std::dec, "]", - " sizeField=", sizeField, " payloadLen=", payloadLen); + // Debug logging disabled - too spammy // Encrypt header if encryption is enabled (all 6 bytes) if (encryptionEnabled) { - uint8_t plainHeader[6]; - memcpy(plainHeader, sendData.data(), 6); encryptCipher.process(sendData.data(), 6); - LOG_DEBUG("Encrypted header: plain=[", std::hex, - (int)plainHeader[0], " ", (int)plainHeader[1], " ", - (int)plainHeader[2], " ", (int)plainHeader[3], " ", - (int)plainHeader[4], " ", (int)plainHeader[5], "] -> enc=[", - (int)sendData[0], " ", (int)sendData[1], " ", - (int)sendData[2], " ", (int)sendData[3], " ", - (int)sendData[4], " ", (int)sendData[5], "]", std::dec); } // Add payload (unencrypted) sendData.insert(sendData.end(), data.begin(), data.end()); - LOG_DEBUG("Sending world packet: opcode=0x", std::hex, opcode, std::dec, - " payload=", payloadLen, " bytes (", sendData.size(), " total)"); // Debug: dump first few movement packets { @@ -177,7 +159,6 @@ void WorldSocket::send(const Packet& packet) { if (sent < 0) { LOG_ERROR("Send failed: ", net::errorString(net::lastError())); } else { - LOG_DEBUG("Actually sent ", sent, " bytes to server"); if (static_cast(sent) != sendData.size()) { LOG_WARNING("Partial send: ", sent, " of ", sendData.size(), " bytes"); } @@ -228,40 +209,18 @@ void WorldSocket::tryParsePackets() { // Opcode: 2 bytes little-endian uint16_t opcode = receiveBuffer[2] | (receiveBuffer[3] << 8); - LOG_DEBUG("RECV encryptionEnabled=", encryptionEnabled, - " header=[", std::hex, (int)receiveBuffer[0], " ", (int)receiveBuffer[1], " ", - (int)receiveBuffer[2], " ", (int)receiveBuffer[3], std::dec, "]", - " -> size=", size, " opcode=0x", std::hex, opcode, std::dec); - // Total packet size: size field (2) + size value (which includes opcode + payload) size_t totalSize = 2 + size; if (receiveBuffer.size() < totalSize) { // Not enough data yet - header stays decrypted in buffer - LOG_DEBUG("Waiting for more data: have ", receiveBuffer.size(), - " bytes, need ", totalSize); break; } - // We have a complete packet! - LOG_DEBUG("Parsing world packet: opcode=0x", std::hex, opcode, std::dec, - " size=", size, " totalSize=", totalSize, " bytes"); - // Extract payload (skip header) std::vector packetData(receiveBuffer.begin() + 4, receiveBuffer.begin() + totalSize); - // Log first few bytes of payload - if (!packetData.empty()) { - std::string payloadHex; - for (size_t i = 0; i < std::min(packetData.size(), size_t(16)); i++) { - char buf[4]; - snprintf(buf, sizeof(buf), "%02x ", packetData[i]); - payloadHex += buf; - } - LOG_DEBUG("Payload (first 16 bytes): ", payloadHex); - } - // Create packet with opcode and payload Packet packet(opcode, packetData); diff --git a/src/pipeline/adt_loader.cpp b/src/pipeline/adt_loader.cpp index 292c4031..50b6cb60 100644 --- a/src/pipeline/adt_loader.cpp +++ b/src/pipeline/adt_loader.cpp @@ -65,8 +65,6 @@ ADTTerrain ADTLoader::load(const std::vector& adtData) { // Log first few chunks for debugging char magic[5] = {0}; std::memcpy(magic, &header.magic, 4); - LOG_INFO("Chunk #", totalChunks, ": magic=", magic, - " (0x", std::hex, header.magic, std::dec, "), size=", chunkSize); } // Parse based on chunk type @@ -101,10 +99,6 @@ ADTTerrain ADTLoader::load(const std::vector& adtData) { } terrain.loaded = true; - LOG_INFO("ADT loaded: ", chunkIndex, " map chunks, ", - terrain.textures.size(), " textures, ", - terrain.doodadNames.size(), " doodads, ", - terrain.wmoNames.size(), " WMOs"); return terrain; } @@ -211,7 +205,6 @@ void ADTLoader::parseMWMO(const uint8_t* data, size_t size, ADTTerrain& terrain) LOG_DEBUG("Loaded ", terrain.wmoNames.size(), " WMO names"); for (size_t i = 0; i < terrain.wmoNames.size(); i++) { - LOG_INFO(" WMO[", i, "]: ", terrain.wmoNames[i]); } } diff --git a/src/pipeline/m2_loader.cpp b/src/pipeline/m2_loader.cpp index 9df74f0c..7d530c36 100644 --- a/src/pipeline/m2_loader.cpp +++ b/src/pipeline/m2_loader.cpp @@ -823,11 +823,6 @@ bool M2Loader::loadSkin(const std::vector& skinData, M2Model& model) { core::Logger::getInstance().debug(" Submeshes: ", submeshes.size()); for (size_t i = 0; i < submeshes.size(); i++) { const auto& sm = submeshes[i]; - core::Logger::getInstance().info(" SkinSection[", i, "]: id=", sm.id, - " level=", sm.level, - " vtxStart=", sm.vertexStart, " vtxCount=", sm.vertexCount, - " idxStart=", sm.indexStart, " idxCount=", sm.indexCount, - " boneCount=", sm.boneCount, " boneStart=", sm.boneStart); } } diff --git a/src/pipeline/terrain_mesh.cpp b/src/pipeline/terrain_mesh.cpp index 458ca30f..9af14144 100644 --- a/src/pipeline/terrain_mesh.cpp +++ b/src/pipeline/terrain_mesh.cpp @@ -13,7 +13,6 @@ TerrainMesh TerrainMeshGenerator::generate(const ADTTerrain& terrain) { return mesh; } - LOG_INFO("Generating terrain mesh for ADT..."); // Copy texture list mesh.textures = terrain.textures; @@ -40,7 +39,6 @@ TerrainMesh TerrainMeshGenerator::generate(const ADTTerrain& terrain) { } mesh.validChunkCount = validCount; - LOG_INFO("Generated ", validCount, " terrain chunk meshes"); return mesh; } diff --git a/src/pipeline/wmo_loader.cpp b/src/pipeline/wmo_loader.cpp index 99f631fe..8e5acb8b 100644 --- a/src/pipeline/wmo_loader.cpp +++ b/src/pipeline/wmo_loader.cpp @@ -75,7 +75,7 @@ WMOModel WMOLoader::load(const std::vector& wmoData) { return model; } - core::Logger::getInstance().info("Loading WMO model..."); + // WMO loader logs disabled uint32_t offset = 0; @@ -95,7 +95,7 @@ WMOModel WMOLoader::load(const std::vector& wmoData) { switch (chunkId) { case MVER: { model.version = read(wmoData, offset); - core::Logger::getInstance().info("WMO version: ", model.version); + // WMO version log disabled break; } @@ -147,11 +147,11 @@ WMOModel WMOLoader::load(const std::vector& wmoData) { // Store mapping from byte offset to texture index model.textureOffsetToIndex[relativeOffset] = texIndex; model.textures.push_back(texName); - core::Logger::getInstance().info(" MOTX texture[", texIndex, "] at offset ", relativeOffset, ": ", texName); + // MOTX texture log disabled texOffset += texName.length() + 1; texIndex++; } - core::Logger::getInstance().info("WMO textures: ", model.textures.size()); + // WMO textures log disabled break; } @@ -200,7 +200,7 @@ WMOModel WMOLoader::load(const std::vector& wmoData) { model.groupNames.push_back(name); nameOffset += name.length() + 1; } - core::Logger::getInstance().info("WMO group names: ", model.groupNames.size()); + // WMO group names log disabled break; } @@ -382,7 +382,7 @@ WMOModel WMOLoader::load(const std::vector& wmoData) { // Initialize groups array model.groups.resize(model.nGroups); - core::Logger::getInstance().info("WMO model loaded successfully"); + // WMO loaded log disabled return model; } diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 384660e3..81147b09 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -860,9 +860,6 @@ uint32_t CharacterRenderer::createInstance(uint32_t modelId, const glm::vec3& po instance.boneMatrices.resize(std::max(static_cast(1), model.bones.size()), glm::mat4(1.0f)); instances[instance.id] = instance; - - core::Logger::getInstance().info("Created character instance ", instance.id, " from model ", modelId); - return instance.id; } @@ -1253,9 +1250,6 @@ void CharacterRenderer::render(const Camera& camera, const glm::mat4& view, cons gpuModel.textureIds.size(), " textures loaded, ", gpuModel.data.textureLookup.size(), " in lookup table"); for (size_t t = 0; t < gpuModel.data.textures.size(); t++) { - LOG_INFO(" Texture[", t, "]: type=", gpuModel.data.textures[t].type, - " file=", gpuModel.data.textures[t].filename, - " glId=", (t < gpuModel.textureIds.size() ? std::to_string(gpuModel.textureIds[t]) : "N/A")); } } diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index afc09bc0..72403579 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -38,6 +38,7 @@ #include "audio/activity_sound_manager.hpp" #include "audio/mount_sound_manager.hpp" #include "audio/npc_voice_manager.hpp" +#include "audio/ambient_sound_manager.hpp" #include #include #include @@ -344,6 +345,7 @@ bool Renderer::initialize(core::Window* win) { activitySoundManager = std::make_unique(); mountSoundManager = std::make_unique(); npcVoiceManager = std::make_unique(); + ambientSoundManager = std::make_unique(); // Underwater full-screen tint overlay (applies to all world geometry). underwaterOverlayShader = std::make_unique(); @@ -1383,12 +1385,21 @@ void Renderer::update(float deltaTime) { activitySoundManager->setSwimmingState(swimming, moving); + // Fade music underwater + if (musicManager) { + musicManager->setUnderwaterMode(swimming); + } + sfxPrevGrounded = grounded; sfxPrevJumping = jumping; sfxPrevFalling = falling; sfxPrevSwimming = swimming; } else { activitySoundManager->setSwimmingState(false, false); + // Restore music volume when activity sounds disabled + if (musicManager) { + musicManager->setUnderwaterMode(false); + } sfxStateInitialized = false; } } @@ -1404,6 +1415,16 @@ void Renderer::update(float deltaTime) { } } + // Ambient environmental sounds: fireplaces, water, birds, etc. + if (ambientSoundManager && camera && wmoRenderer && cameraController) { + glm::vec3 camPos = camera->getPosition(); + uint32_t wmoId = 0; + bool isIndoor = wmoRenderer->isInsideWMO(camPos.x, camPos.y, camPos.z, &wmoId); + bool isSwimming = cameraController->isSwimming(); + + ambientSoundManager->update(deltaTime, camPos, isIndoor, isSwimming); + } + // Update M2 doodad animations (pass camera for frustum-culling bone computation) if (m2Renderer && camera) { m2Renderer->update(deltaTime, camera->getPosition(), @@ -2040,6 +2061,10 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: if (wmoRenderer) { terrainManager->setWMORenderer(wmoRenderer.get()); } + // Set ambient sound manager for environmental audio emitters + if (ambientSoundManager) { + terrainManager->setAmbientSoundManager(ambientSoundManager.get()); + } // Pass asset manager to character renderer for texture loading if (characterRenderer) { characterRenderer->setAssetManager(assetManager); @@ -2117,6 +2142,9 @@ bool Renderer::loadTestTerrain(pipeline::AssetManager* assetManager, const std:: if (npcVoiceManager) { npcVoiceManager->initialize(assetManager); } + if (ambientSoundManager) { + ambientSoundManager->initialize(assetManager); + } cachedAssetManager = assetManager; } @@ -2201,6 +2229,14 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent if (npcVoiceManager && cachedAssetManager) { npcVoiceManager->initialize(cachedAssetManager); } + if (ambientSoundManager && cachedAssetManager) { + ambientSoundManager->initialize(cachedAssetManager); + } + + // Wire ambient sound manager to terrain manager for emitter registration + if (terrainManager && ambientSoundManager) { + terrainManager->setAmbientSoundManager(ambientSoundManager.get()); + } // Wire WMO, M2, and water renderer to camera controller if (cameraController && wmoRenderer) { diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index cc4134d4..a4c97871 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -4,6 +4,7 @@ #include "rendering/m2_renderer.hpp" #include "rendering/wmo_renderer.hpp" #include "rendering/camera.hpp" +#include "audio/ambient_sound_manager.hpp" #include "core/coordinates.hpp" #include "core/memory_monitor.hpp" #include "pipeline/asset_manager.hpp" @@ -465,6 +466,49 @@ std::shared_ptr TerrainManager::prepareTile(int x, int y) { // Extract world position for frustum culling glm::vec3 worldPos = glm::vec3(worldMatrix[3]); + // Detect ambient sound emitters from doodad model path + std::string m2PathLower = m2Path; + std::transform(m2PathLower.begin(), m2PathLower.end(), m2PathLower.begin(), ::tolower); + + // Debug: Log all doodad paths to help identify fire-related models + static int doodadLogCount = 0; + if (doodadLogCount < 50) { // Limit logging to first 50 doodads + LOG_DEBUG("WMO doodad: ", m2Path); + doodadLogCount++; + } + + if (m2PathLower.find("fire") != std::string::npos || + m2PathLower.find("brazier") != std::string::npos || + m2PathLower.find("campfire") != std::string::npos) { + // Fireplace/brazier emitter + PendingTile::AmbientEmitter emitter; + emitter.position = worldPos; + if (m2PathLower.find("small") != std::string::npos || m2PathLower.find("campfire") != std::string::npos) { + emitter.type = 0; // FIREPLACE_SMALL + } else { + emitter.type = 1; // FIREPLACE_LARGE + } + pending->ambientEmitters.push_back(emitter); + } else if (m2PathLower.find("torch") != std::string::npos) { + // Torch emitter + PendingTile::AmbientEmitter emitter; + emitter.position = worldPos; + emitter.type = 2; // TORCH + pending->ambientEmitters.push_back(emitter); + } else if (m2PathLower.find("fountain") != std::string::npos) { + // Fountain emitter + PendingTile::AmbientEmitter emitter; + emitter.position = worldPos; + emitter.type = 3; // FOUNTAIN + pending->ambientEmitters.push_back(emitter); + } else if (m2PathLower.find("waterfall") != std::string::npos) { + // Waterfall emitter + PendingTile::AmbientEmitter emitter; + emitter.position = worldPos; + emitter.type = 6; // WATERFALL + pending->ambientEmitters.push_back(emitter); + } + PendingTile::WMODoodadReady doodadReady; doodadReady.modelId = doodadModelId; doodadReady.model = std::move(m2Model); @@ -533,6 +577,60 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { waterRenderer->loadFromTerrain(pending->terrain, true, x, y); } + // Register water surface ambient sound emitters + if (ambientSoundManager) { + // Scan ADT water data for water surfaces + int waterEmitterCount = 0; + for (size_t chunkIdx = 0; chunkIdx < pending->terrain.waterData.size(); chunkIdx++) { + const auto& chunkWater = pending->terrain.waterData[chunkIdx]; + if (!chunkWater.hasWater()) continue; + + // Calculate chunk position in world coordinates + int chunkX = chunkIdx % 16; + int chunkY = chunkIdx / 16; + + // WoW coordinates: Each ADT tile is 533.33 units, each chunk is 533.33/16 = 33.333 units + // Tile origin in GL space + float tileOriginX = (32.0f - x) * 533.33333f; + float tileOriginY = (32.0f - y) * 533.33333f; + + // Chunk center position + float chunkCenterX = tileOriginX + (chunkX + 0.5f) * 33.333333f; + float chunkCenterY = tileOriginY + (chunkY + 0.5f) * 33.333333f; + + // Use first layer for height and type detection + if (!chunkWater.layers.empty()) { + const auto& layer = chunkWater.layers[0]; + float waterHeight = layer.minHeight; + + // Determine water type and register appropriate emitter + // liquidType: 0=water/lake, 1=ocean, 2=magma, 3=slime + if (layer.liquidType == 0) { + // Lake/river water - add water surface emitter every 32 chunks to avoid spam + if (chunkIdx % 32 == 0) { + PendingTile::AmbientEmitter emitter; + emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight); + emitter.type = 4; // WATER_SURFACE + pending->ambientEmitters.push_back(emitter); + waterEmitterCount++; + } + } else if (layer.liquidType == 1) { + // Ocean - add ocean emitter every 64 chunks (oceans are very large) + if (chunkIdx % 64 == 0) { + PendingTile::AmbientEmitter emitter; + emitter.position = glm::vec3(chunkCenterX, chunkCenterY, waterHeight); + emitter.type = 4; // WATER_SURFACE (could add separate OCEAN type later) + pending->ambientEmitters.push_back(emitter); + waterEmitterCount++; + } + } + // Skip magma and slime for now (no ambient sounds for those) + } + } + if (waterEmitterCount > 0) { + } + } + std::vector m2InstanceIds; std::vector wmoInstanceIds; std::vector tileUniqueIds; @@ -631,6 +729,15 @@ void TerrainManager::finalizeTile(const std::shared_ptr& pending) { } } + // Register ambient sound emitters with ambient sound manager + if (ambientSoundManager && !pending->ambientEmitters.empty()) { + for (const auto& emitter : pending->ambientEmitters) { + // Cast uint32_t type to AmbientSoundManager::AmbientType enum + auto type = static_cast(emitter.type); + ambientSoundManager->addEmitter(emitter.position, type); + } + } + // Create tile entry auto tile = std::make_unique(); tile->coord = coord;