Kelsidavis-WoWee/src/game/game_handler_callbacks.cpp
Kelsi 069dd36698 fix(parsing): bail on suspicious maskBlockCount in CREATE_OBJECT blocks
When spline parsing consumes the wrong number of bytes, the subsequent
blockCount read lands on garbage data (e.g. 71 instead of ~5 for UNIT).
Previously the parser logged a warning but continued, reading garbage
mask/field data until hitting truncation. Now it returns false for
CREATE_OBJECT blocks with suspicious counts, letting the block loop
skip cleanly to the next entity.

Also downgrade ~44 diagnostic LOG_WARNING messages to LOG_DEBUG across
17 files (equipment, transport, DBC, heartbeat, chat, GO raypick, etc.)
to reduce log noise and make real warnings visible.
2026-04-05 20:12:17 -07:00

2467 lines
88 KiB
C++

#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 <glm/gtx/quaternion.hpp>
#include <algorithm>
#include <cmath>
#include <cctype>
#include <ctime>
#include <random>
#include <zlib.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#include <array>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <openssl/sha.h>
#include <openssl/hmac.h>
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_DEBUG("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<int>(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<int>(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<int>(data.result)) + ")"; break;
}
LOG_WARNING("Character creation failed: ", msg, " (code=", static_cast<int>(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<int>(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<int>(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<int>(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<float>(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_->resetMovementClock();
}
movementInfo.time = nextMovementTimestampMs();
if (movementHandler_) {
movementHandler_->setFalling(false);
movementHandler_->setFallStartMs(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<uint32_t, 8> 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::EquipmentSetInfo>& GameHandler::getEquipmentSets() const {
if (inventoryHandler_) return inventoryHandler_->getEquipmentSets();
static const std::vector<EquipmentSetInfo> empty;
return empty;
}
// Trade state delegation to InventoryHandler (which owns the canonical trade state)
GameHandler::TradeStatus GameHandler::getTradeStatus() const {
if (inventoryHandler_) return static_cast<TradeStatus>(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::TradeSlot, GameHandler::TRADE_SLOT_COUNT>& GameHandler::getMyTradeSlots() const {
if (inventoryHandler_) {
// Convert InventoryHandler::TradeSlot → GameHandler::TradeSlot (different struct layouts)
static std::array<TradeSlot, TRADE_SLOT_COUNT> 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::TradeSlot, GameHandler::TRADE_SLOT_COUNT>& GameHandler::getPeerTradeSlots() const {
if (inventoryHandler_) {
static std::array<TradeSlot, TRADE_SLOT_COUNT> 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<uint64_t>& 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::BuybackItem>& GameHandler::getBuybackItems() const {
if (inventoryHandler_) {
// Layout-identical structs (InventoryHandler::BuybackItem == GameHandler::BuybackItem)
return reinterpret_cast<const std::deque<BuybackItem>&>(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<MailMessage>& 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::MailAttachSlot, 12>& GameHandler::getMailAttachments() const {
if (inventoryHandler_) {
// Layout-identical structs (InventoryHandler::MailAttachSlot == GameHandler::MailAttachSlot)
return reinterpret_cast<const std::array<MailAttachSlot, 12>&>(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::TrainerTab>& GameHandler::getTrainerTabs() const {
if (inventoryHandler_) {
// Layout-identical structs (InventoryHandler::TrainerTab == GameHandler::TrainerTab)
return reinterpret_cast<const std::vector<TrainerTab>&>(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<uint32_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(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<Entity> 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<Entity> 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<MessageChatData>& GameHandler::getChatHistory() const {
if (chatHandler_) return chatHandler_->getChatHistory();
static const std::deque<MessageChatData> kEmpty;
return kEmpty;
}
void GameHandler::clearChatHistory() {
if (chatHandler_) chatHandler_->getChatHistory().clear();
}
const std::vector<std::string>& GameHandler::getJoinedChannels() const {
if (chatHandler_) return chatHandler_->getJoinedChannels();
static const std::vector<std::string> 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<uint16_t, uint32_t>& fields) {
if (inventoryHandler_) inventoryHandler_->detectInventorySlotBases(fields);
}
bool GameHandler::applyInventoryFields(const std::map<uint16_t, uint32_t>& fields) {
return inventoryHandler_ ? inventoryHandler_->applyInventoryFields(fields) : false;
}
void GameHandler::extractContainerFields(uint64_t containerGuid, const std::map<uint16_t, uint32_t>& 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<uint16_t, uint32_t>& 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<CombatTextEntry>& GameHandler::getCombatText() const {
static const std::vector<CombatTextEntry> empty;
return combatHandler_ ? combatHandler_->getCombatText() : empty;
}
const std::deque<CombatLogEntry>& GameHandler::getCombatLog() const {
static const std::deque<CombatLogEntry> 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::ThreatEntry>* GameHandler::getThreatList(uint64_t unitGuid) const {
return combatHandler_ ? combatHandler_->getThreatList(unitGuid) : nullptr;
}
const std::vector<GameHandler::ThreatEntry>* 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<uint8_t>(slot),
static_cast<uint8_t>(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_DEBUG("[GO-DIAG] interactWithGameObject called: guid=0x", std::hex, guid, std::dec);
if (guid == 0) { LOG_DEBUG("[GO-DIAG] BLOCKED: guid==0"); return; }
if (!isInWorld()) { LOG_DEBUG("[GO-DIAG] BLOCKED: not in world"); return; }
// Do not overlap an actual spell cast.
if (spellHandler_ && spellHandler_->isCasting() && spellHandler_->getCurrentCastSpellId() != 0) {
LOG_DEBUG("[GO-DIAG] BLOCKED: already casting spellId=", spellHandler_->getCurrentCastSpellId());
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<std::chrono::milliseconds>(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<GameObject>(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<char>(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<GameObject>(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<char>(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<Unit*>(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<uint16_t, uint32_t>& 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<int>(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::milliseconds>(
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::SpellBookTab>& GameHandler::getSpellBookTabs() {
static const std::vector<SpellBookTab> 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