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