mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Add ambient sound system and eliminate log spam
- 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
This commit is contained in:
parent
4a7e599764
commit
dab23f1895
24 changed files with 701 additions and 138 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
111
include/audio/ambient_sound_manager.hpp
Normal file
111
include/audio/ambient_sound_manager.hpp
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <cstdint>
|
||||
#include <glm/vec3.hpp>
|
||||
|
||||
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<uint8_t> data;
|
||||
bool loaded;
|
||||
};
|
||||
|
||||
// Sound libraries
|
||||
std::vector<AmbientSample> fireSoundsSmall_;
|
||||
std::vector<AmbientSample> fireSoundsLarge_;
|
||||
std::vector<AmbientSample> torchSounds_;
|
||||
std::vector<AmbientSample> waterSounds_;
|
||||
std::vector<AmbientSample> riverSounds_;
|
||||
std::vector<AmbientSample> waterfallSounds_;
|
||||
std::vector<AmbientSample> windSounds_;
|
||||
std::vector<AmbientSample> tavernSounds_;
|
||||
|
||||
// Active emitters
|
||||
std::vector<AmbientEmitter> 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<ActiveSound> 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<audio::ActivitySoundManager> activitySoundManager;
|
||||
std::unique_ptr<audio::MountSoundManager> mountSoundManager;
|
||||
std::unique_ptr<audio::NpcVoiceManager> npcVoiceManager;
|
||||
std::unique_ptr<audio::AmbientSoundManager> ambientSoundManager;
|
||||
std::unique_ptr<game::ZoneManager> zoneManager;
|
||||
std::unique_ptr<Shader> underwaterOverlayShader;
|
||||
uint32_t underwaterOverlayVAO = 0;
|
||||
|
|
|
|||
|
|
@ -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<WMODoodadReady> wmoDoodads;
|
||||
|
||||
// Ambient sound emitters (detected from doodads)
|
||||
struct AmbientEmitter {
|
||||
glm::vec3 position;
|
||||
uint32_t type; // Maps to AmbientSoundManager::AmbientType
|
||||
};
|
||||
std::vector<AmbientEmitter> 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<std::string, pipeline::BLPImage> 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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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<float>(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<size_t> 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<Sample>& out, const std::vector<std::string>& 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<Sample>& clips, float v
|
|||
}
|
||||
|
||||
void ActivitySoundManager::startSwimLoop() {
|
||||
if (swimLoopPid != INVALID_PROCESS || swimLoopClips.empty()) return;
|
||||
std::uniform_int_distribution<size_t> 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<const char*>(sample.data.data()), static_cast<std::streamsize>(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<float>(now - lastSplashAt).count() < 0.20f) return;
|
||||
if (std::chrono::duration<float>(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<float>(now - lastSplashAt).count() < 0.20f) return;
|
||||
if (std::chrono::duration<float>(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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
329
src/audio/ambient_sound_manager.cpp
Normal file
329
src/audio/ambient_sound_manager.cpp
Normal file
|
|
@ -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 <random>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
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<float> 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
|
||||
|
|
@ -136,7 +136,7 @@ bool AudioEngine::playSound2D(const std::vector<uint8_t>& 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<uint8_t>& 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<uint8_t>& 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<uint8_t>& 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Player>(block.guid);
|
||||
LOG_INFO("Created player entity: 0x", std::hex, block.guid, std::dec);
|
||||
break;
|
||||
|
||||
case ObjectType::UNIT:
|
||||
entity = std::make_shared<Unit>(block.guid);
|
||||
LOG_INFO("Created unit entity: 0x", std::hex, block.guid, std::dec);
|
||||
break;
|
||||
|
||||
case ObjectType::GAMEOBJECT:
|
||||
entity = std::make_shared<GameObject>(block.guid);
|
||||
LOG_INFO("Created gameobject entity: 0x", std::hex, block.guid, std::dec);
|
||||
break;
|
||||
|
||||
default:
|
||||
entity = std::make_shared<Entity>(block.guid);
|
||||
entity->setType(block.objectType);
|
||||
LOG_INFO("Created generic entity: 0x", std::hex, block.guid, std::dec,
|
||||
", type=", static_cast<int>(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) {
|
||||
|
|
|
|||
|
|
@ -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 ",
|
||||
|
|
|
|||
|
|
@ -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<uint8_t>(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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...");
|
||||
|
||||
|
|
|
|||
|
|
@ -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<size_t>(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<uint8_t> 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -65,8 +65,6 @@ ADTTerrain ADTLoader::load(const std::vector<uint8_t>& 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<uint8_t>& 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]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -823,11 +823,6 @@ bool M2Loader::loadSkin(const std::vector<uint8_t>& 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ WMOModel WMOLoader::load(const std::vector<uint8_t>& 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<uint8_t>& wmoData) {
|
|||
switch (chunkId) {
|
||||
case MVER: {
|
||||
model.version = read<uint32_t>(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<uint8_t>& 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<uint8_t>& 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<uint8_t>& wmoData) {
|
|||
// Initialize groups array
|
||||
model.groups.resize(model.nGroups);
|
||||
|
||||
core::Logger::getInstance().info("WMO model loaded successfully");
|
||||
// WMO loaded log disabled
|
||||
return model;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -860,9 +860,6 @@ uint32_t CharacterRenderer::createInstance(uint32_t modelId, const glm::vec3& po
|
|||
instance.boneMatrices.resize(std::max(static_cast<size_t>(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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <GL/glew.h>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtx/euler_angles.hpp>
|
||||
|
|
@ -344,6 +345,7 @@ bool Renderer::initialize(core::Window* win) {
|
|||
activitySoundManager = std::make_unique<audio::ActivitySoundManager>();
|
||||
mountSoundManager = std::make_unique<audio::MountSoundManager>();
|
||||
npcVoiceManager = std::make_unique<audio::NpcVoiceManager>();
|
||||
ambientSoundManager = std::make_unique<audio::AmbientSoundManager>();
|
||||
|
||||
// Underwater full-screen tint overlay (applies to all world geometry).
|
||||
underwaterOverlayShader = std::make_unique<Shader>();
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<PendingTile> 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<PendingTile>& 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<uint32_t> m2InstanceIds;
|
||||
std::vector<uint32_t> wmoInstanceIds;
|
||||
std::vector<uint32_t> tileUniqueIds;
|
||||
|
|
@ -631,6 +729,15 @@ void TerrainManager::finalizeTile(const std::shared_ptr<PendingTile>& 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<audio::AmbientSoundManager::AmbientType>(emitter.type);
|
||||
ambientSoundManager->addEmitter(emitter.position, type);
|
||||
}
|
||||
}
|
||||
|
||||
// Create tile entry
|
||||
auto tile = std::make_unique<TerrainTile>();
|
||||
tile->coord = coord;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue