fix(render): code quality cleanup

Magic number elimination:
- Create protocol_constants.hpp, warden_constants.hpp,
  render_constants.hpp, ui_constants.hpp
- Replace ~55 magic numbers across game_handler, warden_handler,
  m2_renderer_render

Reduce nesting depth:
- Extract 5 parseEffect* methods from handleSpellLogExecute
  (max indent 52 → 16 cols)
- Extract resolveSpellSchool/playSpellCastSound/playSpellImpactSound
  from 3× duplicate audio blocks in handleSpellGo
- Flatten SMSG_INVENTORY_CHANGE_FAILURE with early-return guards
- Extract drawScreenEdgeVignette() for 3 duplicate vignette blocks

DRY extract patterns:
- Replace 12 compound expansion checks with isPreWotlk() across
  movement_handler (9), chat_handler (1), social_handler (1)

const to constexpr:
- Promote 23+ static const arrays/scalars to static constexpr across
  12 source files

Error handling:
- Convert PIN auth from exceptions to std::optional<PinProof>
- Add [[nodiscard]] to 15+ initialize/parse methods
- Wrap ~20 unchecked initialize() calls with LOG_WARNING/LOG_ERROR

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
Pavel Okhlopkov 2026-04-06 22:43:13 +03:00
parent 2e8856bacd
commit 97106bd6ae
41 changed files with 849 additions and 424 deletions

View file

@ -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);

View file

@ -25,7 +25,7 @@ public:
~AudioEngine();
// Initialization
bool initialize();
[[nodiscard]] bool initialize();
void shutdown();
bool isInitialized() const { return initialized_; }

View file

@ -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;

View file

@ -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

View file

@ -2,6 +2,7 @@
#include <array>
#include <cstdint>
#include <optional>
#include <string>
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<uint8_t, 16>& serverSalt);
// Returns std::nullopt on invalid input (bad length, non-digit chars, or grid corruption).
[[nodiscard]] std::optional<PinProof> computePinProof(
const std::string& pinDigits,
uint32_t pinGridSeed,
const std::array<uint8_t, 16>& serverSalt);
} // namespace auth
} // namespace wowee

View file

@ -0,0 +1,126 @@
#pragma once
#include <cstdint>
// 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

View file

@ -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 <array>
#include <chrono>
@ -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;

View file

@ -0,0 +1,65 @@
#pragma once
#include <cstdint>
// 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

View file

@ -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);

View file

@ -22,7 +22,7 @@ public:
ChargeEffect();
~ChargeEffect();
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
[[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
void recreatePipelines();

View file

@ -26,7 +26,7 @@ public:
Lightning();
~Lightning();
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
[[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
void recreatePipelines();

View file

@ -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; }
/**

View file

@ -16,7 +16,7 @@ public:
MountDust();
~MountDust();
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
[[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
void shutdown();
void recreatePipelines();

View file

@ -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();

View file

@ -0,0 +1,74 @@
#pragma once
#include <cstdint>
// 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.050.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

View file

@ -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,

View file

@ -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();
/**

View file

@ -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

View file

@ -0,0 +1,68 @@
#pragma once
#include <cstdint>
// 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

View file

@ -265,16 +265,15 @@ void AuthHandler::sendLogonProof() {
const std::array<uint8_t, 20>* 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.

View file

@ -1,8 +1,9 @@
#include "auth/pin_auth.hpp"
#include "auth/crypto.hpp"
#include "core/logger.hpp"
#include <algorithm>
#include <optional>
#include <random>
#include <stdexcept>
#include <vector>
namespace wowee {
@ -46,7 +47,7 @@ static std::array<uint8_t, 10> remapPinGrid(uint32_t seed) {
return remapped;
}
static std::vector<uint8_t> randomizePinDigits(const std::string& pinDigits,
static std::optional<std::vector<uint8_t>> randomizePinDigits(const std::string& pinDigits,
const std::array<uint8_t, 10>& remapped) {
// Transforms each pin digit into an index in the remapped permutation.
// Based on:
@ -61,7 +62,8 @@ static std::vector<uint8_t> 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<uint8_t> randomizePinDigits(const std::string& pinDigits,
return out;
}
PinProof computePinProof(const std::string& pinDigits,
std::optional<PinProof> computePinProof(const std::string& pinDigits,
uint32_t pinGridSeed,
const std::array<uint8_t, 16>& 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<uint8_t> 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;

View file

@ -137,7 +137,8 @@ bool Application::initialize() {
// Create and initialize audio coordinator (owns all audio managers)
audioCoordinator_ = std::make_unique<audio::AudioCoordinator>();
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)");
}
}
}

View file

@ -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");

View file

@ -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 <glm/gtx/quaternion.hpp>
#include <algorithm>
@ -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::milliseconds>(
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<Unit>(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<Unit>(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

View file

@ -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();
}
};

View file

@ -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) {

View file

@ -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();

View file

@ -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<uint8_t>(drainPower);
if (drainTarget == playerGuid)
owner_.addCombatText(CombatTextEntry::POWER_DRAIN,
static_cast<int32_t>(drainAmount), spellId, false,
powerByte, caster, drainTarget);
if (!isPlayerCaster) continue;
if (drainTarget != playerGuid)
owner_.addCombatText(CombatTextEntry::POWER_DRAIN,
static_cast<int32_t>(drainAmount), spellId, true,
powerByte, caster, drainTarget);
if (drainMult <= 0.0f || !std::isfinite(drainMult)) continue;
const uint32_t gained = static_cast<uint32_t>(
std::lround(static_cast<double>(drainAmount) * static_cast<double>(drainMult)));
if (gained > 0)
owner_.addCombatText(CombatTextEntry::ENERGIZE,
static_cast<int32_t>(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<int32_t>(leechAmount), spellId, false, 0,
caster, leechTarget);
} else if (isPlayerCaster) {
owner_.addCombatText(CombatTextEntry::SPELL_DAMAGE,
static_cast<int32_t>(leechAmount), spellId, true, 0,
caster, leechTarget);
}
if (!isPlayerCaster || leechMult <= 0.0f || !std::isfinite(leechMult)) continue;
const uint32_t gained = static_cast<uint32_t>(
std::lround(static_cast<double>(leechAmount) * static_cast<double>(leechMult)));
if (gained > 0)
owner_.addCombatText(CombatTextEntry::HEAL,
static_cast<int32_t>(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<int32_t>(drainAmount), exeSpellId, false,
static_cast<uint8_t>(drainPower),
exeCaster, drainTarget);
if (isPlayerCaster) {
if (drainTarget != owner_.getPlayerGuid()) {
owner_.addCombatText(CombatTextEntry::POWER_DRAIN, static_cast<int32_t>(drainAmount), exeSpellId, true,
static_cast<uint8_t>(drainPower), exeCaster, drainTarget);
}
if (drainMult > 0.0f && std::isfinite(drainMult)) {
const uint32_t gainedAmount = static_cast<uint32_t>(
std::lround(static_cast<double>(drainAmount) * static_cast<double>(drainMult)));
if (gainedAmount > 0) {
owner_.addCombatText(CombatTextEntry::ENERGIZE, static_cast<int32_t>(gainedAmount), exeSpellId, true,
static_cast<uint8_t>(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<int32_t>(leechAmount), exeSpellId, false, 0,
exeCaster, leechTarget);
} else if (isPlayerCaster) {
owner_.addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast<int32_t>(leechAmount), exeSpellId, true, 0,
exeCaster, leechTarget);
}
if (isPlayerCaster && leechMult > 0.0f && std::isfinite(leechMult)) {
const uint32_t gainedAmount = static_cast<uint32_t>(
std::lround(static_cast<double>(leechAmount) * static_cast<double>(leechMult)));
if (gainedAmount > 0) {
owner_.addCombatText(CombatTextEntry::HEAL, static_cast<int32_t>(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();

View file

@ -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;
}

View file

@ -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);
}

View file

@ -10,6 +10,7 @@
#include "core/application.hpp"
#include "pipeline/asset_manager.hpp"
#include "core/logger.hpp"
#include "game/warden_constants.hpp"
#include <algorithm>
#include <cctype>
#include <chrono>
@ -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<uint8_t> resp = { 0x00 }; // WARDEN_CMSG_MODULE_MISSING
// Respond with MODULE_MISSING to request the module data
std::vector<uint8_t> 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<uint8_t> resp = { 0x01 }; // WARDEN_CMSG_MODULE_OK
// Send MODULE_OK
std::vector<uint8_t> 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<uint8_t> 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<uint32_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
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,

View file

@ -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,

View file

@ -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);

View file

@ -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
};

View file

@ -2130,7 +2130,9 @@ void CharacterRenderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet,
}
const float renderRadius = static_cast<float>(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;

View file

@ -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<float>(mdl.sequences[0].duration);
instance.animTime = static_cast<float>(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<float>(mdl2.sequences[0].duration);
instance.animTime = static_cast<float>(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<float> distXY(-0.4f, 0.4f);
std::uniform_real_distribution<float> distXY(rendering::SMOKE_OFFSET_XY_MIN, rendering::SMOKE_OFFSET_XY_MAX);
std::uniform_real_distribution<float> distVelXY(-0.3f, 0.3f);
std::uniform_real_distribution<float> distVelZ(3.0f, 5.0f);
std::uniform_real_distribution<float> distLife(4.0f, 7.0f);
std::uniform_real_distribution<float> distVelZ(rendering::SMOKE_VEL_Z_MIN, rendering::SMOKE_VEL_Z_MAX);
std::uniform_real_distribution<float> distLife(rendering::SMOKE_LIFETIME_MIN, rendering::SMOKE_LIFETIME_MAX);
std::uniform_real_distribution<float> 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<float>(smokeRng() % 100) / 100.0f * 1.2f;
p.maxLife = rendering::SPARK_LIFE_BASE + static_cast<float>(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<float>(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<float>(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.050.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<uint8_t>(prevFrameVisible_[i] + 1, 3);
}
}
} else {
// No GPU cull data — conservatively mark all as visible
prevFrameVisible_.assign(static_cast<size_t>(instances.size()), 1u);
// No GPU cull data — conservatively mark all as visible (counter = 0).
prevFrameVisible_.assign(static_cast<size_t>(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;

View file

@ -531,19 +531,24 @@ bool Renderer::initialize(core::Window* win) {
lensFlare = nullptr;
weather = std::make_unique<Weather>();
weather->initialize(vkCtx, perFrameSetLayout);
if (!weather->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Weather effect initialization failed (non-fatal)");
lightning = std::make_unique<Lightning>();
lightning->initialize(vkCtx, perFrameSetLayout);
if (!lightning->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Lightning effect initialization failed (non-fatal)");
swimEffects = std::make_unique<SwimEffects>();
swimEffects->initialize(vkCtx, perFrameSetLayout);
if (!swimEffects->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Swim effect initialization failed (non-fatal)");
mountDust = std::make_unique<MountDust>();
mountDust->initialize(vkCtx, perFrameSetLayout);
if (!mountDust->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Mount dust effect initialization failed (non-fatal)");
chargeEffect = std::make_unique<ChargeEffect>();
chargeEffect->initialize(vkCtx, perFrameSetLayout);
if (!chargeEffect->initialize(vkCtx, perFrameSetLayout))
LOG_WARNING("Charge effect initialization failed (non-fatal)");
levelUpEffect = std::make_unique<LevelUpEffect>();
@ -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>();
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>();
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>();
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;

View file

@ -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);

View file

@ -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<int>(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<int>(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<float>(ImGui::GetTime()) * 9.4f);
int alpha = static_cast<int>(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<int>(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;

View file

@ -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) {

View file

@ -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) {