mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 01:23:51 +00:00
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:
parent
2e8856bacd
commit
97106bd6ae
41 changed files with 849 additions and 424 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public:
|
|||
~AudioEngine();
|
||||
|
||||
// Initialization
|
||||
bool initialize();
|
||||
[[nodiscard]] bool initialize();
|
||||
void shutdown();
|
||||
bool isInitialized() const { return initialized_; }
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
126
include/game/protocol_constants.hpp
Normal file
126
include/game/protocol_constants.hpp
Normal 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
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
65
include/game/warden_constants.hpp
Normal file
65
include/game/warden_constants.hpp
Normal 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
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ public:
|
|||
ChargeEffect();
|
||||
~ChargeEffect();
|
||||
|
||||
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
|
||||
[[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
|
||||
void shutdown();
|
||||
void recreatePipelines();
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ public:
|
|||
Lightning();
|
||||
~Lightning();
|
||||
|
||||
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
|
||||
[[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
|
||||
void shutdown();
|
||||
void recreatePipelines();
|
||||
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public:
|
|||
MountDust();
|
||||
~MountDust();
|
||||
|
||||
bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
|
||||
[[nodiscard]] bool initialize(VkContext* ctx, VkDescriptorSetLayout perFrameLayout);
|
||||
void shutdown();
|
||||
void recreatePipelines();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
74
include/rendering/render_constants.hpp
Normal file
74
include/rendering/render_constants.hpp
Normal 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.05–0.25.
|
||||
constexpr float HIZ_VP_DIFF_THRESHOLD = 0.5f;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Smoke / spark particle tuning
|
||||
// ---------------------------------------------------------------------------
|
||||
constexpr float SMOKE_OFFSET_XY_MIN = -0.4f;
|
||||
constexpr float SMOKE_OFFSET_XY_MAX = 0.4f;
|
||||
constexpr float SMOKE_VEL_Z_MIN = 3.0f;
|
||||
constexpr float SMOKE_VEL_Z_MAX = 5.0f;
|
||||
constexpr float SMOKE_LIFETIME_MIN = 4.0f;
|
||||
constexpr float SMOKE_LIFETIME_MAX = 7.0f;
|
||||
constexpr float SMOKE_Z_VEL_DAMPING = 0.98f;
|
||||
constexpr float SMOKE_SIZE_START = 1.0f;
|
||||
constexpr float SMOKE_SIZE_GROWTH = 2.5f;
|
||||
|
||||
constexpr int SPARK_PROBABILITY_DENOM = 8; // 1-in-8 chance per frame
|
||||
constexpr float SPARK_LIFE_BASE = 0.8f;
|
||||
constexpr float SPARK_LIFE_RANGE = 1.2f;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Character rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
// Default frustum-cull radius when model bounds are unavailable (world units).
|
||||
// 4.0 covers Tauren, mounted characters, and most creature models.
|
||||
constexpr float DEFAULT_CHARACTER_CULL_RADIUS = 4.0f;
|
||||
|
||||
} // namespace rendering
|
||||
} // namespace wowee
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
68
include/ui/ui_constants.hpp
Normal file
68
include/ui/ui_constants.hpp
Normal 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
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.05–0.25. A fast rotation or
|
||||
// zoom easily exceeds 0.5. The previous threshold (0.15) caused
|
||||
// the HiZ pass to toggle on/off every other frame during normal
|
||||
// gameplay, which produced global M2 doodad flicker.
|
||||
if (maxDiff > rendering::HIZ_VP_DIFF_THRESHOLD) hizSafe = false;
|
||||
}
|
||||
|
||||
ubo->hizEnabled = hizSafe ? 1u : 0u;
|
||||
|
|
@ -656,11 +662,11 @@ void M2Renderer::dispatchCullCompute(VkCommandBuffer cmd, uint32_t frameIndex, c
|
|||
if (inst.cachedDisableAnimation) {
|
||||
cullRadius = std::max(cullRadius, 3.0f);
|
||||
}
|
||||
float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / 12.0f);
|
||||
float effectiveMaxDistSq = maxRenderDistanceSq * std::max(1.0f, cullRadius / rendering::M2_CULL_RADIUS_SCALE_DIVISOR);
|
||||
if (inst.cachedDisableAnimation) effectiveMaxDistSq *= 2.6f;
|
||||
if (inst.cachedIsGroundDetail) effectiveMaxDistSq *= 0.9f;
|
||||
|
||||
float paddedRadius = std::max(cullRadius * 1.5f, cullRadius + 3.0f);
|
||||
float paddedRadius = std::max(cullRadius * rendering::M2_PADDED_RADIUS_SCALE, cullRadius + rendering::M2_PADDED_RADIUS_MIN_MARGIN);
|
||||
|
||||
uint32_t flags = 0;
|
||||
if (inst.cachedIsValid) flags |= 1u;
|
||||
|
|
@ -668,7 +674,9 @@ void M2Renderer::dispatchCullCompute(VkCommandBuffer cmd, uint32_t frameIndex, c
|
|||
if (inst.cachedIsInvisibleTrap) flags |= 4u;
|
||||
// Bit 3: previouslyVisible — the shader skips HiZ for objects
|
||||
// that were NOT rendered last frame (no reliable depth data).
|
||||
if (i < prevFrameVisible_.size() && prevFrameVisible_[i])
|
||||
// Hysteresis: treat as "previously visible" unless culled for
|
||||
// 2+ consecutive frames, preventing single-frame false-cull flicker.
|
||||
if (i < prevFrameVisible_.size() && prevFrameVisible_[i] < 2)
|
||||
flags |= 8u;
|
||||
|
||||
input[i].sphere = glm::vec4(inst.position, paddedRadius);
|
||||
|
|
@ -756,15 +764,25 @@ void M2Renderer::render(VkCommandBuffer cmd, VkDescriptorSet perFrameSet, const
|
|||
|
||||
// Snapshot the GPU visibility results into prevFrameVisible_ so the NEXT
|
||||
// frame's compute dispatch can set the per-instance `previouslyVisible`
|
||||
// flag (bit 3). Objects not visible this frame will skip HiZ next frame,
|
||||
// avoiding false culls from stale depth data.
|
||||
// flag (bit 3). We use a hysteresis counter instead of a binary flag to
|
||||
// prevent a 1-frame-on / 1-frame-off oscillation: an object must be HiZ-
|
||||
// culled for 2 consecutive frames before we stop considering it
|
||||
// "previously visible". This eliminates doodad flicker near characters
|
||||
// caused by stale depth data from character movement.
|
||||
if (gpuCullAvailable) {
|
||||
prevFrameVisible_.resize(numInstances);
|
||||
for (uint32_t i = 0; i < numInstances; ++i)
|
||||
prevFrameVisible_[i] = visibility[i] ? 1u : 0u;
|
||||
prevFrameVisible_.resize(numInstances, 0);
|
||||
for (uint32_t i = 0; i < numInstances; ++i) {
|
||||
if (visibility[i]) {
|
||||
// Visible this frame — reset cull counter.
|
||||
prevFrameVisible_[i] = 0;
|
||||
} else {
|
||||
// Culled this frame — increment counter (cap at 3 to avoid overflow).
|
||||
prevFrameVisible_[i] = std::min<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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue