refactor(game): split GameHandler into domain handlers

Extract domain-specific logic from the monolithic GameHandler into
dedicated handler classes, each owning its own opcode registration,
state, and packet parsing:

- CombatHandler: combat, XP, kill, PvP, loot roll (~26 methods)
- SpellHandler: spells, auras, pet stable, talent (~3+ methods)
- SocialHandler: friends, guild, groups, BG, RAF, PvP AFK (~14+ methods)
- ChatHandler: chat messages, channels, GM tickets, server messages,
               defense/area-trigger messages (~7+ methods)
- InventoryHandler: items, trade, loot, mail, vendor, equipment sets,
                    read item (~3+ methods)
- QuestHandler: gossip, quests, completed quest response (~5+ methods)
- MovementHandler: movement, follow, transport (~2 methods)
- WardenHandler: Warden anti-cheat module

Each handler registers its own dispatch table entries via
registerOpcodes(DispatchTable&), called from
GameHandler::registerOpcodeHandlers(). GameHandler retains core
orchestration: auth/session handshake, update-object parsing,
opcode routing, and cross-handler coordination.

game_handler.cpp reduced from ~10,188 to ~9,432 lines.

Also add a POST_BUILD CMake step to symlink Data/ next to the
executable so expansion profiles and opcode tables are found at
runtime when running from build/bin/.
This commit is contained in:
Paul 2026-03-28 09:42:37 +03:00
parent 3762dceaa6
commit b2710258dc
21 changed files with 20771 additions and 17858 deletions

View file

@ -450,6 +450,14 @@ set(WOWEE_SOURCES
src/game/opcode_table.cpp
src/game/update_field_table.cpp
src/game/game_handler.cpp
src/game/chat_handler.cpp
src/game/movement_handler.cpp
src/game/combat_handler.cpp
src/game/spell_handler.cpp
src/game/inventory_handler.cpp
src/game/social_handler.cpp
src/game/quest_handler.cpp
src/game/warden_handler.cpp
src/game/warden_crypto.cpp
src/game/warden_module.cpp
src/game/warden_emulator.cpp
@ -884,6 +892,17 @@ add_custom_command(TARGET wowee POST_BUILD
COMMENT "Syncing assets to $<TARGET_FILE_DIR:wowee>/assets"
)
# Symlink Data/ next to the executable so expansion profiles, opcode tables,
# and other runtime data files are found when running from the build directory.
if(NOT WIN32)
add_custom_command(TARGET wowee POST_BUILD
COMMAND ${CMAKE_COMMAND} -E create_symlink
${CMAKE_CURRENT_SOURCE_DIR}/Data
$<TARGET_FILE_DIR:wowee>/Data
COMMENT "Symlinking Data to $<TARGET_FILE_DIR:wowee>/Data"
)
endif()
# On Windows, SDL 2.28+ uses LoadLibraryExW with LOAD_LIBRARY_SEARCH_DEFAULT_DIRS
# which does NOT include System32. Copy vulkan-1.dll into the output directory so
# SDL_Vulkan_LoadLibrary can locate it without needing a full system PATH search.

View file

@ -0,0 +1,73 @@
#pragma once
#include "game/world_packets.hpp"
#include "game/opcode_table.hpp"
#include "game/handler_types.hpp"
#include "network/packet.hpp"
#include <deque>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
namespace wowee {
namespace game {
class GameHandler;
class ChatHandler {
public:
using PacketHandler = std::function<void(network::Packet&)>;
using DispatchTable = std::unordered_map<LogicalOpcode, PacketHandler>;
explicit ChatHandler(GameHandler& owner);
void registerOpcodes(DispatchTable& table);
// --- Public API (delegated from GameHandler) ---
void sendChatMessage(ChatType type, const std::string& message, const std::string& target = "");
void sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid = 0);
void joinChannel(const std::string& channelName, const std::string& password = "");
void leaveChannel(const std::string& channelName);
std::string getChannelByIndex(int index) const;
int getChannelIndex(const std::string& channelName) const;
const std::vector<std::string>& getJoinedChannels() const { return joinedChannels_; }
void autoJoinDefaultChannels();
void addLocalChatMessage(const MessageChatData& msg);
void addSystemChatMessage(const std::string& message);
void toggleAfk(const std::string& message);
void toggleDnd(const std::string& message);
void replyToLastWhisper(const std::string& message);
// ---- Methods moved from GameHandler ----
void submitGmTicket(const std::string& text);
void handleMotd(network::Packet& packet);
void handleNotification(network::Packet& packet);
// --- State accessors ---
std::deque<MessageChatData>& getChatHistory() { return chatHistory_; }
const std::deque<MessageChatData>& getChatHistory() const { return chatHistory_; }
size_t getMaxChatHistory() const { return maxChatHistory_; }
void setMaxChatHistory(size_t n) { maxChatHistory_ = n; }
// Chat auto-join settings (aliased from handler_types.hpp)
using ChatAutoJoin = game::ChatAutoJoin;
ChatAutoJoin chatAutoJoin;
private:
// --- Packet handlers ---
void handleMessageChat(network::Packet& packet);
void handleTextEmote(network::Packet& packet);
void handleChannelNotify(network::Packet& packet);
void handleChannelList(network::Packet& packet);
GameHandler& owner_;
// --- State ---
std::deque<MessageChatData> chatHistory_;
size_t maxChatHistory_ = 100;
std::vector<std::string> joinedChannels_;
};
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,191 @@
#pragma once
#include "game/world_packets.hpp"
#include "game/opcode_table.hpp"
#include "game/spell_defines.hpp"
#include "network/packet.hpp"
#include <chrono>
#include <deque>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace wowee {
namespace game {
class GameHandler;
class Entity;
class CombatHandler {
public:
using PacketHandler = std::function<void(network::Packet&)>;
using DispatchTable = std::unordered_map<LogicalOpcode, PacketHandler>;
explicit CombatHandler(GameHandler& owner);
void registerOpcodes(DispatchTable& table);
// --- Public API (delegated from GameHandler) ---
void startAutoAttack(uint64_t targetGuid);
void stopAutoAttack();
bool isAutoAttacking() const { return autoAttacking_; }
bool hasAutoAttackIntent() const { return autoAttackRequested_; }
bool isInCombat() const { return autoAttacking_ || !hostileAttackers_.empty(); }
bool isInCombatWith(uint64_t guid) const {
return guid != 0 &&
((autoAttacking_ && autoAttackTarget_ == guid) ||
(hostileAttackers_.count(guid) > 0));
}
uint64_t getAutoAttackTargetGuid() const { return autoAttackTarget_; }
bool isAggressiveTowardPlayer(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
uint64_t getLastMeleeSwingMs() const { return lastMeleeSwingMs_; }
// Floating combat text
const std::vector<CombatTextEntry>& getCombatText() const { return combatText_; }
void updateCombatText(float deltaTime);
// Combat log (persistent rolling history)
const std::deque<CombatLogEntry>& getCombatLog() const { return combatLog_; }
void clearCombatLog() { combatLog_.clear(); }
// Threat
struct ThreatEntry {
uint64_t victimGuid = 0;
uint32_t threat = 0;
};
const std::vector<ThreatEntry>* getThreatList(uint64_t unitGuid) const {
auto it = threatLists_.find(unitGuid);
return (it != threatLists_.end()) ? &it->second : nullptr;
}
// Hostile attacker tracking
bool isHostileAttacker(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
void clearHostileAttackers() { hostileAttackers_.clear(); }
// Forced faction reactions
const std::unordered_map<uint32_t, uint8_t>& getForcedReactions() const { return forcedReactions_; }
// Auto-attack timing state (read by GameHandler::update for retry/resend logic)
bool& autoAttackOutOfRangeRef() { return autoAttackOutOfRange_; }
float& autoAttackOutOfRangeTimeRef() { return autoAttackOutOfRangeTime_; }
float& autoAttackRangeWarnCooldownRef() { return autoAttackRangeWarnCooldown_; }
float& autoAttackResendTimerRef() { return autoAttackResendTimer_; }
float& autoAttackFacingSyncTimerRef() { return autoAttackFacingSyncTimer_; }
bool& autoAttackRetryPendingRef() { return autoAttackRetryPending_; }
// Combat text creation (used by other handlers, e.g. spell handler for periodic damage)
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId,
bool isPlayerSource, uint8_t powerType = 0,
uint64_t srcGuid = 0, uint64_t dstGuid = 0);
// Spellsteal dedup (used by aura update handler)
bool shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId);
// Called from GameHandler::update() each frame
void updateAutoAttack(float deltaTime);
// --- Targeting ---
void setTarget(uint64_t guid);
void clearTarget();
std::shared_ptr<Entity> getTarget() const;
void setFocus(uint64_t guid);
void clearFocus();
std::shared_ptr<Entity> getFocus() const;
void setMouseoverGuid(uint64_t guid);
void targetLastTarget();
void targetEnemy(bool reverse);
void targetFriend(bool reverse);
void tabTarget(float playerX, float playerY, float playerZ);
void assistTarget();
// --- PvP ---
void togglePvp();
// --- Death / Resurrection ---
void releaseSpirit();
bool canReclaimCorpse() const;
float getCorpseReclaimDelaySec() const;
void reclaimCorpse();
void useSelfRes();
void activateSpiritHealer(uint64_t npcGuid);
void acceptResurrect();
void declineResurrect();
// --- XP ---
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
void handleXpGain(network::Packet& packet);
// State management (for resets, entity cleanup)
void resetAllCombatState();
void removeHostileAttacker(uint64_t guid);
void clearCombatText();
void removeCombatTextForGuid(uint64_t guid);
private:
// --- Packet handlers ---
void handleAttackStart(network::Packet& packet);
void handleAttackStop(network::Packet& packet);
void handleAttackerStateUpdate(network::Packet& packet);
void handleSpellDamageLog(network::Packet& packet);
void handleSpellHealLog(network::Packet& packet);
void handleSetForcedReactions(network::Packet& packet);
void handleHealthUpdate(network::Packet& packet);
void handlePowerUpdate(network::Packet& packet);
void handleUpdateComboPoints(network::Packet& packet);
void handlePvpCredit(network::Packet& packet);
void handleProcResist(network::Packet& packet);
void handleEnvironmentalDamageLog(network::Packet& packet);
void handleSpellDamageShield(network::Packet& packet);
void handleSpellOrDamageImmune(network::Packet& packet);
void handleResistLog(network::Packet& packet);
void handlePetTameFailure(network::Packet& packet);
void handlePetActionFeedback(network::Packet& packet);
void handlePetCastFailed(network::Packet& packet);
void handlePetBroken(network::Packet& packet);
void handlePetLearnedSpell(network::Packet& packet);
void handlePetUnlearnedSpell(network::Packet& packet);
void handlePetMode(network::Packet& packet);
void handleResurrectFailed(network::Packet& packet);
void autoTargetAttacker(uint64_t attackerGuid);
GameHandler& owner_;
// --- Combat state ---
bool autoAttacking_ = false;
bool autoAttackRequested_ = false;
bool autoAttackRetryPending_ = false;
uint64_t autoAttackTarget_ = 0;
bool autoAttackOutOfRange_ = false;
float autoAttackOutOfRangeTime_ = 0.0f;
float autoAttackRangeWarnCooldown_ = 0.0f;
float autoAttackResendTimer_ = 0.0f;
float autoAttackFacingSyncTimer_ = 0.0f;
std::unordered_set<uint64_t> hostileAttackers_;
std::vector<CombatTextEntry> combatText_;
static constexpr size_t MAX_COMBAT_LOG = 500;
std::deque<CombatLogEntry> combatLog_;
struct RecentSpellstealLogEntry {
uint64_t casterGuid = 0;
uint64_t victimGuid = 0;
uint32_t spellId = 0;
std::chrono::steady_clock::time_point timestamp{};
};
static constexpr size_t MAX_RECENT_SPELLSTEAL_LOGS = 32;
std::deque<RecentSpellstealLogEntry> recentSpellstealLogs_;
uint64_t lastMeleeSwingMs_ = 0;
// unitGuid → sorted threat list (descending by threat value)
std::unordered_map<uint64_t, std::vector<ThreatEntry>> threatLists_;
// Forced faction reactions
std::unordered_map<uint32_t, uint8_t> forcedReactions_;
};
} // namespace game
} // namespace wowee

View file

@ -7,6 +7,10 @@
#include "game/inventory.hpp"
#include "game/spell_defines.hpp"
#include "game/group_defines.hpp"
#include "game/handler_types.hpp"
#include "game/combat_handler.hpp"
#include "game/spell_handler.hpp"
#include "game/quest_handler.hpp"
#include "network/packet.hpp"
#include <glm/glm.hpp>
#include <memory>
@ -31,6 +35,11 @@ namespace wowee::game {
class WardenModule;
class WardenModuleManager;
class PacketParsers;
class ChatHandler;
class MovementHandler;
class InventoryHandler;
class SocialHandler;
class WardenHandler;
}
namespace wowee {
@ -115,25 +124,9 @@ using WorldConnectFailureCallback = std::function<void(const std::string& reason
*/
class GameHandler {
public:
// Talent data structures (must be public for use in templates)
struct TalentEntry {
uint32_t talentId = 0;
uint32_t tabId = 0; // Which talent tree
uint8_t row = 0; // Tier (0-10)
uint8_t column = 0; // Column (0-3)
uint32_t rankSpells[5] = {}; // Spell IDs for ranks 1-5
uint32_t prereqTalent[3] = {}; // Required talents
uint8_t prereqRank[3] = {}; // Required ranks
uint8_t maxRank = 0; // Number of ranks (1-5)
};
struct TalentTabEntry {
uint32_t tabId = 0;
std::string name;
uint32_t classMask = 0; // Which classes can use this tab
uint8_t orderIndex = 0; // Display order (0-2)
std::string backgroundFile; // Texture path
};
// Talent data structures (aliased from handler_types.hpp)
using TalentEntry = game::TalentEntry;
using TalentTabEntry = game::TalentTabEntry;
GameHandler();
~GameHandler();
@ -262,19 +255,14 @@ public:
void sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid = 0);
void joinChannel(const std::string& channelName, const std::string& password = "");
void leaveChannel(const std::string& channelName);
const std::vector<std::string>& getJoinedChannels() const { return joinedChannels_; }
const std::vector<std::string>& getJoinedChannels() const;
std::string getChannelByIndex(int index) const;
int getChannelIndex(const std::string& channelName) const;
// Chat auto-join settings (set by UI before autoJoinDefaultChannels)
struct ChatAutoJoin {
bool general = true;
bool trade = true;
bool localDefense = true;
bool lfg = true;
bool local = true;
};
// Chat auto-join settings (aliased from handler_types.hpp)
using ChatAutoJoin = game::ChatAutoJoin;
ChatAutoJoin chatAutoJoin;
void autoJoinDefaultChannels();
// Chat bubble callback: (senderGuid, message, isYell)
using ChatBubbleCallback = std::function<void(uint64_t, const std::string&, bool)>;
@ -327,8 +315,8 @@ public:
* @param maxMessages Maximum number of messages to return (0 = all)
* @return Vector of chat messages
*/
const std::deque<MessageChatData>& getChatHistory() const { return chatHistory; }
void clearChatHistory() { chatHistory.clear(); }
const std::deque<MessageChatData>& getChatHistory() const;
void clearChatHistory();
/**
* Add a locally-generated chat message (e.g., emote feedback)
@ -430,27 +418,8 @@ public:
// Inspection
void inspectTarget();
struct InspectArenaTeam {
uint32_t teamId = 0;
uint8_t type = 0; // bracket size: 2, 3, or 5
uint32_t weekGames = 0;
uint32_t weekWins = 0;
uint32_t seasonGames = 0;
uint32_t seasonWins = 0;
std::string name;
uint32_t personalRating = 0;
};
struct InspectResult {
uint64_t guid = 0;
std::string playerName;
uint32_t totalTalents = 0;
uint32_t unspentTalents = 0;
uint8_t talentGroups = 0;
uint8_t activeTalentGroup = 0;
std::array<uint32_t, 19> itemEntries{}; // 0=head…18=ranged
std::array<uint16_t, 19> enchantIds{}; // permanent enchant per slot (0 = none)
std::vector<InspectArenaTeam> arenaTeams; // from MSG_INSPECT_ARENA_TEAMS (WotLK)
};
using InspectArenaTeam = game::InspectArenaTeam;
using InspectResult = game::InspectResult;
const InspectResult* getInspectResult() const {
return inspectResult_.guid ? &inspectResult_ : nullptr;
}
@ -462,15 +431,7 @@ public:
uint32_t getTotalTimePlayed() const { return totalTimePlayed_; }
uint32_t getLevelTimePlayed() const { return levelTimePlayed_; }
// Who results (structured, from last SMSG_WHO response)
struct WhoEntry {
std::string name;
std::string guildName;
uint32_t level = 0;
uint32_t classId = 0;
uint32_t raceId = 0;
uint32_t zoneId = 0;
};
using WhoEntry = game::WhoEntry;
const std::vector<WhoEntry>& getWhoResults() const { return whoResults_; }
uint32_t getWhoOnlineCount() const { return whoOnlineCount_; }
std::string getWhoAreaName(uint32_t zoneId) const { return getAreaName(zoneId); }
@ -486,28 +447,11 @@ public:
// Random roll
void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100);
// Battleground queue slot (public so UI can read invite details)
struct BgQueueSlot {
uint32_t queueSlot = 0;
uint32_t bgTypeId = 0;
uint8_t arenaType = 0;
uint32_t statusId = 0; // 0=none, 1=wait_queue, 2=wait_join, 3=in_progress
uint32_t inviteTimeout = 80;
uint32_t avgWaitTimeSec = 0; // server-estimated average wait (STATUS_WAIT_QUEUE)
uint32_t timeInQueueSec = 0; // time already spent in queue (STATUS_WAIT_QUEUE)
std::chrono::steady_clock::time_point inviteReceivedTime{};
std::string bgName; // human-readable BG/arena name
};
// Battleground queue slot (aliased from handler_types.hpp)
using BgQueueSlot = game::BgQueueSlot;
// Available BG list (populated by SMSG_BATTLEFIELD_LIST)
struct AvailableBgInfo {
uint32_t bgTypeId = 0;
bool isRegistered = false;
bool isHoliday = false;
uint32_t minLevel = 0;
uint32_t maxLevel = 0;
std::vector<uint32_t> instanceIds;
};
// Available BG list (aliased from handler_types.hpp)
using AvailableBgInfo = game::AvailableBgInfo;
// Battleground
bool hasPendingBgInvite() const;
@ -516,42 +460,17 @@ public:
const std::array<BgQueueSlot, 3>& getBgQueues() const { return bgQueues_; }
const std::vector<AvailableBgInfo>& getAvailableBgs() const { return availableBgs_; }
// BG scoreboard (MSG_PVP_LOG_DATA)
struct BgPlayerScore {
uint64_t guid = 0;
std::string name;
uint8_t team = 0; // 0=Horde, 1=Alliance
uint32_t killingBlows = 0;
uint32_t deaths = 0;
uint32_t honorableKills = 0;
uint32_t bonusHonor = 0;
std::vector<std::pair<std::string, uint32_t>> bgStats; // BG-specific fields
};
struct ArenaTeamScore {
std::string teamName;
uint32_t ratingChange = 0; // signed delta packed as uint32
uint32_t newRating = 0;
};
struct BgScoreboardData {
std::vector<BgPlayerScore> players;
bool hasWinner = false;
uint8_t winner = 0; // 0=Horde, 1=Alliance
bool isArena = false;
// Arena-only fields (valid when isArena=true)
ArenaTeamScore arenaTeams[2]; // team 0 = first, team 1 = second
};
// BG scoreboard (aliased from handler_types.hpp)
using BgPlayerScore = game::BgPlayerScore;
using ArenaTeamScore = game::ArenaTeamScore;
using BgScoreboardData = game::BgScoreboardData;
void requestPvpLog();
const BgScoreboardData* getBgScoreboard() const {
return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_;
}
// BG flag carrier / important player positions (MSG_BATTLEGROUND_PLAYER_POSITIONS)
struct BgPlayerPosition {
uint64_t guid = 0;
float wowX = 0.0f; // canonical WoW X (north)
float wowY = 0.0f; // canonical WoW Y (west)
int group = 0; // 0 = first list (usually ally flag carriers), 1 = second list
};
// BG flag carrier positions (aliased from handler_types.hpp)
using BgPlayerPosition = game::BgPlayerPosition;
const std::vector<BgPlayerPosition>& getBgPlayerPositions() const { return bgPlayerPositions_; }
// Network latency (milliseconds, updated each PONG response)
@ -656,19 +575,8 @@ public:
uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; }
// Petition signatures (guild charter signing flow)
struct PetitionSignature {
uint64_t playerGuid = 0;
std::string playerName; // resolved later or empty
};
struct PetitionInfo {
uint64_t petitionGuid = 0;
uint64_t ownerGuid = 0;
std::string guildName;
uint32_t signatureCount = 0;
uint32_t signaturesRequired = 9; // guild default; arena teams differ
std::vector<PetitionSignature> signatures;
bool showUI = false;
};
using PetitionSignature = game::PetitionSignature;
using PetitionInfo = game::PetitionInfo;
const PetitionInfo& getPetitionInfo() const { return petitionInfo_; }
bool hasPetitionSignaturesUI() const { return petitionInfo_.showUI; }
void clearPetitionSignaturesUI() { petitionInfo_.showUI = false; }
@ -682,11 +590,7 @@ public:
// Returns the guildId for a player entity (from PLAYER_GUILDID update field).
uint32_t getEntityGuildId(uint64_t guid) const;
// Ready check
struct ReadyCheckResult {
std::string name;
bool ready = false;
};
using ReadyCheckResult = game::ReadyCheckResult;
void initiateReadyCheck();
void respondToReadyCheck(bool ready);
bool hasPendingReadyCheck() const { return pendingReadyCheck_; }
@ -720,6 +624,8 @@ public:
void initiateTrade(uint64_t targetGuid);
void reportPlayer(uint64_t targetGuid, const std::string& reason);
void stopCasting();
void resetCastState(); // force-clear all cast/craft/queue state without sending packets
void clearUnitCaches(); // clear per-unit cast states and aura caches
// ---- Phase 1: Name queries ----
void queryPlayerName(uint64_t guid);
@ -753,28 +659,26 @@ public:
return (it != creatureInfoCache.end()) ? it->second.family : 0;
}
// ---- Phase 2: Combat ----
// ---- Phase 2: Combat (delegated to CombatHandler) ----
void startAutoAttack(uint64_t targetGuid);
void stopAutoAttack();
bool isAutoAttacking() const { return autoAttacking; }
bool hasAutoAttackIntent() const { return autoAttackRequested_; }
bool isInCombat() const { return autoAttacking || !hostileAttackers_.empty(); }
bool isInCombatWith(uint64_t guid) const {
return guid != 0 &&
((autoAttacking && autoAttackTarget == guid) ||
(hostileAttackers_.count(guid) > 0));
}
uint64_t getAutoAttackTargetGuid() const { return autoAttackTarget; }
bool isAggressiveTowardPlayer(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
bool isAutoAttacking() const;
bool hasAutoAttackIntent() const;
bool isInCombat() const;
bool isInCombatWith(uint64_t guid) const;
uint64_t getAutoAttackTargetGuid() const;
bool isAggressiveTowardPlayer(uint64_t guid) const;
// Timestamp (ms since epoch) of the most recent player melee auto-attack.
// Zero if no swing has occurred this session.
uint64_t getLastMeleeSwingMs() const { return lastMeleeSwingMs_; }
const std::vector<CombatTextEntry>& getCombatText() const { return combatText; }
uint64_t getLastMeleeSwingMs() const;
const std::vector<CombatTextEntry>& getCombatText() const;
void clearCombatText();
void updateCombatText(float deltaTime);
void clearHostileAttackers();
// Combat log (persistent rolling history, max MAX_COMBAT_LOG entries)
const std::deque<CombatLogEntry>& getCombatLog() const { return combatLog_; }
void clearCombatLog() { combatLog_.clear(); }
const std::deque<CombatLogEntry>& getCombatLog() const;
void clearCombatLog();
// Area trigger messages (SMSG_AREA_TRIGGER_MESSAGE) — drained by UI each frame
bool hasAreaTriggerMsg() const { return !areaTriggerMsgs_.empty(); }
@ -786,19 +690,9 @@ public:
}
// Threat
struct ThreatEntry {
uint64_t victimGuid = 0;
uint32_t threat = 0;
};
// Returns the current threat list for a given unit GUID (from last SMSG_THREAT_UPDATE)
const std::vector<ThreatEntry>* getThreatList(uint64_t unitGuid) const {
auto it = threatLists_.find(unitGuid);
return (it != threatLists_.end()) ? &it->second : nullptr;
}
// Returns the threat list for the player's current target, or nullptr
const std::vector<ThreatEntry>* getTargetThreatList() const {
return targetGuid ? getThreatList(targetGuid) : nullptr;
}
using ThreatEntry = CombatHandler::ThreatEntry;
const std::vector<ThreatEntry>* getThreatList(uint64_t unitGuid) const;
const std::vector<ThreatEntry>* getTargetThreatList() const;
// ---- Phase 3: Spells ----
void castSpell(uint32_t spellId, uint64_t targetGuid = 0);
@ -833,14 +727,13 @@ public:
void sendPetAction(uint32_t action, uint64_t targetGuid = 0);
// Toggle autocast for a pet spell via CMSG_PET_SPELL_AUTOCAST
void togglePetSpellAutocast(uint32_t spellId);
const std::unordered_set<uint32_t>& getKnownSpells() const { return knownSpells; }
const std::unordered_set<uint32_t>& getKnownSpells() const {
static const std::unordered_set<uint32_t> empty;
return spellHandler_ ? spellHandler_->getKnownSpells() : empty;
}
// Spell book tabs — groups known spells by class skill line for Lua API
struct SpellBookTab {
std::string name;
std::string texture; // icon path
std::vector<uint32_t> spellIds; // spells in this tab
};
using SpellBookTab = SpellHandler::SpellBookTab;
const std::vector<SpellBookTab>& getSpellBookTabs();
// ---- Pet Stable ----
@ -887,15 +780,15 @@ public:
minimapPings_.end());
}
bool isCasting() const { return casting; }
bool isChanneling() const { return casting && castIsChannel; }
bool isCasting() const { return spellHandler_ ? spellHandler_->isCasting() : false; }
bool isChanneling() const { return spellHandler_ ? spellHandler_->isChanneling() : false; }
bool isGameObjectInteractionCasting() const {
return casting && currentCastSpellId == 0 && pendingGameObjectInteractGuid_ != 0;
return spellHandler_ ? spellHandler_->isGameObjectInteractionCasting() : false;
}
uint32_t getCurrentCastSpellId() const { return currentCastSpellId; }
float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; }
float getCastTimeRemaining() const { return castTimeRemaining; }
float getCastTimeTotal() const { return castTimeTotal; }
uint32_t getCurrentCastSpellId() const { return spellHandler_ ? spellHandler_->getCurrentCastSpellId() : 0; }
float getCastProgress() const { return spellHandler_ ? spellHandler_->getCastProgress() : 0.0f; }
float getCastTimeRemaining() const { return spellHandler_ ? spellHandler_->getCastTimeRemaining() : 0.0f; }
float getCastTimeTotal() const { return spellHandler_ ? spellHandler_->getCastTimeTotal() : 0.0f; }
// Repeat-craft queue
void startCraftQueue(uint32_t spellId, int count);
@ -907,39 +800,19 @@ public:
uint32_t getQueuedSpellId() const { return queuedSpellId_; }
void cancelQueuedSpell() { queuedSpellId_ = 0; queuedSpellTarget_ = 0; }
// Unit cast state (tracked per GUID for target frame + boss frames)
struct UnitCastState {
bool casting = false;
bool isChannel = false; ///< true for channels (MSG_CHANNEL_START), false for casts (SMSG_SPELL_START)
uint32_t spellId = 0;
float timeRemaining = 0.0f;
float timeTotal = 0.0f;
bool interruptible = true; ///< false when SPELL_ATTR_EX_NOT_INTERRUPTIBLE is set
};
// Returns cast state for any unit by GUID (empty/non-casting if not found)
// Unit cast state (aliased from handler_types.hpp)
using UnitCastState = game::UnitCastState;
// Returns cast state for any unit by GUID (delegates to SpellHandler)
const UnitCastState* getUnitCastState(uint64_t guid) const {
auto it = unitCastStates_.find(guid);
return (it != unitCastStates_.end() && it->second.casting) ? &it->second : nullptr;
if (spellHandler_) return spellHandler_->getUnitCastState(guid);
return nullptr;
}
// Convenience helpers for the current target
bool isTargetCasting() const { return getUnitCastState(targetGuid) != nullptr; }
uint32_t getTargetCastSpellId() const {
auto* s = getUnitCastState(targetGuid);
return s ? s->spellId : 0;
}
float getTargetCastProgress() const {
auto* s = getUnitCastState(targetGuid);
return (s && s->timeTotal > 0.0f)
? (s->timeTotal - s->timeRemaining) / s->timeTotal : 0.0f;
}
float getTargetCastTimeRemaining() const {
auto* s = getUnitCastState(targetGuid);
return s ? s->timeRemaining : 0.0f;
}
bool isTargetCastInterruptible() const {
auto* s = getUnitCastState(targetGuid);
return s ? s->interruptible : true;
}
bool isTargetCasting() const { return spellHandler_ ? spellHandler_->isTargetCasting() : false; }
uint32_t getTargetCastSpellId() const { return spellHandler_ ? spellHandler_->getTargetCastSpellId() : 0; }
float getTargetCastProgress() const { return spellHandler_ ? spellHandler_->getTargetCastProgress() : 0.0f; }
float getTargetCastTimeRemaining() const { return spellHandler_ ? spellHandler_->getTargetCastTimeRemaining() : 0.0f; }
bool isTargetCastInterruptible() const { return spellHandler_ ? spellHandler_->isTargetCastInterruptible() : true; }
// Talents
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
@ -998,13 +871,21 @@ public:
void loadCharacterConfig();
static std::string getCharacterConfigDir();
// Auras
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
// Auras — delegate to SpellHandler as canonical authority
const std::vector<AuraSlot>& getPlayerAuras() const {
if (spellHandler_) return spellHandler_->getPlayerAuras();
static const std::vector<AuraSlot> empty;
return empty;
}
const std::vector<AuraSlot>& getTargetAuras() const {
if (spellHandler_) return spellHandler_->getTargetAuras();
static const std::vector<AuraSlot> empty;
return empty;
}
// Per-unit aura cache (populated for party members and any unit we receive updates for)
const std::vector<AuraSlot>* getUnitAuras(uint64_t guid) const {
auto it = unitAurasCache_.find(guid);
return (it != unitAurasCache_.end()) ? &it->second : nullptr;
if (spellHandler_) return spellHandler_->getUnitAuras(guid);
return nullptr;
}
// Completed quests (populated from SMSG_QUERY_QUESTS_COMPLETED_RESPONSE)
@ -1257,7 +1138,10 @@ public:
// Cooldowns
float getSpellCooldown(uint32_t spellId) const;
const std::unordered_map<uint32_t, float>& getSpellCooldowns() const { return spellCooldowns; }
const std::unordered_map<uint32_t, float>& getSpellCooldowns() const {
static const std::unordered_map<uint32_t, float> empty;
return spellHandler_ ? spellHandler_->getSpellCooldowns() : empty;
}
// Player GUID
uint64_t getPlayerGuid() const { return playerGuid; }
@ -1448,14 +1332,8 @@ public:
return rem > 0.0f ? rem : 0.0f;
}
// ---- Instance lockouts ----
struct InstanceLockout {
uint32_t mapId = 0;
uint32_t difficulty = 0; // 0=normal,1=heroic/10man,2=25man,3=25man heroic
uint64_t resetTime = 0; // Unix timestamp of instance reset
bool locked = false;
bool extended = false;
};
// Instance lockouts (aliased from handler_types.hpp)
using InstanceLockout = game::InstanceLockout;
const std::vector<InstanceLockout>& getInstanceLockouts() const { return instanceLockouts_; }
// Boss encounter unit tracking (SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
@ -1483,16 +1361,8 @@ public:
void setRaidMark(uint64_t guid, uint8_t icon);
// ---- LFG / Dungeon Finder ----
enum class LfgState : uint8_t {
None = 0,
RoleCheck = 1,
Queued = 2,
Proposal = 3,
Boot = 4,
InDungeon = 5,
FinishedDungeon= 6,
RaidBrowser = 7,
};
// LFG state (aliased from handler_types.hpp)
using LfgState = game::LfgState;
// roles bitmask: 0x02=tank, 0x04=healer, 0x08=dps; pass LFGDungeonEntry ID
void lfgJoin(uint32_t dungeonId, uint8_t roles);
@ -1517,36 +1387,14 @@ public:
const std::string& getLfgBootTargetName() const { return lfgBootTargetName_; }
const std::string& getLfgBootReason() const { return lfgBootReason_; }
// ---- Arena Team Stats ----
struct ArenaTeamStats {
uint32_t teamId = 0;
uint32_t rating = 0;
uint32_t weekGames = 0;
uint32_t weekWins = 0;
uint32_t seasonGames = 0;
uint32_t seasonWins = 0;
uint32_t rank = 0;
std::string teamName;
uint32_t teamType = 0; // 2, 3, or 5
};
// Arena team stats (aliased from handler_types.hpp)
using ArenaTeamStats = game::ArenaTeamStats;
const std::vector<ArenaTeamStats>& getArenaTeamStats() const { return arenaTeamStats_; }
void requestArenaTeamRoster(uint32_t teamId);
// ---- Arena Team Roster ----
struct ArenaTeamMember {
uint64_t guid = 0;
std::string name;
bool online = false;
uint32_t weekGames = 0;
uint32_t weekWins = 0;
uint32_t seasonGames = 0;
uint32_t seasonWins = 0;
uint32_t personalRating = 0;
};
struct ArenaTeamRoster {
uint32_t teamId = 0;
std::vector<ArenaTeamMember> members;
};
// Arena team roster (aliased from handler_types.hpp)
using ArenaTeamMember = game::ArenaTeamMember;
using ArenaTeamRoster = game::ArenaTeamRoster;
// Returns roster for the given teamId, or nullptr if not yet received
const ArenaTeamRoster* getArenaTeamRoster(uint32_t teamId) const {
for (const auto& r : arenaTeamRosters_) {
@ -1574,36 +1422,15 @@ public:
bool hasMasterLootCandidates() const { return !masterLootCandidates_.empty(); }
void lootMasterGive(uint8_t lootSlot, uint64_t targetGuid);
// Group loot roll
struct LootRollEntry {
uint64_t objectGuid = 0;
uint32_t slot = 0;
uint32_t itemId = 0;
std::string itemName;
uint8_t itemQuality = 0;
uint32_t rollCountdownMs = 60000; // Duration of roll window in ms
uint8_t voteMask = 0xFF; // Bitmask: 0x01=pass, 0x02=need, 0x04=greed, 0x08=disenchant
std::chrono::steady_clock::time_point rollStartedAt{};
struct PlayerRollResult {
std::string playerName;
uint8_t rollNum = 0;
uint8_t rollType = 0; // 0=need,1=greed,2=disenchant,96=pass
};
std::vector<PlayerRollResult> playerRolls; // live roll results from group members
};
// Group loot roll (aliased from handler_types.hpp)
using LootRollEntry = game::LootRollEntry;
bool hasPendingLootRoll() const { return pendingLootRollActive_; }
const LootRollEntry& getPendingLootRoll() const { return pendingLootRoll_; }
void sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType);
// rollType: 0=need, 1=greed, 2=disenchant, 96=pass
// Equipment Sets (WotLK): saved gear loadouts
struct EquipmentSetInfo {
uint64_t setGuid = 0;
uint32_t setId = 0;
std::string name;
std::string iconName;
};
// Equipment Sets (aliased from handler_types.hpp)
using EquipmentSetInfo = game::EquipmentSetInfo;
const std::vector<EquipmentSetInfo>& getEquipmentSets() const { return equipmentSetInfo_; }
bool supportsEquipmentSets() const;
void useEquipmentSet(uint32_t setId);
@ -1638,14 +1465,8 @@ public:
}
const QuestDetailsData& getQuestDetails() const { return currentQuestDetails; }
// Gossip / quest map POI markers (SMSG_GOSSIP_POI)
struct GossipPoi {
float x = 0.0f; // WoW canonical X (north)
float y = 0.0f; // WoW canonical Y (west)
uint32_t icon = 0; // POI icon type
uint32_t data = 0;
std::string name;
};
// Gossip POI (aliased from handler_types.hpp)
using GossipPoi = game::GossipPoi;
const std::vector<GossipPoi>& getGossipPois() const { return gossipPois_; }
void clearGossipPois() { gossipPois_.clear(); }
@ -1661,37 +1482,7 @@ public:
void closeQuestOfferReward();
// Quest log
struct QuestLogEntry {
uint32_t questId = 0;
std::string title;
std::string objectives;
bool complete = false;
// Objective kill counts: npcOrGoEntry -> (current, required)
std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> killCounts;
// Quest item progress: itemId -> current count
std::unordered_map<uint32_t, uint32_t> itemCounts;
// Server-authoritative quest item requirements from REQUEST_ITEMS
std::unordered_map<uint32_t, uint32_t> requiredItemCounts;
// Structured kill objectives parsed from SMSG_QUEST_QUERY_RESPONSE.
// Index 0-3 map to the server's objective slot order (packed into update fields).
// npcOrGoId != 0 => entity objective (kill NPC or interact with GO).
struct KillObjective {
int32_t npcOrGoId = 0; // negative = game-object entry
uint32_t required = 0;
};
std::array<KillObjective, 4> killObjectives{}; // zeroed by default
// Required item objectives parsed from SMSG_QUEST_QUERY_RESPONSE.
// itemId != 0 => collect items of that type.
struct ItemObjective {
uint32_t itemId = 0;
uint32_t required = 0;
};
std::array<ItemObjective, 6> itemObjectives{}; // zeroed by default
// Reward data parsed from SMSG_QUEST_QUERY_RESPONSE
int32_t rewardMoney = 0; // copper; positive=reward, negative=cost
std::array<QuestRewardItem, 4> rewardItems{}; // guaranteed reward items
std::array<QuestRewardItem, 6> rewardChoiceItems{}; // player picks one of these
};
using QuestLogEntry = QuestHandler::QuestLogEntry;
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
int getSelectedQuestLogIndex() const { return selectedQuestLogIndex_; }
void setSelectedQuestLogIndex(int idx) { selectedQuestLogIndex_ = idx; }
@ -1879,7 +1670,7 @@ public:
void setWatchedFactionId(uint32_t factionId);
uint32_t getLastContactListMask() const { return lastContactListMask_; }
uint32_t getLastContactListCount() const { return lastContactListCount_; }
bool isServerMovementAllowed() const { return serverMovementAllowed_; }
bool isServerMovementAllowed() const;
// Quest giver status (! and ? markers)
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const {
@ -2052,7 +1843,7 @@ public:
void setOpenLfgCallback(OpenLfgCallback cb) { openLfgCallback_ = std::move(cb); }
bool isMounted() const { return currentMountDisplayId_ != 0; }
bool isHostileAttacker(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
bool isHostileAttacker(uint64_t guid) const;
bool isHostileFactionPublic(uint32_t factionTemplateId) const { return isHostileFaction(factionTemplateId); }
float getServerRunSpeed() const { return serverRunSpeed_; }
float getServerWalkSpeed() const { return serverWalkSpeed_; }
@ -2337,7 +2128,16 @@ public:
void resetDbcCaches();
private:
void autoTargetAttacker(uint64_t attackerGuid);
friend class ChatHandler;
friend class MovementHandler;
friend class CombatHandler;
friend class SpellHandler;
friend class InventoryHandler;
friend class SocialHandler;
friend class QuestHandler;
friend class WardenHandler;
// Dead: autoTargetAttacker moved to CombatHandler
/**
* Handle incoming packet from world server
@ -2397,7 +2197,6 @@ private:
* Handle SMSG_WARDEN_DATA gate packet from server.
* We do not implement anti-cheat exchange for third-party realms.
*/
void handleWardenData(network::Packet& packet);
/**
* Handle SMSG_ACCOUNT_DATA_TIMES from server
@ -2432,14 +2231,6 @@ private:
*/
void handleDestroyObject(network::Packet& packet);
/**
* Handle SMSG_MESSAGECHAT from server
*/
void handleMessageChat(network::Packet& packet);
void handleTextEmote(network::Packet& packet);
void handleChannelNotify(network::Packet& packet);
void autoJoinDefaultChannels();
// ---- Phase 1 handlers ----
void handleNameQueryResponse(network::Packet& packet);
void handleCreatureQueryResponse(network::Packet& packet);
@ -2447,7 +2238,6 @@ private:
void handleGameObjectPageText(network::Packet& packet);
void handlePageTextQueryResponse(network::Packet& packet);
void handleItemQueryResponse(network::Packet& packet);
void handleInspectResults(network::Packet& packet);
void queryItemInfo(uint32_t entry, uint64_t guid);
void rebuildOnlineInventory();
void maybeDetectVisibleItemLayout();
@ -2459,56 +2249,22 @@ private:
void extractContainerFields(uint64_t containerGuid, const std::map<uint16_t, uint32_t>& fields);
uint64_t resolveOnlineItemGuid(uint32_t itemId) const;
// ---- Phase 2 handlers ----
void handleAttackStart(network::Packet& packet);
void handleAttackStop(network::Packet& packet);
void handleAttackerStateUpdate(network::Packet& packet);
void handleSpellDamageLog(network::Packet& packet);
void handleSpellHealLog(network::Packet& packet);
// ---- Phase 2 handlers (dead — dispatched via CombatHandler) ----
// handleAttackStart, handleAttackStop, handleAttackerStateUpdate,
// handleSpellDamageLog, handleSpellHealLog removed
// ---- Equipment set handler ----
void handleEquipmentSetList(network::Packet& packet);
void handleUpdateAuraDuration(uint8_t slot, uint32_t durationMs);
void handleSetForcedReactions(network::Packet& packet);
// handleSetForcedReactions — dispatched via CombatHandler
// ---- Phase 3 handlers ----
void handleInitialSpells(network::Packet& packet);
void handleCastFailed(network::Packet& packet);
void handleSpellStart(network::Packet& packet);
void handleSpellGo(network::Packet& packet);
void handleSpellCooldown(network::Packet& packet);
void handleCooldownEvent(network::Packet& packet);
void handleAchievementEarned(network::Packet& packet);
void handleAuraUpdate(network::Packet& packet, bool isAll);
void handleLearnedSpell(network::Packet& packet);
void handleSupercededSpell(network::Packet& packet);
void handleRemovedSpell(network::Packet& packet);
void handleUnlearnSpells(network::Packet& packet);
// ---- Talent handlers ----
void handleTalentsInfo(network::Packet& packet);
// ---- Phase 4 handlers ----
void handleGroupInvite(network::Packet& packet);
void handleGroupDecline(network::Packet& packet);
void handleGroupList(network::Packet& packet);
void handleGroupUninvite(network::Packet& packet);
void handlePartyCommandResult(network::Packet& packet);
void handlePartyMemberStats(network::Packet& packet, bool isFull);
// ---- Guild handlers ----
void handleGuildInfo(network::Packet& packet);
void handleGuildRoster(network::Packet& packet);
void handleGuildQueryResponse(network::Packet& packet);
void handleGuildEvent(network::Packet& packet);
void handleGuildInvite(network::Packet& packet);
void handleGuildCommandResult(network::Packet& packet);
void handlePetitionShowlist(network::Packet& packet);
void handlePetitionQueryResponse(network::Packet& packet);
void handlePetitionShowSignatures(network::Packet& packet);
void handlePetitionSignResults(network::Packet& packet);
void handlePetSpells(network::Packet& packet);
void handleTurnInPetitionResults(network::Packet& packet);
// ---- Character creation handler ----
void handleCharCreateResponse(network::Packet& packet);
@ -2517,25 +2273,10 @@ private:
void handleXpGain(network::Packet& packet);
// ---- Creature movement handler ----
void handleMonsterMove(network::Packet& packet);
void handleCompressedMoves(network::Packet& packet);
void handleMonsterMoveTransport(network::Packet& packet);
// ---- Other player movement (MSG_MOVE_* from server) ----
void handleOtherPlayerMovement(network::Packet& packet);
void handleMoveSetSpeed(network::Packet& packet);
// ---- Phase 5 handlers ----
void handleLootResponse(network::Packet& packet);
void handleLootReleaseResponse(network::Packet& packet);
void handleLootRemoved(network::Packet& packet);
void handleGossipMessage(network::Packet& packet);
void handleQuestgiverQuestList(network::Packet& packet);
void handleGossipComplete(network::Packet& packet);
void handleQuestPoiQueryResponse(network::Packet& packet);
void handleQuestDetails(network::Packet& packet);
void handleQuestRequestItems(network::Packet& packet);
void handleQuestOfferReward(network::Packet& packet);
void clearPendingQuestAccept(uint32_t questId);
void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason);
bool hasQuestInLog(uint32_t questId) const;
@ -2546,101 +2287,43 @@ private:
int findQuestLogSlotIndexFromServer(uint32_t questId) const;
void addQuestToLocalLogIfMissing(uint32_t questId, const std::string& title, const std::string& objectives);
bool resyncQuestLogFromServerSlots(bool forceQueryMetadata);
void handleListInventory(network::Packet& packet);
void addMoneyCopper(uint32_t amount);
// ---- Teleport handler ----
void handleTeleportAck(network::Packet& packet);
void handleNewWorld(network::Packet& packet);
// ---- Movement ACK handlers ----
void handleForceRunSpeedChange(network::Packet& packet);
void handleForceSpeedChange(network::Packet& packet, const char* name, Opcode ackOpcode, float* speedStorage);
void handleForceMoveRootState(network::Packet& packet, bool rooted);
void handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set);
void handleMoveSetCollisionHeight(network::Packet& packet);
void handleMoveKnockBack(network::Packet& packet);
// ---- Area trigger detection ----
void loadAreaTriggerDbc();
void checkAreaTriggers();
// ---- Instance lockout handler ----
void handleRaidInstanceInfo(network::Packet& packet);
void handleItemTextQueryResponse(network::Packet& packet);
void handleQuestConfirmAccept(network::Packet& packet);
void handleSummonRequest(network::Packet& packet);
void handleTradeStatus(network::Packet& packet);
void handleTradeStatusExtended(network::Packet& packet);
void resetTradeState();
void handleDuelRequested(network::Packet& packet);
void handleDuelComplete(network::Packet& packet);
void handleDuelWinner(network::Packet& packet);
void handleLootRoll(network::Packet& packet);
void handleLootRollWon(network::Packet& packet);
// ---- LFG / Dungeon Finder handlers ----
void handleLfgJoinResult(network::Packet& packet);
void handleLfgQueueStatus(network::Packet& packet);
void handleLfgProposalUpdate(network::Packet& packet);
void handleLfgRoleCheckUpdate(network::Packet& packet);
void handleLfgUpdatePlayer(network::Packet& packet);
void handleLfgPlayerReward(network::Packet& packet);
void handleLfgBootProposalUpdate(network::Packet& packet);
void handleLfgTeleportDenied(network::Packet& packet);
// ---- Arena / Battleground handlers ----
void handleBattlefieldStatus(network::Packet& packet);
void handleInstanceDifficulty(network::Packet& packet);
void handleArenaTeamCommandResult(network::Packet& packet);
void handleArenaTeamQueryResponse(network::Packet& packet);
void handleArenaTeamRoster(network::Packet& packet);
void handleArenaTeamInvite(network::Packet& packet);
void handleArenaTeamEvent(network::Packet& packet);
void handleArenaTeamStats(network::Packet& packet);
void handleArenaError(network::Packet& packet);
void handlePvpLogData(network::Packet& packet);
// ---- Bank handlers ----
void handleShowBank(network::Packet& packet);
void handleBuyBankSlotResult(network::Packet& packet);
// ---- Guild Bank handlers ----
void handleGuildBankList(network::Packet& packet);
// ---- Auction House handlers ----
void handleAuctionHello(network::Packet& packet);
void handleAuctionListResult(network::Packet& packet);
void handleAuctionOwnerListResult(network::Packet& packet);
void handleAuctionBidderListResult(network::Packet& packet);
void handleAuctionCommandResult(network::Packet& packet);
// ---- Mail handlers ----
void handleShowMailbox(network::Packet& packet);
void handleMailListResult(network::Packet& packet);
void handleSendMailResult(network::Packet& packet);
void handleReceivedMail(network::Packet& packet);
void handleQueryNextMailTime(network::Packet& packet);
// ---- Taxi handlers ----
void handleShowTaxiNodes(network::Packet& packet);
void handleActivateTaxiReply(network::Packet& packet);
void loadTaxiDbc();
// ---- Server info handlers ----
void handleQueryTimeResponse(network::Packet& packet);
void handlePlayedTime(network::Packet& packet);
void handleWho(network::Packet& packet);
// ---- Social handlers ----
void handleFriendList(network::Packet& packet); // Classic SMSG_FRIEND_LIST
void handleContactList(network::Packet& packet); // WotLK SMSG_CONTACT_LIST (full parse)
void handleFriendStatus(network::Packet& packet);
void handleRandomRoll(network::Packet& packet);
// ---- Logout handlers ----
void handleLogoutResponse(network::Packet& packet);
void handleLogoutComplete(network::Packet& packet);
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType = 0,
uint64_t srcGuid = 0, uint64_t dstGuid = 0);
@ -2677,6 +2360,16 @@ private:
float localOrientation);
void clearTransportAttachment(uint64_t childGuid);
// Domain handlers — each manages a specific concern extracted from GameHandler
std::unique_ptr<ChatHandler> chatHandler_;
std::unique_ptr<MovementHandler> movementHandler_;
std::unique_ptr<CombatHandler> combatHandler_;
std::unique_ptr<SpellHandler> spellHandler_;
std::unique_ptr<InventoryHandler> inventoryHandler_;
std::unique_ptr<SocialHandler> socialHandler_;
std::unique_ptr<QuestHandler> questHandler_;
std::unique_ptr<WardenHandler> wardenHandler_;
// Opcode dispatch table — built once in registerOpcodeHandlers(), called by handlePacket()
using PacketHandler = std::function<void(network::Packet&)>;
std::unordered_map<LogicalOpcode, PacketHandler> dispatchTable_;
@ -2736,10 +2429,7 @@ private:
// Entity tracking
EntityManager entityManager; // Manages all entities in view
// Chat
std::deque<MessageChatData> chatHistory; // Recent chat messages
size_t maxChatHistory = 100; // Maximum chat messages to keep
std::vector<std::string> joinedChannels_; // Active channel memberships
// Chat (state lives in ChatHandler; callbacks remain here for cross-domain access)
ChatBubbleCallback chatBubbleCallback_;
AddonChatCallback addonChatCallback_;
AddonEventCallback addonEventCallback_;
@ -2885,32 +2575,9 @@ private:
std::unordered_set<uint64_t> pendingAutoInspect_;
float inspectRateLimit_ = 0.0f;
// ---- Phase 2: Combat ----
bool autoAttacking = false;
bool autoAttackRequested_ = false; // local intent (CMSG_ATTACKSWING sent)
bool autoAttackRetryPending_ = false; // one-shot retry after local start or server stop
uint64_t autoAttackTarget = 0;
bool autoAttackOutOfRange_ = false;
float autoAttackOutOfRangeTime_ = 0.0f;
float autoAttackRangeWarnCooldown_ = 0.0f;
float autoAttackResendTimer_ = 0.0f; // Re-send CMSG_ATTACKSWING every ~1s while attacking
float autoAttackFacingSyncTimer_ = 0.0f; // Periodic facing sync while meleeing
std::unordered_set<uint64_t> hostileAttackers_;
// ---- Phase 2: Combat (state moved to CombatHandler) ----
bool wasCombat_ = false; // Previous frame combat state for PLAYER_REGEN edge detection
std::vector<CombatTextEntry> combatText;
static constexpr size_t MAX_COMBAT_LOG = 500;
struct RecentSpellstealLogEntry {
uint64_t casterGuid = 0;
uint64_t victimGuid = 0;
uint32_t spellId = 0;
std::chrono::steady_clock::time_point timestamp{};
};
static constexpr size_t MAX_RECENT_SPELLSTEAL_LOGS = 32;
std::deque<CombatLogEntry> combatLog_;
std::deque<RecentSpellstealLogEntry> recentSpellstealLogs_;
std::deque<std::string> areaTriggerMsgs_;
// unitGuid → sorted threat list (descending by threat value)
std::unordered_map<uint64_t, std::vector<ThreatEntry>> threatLists_;
// ---- Phase 3: Spells ----
WorldEntryCallback worldEntryCallback_;
@ -3022,7 +2689,6 @@ private:
// ---- Available battleground list (SMSG_BATTLEFIELD_LIST) ----
std::vector<AvailableBgInfo> availableBgs_;
void handleBattlefieldList(network::Packet& packet);
// Instance difficulty
uint32_t instanceDifficulty_ = 0;
@ -3293,11 +2959,8 @@ private:
uint32_t knownTaxiMask_[12] = {}; // Track previously known nodes for discovery alerts
bool taxiMaskInitialized_ = false; // First SMSG_SHOWTAXINODES seeds mask without alerts
std::unordered_map<uint32_t, uint32_t> taxiCostMap_; // destNodeId -> total cost in copper
void buildTaxiCostMap();
void applyTaxiMountForCurrentNode();
uint32_t nextMovementTimestampMs();
void sanitizeMovementForTaxi();
void startClientTaxiPath(const std::vector<uint32_t>& pathNodes);
void updateClientTaxi(float deltaTime);
// Mail
@ -3397,7 +3060,6 @@ private:
// Per-player achievement data from SMSG_RESPOND_INSPECT_ACHIEVEMENTS
// Key: inspected player's GUID; value: set of earned achievement IDs
std::unordered_map<uint64_t, std::unordered_set<uint32_t>> inspectedPlayerAchievements_;
void handleRespondInspectAchievements(network::Packet& packet);
// Area name cache (lazy-loaded from WorldMapArea.dbc; maps AreaTable ID → display name)
mutable std::unordered_map<uint32_t, std::string> areaNameCache_;
@ -3416,7 +3078,6 @@ private:
void loadLfgDungeonDbc() const;
std::string getLfgDungeonName(uint32_t dungeonId) const;
std::vector<TrainerTab> trainerTabs_;
void handleTrainerList(network::Packet& packet);
void loadSpellNameCache() const;
void preloadDBCCaches() const;
void categorizeTrainerSpells();
@ -3466,7 +3127,6 @@ private:
std::vector<WardenCREntry> wardenCREntries_;
// Module-specific check type opcodes [9]: MEM, PAGE_A, PAGE_B, MPQ, LUA, DRIVER, TIMING, PROC, MODULE
uint8_t wardenCheckOpcodes_[9] = {};
bool loadWardenCRFile(const std::string& moduleHashHex);
// Async Warden response: avoids 5-second main-loop stalls from PAGE_A/PAGE_B code pattern searches
std::future<std::vector<uint8_t>> wardenPendingEncrypted_; // encrypted response bytes
@ -3530,7 +3190,7 @@ private:
AppearanceChangedCallback appearanceChangedCallback_;
GhostStateCallback ghostStateCallback_;
MeleeSwingCallback meleeSwingCallback_;
uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing
// lastMeleeSwingMs_ moved to CombatHandler
SpellCastAnimCallback spellCastAnimCallback_;
SpellCastFailedCallback spellCastFailedCallback_;
UnitAnimHintCallback unitAnimHintCallback_;
@ -3615,8 +3275,7 @@ private:
std::string pendingSaveSetIcon_;
std::vector<EquipmentSetInfo> equipmentSetInfo_; // public-facing copy
// ---- Forced faction reactions (SMSG_SET_FORCED_REACTIONS) ----
std::unordered_map<uint32_t, uint8_t> forcedReactions_; // factionId -> reaction tier
// forcedReactions_ moved to CombatHandler
// ---- Server-triggered audio ----
PlayMusicCallback playMusicCallback_;

View file

@ -0,0 +1,27 @@
#pragma once
#include "game/expansion_profile.hpp"
#include "core/application.hpp"
namespace wowee {
namespace game {
inline bool isActiveExpansion(const char* expansionId) {
auto& app = core::Application::getInstance();
auto* registry = app.getExpansionRegistry();
if (!registry) return false;
auto* profile = registry->getActive();
if (!profile) return false;
return profile->id == expansionId;
}
inline bool isClassicLikeExpansion() {
return isActiveExpansion("classic") || isActiveExpansion("turtle");
}
inline bool isPreWotlk() {
return isClassicLikeExpansion() || isActiveExpansion("tbc");
}
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,270 @@
#pragma once
/**
* handler_types.hpp Shared struct definitions used by GameHandler and domain handlers.
*
* These types were previously duplicated across GameHandler, SpellHandler, SocialHandler,
* ChatHandler, QuestHandler, and InventoryHandler. Now they live here at namespace scope,
* and each class provides a `using` alias for backward compatibility
* (e.g. GameHandler::TalentEntry == game::TalentEntry).
*/
#include <array>
#include <chrono>
#include <cstdint>
#include <string>
#include <vector>
namespace wowee {
namespace game {
// ---- Talent DBC data ----
struct TalentEntry {
uint32_t talentId = 0;
uint32_t tabId = 0;
uint8_t row = 0;
uint8_t column = 0;
uint32_t rankSpells[5] = {};
uint32_t prereqTalent[3] = {};
uint8_t prereqRank[3] = {};
uint8_t maxRank = 0;
};
struct TalentTabEntry {
uint32_t tabId = 0;
std::string name;
uint32_t classMask = 0;
uint8_t orderIndex = 0;
std::string backgroundFile;
};
// ---- Spell / cast state ----
struct UnitCastState {
bool casting = false;
bool isChannel = false;
uint32_t spellId = 0;
float timeRemaining = 0.0f;
float timeTotal = 0.0f;
bool interruptible = true;
};
// ---- Equipment sets (WotLK) ----
struct EquipmentSetInfo {
uint64_t setGuid = 0;
uint32_t setId = 0;
std::string name;
std::string iconName;
};
// ---- Inspection ----
struct InspectArenaTeam {
uint32_t teamId = 0;
uint8_t type = 0;
uint32_t weekGames = 0;
uint32_t weekWins = 0;
uint32_t seasonGames = 0;
uint32_t seasonWins = 0;
std::string name;
uint32_t personalRating = 0;
};
struct InspectResult {
uint64_t guid = 0;
std::string playerName;
uint32_t totalTalents = 0;
uint32_t unspentTalents = 0;
uint8_t talentGroups = 0;
uint8_t activeTalentGroup = 0;
std::array<uint32_t, 19> itemEntries{};
std::array<uint16_t, 19> enchantIds{};
std::vector<InspectArenaTeam> arenaTeams;
};
// ---- Who ----
struct WhoEntry {
std::string name;
std::string guildName;
uint32_t level = 0;
uint32_t classId = 0;
uint32_t raceId = 0;
uint32_t zoneId = 0;
};
// ---- Battleground ----
struct BgQueueSlot {
uint32_t queueSlot = 0;
uint32_t bgTypeId = 0;
uint8_t arenaType = 0;
uint32_t statusId = 0;
uint32_t inviteTimeout = 80;
uint32_t avgWaitTimeSec = 0;
uint32_t timeInQueueSec = 0;
std::chrono::steady_clock::time_point inviteReceivedTime{};
std::string bgName;
};
struct AvailableBgInfo {
uint32_t bgTypeId = 0;
bool isRegistered = false;
bool isHoliday = false;
uint32_t minLevel = 0;
uint32_t maxLevel = 0;
std::vector<uint32_t> instanceIds;
};
struct BgPlayerScore {
uint64_t guid = 0;
std::string name;
uint8_t team = 0;
uint32_t killingBlows = 0;
uint32_t deaths = 0;
uint32_t honorableKills = 0;
uint32_t bonusHonor = 0;
std::vector<std::pair<std::string, uint32_t>> bgStats;
};
struct ArenaTeamScore {
std::string teamName;
uint32_t ratingChange = 0;
uint32_t newRating = 0;
};
struct BgScoreboardData {
std::vector<BgPlayerScore> players;
bool hasWinner = false;
uint8_t winner = 0;
bool isArena = false;
ArenaTeamScore arenaTeams[2];
};
struct BgPlayerPosition {
uint64_t guid = 0;
float wowX = 0.0f;
float wowY = 0.0f;
int group = 0;
};
// ---- Guild petition ----
struct PetitionSignature {
uint64_t playerGuid = 0;
std::string playerName;
};
struct PetitionInfo {
uint64_t petitionGuid = 0;
uint64_t ownerGuid = 0;
std::string guildName;
uint32_t signatureCount = 0;
uint32_t signaturesRequired = 9;
std::vector<PetitionSignature> signatures;
bool showUI = false;
};
// ---- Ready check ----
struct ReadyCheckResult {
std::string name;
bool ready = false;
};
// ---- Chat ----
struct ChatAutoJoin {
bool general = true;
bool trade = true;
bool localDefense = true;
bool lfg = true;
bool local = true;
};
// ---- Quest / gossip ----
struct GossipPoi {
float x = 0.0f;
float y = 0.0f;
uint32_t icon = 0;
uint32_t data = 0;
std::string name;
};
// ---- Instance lockouts ----
struct InstanceLockout {
uint32_t mapId = 0;
uint32_t difficulty = 0;
uint64_t resetTime = 0;
bool locked = false;
bool extended = false;
};
// ---- LFG ----
enum class LfgState : uint8_t {
None = 0,
RoleCheck = 1,
Queued = 2,
Proposal = 3,
Boot = 4,
InDungeon = 5,
FinishedDungeon= 6,
RaidBrowser = 7,
};
// ---- Arena teams ----
struct ArenaTeamStats {
uint32_t teamId = 0;
uint32_t rating = 0;
uint32_t weekGames = 0;
uint32_t weekWins = 0;
uint32_t seasonGames = 0;
uint32_t seasonWins = 0;
uint32_t rank = 0;
std::string teamName;
uint32_t teamType = 0;
};
struct ArenaTeamMember {
uint64_t guid = 0;
std::string name;
bool online = false;
uint32_t weekGames = 0;
uint32_t weekWins = 0;
uint32_t seasonGames = 0;
uint32_t seasonWins = 0;
uint32_t personalRating = 0;
};
struct ArenaTeamRoster {
uint32_t teamId = 0;
std::vector<ArenaTeamMember> members;
};
// ---- Group loot roll ----
struct LootRollEntry {
uint64_t objectGuid = 0;
uint32_t slot = 0;
uint32_t itemId = 0;
std::string itemName;
uint8_t itemQuality = 0;
uint32_t rollCountdownMs = 60000;
uint8_t voteMask = 0xFF;
std::chrono::steady_clock::time_point rollStartedAt{};
struct PlayerRollResult {
std::string playerName;
uint8_t rollNum = 0;
uint8_t rollType = 0;
};
std::vector<PlayerRollResult> playerRolls;
};
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,401 @@
#pragma once
#include "game/world_packets.hpp"
#include "game/opcode_table.hpp"
#include "game/inventory.hpp"
#include "game/handler_types.hpp"
#include "network/packet.hpp"
#include <array>
#include <chrono>
#include <deque>
#include <functional>
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace wowee {
namespace game {
class GameHandler;
class InventoryHandler {
public:
using PacketHandler = std::function<void(network::Packet&)>;
using DispatchTable = std::unordered_map<LogicalOpcode, PacketHandler>;
explicit InventoryHandler(GameHandler& owner);
void registerOpcodes(DispatchTable& table);
// ---- Item text (books / readable items) ----
bool isItemTextOpen() const { return itemTextOpen_; }
const std::string& getItemText() const { return itemText_; }
void closeItemText() { itemTextOpen_ = false; }
void queryItemText(uint64_t itemGuid);
// ---- Trade ----
enum class TradeStatus : uint8_t {
None = 0, PendingIncoming, Open, Accepted, Complete
};
static constexpr int TRADE_SLOT_COUNT = 6;
struct TradeSlot {
uint32_t itemId = 0;
uint32_t displayId = 0;
uint32_t stackCount = 0;
uint64_t itemGuid = 0;
};
TradeStatus getTradeStatus() const { return tradeStatus_; }
bool hasPendingTradeRequest() const { return tradeStatus_ == TradeStatus::PendingIncoming; }
bool isTradeOpen() const { return tradeStatus_ == TradeStatus::Open; }
const std::string& getTradePeerName() const { return tradePeerName_; }
const std::array<TradeSlot, TRADE_SLOT_COUNT>& getMyTradeSlots() const { return myTradeSlots_; }
const std::array<TradeSlot, TRADE_SLOT_COUNT>& getPeerTradeSlots() const { return peerTradeSlots_; }
uint64_t getMyTradeGold() const { return myTradeGold_; }
uint64_t getPeerTradeGold() const { return peerTradeGold_; }
void acceptTradeRequest();
void declineTradeRequest();
void acceptTrade();
void cancelTrade();
void setTradeItem(uint8_t tradeSlot, uint8_t srcBag, uint8_t srcSlot);
void clearTradeItem(uint8_t tradeSlot);
void setTradeGold(uint64_t amount);
// ---- Loot ----
void lootTarget(uint64_t targetGuid);
void lootItem(uint8_t slotIndex);
void closeLoot();
bool isLootWindowOpen() const { return lootWindowOpen_; }
const LootResponseData& getCurrentLoot() const { return currentLoot_; }
void setAutoLoot(bool enabled) { autoLoot_ = enabled; }
bool isAutoLoot() const { return autoLoot_; }
void setAutoSellGrey(bool enabled) { autoSellGrey_ = enabled; }
bool isAutoSellGrey() const { return autoSellGrey_; }
void setAutoRepair(bool enabled) { autoRepair_ = enabled; }
bool isAutoRepair() const { return autoRepair_; }
// Master loot candidates (from SMSG_LOOT_MASTER_LIST)
const std::vector<uint64_t>& getMasterLootCandidates() const { return masterLootCandidates_; }
bool hasMasterLootCandidates() const { return !masterLootCandidates_.empty(); }
void lootMasterGive(uint8_t lootSlot, uint64_t targetGuid);
// Group loot roll (aliased from handler_types.hpp)
using LootRollEntry = game::LootRollEntry;
bool hasPendingLootRoll() const { return pendingLootRollActive_; }
const LootRollEntry& getPendingLootRoll() const { return pendingLootRoll_; }
void sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType);
// ---- Equipment Sets (aliased from handler_types.hpp) ----
using EquipmentSetInfo = game::EquipmentSetInfo;
const std::vector<EquipmentSetInfo>& getEquipmentSets() const { return equipmentSetInfo_; }
bool supportsEquipmentSets() const;
void useEquipmentSet(uint32_t setId);
void saveEquipmentSet(const std::string& name, const std::string& iconName = "INV_Misc_QuestionMark",
uint64_t existingGuid = 0, uint32_t setIndex = 0xFFFFFFFF);
void deleteEquipmentSet(uint64_t setGuid);
// ---- Vendor ----
struct BuybackItem {
uint64_t itemGuid = 0;
ItemDef item;
uint32_t count = 1;
};
void openVendor(uint64_t npcGuid);
void closeVendor();
void buyItem(uint64_t vendorGuid, uint32_t itemId, uint32_t slot, uint32_t count);
void sellItem(uint64_t vendorGuid, uint64_t itemGuid, uint32_t count);
void sellItemBySlot(int backpackIndex);
void sellItemInBag(int bagIndex, int slotIndex);
void buyBackItem(uint32_t buybackSlot);
void repairItem(uint64_t vendorGuid, uint64_t itemGuid);
void repairAll(uint64_t vendorGuid, bool useGuildBank = false);
const std::deque<BuybackItem>& getBuybackItems() const { return buybackItems_; }
void autoEquipItemBySlot(int backpackIndex);
void autoEquipItemInBag(int bagIndex, int slotIndex);
void useItemBySlot(int backpackIndex);
void useItemInBag(int bagIndex, int slotIndex);
void openItemBySlot(int backpackIndex);
void openItemInBag(int bagIndex, int slotIndex);
void destroyItem(uint8_t bag, uint8_t slot, uint8_t count = 1);
void splitItem(uint8_t srcBag, uint8_t srcSlot, uint8_t count);
void swapContainerItems(uint8_t srcBag, uint8_t srcSlot, uint8_t dstBag, uint8_t dstSlot);
void swapBagSlots(int srcBagIndex, int dstBagIndex);
void unequipToBackpack(EquipSlot equipSlot);
void useItemById(uint32_t itemId);
bool isVendorWindowOpen() const { return vendorWindowOpen_; }
const ListInventoryData& getVendorItems() const { return currentVendorItems_; }
void setVendorCanRepair(bool v) { currentVendorItems_.canRepair = v; }
uint64_t getVendorGuid() const { return currentVendorItems_.vendorGuid; }
// ---- Mail ----
static constexpr int MAIL_MAX_ATTACHMENTS = 12;
struct MailAttachSlot {
uint64_t itemGuid = 0;
game::ItemDef item;
uint8_t srcBag = 0xFF;
uint8_t srcSlot = 0;
bool occupied() const { return itemGuid != 0; }
};
bool isMailboxOpen() const { return mailboxOpen_; }
const std::vector<MailMessage>& getMailInbox() const { return mailInbox_; }
int getSelectedMailIndex() const { return selectedMailIndex_; }
void setSelectedMailIndex(int idx) { selectedMailIndex_ = idx; }
bool isMailComposeOpen() const { return showMailCompose_; }
void openMailCompose() { showMailCompose_ = true; clearMailAttachments(); }
void closeMailCompose() { showMailCompose_ = false; clearMailAttachments(); }
bool hasNewMail() const { return hasNewMail_; }
void closeMailbox();
void sendMail(const std::string& recipient, const std::string& subject,
const std::string& body, uint64_t money, uint64_t cod = 0);
bool attachItemFromBackpack(int backpackIndex);
bool attachItemFromBag(int bagIndex, int slotIndex);
bool detachMailAttachment(int attachIndex);
void clearMailAttachments();
const std::array<MailAttachSlot, 12>& getMailAttachments() const { return mailAttachments_; }
int getMailAttachmentCount() const;
void mailTakeMoney(uint32_t mailId);
void mailTakeItem(uint32_t mailId, uint32_t itemGuidLow);
void mailDelete(uint32_t mailId);
void mailMarkAsRead(uint32_t mailId);
void refreshMailList();
// ---- Bank ----
void openBank(uint64_t guid);
void closeBank();
void buyBankSlot();
void depositItem(uint8_t srcBag, uint8_t srcSlot);
void withdrawItem(uint8_t srcBag, uint8_t srcSlot);
bool isBankOpen() const { return bankOpen_; }
uint64_t getBankerGuid() const { return bankerGuid_; }
int getEffectiveBankSlots() const { return effectiveBankSlots_; }
int getEffectiveBankBagSlots() const { return effectiveBankBagSlots_; }
// ---- Guild Bank ----
void openGuildBank(uint64_t guid);
void closeGuildBank();
void queryGuildBankTab(uint8_t tabId);
void buyGuildBankTab();
void depositGuildBankMoney(uint32_t amount);
void withdrawGuildBankMoney(uint32_t amount);
void guildBankWithdrawItem(uint8_t tabId, uint8_t bankSlot, uint8_t destBag, uint8_t destSlot);
void guildBankDepositItem(uint8_t tabId, uint8_t bankSlot, uint8_t srcBag, uint8_t srcSlot);
bool isGuildBankOpen() const { return guildBankOpen_; }
const GuildBankData& getGuildBankData() const { return guildBankData_; }
uint8_t getGuildBankActiveTab() const { return guildBankActiveTab_; }
void setGuildBankActiveTab(uint8_t tab) { guildBankActiveTab_ = tab; }
// ---- Auction House ----
void openAuctionHouse(uint64_t guid);
void closeAuctionHouse();
void auctionSearch(const std::string& name, uint8_t levelMin, uint8_t levelMax,
uint32_t quality, uint32_t itemClass, uint32_t itemSubClass,
uint32_t invTypeMask, uint8_t usableOnly, uint32_t offset = 0);
void auctionSellItem(uint64_t itemGuid, uint32_t stackCount, uint32_t bid,
uint32_t buyout, uint32_t duration);
void auctionPlaceBid(uint32_t auctionId, uint32_t amount);
void auctionBuyout(uint32_t auctionId, uint32_t buyoutPrice);
void auctionCancelItem(uint32_t auctionId);
void auctionListOwnerItems(uint32_t offset = 0);
void auctionListBidderItems(uint32_t offset = 0);
bool isAuctionHouseOpen() const { return auctionOpen_; }
uint64_t getAuctioneerGuid() const { return auctioneerGuid_; }
const AuctionListResult& getAuctionBrowseResults() const { return auctionBrowseResults_; }
const AuctionListResult& getAuctionOwnerResults() const { return auctionOwnerResults_; }
const AuctionListResult& getAuctionBidderResults() const { return auctionBidderResults_; }
int getAuctionActiveTab() const { return auctionActiveTab_; }
void setAuctionActiveTab(int tab) { auctionActiveTab_ = tab; }
float getAuctionSearchDelay() const { return auctionSearchDelayTimer_; }
// ---- Trainer ----
struct TrainerTab {
std::string name;
std::vector<const TrainerSpell*> spells;
};
bool isTrainerWindowOpen() const { return trainerWindowOpen_; }
const TrainerListData& getTrainerSpells() const { return currentTrainerList_; }
void trainSpell(uint32_t spellId);
void closeTrainer();
const std::vector<TrainerTab>& getTrainerTabs() const { return trainerTabs_; }
void resetTradeState();
// ---- Methods moved from GameHandler ----
void initiateTrade(uint64_t targetGuid);
uint32_t getTempEnchantRemainingMs(uint32_t slot) const;
void addMoneyCopper(uint32_t amount);
// ---- Inventory field / rebuild methods (moved from GameHandler) ----
void queryItemInfo(uint32_t entry, uint64_t guid);
uint64_t resolveOnlineItemGuid(uint32_t itemId) const;
void detectInventorySlotBases(const std::map<uint16_t, uint32_t>& fields);
bool applyInventoryFields(const std::map<uint16_t, uint32_t>& fields);
void extractContainerFields(uint64_t containerGuid, const std::map<uint16_t, uint32_t>& fields);
void rebuildOnlineInventory();
void maybeDetectVisibleItemLayout();
void updateOtherPlayerVisibleItems(uint64_t guid, const std::map<uint16_t, uint32_t>& fields);
void emitOtherPlayerEquipment(uint64_t guid);
void emitAllOtherPlayerEquipment();
void handleItemQueryResponse(network::Packet& packet);
private:
// --- Packet handlers ---
void handleLootResponse(network::Packet& packet);
void handleLootReleaseResponse(network::Packet& packet);
void handleLootRemoved(network::Packet& packet);
void handleListInventory(network::Packet& packet);
void handleTrainerList(network::Packet& packet);
void handleItemTextQueryResponse(network::Packet& packet);
void handleTradeStatus(network::Packet& packet);
void handleTradeStatusExtended(network::Packet& packet);
void handleLootRoll(network::Packet& packet);
void handleLootRollWon(network::Packet& packet);
void handleShowBank(network::Packet& packet);
void handleBuyBankSlotResult(network::Packet& packet);
void handleGuildBankList(network::Packet& packet);
void handleAuctionHello(network::Packet& packet);
void handleAuctionListResult(network::Packet& packet);
void handleAuctionOwnerListResult(network::Packet& packet);
void handleAuctionBidderListResult(network::Packet& packet);
void handleAuctionCommandResult(network::Packet& packet);
void handleShowMailbox(network::Packet& packet);
void handleMailListResult(network::Packet& packet);
void handleSendMailResult(network::Packet& packet);
void handleReceivedMail(network::Packet& packet);
void handleQueryNextMailTime(network::Packet& packet);
void handleEquipmentSetList(network::Packet& packet);
void categorizeTrainerSpells();
void handleTrainerBuySucceeded(network::Packet& packet);
void handleTrainerBuyFailed(network::Packet& packet);
GameHandler& owner_;
// ---- Item text state ----
bool itemTextOpen_ = false;
std::string itemText_;
// ---- Trade state ----
TradeStatus tradeStatus_ = TradeStatus::None;
uint64_t tradePeerGuid_= 0;
std::string tradePeerName_;
std::array<TradeSlot, TRADE_SLOT_COUNT> myTradeSlots_{};
std::array<TradeSlot, TRADE_SLOT_COUNT> peerTradeSlots_{};
uint64_t myTradeGold_ = 0;
uint64_t peerTradeGold_ = 0;
// ---- Loot state ----
bool lootWindowOpen_ = false;
bool autoLoot_ = false;
bool autoSellGrey_ = false;
bool autoRepair_ = false;
LootResponseData currentLoot_;
std::vector<uint64_t> masterLootCandidates_;
// Group loot roll state
bool pendingLootRollActive_ = false;
LootRollEntry pendingLootRoll_;
struct LocalLootState {
LootResponseData data;
bool moneyTaken = false;
bool itemAutoLootSent = false;
};
std::unordered_map<uint64_t, LocalLootState> localLootState_;
struct PendingLootRetry {
uint64_t guid = 0;
float timer = 0.0f;
uint8_t remainingRetries = 0;
bool sendLoot = false;
};
std::vector<PendingLootRetry> pendingGameObjectLootRetries_;
struct PendingLootOpen {
uint64_t guid = 0;
float timer = 0.0f;
};
std::vector<PendingLootOpen> pendingGameObjectLootOpens_;
uint64_t lastInteractedGoGuid_ = 0;
uint64_t pendingLootMoneyGuid_ = 0;
uint32_t pendingLootMoneyAmount_ = 0;
float pendingLootMoneyNotifyTimer_ = 0.0f;
std::unordered_map<uint64_t, float> recentLootMoneyAnnounceCooldowns_;
// ---- Vendor state ----
bool vendorWindowOpen_ = false;
ListInventoryData currentVendorItems_;
std::deque<BuybackItem> buybackItems_;
std::unordered_map<uint64_t, BuybackItem> pendingSellToBuyback_;
int pendingBuybackSlot_ = -1;
uint32_t pendingBuybackWireSlot_ = 0;
uint32_t pendingBuyItemId_ = 0;
uint32_t pendingBuyItemSlot_ = 0;
// ---- Mail state ----
bool mailboxOpen_ = false;
uint64_t mailboxGuid_ = 0;
std::vector<MailMessage> mailInbox_;
int selectedMailIndex_ = -1;
bool showMailCompose_ = false;
bool hasNewMail_ = false;
std::array<MailAttachSlot, MAIL_MAX_ATTACHMENTS> mailAttachments_{};
// ---- Bank state ----
bool bankOpen_ = false;
uint64_t bankerGuid_ = 0;
std::array<uint64_t, 28> bankSlotGuids_{};
std::array<uint64_t, 7> bankBagSlotGuids_{};
int effectiveBankSlots_ = 28;
int effectiveBankBagSlots_ = 7;
// ---- Guild Bank state ----
bool guildBankOpen_ = false;
uint64_t guildBankerGuid_ = 0;
GuildBankData guildBankData_;
uint8_t guildBankActiveTab_ = 0;
// ---- Auction House state ----
bool auctionOpen_ = false;
uint64_t auctioneerGuid_ = 0;
uint32_t auctionHouseId_ = 0;
AuctionListResult auctionBrowseResults_;
AuctionListResult auctionOwnerResults_;
AuctionListResult auctionBidderResults_;
int auctionActiveTab_ = 0;
float auctionSearchDelayTimer_ = 0.0f;
struct AuctionSearchParams {
std::string name;
uint8_t levelMin = 0, levelMax = 0;
uint32_t quality = 0xFFFFFFFF;
uint32_t itemClass = 0xFFFFFFFF;
uint32_t itemSubClass = 0xFFFFFFFF;
uint32_t invTypeMask = 0;
uint8_t usableOnly = 0;
uint32_t offset = 0;
};
AuctionSearchParams lastAuctionSearch_;
enum class AuctionResultTarget { BROWSE, OWNER, BIDDER };
AuctionResultTarget pendingAuctionTarget_ = AuctionResultTarget::BROWSE;
// ---- Trainer state ----
bool trainerWindowOpen_ = false;
TrainerListData currentTrainerList_;
std::vector<TrainerTab> trainerTabs_;
// ---- Equipment set state ----
struct EquipmentSet {
uint64_t setGuid = 0;
uint32_t setId = 0;
std::string name;
std::string iconName;
uint32_t ignoreSlotMask = 0;
std::array<uint64_t, 19> itemGuids{};
};
std::vector<EquipmentSet> equipmentSets_;
std::string pendingSaveSetName_;
std::string pendingSaveSetIcon_;
std::vector<EquipmentSetInfo> equipmentSetInfo_;
};
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,272 @@
#pragma once
#include "game/world_packets.hpp"
#include "game/opcode_table.hpp"
#include "network/packet.hpp"
#include <glm/glm.hpp>
#include <chrono>
#include <deque>
#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace wowee {
namespace game {
class GameHandler;
class MovementHandler {
public:
using PacketHandler = std::function<void(network::Packet&)>;
using DispatchTable = std::unordered_map<LogicalOpcode, PacketHandler>;
explicit MovementHandler(GameHandler& owner);
void registerOpcodes(DispatchTable& table);
// --- Public API (delegated from GameHandler) ---
void sendMovement(Opcode opcode);
void setPosition(float x, float y, float z);
void setOrientation(float orientation);
void setMovementPitch(float radians) { movementInfo.pitch = radians; }
void dismount();
// Follow target (moved from GameHandler)
void followTarget();
void cancelFollow();
// Area trigger detection
void loadAreaTriggerDbc();
void checkAreaTriggers();
// Transport attachment
void setTransportAttachment(uint64_t childGuid, ObjectType type, uint64_t transportGuid,
const glm::vec3& localOffset, bool hasLocalOrientation,
float localOrientation);
void clearTransportAttachment(uint64_t childGuid);
void updateAttachedTransportChildren(float deltaTime);
// Movement info accessors
const MovementInfo& getMovementInfo() const { return movementInfo; }
MovementInfo& getMovementInfoMut() { return movementInfo; }
// Speed accessors
float getServerRunSpeed() const { return serverRunSpeed_; }
float getServerWalkSpeed() const { return serverWalkSpeed_; }
float getServerSwimSpeed() const { return serverSwimSpeed_; }
float getServerSwimBackSpeed() const { return serverSwimBackSpeed_; }
float getServerFlightSpeed() const { return serverFlightSpeed_; }
float getServerFlightBackSpeed() const { return serverFlightBackSpeed_; }
float getServerRunBackSpeed() const { return serverRunBackSpeed_; }
float getServerTurnRate() const { return serverTurnRate_; }
// Movement flag queries
bool isPlayerRooted() const {
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::ROOT)) != 0;
}
bool isGravityDisabled() const {
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::LEVITATING)) != 0;
}
bool isFeatherFalling() const {
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::FEATHER_FALL)) != 0;
}
bool isWaterWalking() const {
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::WATER_WALK)) != 0;
}
bool isPlayerFlying() const {
const uint32_t flyMask = static_cast<uint32_t>(MovementFlags::CAN_FLY) |
static_cast<uint32_t>(MovementFlags::FLYING);
return (movementInfo.flags & flyMask) == flyMask;
}
bool isHovering() const {
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::HOVER)) != 0;
}
bool isSwimming() const {
return (movementInfo.flags & static_cast<uint32_t>(MovementFlags::SWIMMING)) != 0;
}
// Taxi / Flight Paths
bool isTaxiWindowOpen() const { return taxiWindowOpen_; }
void closeTaxi();
void activateTaxi(uint32_t destNodeId);
bool isOnTaxiFlight() const { return onTaxiFlight_; }
bool isTaxiMountActive() const { return taxiMountActive_; }
bool isTaxiActivationPending() const { return taxiActivatePending_; }
void forceClearTaxiAndMovementState();
const std::string& getTaxiDestName() const { return taxiDestName_; }
const ShowTaxiNodesData& getTaxiData() const { return currentTaxiData_; }
uint32_t getTaxiCurrentNode() const { return currentTaxiData_.nearestNode; }
struct TaxiNode {
uint32_t id = 0;
uint32_t mapId = 0;
float x = 0, y = 0, z = 0;
std::string name;
uint32_t mountDisplayIdAlliance = 0;
uint32_t mountDisplayIdHorde = 0;
};
struct TaxiPathEdge {
uint32_t pathId = 0;
uint32_t fromNode = 0, toNode = 0;
uint32_t cost = 0;
};
struct TaxiPathNode {
uint32_t id = 0;
uint32_t pathId = 0;
uint32_t nodeIndex = 0;
uint32_t mapId = 0;
float x = 0, y = 0, z = 0;
};
const std::unordered_map<uint32_t, TaxiNode>& getTaxiNodes() const { return taxiNodes_; }
bool isKnownTaxiNode(uint32_t nodeId) const {
if (nodeId == 0 || nodeId > 384) return false;
uint32_t idx = nodeId - 1;
return (knownTaxiMask_[idx / 32] & (1u << (idx % 32))) != 0;
}
uint32_t getTaxiCostTo(uint32_t destNodeId) const;
bool taxiNpcHasRoutes(uint64_t guid) const {
auto it = taxiNpcHasRoutes_.find(guid);
return it != taxiNpcHasRoutes_.end() && it->second;
}
void updateClientTaxi(float deltaTime);
uint32_t nextMovementTimestampMs();
void sanitizeMovementForTaxi();
// Heartbeat / movement timing (for GameHandler::update())
float& timeSinceLastMoveHeartbeatRef() { return timeSinceLastMoveHeartbeat_; }
float getMoveHeartbeatInterval() const { return moveHeartbeatInterval_; }
bool isServerMovementAllowed() const { return serverMovementAllowed_; }
void setServerMovementAllowed(bool v) { serverMovementAllowed_ = v; }
uint32_t& monsterMovePacketsThisTickRef() { return monsterMovePacketsThisTick_; }
uint32_t& monsterMovePacketsDroppedThisTickRef() { return monsterMovePacketsDroppedThisTick_; }
// Taxi state references for GameHandler update/processing
bool& onTaxiFlightRef() { return onTaxiFlight_; }
bool& taxiMountActiveRef() { return taxiMountActive_; }
uint32_t& taxiMountDisplayIdRef() { return taxiMountDisplayId_; }
bool& taxiActivatePendingRef() { return taxiActivatePending_; }
float& taxiActivateTimerRef() { return taxiActivateTimer_; }
bool& taxiClientActiveRef() { return taxiClientActive_; }
float& taxiLandingCooldownRef() { return taxiLandingCooldown_; }
float& taxiStartGraceRef() { return taxiStartGrace_; }
bool& taxiRecoverPendingRef() { return taxiRecoverPending_; }
uint32_t& taxiRecoverMapIdRef() { return taxiRecoverMapId_; }
glm::vec3& taxiRecoverPosRef() { return taxiRecoverPos_; }
std::unordered_map<uint64_t, bool>& taxiNpcHasRoutesRef() { return taxiNpcHasRoutes_; }
uint32_t* knownTaxiMaskPtr() { return knownTaxiMask_; }
bool& taxiMaskInitializedRef() { return taxiMaskInitialized_; }
uint64_t& taxiNpcGuidRef() { return taxiNpcGuid_; }
// Other-player movement timing (for cleanup on despawn etc.)
std::unordered_map<uint64_t, uint32_t>& otherPlayerMoveTimeMsRef() { return otherPlayerMoveTimeMs_; }
std::unordered_map<uint64_t, float>& otherPlayerSmoothedIntervalMsRef() { return otherPlayerSmoothedIntervalMs_; }
// Methods also called from GameHandler's registerOpcodeHandlers
void handleCompressedMoves(network::Packet& packet);
void handleForceMoveFlagChange(network::Packet& packet, const char* name, Opcode ackOpcode, uint32_t flag, bool set);
void handleMoveSetCollisionHeight(network::Packet& packet);
void applyTaxiMountForCurrentNode();
private:
// --- Packet handlers ---
void handleMonsterMove(network::Packet& packet);
void handleMonsterMoveTransport(network::Packet& packet);
void handleOtherPlayerMovement(network::Packet& packet);
void handleMoveSetSpeed(network::Packet& packet);
void handleForceRunSpeedChange(network::Packet& packet);
void handleForceSpeedChange(network::Packet& packet, const char* name, Opcode ackOpcode, float* speedStorage);
void handleForceMoveRootState(network::Packet& packet, bool rooted);
void handleMoveKnockBack(network::Packet& packet);
void handleTeleportAck(network::Packet& packet);
void handleNewWorld(network::Packet& packet);
void handleShowTaxiNodes(network::Packet& packet);
void handleClientControlUpdate(network::Packet& packet);
void handleActivateTaxiReply(network::Packet& packet);
void loadTaxiDbc();
// --- Private helpers ---
void buildTaxiCostMap();
void startClientTaxiPath(const std::vector<uint32_t>& pathNodes);
friend class GameHandler;
GameHandler& owner_;
// --- Movement state ---
// Reference to GameHandler's movementInfo to avoid desync
MovementInfo& movementInfo;
std::chrono::steady_clock::time_point movementClockStart_ = std::chrono::steady_clock::now();
uint32_t lastMovementTimestampMs_ = 0;
bool serverMovementAllowed_ = true;
uint32_t monsterMovePacketsThisTick_ = 0;
uint32_t monsterMovePacketsDroppedThisTick_ = 0;
// Fall/jump tracking
bool isFalling_ = false;
uint32_t fallStartMs_ = 0;
// Heartbeat timing
float timeSinceLastMoveHeartbeat_ = 0.0f;
float moveHeartbeatInterval_ = 0.5f;
uint32_t lastHeartbeatSendTimeMs_ = 0;
float lastHeartbeatX_ = 0.0f;
float lastHeartbeatY_ = 0.0f;
float lastHeartbeatZ_ = 0.0f;
uint32_t lastHeartbeatFlags_ = 0;
uint64_t lastHeartbeatTransportGuid_ = 0;
uint32_t lastNonHeartbeatMoveSendTimeMs_ = 0;
uint32_t lastFacingSendTimeMs_ = 0;
float lastFacingSentOrientation_ = 0.0f;
// Speed state
float serverRunSpeed_ = 7.0f;
float serverWalkSpeed_ = 2.5f;
float serverRunBackSpeed_ = 4.5f;
float serverSwimSpeed_ = 4.722f;
float serverSwimBackSpeed_ = 2.5f;
float serverFlightSpeed_ = 7.0f;
float serverFlightBackSpeed_ = 4.5f;
float serverTurnRate_ = 3.14159f;
float serverPitchRate_ = 3.14159f;
// Other-player movement smoothing
std::unordered_map<uint64_t, uint32_t> otherPlayerMoveTimeMs_;
std::unordered_map<uint64_t, float> otherPlayerSmoothedIntervalMs_;
// --- Taxi / Flight Path state ---
std::unordered_map<uint64_t, bool> taxiNpcHasRoutes_;
std::unordered_map<uint32_t, TaxiNode> taxiNodes_;
std::vector<TaxiPathEdge> taxiPathEdges_;
std::unordered_map<uint32_t, std::vector<TaxiPathNode>> taxiPathNodes_;
bool taxiDbcLoaded_ = false;
bool taxiWindowOpen_ = false;
ShowTaxiNodesData currentTaxiData_;
uint64_t taxiNpcGuid_ = 0;
bool onTaxiFlight_ = false;
std::string taxiDestName_;
bool taxiMountActive_ = false;
uint32_t taxiMountDisplayId_ = 0;
bool taxiActivatePending_ = false;
float taxiActivateTimer_ = 0.0f;
bool taxiClientActive_ = false;
float taxiLandingCooldown_ = 0.0f;
float taxiStartGrace_ = 0.0f;
size_t taxiClientIndex_ = 0;
std::vector<glm::vec3> taxiClientPath_;
float taxiClientSpeed_ = 32.0f;
float taxiClientSegmentProgress_ = 0.0f;
bool taxiRecoverPending_ = false;
uint32_t taxiRecoverMapId_ = 0;
glm::vec3 taxiRecoverPos_{0.0f};
uint32_t knownTaxiMask_[12] = {};
bool taxiMaskInitialized_ = false;
std::unordered_map<uint32_t, uint32_t> taxiCostMap_;
};
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,199 @@
#pragma once
#include "game/world_packets.hpp"
#include "game/opcode_table.hpp"
#include "game/handler_types.hpp"
#include "network/packet.hpp"
#include <array>
#include <chrono>
#include <functional>
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace wowee {
namespace game {
class GameHandler;
enum class QuestGiverStatus : uint8_t;
class QuestHandler {
public:
using PacketHandler = std::function<void(network::Packet&)>;
using DispatchTable = std::unordered_map<LogicalOpcode, PacketHandler>;
explicit QuestHandler(GameHandler& owner);
void registerOpcodes(DispatchTable& table);
// --- Public API (delegated from GameHandler) ---
// NPC Gossip
void selectGossipOption(uint32_t optionId);
void selectGossipQuest(uint32_t questId);
void acceptQuest();
void declineQuest();
void closeGossip();
void offerQuestFromItem(uint64_t itemGuid, uint32_t questId);
bool isGossipWindowOpen() const { return gossipWindowOpen_; }
const GossipMessageData& getCurrentGossip() const { return currentGossip_; }
// Quest details
bool isQuestDetailsOpen() {
if (questDetailsOpen_) return true;
if (questDetailsOpenTime_ != std::chrono::steady_clock::time_point{}) {
if (std::chrono::steady_clock::now() >= questDetailsOpenTime_) {
questDetailsOpen_ = true;
questDetailsOpenTime_ = std::chrono::steady_clock::time_point{};
return true;
}
}
return false;
}
const QuestDetailsData& getQuestDetails() const { return currentQuestDetails_; }
// Gossip / quest map POI markers (aliased from handler_types.hpp)
using GossipPoi = game::GossipPoi;
const std::vector<GossipPoi>& getGossipPois() const { return gossipPois_; }
void clearGossipPois() { gossipPois_.clear(); }
// Quest turn-in
bool isQuestRequestItemsOpen() const { return questRequestItemsOpen_; }
const QuestRequestItemsData& getQuestRequestItems() const { return currentQuestRequestItems_; }
void completeQuest();
void closeQuestRequestItems();
bool isQuestOfferRewardOpen() const { return questOfferRewardOpen_; }
const QuestOfferRewardData& getQuestOfferReward() const { return currentQuestOfferReward_; }
void chooseQuestReward(uint32_t rewardIndex);
void closeQuestOfferReward();
// Quest log
struct QuestLogEntry {
uint32_t questId = 0;
std::string title;
std::string objectives;
bool complete = false;
std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> killCounts;
std::unordered_map<uint32_t, uint32_t> itemCounts;
std::unordered_map<uint32_t, uint32_t> requiredItemCounts;
struct KillObjective {
int32_t npcOrGoId = 0;
uint32_t required = 0;
};
std::array<KillObjective, 4> killObjectives{};
struct ItemObjective {
uint32_t itemId = 0;
uint32_t required = 0;
};
std::array<ItemObjective, 6> itemObjectives{};
int32_t rewardMoney = 0;
std::array<QuestRewardItem, 4> rewardItems{};
std::array<QuestRewardItem, 6> rewardChoiceItems{};
};
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
int getSelectedQuestLogIndex() const { return selectedQuestLogIndex_; }
void setSelectedQuestLogIndex(int idx) { selectedQuestLogIndex_ = idx; }
void abandonQuest(uint32_t questId);
void shareQuestWithParty(uint32_t questId);
bool requestQuestQuery(uint32_t questId, bool force = false);
bool isQuestTracked(uint32_t questId) const { return trackedQuestIds_.count(questId) > 0; }
void setQuestTracked(uint32_t questId, bool tracked);
const std::unordered_set<uint32_t>& getTrackedQuestIds() const { return trackedQuestIds_; }
bool isQuestQueryPending(uint32_t questId) const {
return pendingQuestQueryIds_.count(questId) > 0;
}
void clearQuestQueryPending(uint32_t questId) { pendingQuestQueryIds_.erase(questId); }
// Quest giver status (! and ? markers)
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const;
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const { return npcQuestStatus_; }
// Shared quest
bool hasPendingSharedQuest() const { return pendingSharedQuest_; }
uint32_t getSharedQuestId() const { return sharedQuestId_; }
const std::string& getSharedQuestTitle() const { return sharedQuestTitle_; }
const std::string& getSharedQuestSharerName() const { return sharedQuestSharerName_; }
void acceptSharedQuest();
void declineSharedQuest();
// --- Internal helpers called from GameHandler ---
bool hasQuestInLog(uint32_t questId) const;
int findQuestLogSlotIndexFromServer(uint32_t questId) const;
void addQuestToLocalLogIfMissing(uint32_t questId, const std::string& title, const std::string& objectives);
bool resyncQuestLogFromServerSlots(bool forceQueryMetadata);
void applyQuestStateFromFields(const std::map<uint16_t, uint32_t>& fields);
void applyPackedKillCountsFromFields(QuestLogEntry& quest);
void clearPendingQuestAccept(uint32_t questId);
void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason);
// Pending quest accept timeout state (used by GameHandler::update)
std::unordered_map<uint32_t, float>& pendingQuestAcceptTimeoutsRef() { return pendingQuestAcceptTimeouts_; }
std::unordered_map<uint32_t, uint64_t>& pendingQuestAcceptNpcGuidsRef() { return pendingQuestAcceptNpcGuids_; }
bool& pendingLoginQuestResyncRef() { return pendingLoginQuestResync_; }
float& pendingLoginQuestResyncTimeoutRef() { return pendingLoginQuestResyncTimeout_; }
// Direct state access for vendor/gossip interaction in GameHandler
bool& gossipWindowOpenRef() { return gossipWindowOpen_; }
GossipMessageData& currentGossipRef() { return currentGossip_; }
std::unordered_map<uint64_t, QuestGiverStatus>& npcQuestStatusRef() { return npcQuestStatus_; }
private:
// --- Packet handlers ---
void handleGossipMessage(network::Packet& packet);
void handleQuestgiverQuestList(network::Packet& packet);
void handleGossipComplete(network::Packet& packet);
void handleQuestPoiQueryResponse(network::Packet& packet);
void handleQuestDetails(network::Packet& packet);
void handleQuestRequestItems(network::Packet& packet);
void handleQuestOfferReward(network::Packet& packet);
void handleQuestConfirmAccept(network::Packet& packet);
GameHandler& owner_;
// --- State ---
// Gossip
bool gossipWindowOpen_ = false;
GossipMessageData currentGossip_;
std::vector<GossipPoi> gossipPois_;
// Quest details
bool questDetailsOpen_ = false;
std::chrono::steady_clock::time_point questDetailsOpenTime_{};
QuestDetailsData currentQuestDetails_;
// Quest turn-in
bool questRequestItemsOpen_ = false;
QuestRequestItemsData currentQuestRequestItems_;
uint32_t pendingTurnInQuestId_ = 0;
uint64_t pendingTurnInNpcGuid_ = 0;
bool pendingTurnInRewardRequest_ = false;
std::unordered_map<uint32_t, float> pendingQuestAcceptTimeouts_;
std::unordered_map<uint32_t, uint64_t> pendingQuestAcceptNpcGuids_;
bool questOfferRewardOpen_ = false;
QuestOfferRewardData currentQuestOfferReward_;
// Quest log
std::vector<QuestLogEntry> questLog_;
int selectedQuestLogIndex_ = 0;
std::unordered_set<uint32_t> pendingQuestQueryIds_;
std::unordered_set<uint32_t> trackedQuestIds_;
bool pendingLoginQuestResync_ = false;
float pendingLoginQuestResyncTimeout_ = 0.0f;
// Quest giver status per NPC
std::unordered_map<uint64_t, QuestGiverStatus> npcQuestStatus_;
// Shared quest state
bool pendingSharedQuest_ = false;
uint32_t sharedQuestId_ = 0;
std::string sharedQuestTitle_;
std::string sharedQuestSharerName_;
uint64_t sharedQuestSharerGuid_ = 0;
};
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,444 @@
#pragma once
#include "game/world_packets.hpp"
#include "game/opcode_table.hpp"
#include "game/group_defines.hpp"
#include "game/handler_types.hpp"
#include "network/packet.hpp"
#include <array>
#include <chrono>
#include <functional>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace wowee {
namespace game {
class GameHandler;
class SocialHandler {
public:
using PacketHandler = std::function<void(network::Packet&)>;
using DispatchTable = std::unordered_map<LogicalOpcode, PacketHandler>;
explicit SocialHandler(GameHandler& owner);
void registerOpcodes(DispatchTable& table);
// ---- Structs (aliased from handler_types.hpp) ----
using InspectArenaTeam = game::InspectArenaTeam;
using InspectResult = game::InspectResult;
using WhoEntry = game::WhoEntry;
using BgQueueSlot = game::BgQueueSlot;
using AvailableBgInfo = game::AvailableBgInfo;
using BgPlayerScore = game::BgPlayerScore;
using ArenaTeamScore = game::ArenaTeamScore;
using BgScoreboardData = game::BgScoreboardData;
using BgPlayerPosition = game::BgPlayerPosition;
using PetitionSignature = game::PetitionSignature;
using PetitionInfo = game::PetitionInfo;
using ReadyCheckResult = game::ReadyCheckResult;
using InstanceLockout = game::InstanceLockout;
using LfgState = game::LfgState;
using ArenaTeamStats = game::ArenaTeamStats;
using ArenaTeamMember = game::ArenaTeamMember;
using ArenaTeamRoster = game::ArenaTeamRoster;
// ---- Public API ----
// Inspection
void inspectTarget();
const InspectResult* getInspectResult() const {
return inspectResult_.guid ? &inspectResult_ : nullptr;
}
// Server info / who
void queryServerTime();
void requestPlayedTime();
void queryWho(const std::string& playerName = "");
uint32_t getTotalTimePlayed() const { return totalTimePlayed_; }
uint32_t getLevelTimePlayed() const { return levelTimePlayed_; }
const std::vector<WhoEntry>& getWhoResults() const { return whoResults_; }
uint32_t getWhoOnlineCount() const { return whoOnlineCount_; }
std::string getWhoAreaName(uint32_t zoneId) const;
// Social commands
void addFriend(const std::string& playerName, const std::string& note = "");
void removeFriend(const std::string& playerName);
void setFriendNote(const std::string& playerName, const std::string& note);
void addIgnore(const std::string& playerName);
void removeIgnore(const std::string& playerName);
// Random roll
void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100);
// Battleground
bool hasPendingBgInvite() const;
void acceptBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
void declineBattlefield(uint32_t queueSlot = 0xFFFFFFFF);
const std::array<BgQueueSlot, 3>& getBgQueues() const { return bgQueues_; }
const std::vector<AvailableBgInfo>& getAvailableBgs() const { return availableBgs_; }
void requestPvpLog();
const BgScoreboardData* getBgScoreboard() const {
return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_;
}
const std::vector<BgPlayerPosition>& getBgPlayerPositions() const { return bgPlayerPositions_; }
// Logout
void requestLogout();
void cancelLogout();
bool isLoggingOut() const { return loggingOut_; }
float getLogoutCountdown() const { return logoutCountdown_; }
// Guild
void requestGuildInfo();
void requestGuildRoster();
void setGuildMotd(const std::string& motd);
void promoteGuildMember(const std::string& playerName);
void demoteGuildMember(const std::string& playerName);
void leaveGuild();
void inviteToGuild(const std::string& playerName);
void kickGuildMember(const std::string& playerName);
void disbandGuild();
void setGuildLeader(const std::string& name);
void setGuildPublicNote(const std::string& name, const std::string& note);
void setGuildOfficerNote(const std::string& name, const std::string& note);
void acceptGuildInvite();
void declineGuildInvite();
void queryGuildInfo(uint32_t guildId);
void createGuild(const std::string& guildName);
void addGuildRank(const std::string& rankName);
void deleteGuildRank();
void requestPetitionShowlist(uint64_t npcGuid);
void buyPetition(uint64_t npcGuid, const std::string& guildName);
// Guild state accessors
bool isInGuild() const;
const std::string& getGuildName() const { return guildName_; }
const GuildRosterData& getGuildRoster() const { return guildRoster_; }
bool hasGuildRoster() const { return hasGuildRoster_; }
const std::vector<std::string>& getGuildRankNames() const { return guildRankNames_; }
bool hasPendingGuildInvite() const { return pendingGuildInvite_; }
const std::string& getPendingGuildInviterName() const { return pendingGuildInviterName_; }
const std::string& getPendingGuildInviteGuildName() const { return pendingGuildInviteGuildName_; }
const GuildInfoData& getGuildInfoData() const { return guildInfoData_; }
const GuildQueryResponseData& getGuildQueryData() const { return guildQueryData_; }
bool hasGuildInfoData() const { return guildInfoData_.isValid(); }
// Petition
bool hasPetitionShowlist() const { return showPetitionDialog_; }
void clearPetitionDialog() { showPetitionDialog_ = false; }
uint32_t getPetitionCost() const { return petitionCost_; }
uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; }
const PetitionInfo& getPetitionInfo() const { return petitionInfo_; }
bool hasPetitionSignaturesUI() const { return petitionInfo_.showUI; }
void clearPetitionSignaturesUI() { petitionInfo_.showUI = false; }
void signPetition(uint64_t petitionGuid);
void turnInPetition(uint64_t petitionGuid);
// Guild name lookup
const std::string& lookupGuildName(uint32_t guildId);
uint32_t getEntityGuildId(uint64_t guid) const;
// Ready check
void initiateReadyCheck();
void respondToReadyCheck(bool ready);
bool hasPendingReadyCheck() const { return pendingReadyCheck_; }
void dismissReadyCheck() { pendingReadyCheck_ = false; }
const std::string& getReadyCheckInitiator() const { return readyCheckInitiator_; }
const std::vector<ReadyCheckResult>& getReadyCheckResults() const { return readyCheckResults_; }
// Duel
void acceptDuel();
void forfeitDuel();
void proposeDuel(uint64_t targetGuid);
void reportPlayer(uint64_t targetGuid, const std::string& reason);
bool hasPendingDuelRequest() const { return pendingDuelRequest_; }
const std::string& getDuelChallengerName() const { return duelChallengerName_; }
float getDuelCountdownRemaining() const {
if (duelCountdownMs_ == 0) return 0.0f;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - duelCountdownStartedAt_).count();
float rem = (static_cast<float>(duelCountdownMs_) - static_cast<float>(elapsed)) / 1000.0f;
return rem > 0.0f ? rem : 0.0f;
}
// Party/Raid
void inviteToGroup(const std::string& playerName);
void acceptGroupInvite();
void declineGroupInvite();
void leaveGroup();
void convertToRaid();
void sendSetLootMethod(uint32_t method, uint32_t threshold, uint64_t masterLooterGuid);
bool isInGroup() const { return !partyData.isEmpty(); }
const GroupListData& getPartyData() const { return partyData; }
bool hasPendingGroupInvite() const { return pendingGroupInvite; }
const std::string& getPendingInviterName() const { return pendingInviterName; }
void uninvitePlayer(const std::string& playerName);
void leaveParty();
void setMainTank(uint64_t targetGuid);
void setMainAssist(uint64_t targetGuid);
void clearMainTank();
void clearMainAssist();
void setRaidMark(uint64_t guid, uint8_t icon);
void requestRaidInfo();
// Instance lockouts
const std::vector<InstanceLockout>& getInstanceLockouts() const { return instanceLockouts_; }
// Minimap ping
void sendMinimapPing(float wowX, float wowY);
// Summon request
void handleSummonRequest(network::Packet& packet);
void acceptSummon();
void declineSummon();
// Battlefield Manager
void acceptBfMgrInvite();
void declineBfMgrInvite();
// Calendar
void requestCalendar();
// ---- Methods moved from GameHandler ----
void sendSetDifficulty(uint32_t difficulty);
void toggleHelm();
void toggleCloak();
void setStandState(uint8_t standState);
void sendAlterAppearance(uint32_t hairStyle, uint32_t hairColor, uint32_t facialHair);
void deleteGmTicket();
void requestGmTicket();
// Utility methods for delegation from GameHandler
void updateLogoutCountdown(float deltaTime);
void resetTransferState();
GroupListData& mutablePartyData() { return partyData; }
InspectResult& mutableInspectResult() { return inspectResult_; }
void setRaidTargetGuid(uint8_t icon, uint64_t guid) {
if (icon < kRaidMarkCount) raidTargetGuids_[icon] = guid;
}
void setEncounterUnitGuid(uint32_t slot, uint64_t guid) {
if (slot < kMaxEncounterSlots) encounterUnitGuids_[slot] = guid;
}
// Encounter unit tracking
static constexpr uint32_t kMaxEncounterSlots = 5;
uint64_t getEncounterUnitGuid(uint32_t slot) const {
return (slot < kMaxEncounterSlots) ? encounterUnitGuids_[slot] : 0;
}
// Raid target markers (0-7: Star, Circle, Diamond, Triangle, Moon, Square, Cross, Skull)
static constexpr uint32_t kRaidMarkCount = 8;
uint64_t getRaidMarkGuid(uint32_t icon) const {
return (icon < kRaidMarkCount) ? raidTargetGuids_[icon] : 0;
}
uint8_t getEntityRaidMark(uint64_t guid) const {
if (guid == 0) return 0xFF;
for (uint32_t i = 0; i < kRaidMarkCount; ++i)
if (raidTargetGuids_[i] == guid) return static_cast<uint8_t>(i);
return 0xFF;
}
// LFG / Dungeon Finder
void lfgJoin(uint32_t dungeonId, uint8_t roles);
void lfgLeave();
void lfgSetRoles(uint8_t roles);
void lfgAcceptProposal(uint32_t proposalId, bool accept);
void lfgSetBootVote(bool vote);
void lfgTeleport(bool toLfgDungeon = true);
LfgState getLfgState() const { return lfgState_; }
bool isLfgQueued() const { return lfgState_ == LfgState::Queued; }
bool isLfgInDungeon() const { return lfgState_ == LfgState::InDungeon; }
uint32_t getLfgDungeonId() const { return lfgDungeonId_; }
std::string getCurrentLfgDungeonName() const;
uint32_t getLfgProposalId() const { return lfgProposalId_; }
int32_t getLfgAvgWaitSec() const { return lfgAvgWaitSec_; }
uint32_t getLfgTimeInQueueMs() const { return lfgTimeInQueueMs_; }
uint32_t getLfgBootVotes() const { return lfgBootVotes_; }
uint32_t getLfgBootTotal() const { return lfgBootTotal_; }
uint32_t getLfgBootTimeLeft() const { return lfgBootTimeLeft_; }
uint32_t getLfgBootNeeded() const { return lfgBootNeeded_; }
const std::string& getLfgBootTargetName() const { return lfgBootTargetName_; }
const std::string& getLfgBootReason() const { return lfgBootReason_; }
// Arena
const std::vector<ArenaTeamStats>& getArenaTeamStats() const { return arenaTeamStats_; }
void requestArenaTeamRoster(uint32_t teamId);
const ArenaTeamRoster* getArenaTeamRoster(uint32_t teamId) const {
for (const auto& r : arenaTeamRosters_)
if (r.teamId == teamId) return &r;
return nullptr;
}
private:
// ---- Packet handlers ----
void handleInspectResults(network::Packet& packet);
void handleQueryTimeResponse(network::Packet& packet);
void handlePlayedTime(network::Packet& packet);
void handleWho(network::Packet& packet);
void handleFriendList(network::Packet& packet);
void handleContactList(network::Packet& packet);
void handleFriendStatus(network::Packet& packet);
void handleRandomRoll(network::Packet& packet);
void handleLogoutResponse(network::Packet& packet);
void handleLogoutComplete(network::Packet& packet);
void handleGroupInvite(network::Packet& packet);
void handleGroupDecline(network::Packet& packet);
void handleGroupList(network::Packet& packet);
void handleGroupUninvite(network::Packet& packet);
void handlePartyCommandResult(network::Packet& packet);
void handlePartyMemberStats(network::Packet& packet, bool isFull);
void handleGuildInfo(network::Packet& packet);
void handleGuildRoster(network::Packet& packet);
void handleGuildQueryResponse(network::Packet& packet);
void handleGuildEvent(network::Packet& packet);
void handleGuildInvite(network::Packet& packet);
void handleGuildCommandResult(network::Packet& packet);
void handlePetitionShowlist(network::Packet& packet);
void handlePetitionQueryResponse(network::Packet& packet);
void handlePetitionShowSignatures(network::Packet& packet);
void handlePetitionSignResults(network::Packet& packet);
void handleTurnInPetitionResults(network::Packet& packet);
void handleBattlefieldStatus(network::Packet& packet);
void handleBattlefieldList(network::Packet& packet);
void handleRaidInstanceInfo(network::Packet& packet);
void handleInstanceDifficulty(network::Packet& packet);
void handleDuelRequested(network::Packet& packet);
void handleDuelComplete(network::Packet& packet);
void handleDuelWinner(network::Packet& packet);
void handleLfgJoinResult(network::Packet& packet);
void handleLfgQueueStatus(network::Packet& packet);
void handleLfgProposalUpdate(network::Packet& packet);
void handleLfgRoleCheckUpdate(network::Packet& packet);
void handleLfgUpdatePlayer(network::Packet& packet);
void handleLfgPlayerReward(network::Packet& packet);
void handleLfgBootProposalUpdate(network::Packet& packet);
void handleLfgTeleportDenied(network::Packet& packet);
void handleArenaTeamCommandResult(network::Packet& packet);
void handleArenaTeamQueryResponse(network::Packet& packet);
void handleArenaTeamRoster(network::Packet& packet);
void handleArenaTeamInvite(network::Packet& packet);
void handleArenaTeamEvent(network::Packet& packet);
void handleArenaTeamStats(network::Packet& packet);
void handleArenaError(network::Packet& packet);
void handlePvpLogData(network::Packet& packet);
void handleInitializeFactions(network::Packet& packet);
void handleSetFactionStanding(network::Packet& packet);
void handleSetFactionAtWar(network::Packet& packet);
void handleSetFactionVisible(network::Packet& packet);
void handleGroupSetLeader(network::Packet& packet);
GameHandler& owner_;
// ---- State ----
// Inspect
InspectResult inspectResult_;
// Logout
bool loggingOut_ = false;
float logoutCountdown_ = 0.0f;
// Time played
uint32_t totalTimePlayed_ = 0;
uint32_t levelTimePlayed_ = 0;
// Who results
std::vector<WhoEntry> whoResults_;
uint32_t whoOnlineCount_ = 0;
// Duel
bool pendingDuelRequest_ = false;
uint64_t duelChallengerGuid_= 0;
uint64_t duelFlagGuid_ = 0;
std::string duelChallengerName_;
uint32_t duelCountdownMs_ = 0;
std::chrono::steady_clock::time_point duelCountdownStartedAt_{};
// Guild
std::string guildName_;
std::vector<std::string> guildRankNames_;
GuildRosterData guildRoster_;
GuildInfoData guildInfoData_;
GuildQueryResponseData guildQueryData_;
bool hasGuildRoster_ = false;
std::unordered_map<uint32_t, std::string> guildNameCache_;
std::unordered_set<uint32_t> pendingGuildNameQueries_;
bool pendingGuildInvite_ = false;
std::string pendingGuildInviterName_;
std::string pendingGuildInviteGuildName_;
bool showPetitionDialog_ = false;
uint32_t petitionCost_ = 0;
uint64_t petitionNpcGuid_ = 0;
PetitionInfo petitionInfo_;
// Group
GroupListData partyData;
bool pendingGroupInvite = false;
std::string pendingInviterName;
// Ready check
bool pendingReadyCheck_ = false;
uint32_t readyCheckReadyCount_ = 0;
uint32_t readyCheckNotReadyCount_ = 0;
std::string readyCheckInitiator_;
std::vector<ReadyCheckResult> readyCheckResults_;
// Instance
std::vector<InstanceLockout> instanceLockouts_;
uint32_t instanceDifficulty_ = 0;
bool instanceIsHeroic_ = false;
bool inInstance_ = false;
// Raid marks
std::array<uint64_t, kRaidMarkCount> raidTargetGuids_ = {};
// Encounter units
std::array<uint64_t, kMaxEncounterSlots> encounterUnitGuids_ = {};
// Arena
std::vector<ArenaTeamStats> arenaTeamStats_;
std::vector<ArenaTeamRoster> arenaTeamRosters_;
// Battleground
std::array<BgQueueSlot, 3> bgQueues_{};
std::vector<AvailableBgInfo> availableBgs_;
BgScoreboardData bgScoreboard_;
std::vector<BgPlayerPosition> bgPlayerPositions_;
// LFG / Dungeon Finder
LfgState lfgState_ = LfgState::None;
uint32_t lfgDungeonId_ = 0;
uint32_t lfgProposalId_ = 0;
int32_t lfgAvgWaitSec_ = -1;
uint32_t lfgTimeInQueueMs_= 0;
uint32_t lfgBootVotes_ = 0;
uint32_t lfgBootTotal_ = 0;
uint32_t lfgBootTimeLeft_ = 0;
uint32_t lfgBootNeeded_ = 0;
std::string lfgBootTargetName_;
std::string lfgBootReason_;
};
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,331 @@
#pragma once
#include "game/world_packets.hpp"
#include "game/opcode_table.hpp"
#include "game/spell_defines.hpp"
#include "game/handler_types.hpp"
#include "network/packet.hpp"
#include <array>
#include <chrono>
#include <functional>
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace wowee {
namespace game {
class GameHandler;
class SpellHandler {
public:
using PacketHandler = std::function<void(network::Packet&)>;
using DispatchTable = std::unordered_map<LogicalOpcode, PacketHandler>;
explicit SpellHandler(GameHandler& owner);
void registerOpcodes(DispatchTable& table);
// Talent data structures (aliased from handler_types.hpp)
using TalentEntry = game::TalentEntry;
using TalentTabEntry = game::TalentTabEntry;
// --- Spell book tabs ---
struct SpellBookTab {
std::string name;
std::string texture; // icon path
std::vector<uint32_t> spellIds; // spells in this tab
};
// Unit cast state (aliased from handler_types.hpp)
using UnitCastState = game::UnitCastState;
// Equipment set info (aliased from handler_types.hpp)
using EquipmentSetInfo = game::EquipmentSetInfo;
// --- Public API (delegated from GameHandler) ---
void castSpell(uint32_t spellId, uint64_t targetGuid = 0);
void cancelCast();
void cancelAura(uint32_t spellId);
// Known spells
const std::unordered_set<uint32_t>& getKnownSpells() const { return knownSpells_; }
const std::unordered_map<uint32_t, float>& getSpellCooldowns() const { return spellCooldowns_; }
float getSpellCooldown(uint32_t spellId) const;
// Cast state
bool isCasting() const { return casting_; }
bool isChanneling() const { return casting_ && castIsChannel_; }
bool isGameObjectInteractionCasting() const;
uint32_t getCurrentCastSpellId() const { return currentCastSpellId_; }
float getCastProgress() const { return castTimeTotal_ > 0 ? (castTimeTotal_ - castTimeRemaining_) / castTimeTotal_ : 0.0f; }
float getCastTimeRemaining() const { return castTimeRemaining_; }
float getCastTimeTotal() const { return castTimeTotal_; }
// Repeat-craft queue
void startCraftQueue(uint32_t spellId, int count);
void cancelCraftQueue();
int getCraftQueueRemaining() const { return craftQueueRemaining_; }
uint32_t getCraftQueueSpellId() const { return craftQueueSpellId_; }
// Spell queue (400ms window)
uint32_t getQueuedSpellId() const { return queuedSpellId_; }
void cancelQueuedSpell() { queuedSpellId_ = 0; queuedSpellTarget_ = 0; }
// Unit cast state (tracked per GUID for target frame + boss frames)
const UnitCastState* getUnitCastState(uint64_t guid) const {
auto it = unitCastStates_.find(guid);
return (it != unitCastStates_.end() && it->second.casting) ? &it->second : nullptr;
}
// Target cast helpers
bool isTargetCasting() const;
uint32_t getTargetCastSpellId() const;
float getTargetCastProgress() const;
float getTargetCastTimeRemaining() const;
bool isTargetCastInterruptible() const;
// Talents
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
uint8_t getUnspentTalentPoints() const { return unspentTalentPoints_[activeTalentSpec_]; }
uint8_t getUnspentTalentPoints(uint8_t spec) const { return spec < 2 ? unspentTalentPoints_[spec] : 0; }
const std::unordered_map<uint32_t, uint8_t>& getLearnedTalents() const { return learnedTalents_[activeTalentSpec_]; }
const std::unordered_map<uint32_t, uint8_t>& getLearnedTalents(uint8_t spec) const {
static std::unordered_map<uint32_t, uint8_t> empty;
return spec < 2 ? learnedTalents_[spec] : empty;
}
static constexpr uint8_t MAX_GLYPH_SLOTS = 6;
const std::array<uint16_t, MAX_GLYPH_SLOTS>& getGlyphs() const { return learnedGlyphs_[activeTalentSpec_]; }
const std::array<uint16_t, MAX_GLYPH_SLOTS>& getGlyphs(uint8_t spec) const {
static std::array<uint16_t, MAX_GLYPH_SLOTS> empty{};
return spec < 2 ? learnedGlyphs_[spec] : empty;
}
uint8_t getTalentRank(uint32_t talentId) const {
auto it = learnedTalents_[activeTalentSpec_].find(talentId);
return (it != learnedTalents_[activeTalentSpec_].end()) ? it->second : 0;
}
void learnTalent(uint32_t talentId, uint32_t requestedRank);
void switchTalentSpec(uint8_t newSpec);
// Talent DBC access
const TalentEntry* getTalentEntry(uint32_t talentId) const {
auto it = talentCache_.find(talentId);
return (it != talentCache_.end()) ? &it->second : nullptr;
}
const TalentTabEntry* getTalentTabEntry(uint32_t tabId) const {
auto it = talentTabCache_.find(tabId);
return (it != talentTabCache_.end()) ? &it->second : nullptr;
}
const std::unordered_map<uint32_t, TalentEntry>& getAllTalents() const { return talentCache_; }
const std::unordered_map<uint32_t, TalentTabEntry>& getAllTalentTabs() const { return talentTabCache_; }
void loadTalentDbc();
// Auras
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras_; }
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras_; }
const std::vector<AuraSlot>* getUnitAuras(uint64_t guid) const {
auto it = unitAurasCache_.find(guid);
return (it != unitAurasCache_.end()) ? &it->second : nullptr;
}
// Global Cooldown (GCD)
float getGCDRemaining() const {
if (gcdTotal_ <= 0.0f) return 0.0f;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - gcdStartedAt_).count() / 1000.0f;
float rem = gcdTotal_ - elapsed;
return rem > 0.0f ? rem : 0.0f;
}
float getGCDTotal() const { return gcdTotal_; }
bool isGCDActive() const { return getGCDRemaining() > 0.0f; }
// Spell book tabs
const std::vector<SpellBookTab>& getSpellBookTabs();
// Talent wipe confirm dialog
bool showTalentWipeConfirmDialog() const { return talentWipePending_; }
uint32_t getTalentWipeCost() const { return talentWipeCost_; }
void confirmTalentWipe();
void cancelTalentWipe() { talentWipePending_ = false; }
// Pet talent respec confirm
bool showPetUnlearnDialog() const { return petUnlearnPending_; }
uint32_t getPetUnlearnCost() const { return petUnlearnCost_; }
void confirmPetUnlearn();
void cancelPetUnlearn() { petUnlearnPending_ = false; }
// Item use
void useItemBySlot(int backpackIndex);
void useItemInBag(int bagIndex, int slotIndex);
void useItemById(uint32_t itemId);
// Equipment sets
const std::vector<EquipmentSetInfo>& getEquipmentSets() const { return equipmentSetInfo_; }
// Pet spells
void sendPetAction(uint32_t action, uint64_t targetGuid = 0);
void dismissPet();
void togglePetSpellAutocast(uint32_t spellId);
void renamePet(const std::string& newName);
// Spell DBC accessors
const int32_t* getSpellEffectBasePoints(uint32_t spellId) const;
float getSpellDuration(uint32_t spellId) const;
const std::string& getSpellName(uint32_t spellId) const;
const std::string& getSpellRank(uint32_t spellId) const;
const std::string& getSpellDescription(uint32_t spellId) const;
std::string getEnchantName(uint32_t enchantId) const;
uint8_t getSpellDispelType(uint32_t spellId) const;
bool isSpellInterruptible(uint32_t spellId) const;
uint32_t getSpellSchoolMask(uint32_t spellId) const;
const std::string& getSkillLineName(uint32_t spellId) const;
// Cast state
void stopCasting();
void resetCastState();
void clearUnitCaches();
// Aura duration
void handleUpdateAuraDuration(uint8_t slot, uint32_t durationMs);
// Skill DBC
void loadSkillLineDbc();
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
void extractExploredZoneFields(const std::map<uint16_t, uint32_t>& fields);
// Update per-frame timers (call from GameHandler::update)
void updateTimers(float dt);
private:
// --- Packet handlers ---
void handleInitialSpells(network::Packet& packet);
void handleCastFailed(network::Packet& packet);
void handleSpellStart(network::Packet& packet);
void handleSpellGo(network::Packet& packet);
void handleSpellCooldown(network::Packet& packet);
void handleCooldownEvent(network::Packet& packet);
void handleAuraUpdate(network::Packet& packet, bool isAll);
void handleLearnedSpell(network::Packet& packet);
void handlePetSpells(network::Packet& packet);
void handleListStabledPets(network::Packet& packet);
// Pet stable
void requestStabledPetList();
void stablePet(uint8_t slot);
void unstablePet(uint32_t petNumber);
void handleCastResult(network::Packet& packet);
void handleSpellFailedOther(network::Packet& packet);
void handleClearCooldown(network::Packet& packet);
void handleModifyCooldown(network::Packet& packet);
void handlePlaySpellVisual(network::Packet& packet);
void handleSpellModifier(network::Packet& packet, bool isFlat);
void handleSpellDelayed(network::Packet& packet);
void handleSpellLogMiss(network::Packet& packet);
void handleSpellFailure(network::Packet& packet);
void handleItemCooldown(network::Packet& packet);
void handleDispelFailed(network::Packet& packet);
void handleTotemCreated(network::Packet& packet);
void handlePeriodicAuraLog(network::Packet& packet);
void handleSpellEnergizeLog(network::Packet& packet);
void handleExtraAuraInfo(network::Packet& packet, bool isInit);
void handleSpellDispelLog(network::Packet& packet);
void handleSpellStealLog(network::Packet& packet);
void handleSpellChanceProcLog(network::Packet& packet);
void handleSpellInstaKillLog(network::Packet& packet);
void handleSpellLogExecute(network::Packet& packet);
void handleClearExtraAuraInfo(network::Packet& packet);
void handleItemEnchantTimeUpdate(network::Packet& packet);
void handleResumeCastBar(network::Packet& packet);
void handleChannelStart(network::Packet& packet);
void handleChannelUpdate(network::Packet& packet);
// --- Internal helpers ---
void loadSpellNameCache() const;
void loadSkillLineAbilityDbc();
void categorizeTrainerSpells();
void handleSupercededSpell(network::Packet& packet);
void handleRemovedSpell(network::Packet& packet);
void handleUnlearnSpells(network::Packet& packet);
void handleTalentsInfo(network::Packet& packet);
void handleAchievementEarned(network::Packet& packet);
void handleEquipmentSetList(network::Packet& packet);
friend class GameHandler;
friend class InventoryHandler;
friend class CombatHandler;
GameHandler& owner_;
// --- Spell state ---
std::unordered_set<uint32_t> knownSpells_;
std::unordered_map<uint32_t, float> spellCooldowns_; // spellId -> remaining seconds
uint8_t castCount_ = 0;
bool casting_ = false;
bool castIsChannel_ = false;
uint32_t currentCastSpellId_ = 0;
float castTimeRemaining_ = 0.0f;
float castTimeTotal_ = 0.0f;
// Repeat-craft queue
uint32_t craftQueueSpellId_ = 0;
int craftQueueRemaining_ = 0;
// Spell queue (400ms window)
uint32_t queuedSpellId_ = 0;
uint64_t queuedSpellTarget_ = 0;
// Per-unit cast state
std::unordered_map<uint64_t, UnitCastState> unitCastStates_;
// Talents (dual-spec support)
uint8_t activeTalentSpec_ = 0;
uint8_t unspentTalentPoints_[2] = {0, 0};
std::unordered_map<uint32_t, uint8_t> learnedTalents_[2];
std::array<std::array<uint16_t, MAX_GLYPH_SLOTS>, 2> learnedGlyphs_{};
std::unordered_map<uint32_t, TalentEntry> talentCache_;
std::unordered_map<uint32_t, TalentTabEntry> talentTabCache_;
bool talentDbcLoaded_ = false;
bool talentsInitialized_ = false;
// Auras
std::vector<AuraSlot> playerAuras_;
std::vector<AuraSlot> targetAuras_;
std::unordered_map<uint64_t, std::vector<AuraSlot>> unitAurasCache_;
// Global Cooldown
float gcdTotal_ = 0.0f;
std::chrono::steady_clock::time_point gcdStartedAt_{};
// Spell book tabs
std::vector<SpellBookTab> spellBookTabs_;
bool spellBookTabsDirty_ = true;
// Talent wipe confirm dialog
bool talentWipePending_ = false;
uint64_t talentWipeNpcGuid_ = 0;
uint32_t talentWipeCost_ = 0;
// Pet talent respec confirm dialog
bool petUnlearnPending_ = false;
uint64_t petUnlearnGuid_ = 0;
uint32_t petUnlearnCost_ = 0;
// Equipment sets
struct EquipmentSet {
uint64_t setGuid = 0;
uint32_t setId = 0;
std::string name;
std::string iconName;
uint32_t ignoreSlotMask = 0;
std::array<uint64_t, 19> itemGuids{};
};
std::vector<EquipmentSet> equipmentSets_;
std::vector<EquipmentSetInfo> equipmentSetInfo_;
};
} // namespace game
} // namespace wowee

View file

@ -0,0 +1,103 @@
#pragma once
#include "game/opcode_table.hpp"
#include "network/packet.hpp"
#include <cstdint>
#include <functional>
#include <future>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
namespace wowee {
namespace game {
class GameHandler;
class WardenCrypto;
class WardenMemory;
class WardenModule;
class WardenModuleManager;
class WardenHandler {
public:
using PacketHandler = std::function<void(network::Packet&)>;
using DispatchTable = std::unordered_map<LogicalOpcode, PacketHandler>;
explicit WardenHandler(GameHandler& owner);
void registerOpcodes(DispatchTable& table);
// --- Public API ---
/** Reset all warden state (called on connect / disconnect). */
void reset();
/** Initialize warden module manager (called once from GameHandler ctor). */
void initModuleManager();
/** Whether the server requires Warden (gates char enum / create). */
bool requiresWarden() const { return requiresWarden_; }
void setRequiresWarden(bool v) { requiresWarden_ = v; }
bool wardenGateSeen() const { return wardenGateSeen_; }
/** Increment packet-after-gate counter (called from handlePacket). */
void notifyPacketAfterGate() { ++wardenPacketsAfterGate_; }
bool wardenCharEnumBlockedLogged() const { return wardenCharEnumBlockedLogged_; }
void setWardenCharEnumBlockedLogged(bool v) { wardenCharEnumBlockedLogged_ = v; }
/** Called from GameHandler::update() to drain async warden response + log gate timing. */
void update(float deltaTime);
private:
void handleWardenData(network::Packet& packet);
bool loadWardenCRFile(const std::string& moduleHashHex);
GameHandler& owner_;
// --- Warden state ---
bool requiresWarden_ = false;
bool wardenGateSeen_ = false;
float wardenGateElapsed_ = 0.0f;
float wardenGateNextStatusLog_ = 2.0f;
uint32_t wardenPacketsAfterGate_ = 0;
bool wardenCharEnumBlockedLogged_ = false;
std::unique_ptr<WardenCrypto> wardenCrypto_;
std::unique_ptr<WardenMemory> wardenMemory_;
std::unique_ptr<WardenModuleManager> wardenModuleManager_;
// Warden module download state
enum class WardenState {
WAIT_MODULE_USE, // Waiting for first SMSG (MODULE_USE)
WAIT_MODULE_CACHE, // Sent MODULE_MISSING, receiving module chunks
WAIT_HASH_REQUEST, // Module received, waiting for HASH_REQUEST
WAIT_CHECKS, // Hash sent, waiting for check requests
};
WardenState wardenState_ = WardenState::WAIT_MODULE_USE;
std::vector<uint8_t> wardenModuleHash_; // 16 bytes MD5
std::vector<uint8_t> wardenModuleKey_; // 16 bytes RC4
uint32_t wardenModuleSize_ = 0;
std::vector<uint8_t> wardenModuleData_; // Downloaded module chunks
std::vector<uint8_t> wardenLoadedModuleImage_; // Parsed module image for key derivation
std::shared_ptr<WardenModule> wardenLoadedModule_; // Loaded Warden module
// Pre-computed challenge/response entries from .cr file
struct WardenCREntry {
uint8_t seed[16];
uint8_t reply[20];
uint8_t clientKey[16]; // Encrypt key (client→server)
uint8_t serverKey[16]; // Decrypt key (server→client)
};
std::vector<WardenCREntry> wardenCREntries_;
// Module-specific check type opcodes [9]: MEM, PAGE_A, PAGE_B, MPQ, LUA, DRIVER, TIMING, PROC, MODULE
uint8_t wardenCheckOpcodes_[9] = {};
// Async Warden response: avoids 5-second main-loop stalls from PAGE_A/PAGE_B code pattern searches
std::future<std::vector<uint8_t>> wardenPendingEncrypted_; // encrypted response bytes
bool wardenResponsePending_ = false;
};
} // namespace game
} // namespace wowee

713
src/game/chat_handler.cpp Normal file
View file

@ -0,0 +1,713 @@
#include "game/chat_handler.hpp"
#include "game/game_handler.hpp"
#include "game/game_utils.hpp"
#include "game/packet_parsers.hpp"
#include "game/entity.hpp"
#include "game/opcode_table.hpp"
#include "network/world_socket.hpp"
#include "rendering/renderer.hpp"
#include "core/logger.hpp"
#include <algorithm>
namespace wowee {
namespace game {
ChatHandler::ChatHandler(GameHandler& owner)
: owner_(owner) {}
void ChatHandler::registerOpcodes(DispatchTable& table) {
table[Opcode::SMSG_MESSAGECHAT] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD) handleMessageChat(packet);
};
table[Opcode::SMSG_GM_MESSAGECHAT] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD) handleMessageChat(packet);
};
table[Opcode::SMSG_TEXT_EMOTE] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD) handleTextEmote(packet);
};
table[Opcode::SMSG_EMOTE] = [this](network::Packet& packet) {
if (owner_.getState() != WorldState::IN_WORLD) return;
if (packet.getSize() - packet.getReadPos() < 12) return;
uint32_t emoteAnim = packet.readUInt32();
uint64_t sourceGuid = packet.readUInt64();
if (owner_.emoteAnimCallback_ && sourceGuid != 0)
owner_.emoteAnimCallback_(sourceGuid, emoteAnim);
};
table[Opcode::SMSG_CHANNEL_NOTIFY] = [this](network::Packet& packet) {
if (owner_.getState() == WorldState::IN_WORLD ||
owner_.getState() == WorldState::ENTERING_WORLD)
handleChannelNotify(packet);
};
table[Opcode::SMSG_CHAT_PLAYER_NOT_FOUND] = [this](network::Packet& packet) {
std::string name = packet.readString();
if (!name.empty()) addSystemChatMessage("No player named '" + name + "' is currently playing.");
};
table[Opcode::SMSG_CHAT_PLAYER_AMBIGUOUS] = [this](network::Packet& packet) {
std::string name = packet.readString();
if (!name.empty()) addSystemChatMessage("Player name '" + name + "' is ambiguous.");
};
table[Opcode::SMSG_CHAT_WRONG_FACTION] = [this](network::Packet& /*packet*/) {
owner_.addUIError("You cannot send messages to members of that faction.");
addSystemChatMessage("You cannot send messages to members of that faction.");
};
table[Opcode::SMSG_CHAT_NOT_IN_PARTY] = [this](network::Packet& /*packet*/) {
owner_.addUIError("You are not in a party.");
addSystemChatMessage("You are not in a party.");
};
table[Opcode::SMSG_CHAT_RESTRICTED] = [this](network::Packet& /*packet*/) {
owner_.addUIError("You cannot send chat messages in this area.");
addSystemChatMessage("You cannot send chat messages in this area.");
};
// ---- Channel list ----
// ---- Server / defense / area-trigger messages (moved from GameHandler) ----
table[Opcode::SMSG_DEFENSE_MESSAGE] = [this](network::Packet& packet) {
if (packet.hasRemaining(5)) {
/*uint32_t zoneId =*/ packet.readUInt32();
std::string defMsg = packet.readString();
if (!defMsg.empty()) addSystemChatMessage("[Defense] " + defMsg);
}
};
// Server messages
table[Opcode::SMSG_SERVER_MESSAGE] = [this](network::Packet& packet) {
if (packet.hasRemaining(4)) {
uint32_t msgType = packet.readUInt32();
std::string msg = packet.readString();
if (!msg.empty()) {
std::string prefix;
switch (msgType) {
case 1: prefix = "[Shutdown] "; owner_.addUIError("Server shutdown: " + msg); break;
case 2: prefix = "[Restart] "; owner_.addUIError("Server restart: " + msg); break;
case 4: prefix = "[Shutdown cancelled] "; break;
case 5: prefix = "[Restart cancelled] "; break;
default: prefix = "[Server] "; break;
}
addSystemChatMessage(prefix + msg);
}
}
};
table[Opcode::SMSG_CHAT_SERVER_MESSAGE] = [this](network::Packet& packet) {
if (packet.hasRemaining(4)) {
/*uint32_t msgType =*/ packet.readUInt32();
std::string msg = packet.readString();
if (!msg.empty()) addSystemChatMessage("[Announcement] " + msg);
}
};
table[Opcode::SMSG_AREA_TRIGGER_MESSAGE] = [this](network::Packet& packet) {
if (packet.hasRemaining(4)) {
/*uint32_t len =*/ packet.readUInt32();
std::string msg = packet.readString();
if (!msg.empty()) {
owner_.addUIError(msg);
addSystemChatMessage(msg);
owner_.areaTriggerMsgs_.push_back(msg);
}
}
};
table[Opcode::SMSG_CHANNEL_LIST] = [this](network::Packet& p) { handleChannelList(p); };
}
void ChatHandler::sendChatMessage(ChatType type, const std::string& message, const std::string& target) {
if (owner_.getState() != WorldState::IN_WORLD) {
LOG_WARNING("Cannot send chat in state: ", static_cast<int>(owner_.getState()));
return;
}
if (message.empty()) {
LOG_WARNING("Cannot send empty chat message");
return;
}
LOG_INFO("Sending chat message: [", getChatTypeString(type), "] ", message);
ChatLanguage language = ChatLanguage::COMMON;
auto packet = MessageChatPacket::build(type, language, message, target);
owner_.socket->send(packet);
// Add local echo so the player sees their own message immediately
MessageChatData echo;
echo.senderGuid = owner_.playerGuid;
echo.language = language;
echo.message = message;
auto nameIt = owner_.playerNameCache.find(owner_.playerGuid);
if (nameIt != owner_.playerNameCache.end()) {
echo.senderName = nameIt->second;
}
if (type == ChatType::WHISPER) {
echo.type = ChatType::WHISPER_INFORM;
echo.senderName = target;
} else {
echo.type = type;
}
if (type == ChatType::CHANNEL) {
echo.channelName = target;
}
addLocalChatMessage(echo);
}
void ChatHandler::handleMessageChat(network::Packet& packet) {
LOG_DEBUG("Handling SMSG_MESSAGECHAT");
MessageChatData data;
if (!owner_.packetParsers_->parseMessageChat(packet, data)) {
LOG_WARNING("Failed to parse SMSG_MESSAGECHAT");
return;
}
// Skip server echo of our own messages (we already added a local echo)
if (data.senderGuid == owner_.playerGuid && data.senderGuid != 0) {
if (data.type == ChatType::WHISPER && !data.senderName.empty()) {
owner_.lastWhisperSender_ = data.senderName;
}
return;
}
// Resolve sender name from entity/cache if not already set by parser
if (data.senderName.empty() && data.senderGuid != 0) {
auto nameIt = owner_.playerNameCache.find(data.senderGuid);
if (nameIt != owner_.playerNameCache.end()) {
data.senderName = nameIt->second;
} else {
auto entity = owner_.entityManager.getEntity(data.senderGuid);
if (entity) {
if (entity->getType() == ObjectType::PLAYER) {
auto player = std::dynamic_pointer_cast<Player>(entity);
if (player && !player->getName().empty()) {
data.senderName = player->getName();
}
} else if (entity->getType() == ObjectType::UNIT) {
auto unit = std::dynamic_pointer_cast<Unit>(entity);
if (unit && !unit->getName().empty()) {
data.senderName = unit->getName();
}
}
}
}
if (data.senderName.empty()) {
owner_.queryPlayerName(data.senderGuid);
}
}
// Add to chat history
chatHistory_.push_back(data);
if (chatHistory_.size() > maxChatHistory_) {
chatHistory_.erase(chatHistory_.begin());
}
// Track whisper sender for /r command
if (data.type == ChatType::WHISPER && !data.senderName.empty()) {
owner_.lastWhisperSender_ = data.senderName;
if (owner_.afkStatus_ && !data.senderName.empty()) {
std::string reply = owner_.afkMessage_.empty() ? "Away from Keyboard" : owner_.afkMessage_;
sendChatMessage(ChatType::WHISPER, "<AFK> " + reply, data.senderName);
} else if (owner_.dndStatus_ && !data.senderName.empty()) {
std::string reply = owner_.dndMessage_.empty() ? "Do Not Disturb" : owner_.dndMessage_;
sendChatMessage(ChatType::WHISPER, "<DND> " + reply, data.senderName);
}
}
// Trigger chat bubble for SAY/YELL messages from others
if (owner_.chatBubbleCallback_ && data.senderGuid != 0) {
if (data.type == ChatType::SAY || data.type == ChatType::YELL ||
data.type == ChatType::MONSTER_SAY || data.type == ChatType::MONSTER_YELL ||
data.type == ChatType::MONSTER_PARTY) {
bool isYell = (data.type == ChatType::YELL || data.type == ChatType::MONSTER_YELL);
owner_.chatBubbleCallback_(data.senderGuid, data.message, isYell);
}
}
// Log the message
std::string senderInfo;
if (!data.senderName.empty()) {
senderInfo = data.senderName;
} else if (data.senderGuid != 0) {
senderInfo = "Unknown-" + std::to_string(data.senderGuid);
} else {
senderInfo = "System";
}
std::string channelInfo;
if (!data.channelName.empty()) {
channelInfo = "[" + data.channelName + "] ";
}
LOG_DEBUG("[", getChatTypeString(data.type), "] ", channelInfo, senderInfo, ": ", data.message);
// Detect addon messages
if (owner_.addonEventCallback_ &&
data.type != ChatType::SAY && data.type != ChatType::YELL &&
data.type != ChatType::EMOTE && data.type != ChatType::TEXT_EMOTE &&
data.type != ChatType::MONSTER_SAY && data.type != ChatType::MONSTER_YELL) {
auto tabPos = data.message.find('\t');
if (tabPos != std::string::npos && tabPos > 0 && tabPos <= 16 &&
tabPos < data.message.size() - 1) {
std::string prefix = data.message.substr(0, tabPos);
if (prefix.find(' ') == std::string::npos) {
std::string body = data.message.substr(tabPos + 1);
std::string channel = getChatTypeString(data.type);
owner_.addonEventCallback_("CHAT_MSG_ADDON", {prefix, body, channel, data.senderName});
return;
}
}
}
// Fire CHAT_MSG_* addon events
if (owner_.addonChatCallback_) owner_.addonChatCallback_(data);
if (owner_.addonEventCallback_) {
std::string eventName = "CHAT_MSG_";
eventName += getChatTypeString(data.type);
std::string lang = std::to_string(static_cast<int>(data.language));
char guidBuf[32];
snprintf(guidBuf, sizeof(guidBuf), "0x%016llX", (unsigned long long)data.senderGuid);
owner_.addonEventCallback_(eventName, {
data.message,
data.senderName,
lang,
data.channelName,
senderInfo,
"",
"0",
"0",
"",
"0",
"0",
guidBuf
});
}
}
void ChatHandler::sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = TextEmotePacket::build(textEmoteId, targetGuid);
owner_.socket->send(packet);
}
void ChatHandler::handleTextEmote(network::Packet& packet) {
const bool legacyFormat = isClassicLikeExpansion() || isActiveExpansion("tbc");
TextEmoteData data;
if (!TextEmoteParser::parse(packet, data, legacyFormat)) {
LOG_WARNING("Failed to parse SMSG_TEXT_EMOTE");
return;
}
if (data.senderGuid == owner_.playerGuid && data.senderGuid != 0) {
return;
}
std::string senderName;
auto nameIt = owner_.playerNameCache.find(data.senderGuid);
if (nameIt != owner_.playerNameCache.end()) {
senderName = nameIt->second;
} else {
auto entity = owner_.entityManager.getEntity(data.senderGuid);
if (entity) {
auto unit = std::dynamic_pointer_cast<Unit>(entity);
if (unit) senderName = unit->getName();
}
}
if (senderName.empty()) {
senderName = "Unknown";
owner_.queryPlayerName(data.senderGuid);
}
const std::string* targetPtr = data.targetName.empty() ? nullptr : &data.targetName;
std::string emoteText = rendering::Renderer::getEmoteTextByDbcId(data.textEmoteId, senderName, targetPtr);
if (emoteText.empty()) {
emoteText = data.targetName.empty()
? senderName + " performs an emote."
: senderName + " performs an emote at " + data.targetName + ".";
}
MessageChatData chatMsg;
chatMsg.type = ChatType::TEXT_EMOTE;
chatMsg.language = ChatLanguage::COMMON;
chatMsg.senderGuid = data.senderGuid;
chatMsg.senderName = senderName;
chatMsg.message = emoteText;
addLocalChatMessage(chatMsg);
uint32_t animId = rendering::Renderer::getEmoteAnimByDbcId(data.textEmoteId);
if (animId != 0 && owner_.emoteAnimCallback_) {
owner_.emoteAnimCallback_(data.senderGuid, animId);
}
LOG_INFO("TEXT_EMOTE from ", senderName, " (emoteId=", data.textEmoteId, ", anim=", animId, ")");
}
void ChatHandler::joinChannel(const std::string& channelName, const std::string& password) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = owner_.packetParsers_
? owner_.packetParsers_->buildJoinChannel(channelName, password)
: JoinChannelPacket::build(channelName, password);
owner_.socket->send(packet);
LOG_INFO("Requesting to join channel: ", channelName);
}
void ChatHandler::leaveChannel(const std::string& channelName) {
if (owner_.getState() != WorldState::IN_WORLD || !owner_.socket) return;
auto packet = owner_.packetParsers_
? owner_.packetParsers_->buildLeaveChannel(channelName)
: LeaveChannelPacket::build(channelName);
owner_.socket->send(packet);
LOG_INFO("Requesting to leave channel: ", channelName);
}
std::string ChatHandler::getChannelByIndex(int index) const {
if (index < 1 || index > static_cast<int>(joinedChannels_.size())) return "";
return joinedChannels_[index - 1];
}
int ChatHandler::getChannelIndex(const std::string& channelName) const {
for (int i = 0; i < static_cast<int>(joinedChannels_.size()); ++i) {
if (joinedChannels_[i] == channelName) return i + 1;
}
return 0;
}
void ChatHandler::handleChannelNotify(network::Packet& packet) {
ChannelNotifyData data;
if (!ChannelNotifyParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_CHANNEL_NOTIFY");
return;
}
switch (data.notifyType) {
case ChannelNotifyType::YOU_JOINED: {
bool found = false;
for (const auto& ch : joinedChannels_) {
if (ch == data.channelName) { found = true; break; }
}
if (!found) {
joinedChannels_.push_back(data.channelName);
}
MessageChatData msg;
msg.type = ChatType::SYSTEM;
msg.message = "Joined channel: " + data.channelName;
addLocalChatMessage(msg);
LOG_INFO("Joined channel: ", data.channelName);
break;
}
case ChannelNotifyType::YOU_LEFT: {
joinedChannels_.erase(
std::remove(joinedChannels_.begin(), joinedChannels_.end(), data.channelName),
joinedChannels_.end());
MessageChatData msg;
msg.type = ChatType::SYSTEM;
msg.message = "Left channel: " + data.channelName;
addLocalChatMessage(msg);
LOG_INFO("Left channel: ", data.channelName);
break;
}
case ChannelNotifyType::PLAYER_ALREADY_MEMBER: {
bool found = false;
for (const auto& ch : joinedChannels_) {
if (ch == data.channelName) { found = true; break; }
}
if (!found) {
joinedChannels_.push_back(data.channelName);
LOG_INFO("Already in channel: ", data.channelName);
}
break;
}
case ChannelNotifyType::NOT_IN_AREA:
addSystemChatMessage("You must be in the area to join '" + data.channelName + "'.");
LOG_DEBUG("Cannot join channel ", data.channelName, " (not in area)");
break;
case ChannelNotifyType::WRONG_PASSWORD:
addSystemChatMessage("Wrong password for channel '" + data.channelName + "'.");
break;
case ChannelNotifyType::NOT_MEMBER:
addSystemChatMessage("You are not in channel '" + data.channelName + "'.");
break;
case ChannelNotifyType::NOT_MODERATOR:
addSystemChatMessage("You are not a moderator of '" + data.channelName + "'.");
break;
case ChannelNotifyType::MUTED:
addSystemChatMessage("You are muted in channel '" + data.channelName + "'.");
break;
case ChannelNotifyType::BANNED:
addSystemChatMessage("You are banned from channel '" + data.channelName + "'.");
break;
case ChannelNotifyType::THROTTLED:
addSystemChatMessage("Channel '" + data.channelName + "' is throttled. Please wait.");
break;
case ChannelNotifyType::NOT_IN_LFG:
addSystemChatMessage("You must be in a LFG queue to join '" + data.channelName + "'.");
break;
case ChannelNotifyType::PLAYER_KICKED:
addSystemChatMessage("A player was kicked from '" + data.channelName + "'.");
break;
case ChannelNotifyType::PASSWORD_CHANGED:
addSystemChatMessage("Password for '" + data.channelName + "' changed.");
break;
case ChannelNotifyType::OWNER_CHANGED:
addSystemChatMessage("Owner of '" + data.channelName + "' changed.");
break;
case ChannelNotifyType::NOT_OWNER:
addSystemChatMessage("You are not the owner of '" + data.channelName + "'.");
break;
case ChannelNotifyType::INVALID_NAME:
addSystemChatMessage("Invalid channel name '" + data.channelName + "'.");
break;
case ChannelNotifyType::PLAYER_NOT_FOUND:
addSystemChatMessage("Player not found.");
break;
case ChannelNotifyType::ANNOUNCEMENTS_ON:
addSystemChatMessage("Channel '" + data.channelName + "': announcements enabled.");
break;
case ChannelNotifyType::ANNOUNCEMENTS_OFF:
addSystemChatMessage("Channel '" + data.channelName + "': announcements disabled.");
break;
case ChannelNotifyType::MODERATION_ON:
addSystemChatMessage("Channel '" + data.channelName + "' is now moderated.");
break;
case ChannelNotifyType::MODERATION_OFF:
addSystemChatMessage("Channel '" + data.channelName + "' is no longer moderated.");
break;
case ChannelNotifyType::PLAYER_BANNED:
addSystemChatMessage("A player was banned from '" + data.channelName + "'.");
break;
case ChannelNotifyType::PLAYER_UNBANNED:
addSystemChatMessage("A player was unbanned from '" + data.channelName + "'.");
break;
case ChannelNotifyType::PLAYER_NOT_BANNED:
addSystemChatMessage("That player is not banned from '" + data.channelName + "'.");
break;
case ChannelNotifyType::INVITE:
addSystemChatMessage("You have been invited to join channel '" + data.channelName + "'.");
break;
case ChannelNotifyType::INVITE_WRONG_FACTION:
case ChannelNotifyType::WRONG_FACTION:
addSystemChatMessage("Wrong faction for channel '" + data.channelName + "'.");
break;
case ChannelNotifyType::NOT_MODERATED:
addSystemChatMessage("Channel '" + data.channelName + "' is not moderated.");
break;
case ChannelNotifyType::PLAYER_INVITED:
addSystemChatMessage("Player invited to channel '" + data.channelName + "'.");
break;
case ChannelNotifyType::PLAYER_INVITE_BANNED:
addSystemChatMessage("That player is banned from '" + data.channelName + "'.");
break;
default:
LOG_DEBUG("Channel notify type ", static_cast<int>(data.notifyType),
" for channel ", data.channelName);
break;
}
}
void ChatHandler::autoJoinDefaultChannels() {
LOG_INFO("autoJoinDefaultChannels: general=", chatAutoJoin.general,
" trade=", chatAutoJoin.trade, " localDefense=", chatAutoJoin.localDefense,
" lfg=", chatAutoJoin.lfg, " local=", chatAutoJoin.local);
if (chatAutoJoin.general) joinChannel("General");
if (chatAutoJoin.trade) joinChannel("Trade");
if (chatAutoJoin.localDefense) joinChannel("LocalDefense");
if (chatAutoJoin.lfg) joinChannel("LookingForGroup");
if (chatAutoJoin.local) joinChannel("Local");
}
void ChatHandler::addLocalChatMessage(const MessageChatData& msg) {
chatHistory_.push_back(msg);
if (chatHistory_.size() > maxChatHistory_) {
chatHistory_.pop_front();
}
if (owner_.addonChatCallback_) owner_.addonChatCallback_(msg);
if (owner_.addonEventCallback_) {
std::string eventName = "CHAT_MSG_";
eventName += getChatTypeString(msg.type);
const Character* ac = owner_.getActiveCharacter();
std::string senderName = msg.senderName.empty()
? (ac ? ac->name : std::string{}) : msg.senderName;
char guidBuf[32];
snprintf(guidBuf, sizeof(guidBuf), "0x%016llX",
(unsigned long long)(msg.senderGuid != 0 ? msg.senderGuid : owner_.playerGuid));
owner_.addonEventCallback_(eventName, {
msg.message, senderName,
std::to_string(static_cast<int>(msg.language)),
msg.channelName, senderName, "", "0", "0", "", "0", "0", guidBuf
});
}
}
void ChatHandler::addSystemChatMessage(const std::string& message) {
if (message.empty()) return;
MessageChatData msg;
msg.type = ChatType::SYSTEM;
msg.language = ChatLanguage::UNIVERSAL;
msg.message = message;
addLocalChatMessage(msg);
}
void ChatHandler::toggleAfk(const std::string& message) {
owner_.afkStatus_ = !owner_.afkStatus_;
owner_.afkMessage_ = message;
if (owner_.afkStatus_) {
if (message.empty()) {
addSystemChatMessage("You are now AFK.");
} else {
addSystemChatMessage("You are now AFK: " + message);
}
// If DND was active, turn it off
if (owner_.dndStatus_) {
owner_.dndStatus_ = false;
owner_.dndMessage_.clear();
}
} else {
addSystemChatMessage("You are no longer AFK.");
owner_.afkMessage_.clear();
}
LOG_INFO("AFK status: ", owner_.afkStatus_, ", message: ", message);
}
void ChatHandler::toggleDnd(const std::string& message) {
owner_.dndStatus_ = !owner_.dndStatus_;
owner_.dndMessage_ = message;
if (owner_.dndStatus_) {
if (message.empty()) {
addSystemChatMessage("You are now DND (Do Not Disturb).");
} else {
addSystemChatMessage("You are now DND: " + message);
}
// If AFK was active, turn it off
if (owner_.afkStatus_) {
owner_.afkStatus_ = false;
owner_.afkMessage_.clear();
}
} else {
addSystemChatMessage("You are no longer DND.");
owner_.dndMessage_.clear();
}
LOG_INFO("DND status: ", owner_.dndStatus_, ", message: ", message);
}
void ChatHandler::replyToLastWhisper(const std::string& message) {
if (!owner_.isInWorld()) {
LOG_WARNING("Cannot send whisper: not in world or not connected");
return;
}
if (owner_.lastWhisperSender_.empty()) {
addSystemChatMessage("No one has whispered you yet.");
return;
}
if (message.empty()) {
addSystemChatMessage("You must specify a message to send.");
return;
}
// Send whisper using the standard message chat function
sendChatMessage(ChatType::WHISPER, message, owner_.lastWhisperSender_);
LOG_INFO("Replied to ", owner_.lastWhisperSender_, ": ", message);
}
// ============================================================
// Moved opcode handlers (from GameHandler::registerOpcodeHandlers)
// ============================================================
void ChatHandler::handleChannelList(network::Packet& packet) {
std::string chanName = packet.readString();
if (!packet.hasRemaining(5)) return;
/*uint8_t chanFlags =*/ packet.readUInt8();
uint32_t memberCount = packet.readUInt32();
memberCount = std::min(memberCount, 200u);
addSystemChatMessage(chanName + " has " + std::to_string(memberCount) + " member(s):");
for (uint32_t i = 0; i < memberCount; ++i) {
if (!packet.hasRemaining(9)) break;
uint64_t memberGuid = packet.readUInt64();
uint8_t memberFlags = packet.readUInt8();
std::string name;
auto entity = owner_.entityManager.getEntity(memberGuid);
if (entity) {
auto player = std::dynamic_pointer_cast<Player>(entity);
if (player && !player->getName().empty()) name = player->getName();
}
if (name.empty()) name = owner_.lookupName(memberGuid);
if (name.empty()) name = "(unknown)";
std::string entry = " " + name;
if (memberFlags & 0x01) entry += " [Moderator]";
if (memberFlags & 0x02) entry += " [Muted]";
addSystemChatMessage(entry);
}
}
// ============================================================
// Methods moved from GameHandler
// ============================================================
void ChatHandler::submitGmTicket(const std::string& text) {
if (!owner_.isInWorld()) return;
// CMSG_GMTICKET_CREATE (WotLK 3.3.5a):
// string ticket_text
// float[3] position (server coords)
// float facing
// uint32 mapId
// uint8 need_response (1 = yes)
network::Packet pkt(wireOpcode(Opcode::CMSG_GMTICKET_CREATE));
pkt.writeString(text);
pkt.writeFloat(owner_.movementInfo.x);
pkt.writeFloat(owner_.movementInfo.y);
pkt.writeFloat(owner_.movementInfo.z);
pkt.writeFloat(owner_.movementInfo.orientation);
pkt.writeUInt32(owner_.currentMapId_);
pkt.writeUInt8(1); // need_response = yes
owner_.socket->send(pkt);
LOG_INFO("Submitted GM ticket: '", text, "'");
}
void ChatHandler::handleMotd(network::Packet& packet) {
LOG_INFO("Handling SMSG_MOTD");
MotdData data;
if (!MotdParser::parse(packet, data)) {
LOG_WARNING("Failed to parse SMSG_MOTD");
return;
}
if (!data.isEmpty()) {
LOG_INFO("========================================");
LOG_INFO(" MESSAGE OF THE DAY");
LOG_INFO("========================================");
for (const auto& line : data.lines) {
LOG_INFO(line);
addSystemChatMessage(std::string("MOTD: ") + line);
}
// Add a visual separator after MOTD block so subsequent messages don't
// appear glued to the last MOTD line.
MessageChatData spacer;
spacer.type = ChatType::SYSTEM;
spacer.language = ChatLanguage::UNIVERSAL;
spacer.message = "";
addLocalChatMessage(spacer);
LOG_INFO("========================================");
}
}
void ChatHandler::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);
}
}
} // namespace game
} // namespace wowee

1532
src/game/combat_handler.cpp Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

1892
src/game/quest_handler.cpp Normal file

File diff suppressed because it is too large Load diff

2714
src/game/social_handler.cpp Normal file

File diff suppressed because it is too large Load diff

3236
src/game/spell_handler.cpp Normal file

File diff suppressed because it is too large Load diff

1369
src/game/warden_handler.cpp Normal file

File diff suppressed because it is too large Load diff