#include "game/game_handler.hpp" #include "game/game_utils.hpp" #include "game/chat_handler.hpp" #include "game/movement_handler.hpp" #include "game/combat_handler.hpp" #include "game/spell_handler.hpp" #include "game/inventory_handler.hpp" #include "game/social_handler.hpp" #include "game/quest_handler.hpp" #include "game/warden_handler.hpp" #include "game/packet_parsers.hpp" #include "game/transport_manager.hpp" #include "game/warden_crypto.hpp" #include "game/warden_memory.hpp" #include "game/warden_module.hpp" #include "game/opcodes.hpp" #include "game/update_field_table.hpp" #include "game/expansion_profile.hpp" #include "rendering/renderer.hpp" #include "rendering/spell_visual_system.hpp" #include "audio/audio_coordinator.hpp" #include "audio/activity_sound_manager.hpp" #include "audio/combat_sound_manager.hpp" #include "audio/spell_sound_manager.hpp" #include "audio/ui_sound_manager.hpp" #include "pipeline/dbc_layout.hpp" #include "network/world_socket.hpp" #include "network/packet.hpp" #include "auth/crypto.hpp" #include "core/coordinates.hpp" #include "core/application.hpp" #include "pipeline/asset_manager.hpp" #include "pipeline/dbc_loader.hpp" #include "core/logger.hpp" #include "rendering/animation/animation_ids.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace wowee { namespace game { namespace { const char* worldStateName(WorldState state) { switch (state) { case WorldState::DISCONNECTED: return "DISCONNECTED"; case WorldState::CONNECTING: return "CONNECTING"; case WorldState::CONNECTED: return "CONNECTED"; case WorldState::CHALLENGE_RECEIVED: return "CHALLENGE_RECEIVED"; case WorldState::AUTH_SENT: return "AUTH_SENT"; case WorldState::AUTHENTICATED: return "AUTHENTICATED"; case WorldState::READY: return "READY"; case WorldState::CHAR_LIST_REQUESTED: return "CHAR_LIST_REQUESTED"; case WorldState::CHAR_LIST_RECEIVED: return "CHAR_LIST_RECEIVED"; case WorldState::ENTERING_WORLD: return "ENTERING_WORLD"; case WorldState::IN_WORLD: return "IN_WORLD"; case WorldState::FAILED: return "FAILED"; } return "UNKNOWN"; } } // end anonymous namespace void GameHandler::handleAuthChallenge(network::Packet& packet) { LOG_INFO("Handling SMSG_AUTH_CHALLENGE"); AuthChallengeData challenge; if (!AuthChallengeParser::parse(packet, challenge)) { fail("Failed to parse SMSG_AUTH_CHALLENGE"); return; } if (!challenge.isValid()) { fail("Invalid auth challenge data"); return; } // Store server seed serverSeed = challenge.serverSeed; LOG_DEBUG("Server seed: 0x", std::hex, serverSeed, std::dec); setState(WorldState::CHALLENGE_RECEIVED); // Send authentication session sendAuthSession(); } void GameHandler::sendAuthSession() { LOG_INFO("Sending CMSG_AUTH_SESSION"); // Build authentication packet auto packet = AuthSessionPacket::build( build, accountName, clientSeed, sessionKey, serverSeed, realmId_ ); LOG_DEBUG("CMSG_AUTH_SESSION packet size: ", packet.getSize(), " bytes"); // Send packet (unencrypted - this is the last unencrypted packet) socket->send(packet); // Enable encryption IMMEDIATELY after sending AUTH_SESSION // AzerothCore enables encryption before sending AUTH_RESPONSE, // so we need to be ready to decrypt the response LOG_INFO("Enabling encryption immediately after AUTH_SESSION"); socket->initEncryption(sessionKey, build); setState(WorldState::AUTH_SENT); LOG_INFO("CMSG_AUTH_SESSION sent, encryption enabled, waiting for AUTH_RESPONSE..."); } void GameHandler::handleAuthResponse(network::Packet& packet) { LOG_WARNING("Handling SMSG_AUTH_RESPONSE, size=", packet.getSize()); AuthResponseData response; if (!AuthResponseParser::parse(packet, response)) { fail("Failed to parse SMSG_AUTH_RESPONSE"); return; } if (!response.isSuccess()) { std::string reason = std::string("Authentication failed: ") + getAuthResultString(response.result); fail(reason); return; } // Encryption was already enabled after sending AUTH_SESSION LOG_INFO("AUTH_RESPONSE OK - world authentication successful"); setState(WorldState::AUTHENTICATED); LOG_INFO("========================================"); LOG_INFO(" WORLD AUTHENTICATION SUCCESSFUL!"); LOG_INFO("========================================"); LOG_INFO("Connected to world server"); LOG_INFO("Ready for character operations"); setState(WorldState::READY); // Request character list automatically requestCharacterList(); // Call success callback if (onSuccess) { onSuccess(); } } void GameHandler::requestCharacterList() { if (requiresWarden_) { // Gate already surfaced via failure callback/chat; avoid per-frame warning spam. wardenCharEnumBlockedLogged_ = true; return; } if (state == WorldState::FAILED || !socket || !socket->isConnected()) { return; } if (state != WorldState::READY && state != WorldState::AUTHENTICATED && state != WorldState::CHAR_LIST_RECEIVED) { LOG_WARNING("Cannot request character list in state: ", worldStateName(state)); return; } LOG_INFO("Requesting character list from server..."); // Prevent the UI from showing/selecting stale characters while we wait for the new SMSG_CHAR_ENUM. // This matters after character create/delete where the old list can linger for a few frames. characters.clear(); // Build CMSG_CHAR_ENUM packet (no body, just opcode) auto packet = CharEnumPacket::build(); // Send packet socket->send(packet); setState(WorldState::CHAR_LIST_REQUESTED); LOG_INFO("CMSG_CHAR_ENUM sent, waiting for character list..."); } void GameHandler::handleCharEnum(network::Packet& packet) { LOG_INFO("Handling SMSG_CHAR_ENUM"); CharEnumResponse response; // IMPORTANT: Do not infer packet formats from numeric build alone. // Turtle WoW uses a "high" build but classic-era world packet formats. bool parsed = packetParsers_ ? packetParsers_->parseCharEnum(packet, response) : CharEnumParser::parse(packet, response); if (!parsed) { fail("Failed to parse SMSG_CHAR_ENUM"); return; } // Store characters characters = response.characters; setState(WorldState::CHAR_LIST_RECEIVED); LOG_INFO("========================================"); LOG_INFO(" CHARACTER LIST RECEIVED"); LOG_INFO("========================================"); LOG_INFO("Found ", characters.size(), " character(s)"); if (characters.empty()) { LOG_INFO("No characters on this account"); } else { LOG_INFO("Characters:"); for (size_t i = 0; i < characters.size(); ++i) { const auto& character = characters[i]; LOG_INFO(" [", i + 1, "] ", character.name); LOG_INFO(" GUID: 0x", std::hex, character.guid, std::dec); LOG_INFO(" ", getRaceName(character.race), " ", getClassName(character.characterClass)); LOG_INFO(" Level ", static_cast(character.level)); } } LOG_INFO("Ready to select character"); } void GameHandler::createCharacter(const CharCreateData& data) { // Online mode: send packet to server if (!socket) { LOG_WARNING("Cannot create character: not connected"); if (charCreateCallback_) { charCreateCallback_(false, "Not connected to server"); } return; } if (requiresWarden_) { std::string msg = "Server requires anti-cheat/Warden; character creation blocked."; LOG_WARNING("Blocking CMSG_CHAR_CREATE while Warden gate is active"); if (charCreateCallback_) { charCreateCallback_(false, msg); } return; } if (state != WorldState::CHAR_LIST_RECEIVED) { std::string msg = "Character list not ready yet. Wait for SMSG_CHAR_ENUM."; LOG_WARNING("Blocking CMSG_CHAR_CREATE in state=", worldStateName(state), " (awaiting CHAR_LIST_RECEIVED)"); if (charCreateCallback_) { charCreateCallback_(false, msg); } return; } auto packet = CharCreatePacket::build(data); socket->send(packet); LOG_INFO("CMSG_CHAR_CREATE sent for: ", data.name); } void GameHandler::handleCharCreateResponse(network::Packet& packet) { CharCreateResponseData data; if (!CharCreateResponseParser::parse(packet, data)) { LOG_ERROR("Failed to parse SMSG_CHAR_CREATE"); return; } if (data.result == CharCreateResult::SUCCESS || data.result == CharCreateResult::IN_PROGRESS) { LOG_INFO("Character created successfully (code=", static_cast(data.result), ")"); requestCharacterList(); if (charCreateCallback_) { charCreateCallback_(true, "Character created!"); } } else { std::string msg; switch (data.result) { case CharCreateResult::CHAR_ERROR: msg = "Server error"; break; case CharCreateResult::FAILED: msg = "Creation failed"; break; case CharCreateResult::NAME_IN_USE: msg = "Name already in use"; break; case CharCreateResult::DISABLED: msg = "Character creation disabled"; break; case CharCreateResult::PVP_TEAMS_VIOLATION: msg = "PvP faction violation"; break; case CharCreateResult::SERVER_LIMIT: msg = "Server character limit reached"; break; case CharCreateResult::ACCOUNT_LIMIT: msg = "Account character limit reached"; break; case CharCreateResult::SERVER_QUEUE: msg = "Server is queued"; break; case CharCreateResult::ONLY_EXISTING: msg = "Only existing characters allowed"; break; case CharCreateResult::EXPANSION: msg = "Expansion required"; break; case CharCreateResult::EXPANSION_CLASS: msg = "Expansion required for this class"; break; case CharCreateResult::LEVEL_REQUIREMENT: msg = "Level requirement not met"; break; case CharCreateResult::UNIQUE_CLASS_LIMIT: msg = "Unique class limit reached"; break; case CharCreateResult::RESTRICTED_RACECLASS: msg = "Race/class combination not allowed"; break; case CharCreateResult::IN_PROGRESS: msg = "Character creation in progress..."; break; case CharCreateResult::CHARACTER_CHOOSE_RACE: msg = "Please choose a different race"; break; case CharCreateResult::CHARACTER_ARENA_LEADER: msg = "Arena team leader restriction"; break; case CharCreateResult::CHARACTER_DELETE_MAIL: msg = "Character has mail"; break; case CharCreateResult::CHARACTER_SWAP_FACTION: msg = "Faction swap restriction"; break; case CharCreateResult::CHARACTER_RACE_ONLY: msg = "Race-only restriction"; break; case CharCreateResult::CHARACTER_GOLD_LIMIT: msg = "Gold limit reached"; break; case CharCreateResult::FORCE_LOGIN: msg = "Force login required"; break; case CharCreateResult::CHARACTER_IN_GUILD: msg = "Character is in a guild"; break; // Name validation errors case CharCreateResult::NAME_FAILURE: msg = "Invalid name"; break; case CharCreateResult::NAME_NO_NAME: msg = "Please enter a name"; break; case CharCreateResult::NAME_TOO_SHORT: msg = "Name is too short"; break; case CharCreateResult::NAME_TOO_LONG: msg = "Name is too long"; break; case CharCreateResult::NAME_INVALID_CHARACTER: msg = "Name contains invalid characters"; break; case CharCreateResult::NAME_MIXED_LANGUAGES: msg = "Name mixes languages"; break; case CharCreateResult::NAME_PROFANE: msg = "Name contains profanity"; break; case CharCreateResult::NAME_RESERVED: msg = "Name is reserved"; break; case CharCreateResult::NAME_INVALID_APOSTROPHE: msg = "Invalid apostrophe in name"; break; case CharCreateResult::NAME_MULTIPLE_APOSTROPHES: msg = "Name has multiple apostrophes"; break; case CharCreateResult::NAME_THREE_CONSECUTIVE: msg = "Name has 3+ consecutive same letters"; break; case CharCreateResult::NAME_INVALID_SPACE: msg = "Invalid space in name"; break; case CharCreateResult::NAME_CONSECUTIVE_SPACES: msg = "Name has consecutive spaces"; break; default: msg = "Unknown error (code " + std::to_string(static_cast(data.result)) + ")"; break; } LOG_WARNING("Character creation failed: ", msg, " (code=", static_cast(data.result), ")"); if (charCreateCallback_) { charCreateCallback_(false, msg); } } } void GameHandler::deleteCharacter(uint64_t characterGuid) { if (!socket) { if (charDeleteCallback_) charDeleteCallback_(false); return; } network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_DELETE)); packet.writeUInt64(characterGuid); socket->send(packet); LOG_INFO("CMSG_CHAR_DELETE sent for GUID: 0x", std::hex, characterGuid, std::dec); } const Character* GameHandler::getActiveCharacter() const { if (activeCharacterGuid_ == 0) return nullptr; for (const auto& ch : characters) { if (ch.guid == activeCharacterGuid_) return &ch; } return nullptr; } const Character* GameHandler::getFirstCharacter() const { if (characters.empty()) return nullptr; return &characters.front(); } void GameHandler::handleCharLoginFailed(network::Packet& packet) { uint8_t reason = packet.readUInt8(); static const char* reasonNames[] = { "Login failed", // 0 "World server is down", // 1 "Duplicate character", // 2 (session still active) "No instance servers", // 3 "Login disabled", // 4 "Character not found", // 5 "Locked for transfer", // 6 "Locked by billing", // 7 "Using remote", // 8 }; const char* msg = (reason < 9) ? reasonNames[reason] : "Unknown reason"; LOG_ERROR("SMSG_CHARACTER_LOGIN_FAILED: reason=", static_cast(reason), " (", msg, ")"); // Allow the player to re-select a character setState(WorldState::CHAR_LIST_RECEIVED); if (charLoginFailCallback_) { charLoginFailCallback_(msg); } } void GameHandler::selectCharacter(uint64_t characterGuid) { if (state != WorldState::CHAR_LIST_RECEIVED) { LOG_WARNING("Cannot select character in state: ", static_cast(state)); return; } // Make the selected character authoritative in GameHandler. // This avoids relying on UI/Application ordering for appearance-dependent logic. activeCharacterGuid_ = characterGuid; LOG_INFO("========================================"); LOG_INFO(" ENTERING WORLD"); LOG_INFO("========================================"); LOG_INFO("Character GUID: 0x", std::hex, characterGuid, std::dec); // Find character name for logging for (const auto& character : characters) { if (character.guid == characterGuid) { LOG_INFO("Character: ", character.name); LOG_INFO("Level ", static_cast(character.level), " ", getRaceName(character.race), " ", getClassName(character.characterClass)); playerRace_ = character.race; break; } } // Store player GUID playerGuid = characterGuid; // Reset per-character state so previous character data doesn't bleed through inventory = Inventory(); onlineItems_.clear(); itemInfoCache_.clear(); pendingItemQueries_.clear(); equipSlotGuids_ = {}; backpackSlotGuids_ = {}; keyringSlotGuids_ = {}; invSlotBase_ = -1; packSlotBase_ = -1; lastPlayerFields_.clear(); onlineEquipDirty_ = false; playerMoneyCopper_ = 0; playerArmorRating_ = 0; std::fill(std::begin(playerResistances_), std::end(playerResistances_), 0); std::fill(std::begin(playerStats_), std::end(playerStats_), -1); playerMeleeAP_ = -1; playerRangedAP_ = -1; std::fill(std::begin(playerSpellDmgBonus_), std::end(playerSpellDmgBonus_), -1); playerHealBonus_ = -1; playerDodgePct_ = -1.0f; playerParryPct_ = -1.0f; playerBlockPct_ = -1.0f; playerCritPct_ = -1.0f; playerRangedCritPct_ = -1.0f; std::fill(std::begin(playerSpellCritPct_), std::end(playerSpellCritPct_), -1.0f); std::fill(std::begin(playerCombatRatings_), std::end(playerCombatRatings_), -1); if (spellHandler_) spellHandler_->resetAllState(); spellFlatMods_.clear(); spellPctMods_.clear(); actionBar = {}; petGuid_ = 0; stableWindowOpen_ = false; stableMasterGuid_ = 0; stableNumSlots_ = 0; stabledPets_.clear(); playerXp_ = 0; playerNextLevelXp_ = 0; serverPlayerLevel_ = 1; std::fill(playerExploredZones_.begin(), playerExploredZones_.end(), 0u); hasPlayerExploredZones_ = false; playerSkills_.clear(); questLog_.clear(); pendingQuestQueryIds_.clear(); pendingLoginQuestResync_ = false; pendingLoginQuestResyncTimeout_ = 0.0f; pendingQuestAcceptTimeouts_.clear(); pendingQuestAcceptNpcGuids_.clear(); npcQuestStatus_.clear(); if (combatHandler_) combatHandler_->resetAllCombatState(); // resetCastState() already called inside resetAllState() above pendingGameObjectInteractGuid_ = 0; lastInteractedGoGuid_ = 0; playerDead_ = false; releasedSpirit_ = false; corpseGuid_ = 0; corpseReclaimAvailableMs_ = 0; targetGuid = 0; focusGuid = 0; lastTargetGuid = 0; tabCycleStale = true; entityController_->clearAll(); // Build CMSG_PLAYER_LOGIN packet auto packet = PlayerLoginPacket::build(characterGuid); // Send packet socket->send(packet); setState(WorldState::ENTERING_WORLD); LOG_INFO("CMSG_PLAYER_LOGIN sent, entering world..."); } void GameHandler::handleLoginSetTimeSpeed(network::Packet& packet) { // SMSG_LOGIN_SETTIMESPEED (0x042) // Structure: uint32 gameTime, float timeScale // gameTime: Game time in seconds since epoch // timeScale: Time speed multiplier (typically 0.0166 for 1 day = 1 hour) if (packet.getSize() < 8) { LOG_WARNING("SMSG_LOGIN_SETTIMESPEED: packet too small (", packet.getSize(), " bytes)"); return; } uint32_t gameTimePacked = packet.readUInt32(); float timeScale = packet.readFloat(); // Store for celestial/sky system use gameTime_ = static_cast(gameTimePacked); timeSpeed_ = timeScale; LOG_INFO("Server time: gameTime=", gameTime_, "s, timeSpeed=", timeSpeed_); LOG_INFO(" (1 game day = ", (1.0f / timeSpeed_) / 60.0f, " real minutes)"); } void GameHandler::handleLoginVerifyWorld(network::Packet& packet) { LOG_INFO("Handling SMSG_LOGIN_VERIFY_WORLD"); const bool initialWorldEntry = (state == WorldState::ENTERING_WORLD); LoginVerifyWorldData data; if (!LoginVerifyWorldParser::parse(packet, data)) { fail("Failed to parse SMSG_LOGIN_VERIFY_WORLD"); return; } if (!data.isValid()) { fail("Invalid world entry data"); return; } glm::vec3 canonical = core::coords::serverToCanonical(glm::vec3(data.x, data.y, data.z)); const bool alreadyInWorld = (state == WorldState::IN_WORLD); const bool sameMap = alreadyInWorld && (currentMapId_ == data.mapId); const float dxCurrent = movementInfo.x - canonical.x; const float dyCurrent = movementInfo.y - canonical.y; const float dzCurrent = movementInfo.z - canonical.z; const float distSqCurrent = dxCurrent * dxCurrent + dyCurrent * dyCurrent + dzCurrent * dzCurrent; // Some realms emit a late duplicate LOGIN_VERIFY_WORLD after the client is already // in-world. Re-running full world-entry handling here can trigger an expensive // same-map reload/reset path and starve networking for tens of seconds. if (!initialWorldEntry && sameMap && distSqCurrent <= (5.0f * 5.0f)) { LOG_INFO("Ignoring duplicate SMSG_LOGIN_VERIFY_WORLD while already in world: mapId=", data.mapId, " dist=", std::sqrt(distSqCurrent)); return; } // Successfully entered the world (or teleported) currentMapId_ = data.mapId; setState(WorldState::IN_WORLD); if (socket) { socket->tracePacketsFor(std::chrono::seconds(12), "login_verify_world"); } LOG_INFO("========================================"); LOG_INFO(" SUCCESSFULLY ENTERED WORLD!"); LOG_INFO("========================================"); LOG_INFO("Map ID: ", data.mapId); LOG_INFO("Position: (", data.x, ", ", data.y, ", ", data.z, ")"); LOG_INFO("Orientation: ", data.orientation, " radians"); LOG_INFO("Player is now in the game world"); // Initialize movement info with world entry position (server → canonical) LOG_DEBUG("LOGIN_VERIFY_WORLD: server=(", data.x, ", ", data.y, ", ", data.z, ") canonical=(", canonical.x, ", ", canonical.y, ", ", canonical.z, ") mapId=", data.mapId); movementInfo.x = canonical.x; movementInfo.y = canonical.y; movementInfo.z = canonical.z; movementInfo.orientation = core::coords::serverToCanonicalYaw(data.orientation); movementInfo.flags = 0; movementInfo.flags2 = 0; if (movementHandler_) { movementHandler_->movementClockStart_ = std::chrono::steady_clock::now(); movementHandler_->lastMovementTimestampMs_ = 0; } movementInfo.time = nextMovementTimestampMs(); if (movementHandler_) { movementHandler_->isFalling_ = false; movementHandler_->fallStartMs_ = 0; } movementInfo.fallTime = 0; movementInfo.jumpVelocity = 0.0f; movementInfo.jumpSinAngle = 0.0f; movementInfo.jumpCosAngle = 0.0f; movementInfo.jumpXYSpeed = 0.0f; resurrectPending_ = false; resurrectRequestPending_ = false; selfResAvailable_ = false; onTaxiFlight_ = false; taxiMountActive_ = false; taxiActivatePending_ = false; taxiClientActive_ = false; taxiClientPath_.clear(); // taxiRecoverPending_ is NOT cleared here — it must survive the general // state reset so the recovery check below can detect a mid-flight reconnect. taxiStartGrace_ = 0.0f; currentMountDisplayId_ = 0; taxiMountDisplayId_ = 0; vehicleId_ = 0; if (mountCallback_) { mountCallback_(0); } // Clear boss encounter unit slots and raid marks on world transfer if (socialHandler_) socialHandler_->resetTransferState(); // Suppress area triggers on initial login — prevents exit portals from // immediately firing when spawning inside a dungeon/instance. activeAreaTriggers_.clear(); areaTriggerCheckTimer_ = -5.0f; areaTriggerSuppressFirst_ = true; // Notify application to load terrain for this map/position (online mode) if (worldEntryCallback_) { worldEntryCallback_(data.mapId, data.x, data.y, data.z, initialWorldEntry); } // Send CMSG_SET_ACTIVE_MOVER on initial world entry and world transfers. if (playerGuid != 0 && socket) { auto activeMoverPacket = SetActiveMoverPacket::build(playerGuid); socket->send(activeMoverPacket); LOG_INFO("Sent CMSG_SET_ACTIVE_MOVER for player 0x", std::hex, playerGuid, std::dec); } // Kick the first keepalive immediately on world entry. Classic-like realms // can close the session before our default 30s ping cadence fires. timeSinceLastPing = 0.0f; if (socket) { LOG_DEBUG("World entry keepalive: sending immediate ping after LOGIN_VERIFY_WORLD"); sendPing(); } // If we disconnected mid-taxi, attempt to recover to destination after login. if (taxiRecoverPending_ && taxiRecoverMapId_ == data.mapId) { float dx = movementInfo.x - taxiRecoverPos_.x; float dy = movementInfo.y - taxiRecoverPos_.y; float dz = movementInfo.z - taxiRecoverPos_.z; float dist = std::sqrt(dx * dx + dy * dy + dz * dz); if (dist > 5.0f) { // Keep pending until player entity exists; update() will apply. LOG_INFO("Taxi recovery pending: dist=", dist); } else { taxiRecoverPending_ = false; } } if (initialWorldEntry) { // Clear inspect caches on world entry to avoid showing stale data. inspectedPlayerAchievements_.clear(); // Reset talent initialization so the first SMSG_TALENTS_INFO after login // correctly sets the active spec (static locals don't reset across logins). if (spellHandler_) spellHandler_->resetTalentState(); // Auto-join default chat channels only on first world entry. autoJoinDefaultChannels(); // Auto-query guild info on login. const Character* activeChar = getActiveCharacter(); if (activeChar && activeChar->hasGuild() && socket) { auto gqPacket = GuildQueryPacket::build(activeChar->guildId); socket->send(gqPacket); auto grPacket = GuildRosterPacket::build(); socket->send(grPacket); LOG_INFO("Auto-queried guild info (guildId=", activeChar->guildId, ")"); } pendingQuestAcceptTimeouts_.clear(); pendingQuestAcceptNpcGuids_.clear(); pendingQuestQueryIds_.clear(); pendingLoginQuestResync_ = true; pendingLoginQuestResyncTimeout_ = 10.0f; completedQuests_.clear(); LOG_INFO("Queued quest log resync for login (from server quest slots)"); // Request completed quest IDs when the expansion supports it. Classic-like // opcode tables do not define this packet, and sending 0xFFFF during world // entry can desync the early session handshake. if (socket) { const uint16_t queryCompletedWire = wireOpcode(Opcode::CMSG_QUERY_QUESTS_COMPLETED); if (queryCompletedWire != 0xFFFF) { network::Packet cqcPkt(queryCompletedWire); socket->send(cqcPkt); LOG_INFO("Sent CMSG_QUERY_QUESTS_COMPLETED"); } else { LOG_INFO("Skipping CMSG_QUERY_QUESTS_COMPLETED: opcode not mapped for current expansion"); } } // Auto-request played time on login so the character Stats tab is // populated immediately without requiring /played. if (socket) { auto ptPkt = RequestPlayedTimePacket::build(false); // false = don't show in chat socket->send(ptPkt); LOG_INFO("Auto-requested played time on login"); } } // Pre-load DBC name caches during world entry so the first packet that // needs spell/title/achievement data doesn't stall mid-gameplay (the // Spell.dbc cache alone is ~170ms on a cold load). if (initialWorldEntry) { preloadDBCCaches(); } // Fire PLAYER_ENTERING_WORLD — THE most important event for addon initialization. // Fires on initial login, teleports, instance transitions, and zone changes. if (addonEventCallback_) { fireAddonEvent("PLAYER_ENTERING_WORLD", {initialWorldEntry ? "1" : "0"}); // Also fire ZONE_CHANGED_NEW_AREA and UPDATE_WORLD_STATES so map/BG addons refresh fireAddonEvent("ZONE_CHANGED_NEW_AREA", {}); fireAddonEvent("UPDATE_WORLD_STATES", {}); // PLAYER_LOGIN fires only on initial login (not teleports) if (initialWorldEntry) { fireAddonEvent("PLAYER_LOGIN", {}); } } } void GameHandler::handleClientCacheVersion(network::Packet& packet) { if (packet.getSize() < 4) { LOG_WARNING("SMSG_CLIENTCACHE_VERSION too short: ", packet.getSize(), " bytes"); return; } uint32_t version = packet.readUInt32(); LOG_INFO("SMSG_CLIENTCACHE_VERSION: ", version); } void GameHandler::handleTutorialFlags(network::Packet& packet) { if (packet.getSize() < 32) { LOG_WARNING("SMSG_TUTORIAL_FLAGS too short: ", packet.getSize(), " bytes"); return; } std::array flags{}; for (uint32_t& v : flags) { v = packet.readUInt32(); } LOG_INFO("SMSG_TUTORIAL_FLAGS: [", flags[0], ", ", flags[1], ", ", flags[2], ", ", flags[3], ", ", flags[4], ", ", flags[5], ", ", flags[6], ", ", flags[7], "]"); } void GameHandler::handleAccountDataTimes(network::Packet& packet) { LOG_DEBUG("Handling SMSG_ACCOUNT_DATA_TIMES"); AccountDataTimesData data; if (!AccountDataTimesParser::parse(packet, data)) { LOG_WARNING("Failed to parse SMSG_ACCOUNT_DATA_TIMES"); return; } LOG_DEBUG("Account data times received (server time: ", data.serverTime, ")"); } void GameHandler::handleMotd(network::Packet& packet) { if (chatHandler_) chatHandler_->handleMotd(packet); } void GameHandler::handleNotification(network::Packet& packet) { // SMSG_NOTIFICATION: single null-terminated string std::string message = packet.readString(); if (!message.empty()) { LOG_INFO("Server notification: ", message); addSystemChatMessage(message); } } void GameHandler::sendPing() { if (state != WorldState::IN_WORLD) { return; } // Increment sequence number pingSequence++; LOG_DEBUG("Sending CMSG_PING: sequence=", pingSequence, " latencyHintMs=", lastLatency); // Record send time for RTT measurement pingTimestamp_ = std::chrono::steady_clock::now(); // Build and send ping packet auto packet = PingPacket::build(pingSequence, lastLatency); socket->send(packet); } void GameHandler::sendRequestVehicleExit() { if (state != WorldState::IN_WORLD || vehicleId_ == 0) return; // CMSG_REQUEST_VEHICLE_EXIT has no payload — opcode only network::Packet pkt(wireOpcode(Opcode::CMSG_REQUEST_VEHICLE_EXIT)); socket->send(pkt); vehicleId_ = 0; // Optimistically clear; server will confirm via SMSG_PLAYER_VEHICLE_DATA(0) } const std::vector& GameHandler::getEquipmentSets() const { if (inventoryHandler_) return inventoryHandler_->getEquipmentSets(); static const std::vector empty; return empty; } // Trade state delegation to InventoryHandler (which owns the canonical trade state) GameHandler::TradeStatus GameHandler::getTradeStatus() const { if (inventoryHandler_) return static_cast(inventoryHandler_->getTradeStatus()); return tradeStatus_; } bool GameHandler::hasPendingTradeRequest() const { return inventoryHandler_ ? inventoryHandler_->hasPendingTradeRequest() : false; } bool GameHandler::isTradeOpen() const { return inventoryHandler_ ? inventoryHandler_->isTradeOpen() : false; } const std::string& GameHandler::getTradePeerName() const { if (inventoryHandler_) return inventoryHandler_->getTradePeerName(); return tradePeerName_; } const std::array& GameHandler::getMyTradeSlots() const { if (inventoryHandler_) { // Convert InventoryHandler::TradeSlot → GameHandler::TradeSlot (different struct layouts) static std::array converted{}; const auto& src = inventoryHandler_->getMyTradeSlots(); for (size_t i = 0; i < TRADE_SLOT_COUNT; i++) { converted[i].itemId = src[i].itemId; converted[i].displayId = src[i].displayId; converted[i].stackCount = src[i].stackCount; converted[i].itemGuid = src[i].itemGuid; } return converted; } return myTradeSlots_; } const std::array& GameHandler::getPeerTradeSlots() const { if (inventoryHandler_) { static std::array converted{}; const auto& src = inventoryHandler_->getPeerTradeSlots(); for (size_t i = 0; i < TRADE_SLOT_COUNT; i++) { converted[i].itemId = src[i].itemId; converted[i].displayId = src[i].displayId; converted[i].stackCount = src[i].stackCount; converted[i].itemGuid = src[i].itemGuid; } return converted; } return peerTradeSlots_; } uint64_t GameHandler::getMyTradeGold() const { return inventoryHandler_ ? inventoryHandler_->getMyTradeGold() : myTradeGold_; } uint64_t GameHandler::getPeerTradeGold() const { return inventoryHandler_ ? inventoryHandler_->getPeerTradeGold() : peerTradeGold_; } bool GameHandler::supportsEquipmentSets() const { return inventoryHandler_ && inventoryHandler_->supportsEquipmentSets(); } void GameHandler::useEquipmentSet(uint32_t setId) { if (inventoryHandler_) inventoryHandler_->useEquipmentSet(setId); } void GameHandler::saveEquipmentSet(const std::string& name, const std::string& iconName, uint64_t existingGuid, uint32_t setIndex) { if (inventoryHandler_) inventoryHandler_->saveEquipmentSet(name, iconName, existingGuid, setIndex); } void GameHandler::deleteEquipmentSet(uint64_t setGuid) { if (inventoryHandler_) inventoryHandler_->deleteEquipmentSet(setGuid); } // --- Inventory state delegation (canonical state lives in InventoryHandler) --- // Item text bool GameHandler::isItemTextOpen() const { return inventoryHandler_ ? inventoryHandler_->isItemTextOpen() : itemTextOpen_; } const std::string& GameHandler::getItemText() const { if (inventoryHandler_) return inventoryHandler_->getItemText(); return itemText_; } void GameHandler::closeItemText() { if (inventoryHandler_) inventoryHandler_->closeItemText(); else itemTextOpen_ = false; } // Loot bool GameHandler::isLootWindowOpen() const { return inventoryHandler_ ? inventoryHandler_->isLootWindowOpen() : lootWindowOpen; } const LootResponseData& GameHandler::getCurrentLoot() const { if (inventoryHandler_) return inventoryHandler_->getCurrentLoot(); return currentLoot; } void GameHandler::setAutoLoot(bool enabled) { if (inventoryHandler_) inventoryHandler_->setAutoLoot(enabled); else autoLoot_ = enabled; } bool GameHandler::isAutoLoot() const { return inventoryHandler_ ? inventoryHandler_->isAutoLoot() : autoLoot_; } void GameHandler::setAutoSellGrey(bool enabled) { if (inventoryHandler_) inventoryHandler_->setAutoSellGrey(enabled); else autoSellGrey_ = enabled; } bool GameHandler::isAutoSellGrey() const { return inventoryHandler_ ? inventoryHandler_->isAutoSellGrey() : autoSellGrey_; } void GameHandler::setAutoRepair(bool enabled) { if (inventoryHandler_) inventoryHandler_->setAutoRepair(enabled); else autoRepair_ = enabled; } bool GameHandler::isAutoRepair() const { return inventoryHandler_ ? inventoryHandler_->isAutoRepair() : autoRepair_; } const std::vector& GameHandler::getMasterLootCandidates() const { if (inventoryHandler_) return inventoryHandler_->getMasterLootCandidates(); return masterLootCandidates_; } bool GameHandler::hasMasterLootCandidates() const { return inventoryHandler_ ? inventoryHandler_->hasMasterLootCandidates() : !masterLootCandidates_.empty(); } bool GameHandler::hasPendingLootRoll() const { return inventoryHandler_ ? inventoryHandler_->hasPendingLootRoll() : pendingLootRollActive_; } const LootRollEntry& GameHandler::getPendingLootRoll() const { if (inventoryHandler_) return inventoryHandler_->getPendingLootRoll(); return pendingLootRoll_; } // Vendor bool GameHandler::isVendorWindowOpen() const { return inventoryHandler_ ? inventoryHandler_->isVendorWindowOpen() : vendorWindowOpen; } const ListInventoryData& GameHandler::getVendorItems() const { if (inventoryHandler_) return inventoryHandler_->getVendorItems(); return currentVendorItems; } void GameHandler::setVendorCanRepair(bool v) { if (inventoryHandler_) inventoryHandler_->setVendorCanRepair(v); else currentVendorItems.canRepair = v; } const std::deque& GameHandler::getBuybackItems() const { if (inventoryHandler_) { // Layout-identical structs (InventoryHandler::BuybackItem == GameHandler::BuybackItem) return reinterpret_cast&>(inventoryHandler_->getBuybackItems()); } return buybackItems_; } uint64_t GameHandler::getVendorGuid() const { if (inventoryHandler_) return inventoryHandler_->getVendorGuid(); return currentVendorItems.vendorGuid; } // Mail bool GameHandler::isMailboxOpen() const { return inventoryHandler_ ? inventoryHandler_->isMailboxOpen() : mailboxOpen_; } const std::vector& GameHandler::getMailInbox() const { if (inventoryHandler_) return inventoryHandler_->getMailInbox(); return mailInbox_; } int GameHandler::getSelectedMailIndex() const { return inventoryHandler_ ? inventoryHandler_->getSelectedMailIndex() : selectedMailIndex_; } void GameHandler::setSelectedMailIndex(int idx) { if (inventoryHandler_) inventoryHandler_->setSelectedMailIndex(idx); else selectedMailIndex_ = idx; } bool GameHandler::isMailComposeOpen() const { return inventoryHandler_ ? inventoryHandler_->isMailComposeOpen() : showMailCompose_; } void GameHandler::openMailCompose() { if (inventoryHandler_) inventoryHandler_->openMailCompose(); else { showMailCompose_ = true; clearMailAttachments(); } } void GameHandler::closeMailCompose() { if (inventoryHandler_) inventoryHandler_->closeMailCompose(); else { showMailCompose_ = false; clearMailAttachments(); } } bool GameHandler::hasNewMail() const { return inventoryHandler_ ? inventoryHandler_->hasNewMail() : hasNewMail_; } const std::array& GameHandler::getMailAttachments() const { if (inventoryHandler_) { // Layout-identical structs (InventoryHandler::MailAttachSlot == GameHandler::MailAttachSlot) return reinterpret_cast&>(inventoryHandler_->getMailAttachments()); } return mailAttachments_; } // Bank bool GameHandler::isBankOpen() const { return inventoryHandler_ ? inventoryHandler_->isBankOpen() : bankOpen_; } uint64_t GameHandler::getBankerGuid() const { return inventoryHandler_ ? inventoryHandler_->getBankerGuid() : bankerGuid_; } int GameHandler::getEffectiveBankSlots() const { return inventoryHandler_ ? inventoryHandler_->getEffectiveBankSlots() : effectiveBankSlots_; } int GameHandler::getEffectiveBankBagSlots() const { return inventoryHandler_ ? inventoryHandler_->getEffectiveBankBagSlots() : effectiveBankBagSlots_; } // Guild Bank bool GameHandler::isGuildBankOpen() const { return inventoryHandler_ ? inventoryHandler_->isGuildBankOpen() : guildBankOpen_; } const GuildBankData& GameHandler::getGuildBankData() const { if (inventoryHandler_) return inventoryHandler_->getGuildBankData(); return guildBankData_; } uint8_t GameHandler::getGuildBankActiveTab() const { return inventoryHandler_ ? inventoryHandler_->getGuildBankActiveTab() : guildBankActiveTab_; } void GameHandler::setGuildBankActiveTab(uint8_t tab) { if (inventoryHandler_) inventoryHandler_->setGuildBankActiveTab(tab); else guildBankActiveTab_ = tab; } // Auction House bool GameHandler::isAuctionHouseOpen() const { return inventoryHandler_ ? inventoryHandler_->isAuctionHouseOpen() : auctionOpen_; } uint64_t GameHandler::getAuctioneerGuid() const { return inventoryHandler_ ? inventoryHandler_->getAuctioneerGuid() : auctioneerGuid_; } const AuctionListResult& GameHandler::getAuctionBrowseResults() const { if (inventoryHandler_) return inventoryHandler_->getAuctionBrowseResults(); return auctionBrowseResults_; } const AuctionListResult& GameHandler::getAuctionOwnerResults() const { if (inventoryHandler_) return inventoryHandler_->getAuctionOwnerResults(); return auctionOwnerResults_; } const AuctionListResult& GameHandler::getAuctionBidderResults() const { if (inventoryHandler_) return inventoryHandler_->getAuctionBidderResults(); return auctionBidderResults_; } int GameHandler::getAuctionActiveTab() const { return inventoryHandler_ ? inventoryHandler_->getAuctionActiveTab() : auctionActiveTab_; } void GameHandler::setAuctionActiveTab(int tab) { if (inventoryHandler_) inventoryHandler_->setAuctionActiveTab(tab); else auctionActiveTab_ = tab; } float GameHandler::getAuctionSearchDelay() const { return inventoryHandler_ ? inventoryHandler_->getAuctionSearchDelay() : auctionSearchDelayTimer_; } // Trainer bool GameHandler::isTrainerWindowOpen() const { return inventoryHandler_ ? inventoryHandler_->isTrainerWindowOpen() : trainerWindowOpen_; } const TrainerListData& GameHandler::getTrainerSpells() const { if (inventoryHandler_) return inventoryHandler_->getTrainerSpells(); return currentTrainerList_; } const std::vector& GameHandler::getTrainerTabs() const { if (inventoryHandler_) { // Layout-identical structs (InventoryHandler::TrainerTab == GameHandler::TrainerTab) return reinterpret_cast&>(inventoryHandler_->getTrainerTabs()); } return trainerTabs_; } void GameHandler::sendMinimapPing(float wowX, float wowY) { if (socialHandler_) socialHandler_->sendMinimapPing(wowX, wowY); } void GameHandler::handlePong(network::Packet& packet) { LOG_DEBUG("Handling SMSG_PONG"); PongData data; if (!PongParser::parse(packet, data)) { LOG_WARNING("Failed to parse SMSG_PONG"); return; } // Verify sequence matches if (data.sequence != pingSequence) { LOG_WARNING("SMSG_PONG sequence mismatch: expected ", pingSequence, ", got ", data.sequence); return; } // Measure round-trip time auto rtt = std::chrono::steady_clock::now() - pingTimestamp_; lastLatency = static_cast( std::chrono::duration_cast(rtt).count()); LOG_DEBUG("SMSG_PONG acknowledged: sequence=", data.sequence, " latencyMs=", lastLatency); } bool GameHandler::isServerMovementAllowed() const { return movementHandler_ ? movementHandler_->isServerMovementAllowed() : true; } uint32_t GameHandler::nextMovementTimestampMs() { if (movementHandler_) return movementHandler_->nextMovementTimestampMs(); return 0; } void GameHandler::sendMovement(Opcode opcode) { if (movementHandler_) movementHandler_->sendMovement(opcode); } void GameHandler::sanitizeMovementForTaxi() { if (movementHandler_) movementHandler_->sanitizeMovementForTaxi(); } void GameHandler::forceClearTaxiAndMovementState() { if (movementHandler_) movementHandler_->forceClearTaxiAndMovementState(); } void GameHandler::setPosition(float x, float y, float z) { if (movementHandler_) movementHandler_->setPosition(x, y, z); } void GameHandler::setOrientation(float orientation) { if (movementHandler_) movementHandler_->setOrientation(orientation); } // Entity lifecycle methods (handleUpdateObject, processOutOfRangeObjects, // applyUpdateObjectBlock, finalizeUpdateObjectBatch, handleCompressedUpdateObject, // handleDestroyObject) moved to EntityController — see entity_controller.cpp void GameHandler::sendChatMessage(ChatType type, const std::string& message, const std::string& target) { if (chatHandler_) chatHandler_->sendChatMessage(type, message, target); } void GameHandler::sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid) { if (chatHandler_) chatHandler_->sendTextEmote(textEmoteId, targetGuid); } void GameHandler::joinChannel(const std::string& channelName, const std::string& password) { if (chatHandler_) chatHandler_->joinChannel(channelName, password); } void GameHandler::leaveChannel(const std::string& channelName) { if (chatHandler_) chatHandler_->leaveChannel(channelName); } std::string GameHandler::getChannelByIndex(int index) const { return chatHandler_ ? chatHandler_->getChannelByIndex(index) : ""; } int GameHandler::getChannelIndex(const std::string& channelName) const { return chatHandler_ ? chatHandler_->getChannelIndex(channelName) : 0; } void GameHandler::autoJoinDefaultChannels() { if (chatHandler_) { chatHandler_->chatAutoJoin.general = chatAutoJoin.general; chatHandler_->chatAutoJoin.trade = chatAutoJoin.trade; chatHandler_->chatAutoJoin.localDefense = chatAutoJoin.localDefense; chatHandler_->chatAutoJoin.lfg = chatAutoJoin.lfg; chatHandler_->chatAutoJoin.local = chatAutoJoin.local; chatHandler_->autoJoinDefaultChannels(); } } void GameHandler::setTarget(uint64_t guid) { if (combatHandler_) combatHandler_->setTarget(guid); } void GameHandler::clearTarget() { if (combatHandler_) combatHandler_->clearTarget(); } std::shared_ptr GameHandler::getTarget() const { return combatHandler_ ? combatHandler_->getTarget() : nullptr; } void GameHandler::setFocus(uint64_t guid) { if (combatHandler_) combatHandler_->setFocus(guid); } void GameHandler::clearFocus() { if (combatHandler_) combatHandler_->clearFocus(); } void GameHandler::setMouseoverGuid(uint64_t guid) { if (combatHandler_) combatHandler_->setMouseoverGuid(guid); } std::shared_ptr GameHandler::getFocus() const { return combatHandler_ ? combatHandler_->getFocus() : nullptr; } void GameHandler::targetLastTarget() { if (combatHandler_) combatHandler_->targetLastTarget(); } void GameHandler::targetEnemy(bool reverse) { if (combatHandler_) combatHandler_->targetEnemy(reverse); } void GameHandler::targetFriend(bool reverse) { if (combatHandler_) combatHandler_->targetFriend(reverse); } void GameHandler::inspectTarget() { if (socialHandler_) socialHandler_->inspectTarget(); } void GameHandler::queryServerTime() { if (socialHandler_) socialHandler_->queryServerTime(); } void GameHandler::requestPlayedTime() { if (socialHandler_) socialHandler_->requestPlayedTime(); } void GameHandler::queryWho(const std::string& playerName) { if (socialHandler_) socialHandler_->queryWho(playerName); } void GameHandler::addFriend(const std::string& playerName, const std::string& note) { if (socialHandler_) socialHandler_->addFriend(playerName, note); } void GameHandler::removeFriend(const std::string& playerName) { if (socialHandler_) socialHandler_->removeFriend(playerName); } void GameHandler::setFriendNote(const std::string& playerName, const std::string& note) { if (socialHandler_) socialHandler_->setFriendNote(playerName, note); } void GameHandler::randomRoll(uint32_t minRoll, uint32_t maxRoll) { if (socialHandler_) socialHandler_->randomRoll(minRoll, maxRoll); } void GameHandler::addIgnore(const std::string& playerName) { if (socialHandler_) socialHandler_->addIgnore(playerName); } void GameHandler::removeIgnore(const std::string& playerName) { if (socialHandler_) socialHandler_->removeIgnore(playerName); } void GameHandler::requestLogout() { if (socialHandler_) socialHandler_->requestLogout(); } void GameHandler::cancelLogout() { if (socialHandler_) socialHandler_->cancelLogout(); } void GameHandler::sendSetDifficulty(uint32_t difficulty) { if (socialHandler_) socialHandler_->sendSetDifficulty(difficulty); } void GameHandler::setStandState(uint8_t standState) { if (socialHandler_) socialHandler_->setStandState(standState); } void GameHandler::toggleHelm() { if (socialHandler_) socialHandler_->toggleHelm(); } void GameHandler::toggleCloak() { if (socialHandler_) socialHandler_->toggleCloak(); } void GameHandler::followTarget() { if (movementHandler_) movementHandler_->followTarget(); } void GameHandler::cancelFollow() { if (movementHandler_) movementHandler_->cancelFollow(); } void GameHandler::assistTarget() { if (combatHandler_) combatHandler_->assistTarget(); } void GameHandler::togglePvp() { if (combatHandler_) combatHandler_->togglePvp(); } void GameHandler::requestGuildInfo() { if (socialHandler_) socialHandler_->requestGuildInfo(); } void GameHandler::requestGuildRoster() { if (socialHandler_) socialHandler_->requestGuildRoster(); } void GameHandler::setGuildMotd(const std::string& motd) { if (socialHandler_) socialHandler_->setGuildMotd(motd); } void GameHandler::promoteGuildMember(const std::string& playerName) { if (socialHandler_) socialHandler_->promoteGuildMember(playerName); } void GameHandler::demoteGuildMember(const std::string& playerName) { if (socialHandler_) socialHandler_->demoteGuildMember(playerName); } void GameHandler::leaveGuild() { if (socialHandler_) socialHandler_->leaveGuild(); } void GameHandler::inviteToGuild(const std::string& playerName) { if (socialHandler_) socialHandler_->inviteToGuild(playerName); } void GameHandler::initiateReadyCheck() { if (socialHandler_) socialHandler_->initiateReadyCheck(); } void GameHandler::respondToReadyCheck(bool ready) { if (socialHandler_) socialHandler_->respondToReadyCheck(ready); } void GameHandler::acceptDuel() { if (socialHandler_) socialHandler_->acceptDuel(); } void GameHandler::forfeitDuel() { if (socialHandler_) socialHandler_->forfeitDuel(); } void GameHandler::toggleAfk(const std::string& message) { if (chatHandler_) chatHandler_->toggleAfk(message); } void GameHandler::toggleDnd(const std::string& message) { if (chatHandler_) chatHandler_->toggleDnd(message); } void GameHandler::replyToLastWhisper(const std::string& message) { if (chatHandler_) chatHandler_->replyToLastWhisper(message); } void GameHandler::uninvitePlayer(const std::string& playerName) { if (socialHandler_) socialHandler_->uninvitePlayer(playerName); } void GameHandler::leaveParty() { if (socialHandler_) socialHandler_->leaveParty(); } void GameHandler::setMainTank(uint64_t targetGuid) { if (socialHandler_) socialHandler_->setMainTank(targetGuid); } void GameHandler::setMainAssist(uint64_t targetGuid) { if (socialHandler_) socialHandler_->setMainAssist(targetGuid); } void GameHandler::clearMainTank() { if (socialHandler_) socialHandler_->clearMainTank(); } void GameHandler::clearMainAssist() { if (socialHandler_) socialHandler_->clearMainAssist(); } void GameHandler::setRaidMark(uint64_t guid, uint8_t icon) { if (socialHandler_) socialHandler_->setRaidMark(guid, icon); } void GameHandler::requestRaidInfo() { if (socialHandler_) socialHandler_->requestRaidInfo(); } void GameHandler::proposeDuel(uint64_t targetGuid) { if (socialHandler_) socialHandler_->proposeDuel(targetGuid); } void GameHandler::initiateTrade(uint64_t targetGuid) { if (inventoryHandler_) inventoryHandler_->initiateTrade(targetGuid); } void GameHandler::reportPlayer(uint64_t targetGuid, const std::string& reason) { if (socialHandler_) socialHandler_->reportPlayer(targetGuid, reason); } void GameHandler::stopCasting() { if (spellHandler_) spellHandler_->stopCasting(); } void GameHandler::resetCastState() { if (spellHandler_) spellHandler_->resetCastState(); } void GameHandler::clearUnitCaches() { if (spellHandler_) spellHandler_->clearUnitCaches(); } void GameHandler::releaseSpirit() { if (combatHandler_) combatHandler_->releaseSpirit(); } bool GameHandler::canReclaimCorpse() const { return combatHandler_ ? combatHandler_->canReclaimCorpse() : false; } float GameHandler::getCorpseReclaimDelaySec() const { return combatHandler_ ? combatHandler_->getCorpseReclaimDelaySec() : 0.0f; } void GameHandler::reclaimCorpse() { if (combatHandler_) combatHandler_->reclaimCorpse(); } void GameHandler::useSelfRes() { if (combatHandler_) combatHandler_->useSelfRes(); } void GameHandler::activateSpiritHealer(uint64_t npcGuid) { if (combatHandler_) combatHandler_->activateSpiritHealer(npcGuid); } void GameHandler::acceptResurrect() { if (combatHandler_) combatHandler_->acceptResurrect(); } void GameHandler::declineResurrect() { if (combatHandler_) combatHandler_->declineResurrect(); } void GameHandler::tabTarget(float playerX, float playerY, float playerZ) { if (combatHandler_) combatHandler_->tabTarget(playerX, playerY, playerZ); } void GameHandler::addLocalChatMessage(const MessageChatData& msg) { if (chatHandler_) chatHandler_->addLocalChatMessage(msg); } const std::deque& GameHandler::getChatHistory() const { if (chatHandler_) return chatHandler_->getChatHistory(); static const std::deque kEmpty; return kEmpty; } void GameHandler::clearChatHistory() { if (chatHandler_) chatHandler_->getChatHistory().clear(); } const std::vector& GameHandler::getJoinedChannels() const { if (chatHandler_) return chatHandler_->getJoinedChannels(); static const std::vector kEmpty; return kEmpty; } // ============================================================ // Name Queries (delegated to EntityController) // ============================================================ void GameHandler::queryPlayerName(uint64_t guid) { if (entityController_) entityController_->queryPlayerName(guid); } void GameHandler::queryCreatureInfo(uint32_t entry, uint64_t guid) { if (entityController_) entityController_->queryCreatureInfo(entry, guid); } void GameHandler::queryGameObjectInfo(uint32_t entry, uint64_t guid) { if (entityController_) entityController_->queryGameObjectInfo(entry, guid); } std::string GameHandler::getCachedPlayerName(uint64_t guid) const { return entityController_ ? entityController_->getCachedPlayerName(guid) : ""; } std::string GameHandler::getCachedCreatureName(uint32_t entry) const { return entityController_ ? entityController_->getCachedCreatureName(entry) : ""; } // ============================================================ // Item Query (forwarded to InventoryHandler) // ============================================================ void GameHandler::queryItemInfo(uint32_t entry, uint64_t guid) { if (inventoryHandler_) inventoryHandler_->queryItemInfo(entry, guid); } void GameHandler::handleItemQueryResponse(network::Packet& packet) { if (inventoryHandler_) inventoryHandler_->handleItemQueryResponse(packet); } uint64_t GameHandler::resolveOnlineItemGuid(uint32_t itemId) const { return inventoryHandler_ ? inventoryHandler_->resolveOnlineItemGuid(itemId) : 0; } void GameHandler::detectInventorySlotBases(const std::map& fields) { if (inventoryHandler_) inventoryHandler_->detectInventorySlotBases(fields); } bool GameHandler::applyInventoryFields(const std::map& fields) { return inventoryHandler_ ? inventoryHandler_->applyInventoryFields(fields) : false; } void GameHandler::extractContainerFields(uint64_t containerGuid, const std::map& fields) { if (inventoryHandler_) inventoryHandler_->extractContainerFields(containerGuid, fields); } void GameHandler::rebuildOnlineInventory() { if (inventoryHandler_) inventoryHandler_->rebuildOnlineInventory(); } void GameHandler::maybeDetectVisibleItemLayout() { if (inventoryHandler_) inventoryHandler_->maybeDetectVisibleItemLayout(); } void GameHandler::updateOtherPlayerVisibleItems(uint64_t guid, const std::map& fields) { if (inventoryHandler_) inventoryHandler_->updateOtherPlayerVisibleItems(guid, fields); } void GameHandler::emitOtherPlayerEquipment(uint64_t guid) { if (inventoryHandler_) inventoryHandler_->emitOtherPlayerEquipment(guid); } void GameHandler::emitAllOtherPlayerEquipment() { if (inventoryHandler_) inventoryHandler_->emitAllOtherPlayerEquipment(); } // ============================================================ // Combat (delegated to CombatHandler) // ============================================================ void GameHandler::startAutoAttack(uint64_t targetGuid) { if (combatHandler_) combatHandler_->startAutoAttack(targetGuid); } void GameHandler::stopAutoAttack() { if (combatHandler_) combatHandler_->stopAutoAttack(); } void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType, uint64_t srcGuid, uint64_t dstGuid) { if (combatHandler_) combatHandler_->addCombatText(type, amount, spellId, isPlayerSource, powerType, srcGuid, dstGuid); } bool GameHandler::shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId) { return combatHandler_ ? combatHandler_->shouldLogSpellstealAura(casterGuid, victimGuid, spellId) : false; } void GameHandler::updateCombatText(float deltaTime) { if (combatHandler_) combatHandler_->updateCombatText(deltaTime); } bool GameHandler::isAutoAttacking() const { return combatHandler_ ? combatHandler_->isAutoAttacking() : false; } bool GameHandler::hasAutoAttackIntent() const { return combatHandler_ ? combatHandler_->hasAutoAttackIntent() : false; } bool GameHandler::isInCombat() const { return combatHandler_ ? combatHandler_->isInCombat() : false; } bool GameHandler::isInCombatWith(uint64_t guid) const { return combatHandler_ ? combatHandler_->isInCombatWith(guid) : false; } uint64_t GameHandler::getAutoAttackTargetGuid() const { return combatHandler_ ? combatHandler_->getAutoAttackTargetGuid() : 0; } bool GameHandler::isAggressiveTowardPlayer(uint64_t guid) const { return combatHandler_ ? combatHandler_->isAggressiveTowardPlayer(guid) : false; } uint64_t GameHandler::getLastMeleeSwingMs() const { return combatHandler_ ? combatHandler_->getLastMeleeSwingMs() : 0; } const std::vector& GameHandler::getCombatText() const { static const std::vector empty; return combatHandler_ ? combatHandler_->getCombatText() : empty; } const std::deque& GameHandler::getCombatLog() const { static const std::deque empty; return combatHandler_ ? combatHandler_->getCombatLog() : empty; } void GameHandler::clearCombatLog() { if (combatHandler_) combatHandler_->clearCombatLog(); } void GameHandler::clearCombatText() { if (combatHandler_) combatHandler_->clearCombatText(); } void GameHandler::clearHostileAttackers() { if (combatHandler_) combatHandler_->clearHostileAttackers(); } const std::vector* GameHandler::getThreatList(uint64_t unitGuid) const { return combatHandler_ ? combatHandler_->getThreatList(unitGuid) : nullptr; } const std::vector* GameHandler::getTargetThreatList() const { return targetGuid ? getThreatList(targetGuid) : nullptr; } bool GameHandler::isHostileAttacker(uint64_t guid) const { return combatHandler_ ? combatHandler_->isHostileAttacker(guid) : false; } void GameHandler::dismount() { if (movementHandler_) movementHandler_->dismount(); } // ============================================================ // Arena / Battleground Handlers // ============================================================ void GameHandler::declineBattlefield(uint32_t queueSlot) { if (socialHandler_) socialHandler_->declineBattlefield(queueSlot); } bool GameHandler::hasPendingBgInvite() const { return socialHandler_ && socialHandler_->hasPendingBgInvite(); } void GameHandler::acceptBattlefield(uint32_t queueSlot) { if (socialHandler_) socialHandler_->acceptBattlefield(queueSlot); } // --------------------------------------------------------------------------- // LFG / Dungeon Finder handlers (WotLK 3.3.5a) // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // LFG outgoing packets // --------------------------------------------------------------------------- void GameHandler::lfgJoin(uint32_t dungeonId, uint8_t roles) { if (socialHandler_) socialHandler_->lfgJoin(dungeonId, roles); } void GameHandler::lfgLeave() { if (socialHandler_) socialHandler_->lfgLeave(); } void GameHandler::lfgSetRoles(uint8_t roles) { if (socialHandler_) socialHandler_->lfgSetRoles(roles); } void GameHandler::lfgAcceptProposal(uint32_t proposalId, bool accept) { if (socialHandler_) socialHandler_->lfgAcceptProposal(proposalId, accept); } void GameHandler::lfgTeleport(bool toLfgDungeon) { if (socialHandler_) socialHandler_->lfgTeleport(toLfgDungeon); } void GameHandler::lfgSetBootVote(bool vote) { if (socialHandler_) socialHandler_->lfgSetBootVote(vote); } void GameHandler::loadAreaTriggerDbc() { if (movementHandler_) movementHandler_->loadAreaTriggerDbc(); } void GameHandler::checkAreaTriggers() { if (movementHandler_) movementHandler_->checkAreaTriggers(); } void GameHandler::requestArenaTeamRoster(uint32_t teamId) { if (socialHandler_) socialHandler_->requestArenaTeamRoster(teamId); } void GameHandler::requestPvpLog() { if (socialHandler_) socialHandler_->requestPvpLog(); } // ============================================================ // Spells // ============================================================ void GameHandler::castSpell(uint32_t spellId, uint64_t targetGuid) { if (spellHandler_) spellHandler_->castSpell(spellId, targetGuid); } void GameHandler::cancelCast() { if (spellHandler_) spellHandler_->cancelCast(); } void GameHandler::startCraftQueue(uint32_t spellId, int count) { if (spellHandler_) spellHandler_->startCraftQueue(spellId, count); } void GameHandler::cancelCraftQueue() { if (spellHandler_) spellHandler_->cancelCraftQueue(); } void GameHandler::cancelAura(uint32_t spellId) { if (spellHandler_) spellHandler_->cancelAura(spellId); } uint32_t GameHandler::getTempEnchantRemainingMs(uint32_t slot) const { return inventoryHandler_ ? inventoryHandler_->getTempEnchantRemainingMs(slot) : 0u; } void GameHandler::handlePetSpells(network::Packet& packet) { if (spellHandler_) spellHandler_->handlePetSpells(packet); } void GameHandler::sendPetAction(uint32_t action, uint64_t targetGuid) { if (spellHandler_) spellHandler_->sendPetAction(action, targetGuid); } void GameHandler::dismissPet() { if (spellHandler_) spellHandler_->dismissPet(); } void GameHandler::togglePetSpellAutocast(uint32_t spellId) { if (spellHandler_) spellHandler_->togglePetSpellAutocast(spellId); } void GameHandler::renamePet(const std::string& newName) { if (spellHandler_) spellHandler_->renamePet(newName); } void GameHandler::requestStabledPetList() { if (spellHandler_) spellHandler_->requestStabledPetList(); } void GameHandler::stablePet(uint8_t slot) { if (spellHandler_) spellHandler_->stablePet(slot); } void GameHandler::unstablePet(uint32_t petNumber) { if (spellHandler_) spellHandler_->unstablePet(petNumber); } void GameHandler::handleListStabledPets(network::Packet& packet) { if (spellHandler_) spellHandler_->handleListStabledPets(packet); } void GameHandler::setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id) { if (slot < 0 || slot >= ACTION_BAR_SLOTS) return; actionBar[slot].type = type; actionBar[slot].id = id; // Pre-query item information so action bar displays item name instead of "Item" placeholder if (type == ActionBarSlot::ITEM && id != 0) { queryItemInfo(id, 0); } saveCharacterConfig(); // Notify Lua addons that the action bar changed fireAddonEvent("ACTIONBAR_SLOT_CHANGED", {std::to_string(slot + 1)}); fireAddonEvent("ACTIONBAR_UPDATE_STATE", {}); // Notify the server so the action bar persists across relogs. if (isInWorld()) { const bool classic = isClassicLikeExpansion(); auto pkt = SetActionButtonPacket::build( static_cast(slot), static_cast(type), id, classic); socket->send(pkt); } } float GameHandler::getSpellCooldown(uint32_t spellId) const { if (spellHandler_) return spellHandler_->getSpellCooldown(spellId); return 0; } // ============================================================ // Talents // ============================================================ void GameHandler::learnTalent(uint32_t talentId, uint32_t requestedRank) { if (spellHandler_) spellHandler_->learnTalent(talentId, requestedRank); } void GameHandler::switchTalentSpec(uint8_t newSpec) { if (spellHandler_) spellHandler_->switchTalentSpec(newSpec); } void GameHandler::confirmPetUnlearn() { if (spellHandler_) spellHandler_->confirmPetUnlearn(); } void GameHandler::confirmTalentWipe() { if (spellHandler_) spellHandler_->confirmTalentWipe(); } void GameHandler::sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair) { if (socialHandler_) socialHandler_->sendAlterAppearance(hairStyle, hairColor, facialHair); } // ============================================================ // Group/Party // ============================================================ void GameHandler::inviteToGroup(const std::string& playerName) { if (socialHandler_) socialHandler_->inviteToGroup(playerName); } void GameHandler::acceptGroupInvite() { if (socialHandler_) socialHandler_->acceptGroupInvite(); } void GameHandler::declineGroupInvite() { if (socialHandler_) socialHandler_->declineGroupInvite(); } void GameHandler::leaveGroup() { if (socialHandler_) socialHandler_->leaveGroup(); } void GameHandler::convertToRaid() { if (socialHandler_) socialHandler_->convertToRaid(); } void GameHandler::sendSetLootMethod(uint32_t method, uint32_t threshold, uint64_t masterLooterGuid) { if (socialHandler_) socialHandler_->sendSetLootMethod(method, threshold, masterLooterGuid); } // ============================================================ // Guild Handlers // ============================================================ void GameHandler::kickGuildMember(const std::string& playerName) { if (socialHandler_) socialHandler_->kickGuildMember(playerName); } void GameHandler::disbandGuild() { if (socialHandler_) socialHandler_->disbandGuild(); } void GameHandler::setGuildLeader(const std::string& name) { if (socialHandler_) socialHandler_->setGuildLeader(name); } void GameHandler::setGuildPublicNote(const std::string& name, const std::string& note) { if (socialHandler_) socialHandler_->setGuildPublicNote(name, note); } void GameHandler::setGuildOfficerNote(const std::string& name, const std::string& note) { if (socialHandler_) socialHandler_->setGuildOfficerNote(name, note); } void GameHandler::acceptGuildInvite() { if (socialHandler_) socialHandler_->acceptGuildInvite(); } void GameHandler::declineGuildInvite() { if (socialHandler_) socialHandler_->declineGuildInvite(); } void GameHandler::submitGmTicket(const std::string& text) { if (chatHandler_) chatHandler_->submitGmTicket(text); } void GameHandler::deleteGmTicket() { if (socialHandler_) socialHandler_->deleteGmTicket(); } void GameHandler::requestGmTicket() { if (socialHandler_) socialHandler_->requestGmTicket(); } void GameHandler::queryGuildInfo(uint32_t guildId) { if (socialHandler_) socialHandler_->queryGuildInfo(guildId); } static const std::string kEmptyString; const std::string& GameHandler::lookupGuildName(uint32_t guildId) { static const std::string kEmpty; if (socialHandler_) return socialHandler_->lookupGuildName(guildId); return kEmpty; } uint32_t GameHandler::getEntityGuildId(uint64_t guid) const { if (socialHandler_) return socialHandler_->getEntityGuildId(guid); return 0; } void GameHandler::createGuild(const std::string& guildName) { if (socialHandler_) socialHandler_->createGuild(guildName); } void GameHandler::addGuildRank(const std::string& rankName) { if (socialHandler_) socialHandler_->addGuildRank(rankName); } void GameHandler::deleteGuildRank() { if (socialHandler_) socialHandler_->deleteGuildRank(); } void GameHandler::requestPetitionShowlist(uint64_t npcGuid) { if (socialHandler_) socialHandler_->requestPetitionShowlist(npcGuid); } void GameHandler::buyPetition(uint64_t npcGuid, const std::string& guildName) { if (socialHandler_) socialHandler_->buyPetition(npcGuid, guildName); } void GameHandler::signPetition(uint64_t petitionGuid) { if (socialHandler_) socialHandler_->signPetition(petitionGuid); } void GameHandler::turnInPetition(uint64_t petitionGuid) { if (socialHandler_) socialHandler_->turnInPetition(petitionGuid); } // ============================================================ // Loot, Gossip, Vendor // ============================================================ void GameHandler::lootTarget(uint64_t guid) { if (inventoryHandler_) inventoryHandler_->lootTarget(guid); } void GameHandler::lootItem(uint8_t slotIndex) { if (inventoryHandler_) inventoryHandler_->lootItem(slotIndex); } void GameHandler::closeLoot() { if (inventoryHandler_) inventoryHandler_->closeLoot(); } void GameHandler::lootMasterGive(uint8_t lootSlot, uint64_t targetGuid) { if (inventoryHandler_) inventoryHandler_->lootMasterGive(lootSlot, targetGuid); } void GameHandler::interactWithNpc(uint64_t guid) { if (!isInWorld()) return; auto packet = GossipHelloPacket::build(guid); socket->send(packet); } void GameHandler::interactWithGameObject(uint64_t guid) { LOG_WARNING("[GO-DIAG] interactWithGameObject called: guid=0x", std::hex, guid, std::dec); if (guid == 0) { LOG_WARNING("[GO-DIAG] BLOCKED: guid==0"); return; } if (!isInWorld()) { LOG_WARNING("[GO-DIAG] BLOCKED: not in world"); return; } // Do not overlap an actual spell cast. if (spellHandler_ && spellHandler_->casting_ && spellHandler_->currentCastSpellId_ != 0) { LOG_WARNING("[GO-DIAG] BLOCKED: already casting spellId=", spellHandler_->currentCastSpellId_); return; } // Always clear melee intent before GO interactions. stopAutoAttack(); // Set the pending GO guid so that: // 1. cancelCast() won't send CMSG_CANCEL_CAST for GO-triggered casts // (e.g., "Opening" on a quest chest) — without this, any movement // during the cast cancels it server-side and quest credit is lost. // 2. The cast-completion fallback in update() can call // performGameObjectInteractionNow after the cast timer expires. // 3. isGameObjectInteractionCasting() returns true during GO casts. pendingGameObjectInteractGuid_ = guid; performGameObjectInteractionNow(guid); } void GameHandler::performGameObjectInteractionNow(uint64_t guid) { if (guid == 0) return; if (!isInWorld()) return; // Rate-limit to prevent spamming the server static uint64_t lastInteractGuid = 0; static std::chrono::steady_clock::time_point lastInteractTime{}; auto now = std::chrono::steady_clock::now(); // Keep duplicate suppression, but allow quick retry clicks. constexpr int64_t minRepeatMs = 150; if (guid == lastInteractGuid && std::chrono::duration_cast(now - lastInteractTime).count() < minRepeatMs) { return; } lastInteractGuid = guid; lastInteractTime = now; // Ensure GO interaction isn't blocked by stale or active melee state. stopAutoAttack(); auto entity = entityController_->getEntityManager().getEntity(guid); uint32_t goEntry = 0; uint32_t goType = 0; std::string goName; if (entity) { if (entity->getType() == ObjectType::GAMEOBJECT) { auto go = std::static_pointer_cast(entity); goEntry = go->getEntry(); goName = go->getName(); if (auto* info = getCachedGameObjectInfo(goEntry)) goType = info->type; if (goType == 5 && !goName.empty()) { std::string lower = goName; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (lower.rfind("doodad_", 0) != 0) { addSystemChatMessage(goName); } } } // Face object and send heartbeat before use so strict servers don't require // a nudge movement to accept interaction. float dx = entity->getX() - movementInfo.x; float dy = entity->getY() - movementInfo.y; float dz = entity->getZ() - movementInfo.z; float dist3d = std::sqrt(dx * dx + dy * dy + dz * dz); if (dist3d > 10.0f) { addSystemChatMessage("Too far away."); return; } // Stop movement before interacting — servers may reject GO use or // immediately cancel the resulting spell cast if the player is moving. const uint32_t moveFlags = movementInfo.flags; const bool isMoving = (moveFlags & 0x00000001u) || // FORWARD (moveFlags & 0x00000002u) || // BACKWARD (moveFlags & 0x00000004u) || // STRAFE_LEFT (moveFlags & 0x00000008u); // STRAFE_RIGHT if (isMoving) { movementInfo.flags &= ~0x0000000Fu; // clear directional movement flags sendMovement(Opcode::MSG_MOVE_STOP); } if (std::abs(dx) > 0.01f || std::abs(dy) > 0.01f) { movementInfo.orientation = std::atan2(-dy, dx); sendMovement(Opcode::MSG_MOVE_SET_FACING); } sendMovement(Opcode::MSG_MOVE_HEARTBEAT); } // Determine GO type for interaction strategy bool isMailbox = false; bool chestLike = false; if (entity && entity->getType() == ObjectType::GAMEOBJECT) { auto go = std::static_pointer_cast(entity); auto* info = getCachedGameObjectInfo(go->getEntry()); if (info && info->type == 19) { isMailbox = true; } else if (info && info->type == 3) { chestLike = true; } } if (!chestLike && !goName.empty()) { std::string lower = goName; std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); chestLike = (lower.find("chest") != std::string::npos || lower.find("lockbox") != std::string::npos || lower.find("strongbox") != std::string::npos || lower.find("coffer") != std::string::npos || lower.find("cache") != std::string::npos || lower.find("bundle") != std::string::npos); } LOG_INFO("GO interaction: guid=0x", std::hex, guid, std::dec, " entry=", goEntry, " type=", goType, " name='", goName, "' chestLike=", chestLike, " isMailbox=", isMailbox); // Always send CMSG_GAMEOBJ_USE first — this triggers the server-side // GameObject::Use() handler for all GO types. auto usePacket = GameObjectUsePacket::build(guid); socket->send(usePacket); lastInteractedGoGuid_ = guid; if (chestLike) { // Don't send CMSG_LOOT immediately — the server may start a timed cast // (e.g., "Opening") and the GO isn't lootable until the cast finishes. // Sending LOOT prematurely gets an empty response or is silently dropped, // which can interfere with the server's loot state machine. // Instead, handleSpellGo will send LOOT after the cast completes // (using lastInteractedGoGuid_ set above). For instant-open chests // (no cast), the server sends SMSG_LOOT_RESPONSE directly after USE. } else if (isMailbox) { openMailbox(guid); } // CMSG_GAMEOBJ_REPORT_USE triggers GO AI scripts (SmartAI, ScriptAI) which // is where many quest objectives grant credit. Previously this was only sent // for non-chest GOs, so chest-type quest objectives (Bundle of Wood, etc.) // never triggered the server-side quest credit script. if (!isMailbox) { const auto* table = getActiveOpcodeTable(); if (table && table->hasOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)) { network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)); reportUse.writeUInt64(guid); socket->send(reportUse); } } } void GameHandler::selectGossipOption(uint32_t optionId) { if (questHandler_) questHandler_->selectGossipOption(optionId); } void GameHandler::selectGossipQuest(uint32_t questId) { if (questHandler_) questHandler_->selectGossipQuest(questId); } bool GameHandler::requestQuestQuery(uint32_t questId, bool force) { return questHandler_ && questHandler_->requestQuestQuery(questId, force); } bool GameHandler::hasQuestInLog(uint32_t questId) const { return questHandler_ && questHandler_->hasQuestInLog(questId); } Unit* GameHandler::getUnitByGuid(uint64_t guid) { auto entity = entityController_->getEntityManager().getEntity(guid); return entity ? dynamic_cast(entity.get()) : nullptr; } std::string GameHandler::guidToUnitId(uint64_t guid) const { if (guid == playerGuid) return "player"; if (guid == targetGuid) return "target"; if (guid == focusGuid) return "focus"; if (guid == petGuid_) return "pet"; return {}; } std::string GameHandler::getQuestTitle(uint32_t questId) const { for (const auto& q : questLog_) if (q.questId == questId && !q.title.empty()) return q.title; return {}; } const GameHandler::QuestLogEntry* GameHandler::findQuestLogEntry(uint32_t questId) const { for (const auto& q : questLog_) if (q.questId == questId) return &q; return nullptr; } int GameHandler::findQuestLogSlotIndexFromServer(uint32_t questId) const { if (questHandler_) return questHandler_->findQuestLogSlotIndexFromServer(questId); return 0; } void GameHandler::addQuestToLocalLogIfMissing(uint32_t questId, const std::string& title, const std::string& objectives) { if (questHandler_) questHandler_->addQuestToLocalLogIfMissing(questId, title, objectives); } bool GameHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) { return questHandler_ && questHandler_->resyncQuestLogFromServerSlots(forceQueryMetadata); } // Apply quest completion state from player update fields to already-tracked local quests. // Called from VALUES update handler so quests that complete mid-session (or that were // complete on login) get quest.complete=true without waiting for SMSG_QUESTUPDATE_COMPLETE. void GameHandler::applyQuestStateFromFields(const std::map& fields) { if (questHandler_) questHandler_->applyQuestStateFromFields(fields); } // Extract packed 6-bit kill/objective counts from WotLK/TBC/Classic quest-log update fields // and populate quest.killCounts + quest.itemCounts using the structured objectives obtained // from a prior SMSG_QUEST_QUERY_RESPONSE. Silently does nothing if objectives are absent. void GameHandler::applyPackedKillCountsFromFields(QuestLogEntry& quest) { if (questHandler_) questHandler_->applyPackedKillCountsFromFields(quest); } void GameHandler::clearPendingQuestAccept(uint32_t questId) { if (questHandler_) questHandler_->clearPendingQuestAccept(questId); } void GameHandler::triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason) { if (questHandler_) questHandler_->triggerQuestAcceptResync(questId, npcGuid, reason); } void GameHandler::acceptQuest() { if (questHandler_) questHandler_->acceptQuest(); } void GameHandler::declineQuest() { if (questHandler_) questHandler_->declineQuest(); } void GameHandler::abandonQuest(uint32_t questId) { if (questHandler_) questHandler_->abandonQuest(questId); } void GameHandler::shareQuestWithParty(uint32_t questId) { if (questHandler_) questHandler_->shareQuestWithParty(questId); } void GameHandler::completeQuest() { if (questHandler_) questHandler_->completeQuest(); } void GameHandler::closeQuestRequestItems() { if (questHandler_) questHandler_->closeQuestRequestItems(); } void GameHandler::chooseQuestReward(uint32_t rewardIndex) { if (questHandler_) questHandler_->chooseQuestReward(rewardIndex); } void GameHandler::closeQuestOfferReward() { if (questHandler_) questHandler_->closeQuestOfferReward(); } void GameHandler::closeGossip() { if (questHandler_) questHandler_->closeGossip(); } void GameHandler::offerQuestFromItem(uint64_t itemGuid, uint32_t questId) { if (questHandler_) questHandler_->offerQuestFromItem(itemGuid, questId); } uint64_t GameHandler::getBagItemGuid(int bagIndex, int slotIndex) const { if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return 0; if (slotIndex < 0) return 0; uint64_t bagGuid = equipSlotGuids_[Inventory::FIRST_BAG_EQUIP_SLOT + bagIndex]; if (bagGuid == 0) return 0; auto it = containerContents_.find(bagGuid); if (it == containerContents_.end()) return 0; if (slotIndex >= static_cast(it->second.numSlots)) return 0; return it->second.slotGuids[slotIndex]; } void GameHandler::openVendor(uint64_t npcGuid) { if (inventoryHandler_) inventoryHandler_->openVendor(npcGuid); } void GameHandler::closeVendor() { if (inventoryHandler_) inventoryHandler_->closeVendor(); } void GameHandler::buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count) { if (inventoryHandler_) inventoryHandler_->buyItem(vendorGuid, itemId, slot, count); } void GameHandler::buyBackItem(uint32_t buybackSlot) { if (inventoryHandler_) inventoryHandler_->buyBackItem(buybackSlot); } void GameHandler::repairItem(uint64_t vendorGuid, uint64_t itemGuid) { if (inventoryHandler_) inventoryHandler_->repairItem(vendorGuid, itemGuid); } void GameHandler::repairAll(uint64_t vendorGuid, bool useGuildBank) { if (inventoryHandler_) inventoryHandler_->repairAll(vendorGuid, useGuildBank); } uint32_t GameHandler::estimateRepairAllCost() const { if (inventoryHandler_) return inventoryHandler_->estimateRepairAllCost(); return 0; } void GameHandler::sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count) { if (inventoryHandler_) inventoryHandler_->sellItem(vendorGuid, itemGuid, count); } void GameHandler::sellItemBySlot(int backpackIndex) { if (inventoryHandler_) inventoryHandler_->sellItemBySlot(backpackIndex); } void GameHandler::autoEquipItemBySlot(int backpackIndex) { if (inventoryHandler_) inventoryHandler_->autoEquipItemBySlot(backpackIndex); } void GameHandler::autoEquipItemInBag(int bagIndex, int slotIndex) { if (inventoryHandler_) inventoryHandler_->autoEquipItemInBag(bagIndex, slotIndex); } void GameHandler::sellItemInBag(int bagIndex, int slotIndex) { if (inventoryHandler_) inventoryHandler_->sellItemInBag(bagIndex, slotIndex); } void GameHandler::unequipToBackpack(EquipSlot equipSlot) { if (inventoryHandler_) inventoryHandler_->unequipToBackpack(equipSlot); } void GameHandler::swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot) { if (inventoryHandler_) inventoryHandler_->swapContainerItems(srcBag, srcSlot, dstBag, dstSlot); } void GameHandler::swapBagSlots(int srcBagIndex, int dstBagIndex) { if (inventoryHandler_) inventoryHandler_->swapBagSlots(srcBagIndex, dstBagIndex); } void GameHandler::destroyItem(uint8_t bag, uint8_t slot, uint8_t count) { if (inventoryHandler_) inventoryHandler_->destroyItem(bag, slot, count); } void GameHandler::splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count) { if (inventoryHandler_) inventoryHandler_->splitItem(srcBag, srcSlot, count); } void GameHandler::useItemBySlot(int backpackIndex) { if (inventoryHandler_) inventoryHandler_->useItemBySlot(backpackIndex); } void GameHandler::useItemInBag(int bagIndex, int slotIndex) { if (inventoryHandler_) inventoryHandler_->useItemInBag(bagIndex, slotIndex); } void GameHandler::openItemBySlot(int backpackIndex) { if (inventoryHandler_) inventoryHandler_->openItemBySlot(backpackIndex); } void GameHandler::openItemInBag(int bagIndex, int slotIndex) { if (inventoryHandler_) inventoryHandler_->openItemInBag(bagIndex, slotIndex); } void GameHandler::useItemById(uint32_t itemId) { if (inventoryHandler_) inventoryHandler_->useItemById(itemId); } uint32_t GameHandler::getItemIdForSpell(uint32_t spellId) const { if (spellId == 0) return 0; // Search backpack and bags for an item whose on-use spell matches for (int i = 0; i < inventory.getBackpackSize(); i++) { const auto& slot = inventory.getBackpackSlot(i); if (slot.empty()) continue; auto* info = getItemInfo(slot.item.itemId); if (!info || !info->valid) continue; for (const auto& sp : info->spells) { if (sp.spellId == spellId && (sp.spellTrigger == 0 || sp.spellTrigger == 5)) return slot.item.itemId; } } for (int bag = 0; bag < inventory.NUM_BAG_SLOTS; bag++) { for (int s = 0; s < inventory.getBagSize(bag); s++) { const auto& slot = inventory.getBagSlot(bag, s); if (slot.empty()) continue; auto* info = getItemInfo(slot.item.itemId); if (!info || !info->valid) continue; for (const auto& sp : info->spells) { if (sp.spellId == spellId && (sp.spellTrigger == 0 || sp.spellTrigger == 5)) return slot.item.itemId; } } } return 0; } void GameHandler::unstuck() { if (unstuckCallback_) { unstuckCallback_(); addSystemChatMessage("Unstuck: snapped upward. Use /unstuckgy for full teleport."); } } void GameHandler::unstuckGy() { if (unstuckGyCallback_) { unstuckGyCallback_(); addSystemChatMessage("Unstuck: teleported to safe location."); } } void GameHandler::unstuckHearth() { if (unstuckHearthCallback_) { unstuckHearthCallback_(); addSystemChatMessage("Unstuck: teleported to hearthstone location."); } else { addSystemChatMessage("No hearthstone bind point set."); } } // ============================================================ // Trainer // ============================================================ void GameHandler::trainSpell(uint32_t spellId) { if (inventoryHandler_) inventoryHandler_->trainSpell(spellId); } void GameHandler::closeTrainer() { if (inventoryHandler_) inventoryHandler_->closeTrainer(); } void GameHandler::preloadDBCCaches() const { LOG_INFO("Pre-loading DBC caches during world entry..."); auto t0 = std::chrono::steady_clock::now(); loadSpellNameCache(); // Spell.dbc — largest, ~170ms cold loadTitleNameCache(); // CharTitles.dbc loadFactionNameCache(); // Faction.dbc loadAreaNameCache(); // WorldMapArea.dbc loadMapNameCache(); // Map.dbc loadLfgDungeonDbc(); // LFGDungeons.dbc // Validate animation constants against AnimationData.dbc if (auto* am = services_.assetManager) { auto animDbc = am->loadDBC("AnimationData.dbc"); rendering::anim::validateAgainstDBC(animDbc); } auto elapsed = std::chrono::duration_cast( std::chrono::steady_clock::now() - t0).count(); LOG_INFO("DBC cache pre-load complete in ", elapsed, " ms"); } void GameHandler::loadSpellNameCache() const { if (spellHandler_) spellHandler_->loadSpellNameCache(); } void GameHandler::loadSkillLineAbilityDbc() { if (spellHandler_) spellHandler_->loadSkillLineAbilityDbc(); } const std::vector& GameHandler::getSpellBookTabs() { static const std::vector kEmpty; if (spellHandler_) return spellHandler_->getSpellBookTabs(); return kEmpty; } void GameHandler::categorizeTrainerSpells() { if (spellHandler_) spellHandler_->categorizeTrainerSpells(); } void GameHandler::loadTalentDbc() { if (spellHandler_) spellHandler_->loadTalentDbc(); } static const std::string EMPTY_STRING; const int32_t* GameHandler::getSpellEffectBasePoints(uint32_t spellId) const { if (spellHandler_) return spellHandler_->getSpellEffectBasePoints(spellId); return nullptr; } float GameHandler::getSpellDuration(uint32_t spellId) const { if (spellHandler_) return spellHandler_->getSpellDuration(spellId); return 0.0f; } const std::string& GameHandler::getSpellName(uint32_t spellId) const { if (spellHandler_) return spellHandler_->getSpellName(spellId); return EMPTY_STRING; } const std::string& GameHandler::getSpellRank(uint32_t spellId) const { if (spellHandler_) return spellHandler_->getSpellRank(spellId); return EMPTY_STRING; } const std::string& GameHandler::getSpellDescription(uint32_t spellId) const { if (spellHandler_) return spellHandler_->getSpellDescription(spellId); return EMPTY_STRING; } std::string GameHandler::getEnchantName(uint32_t enchantId) const { if (spellHandler_) return spellHandler_->getEnchantName(enchantId); return {}; } uint8_t GameHandler::getSpellDispelType(uint32_t spellId) const { if (spellHandler_) return spellHandler_->getSpellDispelType(spellId); return 0; } bool GameHandler::isSpellInterruptible(uint32_t spellId) const { if (spellHandler_) return spellHandler_->isSpellInterruptible(spellId); return true; } uint32_t GameHandler::getSpellSchoolMask(uint32_t spellId) const { if (spellHandler_) return spellHandler_->getSpellSchoolMask(spellId); return 0; } const std::string& GameHandler::getSkillLineName(uint32_t spellId) const { if (spellHandler_) return spellHandler_->getSkillLineName(spellId); return EMPTY_STRING; } } // namespace game } // namespace wowee