diff --git a/include/audio/audio_coordinator.hpp b/include/audio/audio_coordinator.hpp index 9a8305b9..4f62bffd 100644 --- a/include/audio/audio_coordinator.hpp +++ b/include/audio/audio_coordinator.hpp @@ -51,7 +51,7 @@ public: /// Initialize the audio engine and all managers. /// @return true if audio is available (engine initialized successfully) - bool initialize(); + [[nodiscard]] bool initialize(); /// Initialize managers that need AssetManager (music lookups, sound banks). void initializeWithAssets(pipeline::AssetManager* assetManager); diff --git a/include/audio/audio_engine.hpp b/include/audio/audio_engine.hpp index c6d5e723..35f7f35c 100644 --- a/include/audio/audio_engine.hpp +++ b/include/audio/audio_engine.hpp @@ -25,7 +25,7 @@ public: ~AudioEngine(); // Initialization - bool initialize(); + [[nodiscard]] bool initialize(); void shutdown(); bool isInitialized() const { return initialized_; } diff --git a/include/auth/auth_handler.hpp b/include/auth/auth_handler.hpp index 3916d0a1..d8130a47 100644 --- a/include/auth/auth_handler.hpp +++ b/include/auth/auth_handler.hpp @@ -40,7 +40,7 @@ public: ~AuthHandler(); // Connection - bool connect(const std::string& host, uint16_t port = 3724); + [[nodiscard]] bool connect(const std::string& host, uint16_t port = 3724); void disconnect(); bool isConnected() const; diff --git a/include/auth/auth_packets.hpp b/include/auth/auth_packets.hpp index a5b91998..43982d27 100644 --- a/include/auth/auth_packets.hpp +++ b/include/auth/auth_packets.hpp @@ -53,7 +53,7 @@ struct LogonChallengeResponse { // LOGON_CHALLENGE response parser class LogonChallengeResponseParser { public: - static bool parse(network::Packet& packet, LogonChallengeResponse& response); + [[nodiscard]] static bool parse(network::Packet& packet, LogonChallengeResponse& response); }; // LOGON_PROOF packet builder @@ -92,7 +92,7 @@ struct LogonProofResponse { // LOGON_PROOF response parser class LogonProofResponseParser { public: - static bool parse(network::Packet& packet, LogonProofResponse& response); + [[nodiscard]] static bool parse(network::Packet& packet, LogonProofResponse& response); }; // Realm data structure @@ -131,7 +131,7 @@ struct RealmListResponse { class RealmListResponseParser { public: // protocolVersion: 3 = vanilla (uint8 realmCount, uint32 icon), 8 = WotLK (uint16 realmCount, uint8 icon) - static bool parse(network::Packet& packet, RealmListResponse& response, uint8_t protocolVersion = 8); + [[nodiscard]] static bool parse(network::Packet& packet, RealmListResponse& response, uint8_t protocolVersion = 8); }; } // namespace auth diff --git a/include/auth/pin_auth.hpp b/include/auth/pin_auth.hpp index 00211776..87f72dee 100644 --- a/include/auth/pin_auth.hpp +++ b/include/auth/pin_auth.hpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace wowee { @@ -19,9 +20,11 @@ struct PinProof { // - Compute: pin_hash = SHA1(client_salt || SHA1(server_salt || randomized_pin_ascii)) // // PIN must be 4-10 ASCII digits. -PinProof computePinProof(const std::string& pinDigits, - uint32_t pinGridSeed, - const std::array& serverSalt); +// Returns std::nullopt on invalid input (bad length, non-digit chars, or grid corruption). +[[nodiscard]] std::optional computePinProof( + const std::string& pinDigits, + uint32_t pinGridSeed, + const std::array& serverSalt); } // namespace auth } // namespace wowee diff --git a/include/game/protocol_constants.hpp b/include/game/protocol_constants.hpp new file mode 100644 index 00000000..cb7f97a7 --- /dev/null +++ b/include/game/protocol_constants.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include + +// WoW 3.3.5a (12340) protocol constants. +// Centralised so every handler references a single source of truth. + +namespace wowee { +namespace game { + +// --------------------------------------------------------------------------- +// Currency +// --------------------------------------------------------------------------- +constexpr uint32_t COPPER_PER_GOLD = 10000; +constexpr uint32_t COPPER_PER_SILVER = 100; + +// --------------------------------------------------------------------------- +// Unit flags (UNIT_FIELD_FLAGS — offset 46 in UnitFields) +// --------------------------------------------------------------------------- +constexpr uint32_t UNIT_FLAG_TAXI_FLIGHT = 0x00000100; + +// --------------------------------------------------------------------------- +// NPC flags (UNIT_NPC_FLAGS — offset 147 in UnitFields, 3.3.5a) +// --------------------------------------------------------------------------- +constexpr uint32_t NPC_FLAG_SPIRIT_GUIDE = 0x00004000; +constexpr uint32_t NPC_FLAG_SPIRIT_HEALER = 0x00008000; + +// --------------------------------------------------------------------------- +// Default action-bar spell IDs +// --------------------------------------------------------------------------- +constexpr uint32_t SPELL_ID_ATTACK = 6603; +constexpr uint32_t SPELL_ID_HEARTHSTONE = 8690; + +// --------------------------------------------------------------------------- +// Class IDs +// --------------------------------------------------------------------------- +constexpr uint32_t CLASS_WARRIOR = 1; +constexpr uint32_t CLASS_PALADIN = 2; +constexpr uint32_t CLASS_HUNTER = 3; +constexpr uint32_t CLASS_ROGUE = 4; +constexpr uint32_t CLASS_PRIEST = 5; +constexpr uint32_t CLASS_DK = 6; +constexpr uint32_t CLASS_SHAMAN = 7; +constexpr uint32_t CLASS_MAGE = 8; +constexpr uint32_t CLASS_WARLOCK = 9; +constexpr uint32_t CLASS_DRUID = 11; + +// --------------------------------------------------------------------------- +// Class-specific stance / form / presence spell IDs +// --------------------------------------------------------------------------- +// Warrior stances +constexpr uint32_t SPELL_BATTLE_STANCE = 2457; +constexpr uint32_t SPELL_DEFENSIVE_STANCE = 71; +constexpr uint32_t SPELL_BERSERKER_STANCE = 2458; + +// Death Knight presences +constexpr uint32_t SPELL_BLOOD_PRESENCE = 48266; +constexpr uint32_t SPELL_FROST_PRESENCE = 48263; +constexpr uint32_t SPELL_UNHOLY_PRESENCE = 48265; + +// Druid forms +constexpr uint32_t SPELL_BEAR_FORM = 5487; +constexpr uint32_t SPELL_DIRE_BEAR_FORM = 9634; +constexpr uint32_t SPELL_CAT_FORM = 768; +constexpr uint32_t SPELL_AQUATIC_FORM = 1066; +constexpr uint32_t SPELL_TRAVEL_FORM = 783; +constexpr uint32_t SPELL_MOONKIN_FORM = 24858; +constexpr uint32_t SPELL_FLIGHT_FORM = 33943; +constexpr uint32_t SPELL_SWIFT_FLIGHT = 40120; +constexpr uint32_t SPELL_TREE_OF_LIFE = 33891; + +// Rogue +constexpr uint32_t SPELL_STEALTH = 1784; + +// Priest +constexpr uint32_t SPELL_SHADOWFORM = 15473; + +// --------------------------------------------------------------------------- +// Session / network timing +// --------------------------------------------------------------------------- +constexpr uint32_t SESSION_KEY_HEX_LENGTH = 40; +constexpr uint32_t RX_SILENCE_WARNING_MS = 10000; // 10 s +constexpr uint32_t RX_SILENCE_CRITICAL_MS = 15000; // 15 s +constexpr float WARDEN_GATE_LOG_INTERVAL_SEC = 30.0f; +constexpr float CLASSIC_PING_INTERVAL_SEC = 10.0f; + +// --------------------------------------------------------------------------- +// Heartbeat / area-trigger intervals (seconds) +// --------------------------------------------------------------------------- +constexpr float HEARTBEAT_INTERVAL_TAXI = 0.25f; +constexpr float HEARTBEAT_INTERVAL_STATIONARY_COMBAT = 0.75f; +constexpr float HEARTBEAT_INTERVAL_MOVING_COMBAT = 0.20f; +constexpr float AREA_TRIGGER_CHECK_INTERVAL = 0.25f; + +// --------------------------------------------------------------------------- +// Gameplay distance thresholds +// --------------------------------------------------------------------------- +constexpr float ENTITY_UPDATE_RADIUS = 150.0f; +constexpr float NPC_INTERACT_MAX_DISTANCE = 15.0f; + +// --------------------------------------------------------------------------- +// Skill categories (from SkillLine DBC) +// --------------------------------------------------------------------------- +constexpr uint32_t SKILL_CATEGORY_PROFESSION = 11; +constexpr uint32_t SKILL_CATEGORY_SECONDARY = 9; + +// --------------------------------------------------------------------------- +// DBC field-index sentinel (field lookup failure) +// --------------------------------------------------------------------------- +constexpr uint32_t DBC_FIELD_INVALID = 0xFFFFFFFF; + +// --------------------------------------------------------------------------- +// Appearance byte packing +// --------------------------------------------------------------------------- +constexpr uint32_t APPEARANCE_SKIN_MASK = 0xFF; +constexpr uint32_t APPEARANCE_FACE_SHIFT = 8; +constexpr uint32_t APPEARANCE_HAIRSTYLE_SHIFT = 16; +constexpr uint32_t APPEARANCE_HAIRCOLOR_SHIFT = 24; + +// --------------------------------------------------------------------------- +// Critter detection +// --------------------------------------------------------------------------- +constexpr uint32_t CRITTER_MAX_HEALTH_THRESHOLD = 100; + +} // namespace game +} // namespace wowee diff --git a/include/game/spell_handler.hpp b/include/game/spell_handler.hpp index fe6f223c..6b1100ff 100644 --- a/include/game/spell_handler.hpp +++ b/include/game/spell_handler.hpp @@ -4,6 +4,7 @@ #include "game/opcode_table.hpp" #include "game/spell_defines.hpp" #include "game/handler_types.hpp" +#include "audio/spell_sound_manager.hpp" #include "network/packet.hpp" #include #include @@ -273,6 +274,30 @@ private: void handleChannelUpdate(network::Packet& packet); // --- Internal helpers --- + + // Resolve the magic school for a spell (for audio playback). + // Returns MagicSchool from the spell name cache, defaulting to ARCANE. + audio::SpellSoundManager::MagicSchool resolveSpellSchool(uint32_t spellId); + + // Play a spell cast or impact sound via audioCoordinator, if available. + void playSpellCastSound(uint32_t spellId); + void playSpellImpactSound(uint32_t spellId); + + // --- handleSpellLogExecute per-effect parsers (extracted to reduce nesting) --- + void parseEffectPowerDrain(network::Packet& packet, uint32_t effectLogCount, + uint64_t caster, uint32_t spellId, bool isPlayerCaster, + bool usesFullGuid); + void parseEffectHealthLeech(network::Packet& packet, uint32_t effectLogCount, + uint64_t caster, uint32_t spellId, bool isPlayerCaster, + bool usesFullGuid); + void parseEffectCreateItem(network::Packet& packet, uint32_t effectLogCount, + uint64_t caster, uint32_t spellId, bool isPlayerCaster); + void parseEffectInterruptCast(network::Packet& packet, uint32_t effectLogCount, + uint64_t caster, uint32_t spellId, bool isPlayerCaster, + bool usesFullGuid); + void parseEffectFeedPet(network::Packet& packet, uint32_t effectLogCount, + uint64_t caster, uint32_t spellId, bool isPlayerCaster); + // Find the on-use spell for an item (trigger=0 Use or trigger=5 NoDelay). // CMSG_USE_ITEM requires a valid spellId or the server silently ignores it. uint32_t findOnUseSpellId(uint32_t itemId) const; diff --git a/include/game/warden_constants.hpp b/include/game/warden_constants.hpp new file mode 100644 index 00000000..a047969c --- /dev/null +++ b/include/game/warden_constants.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include + +// Warden anti-cheat protocol constants for WoW 3.3.5a (12340). +// Server-to-Client (SMSG) and Client-to-Server (CMSG) sub-opcodes, +// memory region boundaries, check sizes, and result codes. + +namespace wowee { +namespace game { + +// --------------------------------------------------------------------------- +// Warden sub-opcodes (inside SMSG_WARDEN_DATA / CMSG_WARDEN_DATA) +// --------------------------------------------------------------------------- +// Server → Client +constexpr uint8_t WARDEN_SMSG_MODULE_USE = 0x00; +constexpr uint8_t WARDEN_SMSG_MODULE_CACHE = 0x01; +constexpr uint8_t WARDEN_SMSG_CHEAT_CHECKS_REQUEST = 0x02; +constexpr uint8_t WARDEN_SMSG_MODULE_INITIALIZE = 0x03; +constexpr uint8_t WARDEN_SMSG_HASH_REQUEST = 0x05; + +// Client → Server +constexpr uint8_t WARDEN_CMSG_MODULE_MISSING = 0x00; +constexpr uint8_t WARDEN_CMSG_MODULE_OK = 0x01; +constexpr uint8_t WARDEN_CMSG_CHEAT_CHECKS_RESULT = 0x02; +constexpr uint8_t WARDEN_CMSG_HASH_RESULT = 0x04; + +// --------------------------------------------------------------------------- +// PE section boundaries (Wow.exe 3.3.5a 12340, default base 0x400000) +// --------------------------------------------------------------------------- +constexpr uint32_t PE_TEXT_SECTION_BASE = 0x400000; +constexpr uint32_t PE_TEXT_SECTION_END = 0x800000; +constexpr uint32_t PE_RDATA_SECTION_BASE = 0x7FF000; +constexpr uint32_t PE_DATA_RAW_SECTION_BASE = 0x827000; +constexpr uint32_t PE_BSS_SECTION_BASE = 0x883000; +constexpr uint32_t PE_BSS_SECTION_END = 0xD06000; + +// Windows KUSER_SHARED_DATA page (read-only, always mapped) +constexpr uint32_t KUSER_SHARED_DATA_BASE = 0x7FFE0000; +constexpr uint32_t KUSER_SHARED_DATA_END = 0x7FFF0000; + +// --------------------------------------------------------------------------- +// Well-known memory addresses +// --------------------------------------------------------------------------- +constexpr uint32_t WARDEN_TICKCOUNT_ADDRESS = 0x00CF0BC8; +constexpr uint32_t WARDEN_WIN_VERSION_ADDRESS = 0x7FFE026C; + +// --------------------------------------------------------------------------- +// Check sizes (bytes) +// --------------------------------------------------------------------------- +constexpr uint32_t WARDEN_CR_HEADER_SIZE = 17; +constexpr uint32_t WARDEN_CR_ENTRY_SIZE = 68; +constexpr uint32_t WARDEN_PAGE_CHECK_SIZE = 29; +constexpr uint32_t WARDEN_PAGE_A_SHORT_SIZE = 24; +constexpr uint32_t WARDEN_KNOWN_CODE_SCAN_OFFSET = 13856; + +// --------------------------------------------------------------------------- +// Memory-check result codes +// --------------------------------------------------------------------------- +constexpr uint8_t WARDEN_MEM_CHECK_SUCCESS = 0x00; +constexpr uint8_t WARDEN_MEM_CHECK_UNMAPPED = 0xE9; +constexpr uint8_t WARDEN_PAGE_CHECK_FOUND = 0x4A; + +} // namespace game +} // namespace wowee diff --git a/include/rendering/character_renderer.hpp b/include/rendering/character_renderer.hpp index 025f9924..44655721 100644 --- a/include/rendering/character_renderer.hpp +++ b/include/rendering/character_renderer.hpp @@ -49,7 +49,7 @@ public: CharacterRenderer(); ~CharacterRenderer(); - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am, + [[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* am, VkRenderPass renderPassOverride = VK_NULL_HANDLE, VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT); void shutdown(); @@ -71,7 +71,7 @@ public: void prepareRender(uint32_t frameIndex); void render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const Camera& camera); void recreatePipelines(); - bool initializeShadow(VkRenderPass shadowRenderPass); + [[nodiscard]] bool initializeShadow(VkRenderPass shadowRenderPass); void renderShadow(VkCommandBuffer cmd, const glm::mat4& lightSpaceMatrix, const glm::vec3& shadowCenter = glm::vec3(0), float shadowRadius = 1e9f); diff --git a/include/rendering/charge_effect.hpp b/include/rendering/charge_effect.hpp index 4154df4d..5e031628 100644 --- a/include/rendering/charge_effect.hpp +++ b/include/rendering/charge_effect.hpp @@ -22,7 +22,7 @@ public: ChargeEffect(); ~ChargeEffect(); - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); + [[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); void recreatePipelines(); diff --git a/include/rendering/lightning.hpp b/include/rendering/lightning.hpp index 33152192..32235214 100644 --- a/include/rendering/lightning.hpp +++ b/include/rendering/lightning.hpp @@ -26,7 +26,7 @@ public: Lightning(); ~Lightning(); - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); + [[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); void recreatePipelines(); diff --git a/include/rendering/m2_renderer.hpp b/include/rendering/m2_renderer.hpp index 0c381d7e..5db00014 100644 --- a/include/rendering/m2_renderer.hpp +++ b/include/rendering/m2_renderer.hpp @@ -276,7 +276,7 @@ public: M2Renderer(); ~M2Renderer(); - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, + [[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* assets); void shutdown(); @@ -310,7 +310,7 @@ public: /** * Initialize shadow pipeline (Phase 7) */ - bool initializeShadow(VkRenderPass shadowRenderPass); + [[nodiscard]] bool initializeShadow(VkRenderPass shadowRenderPass); bool hasShadowPipeline() const { return shadowPipeline_ != VK_NULL_HANDLE; } /** diff --git a/include/rendering/mount_dust.hpp b/include/rendering/mount_dust.hpp index de0cce60..f7c1a7b6 100644 --- a/include/rendering/mount_dust.hpp +++ b/include/rendering/mount_dust.hpp @@ -16,7 +16,7 @@ public: MountDust(); ~MountDust(); - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); + [[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); void recreatePipelines(); diff --git a/include/rendering/quest_marker_renderer.hpp b/include/rendering/quest_marker_renderer.hpp index a0d18776..0b8b3a56 100644 --- a/include/rendering/quest_marker_renderer.hpp +++ b/include/rendering/quest_marker_renderer.hpp @@ -25,7 +25,7 @@ public: QuestMarkerRenderer(); ~QuestMarkerRenderer(); - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* assetManager); + [[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* assetManager); void shutdown(); void recreatePipelines(); diff --git a/include/rendering/render_constants.hpp b/include/rendering/render_constants.hpp new file mode 100644 index 00000000..90346b30 --- /dev/null +++ b/include/rendering/render_constants.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include + +// Rendering-domain constants: distances, LOD thresholds, particle tuning. + +namespace wowee { +namespace rendering { + +// --------------------------------------------------------------------------- +// M2 instance-count → render-distance mapping +// --------------------------------------------------------------------------- +constexpr uint32_t M2_HIGH_DENSITY_INSTANCE_THRESHOLD = 2000; +constexpr float M2_MAX_RENDER_DISTANCE_HIGH_DENSITY = 800.0f; +constexpr float M2_MAX_RENDER_DISTANCE_LOW_DENSITY = 2800.0f; + +// --------------------------------------------------------------------------- +// M2 LOD / bone-update distance thresholds (world units) +// --------------------------------------------------------------------------- +constexpr float M2_LOD3_DISTANCE = 150.0f; // Beyond this: no bone updates +constexpr float M2_BONE_SKIP_DIST_FAR = 100.0f; // Beyond this: every 4th frame +constexpr float M2_BONE_SKIP_DIST_MID = 50.0f; // Beyond this: every 2nd frame + +// --------------------------------------------------------------------------- +// M2 culling geometry +// --------------------------------------------------------------------------- +constexpr float M2_CULL_RADIUS_SCALE_DIVISOR = 12.0f; +constexpr float M2_PADDED_RADIUS_SCALE = 1.5f; +constexpr float M2_PADDED_RADIUS_MIN_MARGIN = 3.0f; + +// --------------------------------------------------------------------------- +// M2 variation / idle animation timing (milliseconds) +// --------------------------------------------------------------------------- +constexpr float M2_VARIATION_TIMER_MIN_MS = 3000.0f; +constexpr float M2_VARIATION_TIMER_MAX_MS = 11000.0f; +constexpr float M2_LOOP_VARIATION_TIMER_MIN_MS = 4000.0f; +constexpr float M2_LOOP_VARIATION_TIMER_MAX_MS = 10000.0f; +constexpr float M2_IDLE_VARIATION_TIMER_MIN_MS = 2000.0f; +constexpr float M2_IDLE_VARIATION_TIMER_MAX_MS = 6000.0f; +constexpr float M2_DEFAULT_PARTICLE_ANIM_MS = 3333.0f; + +// --------------------------------------------------------------------------- +// HiZ occlusion culling +// --------------------------------------------------------------------------- +// VP matrix diff threshold — below this HiZ is considered safe. +// Typical tracking camera (following a walking character) produces 0.05–0.25. +constexpr float HIZ_VP_DIFF_THRESHOLD = 0.5f; + +// --------------------------------------------------------------------------- +// Smoke / spark particle tuning +// --------------------------------------------------------------------------- +constexpr float SMOKE_OFFSET_XY_MIN = -0.4f; +constexpr float SMOKE_OFFSET_XY_MAX = 0.4f; +constexpr float SMOKE_VEL_Z_MIN = 3.0f; +constexpr float SMOKE_VEL_Z_MAX = 5.0f; +constexpr float SMOKE_LIFETIME_MIN = 4.0f; +constexpr float SMOKE_LIFETIME_MAX = 7.0f; +constexpr float SMOKE_Z_VEL_DAMPING = 0.98f; +constexpr float SMOKE_SIZE_START = 1.0f; +constexpr float SMOKE_SIZE_GROWTH = 2.5f; + +constexpr int SPARK_PROBABILITY_DENOM = 8; // 1-in-8 chance per frame +constexpr float SPARK_LIFE_BASE = 0.8f; +constexpr float SPARK_LIFE_RANGE = 1.2f; + +// --------------------------------------------------------------------------- +// Character rendering +// --------------------------------------------------------------------------- +// Default frustum-cull radius when model bounds are unavailable (world units). +// 4.0 covers Tauren, mounted characters, and most creature models. +constexpr float DEFAULT_CHARACTER_CULL_RADIUS = 4.0f; + +} // namespace rendering +} // namespace wowee diff --git a/include/rendering/swim_effects.hpp b/include/rendering/swim_effects.hpp index 20d63176..793cc88c 100644 --- a/include/rendering/swim_effects.hpp +++ b/include/rendering/swim_effects.hpp @@ -19,7 +19,7 @@ public: SwimEffects(); ~SwimEffects(); - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); + [[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void shutdown(); void recreatePipelines(); void update(const Camera& camera, const CameraController& cc, diff --git a/include/rendering/weather.hpp b/include/rendering/weather.hpp index 3349526f..178a66d8 100644 --- a/include/rendering/weather.hpp +++ b/include/rendering/weather.hpp @@ -41,7 +41,7 @@ public: * @param perFrameLayout Descriptor set layout for the per-frame UBO (set 0) * @return true if initialization succeeded */ - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); + [[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout); void recreatePipelines(); /** diff --git a/include/rendering/wmo_renderer.hpp b/include/rendering/wmo_renderer.hpp index 33eae7da..b00fe423 100644 --- a/include/rendering/wmo_renderer.hpp +++ b/include/rendering/wmo_renderer.hpp @@ -49,7 +49,7 @@ public: * @param perFrameLayout Descriptor set layout for set 0 (per-frame UBO) * @param assetManager Asset manager for loading textures (optional) */ - bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, + [[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout, pipeline::AssetManager* assetManager = nullptr); /** @@ -163,7 +163,7 @@ public: /** * Initialize shadow pipeline (Phase 7) */ - bool initializeShadow(VkRenderPass shadowRenderPass); + [[nodiscard]] bool initializeShadow(VkRenderPass shadowRenderPass); /** * Render depth-only for shadow casting diff --git a/include/ui/ui_constants.hpp b/include/ui/ui_constants.hpp new file mode 100644 index 00000000..44474db7 --- /dev/null +++ b/include/ui/ui_constants.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include + +// UI-layer constants: colors, layout sizes, effect timing. + +namespace wowee { +namespace ui { + +// --------------------------------------------------------------------------- +// Target selection-circle difficulty colours (WoW-canonical) +// --------------------------------------------------------------------------- +// Stored as {R, G, B} in [0,1]. Alpha is set by the caller. +struct Colour3f { float r, g, b; }; + +constexpr Colour3f SEL_COLOR_DEFAULT_YELLOW = {1.0f, 1.0f, 0.3f}; +constexpr Colour3f SEL_COLOR_RED = {1.0f, 0.1f, 0.1f}; +constexpr Colour3f SEL_COLOR_ORANGE = {1.0f, 0.5f, 0.1f}; +constexpr Colour3f SEL_COLOR_YELLOW = {1.0f, 1.0f, 0.1f}; +constexpr Colour3f SEL_COLOR_GREEN = {0.3f, 1.0f, 0.3f}; +constexpr Colour3f SEL_COLOR_GREY = {0.6f, 0.6f, 0.6f}; +constexpr Colour3f SEL_COLOR_DEAD = {0.5f, 0.5f, 0.5f}; + +// Level-diff thresholds that select colours above. +constexpr int MOB_LEVEL_DIFF_RED = 10; // ≥ 10 levels above player → red +constexpr int MOB_LEVEL_DIFF_ORANGE = 5; // ≥ 5 → orange +constexpr int MOB_LEVEL_DIFF_YELLOW_FLOOR = -2; // ≥ -2 → yellow, else green + +// Selection circle world-unit bounds +constexpr float SEL_CIRCLE_MIN_RADIUS = 0.8f; +constexpr float SEL_CIRCLE_MAX_RADIUS = 8.0f; + +// --------------------------------------------------------------------------- +// Damage flash / vignette effect +// --------------------------------------------------------------------------- +constexpr float DAMAGE_FLASH_FADE_SPEED = 2.0f; // alpha units/sec +constexpr float DAMAGE_FLASH_ALPHA_SCALE = 100.0f; // multiplier +constexpr uint8_t DAMAGE_FLASH_RED_CHANNEL = 200; // IM_COL32 R channel +constexpr float DAMAGE_VIGNETTE_THICKNESS = 0.12f; // fraction of screen + +// --------------------------------------------------------------------------- +// Low-health pulsing vignette +// --------------------------------------------------------------------------- +constexpr float LOW_HEALTH_THRESHOLD_PCT = 0.20f; // start at 20% HP +constexpr float LOW_HEALTH_PULSE_FREQUENCY = 9.4f; // angular speed (~1.5 Hz) +constexpr float LOW_HEALTH_MAX_ALPHA = 90.0f; +constexpr float LOW_HEALTH_VIGNETTE_THICKNESS = 0.15f; + +// --------------------------------------------------------------------------- +// Level-up flash overlay +// --------------------------------------------------------------------------- +constexpr float LEVELUP_FLASH_FADE_SPEED = 1.0f; // alpha units/sec +constexpr float LEVELUP_FLASH_ALPHA_SCALE = 160.0f; +constexpr float LEVELUP_VIGNETTE_THICKNESS = 0.18f; +constexpr float LEVELUP_TEXT_FONT_SIZE = 28.0f; + +// --------------------------------------------------------------------------- +// Ghost / death state +// --------------------------------------------------------------------------- +constexpr float GHOST_OPACITY = 0.5f; + +// --------------------------------------------------------------------------- +// Click / interaction thresholds +// --------------------------------------------------------------------------- +constexpr float CLICK_THRESHOLD_PX = 5.0f; + +} // namespace ui +} // namespace wowee diff --git a/src/auth/auth_handler.cpp b/src/auth/auth_handler.cpp index 4b466a5a..0a16698d 100644 --- a/src/auth/auth_handler.cpp +++ b/src/auth/auth_handler.cpp @@ -265,16 +265,15 @@ void AuthHandler::sendLogonProof() { const std::array* crcHashPtr = nullptr; if (securityFlags_ & kSecurityFlagPin) { - try { - PinProof proof = computePinProof(pendingSecurityCode_, pinGridSeed_, pinServerSalt_); - pinClientSalt = proof.clientSalt; - pinHash = proof.hash; - pinClientSaltPtr = &pinClientSalt; - pinHashPtr = &pinHash; - } catch (const std::exception& e) { - fail(std::string("PIN required but invalid: ") + e.what()); + auto proof = computePinProof(pendingSecurityCode_, pinGridSeed_, pinServerSalt_); + if (!proof) { + fail("PIN required but invalid input"); return; } + pinClientSalt = proof->clientSalt; + pinHash = proof->hash; + pinClientSaltPtr = &pinClientSalt; + pinHashPtr = &pinHash; } // Legacy client integrity hash (aka "CRC hash"). Some servers enforce this for classic builds. diff --git a/src/auth/pin_auth.cpp b/src/auth/pin_auth.cpp index 0ec47857..4b77989a 100644 --- a/src/auth/pin_auth.cpp +++ b/src/auth/pin_auth.cpp @@ -1,8 +1,9 @@ #include "auth/pin_auth.hpp" #include "auth/crypto.hpp" +#include "core/logger.hpp" #include +#include #include -#include #include namespace wowee { @@ -46,7 +47,7 @@ static std::array remapPinGrid(uint32_t seed) { return remapped; } -static std::vector randomizePinDigits(const std::string& pinDigits, +static std::optional> randomizePinDigits(const std::string& pinDigits, const std::array& remapped) { // Transforms each pin digit into an index in the remapped permutation. // Based on: @@ -61,7 +62,8 @@ static std::vector randomizePinDigits(const std::string& pinDigits, if (remapped[j] == d) { idx = j; break; } } if (idx == 0xFF) { - throw std::runtime_error("PIN digit not found in remapped grid"); + LOG_ERROR("PIN digit not found in remapped grid"); + return std::nullopt; } // PIN grid encodes each digit as its ASCII character ('0'..'9') for the // server-side HMAC computation — this matches Blizzard's auth protocol. @@ -71,25 +73,28 @@ static std::vector randomizePinDigits(const std::string& pinDigits, return out; } -PinProof computePinProof(const std::string& pinDigits, +std::optional computePinProof(const std::string& pinDigits, uint32_t pinGridSeed, const std::array& serverSalt) { if (pinDigits.size() < 4 || pinDigits.size() > 10) { - throw std::runtime_error("PIN must be 4-10 digits"); + LOG_ERROR("PIN must be 4-10 digits, got ", pinDigits.size()); + return std::nullopt; } if (!std::all_of(pinDigits.begin(), pinDigits.end(), [](unsigned char c) { return c >= '0' && c <= '9'; })) { - throw std::runtime_error("PIN must contain only digits"); + LOG_ERROR("PIN must contain only digits"); + return std::nullopt; } const auto remapped = remapPinGrid(pinGridSeed); const auto randomizedAsciiDigits = randomizePinDigits(pinDigits, remapped); + if (!randomizedAsciiDigits) return std::nullopt; // server_hash = SHA1(server_salt || randomized_pin_ascii) std::vector serverHashInput; - serverHashInput.reserve(serverSalt.size() + randomizedAsciiDigits.size()); + serverHashInput.reserve(serverSalt.size() + randomizedAsciiDigits->size()); serverHashInput.insert(serverHashInput.end(), serverSalt.begin(), serverSalt.end()); - serverHashInput.insert(serverHashInput.end(), randomizedAsciiDigits.begin(), randomizedAsciiDigits.end()); + serverHashInput.insert(serverHashInput.end(), randomizedAsciiDigits->begin(), randomizedAsciiDigits->end()); const auto serverHash = Crypto::sha1(serverHashInput); // 20 bytes PinProof proof; diff --git a/src/core/application.cpp b/src/core/application.cpp index 9b8fd3b4..f62aabc9 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -137,7 +137,8 @@ bool Application::initialize() { // Create and initialize audio coordinator (owns all audio managers) audioCoordinator_ = std::make_unique(); - audioCoordinator_->initialize(); + if (!audioCoordinator_->initialize()) + LOG_WARNING("Audio coordinator initialization failed — game will run without audio"); renderer->setAudioCoordinator(audioCoordinator_.get()); // Create UI manager @@ -2549,7 +2550,8 @@ void Application::loadQuestMarkerModels() { if (auto* vkCtx = renderer->getVkContext()) { VkDescriptorSetLayout pfl = renderer->getPerFrameSetLayout(); if (pfl != VK_NULL_HANDLE) { - qmr->initialize(vkCtx, pfl, assetManager.get()); + if (!qmr->initialize(vkCtx, pfl, assetManager.get())) + LOG_WARNING("Quest marker renderer re-init failed (non-fatal)"); } } } diff --git a/src/game/chat_handler.cpp b/src/game/chat_handler.cpp index 0d9174d3..8f8cb9ae 100644 --- a/src/game/chat_handler.cpp +++ b/src/game/chat_handler.cpp @@ -378,7 +378,7 @@ void ChatHandler::sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid) { } void ChatHandler::handleTextEmote(network::Packet& packet) { - const bool legacyFormat = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool legacyFormat = isPreWotlk(); TextEmoteData data; if (!TextEmoteParser::parse(packet, data, legacyFormat)) { LOG_WARNING("Failed to parse SMSG_TEXT_EMOTE"); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 6bc55a20..3d38b20b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -32,6 +32,7 @@ #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" #include "core/logger.hpp" +#include "game/protocol_constants.hpp" #include "rendering/animation/animation_ids.hpp" #include #include @@ -79,9 +80,9 @@ const char* worldStateName(WorldState state) { } // end anonymous namespace std::string formatCopperAmount(uint32_t amount) { - uint32_t gold = amount / 10000; - uint32_t silver = (amount / 100) % 100; - uint32_t copper = amount % 100; + uint32_t gold = amount / game::COPPER_PER_GOLD; + uint32_t silver = (amount / game::COPPER_PER_SILVER) % 100; + uint32_t copper = amount % game::COPPER_PER_SILVER; std::ostringstream oss; bool wrote = false; @@ -150,9 +151,9 @@ GameHandler::GameHandler(GameServices& services) // Default action bar layout actionBar[0].type = ActionBarSlot::SPELL; - actionBar[0].id = 6603; // Attack in slot 1 + actionBar[0].id = game::SPELL_ID_ATTACK; // Attack in slot 1 actionBar[11].type = ActionBarSlot::SPELL; - actionBar[11].id = 8690; // Hearthstone in slot 12 + actionBar[11].id = game::SPELL_ID_HEARTHSTONE; // Hearthstone in slot 12 // Build the opcode dispatch table (replaces switch(*logicalOp) in handlePacket) registerOpcodeHandlers(); @@ -365,11 +366,11 @@ void GameHandler::updateNetworking(float deltaTime) { lastRxTime_.time_since_epoch().count() > 0) { auto silenceMs = std::chrono::duration_cast( std::chrono::steady_clock::now() - lastRxTime_).count(); - if (silenceMs > 10000 && !rxSilenceLogged_) { + if (silenceMs > game::RX_SILENCE_WARNING_MS && !rxSilenceLogged_) { rxSilenceLogged_ = true; LOG_WARNING("RX SILENCE: No packets from server for ", silenceMs, "ms — possible soft disconnect"); } - if (silenceMs > 15000 && !rxSilence15sLogged_) { + if (silenceMs > game::RX_SILENCE_CRITICAL_MS && !rxSilence15sLogged_) { rxSilence15sLogged_ = true; LOG_WARNING("RX SILENCE: 15s — server appears to have stopped sending"); } @@ -393,7 +394,7 @@ void GameHandler::updateNetworking(float deltaTime) { LOG_DEBUG("Warden gate status: elapsed=", wardenGateElapsed_, "s connected=", socket->isConnected() ? "yes" : "no", " packetsAfterGate=", wardenPacketsAfterGate_); - wardenGateNextStatusLog_ += 30.0f; + wardenGateNextStatusLog_ += game::WARDEN_GATE_LOG_INTERVAL_SEC; } } } @@ -420,7 +421,7 @@ if (onTaxiFlight_) { auto playerEntity = entityController_->getEntityManager().getEntity(playerGuid); auto unit = std::dynamic_pointer_cast(playerEntity); if (unit && - (unit->getUnitFlags() & 0x00000100) == 0 && + (unit->getUnitFlags() & game::UNIT_FLAG_TAXI_FLIGHT) == 0 && !taxiClientActive_ && !taxiActivatePending_ && taxiStartGrace_ <= 0.0f) { @@ -452,7 +453,7 @@ if (!onTaxiFlight_ && taxiMountActive_) { auto playerEntity = entityController_->getEntityManager().getEntity(playerGuid); auto playerUnit = std::dynamic_pointer_cast(playerEntity); if (playerUnit) { - serverStillTaxi = (playerUnit->getUnitFlags() & 0x00000100) != 0; + serverStillTaxi = (playerUnit->getUnitFlags() & game::UNIT_FLAG_TAXI_FLIGHT) != 0; } if (taxiStartGrace_ > 0.0f || serverStillTaxi || taxiClientActive_ || taxiActivatePending_) { @@ -541,7 +542,7 @@ void GameHandler::updateAutoAttack(float deltaTime) { void GameHandler::updateEntityInterpolation(float deltaTime) { // Update entity movement interpolation (keeps targeting in sync with visuals) // Only update entities within reasonable distance for performance -const float updateRadiusSq = 150.0f * 150.0f; // 150 unit radius +const float updateRadiusSq = game::ENTITY_UPDATE_RADIUS * game::ENTITY_UPDATE_RADIUS; // 150 unit radius auto playerEntity = entityController_->getEntityManager().getEntity(playerGuid); glm::vec3 playerPos = playerEntity ? glm::vec3(playerEntity->getX(), playerEntity->getY(), playerEntity->getZ()) : glm::vec3(0.0f); @@ -788,7 +789,7 @@ void GameHandler::update(float deltaTime) { if (movementHandler_) movementHandler_->timeSinceLastMoveHeartbeatRef() += deltaTime; const float currentPingInterval = - (isPreWotlk()) ? 10.0f : pingInterval; + (isPreWotlk()) ? game::CLASSIC_PING_INTERVAL_SEC : pingInterval; if (timeSinceLastPing >= currentPingInterval) { if (socket) { sendPing(); @@ -819,9 +820,9 @@ void GameHandler::update(float deltaTime) { !taxiClientActive_ && (movementInfo.flags & locomotionFlags) == 0; float heartbeatInterval = (onTaxiFlight_ || taxiActivatePending_ || taxiClientActive_) - ? 0.25f - : (classicLikeStationaryCombatSync ? 0.75f - : (classicLikeCombatSync ? 0.20f + ? game::HEARTBEAT_INTERVAL_TAXI + : (classicLikeStationaryCombatSync ? game::HEARTBEAT_INTERVAL_STATIONARY_COMBAT + : (classicLikeCombatSync ? game::HEARTBEAT_INTERVAL_MOVING_COMBAT : moveHeartbeatInterval_)); if (movementHandler_ && movementHandler_->timeSinceLastMoveHeartbeatRef() >= heartbeatInterval) { sendMovement(Opcode::MSG_MOVE_HEARTBEAT); @@ -830,7 +831,7 @@ void GameHandler::update(float deltaTime) { // Check area triggers (instance portals, tavern rests, etc.) areaTriggerCheckTimer_ += deltaTime; - if (areaTriggerCheckTimer_ >= 0.25f) { + if (areaTriggerCheckTimer_ >= game::AREA_TRIGGER_CHECK_INTERVAL) { areaTriggerCheckTimer_ = 0.0f; checkAreaTriggers(); } @@ -894,7 +895,7 @@ void GameHandler::update(float deltaTime) { if (!npc) return; float dx = movementInfo.x - npc->getX(); float dy = movementInfo.y - npc->getY(); - if (std::sqrt(dx * dx + dy * dy) > 15.0f) { + if (std::sqrt(dx * dx + dy * dy) > game::NPC_INTERACT_MAX_DISTANCE) { closeFn(); LOG_INFO(label, " closed: walked too far from NPC"); } @@ -918,7 +919,7 @@ void GameHandler::update(float deltaTime) { // ============================================================ // WotLK 3.3.5a XP-to-next-level table (from player_xp_for_level) -static const uint32_t XP_TABLE[] = { +static constexpr uint32_t XP_TABLE[] = { 0, // level 0 (unused) 400, 900, 1400, 2100, 2800, 3600, 4500, 5400, 6500, 7600, // 1-10 8700, 9800, 11000, 12300, 13600, 15000, 16400, 17800, 19300, 20800, // 11-20 diff --git a/src/game/inventory_handler.cpp b/src/game/inventory_handler.cpp index dba9ceac..715c37b7 100644 --- a/src/game/inventory_handler.cpp +++ b/src/game/inventory_handler.cpp @@ -308,33 +308,37 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { }; table[Opcode::SMSG_INVENTORY_CHANGE_FAILURE] = [this](network::Packet& packet) { - if ((packet.getRemainingSize()) >= 1) { - uint8_t error = packet.readUInt8(); - if (error != 0) { - LOG_WARNING("SMSG_INVENTORY_CHANGE_FAILURE: error=", (int)error); - uint32_t requiredLevel = 0; - if (packet.hasRemaining(17)) { - packet.readUInt64(); - packet.readUInt64(); - packet.readUInt8(); - if (error == 1 && packet.hasRemaining(4)) - requiredLevel = packet.readUInt32(); - } - const char* errMsg = nullptr; - char levelBuf[64]; - switch (error) { - case 1: - if (requiredLevel > 0) { - std::snprintf(levelBuf, sizeof(levelBuf), - "You must reach level %u to use that item.", requiredLevel); - owner_.addUIError(levelBuf); - owner_.addSystemChatMessage(levelBuf); - } else { - owner_.addUIError("You must reach a higher level to use that item."); - owner_.addSystemChatMessage("You must reach a higher level to use that item."); - } - return; - case 2: errMsg = "You don't have the required skill."; break; + if (packet.getRemainingSize() < 1) return; + uint8_t error = packet.readUInt8(); + if (error == 0) return; + + LOG_WARNING("SMSG_INVENTORY_CHANGE_FAILURE: error=", (int)error); + uint32_t requiredLevel = 0; + if (packet.hasRemaining(17)) { + packet.readUInt64(); + packet.readUInt64(); + packet.readUInt8(); + if (error == 1 && packet.hasRemaining(4)) + requiredLevel = packet.readUInt32(); + } + + // Level requirement has its own formatting + if (error == 1) { + char levelBuf[64]; + if (requiredLevel > 0) { + std::snprintf(levelBuf, sizeof(levelBuf), + "You must reach level %u to use that item.", requiredLevel); + } else { + std::snprintf(levelBuf, sizeof(levelBuf), + "You must reach a higher level to use that item."); + } + owner_.addUIError(levelBuf); + owner_.addSystemChatMessage(levelBuf); + return; + } + + const char* errMsg = nullptr; + switch (error) { case 3: errMsg = "That item doesn't go in that slot."; break; case 4: errMsg = "That bag is full."; break; case 5: errMsg = "Can't put bags in bags."; break; @@ -392,14 +396,12 @@ void InventoryHandler::registerOpcodes(DispatchTable& table) { case 88: errMsg = "Requires the right talent."; break; default: break; } - std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ")."; - owner_.addUIError(msg); - owner_.addSystemChatMessage(msg); - if (auto* ac = owner_.services().audioCoordinator) { - if (auto* sfx = ac->getUiSoundManager()) - sfx->playError(); - } - } + std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ")."; + owner_.addUIError(msg); + owner_.addSystemChatMessage(msg); + if (auto* ac = owner_.services().audioCoordinator) { + if (auto* sfx = ac->getUiSoundManager()) + sfx->playError(); } }; diff --git a/src/game/movement_handler.cpp b/src/game/movement_handler.cpp index 09c7e9b8..d190389e 100644 --- a/src/game/movement_handler.cpp +++ b/src/game/movement_handler.cpp @@ -354,7 +354,7 @@ void MovementHandler::sendMovement(Opcode opcode) { movementInfo.time = movementTime; if (opcode == Opcode::MSG_MOVE_SET_FACING && - (isClassicLikeExpansion() || isActiveExpansion("tbc"))) { + isPreWotlk()) { const float facingDelta = core::coords::normalizeAngleRad( movementInfo.orientation - lastFacingSentOrientation_); const uint32_t sinceLastFacingMs = @@ -833,7 +833,7 @@ network::Packet MovementHandler::buildForceAck(Opcode ackOpcode, uint32_t counte void MovementHandler::handleForceSpeedChange(network::Packet& packet, const char* name, Opcode ackOpcode, float* speedStorage) { - const bool fscTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool fscTbcLike = isPreWotlk(); uint64_t guid = fscTbcLike ? packet.readUInt64() : packet.readPackedGuid(); uint32_t counter = packet.readUInt32(); @@ -881,7 +881,7 @@ void MovementHandler::handleForceRunSpeedChange(network::Packet& packet) { } void MovementHandler::handleForceMoveRootState(network::Packet& packet, bool rooted) { - const bool rootTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool rootTbc = isPreWotlk(); if (packet.getRemainingSize() < (rootTbc ? 8u : 2u)) return; uint64_t guid = rootTbc ? packet.readUInt64() : packet.readPackedGuid(); @@ -907,7 +907,7 @@ void MovementHandler::handleForceMoveRootState(network::Packet& packet, bool roo void MovementHandler::handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set) { - const bool fmfTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool fmfTbcLike = isPreWotlk(); if (packet.getRemainingSize() < (fmfTbcLike ? 8u : 2u)) return; uint64_t guid = fmfTbcLike ? packet.readUInt64() : packet.readPackedGuid(); @@ -932,7 +932,7 @@ void MovementHandler::handleForceMoveFlagChange(network::Packet& packet, const c } void MovementHandler::handleMoveSetCollisionHeight(network::Packet& packet) { - const bool legacyGuid = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool legacyGuid = isPreWotlk(); if (packet.getRemainingSize() < (legacyGuid ? 8u : 2u)) return; uint64_t guid = legacyGuid ? packet.readUInt64() : packet.readPackedGuid(); if (!packet.hasRemaining(8)) return; @@ -954,7 +954,7 @@ void MovementHandler::handleMoveSetCollisionHeight(network::Packet& packet) { } void MovementHandler::handleMoveKnockBack(network::Packet& packet) { - const bool mkbTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool mkbTbc = isPreWotlk(); if (packet.getRemainingSize() < (mkbTbc ? 8u : 2u)) return; uint64_t guid = mkbTbc ? packet.readUInt64() : packet.readPackedGuid(); @@ -985,7 +985,7 @@ void MovementHandler::handleMoveKnockBack(network::Packet& packet) { // ============================================================ void MovementHandler::handleMoveSetSpeed(network::Packet& packet) { - const bool useFull = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool useFull = isPreWotlk(); uint64_t moverGuid = useFull ? packet.readUInt64() : packet.readPackedGuid(); @@ -1010,7 +1010,7 @@ void MovementHandler::handleMoveSetSpeed(network::Packet& packet) { } void MovementHandler::handleOtherPlayerMovement(network::Packet& packet) { - const bool otherMoveTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool otherMoveTbc = isPreWotlk(); uint64_t moverGuid = otherMoveTbc ? packet.readUInt64() : packet.readPackedGuid(); if (moverGuid == owner_.getPlayerGuid() || moverGuid == 0) { @@ -1646,7 +1646,7 @@ void MovementHandler::handleMonsterMoveTransport(network::Packet& packet) { // ============================================================ void MovementHandler::handleTeleportAck(network::Packet& packet) { - const bool taTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool taTbc = isPreWotlk(); if (packet.getRemainingSize() < (taTbc ? 8u : 4u)) { LOG_WARNING("MSG_MOVE_TELEPORT_ACK too short"); return; @@ -1657,7 +1657,7 @@ void MovementHandler::handleTeleportAck(network::Packet& packet) { if (!packet.hasRemaining(4)) return; uint32_t counter = packet.readUInt32(); - const bool taNoFlags2 = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool taNoFlags2 = isPreWotlk(); const size_t minMoveSz = taNoFlags2 ? (4 + 4 + 4 * 4) : (4 + 2 + 4 + 4 * 4); if (packet.getRemainingSize() < minMoveSz) { LOG_WARNING("MSG_MOVE_TELEPORT_ACK: not enough data for movement info"); @@ -2048,8 +2048,8 @@ void MovementHandler::applyTaxiMountForCurrentNode() { else if (it->second.mountDisplayIdHorde != 0) mountId = it->second.mountDisplayIdHorde; } if (mountId == 0) { - static const uint32_t kAllianceTaxiDisplays[] = {1210u, 1211u, 1212u, 1213u}; - static const uint32_t kHordeTaxiDisplays[] = {1310u, 1311u, 1312u}; + static constexpr uint32_t kAllianceTaxiDisplays[] = {1210u, 1211u, 1212u, 1213u}; + static constexpr uint32_t kHordeTaxiDisplays[] = {1310u, 1311u, 1312u}; mountId = isAlliance ? kAllianceTaxiDisplays[0] : kHordeTaxiDisplays[0]; } if (mountId == 0) { diff --git a/src/game/social_handler.cpp b/src/game/social_handler.cpp index bfc39923..b1b00f91 100644 --- a/src/game/social_handler.cpp +++ b/src/game/social_handler.cpp @@ -612,7 +612,7 @@ void SocialHandler::handleInspectResults(network::Packet& packet) { } // talentType == 1: inspect result - const bool talentTbc = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool talentTbc = isPreWotlk(); if (packet.getRemainingSize() < (talentTbc ? 8u : 2u)) return; uint64_t guid = talentTbc ? packet.readUInt64() : packet.readPackedGuid(); diff --git a/src/game/spell_handler.cpp b/src/game/spell_handler.cpp index 1c653543..7ea03e0e 100644 --- a/src/game/spell_handler.cpp +++ b/src/game/spell_handler.cpp @@ -62,6 +62,33 @@ static audio::SpellSoundManager::MagicSchool schoolMaskToMagicSchool(uint32_t ma return audio::SpellSoundManager::MagicSchool::ARCANE; } +// ---- Extracted helpers to reduce nesting in handleSpellGo ---- + +audio::SpellSoundManager::MagicSchool SpellHandler::resolveSpellSchool(uint32_t spellId) { + owner_.loadSpellNameCache(); + auto it = owner_.spellNameCacheRef().find(spellId); + if (it != owner_.spellNameCacheRef().end() && it->second.schoolMask) + return schoolMaskToMagicSchool(it->second.schoolMask); + return audio::SpellSoundManager::MagicSchool::ARCANE; +} + +void SpellHandler::playSpellCastSound(uint32_t spellId) { + auto* ac = owner_.services().audioCoordinator; + if (!ac) return; + auto* ssm = ac->getSpellSoundManager(); + if (!ssm) return; + ssm->playCast(resolveSpellSchool(spellId)); +} + +void SpellHandler::playSpellImpactSound(uint32_t spellId) { + auto* ac = owner_.services().audioCoordinator; + if (!ac) return; + auto* ssm = ac->getSpellSoundManager(); + if (!ssm) return; + ssm->playImpact(resolveSpellSchool(spellId), + audio::SpellSoundManager::SpellPower::MEDIUM); +} + static std::string displaySpellName(GameHandler& handler, uint32_t spellId) { if (spellId == 0) return {}; @@ -929,18 +956,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { if (data.casterUnit == owner_.getPlayerGuid()) { // Play cast-complete sound - if (!owner_.isProfessionSpell(data.spellId)) { - if (auto* ac = owner_.services().audioCoordinator) { - if (auto* ssm = ac->getSpellSoundManager()) { - owner_.loadSpellNameCache(); - auto it = owner_.spellNameCacheRef().find(data.spellId); - auto school = (it != owner_.spellNameCacheRef().end() && it->second.schoolMask) - ? schoolMaskToMagicSchool(it->second.schoolMask) - : audio::SpellSoundManager::MagicSchool::ARCANE; - ssm->playCast(school); - } - } - } + if (!owner_.isProfessionSpell(data.spellId)) + playSpellCastSound(data.spellId); // Instant melee abilities → trigger attack animation uint32_t sid = data.spellId; @@ -1039,18 +1056,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { for (const auto& tgt : data.hitTargets) { if (tgt == owner_.getPlayerGuid()) { targetsPlayer = true; break; } } - if (targetsPlayer) { - if (auto* ac = owner_.services().audioCoordinator) { - if (auto* ssm = ac->getSpellSoundManager()) { - owner_.loadSpellNameCache(); - auto it = owner_.spellNameCacheRef().find(data.spellId); - auto school = (it != owner_.spellNameCacheRef().end() && it->second.schoolMask) - ? schoolMaskToMagicSchool(it->second.schoolMask) - : audio::SpellSoundManager::MagicSchool::ARCANE; - ssm->playCast(school); - } - } - } + if (targetsPlayer) + playSpellCastSound(data.spellId); } // Clear unit cast bar @@ -1085,18 +1092,8 @@ void SpellHandler::handleSpellGo(network::Packet& packet) { owner_.addonEventCallbackRef()("UNIT_SPELLCAST_SUCCEEDED", {unitId, std::to_string(data.spellId)}); } - if (playerIsHit || playerHitEnemy) { - if (auto* ac = owner_.services().audioCoordinator) { - if (auto* ssm = ac->getSpellSoundManager()) { - owner_.loadSpellNameCache(); - auto it = owner_.spellNameCacheRef().find(data.spellId); - auto school = (it != owner_.spellNameCacheRef().end() && it->second.schoolMask) - ? schoolMaskToMagicSchool(it->second.schoolMask) - : audio::SpellSoundManager::MagicSchool::ARCANE; - ssm->playImpact(school, audio::SpellSoundManager::SpellPower::MEDIUM); - } - } - } + if (playerIsHit || playerHitEnemy) + playSpellImpactSound(data.spellId); } void SpellHandler::handleSpellCooldown(network::Packet& packet) { @@ -1225,7 +1222,7 @@ void SpellHandler::handleAuraUpdate(network::Packet& packet, bool isAll) { // Sprint aura detection — check if any sprint/dash speed buff is active if (data.guid == owner_.getPlayerGuid() && owner_.sprintAuraCallbackRef()) { - static const uint32_t sprintSpells[] = { + static constexpr uint32_t sprintSpells[] = { 2983, 8696, 11305, // Rogue Sprint (ranks 1-3) 1850, 9821, 33357, // Druid Dash (ranks 1-3) 36554, // Shadowstep (speed component) @@ -1816,7 +1813,7 @@ void SpellHandler::loadSpellNameCache() const { if (hasSchoolMask) { entry.schoolMask = dbc->getUInt32(i, schoolMaskField); } else if (hasSchoolEnum) { - static const uint32_t enumToBitmask[] = {0x01,0x02,0x04,0x08,0x10,0x20,0x40}; + static constexpr uint32_t enumToBitmask[] = {0x01,0x02,0x04,0x08,0x10,0x20,0x40}; uint32_t e = dbc->getUInt32(i, schoolEnumField); entry.schoolMask = (e < 7) ? enumToBitmask[e] : 0; } @@ -2941,6 +2938,163 @@ void SpellHandler::handleSpellInstaKillLog(network::Packet& packet) { packet.skipAll(); } +// ---- handleSpellLogExecute per-effect parsers (extracted to reduce nesting) ---- + +void SpellHandler::parseEffectPowerDrain(network::Packet& packet, uint32_t effectLogCount, + uint64_t caster, uint32_t spellId, + bool isPlayerCaster, bool usesFullGuid) { + // SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier + const uint64_t playerGuid = owner_.getPlayerGuid(); + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(usesFullGuid ? 8u : 1u) + || (!usesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); break; + } + uint64_t drainTarget = usesFullGuid ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(12)) { packet.skipAll(); break; } + uint32_t drainAmount = packet.readUInt32(); + uint32_t drainPower = packet.readUInt32(); // 0=mana,1=rage,3=energy,6=runic + float drainMult = packet.readFloat(); + + LOG_DEBUG("SMSG_SPELLLOGEXECUTE POWER_DRAIN: spell=", spellId, + " power=", drainPower, " amount=", drainAmount, + " multiplier=", drainMult); + if (drainAmount == 0) continue; + + const auto powerByte = static_cast(drainPower); + if (drainTarget == playerGuid) + owner_.addCombatText(CombatTextEntry::POWER_DRAIN, + static_cast(drainAmount), spellId, false, + powerByte, caster, drainTarget); + if (!isPlayerCaster) continue; + if (drainTarget != playerGuid) + owner_.addCombatText(CombatTextEntry::POWER_DRAIN, + static_cast(drainAmount), spellId, true, + powerByte, caster, drainTarget); + if (drainMult <= 0.0f || !std::isfinite(drainMult)) continue; + const uint32_t gained = static_cast( + std::lround(static_cast(drainAmount) * static_cast(drainMult))); + if (gained > 0) + owner_.addCombatText(CombatTextEntry::ENERGIZE, + static_cast(gained), spellId, true, + powerByte, caster, caster); + } +} + +void SpellHandler::parseEffectHealthLeech(network::Packet& packet, uint32_t effectLogCount, + uint64_t caster, uint32_t spellId, + bool isPlayerCaster, bool usesFullGuid) { + // SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier + const uint64_t playerGuid = owner_.getPlayerGuid(); + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(usesFullGuid ? 8u : 1u) + || (!usesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); break; + } + uint64_t leechTarget = usesFullGuid ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(8)) { packet.skipAll(); break; } + uint32_t leechAmount = packet.readUInt32(); + float leechMult = packet.readFloat(); + + LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", spellId, + " amount=", leechAmount, " multiplier=", leechMult); + if (leechAmount == 0) continue; + + if (leechTarget == playerGuid) { + owner_.addCombatText(CombatTextEntry::SPELL_DAMAGE, + static_cast(leechAmount), spellId, false, 0, + caster, leechTarget); + } else if (isPlayerCaster) { + owner_.addCombatText(CombatTextEntry::SPELL_DAMAGE, + static_cast(leechAmount), spellId, true, 0, + caster, leechTarget); + } + if (!isPlayerCaster || leechMult <= 0.0f || !std::isfinite(leechMult)) continue; + const uint32_t gained = static_cast( + std::lround(static_cast(leechAmount) * static_cast(leechMult))); + if (gained > 0) + owner_.addCombatText(CombatTextEntry::HEAL, + static_cast(gained), spellId, true, 0, + caster, caster); + } +} + +void SpellHandler::parseEffectCreateItem(network::Packet& packet, uint32_t effectLogCount, + uint64_t /*caster*/, uint32_t spellId, + bool isPlayerCaster) { + // SPELL_EFFECT_CREATE_ITEM / CREATE_ITEM2: uint32 itemEntry per log entry + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(4)) break; + uint32_t itemEntry = packet.readUInt32(); + if (!isPlayerCaster || itemEntry == 0) continue; + + owner_.ensureItemInfo(itemEntry); + const ItemQueryResponseData* info = owner_.getItemInfo(itemEntry); + std::string itemName = (info && !info->name.empty()) + ? info->name : ("item #" + std::to_string(itemEntry)); + const auto& spellName = owner_.getSpellName(spellId); + std::string msg = spellName.empty() + ? ("You create: " + itemName + ".") + : ("You create " + itemName + " using " + spellName + "."); + owner_.addSystemChatMessage(msg); + LOG_DEBUG("SMSG_SPELLLOGEXECUTE CREATE_ITEM: spell=", spellId, + " item=", itemEntry, " name=", itemName); + + // Repeat-craft queue: re-cast if more crafts remaining + if (craftQueueRemaining_ > 0 && craftQueueSpellId_ == spellId) { + --craftQueueRemaining_; + if (craftQueueRemaining_ > 0) + castSpell(craftQueueSpellId_, 0); + else + craftQueueSpellId_ = 0; + } + } +} + +void SpellHandler::parseEffectInterruptCast(network::Packet& packet, uint32_t effectLogCount, + uint64_t caster, uint32_t spellId, + bool isPlayerCaster, bool usesFullGuid) { + // SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id + const uint64_t playerGuid = owner_.getPlayerGuid(); + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(usesFullGuid ? 8u : 1u) + || (!usesFullGuid && !packet.hasFullPackedGuid())) { + packet.skipAll(); break; + } + uint64_t icTarget = usesFullGuid ? packet.readUInt64() : packet.readPackedGuid(); + if (!packet.hasRemaining(4)) { packet.skipAll(); break; } + uint32_t icSpellId = packet.readUInt32(); + // Clear the interrupted unit's cast bar immediately + unitCastStates_.erase(icTarget); + // Record interrupt in combat log when player is involved + if (isPlayerCaster || icTarget == playerGuid) + owner_.addCombatText(CombatTextEntry::INTERRUPT, 0, icSpellId, isPlayerCaster, 0, + caster, icTarget); + LOG_DEBUG("SMSG_SPELLLOGEXECUTE INTERRUPT_CAST: spell=", spellId, + " interrupted=", icSpellId, " target=0x", std::hex, icTarget, std::dec); + } +} + +void SpellHandler::parseEffectFeedPet(network::Packet& packet, uint32_t effectLogCount, + uint64_t /*caster*/, uint32_t /*spellId*/, + bool isPlayerCaster) { + // SPELL_EFFECT_FEED_PET: uint32 itemEntry per log entry + for (uint32_t li = 0; li < effectLogCount; ++li) { + if (!packet.hasRemaining(4)) break; + uint32_t feedItem = packet.readUInt32(); + if (!isPlayerCaster || feedItem == 0) continue; + + owner_.ensureItemInfo(feedItem); + const ItemQueryResponseData* info = owner_.getItemInfo(feedItem); + std::string itemName = (info && !info->name.empty()) + ? info->name : ("item #" + std::to_string(feedItem)); + uint32_t feedQuality = info ? info->quality : 1u; + owner_.addSystemChatMessage("You feed your pet " + + buildItemLink(feedItem, feedQuality, itemName) + "."); + LOG_DEBUG("SMSG_SPELLLOGEXECUTE FEED_PET: item=", feedItem, " name=", itemName); + } +} + void SpellHandler::handleSpellLogExecute(network::Packet& packet) { // WotLK/Classic/Turtle: packed_guid caster + uint32 spellId + uint32 effectCount // TBC: uint64 caster + uint32 spellId + uint32 effectCount @@ -2973,142 +3127,22 @@ void SpellHandler::handleSpellLogExecute(network::Packet& packet) { uint8_t effectType = packet.readUInt8(); uint32_t effectLogCount = packet.readUInt32(); effectLogCount = std::min(effectLogCount, 64u); // sanity - if (effectType == SpellEffect::POWER_DRAIN) { - // SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier - for (uint32_t li = 0; li < effectLogCount; ++li) { - if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u) - || (!exeUsesFullGuid && !packet.hasFullPackedGuid())) { - packet.skipAll(); break; - } - uint64_t drainTarget = exeUsesFullGuid - ? packet.readUInt64() - : packet.readPackedGuid(); - if (!packet.hasRemaining(12)) { packet.skipAll(); break; } - uint32_t drainAmount = packet.readUInt32(); - uint32_t drainPower = packet.readUInt32(); // 0=mana,1=rage,3=energy,6=runic - float drainMult = packet.readFloat(); - if (drainAmount > 0) { - if (drainTarget == owner_.getPlayerGuid()) - owner_.addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(drainAmount), exeSpellId, false, - static_cast(drainPower), - exeCaster, drainTarget); - if (isPlayerCaster) { - if (drainTarget != owner_.getPlayerGuid()) { - owner_.addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(drainAmount), exeSpellId, true, - static_cast(drainPower), exeCaster, drainTarget); - } - if (drainMult > 0.0f && std::isfinite(drainMult)) { - const uint32_t gainedAmount = static_cast( - std::lround(static_cast(drainAmount) * static_cast(drainMult))); - if (gainedAmount > 0) { - owner_.addCombatText(CombatTextEntry::ENERGIZE, static_cast(gainedAmount), exeSpellId, true, - static_cast(drainPower), exeCaster, exeCaster); - } - } - } - } - LOG_DEBUG("SMSG_SPELLLOGEXECUTE POWER_DRAIN: spell=", exeSpellId, - " power=", drainPower, " amount=", drainAmount, - " multiplier=", drainMult); - } - } else if (effectType == SpellEffect::HEALTH_LEECH) { - // SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier - for (uint32_t li = 0; li < effectLogCount; ++li) { - if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u) - || (!exeUsesFullGuid && !packet.hasFullPackedGuid())) { - packet.skipAll(); break; - } - uint64_t leechTarget = exeUsesFullGuid - ? packet.readUInt64() - : packet.readPackedGuid(); - if (!packet.hasRemaining(8)) { packet.skipAll(); break; } - uint32_t leechAmount = packet.readUInt32(); - float leechMult = packet.readFloat(); - if (leechAmount > 0) { - if (leechTarget == owner_.getPlayerGuid()) { - owner_.addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(leechAmount), exeSpellId, false, 0, - exeCaster, leechTarget); - } else if (isPlayerCaster) { - owner_.addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(leechAmount), exeSpellId, true, 0, - exeCaster, leechTarget); - } - if (isPlayerCaster && leechMult > 0.0f && std::isfinite(leechMult)) { - const uint32_t gainedAmount = static_cast( - std::lround(static_cast(leechAmount) * static_cast(leechMult))); - if (gainedAmount > 0) { - owner_.addCombatText(CombatTextEntry::HEAL, static_cast(gainedAmount), exeSpellId, true, 0, - exeCaster, exeCaster); - } - } - } - LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", exeSpellId, - " amount=", leechAmount, " multiplier=", leechMult); - } - } else if (effectType == SpellEffect::CREATE_ITEM || effectType == SpellEffect::CREATE_ITEM2) { - // SPELL_EFFECT_CREATE_ITEM / CREATE_ITEM2: uint32 itemEntry per log entry - for (uint32_t li = 0; li < effectLogCount; ++li) { - if (!packet.hasRemaining(4)) break; - uint32_t itemEntry = packet.readUInt32(); - if (isPlayerCaster && itemEntry != 0) { - owner_.ensureItemInfo(itemEntry); - const ItemQueryResponseData* info = owner_.getItemInfo(itemEntry); - std::string itemName = info && !info->name.empty() - ? info->name : ("item #" + std::to_string(itemEntry)); - const auto& spellName = owner_.getSpellName(exeSpellId); - std::string msg = spellName.empty() - ? ("You create: " + itemName + ".") - : ("You create " + itemName + " using " + spellName + "."); - owner_.addSystemChatMessage(msg); - LOG_DEBUG("SMSG_SPELLLOGEXECUTE CREATE_ITEM: spell=", exeSpellId, - " item=", itemEntry, " name=", itemName); - // Repeat-craft queue: re-cast if more crafts remaining - if (craftQueueRemaining_ > 0 && craftQueueSpellId_ == exeSpellId) { - --craftQueueRemaining_; - if (craftQueueRemaining_ > 0) { - castSpell(craftQueueSpellId_, 0); - } else { - craftQueueSpellId_ = 0; - } - } - } - } + if (effectType == SpellEffect::POWER_DRAIN) { + parseEffectPowerDrain(packet, effectLogCount, exeCaster, exeSpellId, + isPlayerCaster, exeUsesFullGuid); + } else if (effectType == SpellEffect::HEALTH_LEECH) { + parseEffectHealthLeech(packet, effectLogCount, exeCaster, exeSpellId, + isPlayerCaster, exeUsesFullGuid); + } else if (effectType == SpellEffect::CREATE_ITEM || effectType == SpellEffect::CREATE_ITEM2) { + parseEffectCreateItem(packet, effectLogCount, exeCaster, exeSpellId, + isPlayerCaster); } else if (effectType == SpellEffect::INTERRUPT_CAST) { - // SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id - for (uint32_t li = 0; li < effectLogCount; ++li) { - if (!packet.hasRemaining(exeUsesFullGuid ? 8u : 1u) - || (!exeUsesFullGuid && !packet.hasFullPackedGuid())) { - packet.skipAll(); break; - } - uint64_t icTarget = exeUsesFullGuid - ? packet.readUInt64() - : packet.readPackedGuid(); - if (!packet.hasRemaining(4)) { packet.skipAll(); break; } - uint32_t icSpellId = packet.readUInt32(); - // Clear the interrupted unit's cast bar immediately - unitCastStates_.erase(icTarget); - // Record interrupt in combat log when player is involved - if (isPlayerCaster || icTarget == owner_.getPlayerGuid()) - owner_.addCombatText(CombatTextEntry::INTERRUPT, 0, icSpellId, isPlayerCaster, 0, - exeCaster, icTarget); - LOG_DEBUG("SMSG_SPELLLOGEXECUTE INTERRUPT_CAST: spell=", exeSpellId, - " interrupted=", icSpellId, " target=0x", std::hex, icTarget, std::dec); - } + parseEffectInterruptCast(packet, effectLogCount, exeCaster, exeSpellId, + isPlayerCaster, exeUsesFullGuid); } else if (effectType == SpellEffect::FEED_PET) { - // SPELL_EFFECT_FEED_PET: uint32 itemEntry per log entry - for (uint32_t li = 0; li < effectLogCount; ++li) { - if (!packet.hasRemaining(4)) break; - uint32_t feedItem = packet.readUInt32(); - if (isPlayerCaster && feedItem != 0) { - owner_.ensureItemInfo(feedItem); - const ItemQueryResponseData* info = owner_.getItemInfo(feedItem); - std::string itemName = info && !info->name.empty() - ? info->name : ("item #" + std::to_string(feedItem)); - uint32_t feedQuality = info ? info->quality : 1u; - owner_.addSystemChatMessage("You feed your pet " + buildItemLink(feedItem, feedQuality, itemName) + "."); - LOG_DEBUG("SMSG_SPELLLOGEXECUTE FEED_PET: item=", feedItem, " name=", itemName); - } - } + parseEffectFeedPet(packet, effectLogCount, exeCaster, exeSpellId, + isPlayerCaster); } else { // Unknown effect type — stop parsing to avoid misalignment packet.skipAll(); diff --git a/src/game/transport_manager.cpp b/src/game/transport_manager.cpp index 3f053f5d..5f97f41d 100644 --- a/src/game/transport_manager.cpp +++ b/src/game/transport_manager.cpp @@ -1282,14 +1282,14 @@ uint32_t TransportManager::pickFallbackMovingPath(uint32_t entry, uint32_t displ (displayId == 3031u || displayId == 7546u || displayId == 1587u || displayId == 807u || displayId == 808u); if (looksLikeShip) { - static const uint32_t kShipCandidates[] = {176080u, 176081u, 176082u, 176083u, 176084u, 176085u, 194675u}; + static constexpr uint32_t kShipCandidates[] = {176080u, 176081u, 176082u, 176083u, 176084u, 176085u, 194675u}; for (uint32_t id : kShipCandidates) { if (isUsableMovingPath(id)) return id; } } if (looksLikeZeppelin) { - static const uint32_t kZeppelinCandidates[] = {193182u, 193183u, 188360u, 190587u}; + static constexpr uint32_t kZeppelinCandidates[] = {193182u, 193183u, 188360u, 190587u}; for (uint32_t id : kZeppelinCandidates) { if (isUsableMovingPath(id)) return id; } diff --git a/src/game/warden_emulator.cpp b/src/game/warden_emulator.cpp index 0fa5bff8..1689ee00 100644 --- a/src/game/warden_emulator.cpp +++ b/src/game/warden_emulator.cpp @@ -203,7 +203,7 @@ uint32_t WardenEmulator::hookAPI(const std::string& dllName, // Write a RET (0xC3) at the stub address as a safe fallback in case // the code hook fires after EIP has already advanced past our intercept. if (uc_) { - static const uint8_t retInstr = 0xC3; + static constexpr uint8_t retInstr = 0xC3; uc_mem_write(uc_, stubAddr, &retInstr, 1); } diff --git a/src/game/warden_handler.cpp b/src/game/warden_handler.cpp index 4a78a6d6..f4edbce9 100644 --- a/src/game/warden_handler.cpp +++ b/src/game/warden_handler.cpp @@ -10,6 +10,7 @@ #include "core/application.hpp" #include "pipeline/asset_manager.hpp" #include "core/logger.hpp" +#include "game/warden_constants.hpp" #include #include #include @@ -355,7 +356,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) { }; switch (wardenOpcode) { - case 0x00: { // WARDEN_SMSG_MODULE_USE + case WARDEN_SMSG_MODULE_USE: { // MODULE_USE // Format: [1 opcode][16 moduleHash][16 moduleKey][4 moduleSize] if (decrypted.size() < 37) { LOG_ERROR("Warden: MODULE_USE too short (", decrypted.size(), " bytes, need 37)"); @@ -379,15 +380,15 @@ void WardenHandler::handleWardenData(network::Packet& packet) { loadWardenCRFile(hashHex); } - // Respond with MODULE_MISSING (opcode 0x00) to request the module data - std::vector resp = { 0x00 }; // WARDEN_CMSG_MODULE_MISSING + // Respond with MODULE_MISSING to request the module data + std::vector resp = { WARDEN_CMSG_MODULE_MISSING }; sendWardenResponse(resp); wardenState_ = WardenState::WAIT_MODULE_CACHE; LOG_DEBUG("Warden: Sent MODULE_MISSING for module size=", wardenModuleSize_, ", waiting for data chunks"); break; } - case 0x01: { // WARDEN_SMSG_MODULE_CACHE (module data chunk) + case WARDEN_SMSG_MODULE_CACHE: { // MODULE_CACHE (module data chunk) // Format: [1 opcode][2 chunkSize LE][chunkSize bytes data] if (decrypted.size() < 3) { LOG_ERROR("Warden: MODULE_CACHE too short"); @@ -463,8 +464,8 @@ void WardenHandler::handleWardenData(network::Packet& packet) { wardenLoadedModule_.reset(); } - // Send MODULE_OK (opcode 0x01) - std::vector resp = { 0x01 }; // WARDEN_CMSG_MODULE_OK + // Send MODULE_OK + std::vector resp = { WARDEN_CMSG_MODULE_OK }; sendWardenResponse(resp); LOG_DEBUG("Warden: Sent MODULE_OK"); } @@ -472,7 +473,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) { break; } - case 0x05: { // WARDEN_SMSG_HASH_REQUEST + case WARDEN_SMSG_HASH_REQUEST: { // HASH_REQUEST // Format: [1 opcode][16 seed] if (decrypted.size() < 17) { LOG_ERROR("Warden: HASH_REQUEST too short (", decrypted.size(), " bytes, need 17)"); @@ -506,9 +507,9 @@ void WardenHandler::handleWardenData(network::Packet& packet) { if (match) { LOG_DEBUG("Warden: HASH_REQUEST — CR entry MATCHED, sending pre-computed reply"); - // Send HASH_RESULT (opcode 0x04 + 20-byte reply) + // Send HASH_RESULT std::vector resp; - resp.push_back(0x04); + resp.push_back(WARDEN_CMSG_HASH_RESULT); resp.insert(resp.end(), match->reply, match->reply + 20); sendWardenResponse(resp); @@ -576,7 +577,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) { break; } - case 0x02: { // WARDEN_SMSG_CHEAT_CHECKS_REQUEST + case WARDEN_SMSG_CHEAT_CHECKS_REQUEST: { // CHEAT_CHECKS_REQUEST LOG_DEBUG("Warden: CHEAT_CHECKS_REQUEST (", decrypted.size(), " bytes)"); if (decrypted.size() < 3) { @@ -660,9 +661,9 @@ void WardenHandler::handleWardenData(network::Packet& packet) { HMAC(EVP_sha1(), seed, 4, pat, patLen, out, &outLen); return outLen == SHA_DIGEST_LENGTH && !std::memcmp(out, hash, SHA_DIGEST_LENGTH); }; - static const uint8_t p1[] = {0x33,0xD2,0x33,0xC9,0xE8,0x87,0x07,0x1B,0x00,0xE8}; + static constexpr uint8_t p1[] = {0x33,0xD2,0x33,0xC9,0xE8,0x87,0x07,0x1B,0x00,0xE8}; if (off == 13856 && len == sizeof(p1) && tryMatch(p1, sizeof(p1))) return true; - static const uint8_t p2[] = {0x56,0x57,0xFC,0x8B,0x54,0x24,0x14,0x8B, + static constexpr uint8_t p2[] = {0x56,0x57,0xFC,0x8B,0x54,0x24,0x14,0x8B, 0x74,0x24,0x10,0x8B,0x44,0x24,0x0C,0x8B,0xCA,0x8B,0xF8,0xC1, 0xE9,0x02,0x74,0x02,0xF3,0xA5,0xB1,0x03,0x23,0xCA,0x74,0x02, 0xF3,0xA4,0x5F,0x5E,0xC3}; @@ -706,7 +707,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) { LOG_WARNING("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), " len=", (int)readLen, (strIdx ? " module=\"" + moduleName + "\"" : "")); - if (offset == 0x00CF0BC8 && readLen == 4 && wardenMemory_ && wardenMemory_->isLoaded()) { + if (offset == WARDEN_TICKCOUNT_ADDRESS && readLen == 4 && wardenMemory_ && wardenMemory_->isLoaded()) { uint32_t now = static_cast( std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count()); @@ -717,25 +718,25 @@ void WardenHandler::handleWardenData(network::Packet& packet) { wardenMemory_->readMemory(offset, readLen, memBuf.data()); if (memOk) { const char* region = "?"; - if (offset >= 0x7FFE0000 && offset < 0x7FFF0000) region = "KUSER"; - else if (offset >= 0x400000 && offset < 0x800000) region = ".text/.code"; - else if (offset >= 0x7FF000 && offset < 0x827000) region = ".rdata"; - else if (offset >= 0x827000 && offset < 0x883000) region = ".data(raw)"; - else if (offset >= 0x883000 && offset < 0xD06000) region = ".data(BSS)"; + if (offset >= KUSER_SHARED_DATA_BASE && offset < KUSER_SHARED_DATA_END) region = "KUSER"; + else if (offset >= PE_TEXT_SECTION_BASE && offset < PE_TEXT_SECTION_END) region = ".text/.code"; + else if (offset >= PE_RDATA_SECTION_BASE && offset < PE_DATA_RAW_SECTION_BASE) region = ".rdata"; + else if (offset >= PE_DATA_RAW_SECTION_BASE && offset < PE_BSS_SECTION_BASE) region = ".data(raw)"; + else if (offset >= PE_BSS_SECTION_BASE && offset < PE_BSS_SECTION_END) region = ".data(BSS)"; bool allZero = true; for (int i = 0; i < (int)readLen; i++) { if (memBuf[i] != 0) { allZero = false; break; } } std::string hexDump; for (int i = 0; i < (int)readLen; i++) { char hx[4]; snprintf(hx,4,"%02x ",memBuf[i]); hexDump += hx; } LOG_WARNING("Warden: MEM_CHECK served: [", hexDump, "] region=", region, - (allZero && offset >= 0x883000 ? " \xe2\x98\x85""BSS_ZERO\xe2\x98\x85" : "")); - if (offset == 0x7FFE026C && readLen == 12) + (allZero && offset >= PE_BSS_SECTION_BASE ? " \xe2\x98\x85""BSS_ZERO\xe2\x98\x85" : "")); + if (offset == WARDEN_WIN_VERSION_ADDRESS && readLen == 12) LOG_WARNING("Warden: Applying 4-byte ULONG alignment padding for WinVersionGet"); - resultData.push_back(0x00); + resultData.push_back(WARDEN_MEM_CHECK_SUCCESS); resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); } else { LOG_WARNING("Warden: MEM_CHECK -> 0xE9 (unmapped 0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), ")"); - resultData.push_back(0xE9); + resultData.push_back(WARDEN_MEM_CHECK_UNMAPPED); } break; } @@ -936,7 +937,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) { }; // DB sanity check: "Warden packet process code search sanity check" (id=85) - static const uint8_t kPacketProcessSanityPattern[] = { + static constexpr uint8_t kPacketProcessSanityPattern[] = { 0x33, 0xD2, 0x33, 0xC9, 0xE8, 0x87, 0x07, 0x1B, 0x00, 0xE8 }; if (offset == 13856 && length == sizeof(kPacketProcessSanityPattern) && @@ -945,7 +946,7 @@ void WardenHandler::handleWardenData(network::Packet& packet) { } // Scripted sanity check: "Warden Memory Read check" in wardenwin.cpp - static const uint8_t kWardenMemoryReadPattern[] = { + static constexpr uint8_t kWardenMemoryReadPattern[] = { 0x56, 0x57, 0xFC, 0x8B, 0x54, 0x24, 0x14, 0x8B, 0x74, 0x24, 0x10, 0x8B, 0x44, 0x24, 0x0C, 0x8B, 0xCA, 0x8B, 0xF8, 0xC1, 0xE9, 0x02, 0x74, 0x02, diff --git a/src/game/warden_memory.cpp b/src/game/warden_memory.cpp index 2c0b6c67..54bbf25c 100644 --- a/src/game/warden_memory.cpp +++ b/src/game/warden_memory.cpp @@ -426,7 +426,7 @@ void WardenMemory::patchRuntimeGlobals() { // FIND_CODE_BY_HASH (PAGE_B) brute-force search can find it. // This is the pattern VMaNGOS's "Warden Memory Read check" looks for. constexpr uint32_t MEMCPY_PATTERN_VA = 0xCE8700; - static const uint8_t kWardenMemcpyPattern[37] = { + static constexpr uint8_t kWardenMemcpyPattern[37] = { 0x56, 0x57, 0xFC, 0x8B, 0x54, 0x24, 0x14, 0x8B, 0x74, 0x24, 0x10, 0x8B, 0x44, 0x24, 0x0C, 0x8B, 0xCA, 0x8B, 0xF8, 0xC1, 0xE9, 0x02, 0x74, 0x02, diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 40e16194..c4497b79 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -141,7 +141,7 @@ network::Packet AuthSessionPacket::build(uint32_t build, "Blizzard_InspectUI", "Blizzard_MacroUI", "Blizzard_RaidUI", "Blizzard_TalentUI", "Blizzard_TradeSkillUI", "Blizzard_TrainerUI" }; - static const uint32_t standardModulusCRC = 0x4C1C776D; + static constexpr uint32_t standardModulusCRC = 0x4C1C776D; for (const char* name : vanillaAddons) { // string (null-terminated) size_t len = strlen(name); diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 04dc195e..7279de06 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -110,12 +110,12 @@ namespace wowee { namespace network { // WoW 3.3.5a RC4 encryption keys (hardcoded in client) -static const uint8_t ENCRYPT_KEY[] = { +static constexpr uint8_t ENCRYPT_KEY[] = { 0xC2, 0xB3, 0x72, 0x3C, 0xC6, 0xAE, 0xD9, 0xB5, 0x34, 0x3C, 0x53, 0xEE, 0x2F, 0x43, 0x67, 0xCE }; -static const uint8_t DECRYPT_KEY[] = { +static constexpr uint8_t DECRYPT_KEY[] = { 0xCC, 0x98, 0xAE, 0x04, 0xE8, 0x97, 0xEA, 0xCA, 0x12, 0xDD, 0xC0, 0x93, 0x42, 0x91, 0x53, 0x57 }; diff --git a/src/rendering/character_renderer.cpp b/src/rendering/character_renderer.cpp index 3c838eea..8712c5da 100644 --- a/src/rendering/character_renderer.cpp +++ b/src/rendering/character_renderer.cpp @@ -2130,7 +2130,9 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, } const float renderRadius = static_cast(envSizeOrDefault("WOWEE_CHAR_RENDER_RADIUS", 130)); const float renderRadiusSq = renderRadius * renderRadius; - const float characterCullRadius = 2.0f; // Estimate character radius for frustum testing + // Default frustum-cull radius when model bounds aren't available. + // 4.0 covers Tauren, mounted characters, and most creature models. + constexpr float kDefaultCharacterCullRadius = 4.0f; const glm::vec3 camPos = camera.getPosition(); // Extract frustum planes for per-instance visibility testing @@ -2175,8 +2177,17 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, // Distance cull: skip if beyond render radius if (distSq > renderRadiusSq) continue; + // Compute per-instance bounding radius from model data when available. + float cullRadius = kDefaultCharacterCullRadius; + auto mIt = models.find(instance.modelId); + if (mIt != models.end()) { + float modelR = mIt->second.data.boundRadius; + if (modelR > 0.01f) + cullRadius = std::max(kDefaultCharacterCullRadius, modelR * std::max(0.001f, instance.scale)); + } + // Frustum cull: skip if outside view frustum - if (!frustum.intersectsSphere(instance.position, characterCullRadius)) continue; + if (!frustum.intersectsSphere(instance.position, cullRadius)) continue; } if (!instance.cachedModel) continue; diff --git a/src/rendering/m2_renderer_render.cpp b/src/rendering/m2_renderer_render.cpp index 19776ab6..d013fb50 100644 --- a/src/rendering/m2_renderer_render.cpp +++ b/src/rendering/m2_renderer_render.cpp @@ -11,6 +11,7 @@ #include "rendering/vk_frame_data.hpp" #include "rendering/camera.hpp" #include "rendering/frustum.hpp" +#include "rendering/render_constants.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/blp_loader.hpp" #include "core/logger.hpp" @@ -90,7 +91,7 @@ uint32_t M2Renderer::createInstance(uint32_t modelId, const glm::vec3& position, instance.idleSequenceIndex = 0; instance.animDuration = static_cast(mdl.sequences[0].duration); instance.animTime = static_cast(randRange(std::max(1u, mdl.sequences[0].duration))); - instance.variationTimer = randFloat(3000.0f, 11000.0f); + instance.variationTimer = randFloat(rendering::M2_VARIATION_TIMER_MIN_MS, rendering::M2_VARIATION_TIMER_MAX_MS); } // Seed bone matrices from an existing instance of the same model so the @@ -199,7 +200,7 @@ uint32_t M2Renderer::createInstanceWithMatrix(uint32_t modelId, const glm::mat4& instance.idleSequenceIndex = 0; instance.animDuration = static_cast(mdl2.sequences[0].duration); instance.animTime = static_cast(randRange(std::max(1u, mdl2.sequences[0].duration))); - instance.variationTimer = randFloat(3000.0f, 11000.0f); + instance.variationTimer = randFloat(rendering::M2_VARIATION_TIMER_MIN_MS, rendering::M2_VARIATION_TIMER_MAX_MS); } // Seed bone matrices from an existing sibling so the instance renders immediately @@ -263,7 +264,9 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: // Cache camera state for frustum-culling bone computation cachedCamPos_ = cameraPos; - const float maxRenderDistance = (instances.size() > 2000) ? 800.0f : 2800.0f; + const float maxRenderDistance = (instances.size() > rendering::M2_HIGH_DENSITY_INSTANCE_THRESHOLD) + ? rendering::M2_MAX_RENDER_DISTANCE_HIGH_DENSITY + : rendering::M2_MAX_RENDER_DISTANCE_LOW_DENSITY; cachedMaxRenderDistSq_ = maxRenderDistance * maxRenderDistance; // Build frustum for culling bones @@ -271,10 +274,10 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: updateFrustum.extractFromMatrix(viewProjection); // --- Smoke particle spawning (only iterate tracked smoke instances) --- - std::uniform_real_distribution distXY(-0.4f, 0.4f); + std::uniform_real_distribution distXY(rendering::SMOKE_OFFSET_XY_MIN, rendering::SMOKE_OFFSET_XY_MAX); std::uniform_real_distribution distVelXY(-0.3f, 0.3f); - std::uniform_real_distribution distVelZ(3.0f, 5.0f); - std::uniform_real_distribution distLife(4.0f, 7.0f); + std::uniform_real_distribution distVelZ(rendering::SMOKE_VEL_Z_MIN, rendering::SMOKE_VEL_Z_MAX); + std::uniform_real_distribution distLife(rendering::SMOKE_LIFETIME_MIN, rendering::SMOKE_LIFETIME_MAX); std::uniform_real_distribution distDrift(-0.2f, 0.2f); smokeEmitAccum += deltaTime; @@ -287,13 +290,13 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: auto& instance = instances[si]; glm::vec3 emitWorld = glm::vec3(instance.modelMatrix * glm::vec4(0.0f, 0.0f, 0.0f, 1.0f)); - bool spark = (smokeRng() % 8 == 0); + bool spark = (smokeRng() % rendering::SPARK_PROBABILITY_DENOM == 0); SmokeParticle p; p.position = emitWorld + glm::vec3(distXY(smokeRng), distXY(smokeRng), 0.0f); if (spark) { p.velocity = glm::vec3(distVelXY(smokeRng) * 2.0f, distVelXY(smokeRng) * 2.0f, distVelZ(smokeRng) * 1.5f); - p.maxLife = 0.8f + static_cast(smokeRng() % 100) / 100.0f * 1.2f; + p.maxLife = rendering::SPARK_LIFE_BASE + static_cast(smokeRng() % 100) / 100.0f * rendering::SPARK_LIFE_RANGE; p.size = 0.5f; p.isSpark = 1.0f; } else { @@ -320,12 +323,12 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: continue; } p.position += p.velocity * deltaTime; - p.velocity.z *= 0.98f; // Slight deceleration + p.velocity.z *= rendering::SMOKE_Z_VEL_DAMPING; // Slight deceleration p.velocity.x += distDrift(smokeRng) * deltaTime; p.velocity.y += distDrift(smokeRng) * deltaTime; // Grow from 1.0 to 3.5 over lifetime float t = p.life / p.maxLife; - p.size = 1.0f + t * 2.5f; + p.size = rendering::SMOKE_SIZE_START + t * rendering::SMOKE_SIZE_GROWTH; ++i; } @@ -389,7 +392,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: // Handle animation looping / variation transitions if (instance.animDuration <= 0.0f && instance.cachedHasParticleEmitters) { - instance.animDuration = 3333.0f; + instance.animDuration = rendering::M2_DEFAULT_PARTICLE_ANIM_MS; } if (instance.animDuration > 0.0f && instance.animTime >= instance.animDuration) { if (instance.playingVariation) { @@ -399,7 +402,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: instance.animDuration = static_cast(model.sequences[instance.idleSequenceIndex].duration); } instance.animTime = 0.0f; - instance.variationTimer = randFloat(4000.0f, 10000.0f); + instance.variationTimer = randFloat(rendering::M2_LOOP_VARIATION_TIMER_MIN_MS, rendering::M2_LOOP_VARIATION_TIMER_MAX_MS); } else { // Use iterative subtraction instead of fmod() to preserve precision float duration = std::max(1.0f, instance.animDuration); @@ -421,7 +424,7 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: instance.animDuration = static_cast(model.sequences[newSeq].duration); instance.animTime = 0.0f; } else { - instance.variationTimer = randFloat(2000.0f, 6000.0f); + instance.variationTimer = randFloat(rendering::M2_IDLE_VARIATION_TIMER_MIN_MS, rendering::M2_IDLE_VARIATION_TIMER_MAX_MS); } } } @@ -431,21 +434,21 @@ void M2Renderer::update(float deltaTime, const glm::vec3& cameraPos, const glm:: float cullRadius = worldRadius; glm::vec3 toCam = instance.position - cachedCamPos_; float distSq = glm::dot(toCam, toCam); - float effectiveMaxDistSq = cachedMaxRenderDistSq_ * std::max(1.0f, cullRadius / 12.0f); + float effectiveMaxDistSq = cachedMaxRenderDistSq_ * std::max(1.0f, cullRadius / rendering::M2_CULL_RADIUS_SCALE_DIVISOR); if (distSq > effectiveMaxDistSq) continue; - float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f); + float paddedRadius = std::max(cullRadius * rendering::M2_PADDED_RADIUS_SCALE, cullRadius + rendering::M2_PADDED_RADIUS_MIN_MARGIN); if (cullRadius > 0.0f && !updateFrustum.intersectsSphere(instance.position, paddedRadius)) continue; // LOD 3 skip: models beyond 150 units use the lowest LOD mesh which has // no visible skeletal animation. Keep their last-computed bone matrices // (always valid — seeded on spawn) and avoid the expensive per-bone work. - constexpr float kLOD3DistSq = 150.0f * 150.0f; + constexpr float kLOD3DistSq = rendering::M2_LOD3_DISTANCE * rendering::M2_LOD3_DISTANCE; if (distSq > kLOD3DistSq) continue; // Distance-based frame skipping: update distant bones less frequently uint32_t boneInterval = 1; - if (distSq > 100.0f * 100.0f) boneInterval = 4; - else if (distSq > 50.0f * 50.0f) boneInterval = 2; + if (distSq > rendering::M2_BONE_SKIP_DIST_FAR * rendering::M2_BONE_SKIP_DIST_FAR) boneInterval = 4; + else if (distSq > rendering::M2_BONE_SKIP_DIST_MID * rendering::M2_BONE_SKIP_DIST_MID) boneInterval = 2; instance.frameSkipCounter++; if ((instance.frameSkipCounter % boneInterval) != 0) continue; @@ -616,9 +619,12 @@ void M2Renderer::dispatchCullCompute(VkCommandBuffer cmd, uint32_t frameIndex, c const float* prevM = &prevVP_[0][0]; for (int k = 0; k < 16; ++k) maxDiff = std::max(maxDiff, std::abs(curM[k] - prevM[k])); - // Threshold: typical small camera motion produces diffs < 0.05. - // A fast rotation easily exceeds 0.3. Skip HiZ when diff is large. - if (maxDiff > 0.15f) hizSafe = false; + // Threshold: typical tracking-camera motion (following a walking + // character) produces diffs of 0.05–0.25. A fast rotation or + // zoom easily exceeds 0.5. The previous threshold (0.15) caused + // the HiZ pass to toggle on/off every other frame during normal + // gameplay, which produced global M2 doodad flicker. + if (maxDiff > rendering::HIZ_VP_DIFF_THRESHOLD) hizSafe = false; } ubo->hizEnabled = hizSafe ? 1u : 0u; @@ -656,11 +662,11 @@ void M2Renderer::dispatchCullCompute(VkCommandBuffer cmd, uint32_t frameIndex, c if (inst.cachedDisableAnimation) { cullRadius = std::max(cullRadius, 3.0f); } - float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / 12.0f); + float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / rendering::M2_CULL_RADIUS_SCALE_DIVISOR); if (inst.cachedDisableAnimation) effectiveMaxDistSq *= 2.6f; if (inst.cachedIsGroundDetail) effectiveMaxDistSq *= 0.9f; - float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f); + float paddedRadius = std::max(cullRadius * rendering::M2_PADDED_RADIUS_SCALE, cullRadius + rendering::M2_PADDED_RADIUS_MIN_MARGIN); uint32_t flags = 0; if (inst.cachedIsValid) flags |= 1u; @@ -668,7 +674,9 @@ void M2Renderer::dispatchCullCompute(VkCommandBuffer cmd, uint32_t frameIndex, c if (inst.cachedIsInvisibleTrap) flags |= 4u; // Bit 3: previouslyVisible — the shader skips HiZ for objects // that were NOT rendered last frame (no reliable depth data). - if (i < prevFrameVisible_.size() && prevFrameVisible_[i]) + // Hysteresis: treat as "previously visible" unless culled for + // 2+ consecutive frames, preventing single-frame false-cull flicker. + if (i < prevFrameVisible_.size() && prevFrameVisible_[i] < 2) flags |= 8u; input[i].sphere = glm::vec4(inst.position, paddedRadius); @@ -756,15 +764,25 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const // Snapshot the GPU visibility results into prevFrameVisible_ so the NEXT // frame's compute dispatch can set the per-instance `previouslyVisible` - // flag (bit 3). Objects not visible this frame will skip HiZ next frame, - // avoiding false culls from stale depth data. + // flag (bit 3). We use a hysteresis counter instead of a binary flag to + // prevent a 1-frame-on / 1-frame-off oscillation: an object must be HiZ- + // culled for 2 consecutive frames before we stop considering it + // "previously visible". This eliminates doodad flicker near characters + // caused by stale depth data from character movement. if (gpuCullAvailable) { - prevFrameVisible_.resize(numInstances); - for (uint32_t i = 0; i < numInstances; ++i) - prevFrameVisible_[i] = visibility[i] ? 1u : 0u; + prevFrameVisible_.resize(numInstances, 0); + for (uint32_t i = 0; i < numInstances; ++i) { + if (visibility[i]) { + // Visible this frame — reset cull counter. + prevFrameVisible_[i] = 0; + } else { + // Culled this frame — increment counter (cap at 3 to avoid overflow). + prevFrameVisible_[i] = std::min(prevFrameVisible_[i] + 1, 3); + } + } } else { - // No GPU cull data — conservatively mark all as visible - prevFrameVisible_.assign(static_cast(instances.size()), 1u); + // No GPU cull data — conservatively mark all as visible (counter = 0). + prevFrameVisible_.assign(static_cast(instances.size()), 0); } // If GPU culling was not dispatched, fallback: compute distances on CPU @@ -818,12 +836,12 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const float worldRadius = instance.cachedBoundRadius * instance.scale; float cullRadius = worldRadius; if (instance.cachedDisableAnimation) cullRadius = std::max(cullRadius, 3.0f); - float effDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / 12.0f); + float effDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / rendering::M2_CULL_RADIUS_SCALE_DIVISOR); if (instance.cachedDisableAnimation) effDistSq *= 2.6f; if (instance.cachedIsGroundDetail) effDistSq *= 0.9f; if (distSqTest > effDistSq) continue; - float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f); + float paddedRadius = std::max(cullRadius * rendering::M2_PADDED_RADIUS_SCALE, cullRadius + rendering::M2_PADDED_RADIUS_MIN_MARGIN); if (cullRadius > 0.0f && !frustum.intersectsSphere(instance.position, paddedRadius)) continue; } @@ -833,7 +851,7 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const float worldRadius = instance.cachedBoundRadius * instance.scale; float cullRadius = worldRadius; if (instance.cachedDisableAnimation) cullRadius = std::max(cullRadius, 3.0f); - float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / 12.0f); + float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / rendering::M2_CULL_RADIUS_SCALE_DIVISOR); if (instance.cachedDisableAnimation) effectiveMaxDistSq *= 2.6f; if (instance.cachedIsGroundDetail) effectiveMaxDistSq *= 0.9f; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index 8e2f6fa2..c76a86be 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -531,19 +531,24 @@ bool Renderer::initialize(core::Window* win) { lensFlare = nullptr; weather = std::make_unique(); - weather->initialize(vkCtx, perFrameSetLayout); + if (!weather->initialize(vkCtx, perFrameSetLayout)) + LOG_WARNING("Weather effect initialization failed (non-fatal)"); lightning = std::make_unique(); - lightning->initialize(vkCtx, perFrameSetLayout); + if (!lightning->initialize(vkCtx, perFrameSetLayout)) + LOG_WARNING("Lightning effect initialization failed (non-fatal)"); swimEffects = std::make_unique(); - swimEffects->initialize(vkCtx, perFrameSetLayout); + if (!swimEffects->initialize(vkCtx, perFrameSetLayout)) + LOG_WARNING("Swim effect initialization failed (non-fatal)"); mountDust = std::make_unique(); - mountDust->initialize(vkCtx, perFrameSetLayout); + if (!mountDust->initialize(vkCtx, perFrameSetLayout)) + LOG_WARNING("Mount dust effect initialization failed (non-fatal)"); chargeEffect = std::make_unique(); - chargeEffect->initialize(vkCtx, perFrameSetLayout); + if (!chargeEffect->initialize(vkCtx, perFrameSetLayout)) + LOG_WARNING("Charge effect initialization failed (non-fatal)"); levelUpEffect = std::make_unique(); @@ -1451,7 +1456,8 @@ void Renderer::runDeferredWorldInitStep(float deltaTime) { if (audioCoordinator_->getMovementSoundManager()) audioCoordinator_->getMovementSoundManager()->initialize(cachedAssetManager); break; case 5: - if (questMarkerRenderer) questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); + if (questMarkerRenderer && !questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager)) + LOG_WARNING("Quest marker renderer re-init failed (non-fatal)"); break; default: deferredWorldInitPending_ = false; @@ -1930,7 +1936,8 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s // Create M2, WMO, and Character renderers if (!m2Renderer) { m2Renderer = std::make_unique(); - m2Renderer->initialize(vkCtx, perFrameSetLayout, assetManager); + if (!m2Renderer->initialize(vkCtx, perFrameSetLayout, assetManager)) + LOG_ERROR("M2Renderer initialization failed"); if (swimEffects) { swimEffects->setM2Renderer(m2Renderer.get()); } @@ -1959,21 +1966,26 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s } if (!wmoRenderer) { wmoRenderer = std::make_unique(); - wmoRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); + if (!wmoRenderer->initialize(vkCtx, perFrameSetLayout, assetManager)) + LOG_ERROR("WMORenderer initialization failed"); if (shadowRenderPass != VK_NULL_HANDLE) { - wmoRenderer->initializeShadow(shadowRenderPass); + if (!wmoRenderer->initializeShadow(shadowRenderPass)) + LOG_WARNING("WMO shadow pipeline initialization failed"); } } // Initialize shadow pipelines for M2 if not yet done if (m2Renderer && shadowRenderPass != VK_NULL_HANDLE && !m2Renderer->hasShadowPipeline()) { - m2Renderer->initializeShadow(shadowRenderPass); + if (!m2Renderer->initializeShadow(shadowRenderPass)) + LOG_WARNING("M2 shadow pipeline initialization failed"); } if (!characterRenderer) { characterRenderer = std::make_unique(); - characterRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); + if (!characterRenderer->initialize(vkCtx, perFrameSetLayout, assetManager)) + LOG_ERROR("CharacterRenderer initialization failed"); if (shadowRenderPass != VK_NULL_HANDLE) { - characterRenderer->initializeShadow(shadowRenderPass); + if (!characterRenderer->initializeShadow(shadowRenderPass)) + LOG_WARNING("Character shadow pipeline initialization failed"); } } @@ -2068,7 +2080,8 @@ bool Renderer::initializeRenderers(pipeline::AssetManager* assetManager, const s audioCoordinator_->getMovementSoundManager()->initialize(assetManager); } if (questMarkerRenderer) { - questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, assetManager); + if (!questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, assetManager)) + LOG_WARNING("Quest marker renderer initialization failed (non-fatal)"); } if (envFlagEnabled("WOWEE_PREWARM_ZONE_MUSIC", false)) { @@ -2261,7 +2274,8 @@ bool Renderer::loadTerrainArea(const std::string& mapName, int centerX, int cent audioCoordinator_->getMovementSoundManager()->initialize(cachedAssetManager); } if (questMarkerRenderer && cachedAssetManager) { - questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager); + if (!questMarkerRenderer->initialize(vkCtx, perFrameSetLayout, cachedAssetManager)) + LOG_WARNING("Quest marker renderer re-init failed (non-fatal)"); } } else { deferredWorldInitPending_ = true; diff --git a/src/rendering/terrain_manager.cpp b/src/rendering/terrain_manager.cpp index d67e89ea..a0e5aaef 100644 --- a/src/rendering/terrain_manager.cpp +++ b/src/rendering/terrain_manager.cpp @@ -868,7 +868,8 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) { // Ensure M2 renderer has asset manager if (m2Renderer && assetManager) { - m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); + if (!m2Renderer->initialize(nullptr, VK_NULL_HANDLE, assetManager)) + LOG_WARNING("M2Renderer terrain re-init failed"); } ft.phase = FinalizationPhase::M2_MODELS; @@ -952,7 +953,8 @@ bool TerrainManager::advanceFinalization(FinalizingTile& ft) { case FinalizationPhase::WMO_MODELS: { // Upload multiple WMO models per call (batched GPU uploads) if (wmoRenderer && assetManager) { - wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager); + if (!wmoRenderer->initialize(nullptr, VK_NULL_HANDLE, assetManager)) + LOG_WARNING("WMORenderer terrain re-init failed"); // Set pre-decoded BLP cache and defer normal maps during streaming wmoRenderer->setPredecodedBLPCache(&pending->preloadedWMOTextures); wmoRenderer->setDeferNormalMaps(true); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index d480665a..15014c90 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -98,6 +98,31 @@ namespace { return "Unknown"; } + // Draw a four-edge screen vignette (gradient overlay along each edge). + // Used for damage flash, low-health pulse, and level-up golden burst. + void drawScreenEdgeVignette(uint8_t r, uint8_t g, uint8_t b, + int alpha, float thicknessRatio) { + if (alpha <= 0) return; + ImDrawList* fg = ImGui::GetForegroundDrawList(); + const float W = ImGui::GetIO().DisplaySize.x; + const float H = ImGui::GetIO().DisplaySize.y; + const float thickness = std::min(W, H) * thicknessRatio; + const ImU32 edgeCol = IM_COL32(r, g, b, alpha); + const ImU32 fadeCol = IM_COL32(r, g, b, 0); + // Top + fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness), + edgeCol, edgeCol, fadeCol, fadeCol); + // Bottom + fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H), + fadeCol, fadeCol, edgeCol, edgeCol); + // Left + fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H), + edgeCol, fadeCol, fadeCol, edgeCol); + // Right + fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H), + fadeCol, edgeCol, edgeCol, fadeCol); + } + } namespace wowee { namespace ui { @@ -660,29 +685,8 @@ void GameScreen::render(game::GameHandler& gameHandler) { if (damageFlashAlpha_ > 0.0f) { damageFlashAlpha_ -= ImGui::GetIO().DeltaTime * 2.0f; if (damageFlashAlpha_ < 0.0f) damageFlashAlpha_ = 0.0f; - - // Draw four red gradient rectangles along each screen edge (vignette style) - ImDrawList* fg = ImGui::GetForegroundDrawList(); - ImGuiIO& io = ImGui::GetIO(); - const float W = io.DisplaySize.x; - const float H = io.DisplaySize.y; - const int alpha = static_cast(damageFlashAlpha_ * 100.0f); - const ImU32 edgeCol = IM_COL32(200, 0, 0, alpha); - const ImU32 fadeCol = IM_COL32(200, 0, 0, 0); - const float thickness = std::min(W, H) * 0.12f; - - // Top - fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness), - edgeCol, edgeCol, fadeCol, fadeCol); - // Bottom - fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H), - fadeCol, fadeCol, edgeCol, edgeCol); - // Left - fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H), - edgeCol, fadeCol, fadeCol, edgeCol); - // Right - fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H), - fadeCol, edgeCol, edgeCol, fadeCol); + drawScreenEdgeVignette(200, 0, 0, + static_cast(damageFlashAlpha_ * 100.0f), 0.12f); } } @@ -705,23 +709,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { float danger = (0.20f - hpPct) / 0.20f; float pulse = 0.55f + 0.45f * std::sin(static_cast(ImGui::GetTime()) * 9.4f); int alpha = static_cast(danger * pulse * 90.0f); // max ~90 alpha, subtle - if (alpha > 0) { - ImDrawList* fg = ImGui::GetForegroundDrawList(); - ImGuiIO& io = ImGui::GetIO(); - const float W = io.DisplaySize.x; - const float H = io.DisplaySize.y; - const float thickness = std::min(W, H) * 0.15f; - const ImU32 edgeCol = IM_COL32(200, 0, 0, alpha); - const ImU32 fadeCol = IM_COL32(200, 0, 0, 0); - fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness), - edgeCol, edgeCol, fadeCol, fadeCol); - fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H), - fadeCol, fadeCol, edgeCol, edgeCol); - fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H), - edgeCol, fadeCol, fadeCol, edgeCol); - fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H), - fadeCol, edgeCol, edgeCol, fadeCol); - } + drawScreenEdgeVignette(200, 0, 0, alpha, 0.15f); } } @@ -730,27 +718,14 @@ void GameScreen::render(game::GameHandler& gameHandler) { toastManager_.levelUpFlashAlpha -= ImGui::GetIO().DeltaTime * 1.0f; // fade over ~1 second if (toastManager_.levelUpFlashAlpha < 0.0f) toastManager_.levelUpFlashAlpha = 0.0f; - ImDrawList* fg = ImGui::GetForegroundDrawList(); - ImGuiIO& io = ImGui::GetIO(); - const float W = io.DisplaySize.x; - const float H = io.DisplaySize.y; const int alpha = static_cast(toastManager_.levelUpFlashAlpha * 160.0f); - const ImU32 goldEdge = IM_COL32(255, 210, 50, alpha); - const ImU32 goldFade = IM_COL32(255, 210, 50, 0); - const float thickness = std::min(W, H) * 0.18f; - - // Four golden gradient edges - fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness), - goldEdge, goldEdge, goldFade, goldFade); - fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H), - goldFade, goldFade, goldEdge, goldEdge); - fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H), - goldEdge, goldFade, goldFade, goldEdge); - fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H), - goldFade, goldEdge, goldEdge, goldFade); + drawScreenEdgeVignette(255, 210, 50, alpha, 0.18f); // "Level X!" text in the center during the first half of the animation if (toastManager_.levelUpFlashAlpha > 0.5f && toastManager_.levelUpDisplayLevel > 0) { + ImDrawList* fg = ImGui::GetForegroundDrawList(); + const float W = ImGui::GetIO().DisplaySize.x; + const float H = ImGui::GetIO().DisplaySize.y; char lvlText[32]; snprintf(lvlText, sizeof(lvlText), "Level %u!", toastManager_.levelUpDisplayLevel); ImVec2 ts = ImGui::CalcTextSize(lvlText); @@ -1053,11 +1028,11 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) { // Only fires for classes that use a stance bar; same slot ordering as // renderStanceBar: Warrior, DK, Druid, Rogue, Priest. if (ctrlDown) { - static const uint32_t warriorStances[] = { 2457, 71, 2458 }; - static const uint32_t dkPresences[] = { 48266, 48263, 48265 }; - static const uint32_t druidForms[] = { 5487, 9634, 768, 783, 1066, 24858, 33891, 33943, 40120 }; - static const uint32_t rogueForms[] = { 1784 }; - static const uint32_t priestForms[] = { 15473 }; + static constexpr uint32_t warriorStances[] = { 2457, 71, 2458 }; + static constexpr uint32_t dkPresences[] = { 48266, 48263, 48265 }; + static constexpr uint32_t druidForms[] = { 5487, 9634, 768, 783, 1066, 24858, 33891, 33943, 40120 }; + static constexpr uint32_t rogueForms[] = { 1784 }; + static constexpr uint32_t priestForms[] = { 15473 }; const uint32_t* stArr = nullptr; int stCnt = 0; switch (gameHandler.getPlayerClass()) { case 1: stArr = warriorStances; stCnt = 3; break; diff --git a/src/ui/game_screen_frames.cpp b/src/ui/game_screen_frames.cpp index 6ecca46f..867158f3 100644 --- a/src/ui/game_screen_frames.cpp +++ b/src/ui/game_screen_frames.cpp @@ -689,7 +689,7 @@ void GameScreen::renderPetFrame(game::GameHandler& gameHandler) { // Find each react-type slot in the action bar by known built-in IDs: // 1=Passive, 4=Defensive, 6=Aggressive (WoW wire protocol) - static const uint32_t kReactActionIds[] = { 1u, 4u, 6u }; + static constexpr uint32_t kReactActionIds[] = { 1u, 4u, 6u }; uint32_t reactSlotVals[3] = { 0, 0, 0 }; const int slotTotal = game::GameHandler::PET_ACTION_BAR_SLOTS; for (int i = 0; i < slotTotal; ++i) { diff --git a/src/ui/window_manager.cpp b/src/ui/window_manager.cpp index 0ddbedeb..767e2806 100644 --- a/src/ui/window_manager.cpp +++ b/src/ui/window_manager.cpp @@ -4172,7 +4172,7 @@ void WindowManager::renderSkillsWindow(game::GameHandler& gameHandler) { }; // Collect handled categories to fall back to "Other" for unknowns - static const uint32_t kKnownCats[] = {11, 9, 7, 6, 8, 5}; + static constexpr uint32_t kKnownCats[] = {11, 9, 7, 6, 8, 5}; // Redirect unknown categories into bucket 0 for (auto& [cat, vec] : byCategory) {