mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-14 16:33:52 +00:00
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.
2467 lines
88 KiB
C++
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
|