mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-03 20:03:50 +00:00
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:
parent
3762dceaa6
commit
b2710258dc
21 changed files with 20771 additions and 17858 deletions
|
|
@ -450,6 +450,14 @@ set(WOWEE_SOURCES
|
||||||
src/game/opcode_table.cpp
|
src/game/opcode_table.cpp
|
||||||
src/game/update_field_table.cpp
|
src/game/update_field_table.cpp
|
||||||
src/game/game_handler.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_crypto.cpp
|
||||||
src/game/warden_module.cpp
|
src/game/warden_module.cpp
|
||||||
src/game/warden_emulator.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"
|
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
|
# 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
|
# 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.
|
# SDL_Vulkan_LoadLibrary can locate it without needing a full system PATH search.
|
||||||
|
|
|
||||||
73
include/game/chat_handler.hpp
Normal file
73
include/game/chat_handler.hpp
Normal 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
|
||||||
191
include/game/combat_handler.hpp
Normal file
191
include/game/combat_handler.hpp
Normal 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
|
||||||
|
|
@ -7,6 +7,10 @@
|
||||||
#include "game/inventory.hpp"
|
#include "game/inventory.hpp"
|
||||||
#include "game/spell_defines.hpp"
|
#include "game/spell_defines.hpp"
|
||||||
#include "game/group_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 "network/packet.hpp"
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
@ -31,6 +35,11 @@ namespace wowee::game {
|
||||||
class WardenModule;
|
class WardenModule;
|
||||||
class WardenModuleManager;
|
class WardenModuleManager;
|
||||||
class PacketParsers;
|
class PacketParsers;
|
||||||
|
class ChatHandler;
|
||||||
|
class MovementHandler;
|
||||||
|
class InventoryHandler;
|
||||||
|
class SocialHandler;
|
||||||
|
class WardenHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -115,25 +124,9 @@ using WorldConnectFailureCallback = std::function<void(const std::string& reason
|
||||||
*/
|
*/
|
||||||
class GameHandler {
|
class GameHandler {
|
||||||
public:
|
public:
|
||||||
// Talent data structures (must be public for use in templates)
|
// Talent data structures (aliased from handler_types.hpp)
|
||||||
struct TalentEntry {
|
using TalentEntry = game::TalentEntry;
|
||||||
uint32_t talentId = 0;
|
using TalentTabEntry = game::TalentTabEntry;
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
GameHandler();
|
GameHandler();
|
||||||
~GameHandler();
|
~GameHandler();
|
||||||
|
|
@ -262,19 +255,14 @@ public:
|
||||||
void sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid = 0);
|
void sendTextEmote(uint32_t textEmoteId, uint64_t targetGuid = 0);
|
||||||
void joinChannel(const std::string& channelName, const std::string& password = "");
|
void joinChannel(const std::string& channelName, const std::string& password = "");
|
||||||
void leaveChannel(const std::string& channelName);
|
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;
|
std::string getChannelByIndex(int index) const;
|
||||||
int getChannelIndex(const std::string& channelName) const;
|
int getChannelIndex(const std::string& channelName) const;
|
||||||
|
|
||||||
// Chat auto-join settings (set by UI before autoJoinDefaultChannels)
|
// Chat auto-join settings (aliased from handler_types.hpp)
|
||||||
struct ChatAutoJoin {
|
using ChatAutoJoin = game::ChatAutoJoin;
|
||||||
bool general = true;
|
|
||||||
bool trade = true;
|
|
||||||
bool localDefense = true;
|
|
||||||
bool lfg = true;
|
|
||||||
bool local = true;
|
|
||||||
};
|
|
||||||
ChatAutoJoin chatAutoJoin;
|
ChatAutoJoin chatAutoJoin;
|
||||||
|
void autoJoinDefaultChannels();
|
||||||
|
|
||||||
// Chat bubble callback: (senderGuid, message, isYell)
|
// Chat bubble callback: (senderGuid, message, isYell)
|
||||||
using ChatBubbleCallback = std::function<void(uint64_t, const std::string&, bool)>;
|
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)
|
* @param maxMessages Maximum number of messages to return (0 = all)
|
||||||
* @return Vector of chat messages
|
* @return Vector of chat messages
|
||||||
*/
|
*/
|
||||||
const std::deque<MessageChatData>& getChatHistory() const { return chatHistory; }
|
const std::deque<MessageChatData>& getChatHistory() const;
|
||||||
void clearChatHistory() { chatHistory.clear(); }
|
void clearChatHistory();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a locally-generated chat message (e.g., emote feedback)
|
* Add a locally-generated chat message (e.g., emote feedback)
|
||||||
|
|
@ -430,27 +418,8 @@ public:
|
||||||
// Inspection
|
// Inspection
|
||||||
void inspectTarget();
|
void inspectTarget();
|
||||||
|
|
||||||
struct InspectArenaTeam {
|
using InspectArenaTeam = game::InspectArenaTeam;
|
||||||
uint32_t teamId = 0;
|
using InspectResult = game::InspectResult;
|
||||||
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)
|
|
||||||
};
|
|
||||||
const InspectResult* getInspectResult() const {
|
const InspectResult* getInspectResult() const {
|
||||||
return inspectResult_.guid ? &inspectResult_ : nullptr;
|
return inspectResult_.guid ? &inspectResult_ : nullptr;
|
||||||
}
|
}
|
||||||
|
|
@ -462,15 +431,7 @@ public:
|
||||||
uint32_t getTotalTimePlayed() const { return totalTimePlayed_; }
|
uint32_t getTotalTimePlayed() const { return totalTimePlayed_; }
|
||||||
uint32_t getLevelTimePlayed() const { return levelTimePlayed_; }
|
uint32_t getLevelTimePlayed() const { return levelTimePlayed_; }
|
||||||
|
|
||||||
// Who results (structured, from last SMSG_WHO response)
|
using WhoEntry = game::WhoEntry;
|
||||||
struct WhoEntry {
|
|
||||||
std::string name;
|
|
||||||
std::string guildName;
|
|
||||||
uint32_t level = 0;
|
|
||||||
uint32_t classId = 0;
|
|
||||||
uint32_t raceId = 0;
|
|
||||||
uint32_t zoneId = 0;
|
|
||||||
};
|
|
||||||
const std::vector<WhoEntry>& getWhoResults() const { return whoResults_; }
|
const std::vector<WhoEntry>& getWhoResults() const { return whoResults_; }
|
||||||
uint32_t getWhoOnlineCount() const { return whoOnlineCount_; }
|
uint32_t getWhoOnlineCount() const { return whoOnlineCount_; }
|
||||||
std::string getWhoAreaName(uint32_t zoneId) const { return getAreaName(zoneId); }
|
std::string getWhoAreaName(uint32_t zoneId) const { return getAreaName(zoneId); }
|
||||||
|
|
@ -486,28 +447,11 @@ public:
|
||||||
// Random roll
|
// Random roll
|
||||||
void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100);
|
void randomRoll(uint32_t minRoll = 1, uint32_t maxRoll = 100);
|
||||||
|
|
||||||
// Battleground queue slot (public so UI can read invite details)
|
// Battleground queue slot (aliased from handler_types.hpp)
|
||||||
struct BgQueueSlot {
|
using BgQueueSlot = game::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
|
|
||||||
};
|
|
||||||
|
|
||||||
// Available BG list (populated by SMSG_BATTLEFIELD_LIST)
|
// Available BG list (aliased from handler_types.hpp)
|
||||||
struct AvailableBgInfo {
|
using AvailableBgInfo = game::AvailableBgInfo;
|
||||||
uint32_t bgTypeId = 0;
|
|
||||||
bool isRegistered = false;
|
|
||||||
bool isHoliday = false;
|
|
||||||
uint32_t minLevel = 0;
|
|
||||||
uint32_t maxLevel = 0;
|
|
||||||
std::vector<uint32_t> instanceIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Battleground
|
// Battleground
|
||||||
bool hasPendingBgInvite() const;
|
bool hasPendingBgInvite() const;
|
||||||
|
|
@ -516,42 +460,17 @@ public:
|
||||||
const std::array<BgQueueSlot, 3>& getBgQueues() const { return bgQueues_; }
|
const std::array<BgQueueSlot, 3>& getBgQueues() const { return bgQueues_; }
|
||||||
const std::vector<AvailableBgInfo>& getAvailableBgs() const { return availableBgs_; }
|
const std::vector<AvailableBgInfo>& getAvailableBgs() const { return availableBgs_; }
|
||||||
|
|
||||||
// BG scoreboard (MSG_PVP_LOG_DATA)
|
// BG scoreboard (aliased from handler_types.hpp)
|
||||||
struct BgPlayerScore {
|
using BgPlayerScore = game::BgPlayerScore;
|
||||||
uint64_t guid = 0;
|
using ArenaTeamScore = game::ArenaTeamScore;
|
||||||
std::string name;
|
using BgScoreboardData = game::BgScoreboardData;
|
||||||
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
|
|
||||||
};
|
|
||||||
void requestPvpLog();
|
void requestPvpLog();
|
||||||
const BgScoreboardData* getBgScoreboard() const {
|
const BgScoreboardData* getBgScoreboard() const {
|
||||||
return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_;
|
return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_;
|
||||||
}
|
}
|
||||||
|
|
||||||
// BG flag carrier / important player positions (MSG_BATTLEGROUND_PLAYER_POSITIONS)
|
// BG flag carrier positions (aliased from handler_types.hpp)
|
||||||
struct BgPlayerPosition {
|
using BgPlayerPosition = game::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
|
|
||||||
};
|
|
||||||
const std::vector<BgPlayerPosition>& getBgPlayerPositions() const { return bgPlayerPositions_; }
|
const std::vector<BgPlayerPosition>& getBgPlayerPositions() const { return bgPlayerPositions_; }
|
||||||
|
|
||||||
// Network latency (milliseconds, updated each PONG response)
|
// Network latency (milliseconds, updated each PONG response)
|
||||||
|
|
@ -656,19 +575,8 @@ public:
|
||||||
uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; }
|
uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; }
|
||||||
|
|
||||||
// Petition signatures (guild charter signing flow)
|
// Petition signatures (guild charter signing flow)
|
||||||
struct PetitionSignature {
|
using PetitionSignature = game::PetitionSignature;
|
||||||
uint64_t playerGuid = 0;
|
using PetitionInfo = game::PetitionInfo;
|
||||||
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;
|
|
||||||
};
|
|
||||||
const PetitionInfo& getPetitionInfo() const { return petitionInfo_; }
|
const PetitionInfo& getPetitionInfo() const { return petitionInfo_; }
|
||||||
bool hasPetitionSignaturesUI() const { return petitionInfo_.showUI; }
|
bool hasPetitionSignaturesUI() const { return petitionInfo_.showUI; }
|
||||||
void clearPetitionSignaturesUI() { petitionInfo_.showUI = false; }
|
void clearPetitionSignaturesUI() { petitionInfo_.showUI = false; }
|
||||||
|
|
@ -682,11 +590,7 @@ public:
|
||||||
// Returns the guildId for a player entity (from PLAYER_GUILDID update field).
|
// Returns the guildId for a player entity (from PLAYER_GUILDID update field).
|
||||||
uint32_t getEntityGuildId(uint64_t guid) const;
|
uint32_t getEntityGuildId(uint64_t guid) const;
|
||||||
|
|
||||||
// Ready check
|
using ReadyCheckResult = game::ReadyCheckResult;
|
||||||
struct ReadyCheckResult {
|
|
||||||
std::string name;
|
|
||||||
bool ready = false;
|
|
||||||
};
|
|
||||||
void initiateReadyCheck();
|
void initiateReadyCheck();
|
||||||
void respondToReadyCheck(bool ready);
|
void respondToReadyCheck(bool ready);
|
||||||
bool hasPendingReadyCheck() const { return pendingReadyCheck_; }
|
bool hasPendingReadyCheck() const { return pendingReadyCheck_; }
|
||||||
|
|
@ -720,6 +624,8 @@ public:
|
||||||
void initiateTrade(uint64_t targetGuid);
|
void initiateTrade(uint64_t targetGuid);
|
||||||
void reportPlayer(uint64_t targetGuid, const std::string& reason);
|
void reportPlayer(uint64_t targetGuid, const std::string& reason);
|
||||||
void stopCasting();
|
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 ----
|
// ---- Phase 1: Name queries ----
|
||||||
void queryPlayerName(uint64_t guid);
|
void queryPlayerName(uint64_t guid);
|
||||||
|
|
@ -753,28 +659,26 @@ public:
|
||||||
return (it != creatureInfoCache.end()) ? it->second.family : 0;
|
return (it != creatureInfoCache.end()) ? it->second.family : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Phase 2: Combat ----
|
// ---- Phase 2: Combat (delegated to CombatHandler) ----
|
||||||
void startAutoAttack(uint64_t targetGuid);
|
void startAutoAttack(uint64_t targetGuid);
|
||||||
void stopAutoAttack();
|
void stopAutoAttack();
|
||||||
bool isAutoAttacking() const { return autoAttacking; }
|
bool isAutoAttacking() const;
|
||||||
bool hasAutoAttackIntent() const { return autoAttackRequested_; }
|
bool hasAutoAttackIntent() const;
|
||||||
bool isInCombat() const { return autoAttacking || !hostileAttackers_.empty(); }
|
bool isInCombat() const;
|
||||||
bool isInCombatWith(uint64_t guid) const {
|
bool isInCombatWith(uint64_t guid) const;
|
||||||
return guid != 0 &&
|
uint64_t getAutoAttackTargetGuid() const;
|
||||||
((autoAttacking && autoAttackTarget == guid) ||
|
bool isAggressiveTowardPlayer(uint64_t guid) const;
|
||||||
(hostileAttackers_.count(guid) > 0));
|
|
||||||
}
|
|
||||||
uint64_t getAutoAttackTargetGuid() const { return autoAttackTarget; }
|
|
||||||
bool isAggressiveTowardPlayer(uint64_t guid) const { return hostileAttackers_.count(guid) > 0; }
|
|
||||||
// Timestamp (ms since epoch) of the most recent player melee auto-attack.
|
// Timestamp (ms since epoch) of the most recent player melee auto-attack.
|
||||||
// Zero if no swing has occurred this session.
|
// Zero if no swing has occurred this session.
|
||||||
uint64_t getLastMeleeSwingMs() const { return lastMeleeSwingMs_; }
|
uint64_t getLastMeleeSwingMs() const;
|
||||||
const std::vector<CombatTextEntry>& getCombatText() const { return combatText; }
|
const std::vector<CombatTextEntry>& getCombatText() const;
|
||||||
|
void clearCombatText();
|
||||||
void updateCombatText(float deltaTime);
|
void updateCombatText(float deltaTime);
|
||||||
|
void clearHostileAttackers();
|
||||||
|
|
||||||
// Combat log (persistent rolling history, max MAX_COMBAT_LOG entries)
|
// Combat log (persistent rolling history, max MAX_COMBAT_LOG entries)
|
||||||
const std::deque<CombatLogEntry>& getCombatLog() const { return combatLog_; }
|
const std::deque<CombatLogEntry>& getCombatLog() const;
|
||||||
void clearCombatLog() { combatLog_.clear(); }
|
void clearCombatLog();
|
||||||
|
|
||||||
// Area trigger messages (SMSG_AREA_TRIGGER_MESSAGE) — drained by UI each frame
|
// Area trigger messages (SMSG_AREA_TRIGGER_MESSAGE) — drained by UI each frame
|
||||||
bool hasAreaTriggerMsg() const { return !areaTriggerMsgs_.empty(); }
|
bool hasAreaTriggerMsg() const { return !areaTriggerMsgs_.empty(); }
|
||||||
|
|
@ -786,19 +690,9 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Threat
|
// Threat
|
||||||
struct ThreatEntry {
|
using ThreatEntry = CombatHandler::ThreatEntry;
|
||||||
uint64_t victimGuid = 0;
|
const std::vector<ThreatEntry>* getThreatList(uint64_t unitGuid) const;
|
||||||
uint32_t threat = 0;
|
const std::vector<ThreatEntry>* getTargetThreatList() const;
|
||||||
};
|
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Phase 3: Spells ----
|
// ---- Phase 3: Spells ----
|
||||||
void castSpell(uint32_t spellId, uint64_t targetGuid = 0);
|
void castSpell(uint32_t spellId, uint64_t targetGuid = 0);
|
||||||
|
|
@ -833,14 +727,13 @@ public:
|
||||||
void sendPetAction(uint32_t action, uint64_t targetGuid = 0);
|
void sendPetAction(uint32_t action, uint64_t targetGuid = 0);
|
||||||
// Toggle autocast for a pet spell via CMSG_PET_SPELL_AUTOCAST
|
// Toggle autocast for a pet spell via CMSG_PET_SPELL_AUTOCAST
|
||||||
void togglePetSpellAutocast(uint32_t spellId);
|
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
|
// Spell book tabs — groups known spells by class skill line for Lua API
|
||||||
struct SpellBookTab {
|
using SpellBookTab = SpellHandler::SpellBookTab;
|
||||||
std::string name;
|
|
||||||
std::string texture; // icon path
|
|
||||||
std::vector<uint32_t> spellIds; // spells in this tab
|
|
||||||
};
|
|
||||||
const std::vector<SpellBookTab>& getSpellBookTabs();
|
const std::vector<SpellBookTab>& getSpellBookTabs();
|
||||||
|
|
||||||
// ---- Pet Stable ----
|
// ---- Pet Stable ----
|
||||||
|
|
@ -887,15 +780,15 @@ public:
|
||||||
minimapPings_.end());
|
minimapPings_.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isCasting() const { return casting; }
|
bool isCasting() const { return spellHandler_ ? spellHandler_->isCasting() : false; }
|
||||||
bool isChanneling() const { return casting && castIsChannel; }
|
bool isChanneling() const { return spellHandler_ ? spellHandler_->isChanneling() : false; }
|
||||||
bool isGameObjectInteractionCasting() const {
|
bool isGameObjectInteractionCasting() const {
|
||||||
return casting && currentCastSpellId == 0 && pendingGameObjectInteractGuid_ != 0;
|
return spellHandler_ ? spellHandler_->isGameObjectInteractionCasting() : false;
|
||||||
}
|
}
|
||||||
uint32_t getCurrentCastSpellId() const { return currentCastSpellId; }
|
uint32_t getCurrentCastSpellId() const { return spellHandler_ ? spellHandler_->getCurrentCastSpellId() : 0; }
|
||||||
float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; }
|
float getCastProgress() const { return spellHandler_ ? spellHandler_->getCastProgress() : 0.0f; }
|
||||||
float getCastTimeRemaining() const { return castTimeRemaining; }
|
float getCastTimeRemaining() const { return spellHandler_ ? spellHandler_->getCastTimeRemaining() : 0.0f; }
|
||||||
float getCastTimeTotal() const { return castTimeTotal; }
|
float getCastTimeTotal() const { return spellHandler_ ? spellHandler_->getCastTimeTotal() : 0.0f; }
|
||||||
|
|
||||||
// Repeat-craft queue
|
// Repeat-craft queue
|
||||||
void startCraftQueue(uint32_t spellId, int count);
|
void startCraftQueue(uint32_t spellId, int count);
|
||||||
|
|
@ -907,39 +800,19 @@ public:
|
||||||
uint32_t getQueuedSpellId() const { return queuedSpellId_; }
|
uint32_t getQueuedSpellId() const { return queuedSpellId_; }
|
||||||
void cancelQueuedSpell() { queuedSpellId_ = 0; queuedSpellTarget_ = 0; }
|
void cancelQueuedSpell() { queuedSpellId_ = 0; queuedSpellTarget_ = 0; }
|
||||||
|
|
||||||
// Unit cast state (tracked per GUID for target frame + boss frames)
|
// Unit cast state (aliased from handler_types.hpp)
|
||||||
struct UnitCastState {
|
using UnitCastState = game::UnitCastState;
|
||||||
bool casting = false;
|
// Returns cast state for any unit by GUID (delegates to SpellHandler)
|
||||||
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)
|
|
||||||
const UnitCastState* getUnitCastState(uint64_t guid) const {
|
const UnitCastState* getUnitCastState(uint64_t guid) const {
|
||||||
auto it = unitCastStates_.find(guid);
|
if (spellHandler_) return spellHandler_->getUnitCastState(guid);
|
||||||
return (it != unitCastStates_.end() && it->second.casting) ? &it->second : nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
// Convenience helpers for the current target
|
// Convenience helpers for the current target
|
||||||
bool isTargetCasting() const { return getUnitCastState(targetGuid) != nullptr; }
|
bool isTargetCasting() const { return spellHandler_ ? spellHandler_->isTargetCasting() : false; }
|
||||||
uint32_t getTargetCastSpellId() const {
|
uint32_t getTargetCastSpellId() const { return spellHandler_ ? spellHandler_->getTargetCastSpellId() : 0; }
|
||||||
auto* s = getUnitCastState(targetGuid);
|
float getTargetCastProgress() const { return spellHandler_ ? spellHandler_->getTargetCastProgress() : 0.0f; }
|
||||||
return s ? s->spellId : 0;
|
float getTargetCastTimeRemaining() const { return spellHandler_ ? spellHandler_->getTargetCastTimeRemaining() : 0.0f; }
|
||||||
}
|
bool isTargetCastInterruptible() const { return spellHandler_ ? spellHandler_->isTargetCastInterruptible() : true; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Talents
|
// Talents
|
||||||
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
|
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
|
||||||
|
|
@ -998,13 +871,21 @@ public:
|
||||||
void loadCharacterConfig();
|
void loadCharacterConfig();
|
||||||
static std::string getCharacterConfigDir();
|
static std::string getCharacterConfigDir();
|
||||||
|
|
||||||
// Auras
|
// Auras — delegate to SpellHandler as canonical authority
|
||||||
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
|
const std::vector<AuraSlot>& getPlayerAuras() const {
|
||||||
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
|
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)
|
// Per-unit aura cache (populated for party members and any unit we receive updates for)
|
||||||
const std::vector<AuraSlot>* getUnitAuras(uint64_t guid) const {
|
const std::vector<AuraSlot>* getUnitAuras(uint64_t guid) const {
|
||||||
auto it = unitAurasCache_.find(guid);
|
if (spellHandler_) return spellHandler_->getUnitAuras(guid);
|
||||||
return (it != unitAurasCache_.end()) ? &it->second : nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Completed quests (populated from SMSG_QUERY_QUESTS_COMPLETED_RESPONSE)
|
// Completed quests (populated from SMSG_QUERY_QUESTS_COMPLETED_RESPONSE)
|
||||||
|
|
@ -1257,7 +1138,10 @@ public:
|
||||||
|
|
||||||
// Cooldowns
|
// Cooldowns
|
||||||
float getSpellCooldown(uint32_t spellId) const;
|
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
|
// Player GUID
|
||||||
uint64_t getPlayerGuid() const { return playerGuid; }
|
uint64_t getPlayerGuid() const { return playerGuid; }
|
||||||
|
|
@ -1448,14 +1332,8 @@ public:
|
||||||
return rem > 0.0f ? rem : 0.0f;
|
return rem > 0.0f ? rem : 0.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Instance lockouts ----
|
// Instance lockouts (aliased from handler_types.hpp)
|
||||||
struct InstanceLockout {
|
using InstanceLockout = game::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;
|
|
||||||
};
|
|
||||||
const std::vector<InstanceLockout>& getInstanceLockouts() const { return instanceLockouts_; }
|
const std::vector<InstanceLockout>& getInstanceLockouts() const { return instanceLockouts_; }
|
||||||
|
|
||||||
// Boss encounter unit tracking (SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
// Boss encounter unit tracking (SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
||||||
|
|
@ -1483,16 +1361,8 @@ public:
|
||||||
void setRaidMark(uint64_t guid, uint8_t icon);
|
void setRaidMark(uint64_t guid, uint8_t icon);
|
||||||
|
|
||||||
// ---- LFG / Dungeon Finder ----
|
// ---- LFG / Dungeon Finder ----
|
||||||
enum class LfgState : uint8_t {
|
// LFG state (aliased from handler_types.hpp)
|
||||||
None = 0,
|
using LfgState = game::LfgState;
|
||||||
RoleCheck = 1,
|
|
||||||
Queued = 2,
|
|
||||||
Proposal = 3,
|
|
||||||
Boot = 4,
|
|
||||||
InDungeon = 5,
|
|
||||||
FinishedDungeon= 6,
|
|
||||||
RaidBrowser = 7,
|
|
||||||
};
|
|
||||||
|
|
||||||
// roles bitmask: 0x02=tank, 0x04=healer, 0x08=dps; pass LFGDungeonEntry ID
|
// roles bitmask: 0x02=tank, 0x04=healer, 0x08=dps; pass LFGDungeonEntry ID
|
||||||
void lfgJoin(uint32_t dungeonId, uint8_t roles);
|
void lfgJoin(uint32_t dungeonId, uint8_t roles);
|
||||||
|
|
@ -1517,36 +1387,14 @@ public:
|
||||||
const std::string& getLfgBootTargetName() const { return lfgBootTargetName_; }
|
const std::string& getLfgBootTargetName() const { return lfgBootTargetName_; }
|
||||||
const std::string& getLfgBootReason() const { return lfgBootReason_; }
|
const std::string& getLfgBootReason() const { return lfgBootReason_; }
|
||||||
|
|
||||||
// ---- Arena Team Stats ----
|
// Arena team stats (aliased from handler_types.hpp)
|
||||||
struct ArenaTeamStats {
|
using ArenaTeamStats = game::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
|
|
||||||
};
|
|
||||||
const std::vector<ArenaTeamStats>& getArenaTeamStats() const { return arenaTeamStats_; }
|
const std::vector<ArenaTeamStats>& getArenaTeamStats() const { return arenaTeamStats_; }
|
||||||
void requestArenaTeamRoster(uint32_t teamId);
|
void requestArenaTeamRoster(uint32_t teamId);
|
||||||
|
|
||||||
// ---- Arena Team Roster ----
|
// Arena team roster (aliased from handler_types.hpp)
|
||||||
struct ArenaTeamMember {
|
using ArenaTeamMember = game::ArenaTeamMember;
|
||||||
uint64_t guid = 0;
|
using ArenaTeamRoster = game::ArenaTeamRoster;
|
||||||
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;
|
|
||||||
};
|
|
||||||
// Returns roster for the given teamId, or nullptr if not yet received
|
// Returns roster for the given teamId, or nullptr if not yet received
|
||||||
const ArenaTeamRoster* getArenaTeamRoster(uint32_t teamId) const {
|
const ArenaTeamRoster* getArenaTeamRoster(uint32_t teamId) const {
|
||||||
for (const auto& r : arenaTeamRosters_) {
|
for (const auto& r : arenaTeamRosters_) {
|
||||||
|
|
@ -1574,36 +1422,15 @@ public:
|
||||||
bool hasMasterLootCandidates() const { return !masterLootCandidates_.empty(); }
|
bool hasMasterLootCandidates() const { return !masterLootCandidates_.empty(); }
|
||||||
void lootMasterGive(uint8_t lootSlot, uint64_t targetGuid);
|
void lootMasterGive(uint8_t lootSlot, uint64_t targetGuid);
|
||||||
|
|
||||||
// Group loot roll
|
// Group loot roll (aliased from handler_types.hpp)
|
||||||
struct LootRollEntry {
|
using LootRollEntry = game::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
|
|
||||||
};
|
|
||||||
bool hasPendingLootRoll() const { return pendingLootRollActive_; }
|
bool hasPendingLootRoll() const { return pendingLootRollActive_; }
|
||||||
const LootRollEntry& getPendingLootRoll() const { return pendingLootRoll_; }
|
const LootRollEntry& getPendingLootRoll() const { return pendingLootRoll_; }
|
||||||
void sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType);
|
void sendLootRoll(uint64_t objectGuid, uint32_t slot, uint8_t rollType);
|
||||||
// rollType: 0=need, 1=greed, 2=disenchant, 96=pass
|
// rollType: 0=need, 1=greed, 2=disenchant, 96=pass
|
||||||
|
|
||||||
// Equipment Sets (WotLK): saved gear loadouts
|
// Equipment Sets (aliased from handler_types.hpp)
|
||||||
struct EquipmentSetInfo {
|
using EquipmentSetInfo = game::EquipmentSetInfo;
|
||||||
uint64_t setGuid = 0;
|
|
||||||
uint32_t setId = 0;
|
|
||||||
std::string name;
|
|
||||||
std::string iconName;
|
|
||||||
};
|
|
||||||
const std::vector<EquipmentSetInfo>& getEquipmentSets() const { return equipmentSetInfo_; }
|
const std::vector<EquipmentSetInfo>& getEquipmentSets() const { return equipmentSetInfo_; }
|
||||||
bool supportsEquipmentSets() const;
|
bool supportsEquipmentSets() const;
|
||||||
void useEquipmentSet(uint32_t setId);
|
void useEquipmentSet(uint32_t setId);
|
||||||
|
|
@ -1638,14 +1465,8 @@ public:
|
||||||
}
|
}
|
||||||
const QuestDetailsData& getQuestDetails() const { return currentQuestDetails; }
|
const QuestDetailsData& getQuestDetails() const { return currentQuestDetails; }
|
||||||
|
|
||||||
// Gossip / quest map POI markers (SMSG_GOSSIP_POI)
|
// Gossip POI (aliased from handler_types.hpp)
|
||||||
struct GossipPoi {
|
using GossipPoi = game::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;
|
|
||||||
};
|
|
||||||
const std::vector<GossipPoi>& getGossipPois() const { return gossipPois_; }
|
const std::vector<GossipPoi>& getGossipPois() const { return gossipPois_; }
|
||||||
void clearGossipPois() { gossipPois_.clear(); }
|
void clearGossipPois() { gossipPois_.clear(); }
|
||||||
|
|
||||||
|
|
@ -1661,37 +1482,7 @@ public:
|
||||||
void closeQuestOfferReward();
|
void closeQuestOfferReward();
|
||||||
|
|
||||||
// Quest log
|
// Quest log
|
||||||
struct QuestLogEntry {
|
using QuestLogEntry = QuestHandler::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
|
|
||||||
};
|
|
||||||
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
||||||
int getSelectedQuestLogIndex() const { return selectedQuestLogIndex_; }
|
int getSelectedQuestLogIndex() const { return selectedQuestLogIndex_; }
|
||||||
void setSelectedQuestLogIndex(int idx) { selectedQuestLogIndex_ = idx; }
|
void setSelectedQuestLogIndex(int idx) { selectedQuestLogIndex_ = idx; }
|
||||||
|
|
@ -1879,7 +1670,7 @@ public:
|
||||||
void setWatchedFactionId(uint32_t factionId);
|
void setWatchedFactionId(uint32_t factionId);
|
||||||
uint32_t getLastContactListMask() const { return lastContactListMask_; }
|
uint32_t getLastContactListMask() const { return lastContactListMask_; }
|
||||||
uint32_t getLastContactListCount() const { return lastContactListCount_; }
|
uint32_t getLastContactListCount() const { return lastContactListCount_; }
|
||||||
bool isServerMovementAllowed() const { return serverMovementAllowed_; }
|
bool isServerMovementAllowed() const;
|
||||||
|
|
||||||
// Quest giver status (! and ? markers)
|
// Quest giver status (! and ? markers)
|
||||||
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const {
|
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const {
|
||||||
|
|
@ -2052,7 +1843,7 @@ public:
|
||||||
void setOpenLfgCallback(OpenLfgCallback cb) { openLfgCallback_ = std::move(cb); }
|
void setOpenLfgCallback(OpenLfgCallback cb) { openLfgCallback_ = std::move(cb); }
|
||||||
|
|
||||||
bool isMounted() const { return currentMountDisplayId_ != 0; }
|
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); }
|
bool isHostileFactionPublic(uint32_t factionTemplateId) const { return isHostileFaction(factionTemplateId); }
|
||||||
float getServerRunSpeed() const { return serverRunSpeed_; }
|
float getServerRunSpeed() const { return serverRunSpeed_; }
|
||||||
float getServerWalkSpeed() const { return serverWalkSpeed_; }
|
float getServerWalkSpeed() const { return serverWalkSpeed_; }
|
||||||
|
|
@ -2337,7 +2128,16 @@ public:
|
||||||
void resetDbcCaches();
|
void resetDbcCaches();
|
||||||
|
|
||||||
private:
|
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
|
* Handle incoming packet from world server
|
||||||
|
|
@ -2397,7 +2197,6 @@ private:
|
||||||
* Handle SMSG_WARDEN_DATA gate packet from server.
|
* Handle SMSG_WARDEN_DATA gate packet from server.
|
||||||
* We do not implement anti-cheat exchange for third-party realms.
|
* We do not implement anti-cheat exchange for third-party realms.
|
||||||
*/
|
*/
|
||||||
void handleWardenData(network::Packet& packet);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle SMSG_ACCOUNT_DATA_TIMES from server
|
* Handle SMSG_ACCOUNT_DATA_TIMES from server
|
||||||
|
|
@ -2432,14 +2231,6 @@ private:
|
||||||
*/
|
*/
|
||||||
void handleDestroyObject(network::Packet& packet);
|
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 ----
|
// ---- Phase 1 handlers ----
|
||||||
void handleNameQueryResponse(network::Packet& packet);
|
void handleNameQueryResponse(network::Packet& packet);
|
||||||
void handleCreatureQueryResponse(network::Packet& packet);
|
void handleCreatureQueryResponse(network::Packet& packet);
|
||||||
|
|
@ -2447,7 +2238,6 @@ private:
|
||||||
void handleGameObjectPageText(network::Packet& packet);
|
void handleGameObjectPageText(network::Packet& packet);
|
||||||
void handlePageTextQueryResponse(network::Packet& packet);
|
void handlePageTextQueryResponse(network::Packet& packet);
|
||||||
void handleItemQueryResponse(network::Packet& packet);
|
void handleItemQueryResponse(network::Packet& packet);
|
||||||
void handleInspectResults(network::Packet& packet);
|
|
||||||
void queryItemInfo(uint32_t entry, uint64_t guid);
|
void queryItemInfo(uint32_t entry, uint64_t guid);
|
||||||
void rebuildOnlineInventory();
|
void rebuildOnlineInventory();
|
||||||
void maybeDetectVisibleItemLayout();
|
void maybeDetectVisibleItemLayout();
|
||||||
|
|
@ -2459,56 +2249,22 @@ private:
|
||||||
void extractContainerFields(uint64_t containerGuid, const std::map<uint16_t, uint32_t>& fields);
|
void extractContainerFields(uint64_t containerGuid, const std::map<uint16_t, uint32_t>& fields);
|
||||||
uint64_t resolveOnlineItemGuid(uint32_t itemId) const;
|
uint64_t resolveOnlineItemGuid(uint32_t itemId) const;
|
||||||
|
|
||||||
// ---- Phase 2 handlers ----
|
// ---- Phase 2 handlers (dead — dispatched via CombatHandler) ----
|
||||||
void handleAttackStart(network::Packet& packet);
|
// handleAttackStart, handleAttackStop, handleAttackerStateUpdate,
|
||||||
void handleAttackStop(network::Packet& packet);
|
// handleSpellDamageLog, handleSpellHealLog removed
|
||||||
void handleAttackerStateUpdate(network::Packet& packet);
|
|
||||||
void handleSpellDamageLog(network::Packet& packet);
|
|
||||||
void handleSpellHealLog(network::Packet& packet);
|
|
||||||
|
|
||||||
// ---- Equipment set handler ----
|
// ---- Equipment set handler ----
|
||||||
void handleEquipmentSetList(network::Packet& packet);
|
|
||||||
void handleUpdateAuraDuration(uint8_t slot, uint32_t durationMs);
|
void handleUpdateAuraDuration(uint8_t slot, uint32_t durationMs);
|
||||||
void handleSetForcedReactions(network::Packet& packet);
|
// handleSetForcedReactions — dispatched via CombatHandler
|
||||||
|
|
||||||
// ---- Phase 3 handlers ----
|
// ---- 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 ----
|
// ---- Talent handlers ----
|
||||||
void handleTalentsInfo(network::Packet& packet);
|
|
||||||
|
|
||||||
// ---- Phase 4 handlers ----
|
// ---- 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 ----
|
// ---- 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 handlePetSpells(network::Packet& packet);
|
||||||
void handleTurnInPetitionResults(network::Packet& packet);
|
|
||||||
|
|
||||||
// ---- Character creation handler ----
|
// ---- Character creation handler ----
|
||||||
void handleCharCreateResponse(network::Packet& packet);
|
void handleCharCreateResponse(network::Packet& packet);
|
||||||
|
|
@ -2517,25 +2273,10 @@ private:
|
||||||
void handleXpGain(network::Packet& packet);
|
void handleXpGain(network::Packet& packet);
|
||||||
|
|
||||||
// ---- Creature movement handler ----
|
// ---- 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) ----
|
// ---- Other player movement (MSG_MOVE_* from server) ----
|
||||||
void handleOtherPlayerMovement(network::Packet& packet);
|
|
||||||
void handleMoveSetSpeed(network::Packet& packet);
|
|
||||||
|
|
||||||
// ---- Phase 5 handlers ----
|
// ---- 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 clearPendingQuestAccept(uint32_t questId);
|
||||||
void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason);
|
void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason);
|
||||||
bool hasQuestInLog(uint32_t questId) const;
|
bool hasQuestInLog(uint32_t questId) const;
|
||||||
|
|
@ -2546,101 +2287,43 @@ private:
|
||||||
int findQuestLogSlotIndexFromServer(uint32_t questId) const;
|
int findQuestLogSlotIndexFromServer(uint32_t questId) const;
|
||||||
void addQuestToLocalLogIfMissing(uint32_t questId, const std::string& title, const std::string& objectives);
|
void addQuestToLocalLogIfMissing(uint32_t questId, const std::string& title, const std::string& objectives);
|
||||||
bool resyncQuestLogFromServerSlots(bool forceQueryMetadata);
|
bool resyncQuestLogFromServerSlots(bool forceQueryMetadata);
|
||||||
void handleListInventory(network::Packet& packet);
|
|
||||||
void addMoneyCopper(uint32_t amount);
|
void addMoneyCopper(uint32_t amount);
|
||||||
|
|
||||||
// ---- Teleport handler ----
|
// ---- Teleport handler ----
|
||||||
void handleTeleportAck(network::Packet& packet);
|
|
||||||
void handleNewWorld(network::Packet& packet);
|
|
||||||
|
|
||||||
// ---- Movement ACK handlers ----
|
// ---- 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 ----
|
// ---- Area trigger detection ----
|
||||||
void loadAreaTriggerDbc();
|
void loadAreaTriggerDbc();
|
||||||
void checkAreaTriggers();
|
void checkAreaTriggers();
|
||||||
|
|
||||||
// ---- Instance lockout handler ----
|
// ---- Instance lockout handler ----
|
||||||
void handleRaidInstanceInfo(network::Packet& packet);
|
|
||||||
void handleItemTextQueryResponse(network::Packet& packet);
|
|
||||||
void handleQuestConfirmAccept(network::Packet& packet);
|
|
||||||
void handleSummonRequest(network::Packet& packet);
|
void handleSummonRequest(network::Packet& packet);
|
||||||
void handleTradeStatus(network::Packet& packet);
|
|
||||||
void handleTradeStatusExtended(network::Packet& packet);
|
|
||||||
void resetTradeState();
|
void resetTradeState();
|
||||||
void handleDuelRequested(network::Packet& packet);
|
void handleDuelRequested(network::Packet& packet);
|
||||||
void handleDuelComplete(network::Packet& packet);
|
void handleDuelComplete(network::Packet& packet);
|
||||||
void handleDuelWinner(network::Packet& packet);
|
void handleDuelWinner(network::Packet& packet);
|
||||||
void handleLootRoll(network::Packet& packet);
|
|
||||||
void handleLootRollWon(network::Packet& packet);
|
|
||||||
|
|
||||||
// ---- LFG / Dungeon Finder handlers ----
|
// ---- 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 ----
|
// ---- 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 ----
|
// ---- Bank handlers ----
|
||||||
void handleShowBank(network::Packet& packet);
|
|
||||||
void handleBuyBankSlotResult(network::Packet& packet);
|
|
||||||
|
|
||||||
// ---- Guild Bank handlers ----
|
// ---- Guild Bank handlers ----
|
||||||
void handleGuildBankList(network::Packet& packet);
|
|
||||||
|
|
||||||
// ---- Auction House handlers ----
|
// ---- 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 ----
|
// ---- 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 ----
|
// ---- Taxi handlers ----
|
||||||
void handleShowTaxiNodes(network::Packet& packet);
|
|
||||||
void handleActivateTaxiReply(network::Packet& packet);
|
|
||||||
void loadTaxiDbc();
|
|
||||||
|
|
||||||
// ---- Server info handlers ----
|
// ---- Server info handlers ----
|
||||||
void handleQueryTimeResponse(network::Packet& packet);
|
void handleQueryTimeResponse(network::Packet& packet);
|
||||||
void handlePlayedTime(network::Packet& packet);
|
|
||||||
void handleWho(network::Packet& packet);
|
|
||||||
|
|
||||||
// ---- Social handlers ----
|
// ---- 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 ----
|
// ---- 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,
|
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);
|
uint64_t srcGuid = 0, uint64_t dstGuid = 0);
|
||||||
|
|
@ -2677,6 +2360,16 @@ private:
|
||||||
float localOrientation);
|
float localOrientation);
|
||||||
void clearTransportAttachment(uint64_t childGuid);
|
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()
|
// Opcode dispatch table — built once in registerOpcodeHandlers(), called by handlePacket()
|
||||||
using PacketHandler = std::function<void(network::Packet&)>;
|
using PacketHandler = std::function<void(network::Packet&)>;
|
||||||
std::unordered_map<LogicalOpcode, PacketHandler> dispatchTable_;
|
std::unordered_map<LogicalOpcode, PacketHandler> dispatchTable_;
|
||||||
|
|
@ -2736,10 +2429,7 @@ private:
|
||||||
// Entity tracking
|
// Entity tracking
|
||||||
EntityManager entityManager; // Manages all entities in view
|
EntityManager entityManager; // Manages all entities in view
|
||||||
|
|
||||||
// Chat
|
// Chat (state lives in ChatHandler; callbacks remain here for cross-domain access)
|
||||||
std::deque<MessageChatData> chatHistory; // Recent chat messages
|
|
||||||
size_t maxChatHistory = 100; // Maximum chat messages to keep
|
|
||||||
std::vector<std::string> joinedChannels_; // Active channel memberships
|
|
||||||
ChatBubbleCallback chatBubbleCallback_;
|
ChatBubbleCallback chatBubbleCallback_;
|
||||||
AddonChatCallback addonChatCallback_;
|
AddonChatCallback addonChatCallback_;
|
||||||
AddonEventCallback addonEventCallback_;
|
AddonEventCallback addonEventCallback_;
|
||||||
|
|
@ -2885,32 +2575,9 @@ private:
|
||||||
std::unordered_set<uint64_t> pendingAutoInspect_;
|
std::unordered_set<uint64_t> pendingAutoInspect_;
|
||||||
float inspectRateLimit_ = 0.0f;
|
float inspectRateLimit_ = 0.0f;
|
||||||
|
|
||||||
// ---- Phase 2: Combat ----
|
// ---- Phase 2: Combat (state moved to CombatHandler) ----
|
||||||
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_;
|
|
||||||
bool wasCombat_ = false; // Previous frame combat state for PLAYER_REGEN edge detection
|
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_;
|
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 ----
|
// ---- Phase 3: Spells ----
|
||||||
WorldEntryCallback worldEntryCallback_;
|
WorldEntryCallback worldEntryCallback_;
|
||||||
|
|
@ -3022,7 +2689,6 @@ private:
|
||||||
|
|
||||||
// ---- Available battleground list (SMSG_BATTLEFIELD_LIST) ----
|
// ---- Available battleground list (SMSG_BATTLEFIELD_LIST) ----
|
||||||
std::vector<AvailableBgInfo> availableBgs_;
|
std::vector<AvailableBgInfo> availableBgs_;
|
||||||
void handleBattlefieldList(network::Packet& packet);
|
|
||||||
|
|
||||||
// Instance difficulty
|
// Instance difficulty
|
||||||
uint32_t instanceDifficulty_ = 0;
|
uint32_t instanceDifficulty_ = 0;
|
||||||
|
|
@ -3293,11 +2959,8 @@ private:
|
||||||
uint32_t knownTaxiMask_[12] = {}; // Track previously known nodes for discovery alerts
|
uint32_t knownTaxiMask_[12] = {}; // Track previously known nodes for discovery alerts
|
||||||
bool taxiMaskInitialized_ = false; // First SMSG_SHOWTAXINODES seeds mask without alerts
|
bool taxiMaskInitialized_ = false; // First SMSG_SHOWTAXINODES seeds mask without alerts
|
||||||
std::unordered_map<uint32_t, uint32_t> taxiCostMap_; // destNodeId -> total cost in copper
|
std::unordered_map<uint32_t, uint32_t> taxiCostMap_; // destNodeId -> total cost in copper
|
||||||
void buildTaxiCostMap();
|
|
||||||
void applyTaxiMountForCurrentNode();
|
|
||||||
uint32_t nextMovementTimestampMs();
|
uint32_t nextMovementTimestampMs();
|
||||||
void sanitizeMovementForTaxi();
|
void sanitizeMovementForTaxi();
|
||||||
void startClientTaxiPath(const std::vector<uint32_t>& pathNodes);
|
|
||||||
void updateClientTaxi(float deltaTime);
|
void updateClientTaxi(float deltaTime);
|
||||||
|
|
||||||
// Mail
|
// Mail
|
||||||
|
|
@ -3397,7 +3060,6 @@ private:
|
||||||
// Per-player achievement data from SMSG_RESPOND_INSPECT_ACHIEVEMENTS
|
// Per-player achievement data from SMSG_RESPOND_INSPECT_ACHIEVEMENTS
|
||||||
// Key: inspected player's GUID; value: set of earned achievement IDs
|
// Key: inspected player's GUID; value: set of earned achievement IDs
|
||||||
std::unordered_map<uint64_t, std::unordered_set<uint32_t>> inspectedPlayerAchievements_;
|
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)
|
// Area name cache (lazy-loaded from WorldMapArea.dbc; maps AreaTable ID → display name)
|
||||||
mutable std::unordered_map<uint32_t, std::string> areaNameCache_;
|
mutable std::unordered_map<uint32_t, std::string> areaNameCache_;
|
||||||
|
|
@ -3416,7 +3078,6 @@ private:
|
||||||
void loadLfgDungeonDbc() const;
|
void loadLfgDungeonDbc() const;
|
||||||
std::string getLfgDungeonName(uint32_t dungeonId) const;
|
std::string getLfgDungeonName(uint32_t dungeonId) const;
|
||||||
std::vector<TrainerTab> trainerTabs_;
|
std::vector<TrainerTab> trainerTabs_;
|
||||||
void handleTrainerList(network::Packet& packet);
|
|
||||||
void loadSpellNameCache() const;
|
void loadSpellNameCache() const;
|
||||||
void preloadDBCCaches() const;
|
void preloadDBCCaches() const;
|
||||||
void categorizeTrainerSpells();
|
void categorizeTrainerSpells();
|
||||||
|
|
@ -3466,7 +3127,6 @@ private:
|
||||||
std::vector<WardenCREntry> wardenCREntries_;
|
std::vector<WardenCREntry> wardenCREntries_;
|
||||||
// Module-specific check type opcodes [9]: MEM, PAGE_A, PAGE_B, MPQ, LUA, DRIVER, TIMING, PROC, MODULE
|
// Module-specific check type opcodes [9]: MEM, PAGE_A, PAGE_B, MPQ, LUA, DRIVER, TIMING, PROC, MODULE
|
||||||
uint8_t wardenCheckOpcodes_[9] = {};
|
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
|
// 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
|
std::future<std::vector<uint8_t>> wardenPendingEncrypted_; // encrypted response bytes
|
||||||
|
|
@ -3530,7 +3190,7 @@ private:
|
||||||
AppearanceChangedCallback appearanceChangedCallback_;
|
AppearanceChangedCallback appearanceChangedCallback_;
|
||||||
GhostStateCallback ghostStateCallback_;
|
GhostStateCallback ghostStateCallback_;
|
||||||
MeleeSwingCallback meleeSwingCallback_;
|
MeleeSwingCallback meleeSwingCallback_;
|
||||||
uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing
|
// lastMeleeSwingMs_ moved to CombatHandler
|
||||||
SpellCastAnimCallback spellCastAnimCallback_;
|
SpellCastAnimCallback spellCastAnimCallback_;
|
||||||
SpellCastFailedCallback spellCastFailedCallback_;
|
SpellCastFailedCallback spellCastFailedCallback_;
|
||||||
UnitAnimHintCallback unitAnimHintCallback_;
|
UnitAnimHintCallback unitAnimHintCallback_;
|
||||||
|
|
@ -3615,8 +3275,7 @@ private:
|
||||||
std::string pendingSaveSetIcon_;
|
std::string pendingSaveSetIcon_;
|
||||||
std::vector<EquipmentSetInfo> equipmentSetInfo_; // public-facing copy
|
std::vector<EquipmentSetInfo> equipmentSetInfo_; // public-facing copy
|
||||||
|
|
||||||
// ---- Forced faction reactions (SMSG_SET_FORCED_REACTIONS) ----
|
// forcedReactions_ moved to CombatHandler
|
||||||
std::unordered_map<uint32_t, uint8_t> forcedReactions_; // factionId -> reaction tier
|
|
||||||
|
|
||||||
// ---- Server-triggered audio ----
|
// ---- Server-triggered audio ----
|
||||||
PlayMusicCallback playMusicCallback_;
|
PlayMusicCallback playMusicCallback_;
|
||||||
|
|
|
||||||
27
include/game/game_utils.hpp
Normal file
27
include/game/game_utils.hpp
Normal 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
|
||||||
270
include/game/handler_types.hpp
Normal file
270
include/game/handler_types.hpp
Normal 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
|
||||||
401
include/game/inventory_handler.hpp
Normal file
401
include/game/inventory_handler.hpp
Normal 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
|
||||||
272
include/game/movement_handler.hpp
Normal file
272
include/game/movement_handler.hpp
Normal 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
|
||||||
199
include/game/quest_handler.hpp
Normal file
199
include/game/quest_handler.hpp
Normal 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
|
||||||
444
include/game/social_handler.hpp
Normal file
444
include/game/social_handler.hpp
Normal 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
|
||||||
331
include/game/spell_handler.hpp
Normal file
331
include/game/spell_handler.hpp
Normal 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
|
||||||
103
include/game/warden_handler.hpp
Normal file
103
include/game/warden_handler.hpp
Normal 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
713
src/game/chat_handler.cpp
Normal 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
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
3266
src/game/inventory_handler.cpp
Normal file
3266
src/game/inventory_handler.cpp
Normal file
File diff suppressed because it is too large
Load diff
2930
src/game/movement_handler.cpp
Normal file
2930
src/game/movement_handler.cpp
Normal file
File diff suppressed because it is too large
Load diff
1892
src/game/quest_handler.cpp
Normal file
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
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
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
1369
src/game/warden_handler.cpp
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue