Merge pull request #38 from ldmonster/chore/renderer-refactor

[chore] refactor: renderer extract domains
This commit is contained in:
Kelsi Rae Davis 2026-04-02 13:18:45 -07:00 committed by GitHub
commit 0a4cc9ab96
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 4880 additions and 4207 deletions

View file

@ -611,6 +611,9 @@ set(WOWEE_SOURCES
src/rendering/mount_dust.cpp
src/rendering/levelup_effect.cpp
src/rendering/charge_effect.cpp
src/rendering/spell_visual_system.cpp
src/rendering/post_process_pipeline.cpp
src/rendering/animation_controller.cpp
src/rendering/loading_screen.cpp
# UI

View file

@ -4,6 +4,7 @@
namespace wowee {
namespace rendering { class Renderer; }
namespace pipeline { class AssetManager; }
namespace audio { class AudioCoordinator; }
namespace game { class ExpansionRegistry; }
namespace game {
@ -13,6 +14,7 @@ namespace game {
// Replaces hidden Application::getInstance() singleton access.
struct GameServices {
rendering::Renderer* renderer = nullptr;
audio::AudioCoordinator* audioCoordinator = nullptr;
pipeline::AssetManager* assetManager = nullptr;
ExpansionRegistry* expansionRegistry = nullptr;
uint32_t gryphonDisplayId = 0;

View file

@ -0,0 +1,182 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <glm/glm.hpp>
namespace wowee {
namespace audio { enum class FootstepSurface : uint8_t; }
namespace rendering {
class Renderer;
// ============================================================================
// AnimationController — extracted from Renderer (§4.2)
//
// Owns the character locomotion state machine, mount animation state,
// emote system, footstep triggering, surface detection, melee combat
// animation, and activity SFX transition tracking.
// ============================================================================
class AnimationController {
public:
AnimationController();
~AnimationController();
void initialize(Renderer* renderer);
// ── Per-frame update hooks (called from Renderer::update) ──────────────
// Runs the character animation state machine (mounted + unmounted).
void updateCharacterAnimation();
// Processes animation-driven footstep events (player + mount).
void updateFootsteps(float deltaTime);
// Tracks state transitions for activity SFX (jump, landing, swim) and
// mount ambient sounds.
void updateSfxState(float deltaTime);
// Decrements melee swing timer / cooldown.
void updateMeleeTimers(float deltaTime);
// Store per-frame delta time (used inside animation state machine).
void setDeltaTime(float dt) { lastDeltaTime_ = dt; }
// ── Character follow ───────────────────────────────────────────────────
void onCharacterFollow(uint32_t instanceId);
// ── Emote support ──────────────────────────────────────────────────────
void playEmote(const std::string& emoteName);
void cancelEmote();
bool isEmoteActive() const { return emoteActive_; }
static std::string getEmoteText(const std::string& emoteName,
const std::string* targetName = nullptr);
static uint32_t getEmoteDbcId(const std::string& emoteName);
static std::string getEmoteTextByDbcId(uint32_t dbcId,
const std::string& senderName,
const std::string* targetName = nullptr);
static uint32_t getEmoteAnimByDbcId(uint32_t dbcId);
// ── Targeting / combat ─────────────────────────────────────────────────
void setTargetPosition(const glm::vec3* pos);
void setInCombat(bool combat) { inCombat_ = combat; }
bool isInCombat() const { return inCombat_; }
const glm::vec3* getTargetPosition() const { return targetPosition_; }
void resetCombatVisualState();
bool isMoving() const;
// ── Melee combat ───────────────────────────────────────────────────────
void triggerMeleeSwing();
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId_ = 0; }
void setCharging(bool charging) { charging_ = charging; }
bool isCharging() const { return charging_; }
// ── Effect triggers ────────────────────────────────────────────────────
void triggerLevelUpEffect(const glm::vec3& position);
void startChargeEffect(const glm::vec3& position, const glm::vec3& direction);
void emitChargeEffect(const glm::vec3& position, const glm::vec3& direction);
void stopChargeEffect();
// ── Mount ──────────────────────────────────────────────────────────────
void setMounted(uint32_t mountInstId, uint32_t mountDisplayId,
float heightOffset, const std::string& modelPath = "");
void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; }
void setMountPitchRoll(float pitch, float roll) { mountPitch_ = pitch; mountRoll_ = roll; }
void clearMount();
bool isMounted() const { return mountInstanceId_ != 0; }
uint32_t getMountInstanceId() const { return mountInstanceId_; }
// ── Query helpers (used by Renderer) ───────────────────────────────────
bool isFootstepAnimationState() const;
float getMeleeSwingTimer() const { return meleeSwingTimer_; }
float getMountHeightOffset() const { return mountHeightOffset_; }
bool isTaxiFlight() const { return taxiFlight_; }
private:
Renderer* renderer_ = nullptr;
// Character animation state machine
enum class CharAnimState {
IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING,
EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE
};
CharAnimState charAnimState_ = CharAnimState::IDLE;
float locomotionStopGraceTimer_ = 0.0f;
bool locomotionWasSprinting_ = false;
uint32_t lastPlayerAnimRequest_ = UINT32_MAX;
bool lastPlayerAnimLoopRequest_ = true;
// Emote state
bool emoteActive_ = false;
uint32_t emoteAnimId_ = 0;
bool emoteLoop_ = false;
// Target facing
const glm::vec3* targetPosition_ = nullptr;
bool inCombat_ = false;
// Footstep event tracking (animation-driven)
uint32_t footstepLastAnimationId_ = 0;
float footstepLastNormTime_ = 0.0f;
bool footstepNormInitialized_ = false;
// Footstep surface cache (avoid expensive queries every step)
mutable audio::FootstepSurface cachedFootstepSurface_{};
mutable glm::vec3 cachedFootstepPosition_{0.0f, 0.0f, 0.0f};
mutable float cachedFootstepUpdateTimer_{999.0f};
// Mount footstep tracking (separate from player's)
uint32_t mountFootstepLastAnimId_ = 0;
float mountFootstepLastNormTime_ = 0.0f;
bool mountFootstepNormInitialized_ = false;
// SFX transition state
bool sfxStateInitialized_ = false;
bool sfxPrevGrounded_ = true;
bool sfxPrevJumping_ = false;
bool sfxPrevFalling_ = false;
bool sfxPrevSwimming_ = false;
// Melee combat
bool charging_ = false;
float meleeSwingTimer_ = 0.0f;
float meleeSwingCooldown_ = 0.0f;
float meleeAnimDurationMs_ = 0.0f;
uint32_t meleeAnimId_ = 0;
uint32_t equippedWeaponInvType_ = 0;
// Mount animation capabilities (discovered at mount time, varies per model)
struct MountAnimSet {
uint32_t jumpStart = 0; // Jump start animation
uint32_t jumpLoop = 0; // Jump airborne loop
uint32_t jumpEnd = 0; // Jump landing
uint32_t rearUp = 0; // Rear-up / special flourish
uint32_t run = 0; // Run animation (discovered, don't assume)
uint32_t stand = 0; // Stand animation (discovered)
std::vector<uint32_t> fidgets; // Idle fidget animations (head turn, tail swish, etc.)
};
enum class MountAction { None, Jump, RearUp };
uint32_t mountInstanceId_ = 0;
float mountHeightOffset_ = 0.0f;
float mountPitch_ = 0.0f; // Up/down tilt (radians)
float mountRoll_ = 0.0f; // Left/right banking (radians)
int mountSeatAttachmentId_ = -1; // -1 unknown, -2 unavailable
glm::vec3 smoothedMountSeatPos_ = glm::vec3(0.0f);
bool mountSeatSmoothingInit_ = false;
float prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean)
float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation()
MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up)
uint32_t mountActionPhase_ = 0; // 0=start, 1=loop, 2=end (for jump chaining)
MountAnimSet mountAnims_; // Cached animation IDs for current mount
float mountIdleFidgetTimer_ = 0.0f; // Timer for random idle fidgets
float mountIdleSoundTimer_ = 0.0f; // Timer for ambient idle sounds
uint32_t mountActiveFidget_ = 0; // Currently playing fidget animation ID (0 = none)
bool taxiFlight_ = false;
bool taxiAnimsLogged_ = false;
// Private animation helpers
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
audio::FootstepSurface resolveFootstepSurface() const;
uint32_t resolveMeleeAnimId();
};
} // namespace rendering
} // namespace wowee

View file

@ -0,0 +1,282 @@
#pragma once
#include <memory>
#include <string>
#include <cstdint>
#include <glm/glm.hpp>
#include <vulkan/vulkan.h>
#include <vk_mem_alloc.h>
#include "rendering/vk_utils.hpp"
#if WOWEE_HAS_AMD_FSR2
#include "ffx_fsr2.h"
#include "ffx_fsr2_vk.h"
#endif
namespace wowee {
namespace rendering {
class VkContext;
class Camera;
class AmdFsr3Runtime;
/// Returned by setFSREnabled/setFSR2Enabled when they need the Renderer
/// to schedule an MSAA sample-count change (§4.3).
struct MsaaChangeRequest {
bool requested = false;
VkSampleCountFlagBits samples = VK_SAMPLE_COUNT_1_BIT;
};
/// PostProcessPipeline owns all FSR 1.0, FXAA, and FSR 2.2/3 state and
/// orchestrates post-processing passes between the scene render pass and
/// the final swapchain presentation (§4.3 extraction from Renderer).
class PostProcessPipeline {
public:
PostProcessPipeline();
~PostProcessPipeline();
void initialize(VkContext* ctx);
void shutdown();
// --- Frame-loop integration (called from Renderer::beginFrame) ---
/// Lazy-create / lazy-destroy FSR/FXAA/FSR2 resources between frames.
void manageResources();
/// Recreate post-process resources after swapchain resize.
void handleSwapchainResize();
/// Apply FSR2 temporal jitter to the camera projection.
void applyJitter(Camera* camera);
/// Returns the framebuffer the scene should render into.
/// If no post-processing is active, returns VK_NULL_HANDLE (use swapchain).
VkFramebuffer getSceneFramebuffer() const;
/// Returns the render extent for the active post-process pipeline.
/// Falls back to swapchain extent if nothing is active.
VkExtent2D getSceneRenderExtent() const;
/// True if any post-process pipeline is active (FSR/FXAA/FSR2).
bool hasActivePostProcess() const;
/// True when FXAA alone (no FSR2) needs its own off-screen pass.
bool useFXAAPostPass() const { return fxaa_.enabled; }
// --- Frame-loop integration (called from Renderer::endFrame) ---
/// Execute all post-processing passes. Returns true if an INLINE
/// render pass was started (affects ImGui recording mode).
bool executePostProcessing(VkCommandBuffer cmd, uint32_t imageIndex,
Camera* camera, float deltaTime);
// --- MSAA interop (called from Renderer::applyMsaaChange) ---
/// Destroy FSR/FSR2/FXAA resources (they will be lazily recreated).
void destroyAllResources();
/// True when FSR2 is active and MSAA changes should be blocked.
bool isFsr2BlockingMsaa() const { return fsr2_.enabled; }
// --- Public API (delegated from Renderer) ---
// FXAA
void setFXAAEnabled(bool enabled);
bool isFXAAEnabled() const { return fxaa_.enabled; }
// FSR 1.0
MsaaChangeRequest setFSREnabled(bool enabled);
bool isFSREnabled() const { return fsr_.enabled; }
void setFSRQuality(float scaleFactor);
void setFSRSharpness(float sharpness);
float getFSRScaleFactor() const { return fsr_.scaleFactor; }
float getFSRSharpness() const { return fsr_.sharpness; }
// FSR 2.2
MsaaChangeRequest setFSR2Enabled(bool enabled, Camera* camera);
bool isFSR2Enabled() const { return fsr2_.enabled; }
void setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY);
// FSR3 Framegen
void setAmdFsr3FramegenEnabled(bool enabled);
bool isAmdFsr3FramegenEnabled() const { return fsr2_.amdFsr3FramegenEnabled; }
float getFSR2JitterSign() const { return fsr2_.jitterSign; }
float getFSR2MotionVecScaleX() const { return fsr2_.motionVecScaleX; }
float getFSR2MotionVecScaleY() const { return fsr2_.motionVecScaleY; }
#if WOWEE_HAS_AMD_FSR2
bool isAmdFsr2SdkAvailable() const { return true; }
#else
bool isAmdFsr2SdkAvailable() const { return false; }
#endif
#if WOWEE_HAS_AMD_FSR3_FRAMEGEN
bool isAmdFsr3FramegenSdkAvailable() const { return true; }
#else
bool isAmdFsr3FramegenSdkAvailable() const { return false; }
#endif
bool isAmdFsr3FramegenRuntimeActive() const { return fsr2_.amdFsr3FramegenRuntimeActive; }
bool isAmdFsr3FramegenRuntimeReady() const { return fsr2_.amdFsr3FramegenRuntimeReady; }
const char* getAmdFsr3FramegenRuntimePath() const;
const std::string& getAmdFsr3FramegenRuntimeError() const { return fsr2_.amdFsr3RuntimeLastError; }
size_t getAmdFsr3UpscaleDispatchCount() const { return fsr2_.amdFsr3UpscaleDispatchCount; }
size_t getAmdFsr3FramegenDispatchCount() const { return fsr2_.amdFsr3FramegenDispatchCount; }
size_t getAmdFsr3FallbackCount() const { return fsr2_.amdFsr3FallbackCount; }
// Brightness (1.0 = default, <1 darkens, >1 brightens)
void setBrightness(float b) { brightness_ = b; }
float getBrightness() const { return brightness_; }
private:
VkContext* vkCtx_ = nullptr;
// Per-frame state set during executePostProcessing
VkCommandBuffer currentCmd_ = VK_NULL_HANDLE;
Camera* camera_ = nullptr;
float lastDeltaTime_ = 0.0f;
// Brightness
float brightness_ = 1.0f;
// FSR 1.0 upscaling state
struct FSRState {
bool enabled = false;
bool needsRecreate = false;
float scaleFactor = 1.00f; // Native default
float sharpness = 1.6f;
uint32_t internalWidth = 0;
uint32_t internalHeight = 0;
// Off-screen scene target (reduced resolution)
AllocatedImage sceneColor{}; // 1x color (non-MSAA render target / MSAA resolve target)
AllocatedImage sceneDepth{}; // Depth (matches current MSAA sample count)
AllocatedImage sceneMsaaColor{}; // MSAA color target (only when MSAA > 1x)
AllocatedImage sceneDepthResolve{}; // Depth resolve (only when MSAA + depth resolve)
VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE;
VkSampler sceneSampler = VK_NULL_HANDLE;
// Upscale pipeline
VkPipeline pipeline = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE;
VkDescriptorPool descPool = VK_NULL_HANDLE;
VkDescriptorSet descSet = VK_NULL_HANDLE;
};
FSRState fsr_;
bool initFSRResources();
void destroyFSRResources();
void renderFSRUpscale();
// FXAA post-process state
struct FXAAState {
bool enabled = false;
bool needsRecreate = false;
// Off-screen scene target (same resolution as swapchain — no scaling)
AllocatedImage sceneColor{}; // 1x resolved color target
AllocatedImage sceneDepth{}; // Depth (matches MSAA sample count)
AllocatedImage sceneMsaaColor{}; // MSAA color target (when MSAA > 1x)
AllocatedImage sceneDepthResolve{}; // Depth resolve (MSAA + depth resolve)
VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE;
VkSampler sceneSampler = VK_NULL_HANDLE;
// FXAA fullscreen pipeline
VkPipeline pipeline = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE;
VkDescriptorPool descPool = VK_NULL_HANDLE;
VkDescriptorSet descSet = VK_NULL_HANDLE;
};
FXAAState fxaa_;
bool initFXAAResources();
void destroyFXAAResources();
void renderFXAAPass();
// FSR 2.2 temporal upscaling state
struct FSR2State {
bool enabled = false;
bool needsRecreate = false;
float scaleFactor = 0.77f;
float sharpness = 3.0f; // Very strong RCAS to counteract upscale softness
uint32_t internalWidth = 0;
uint32_t internalHeight = 0;
// Off-screen scene targets (internal resolution, no MSAA — FSR2 replaces AA)
AllocatedImage sceneColor{};
AllocatedImage sceneDepth{};
VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE;
// Samplers
VkSampler linearSampler = VK_NULL_HANDLE; // For color
VkSampler nearestSampler = VK_NULL_HANDLE; // For depth / motion vectors
// Motion vector buffer (internal resolution)
AllocatedImage motionVectors{};
// History buffers (display resolution, ping-pong)
AllocatedImage history[2]{};
AllocatedImage framegenOutput{};
bool framegenOutputValid = false;
uint32_t currentHistory = 0; // Output index (0 or 1)
// Compute pipelines
VkPipeline motionVecPipeline = VK_NULL_HANDLE;
VkPipelineLayout motionVecPipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout motionVecDescSetLayout = VK_NULL_HANDLE;
VkDescriptorPool motionVecDescPool = VK_NULL_HANDLE;
VkDescriptorSet motionVecDescSet = VK_NULL_HANDLE;
VkPipeline accumulatePipeline = VK_NULL_HANDLE;
VkPipelineLayout accumulatePipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout accumulateDescSetLayout = VK_NULL_HANDLE;
VkDescriptorPool accumulateDescPool = VK_NULL_HANDLE;
VkDescriptorSet accumulateDescSets[2] = {}; // Per ping-pong
// RCAS sharpening pass (display resolution)
VkPipeline sharpenPipeline = VK_NULL_HANDLE;
VkPipelineLayout sharpenPipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout sharpenDescSetLayout = VK_NULL_HANDLE;
VkDescriptorPool sharpenDescPool = VK_NULL_HANDLE;
VkDescriptorSet sharpenDescSets[2] = {};
// Previous frame state for motion vector reprojection
glm::mat4 prevViewProjection = glm::mat4(1.0f);
glm::vec2 prevJitter = glm::vec2(0.0f);
uint32_t frameIndex = 0;
bool needsHistoryReset = true;
bool useAmdBackend = false;
bool amdFsr3FramegenEnabled = false;
bool amdFsr3FramegenRuntimeActive = false;
bool amdFsr3FramegenRuntimeReady = false;
std::string amdFsr3RuntimePath = "Path C";
std::string amdFsr3RuntimeLastError{};
size_t amdFsr3UpscaleDispatchCount = 0;
size_t amdFsr3FramegenDispatchCount = 0;
size_t amdFsr3FallbackCount = 0;
uint64_t amdFsr3InteropSyncValue = 1;
float jitterSign = 0.38f;
float motionVecScaleX = 1.0f;
float motionVecScaleY = 1.0f;
#if WOWEE_HAS_AMD_FSR2
FfxFsr2Context amdContext{};
FfxFsr2Interface amdInterface{};
void* amdScratchBuffer = nullptr;
size_t amdScratchBufferSize = 0;
#endif
std::unique_ptr<AmdFsr3Runtime> amdFsr3Runtime;
// Convergent accumulation: jitter for N frames then freeze
int convergenceFrame = 0;
static constexpr int convergenceMaxFrames = 8;
glm::mat4 lastStableVP = glm::mat4(1.0f);
};
FSR2State fsr2_;
bool initFSR2Resources();
void destroyFSR2Resources();
void dispatchMotionVectors();
void dispatchTemporalAccumulate();
void dispatchAmdFsr2();
void dispatchAmdFsr3Framegen();
void renderFSR2Sharpen();
static float halton(uint32_t index, uint32_t base);
};
} // namespace rendering
} // namespace wowee

View file

@ -14,16 +14,12 @@
#include "rendering/vk_frame_data.hpp"
#include "rendering/vk_utils.hpp"
#include "rendering/sky_system.hpp"
#if WOWEE_HAS_AMD_FSR2
#include "ffx_fsr2.h"
#include "ffx_fsr2_vk.h"
#endif
namespace wowee {
namespace core { class Window; }
namespace rendering { class VkContext; }
namespace game { class World; class ZoneManager; class GameHandler; }
namespace audio { class MusicManager; class FootstepManager; class ActivitySoundManager; class MountSoundManager; class NpcVoiceManager; class AmbientSoundManager; class UiSoundManager; class CombatSoundManager; class SpellSoundManager; class MovementSoundManager; enum class FootstepSurface : uint8_t; enum class VoiceType; }
namespace audio { class AudioCoordinator; }
namespace pipeline { class AssetManager; }
namespace rendering {
@ -54,6 +50,12 @@ class WorldMap;
class QuestMarkerRenderer;
class CharacterPreview;
class AmdFsr3Runtime;
class SpellVisualSystem;
class PostProcessPipeline;
class AnimationController;
class LevelUpEffect;
class ChargeEffect;
class SwimEffects;
class Renderer {
public:
@ -148,7 +150,7 @@ public:
float getCharacterYaw() const { return characterYaw; }
void setCharacterYaw(float yawDeg) { characterYaw = yawDeg; }
// Emote support
// Emote support — delegates to AnimationController (§4.2)
void playEmote(const std::string& emoteName);
void triggerLevelUpEffect(const glm::vec3& position);
void cancelEmote();
@ -157,34 +159,41 @@ public:
bool captureScreenshot(const std::string& outputPath);
// Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT)
// useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path
// Delegates to SpellVisualSystem (owned by Renderer)
void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
bool useImpactKit = false);
bool isEmoteActive() const { return emoteActive; }
SpellVisualSystem* getSpellVisualSystem() const { return spellVisualSystem_.get(); }
bool isEmoteActive() const;
static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr);
static uint32_t getEmoteDbcId(const std::string& emoteName);
static std::string getEmoteTextByDbcId(uint32_t dbcId, const std::string& senderName, const std::string* targetName = nullptr);
static uint32_t getEmoteAnimByDbcId(uint32_t dbcId);
// Targeting support
// Targeting support — delegates to AnimationController (§4.2)
void setTargetPosition(const glm::vec3* pos);
void setInCombat(bool combat) { inCombat_ = combat; }
void setInCombat(bool combat);
void resetCombatVisualState();
bool isMoving() const;
void triggerMeleeSwing();
void setEquippedWeaponType(uint32_t inventoryType) { equippedWeaponInvType_ = inventoryType; meleeAnimId = 0; }
void setCharging(bool charging) { charging_ = charging; }
bool isCharging() const { return charging_; }
void setEquippedWeaponType(uint32_t inventoryType);
void setCharging(bool charging);
bool isCharging() const;
void startChargeEffect(const glm::vec3& position, const glm::vec3& direction);
void emitChargeEffect(const glm::vec3& position, const glm::vec3& direction);
void stopChargeEffect();
// Mount rendering
// Mount rendering — delegates to AnimationController (§4.2)
void setMounted(uint32_t mountInstId, uint32_t mountDisplayId, float heightOffset, const std::string& modelPath = "");
void setTaxiFlight(bool onTaxi) { taxiFlight_ = onTaxi; }
void setMountPitchRoll(float pitch, float roll) { mountPitch_ = pitch; mountRoll_ = roll; }
void setTaxiFlight(bool onTaxi);
void setMountPitchRoll(float pitch, float roll);
void clearMount();
bool isMounted() const { return mountInstanceId_ != 0; }
bool isMounted() const;
// AnimationController access (§4.2)
AnimationController* getAnimationController() const { return animationController_.get(); }
LevelUpEffect* getLevelUpEffect() const { return levelUpEffect.get(); }
ChargeEffect* getChargeEffect() const { return chargeEffect.get(); }
SwimEffects* getSwimEffects() const { return swimEffects.get(); }
// Selection circle for targeted entity
void setSelectionCircle(const glm::vec3& pos, float radius, const glm::vec3& color);
@ -197,17 +206,10 @@ public:
double getLastTerrainRenderMs() const { return lastTerrainRenderMs; }
double getLastWMORenderMs() const { return lastWMORenderMs; }
double getLastM2RenderMs() const { return lastM2RenderMs; }
audio::MusicManager* getMusicManager() { return musicManager.get(); }
// Audio coordinator — owned by Application, set via setAudioCoordinator().
void setAudioCoordinator(audio::AudioCoordinator* ac) { audioCoordinator_ = ac; }
audio::AudioCoordinator* getAudioCoordinator() { return audioCoordinator_; }
game::ZoneManager* getZoneManager() { return zoneManager.get(); }
audio::FootstepManager* getFootstepManager() { return footstepManager.get(); }
audio::ActivitySoundManager* getActivitySoundManager() { return activitySoundManager.get(); }
audio::MountSoundManager* getMountSoundManager() { return mountSoundManager.get(); }
audio::NpcVoiceManager* getNpcVoiceManager() { return npcVoiceManager.get(); }
audio::AmbientSoundManager* getAmbientSoundManager() { return ambientSoundManager.get(); }
audio::UiSoundManager* getUiSoundManager() { return uiSoundManager.get(); }
audio::CombatSoundManager* getCombatSoundManager() { return combatSoundManager.get(); }
audio::SpellSoundManager* getSpellSoundManager() { return spellSoundManager.get(); }
audio::MovementSoundManager* getMovementSoundManager() { return movementSoundManager.get(); }
LightingManager* getLightingManager() { return lightingManager.get(); }
private:
@ -239,16 +241,8 @@ private:
std::unique_ptr<Minimap> minimap;
std::unique_ptr<WorldMap> worldMap;
std::unique_ptr<QuestMarkerRenderer> questMarkerRenderer;
std::unique_ptr<audio::MusicManager> musicManager;
std::unique_ptr<audio::FootstepManager> footstepManager;
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<audio::UiSoundManager> uiSoundManager;
std::unique_ptr<audio::CombatSoundManager> combatSoundManager;
std::unique_ptr<audio::SpellSoundManager> spellSoundManager;
std::unique_ptr<audio::MovementSoundManager> movementSoundManager;
audio::AudioCoordinator* audioCoordinator_ = nullptr; // Owned by Application
std::unique_ptr<AnimationController> animationController_; // §4.2
std::unique_ptr<game::ZoneManager> zoneManager;
// Shadow mapping (Vulkan)
static constexpr uint32_t SHADOW_MAP_SIZE = 4096;
@ -282,42 +276,35 @@ public:
float getShadowDistance() const { return shadowDistance_; }
void setMsaaSamples(VkSampleCountFlagBits samples);
// FXAA post-process anti-aliasing (combinable with MSAA)
// Post-process pipeline API — delegates to PostProcessPipeline (§4.3)
PostProcessPipeline* getPostProcessPipeline() const;
void setFXAAEnabled(bool enabled);
bool isFXAAEnabled() const { return fxaa_.enabled; }
// FSR (FidelityFX Super Resolution) upscaling
bool isFXAAEnabled() const;
void setFSREnabled(bool enabled);
bool isFSREnabled() const { return fsr_.enabled; }
void setFSRQuality(float scaleFactor); // 0.59=Balanced, 0.67=Quality, 0.77=UltraQuality, 1.00=Native
void setFSRSharpness(float sharpness); // 0.0 - 2.0
float getFSRScaleFactor() const { return fsr_.scaleFactor; }
float getFSRSharpness() const { return fsr_.sharpness; }
bool isFSREnabled() const;
void setFSRQuality(float scaleFactor);
void setFSRSharpness(float sharpness);
float getFSRScaleFactor() const;
float getFSRSharpness() const;
void setFSR2Enabled(bool enabled);
bool isFSR2Enabled() const { return fsr2_.enabled; }
bool isFSR2Enabled() const;
void setFSR2DebugTuning(float jitterSign, float motionVecScaleX, float motionVecScaleY);
void setAmdFsr3FramegenEnabled(bool enabled);
bool isAmdFsr3FramegenEnabled() const { return fsr2_.amdFsr3FramegenEnabled; }
float getFSR2JitterSign() const { return fsr2_.jitterSign; }
float getFSR2MotionVecScaleX() const { return fsr2_.motionVecScaleX; }
float getFSR2MotionVecScaleY() const { return fsr2_.motionVecScaleY; }
#if WOWEE_HAS_AMD_FSR2
bool isAmdFsr2SdkAvailable() const { return true; }
#else
bool isAmdFsr2SdkAvailable() const { return false; }
#endif
#if WOWEE_HAS_AMD_FSR3_FRAMEGEN
bool isAmdFsr3FramegenSdkAvailable() const { return true; }
#else
bool isAmdFsr3FramegenSdkAvailable() const { return false; }
#endif
bool isAmdFsr3FramegenRuntimeActive() const { return fsr2_.amdFsr3FramegenRuntimeActive; }
bool isAmdFsr3FramegenRuntimeReady() const { return fsr2_.amdFsr3FramegenRuntimeReady; }
bool isAmdFsr3FramegenEnabled() const;
float getFSR2JitterSign() const;
float getFSR2MotionVecScaleX() const;
float getFSR2MotionVecScaleY() const;
bool isAmdFsr2SdkAvailable() const;
bool isAmdFsr3FramegenSdkAvailable() const;
bool isAmdFsr3FramegenRuntimeActive() const;
bool isAmdFsr3FramegenRuntimeReady() const;
const char* getAmdFsr3FramegenRuntimePath() const;
const std::string& getAmdFsr3FramegenRuntimeError() const { return fsr2_.amdFsr3RuntimeLastError; }
size_t getAmdFsr3UpscaleDispatchCount() const { return fsr2_.amdFsr3UpscaleDispatchCount; }
size_t getAmdFsr3FramegenDispatchCount() const { return fsr2_.amdFsr3FramegenDispatchCount; }
size_t getAmdFsr3FallbackCount() const { return fsr2_.amdFsr3FallbackCount; }
const std::string& getAmdFsr3FramegenRuntimeError() const;
size_t getAmdFsr3UpscaleDispatchCount() const;
size_t getAmdFsr3FramegenDispatchCount() const;
size_t getAmdFsr3FallbackCount() const;
void setBrightness(float b);
float getBrightness() const;
void setWaterRefractionEnabled(bool enabled);
bool isWaterRefractionEnabled() const;
@ -331,23 +318,11 @@ private:
pipeline::AssetManager* cachedAssetManager = nullptr;
// Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT
struct SpellVisualInstance {
uint32_t instanceId;
float elapsed;
float duration; // per-instance lifetime in seconds (from M2 anim or default)
};
std::vector<SpellVisualInstance> activeSpellVisuals_;
std::unordered_map<uint32_t, std::string> spellVisualCastPath_; // visualId → cast M2 path
std::unordered_map<uint32_t, std::string> spellVisualImpactPath_; // visualId → impact M2 path
std::unordered_map<std::string, uint32_t> spellVisualModelIds_; // M2 path → M2Renderer modelId
std::unordered_set<uint32_t> spellVisualFailedModels_; // modelIds that failed to load (negative cache)
uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799
bool spellVisualDbcLoaded_ = false;
void loadSpellVisualDbc();
void updateSpellVisuals(float deltaTime);
static constexpr float SPELL_VISUAL_MAX_DURATION = 5.0f;
static constexpr float SPELL_VISUAL_DEFAULT_DURATION = 2.0f;
// Spell visual effects — owned SpellVisualSystem (extracted from Renderer §4.4)
std::unique_ptr<SpellVisualSystem> spellVisualSystem_;
// Post-process pipeline — owns all FSR/FXAA/FSR2 state (extracted §4.3)
std::unique_ptr<PostProcessPipeline> postProcessPipeline_;
uint32_t currentZoneId = 0;
std::string currentZoneName;
@ -365,27 +340,7 @@ private:
uint32_t characterInstanceId = 0;
float characterYaw = 0.0f;
// Character animation state
enum class CharAnimState { IDLE, WALK, RUN, JUMP_START, JUMP_MID, JUMP_END, SIT_DOWN, SITTING, EMOTE, SWIM_IDLE, SWIM, MELEE_SWING, MOUNT, CHARGE, COMBAT_IDLE };
CharAnimState charAnimState = CharAnimState::IDLE;
float locomotionStopGraceTimer_ = 0.0f;
bool locomotionWasSprinting_ = false;
uint32_t lastPlayerAnimRequest_ = UINT32_MAX;
bool lastPlayerAnimLoopRequest_ = true;
void updateCharacterAnimation();
bool isFootstepAnimationState() const;
bool shouldTriggerFootstepEvent(uint32_t animationId, float animationTimeMs, float animationDurationMs);
audio::FootstepSurface resolveFootstepSurface() const;
uint32_t resolveMeleeAnimId();
// Emote state
bool emoteActive = false;
uint32_t emoteAnimId = 0;
bool emoteLoop = false;
// Target facing
const glm::vec3* targetPosition = nullptr;
bool inCombat_ = false;
// Selection circle rendering (Vulkan)
VkPipeline selCirclePipeline = VK_NULL_HANDLE;
@ -408,213 +363,7 @@ private:
void initOverlayPipeline();
void renderOverlay(const glm::vec4& color, VkCommandBuffer overrideCmd = VK_NULL_HANDLE);
// Brightness (1.0 = default, <1 darkens, >1 brightens)
float brightness_ = 1.0f;
public:
void setBrightness(float b) { brightness_ = b; }
float getBrightness() const { return brightness_; }
private:
// FSR 1.0 upscaling state
struct FSRState {
bool enabled = false;
bool needsRecreate = false;
float scaleFactor = 1.00f; // Native default
float sharpness = 1.6f;
uint32_t internalWidth = 0;
uint32_t internalHeight = 0;
// Off-screen scene target (reduced resolution)
AllocatedImage sceneColor{}; // 1x color (non-MSAA render target / MSAA resolve target)
AllocatedImage sceneDepth{}; // Depth (matches current MSAA sample count)
AllocatedImage sceneMsaaColor{}; // MSAA color target (only when MSAA > 1x)
AllocatedImage sceneDepthResolve{}; // Depth resolve (only when MSAA + depth resolve)
VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE;
VkSampler sceneSampler = VK_NULL_HANDLE;
// Upscale pipeline
VkPipeline pipeline = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE;
VkDescriptorPool descPool = VK_NULL_HANDLE;
VkDescriptorSet descSet = VK_NULL_HANDLE;
};
FSRState fsr_;
bool initFSRResources();
void destroyFSRResources();
void renderFSRUpscale();
// FXAA post-process state
struct FXAAState {
bool enabled = false;
bool needsRecreate = false;
// Off-screen scene target (same resolution as swapchain — no scaling)
AllocatedImage sceneColor{}; // 1x resolved color target
AllocatedImage sceneDepth{}; // Depth (matches MSAA sample count)
AllocatedImage sceneMsaaColor{}; // MSAA color target (when MSAA > 1x)
AllocatedImage sceneDepthResolve{}; // Depth resolve (MSAA + depth resolve)
VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE;
VkSampler sceneSampler = VK_NULL_HANDLE;
// FXAA fullscreen pipeline
VkPipeline pipeline = VK_NULL_HANDLE;
VkPipelineLayout pipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout descSetLayout = VK_NULL_HANDLE;
VkDescriptorPool descPool = VK_NULL_HANDLE;
VkDescriptorSet descSet = VK_NULL_HANDLE;
};
FXAAState fxaa_;
bool initFXAAResources();
void destroyFXAAResources();
void renderFXAAPass();
// FSR 2.2 temporal upscaling state
struct FSR2State {
bool enabled = false;
bool needsRecreate = false;
float scaleFactor = 0.77f;
float sharpness = 3.0f; // Very strong RCAS to counteract upscale softness
uint32_t internalWidth = 0;
uint32_t internalHeight = 0;
// Off-screen scene targets (internal resolution, no MSAA — FSR2 replaces AA)
AllocatedImage sceneColor{};
AllocatedImage sceneDepth{};
VkFramebuffer sceneFramebuffer = VK_NULL_HANDLE;
// Samplers
VkSampler linearSampler = VK_NULL_HANDLE; // For color
VkSampler nearestSampler = VK_NULL_HANDLE; // For depth / motion vectors
// Motion vector buffer (internal resolution)
AllocatedImage motionVectors{};
// History buffers (display resolution, ping-pong)
AllocatedImage history[2]{};
AllocatedImage framegenOutput{};
bool framegenOutputValid = false;
uint32_t currentHistory = 0; // Output index (0 or 1)
// Compute pipelines
VkPipeline motionVecPipeline = VK_NULL_HANDLE;
VkPipelineLayout motionVecPipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout motionVecDescSetLayout = VK_NULL_HANDLE;
VkDescriptorPool motionVecDescPool = VK_NULL_HANDLE;
VkDescriptorSet motionVecDescSet = VK_NULL_HANDLE;
VkPipeline accumulatePipeline = VK_NULL_HANDLE;
VkPipelineLayout accumulatePipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout accumulateDescSetLayout = VK_NULL_HANDLE;
VkDescriptorPool accumulateDescPool = VK_NULL_HANDLE;
VkDescriptorSet accumulateDescSets[2] = {}; // Per ping-pong
// RCAS sharpening pass (display resolution)
VkPipeline sharpenPipeline = VK_NULL_HANDLE;
VkPipelineLayout sharpenPipelineLayout = VK_NULL_HANDLE;
VkDescriptorSetLayout sharpenDescSetLayout = VK_NULL_HANDLE;
VkDescriptorPool sharpenDescPool = VK_NULL_HANDLE;
VkDescriptorSet sharpenDescSets[2] = {};
// Previous frame state for motion vector reprojection
glm::mat4 prevViewProjection = glm::mat4(1.0f);
glm::vec2 prevJitter = glm::vec2(0.0f);
uint32_t frameIndex = 0;
bool needsHistoryReset = true;
bool useAmdBackend = false;
bool amdFsr3FramegenEnabled = false;
bool amdFsr3FramegenRuntimeActive = false;
bool amdFsr3FramegenRuntimeReady = false;
std::string amdFsr3RuntimePath = "Path C";
std::string amdFsr3RuntimeLastError{};
size_t amdFsr3UpscaleDispatchCount = 0;
size_t amdFsr3FramegenDispatchCount = 0;
size_t amdFsr3FallbackCount = 0;
uint64_t amdFsr3InteropSyncValue = 1;
float jitterSign = 0.38f;
float motionVecScaleX = 1.0f;
float motionVecScaleY = 1.0f;
#if WOWEE_HAS_AMD_FSR2
FfxFsr2Context amdContext{};
FfxFsr2Interface amdInterface{};
void* amdScratchBuffer = nullptr;
size_t amdScratchBufferSize = 0;
#endif
std::unique_ptr<AmdFsr3Runtime> amdFsr3Runtime;
// Convergent accumulation: jitter for N frames then freeze
int convergenceFrame = 0;
static constexpr int convergenceMaxFrames = 8;
glm::mat4 lastStableVP = glm::mat4(1.0f);
};
FSR2State fsr2_;
bool initFSR2Resources();
void destroyFSR2Resources();
void dispatchMotionVectors();
void dispatchTemporalAccumulate();
void dispatchAmdFsr2();
void dispatchAmdFsr3Framegen();
void renderFSR2Sharpen();
static float halton(uint32_t index, uint32_t base);
// Footstep event tracking (animation-driven)
uint32_t footstepLastAnimationId = 0;
float footstepLastNormTime = 0.0f;
bool footstepNormInitialized = false;
// Footstep surface cache (avoid expensive queries every step)
mutable audio::FootstepSurface cachedFootstepSurface{};
mutable glm::vec3 cachedFootstepPosition{0.0f, 0.0f, 0.0f};
mutable float cachedFootstepUpdateTimer{999.0f}; // Force initial query
// Mount footstep tracking (separate from player's)
uint32_t mountFootstepLastAnimId = 0;
float mountFootstepLastNormTime = 0.0f;
bool mountFootstepNormInitialized = false;
bool sfxStateInitialized = false;
bool sfxPrevGrounded = true;
bool sfxPrevJumping = false;
bool sfxPrevFalling = false;
bool sfxPrevSwimming = false;
bool charging_ = false;
float meleeSwingTimer = 0.0f;
float meleeSwingCooldown = 0.0f;
float meleeAnimDurationMs = 0.0f;
uint32_t meleeAnimId = 0;
uint32_t equippedWeaponInvType_ = 0;
// Mount state
// Mount animation capabilities (discovered at mount time, varies per model)
struct MountAnimSet {
uint32_t jumpStart = 0; // Jump start animation
uint32_t jumpLoop = 0; // Jump airborne loop
uint32_t jumpEnd = 0; // Jump landing
uint32_t rearUp = 0; // Rear-up / special flourish
uint32_t run = 0; // Run animation (discovered, don't assume)
uint32_t stand = 0; // Stand animation (discovered)
std::vector<uint32_t> fidgets; // Idle fidget animations (head turn, tail swish, etc.)
};
enum class MountAction { None, Jump, RearUp };
uint32_t mountInstanceId_ = 0;
float mountHeightOffset_ = 0.0f;
float mountPitch_ = 0.0f; // Up/down tilt (radians)
float mountRoll_ = 0.0f; // Left/right banking (radians)
int mountSeatAttachmentId_ = -1; // -1 unknown, -2 unavailable
glm::vec3 smoothedMountSeatPos_ = glm::vec3(0.0f);
bool mountSeatSmoothingInit_ = false;
float prevMountYaw_ = 0.0f; // Previous yaw for turn rate calculation (procedural lean)
float lastDeltaTime_ = 0.0f; // Cached for use in updateCharacterAnimation()
MountAction mountAction_ = MountAction::None; // Current mount action (jump/rear-up)
uint32_t mountActionPhase_ = 0; // 0=start, 1=loop, 2=end (for jump chaining)
MountAnimSet mountAnims_; // Cached animation IDs for current mount
float mountIdleFidgetTimer_ = 0.0f; // Timer for random idle fidgets
float mountIdleSoundTimer_ = 0.0f; // Timer for ambient idle sounds
uint32_t mountActiveFidget_ = 0; // Currently playing fidget animation ID (0 = none)
bool taxiFlight_ = false;
bool taxiAnimsLogged_ = false;
// Vulkan frame state
VkContext* vkCtx = nullptr;
@ -665,6 +414,7 @@ private:
bool parallelRecordingEnabled_ = false; // set true after pools/buffers created
bool endFrameInlineMode_ = false; // true when endFrame switched to INLINE render pass
float lastDeltaTime_ = 0.0f; // cached for post-process pipeline
bool createSecondaryCommandResources();
void destroySecondaryCommandResources();
VkCommandBuffer beginSecondary(uint32_t secondaryIndex);

View file

@ -0,0 +1,62 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <glm/glm.hpp>
namespace wowee {
namespace pipeline { class AssetManager; }
namespace rendering {
class M2Renderer;
class SpellVisualSystem {
public:
SpellVisualSystem() = default;
~SpellVisualSystem() = default;
// Initialize with references to the M2 renderer (for model loading/instance spawning)
void initialize(M2Renderer* m2Renderer);
void shutdown();
// Spawn a spell visual at a world position.
// useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path
void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
bool useImpactKit = false);
// Advance lifetime timers and remove expired instances.
void update(float deltaTime);
// Remove all active spell visual instances and reset caches.
// Called on map change / combat reset.
void reset();
private:
// Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT
struct SpellVisualInstance {
uint32_t instanceId;
float elapsed;
float duration; // per-instance lifetime in seconds (from M2 anim or default)
};
void loadSpellVisualDbc();
M2Renderer* m2Renderer_ = nullptr;
pipeline::AssetManager* cachedAssetManager_ = nullptr;
std::vector<SpellVisualInstance> activeSpellVisuals_;
std::unordered_map<uint32_t, std::string> spellVisualCastPath_; // visualId → cast M2 path
std::unordered_map<uint32_t, std::string> spellVisualImpactPath_; // visualId → impact M2 path
std::unordered_map<std::string, uint32_t> spellVisualModelIds_; // M2 path → M2Renderer modelId
std::unordered_set<uint32_t> spellVisualFailedModels_; // modelIds that failed to load (negative cache)
uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799
bool spellVisualDbcLoaded_ = false;
static constexpr float SPELL_VISUAL_MAX_DURATION = 5.0f;
static constexpr float SPELL_VISUAL_DEFAULT_DURATION = 2.0f;
};
} // namespace rendering
} // namespace wowee

View file

@ -8,6 +8,7 @@
namespace wowee {
namespace rendering { class Renderer; }
namespace audio { class AudioCoordinator; }
namespace ui {
class InventoryScreen;
@ -144,8 +145,8 @@ public:
void renderSettingsWindow(InventoryScreen& inventoryScreen, ChatPanel& chatPanel,
std::function<void()> saveCallback);
/// Apply audio volume levels to all renderer sound managers
void applyAudioVolumes(rendering::Renderer* renderer);
/// Apply audio volume levels to all audio coordinator sound managers
void applyAudioVolumes(audio::AudioCoordinator* ac);
/// Return the platform-specific settings file path
static std::string getSettingsPath();

View file

@ -6,6 +6,7 @@
#include "core/logger.hpp"
#include "core/application.hpp"
#include "rendering/renderer.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/ui_sound_manager.hpp"
#include "game/expansion_profile.hpp"
#include <imgui.h>
@ -1003,9 +1004,9 @@ static int lua_IsInRaid(lua_State* L) {
// PlaySound(soundId) — play a WoW UI sound by ID or name
static int lua_PlaySound(lua_State* L) {
auto* renderer = core::Application::getInstance().getRenderer();
if (!renderer) return 0;
auto* sfx = renderer->getUiSoundManager();
auto* ac = core::Application::getInstance().getAudioCoordinator();
if (!ac) return 0;
auto* sfx = ac->getUiSoundManager();
if (!sfx) return 0;
// Accept numeric sound ID or string name

View file

@ -50,13 +50,16 @@ bool AudioCoordinator::initialize() {
void AudioCoordinator::initializeWithAssets(pipeline::AssetManager* assetManager) {
if (!audioAvailable_ || !assetManager) return;
// MusicManager needs asset manager for zone music lookups
if (musicManager_) {
musicManager_->initialize(assetManager);
}
// Other managers may need asset manager for sound bank loading
// (Add similar calls as needed for other managers)
if (musicManager_) musicManager_->initialize(assetManager);
if (footstepManager_) footstepManager_->initialize(assetManager);
if (activitySoundManager_) activitySoundManager_->initialize(assetManager);
if (mountSoundManager_) mountSoundManager_->initialize(assetManager);
if (npcVoiceManager_) npcVoiceManager_->initialize(assetManager);
if (ambientSoundManager_) ambientSoundManager_->initialize(assetManager);
if (uiSoundManager_) uiSoundManager_->initialize(assetManager);
if (combatSoundManager_) combatSoundManager_->initialize(assetManager);
if (spellSoundManager_) spellSoundManager_->initialize(assetManager);
if (movementSoundManager_) movementSoundManager_->initialize(assetManager);
LOG_INFO("AudioCoordinator initialized with asset manager");
}

View file

@ -125,6 +125,11 @@ bool Application::initialize() {
return false;
}
// Create and initialize audio coordinator (owns all audio managers)
audioCoordinator_ = std::make_unique<audio::AudioCoordinator>();
audioCoordinator_->initialize();
renderer->setAudioCoordinator(audioCoordinator_.get());
// Create UI manager
uiManager = std::make_unique<ui::UIManager>();
if (!uiManager->initialize(window.get())) {
@ -147,6 +152,7 @@ bool Application::initialize() {
// Populate game services — all subsystems now available
gameServices_.renderer = renderer.get();
gameServices_.audioCoordinator = audioCoordinator_.get();
gameServices_.assetManager = assetManager.get();
gameServices_.expansionRegistry = expansionRegistry_.get();
@ -845,6 +851,12 @@ void Application::shutdown() {
LOG_WARNING("Renderer shutdown complete, resetting...");
renderer.reset();
// Shutdown audio coordinator after renderer (renderer may reference audio during shutdown)
if (audioCoordinator_) {
audioCoordinator_->shutdown();
}
audioCoordinator_.reset();
LOG_WARNING("Resetting world...");
world.reset();
LOG_WARNING("Resetting gameHandler...");
@ -1080,7 +1092,7 @@ void Application::logoutToLogin() {
}
renderer->clearMount();
renderer->setCharacterFollow(0);
if (auto* music = renderer->getMusicManager()) {
if (auto* music = audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr) {
music->stopMusic(0.0f);
}
}
@ -2817,7 +2829,7 @@ void Application::setupUICallbacks() {
// Resolves soundId → SoundEntries.dbc → MPQ path → MusicManager.
gameHandler->setPlayMusicCallback([this](uint32_t soundId) {
if (!assetManager || !renderer) return;
auto* music = renderer->getMusicManager();
auto* music = audioCoordinator_ ? audioCoordinator_->getMusicManager() : nullptr;
if (!music) return;
auto dbc = assetManager->loadDBC("SoundEntries.dbc");
@ -3407,7 +3419,7 @@ void Application::setupUICallbacks() {
// NPC greeting callback - play voice line
gameHandler->setNpcGreetingCallback([this](uint64_t guid, const glm::vec3& position) {
if (renderer && renderer->getNpcVoiceManager()) {
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
// Convert canonical to render coords for 3D audio
glm::vec3 renderPos = core::coords::canonicalToRender(position);
@ -3420,13 +3432,13 @@ void Application::setupUICallbacks() {
voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId);
}
renderer->getNpcVoiceManager()->playGreeting(guid, voiceType, renderPos);
audioCoordinator_->getNpcVoiceManager()->playGreeting(guid, voiceType, renderPos);
}
});
// NPC farewell callback - play farewell voice line
gameHandler->setNpcFarewellCallback([this](uint64_t guid, const glm::vec3& position) {
if (renderer && renderer->getNpcVoiceManager()) {
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
glm::vec3 renderPos = core::coords::canonicalToRender(position);
audio::VoiceType voiceType = audio::VoiceType::GENERIC;
@ -3437,13 +3449,13 @@ void Application::setupUICallbacks() {
voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId);
}
renderer->getNpcVoiceManager()->playFarewell(guid, voiceType, renderPos);
audioCoordinator_->getNpcVoiceManager()->playFarewell(guid, voiceType, renderPos);
}
});
// NPC vendor callback - play vendor voice line
gameHandler->setNpcVendorCallback([this](uint64_t guid, const glm::vec3& position) {
if (renderer && renderer->getNpcVoiceManager()) {
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
glm::vec3 renderPos = core::coords::canonicalToRender(position);
audio::VoiceType voiceType = audio::VoiceType::GENERIC;
@ -3454,13 +3466,13 @@ void Application::setupUICallbacks() {
voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId);
}
renderer->getNpcVoiceManager()->playVendor(guid, voiceType, renderPos);
audioCoordinator_->getNpcVoiceManager()->playVendor(guid, voiceType, renderPos);
}
});
// NPC aggro callback - play combat start voice line
gameHandler->setNpcAggroCallback([this](uint64_t guid, const glm::vec3& position) {
if (renderer && renderer->getNpcVoiceManager()) {
if (audioCoordinator_ && audioCoordinator_->getNpcVoiceManager()) {
glm::vec3 renderPos = core::coords::canonicalToRender(position);
audio::VoiceType voiceType = audio::VoiceType::GENERIC;
@ -3471,7 +3483,7 @@ void Application::setupUICallbacks() {
voiceType = entitySpawner_->detectVoiceTypeFromDisplayId(displayId);
}
renderer->getNpcVoiceManager()->playAggro(guid, voiceType, renderPos);
audioCoordinator_->getNpcVoiceManager()->playAggro(guid, voiceType, renderPos);
}
});
@ -3707,7 +3719,7 @@ void Application::spawnPlayerCharacter() {
playerCharacterSpawned = true;
// Set voice profile to match character race/gender
if (auto* asm_ = renderer->getActivitySoundManager()) {
if (auto* asm_ = audioCoordinator_ ? audioCoordinator_->getActivitySoundManager() : nullptr) {
const char* raceFolder = "Human";
const char* raceBase = "Human";
switch (playerRace_) {

View file

@ -6,6 +6,7 @@
#include "game/update_field_table.hpp"
#include "game/opcode_table.hpp"
#include "rendering/renderer.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/combat_sound_manager.hpp"
#include "audio/activity_sound_manager.hpp"
#include "core/application.hpp"
@ -451,8 +452,8 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
}
// Play combat sounds via CombatSoundManager + character vocalizations
if (auto* renderer = owner_.services().renderer) {
if (auto* csm = renderer->getCombatSoundManager()) {
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* csm = ac->getCombatSoundManager()) {
auto weaponSize = audio::CombatSoundManager::WeaponSize::MEDIUM;
if (data.isMiss()) {
csm->playWeaponMiss(false);
@ -466,7 +467,7 @@ void CombatHandler::handleAttackerStateUpdate(network::Packet& packet) {
}
}
// Character vocalizations
if (auto* asm_ = renderer->getActivitySoundManager()) {
if (auto* asm_ = ac->getActivitySoundManager()) {
if (isPlayerAttacker && !data.isMiss() && data.victimState != 1 && data.victimState != 2) {
asm_->playAttackGrunt();
}

View file

@ -17,6 +17,7 @@
#include "game/update_field_table.hpp"
#include "game/expansion_profile.hpp"
#include "rendering/renderer.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/activity_sound_manager.hpp"
#include "audio/combat_sound_manager.hpp"
#include "audio/spell_sound_manager.hpp"
@ -599,8 +600,8 @@ static QuestQueryRewards tryParseQuestRewards(const std::vector<uint8_t>& data,
template<typename ManagerGetter, typename Callback>
void GameHandler::withSoundManager(ManagerGetter getter, Callback cb) {
if (auto* renderer = services_.renderer) {
if (auto* mgr = (renderer->*getter)()) cb(mgr);
if (auto* ac = services_.audioCoordinator) {
if (auto* mgr = (ac->*getter)()) cb(mgr);
}
}
@ -1198,9 +1199,9 @@ void GameHandler::updateTimers(float deltaTime) {
}
if (!alreadyAnnounced && pendingLootMoneyAmount_ > 0) {
addSystemChatMessage("Looted: " + formatCopperAmount(pendingLootMoneyAmount_));
auto* renderer = services_.renderer;
if (renderer) {
if (auto* sfx = renderer->getUiSoundManager()) {
auto* ac = services_.audioCoordinator;
if (ac) {
if (auto* sfx = ac->getUiSoundManager()) {
if (pendingLootMoneyAmount_ >= 10000) {
sfx->playLootCoinLarge();
} else {
@ -1974,7 +1975,7 @@ void GameHandler::registerOpcodeHandlers() {
ping.age = 0.0f;
minimapPings_.push_back(ping);
if (senderGuid != playerGuid) {
withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playMinimapPing(); });
withSoundManager(&audio::AudioCoordinator::getUiSoundManager, [](auto* sfx) { sfx->playMinimapPing(); });
}
};
dispatchTable_[Opcode::SMSG_ZONE_UNDER_ATTACK] = [this](network::Packet& packet) {
@ -2124,7 +2125,7 @@ void GameHandler::registerOpcodeHandlers() {
if (info && info->type == 17) {
addUIError("A fish is on your line!");
addSystemChatMessage("A fish is on your line!");
withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playQuestUpdate(); });
withSoundManager(&audio::AudioCoordinator::getUiSoundManager, [](auto* sfx) { sfx->playQuestUpdate(); });
}
}
}
@ -2350,7 +2351,7 @@ void GameHandler::registerOpcodeHandlers() {
}
if (newLevel > oldLevel) {
addSystemChatMessage("You have reached level " + std::to_string(newLevel) + "!");
withSoundManager(&rendering::Renderer::getUiSoundManager, [](auto* sfx) { sfx->playLevelUp(); });
withSoundManager(&audio::AudioCoordinator::getUiSoundManager, [](auto* sfx) { sfx->playLevelUp(); });
if (levelUpCallback_) levelUpCallback_(newLevel);
fireAddonEvent("PLAYER_LEVEL_UP", {std::to_string(newLevel)});
}

View file

@ -4,6 +4,7 @@
#include "game/entity.hpp"
#include "game/packet_parsers.hpp"
#include "rendering/renderer.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/ui_sound_manager.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
@ -70,9 +71,9 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
}
if (!alreadyAnnounced) {
owner_.addSystemChatMessage("Looted: " + formatCopperAmount(amount));
auto* renderer = owner_.services().renderer;
if (renderer) {
if (auto* sfx = renderer->getUiSoundManager()) {
auto* ac = owner_.services().audioCoordinator;
if (ac) {
if (auto* sfx = ac->getUiSoundManager()) {
if (amount >= 10000) sfx->playLootCoinLarge();
else sfx->playLootCoinSmall();
}
@ -222,8 +223,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
std::string msg = "Received item: " + link;
if (count > 1) msg += " x" + std::to_string(count);
owner_.addSystemChatMessage(msg);
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playLootItem();
}
if (owner_.addonEventCallback_) {
@ -253,8 +254,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
" result=", static_cast<int>(result));
if (result == 0) {
pendingSellToBuyback_.erase(itemGuid);
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playDropOnGround();
}
if (owner_.addonEventCallback_) {
@ -295,8 +296,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error";
owner_.addUIError(std::string("Sell failed: ") + msg);
owner_.addSystemChatMessage(std::string("Sell failed: ") + msg);
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playError();
}
LOG_WARNING("SMSG_SELL_ITEM error: ", (int)result, " (", msg, ")");
@ -392,8 +393,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ").";
owner_.addUIError(msg);
owner_.addSystemChatMessage(msg);
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playError();
}
}
@ -450,8 +451,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
}
owner_.addUIError(msg);
owner_.addSystemChatMessage(msg);
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playError();
}
}
@ -474,8 +475,8 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) {
std::string msg = "Purchased: " + buildItemLink(pendingBuyItemId_, buyQuality, itemLabel);
if (itemCount > 1) msg += " x" + std::to_string(itemCount);
owner_.addSystemChatMessage(msg);
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playPickupBag();
}
}
@ -766,8 +767,8 @@ void InventoryHandler::handleLootRemoved(network::Packet& packet) {
std::string msgStr = "Looted: " + link;
if (it->count > 1) msgStr += " x" + std::to_string(it->count);
owner_.addSystemChatMessage(msgStr);
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playLootItem();
}
currentLoot_.items.erase(it);
@ -2382,8 +2383,8 @@ void InventoryHandler::handleItemQueryResponse(network::Packet& packet) {
std::string msg = "Received: " + link;
if (it->count > 1) msg += " x" + std::to_string(it->count);
owner_.addSystemChatMessage(msg);
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager()) sfx->playLootItem();
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager()) sfx->playLootItem();
}
if (owner_.itemLootCallback_) owner_.itemLootCallback_(data.entry, it->count, data.quality, itemName);
it = owner_.pendingItemPushNotifs_.erase(it);
@ -3149,8 +3150,8 @@ void InventoryHandler::handleTrainerBuySucceeded(network::Packet& packet) {
owner_.addSystemChatMessage("You have learned " + name + ".");
else
owner_.addSystemChatMessage("Spell learned.");
if (auto* renderer = owner_.services().renderer)
if (auto* sfx = renderer->getUiSoundManager()) sfx->playQuestActivate();
if (auto* ac = owner_.services().audioCoordinator)
if (auto* sfx = ac->getUiSoundManager()) sfx->playQuestActivate();
owner_.fireAddonEvent("TRAINER_UPDATE", {});
owner_.fireAddonEvent("SPELLS_CHANGED", {});
}
@ -3171,8 +3172,8 @@ void InventoryHandler::handleTrainerBuyFailed(network::Packet& packet) {
else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")";
owner_.addUIError(msg);
owner_.addSystemChatMessage(msg);
if (auto* renderer = owner_.services().renderer)
if (auto* sfx = renderer->getUiSoundManager()) sfx->playError();
if (auto* ac = owner_.services().audioCoordinator)
if (auto* sfx = ac->getUiSoundManager()) sfx->playError();
}
// ============================================================

View file

@ -6,6 +6,7 @@
#include "game/packet_parsers.hpp"
#include "network/world_socket.hpp"
#include "rendering/renderer.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/ui_sound_manager.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
@ -469,8 +470,8 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
owner_.questCompleteCallback_(questId, it->title);
}
// Play quest-complete sound
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playQuestComplete();
}
questLog_.erase(it);
@ -1095,8 +1096,8 @@ void QuestHandler::acceptQuest() {
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
// Play quest-accept sound
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playQuestActivate();
}

View file

@ -5,6 +5,7 @@
#include "game/packet_parsers.hpp"
#include "game/update_field_table.hpp"
#include "game/opcode_table.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/ui_sound_manager.hpp"
#include "network/world_socket.hpp"
#include "rendering/renderer.hpp"
@ -1053,8 +1054,8 @@ void SocialHandler::handleDuelRequested(network::Packet& packet) {
}
pendingDuelRequest_ = true;
owner_.addSystemChatMessage(duelChallengerName_ + " challenges you to a duel!");
if (auto* renderer = owner_.services().renderer)
if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect();
if (auto* ac = owner_.services().audioCoordinator)
if (auto* sfx = ac->getUiSoundManager()) sfx->playTargetSelect();
if (owner_.addonEventCallback_) owner_.addonEventCallback_("DUEL_REQUESTED", {duelChallengerName_});
}
@ -1219,8 +1220,8 @@ void SocialHandler::handleGroupInvite(network::Packet& packet) {
pendingInviterName = data.inviterName;
if (!data.inviterName.empty())
owner_.addSystemChatMessage(data.inviterName + " has invited you to a group.");
if (auto* renderer = owner_.services().renderer)
if (auto* sfx = renderer->getUiSoundManager()) sfx->playTargetSelect();
if (auto* ac = owner_.services().audioCoordinator)
if (auto* sfx = ac->getUiSoundManager()) sfx->playTargetSelect();
if (owner_.addonEventCallback_)
owner_.addonEventCallback_("PARTY_INVITE_REQUEST", {data.inviterName});
}

View file

@ -4,6 +4,7 @@
#include "game/packet_parsers.hpp"
#include "game/entity.hpp"
#include "rendering/renderer.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/spell_sound_manager.hpp"
#include "audio/combat_sound_manager.hpp"
#include "core/application.hpp"
@ -795,8 +796,8 @@ void SpellHandler::handleCastFailed(network::Packet& packet) {
queuedSpellTarget_ = 0;
// Stop precast sound
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* ssm = ac->getSpellSoundManager()) {
ssm->stopPrecast();
}
}
@ -817,8 +818,8 @@ void SpellHandler::handleCastFailed(network::Packet& packet) {
msg.message = errMsg;
owner_.addLocalChatMessage(msg);
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playError();
}
@ -869,8 +870,8 @@ void SpellHandler::handleSpellStart(network::Packet& packet) {
// Play precast sound — skip profession/tradeskill spells
if (!owner_.isProfessionSpell(data.spellId)) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* ssm = ac->getSpellSoundManager()) {
owner_.loadSpellNameCache();
auto it = owner_.spellNameCache_.find(data.spellId);
auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask)
@ -907,8 +908,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
if (data.casterUnit == owner_.playerGuid) {
// Play cast-complete sound
if (!owner_.isProfessionSpell(data.spellId)) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* ssm = ac->getSpellSoundManager()) {
owner_.loadSpellNameCache();
auto it = owner_.spellNameCache_.find(data.spellId);
auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask)
@ -931,8 +932,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
}
if (isMeleeAbility) {
if (owner_.meleeSwingCallback_) owner_.meleeSwingCallback_();
if (auto* renderer = owner_.services().renderer) {
if (auto* csm = renderer->getCombatSoundManager()) {
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* csm = ac->getCombatSoundManager()) {
csm->playWeaponSwing(audio::CombatSoundManager::WeaponSize::MEDIUM, false);
csm->playImpact(audio::CombatSoundManager::WeaponSize::MEDIUM,
audio::CombatSoundManager::ImpactType::FLESH, false);
@ -990,8 +991,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
if (tgt == owner_.playerGuid) { targetsPlayer = true; break; }
}
if (targetsPlayer) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* ssm = ac->getSpellSoundManager()) {
owner_.loadSpellNameCache();
auto it = owner_.spellNameCache_.find(data.spellId);
auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask)
@ -1036,8 +1037,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) {
}
if (playerIsHit || playerHitEnemy) {
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* ssm = ac->getSpellSoundManager()) {
owner_.loadSpellNameCache();
auto it = owner_.spellNameCache_.find(data.spellId);
auto school = (it != owner_.spellNameCache_.end() && it->second.schoolMask)
@ -1396,8 +1397,8 @@ void SpellHandler::handleAchievementEarned(network::Packet& packet) {
owner_.earnedAchievements_.insert(achievementId);
owner_.achievementDates_[achievementId] = earnDate;
if (auto* renderer = owner_.services().renderer) {
if (auto* sfx = renderer->getUiSoundManager())
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
sfx->playAchievementAlert();
}
if (owner_.achievementEarnedCallback_) {
@ -2350,8 +2351,8 @@ void SpellHandler::handleSpellFailure(network::Packet& packet) {
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
if (auto* renderer = owner_.services().renderer) {
if (auto* ssm = renderer->getSpellSoundManager()) {
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* ssm = ac->getSpellSoundManager()) {
ssm->stopPrecast();
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,231 @@
#include "rendering/spell_visual_system.hpp"
#include "rendering/m2_renderer.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_loader.hpp"
#include "pipeline/dbc_layout.hpp"
#include "pipeline/m2_loader.hpp"
#include "core/application.hpp"
#include "core/logger.hpp"
#include <algorithm>
namespace wowee {
namespace rendering {
void SpellVisualSystem::initialize(M2Renderer* m2Renderer) {
m2Renderer_ = m2Renderer;
}
void SpellVisualSystem::shutdown() {
reset();
m2Renderer_ = nullptr;
cachedAssetManager_ = nullptr;
}
// Load SpellVisual DBC chain: SpellVisualEffectName → SpellVisualKit → SpellVisual
// to build cast/impact M2 path lookup maps.
void SpellVisualSystem::loadSpellVisualDbc() {
if (spellVisualDbcLoaded_) return;
spellVisualDbcLoaded_ = true; // Set early to prevent re-entry on failure
if (!cachedAssetManager_) {
cachedAssetManager_ = core::Application::getInstance().getAssetManager();
}
if (!cachedAssetManager_) return;
auto* layout = pipeline::getActiveDBCLayout();
const pipeline::DBCFieldMap* svLayout = layout ? layout->getLayout("SpellVisual") : nullptr;
const pipeline::DBCFieldMap* kitLayout = layout ? layout->getLayout("SpellVisualKit") : nullptr;
const pipeline::DBCFieldMap* fxLayout = layout ? layout->getLayout("SpellVisualEffectName") : nullptr;
uint32_t svCastKitField = svLayout ? (*svLayout)["CastKit"] : 2;
uint32_t svImpactKitField = svLayout ? (*svLayout)["ImpactKit"] : 3;
uint32_t svMissileField = svLayout ? (*svLayout)["MissileModel"] : 8;
uint32_t kitSpecial0Field = kitLayout ? (*kitLayout)["SpecialEffect0"] : 11;
uint32_t kitBaseField = kitLayout ? (*kitLayout)["BaseEffect"] : 5;
uint32_t fxFilePathField = fxLayout ? (*fxLayout)["FilePath"] : 2;
// Helper to look up effectName path from a kit ID
// Load SpellVisualEffectName.dbc — ID → M2 path
auto fxDbc = cachedAssetManager_->loadDBC("SpellVisualEffectName.dbc");
if (!fxDbc || !fxDbc->isLoaded() || fxDbc->getFieldCount() <= fxFilePathField) {
LOG_DEBUG("SpellVisual: SpellVisualEffectName.dbc unavailable (fc=",
fxDbc ? fxDbc->getFieldCount() : 0, ")");
return;
}
std::unordered_map<uint32_t, std::string> effectPaths; // effectNameId → path
for (uint32_t i = 0; i < fxDbc->getRecordCount(); ++i) {
uint32_t id = fxDbc->getUInt32(i, 0);
std::string p = fxDbc->getString(i, fxFilePathField);
if (id && !p.empty()) effectPaths[id] = p;
}
// Load SpellVisualKit.dbc — kitId → best SpellVisualEffectName ID
auto kitDbc = cachedAssetManager_->loadDBC("SpellVisualKit.dbc");
std::unordered_map<uint32_t, uint32_t> kitToEffectName; // kitId → effectNameId
if (kitDbc && kitDbc->isLoaded()) {
uint32_t fc = kitDbc->getFieldCount();
for (uint32_t i = 0; i < kitDbc->getRecordCount(); ++i) {
uint32_t kitId = kitDbc->getUInt32(i, 0);
if (!kitId) continue;
// Prefer SpecialEffect0, fall back to BaseEffect
uint32_t eff = 0;
if (kitSpecial0Field < fc) eff = kitDbc->getUInt32(i, kitSpecial0Field);
if (!eff && kitBaseField < fc) eff = kitDbc->getUInt32(i, kitBaseField);
if (eff) kitToEffectName[kitId] = eff;
}
}
// Helper: resolve path for a given kit ID
auto kitPath = [&](uint32_t kitId) -> std::string {
if (!kitId) return {};
auto kitIt = kitToEffectName.find(kitId);
if (kitIt == kitToEffectName.end()) return {};
auto fxIt = effectPaths.find(kitIt->second);
return (fxIt != effectPaths.end()) ? fxIt->second : std::string{};
};
auto missilePath = [&](uint32_t effId) -> std::string {
if (!effId) return {};
auto fxIt = effectPaths.find(effId);
return (fxIt != effectPaths.end()) ? fxIt->second : std::string{};
};
// Load SpellVisual.dbc — visualId → cast/impact M2 paths via kit chain
auto svDbc = cachedAssetManager_->loadDBC("SpellVisual.dbc");
if (!svDbc || !svDbc->isLoaded()) {
LOG_DEBUG("SpellVisual: SpellVisual.dbc unavailable");
return;
}
uint32_t svFc = svDbc->getFieldCount();
uint32_t loadedCast = 0, loadedImpact = 0;
for (uint32_t i = 0; i < svDbc->getRecordCount(); ++i) {
uint32_t vid = svDbc->getUInt32(i, 0);
if (!vid) continue;
// Cast path: CastKit → SpecialEffect0/BaseEffect, fallback to MissileModel
{
std::string path;
if (svCastKitField < svFc)
path = kitPath(svDbc->getUInt32(i, svCastKitField));
if (path.empty() && svMissileField < svFc)
path = missilePath(svDbc->getUInt32(i, svMissileField));
if (!path.empty()) { spellVisualCastPath_[vid] = path; ++loadedCast; }
}
// Impact path: ImpactKit → SpecialEffect0/BaseEffect, fallback to MissileModel
{
std::string path;
if (svImpactKitField < svFc)
path = kitPath(svDbc->getUInt32(i, svImpactKitField));
if (path.empty() && svMissileField < svFc)
path = missilePath(svDbc->getUInt32(i, svMissileField));
if (!path.empty()) { spellVisualImpactPath_[vid] = path; ++loadedImpact; }
}
}
LOG_INFO("SpellVisual: loaded cast=", loadedCast, " impact=", loadedImpact,
" visual→M2 mappings (of ", svDbc->getRecordCount(), " records)");
}
void SpellVisualSystem::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
bool useImpactKit) {
if (!m2Renderer_ || visualId == 0) return;
if (!cachedAssetManager_)
cachedAssetManager_ = core::Application::getInstance().getAssetManager();
if (!cachedAssetManager_) return;
if (!spellVisualDbcLoaded_) loadSpellVisualDbc();
// Select cast or impact path map
auto& pathMap = useImpactKit ? spellVisualImpactPath_ : spellVisualCastPath_;
auto pathIt = pathMap.find(visualId);
if (pathIt == pathMap.end()) return; // No model for this visual
const std::string& modelPath = pathIt->second;
// Get or assign a model ID for this path
auto midIt = spellVisualModelIds_.find(modelPath);
uint32_t modelId = 0;
if (midIt != spellVisualModelIds_.end()) {
modelId = midIt->second;
} else {
if (nextSpellVisualModelId_ >= 999800) {
LOG_WARNING("SpellVisual: model ID pool exhausted");
return;
}
modelId = nextSpellVisualModelId_++;
spellVisualModelIds_[modelPath] = modelId;
}
// Skip models that have previously failed to load (avoid repeated I/O)
if (spellVisualFailedModels_.count(modelId)) return;
// Load the M2 model if not already loaded
if (!m2Renderer_->hasModel(modelId)) {
auto m2Data = cachedAssetManager_->readFile(modelPath);
if (m2Data.empty()) {
LOG_DEBUG("SpellVisual: could not read model: ", modelPath);
spellVisualFailedModels_.insert(modelId);
return;
}
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
if (model.vertices.empty() && model.particleEmitters.empty()) {
LOG_DEBUG("SpellVisual: empty model: ", modelPath);
spellVisualFailedModels_.insert(modelId);
return;
}
// Load skin file for WotLK-format M2s
if (model.version >= 264) {
std::string skinPath = modelPath.substr(0, modelPath.rfind('.')) + "00.skin";
auto skinData = cachedAssetManager_->readFile(skinPath);
if (!skinData.empty()) pipeline::M2Loader::loadSkin(skinData, model);
}
if (!m2Renderer_->loadModel(model, modelId)) {
LOG_WARNING("SpellVisual: failed to load model to GPU: ", modelPath);
spellVisualFailedModels_.insert(modelId);
return;
}
LOG_DEBUG("SpellVisual: loaded model id=", modelId, " path=", modelPath);
}
// Spawn instance at world position
uint32_t instanceId = m2Renderer_->createInstance(modelId, worldPosition,
glm::vec3(0.0f), 1.0f);
if (instanceId == 0) {
LOG_WARNING("SpellVisual: failed to create instance for visualId=", visualId);
return;
}
// Determine lifetime from M2 animation duration (clamp to reasonable range)
float animDurMs = m2Renderer_->getInstanceAnimDuration(instanceId);
float duration = (animDurMs > 100.0f)
? std::clamp(animDurMs / 1000.0f, 0.5f, SPELL_VISUAL_MAX_DURATION)
: SPELL_VISUAL_DEFAULT_DURATION;
activeSpellVisuals_.push_back({instanceId, 0.0f, duration});
LOG_DEBUG("SpellVisual: spawned visualId=", visualId, " instanceId=", instanceId,
" duration=", duration, "s model=", modelPath);
}
void SpellVisualSystem::update(float deltaTime) {
if (activeSpellVisuals_.empty() || !m2Renderer_) return;
for (auto it = activeSpellVisuals_.begin(); it != activeSpellVisuals_.end(); ) {
it->elapsed += deltaTime;
if (it->elapsed >= it->duration) {
m2Renderer_->removeInstance(it->instanceId);
it = activeSpellVisuals_.erase(it);
} else {
++it;
}
}
}
void SpellVisualSystem::reset() {
// Clear lingering spell visual instances from the previous map/combat session.
// Without this, old effects could remain visible after teleport or map change.
for (auto& sv : activeSpellVisuals_) {
if (m2Renderer_) m2Renderer_->removeInstance(sv.instanceId);
}
activeSpellVisuals_.clear();
// Reset the negative cache so models that failed during asset loading can retry.
spellVisualFailedModels_.clear();
}
} // namespace rendering
} // namespace wowee

View file

@ -6,6 +6,7 @@
#include "rendering/renderer.hpp"
#include "rendering/vk_context.hpp"
#include "pipeline/asset_manager.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/music_manager.hpp"
#include "game/expansion_profile.hpp"
#include <imgui.h>
@ -199,20 +200,20 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
}
auto& app = core::Application::getInstance();
auto* renderer = app.getRenderer();
auto* ac = app.getAudioCoordinator();
if (!musicInitAttempted) {
musicInitAttempted = true;
auto* assets = app.getAssetManager();
if (renderer) {
auto* music = renderer->getMusicManager();
if (ac) {
auto* music = ac->getMusicManager();
if (music && assets && assets->isInitialized() && !music->isInitialized()) {
music->initialize(assets);
}
}
}
// Login screen music
if (renderer) {
auto* music = renderer->getMusicManager();
if (ac) {
auto* music = ac->getMusicManager();
if (music) {
if (!loginMusicVolumeAdjusted_) {
savedMusicVolume_ = music->getVolume();
@ -506,9 +507,9 @@ void AuthScreen::render(auth::AuthHandler& authHandler) {
void AuthScreen::stopLoginMusic() {
auto& app = core::Application::getInstance();
auto* renderer = app.getRenderer();
if (!renderer) return;
auto* music = renderer->getMusicManager();
auto* ac = app.getAudioCoordinator();
if (!ac) return;
auto* music = ac->getMusicManager();
if (!music) return;
if (musicPlaying) {
music->stopMusic(500.0f);

View file

@ -11,6 +11,7 @@
#include "rendering/renderer.hpp"
#include "rendering/camera.hpp"
#include "rendering/camera_controller.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/audio_engine.hpp"
#include "audio/ui_sound_manager.hpp"
#include "pipeline/asset_manager.hpp"
@ -1109,8 +1110,8 @@ void ChatPanel::render(game::GameHandler& gameHandler,
std::string bodyLower = mMsg.message;
for (auto& c : bodyLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
if (bodyLower.find(selfNameLower) != std::string::npos) {
if (auto* renderer = services_.renderer) {
if (auto* ui = renderer->getUiSoundManager())
if (auto* ac = services_.audioCoordinator) {
if (auto* ui = ac->getUiSoundManager())
ui->playWhisperReceived();
}
break; // play at most once per scan pass

View file

@ -15,6 +15,7 @@
#include "rendering/camera.hpp"
#include "game/game_handler.hpp"
#include "pipeline/asset_manager.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/audio_engine.hpp"
#include "audio/ui_sound_manager.hpp"
#include <imgui.h>
@ -283,9 +284,11 @@ void CombatUI::renderRaidWarningOverlay(game::GameHandler& gameHandler) {
raidWarnEntries_.erase(raidWarnEntries_.begin());
}
// Whisper audio notification
if (msg.type == game::ChatType::WHISPER && renderer) {
if (auto* ui = renderer->getUiSoundManager())
ui->playWhisperReceived();
if (msg.type == game::ChatType::WHISPER) {
if (auto* ac = services_.audioCoordinator) {
if (auto* ui = ac->getUiSoundManager())
ui->playWhisperReceived();
}
}
}
raidWarnChatSeenCount_ = newCount;

View file

@ -14,6 +14,7 @@
#include "rendering/character_renderer.hpp"
#include "rendering/camera.hpp"
#include "rendering/camera_controller.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/audio_engine.hpp"
#include "audio/music_manager.hpp"
#include "game/zone_manager.hpp"
@ -285,8 +286,8 @@ void GameScreen::render(game::GameHandler& gameHandler) {
uiErrors_.push_back({msg, 0.0f});
if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin());
// Play error sound for each new error (rate-limited by deque cap of 5)
if (auto* r = services_.renderer) {
if (auto* sfx = r->getUiSoundManager()) sfx->playError();
if (auto* ac = services_.audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager()) sfx->playError();
}
});
uiErrorCallbackSet_ = true;
@ -345,9 +346,9 @@ void GameScreen::render(game::GameHandler& gameHandler) {
// Apply saved volume settings once when audio managers first become available
if (!settingsPanel_.volumeSettingsApplied_) {
auto* renderer = services_.renderer;
if (renderer && renderer->getUiSoundManager()) {
settingsPanel_.applyAudioVolumes(renderer);
auto* ac = services_.audioCoordinator;
if (ac && ac->getUiSoundManager()) {
settingsPanel_.applyAudioVolumes(ac);
settingsPanel_.volumeSettingsApplied_ = true;
}
}
@ -6525,38 +6526,38 @@ void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
}
auto applyMuteState = [&]() {
auto* activeRenderer = services_.renderer;
auto* ac = services_.audioCoordinator;
float masterScale = settingsPanel_.soundMuted_ ? 0.0f : static_cast<float>(settingsPanel_.pendingMasterVolume) / 100.0f;
audio::AudioEngine::instance().setMasterVolume(masterScale);
if (!activeRenderer) return;
if (auto* music = activeRenderer->getMusicManager()) {
if (!ac) return;
if (auto* music = ac->getMusicManager()) {
music->setVolume(settingsPanel_.pendingMusicVolume);
}
if (auto* ambient = activeRenderer->getAmbientSoundManager()) {
if (auto* ambient = ac->getAmbientSoundManager()) {
ambient->setVolumeScale(settingsPanel_.pendingAmbientVolume / 100.0f);
}
if (auto* ui = activeRenderer->getUiSoundManager()) {
if (auto* ui = ac->getUiSoundManager()) {
ui->setVolumeScale(settingsPanel_.pendingUiVolume / 100.0f);
}
if (auto* combat = activeRenderer->getCombatSoundManager()) {
if (auto* combat = ac->getCombatSoundManager()) {
combat->setVolumeScale(settingsPanel_.pendingCombatVolume / 100.0f);
}
if (auto* spell = activeRenderer->getSpellSoundManager()) {
if (auto* spell = ac->getSpellSoundManager()) {
spell->setVolumeScale(settingsPanel_.pendingSpellVolume / 100.0f);
}
if (auto* movement = activeRenderer->getMovementSoundManager()) {
if (auto* movement = ac->getMovementSoundManager()) {
movement->setVolumeScale(settingsPanel_.pendingMovementVolume / 100.0f);
}
if (auto* footstep = activeRenderer->getFootstepManager()) {
if (auto* footstep = ac->getFootstepManager()) {
footstep->setVolumeScale(settingsPanel_.pendingFootstepVolume / 100.0f);
}
if (auto* npcVoice = activeRenderer->getNpcVoiceManager()) {
if (auto* npcVoice = ac->getNpcVoiceManager()) {
npcVoice->setVolumeScale(settingsPanel_.pendingNpcVoiceVolume / 100.0f);
}
if (auto* mount = activeRenderer->getMountSoundManager()) {
if (auto* mount = ac->getMountSoundManager()) {
mount->setVolumeScale(settingsPanel_.pendingMountVolume / 100.0f);
}
if (auto* activity = activeRenderer->getActivitySoundManager()) {
if (auto* activity = ac->getActivitySoundManager()) {
activity->setVolumeScale(settingsPanel_.pendingActivityVolume / 100.0f);
}
};

View file

@ -17,6 +17,7 @@
#include "rendering/wmo_renderer.hpp"
#include "rendering/character_renderer.hpp"
#include "game/zone_manager.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/audio_engine.hpp"
#include "audio/music_manager.hpp"
#include "audio/ambient_sound_manager.hpp"
@ -439,7 +440,7 @@ ImGui::BeginChild("AudioSettings", ImVec2(0, 360), true);
// Helper lambda to apply audio settings
auto applyAudioSettings = [&]() {
applyAudioVolumes(renderer);
applyAudioVolumes(services_.audioCoordinator);
saveCallback();
};
@ -1227,29 +1228,29 @@ std::string SettingsPanel::getSettingsPath() {
return dir + "/settings.cfg";
}
void SettingsPanel::applyAudioVolumes(rendering::Renderer* renderer) {
if (!renderer) return;
void SettingsPanel::applyAudioVolumes(audio::AudioCoordinator* ac) {
if (!ac) return;
float masterScale = soundMuted_ ? 0.0f : static_cast<float>(pendingMasterVolume) / 100.0f;
audio::AudioEngine::instance().setMasterVolume(masterScale);
if (auto* music = renderer->getMusicManager())
if (auto* music = ac->getMusicManager())
music->setVolume(pendingMusicVolume);
if (auto* ambient = renderer->getAmbientSoundManager())
if (auto* ambient = ac->getAmbientSoundManager())
ambient->setVolumeScale(pendingAmbientVolume / 100.0f);
if (auto* ui = renderer->getUiSoundManager())
if (auto* ui = ac->getUiSoundManager())
ui->setVolumeScale(pendingUiVolume / 100.0f);
if (auto* combat = renderer->getCombatSoundManager())
if (auto* combat = ac->getCombatSoundManager())
combat->setVolumeScale(pendingCombatVolume / 100.0f);
if (auto* spell = renderer->getSpellSoundManager())
if (auto* spell = ac->getSpellSoundManager())
spell->setVolumeScale(pendingSpellVolume / 100.0f);
if (auto* movement = renderer->getMovementSoundManager())
if (auto* movement = ac->getMovementSoundManager())
movement->setVolumeScale(pendingMovementVolume / 100.0f);
if (auto* footstep = renderer->getFootstepManager())
if (auto* footstep = ac->getFootstepManager())
footstep->setVolumeScale(pendingFootstepVolume / 100.0f);
if (auto* npcVoice = renderer->getNpcVoiceManager())
if (auto* npcVoice = ac->getNpcVoiceManager())
npcVoice->setVolumeScale(pendingNpcVoiceVolume / 100.0f);
if (auto* mount = renderer->getMountSoundManager())
if (auto* mount = ac->getMountSoundManager())
mount->setVolumeScale(pendingMountVolume / 100.0f);
if (auto* activity = renderer->getActivitySoundManager())
if (auto* activity = ac->getActivitySoundManager())
activity->setVolumeScale(pendingActivityVolume / 100.0f);
}

View file

@ -2,6 +2,7 @@
#include "game/game_handler.hpp"
#include "core/application.hpp"
#include "rendering/renderer.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/ui_sound_manager.hpp"
#include <imgui.h>
@ -461,11 +462,13 @@ void ToastManager::triggerDing(uint32_t newLevel, uint32_t hpDelta, uint32_t man
dingStats_[3] = intel;
dingStats_[4] = spi;
auto* renderer = services_.renderer;
if (renderer) {
if (auto* sfx = renderer->getUiSoundManager()) {
auto* ac = services_.audioCoordinator;
if (ac) {
if (auto* sfx = ac->getUiSoundManager()) {
sfx->playLevelUp();
}
}
if (auto* renderer = services_.renderer) {
renderer->playEmote("cheer");
}
}
@ -550,9 +553,9 @@ void ToastManager::triggerAchievementToast(uint32_t achievementId, std::string n
achievementToastTimer_ = ACHIEVEMENT_TOAST_DURATION;
// Play a UI sound if available
auto* renderer = services_.renderer;
if (renderer) {
if (auto* sfx = renderer->getUiSoundManager()) {
auto* ac = services_.audioCoordinator;
if (ac) {
if (auto* sfx = ac->getUiSoundManager()) {
sfx->playAchievementAlert();
}
}

View file

@ -16,6 +16,7 @@
#include "game/game_handler.hpp"
#include "pipeline/asset_manager.hpp"
#include "pipeline/dbc_layout.hpp"
#include "audio/audio_coordinator.hpp"
#include "audio/ui_sound_manager.hpp"
#include "audio/music_manager.hpp"
#include <imgui.h>
@ -1701,9 +1702,9 @@ void WindowManager::renderEscapeMenu(SettingsPanel& settingsPanel) {
settingsPanel.showEscapeSettingsNotice = false;
}
if (ImGui::Button("Quit", ImVec2(-1, 0))) {
auto* renderer = services_.renderer;
if (renderer) {
if (auto* music = renderer->getMusicManager()) {
auto* ac = services_.audioCoordinator;
if (ac) {
if (auto* music = ac->getMusicManager()) {
music->stopMusic(0.0f);
}
}