mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-23 07:40:14 +00:00
Implements CMSG_SPLIT_ITEM (0x10E) with a slider popup for choosing split count. Auto-finds empty destination slot across backpack and bags. Shift+right-click on stackable items (count > 1) opens split dialog; non-stackable items still get the destroy confirmation.
3471 lines
158 KiB
C++
3471 lines
158 KiB
C++
#pragma once
|
||
|
||
#include "game/world_packets.hpp"
|
||
#include "game/character.hpp"
|
||
#include "game/opcode_table.hpp"
|
||
#include "game/update_field_table.hpp"
|
||
#include "game/inventory.hpp"
|
||
#include "game/spell_defines.hpp"
|
||
#include "game/group_defines.hpp"
|
||
#include "network/packet.hpp"
|
||
#include <glm/glm.hpp>
|
||
#include <memory>
|
||
#include <string>
|
||
#include <vector>
|
||
#include <deque>
|
||
#include <array>
|
||
#include <functional>
|
||
#include <cstdint>
|
||
#include <unordered_map>
|
||
#include <unordered_set>
|
||
#include <map>
|
||
#include <optional>
|
||
#include <algorithm>
|
||
#include <chrono>
|
||
#include <future>
|
||
|
||
namespace wowee::game {
|
||
class TransportManager;
|
||
class WardenCrypto;
|
||
class WardenMemory;
|
||
class WardenModule;
|
||
class WardenModuleManager;
|
||
class PacketParsers;
|
||
}
|
||
|
||
namespace wowee {
|
||
namespace network { class WorldSocket; class Packet; }
|
||
|
||
namespace game {
|
||
|
||
struct PlayerSkill {
|
||
uint32_t skillId = 0;
|
||
uint16_t value = 0; // base + permanent item bonuses
|
||
uint16_t maxValue = 0;
|
||
uint16_t bonusTemp = 0; // temporary buff bonus (food, potions, etc.)
|
||
uint16_t bonusPerm = 0; // permanent spec/misc bonus (rarely non-zero)
|
||
uint16_t effectiveValue() const { return value + bonusTemp + bonusPerm; }
|
||
};
|
||
|
||
/**
|
||
* Quest giver status values (WoW 3.3.5a)
|
||
*/
|
||
enum class QuestGiverStatus : uint8_t {
|
||
NONE = 0,
|
||
UNAVAILABLE = 1,
|
||
INCOMPLETE = 5, // ? (gray)
|
||
REWARD_REP = 6,
|
||
AVAILABLE_LOW = 7, // ! (gray, low-level)
|
||
AVAILABLE = 8, // ! (yellow)
|
||
REWARD = 10 // ? (yellow)
|
||
};
|
||
|
||
/**
|
||
* A single contact list entry (friend, ignore, or mute).
|
||
*/
|
||
struct ContactEntry {
|
||
uint64_t guid = 0;
|
||
std::string name;
|
||
std::string note;
|
||
uint32_t flags = 0; // 0x1=friend, 0x2=ignore, 0x4=mute
|
||
uint8_t status = 0; // 0=offline, 1=online, 2=AFK, 3=DND
|
||
uint32_t areaId = 0;
|
||
uint32_t level = 0;
|
||
uint32_t classId = 0;
|
||
|
||
bool isFriend() const { return (flags & 0x1) != 0; }
|
||
bool isIgnored() const { return (flags & 0x2) != 0; }
|
||
bool isOnline() const { return status != 0; }
|
||
};
|
||
|
||
/**
|
||
* World connection state
|
||
*/
|
||
enum class WorldState {
|
||
DISCONNECTED, // Not connected
|
||
CONNECTING, // TCP connection in progress
|
||
CONNECTED, // Connected, waiting for challenge
|
||
CHALLENGE_RECEIVED, // Received SMSG_AUTH_CHALLENGE
|
||
AUTH_SENT, // Sent CMSG_AUTH_SESSION, encryption initialized
|
||
AUTHENTICATED, // Received SMSG_AUTH_RESPONSE success
|
||
READY, // Ready for character/world operations
|
||
CHAR_LIST_REQUESTED, // CMSG_CHAR_ENUM sent
|
||
CHAR_LIST_RECEIVED, // SMSG_CHAR_ENUM received
|
||
ENTERING_WORLD, // CMSG_PLAYER_LOGIN sent
|
||
IN_WORLD, // In game world
|
||
FAILED // Connection or authentication failed
|
||
};
|
||
|
||
/**
|
||
* World connection callbacks
|
||
*/
|
||
using WorldConnectSuccessCallback = std::function<void()>;
|
||
using WorldConnectFailureCallback = std::function<void(const std::string& reason)>;
|
||
|
||
/**
|
||
* GameHandler - Manages world server connection and game protocol
|
||
*
|
||
* Handles:
|
||
* - Connection to world server
|
||
* - Authentication with session key from auth server
|
||
* - RC4 header encryption
|
||
* - Character enumeration
|
||
* - World entry
|
||
* - Game packets
|
||
*/
|
||
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
|
||
};
|
||
|
||
GameHandler();
|
||
~GameHandler();
|
||
|
||
/** Access the active opcode table (wire ↔ logical mapping). */
|
||
const OpcodeTable& getOpcodeTable() const { return opcodeTable_; }
|
||
OpcodeTable& getOpcodeTable() { return opcodeTable_; }
|
||
const UpdateFieldTable& getUpdateFieldTable() const { return updateFieldTable_; }
|
||
UpdateFieldTable& getUpdateFieldTable() { return updateFieldTable_; }
|
||
PacketParsers* getPacketParsers() { return packetParsers_.get(); }
|
||
void setPacketParsers(std::unique_ptr<PacketParsers> parsers);
|
||
|
||
/**
|
||
* Connect to world server
|
||
*
|
||
* @param host World server hostname/IP
|
||
* @param port World server port (default 8085)
|
||
* @param sessionKey 40-byte session key from auth server
|
||
* @param accountName Account name (will be uppercased)
|
||
* @param build Client build number (default 12340 for 3.3.5a)
|
||
* @return true if connection initiated
|
||
*/
|
||
bool connect(const std::string& host,
|
||
uint16_t port,
|
||
const std::vector<uint8_t>& sessionKey,
|
||
const std::string& accountName,
|
||
uint32_t build = 12340,
|
||
uint32_t realmId = 0);
|
||
|
||
/**
|
||
* Disconnect from world server
|
||
*/
|
||
void disconnect();
|
||
|
||
/**
|
||
* Check if connected to world server
|
||
*/
|
||
bool isConnected() const;
|
||
|
||
/**
|
||
* Get current connection state
|
||
*/
|
||
WorldState getState() const { return state; }
|
||
|
||
/**
|
||
* Request character list from server
|
||
* Must be called when state is READY or AUTHENTICATED
|
||
*/
|
||
void requestCharacterList();
|
||
|
||
/**
|
||
* Get list of characters (available after CHAR_LIST_RECEIVED state)
|
||
*/
|
||
const std::vector<Character>& getCharacters() const { return characters; }
|
||
|
||
void createCharacter(const CharCreateData& data);
|
||
void deleteCharacter(uint64_t characterGuid);
|
||
|
||
using CharCreateCallback = std::function<void(bool success, const std::string& message)>;
|
||
void setCharCreateCallback(CharCreateCallback cb) { charCreateCallback_ = std::move(cb); }
|
||
|
||
using CharDeleteCallback = std::function<void(bool success)>;
|
||
void setCharDeleteCallback(CharDeleteCallback cb) { charDeleteCallback_ = std::move(cb); }
|
||
uint8_t getLastCharDeleteResult() const { return lastCharDeleteResult_; }
|
||
|
||
using CharLoginFailCallback = std::function<void(const std::string& reason)>;
|
||
void setCharLoginFailCallback(CharLoginFailCallback cb) { charLoginFailCallback_ = std::move(cb); }
|
||
|
||
/**
|
||
* Select and log in with a character
|
||
* @param characterGuid GUID of character to log in with
|
||
*/
|
||
void selectCharacter(uint64_t characterGuid);
|
||
void setActiveCharacterGuid(uint64_t guid) { activeCharacterGuid_ = guid; }
|
||
uint64_t getActiveCharacterGuid() const { return activeCharacterGuid_; }
|
||
const Character* getActiveCharacter() const;
|
||
const Character* getFirstCharacter() const;
|
||
|
||
/**
|
||
* Get current player movement info
|
||
*/
|
||
const MovementInfo& getMovementInfo() const { return movementInfo; }
|
||
uint32_t getCurrentMapId() const { return currentMapId_; }
|
||
bool getHomeBind(uint32_t& mapId, glm::vec3& pos) const {
|
||
if (!hasHomeBind_) return false;
|
||
mapId = homeBindMapId_;
|
||
pos = homeBindPos_;
|
||
return true;
|
||
}
|
||
uint32_t getHomeBindZoneId() const { return homeBindZoneId_; }
|
||
|
||
/**
|
||
* Send a movement packet
|
||
* @param opcode Movement opcode (MSG_MOVE_START_FORWARD, etc.)
|
||
*/
|
||
void sendMovement(Opcode opcode);
|
||
|
||
/**
|
||
* Update player position
|
||
* @param x X coordinate
|
||
* @param y Y coordinate
|
||
* @param z Z coordinate
|
||
*/
|
||
void setPosition(float x, float y, float z);
|
||
|
||
/**
|
||
* Update player orientation
|
||
* @param orientation Facing direction in radians
|
||
*/
|
||
void setOrientation(float orientation);
|
||
|
||
/**
|
||
* Get entity manager (for accessing entities in view)
|
||
*/
|
||
EntityManager& getEntityManager() { return entityManager; }
|
||
const EntityManager& getEntityManager() const { return entityManager; }
|
||
|
||
/**
|
||
* Send a chat message
|
||
* @param type Chat type (SAY, YELL, WHISPER, etc.)
|
||
* @param message Message text
|
||
* @param target Target name (for whispers, empty otherwise)
|
||
*/
|
||
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);
|
||
const std::vector<std::string>& getJoinedChannels() const { return joinedChannels_; }
|
||
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;
|
||
};
|
||
ChatAutoJoin chatAutoJoin;
|
||
|
||
// Chat bubble callback: (senderGuid, message, isYell)
|
||
using ChatBubbleCallback = std::function<void(uint64_t, const std::string&, bool)>;
|
||
void setChatBubbleCallback(ChatBubbleCallback cb) { chatBubbleCallback_ = std::move(cb); }
|
||
|
||
// Emote animation callback: (entityGuid, animationId)
|
||
using EmoteAnimCallback = std::function<void(uint64_t, uint32_t)>;
|
||
void setEmoteAnimCallback(EmoteAnimCallback cb) { emoteAnimCallback_ = std::move(cb); }
|
||
|
||
/**
|
||
* Get chat history (recent messages)
|
||
* @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(); }
|
||
|
||
/**
|
||
* Add a locally-generated chat message (e.g., emote feedback)
|
||
*/
|
||
void addLocalChatMessage(const MessageChatData& msg);
|
||
|
||
// Money (copper)
|
||
uint64_t getMoneyCopper() const { return playerMoneyCopper_; }
|
||
|
||
// Server-authoritative armor (UNIT_FIELD_RESISTANCES[0])
|
||
int32_t getArmorRating() const { return playerArmorRating_; }
|
||
|
||
// Server-authoritative elemental resistances (UNIT_FIELD_RESISTANCES[1-6]).
|
||
// school: 1=Holy, 2=Fire, 3=Nature, 4=Frost, 5=Shadow, 6=Arcane. Returns 0 if not received.
|
||
int32_t getResistance(int school) const {
|
||
if (school < 1 || school > 6) return 0;
|
||
return playerResistances_[school - 1];
|
||
}
|
||
|
||
// Server-authoritative primary stats (UNIT_FIELD_STAT0-4: STR, AGI, STA, INT, SPI).
|
||
// Returns -1 if the server hasn't sent the value yet.
|
||
int32_t getPlayerStat(int idx) const {
|
||
if (idx < 0 || idx > 4) return -1;
|
||
return playerStats_[idx];
|
||
}
|
||
|
||
// Server-authoritative attack power (WotLK: UNIT_FIELD_ATTACK_POWER / RANGED).
|
||
// Returns -1 if not yet received.
|
||
int32_t getMeleeAttackPower() const { return playerMeleeAP_; }
|
||
int32_t getRangedAttackPower() const { return playerRangedAP_; }
|
||
|
||
// Server-authoritative spell damage / healing bonus (WotLK: PLAYER_FIELD_MOD_*).
|
||
// getSpellPower returns the max damage bonus across magic schools 1-6 (Holy/Fire/Nature/Frost/Shadow/Arcane).
|
||
// Returns -1 if not yet received.
|
||
int32_t getSpellPower() const {
|
||
int32_t sp = -1;
|
||
for (int i = 1; i <= 6; ++i) {
|
||
if (playerSpellDmgBonus_[i] > sp) sp = playerSpellDmgBonus_[i];
|
||
}
|
||
return sp;
|
||
}
|
||
int32_t getHealingPower() const { return playerHealBonus_; }
|
||
|
||
// Server-authoritative combat chance percentages (WotLK: PLAYER_* float fields).
|
||
// Returns -1.0f if not yet received.
|
||
float getDodgePct() const { return playerDodgePct_; }
|
||
float getParryPct() const { return playerParryPct_; }
|
||
float getBlockPct() const { return playerBlockPct_; }
|
||
float getCritPct() const { return playerCritPct_; }
|
||
float getRangedCritPct() const { return playerRangedCritPct_; }
|
||
// Spell crit by school (0=Physical,1=Holy,2=Fire,3=Nature,4=Frost,5=Shadow,6=Arcane)
|
||
float getSpellCritPct(int school = 1) const {
|
||
if (school < 0 || school > 6) return -1.0f;
|
||
return playerSpellCritPct_[school];
|
||
}
|
||
|
||
// Server-authoritative combat ratings (WotLK: PLAYER_FIELD_COMBAT_RATING_1+idx).
|
||
// Returns -1 if not yet received. Indices match AzerothCore CombatRating enum.
|
||
int32_t getCombatRating(int cr) const {
|
||
if (cr < 0 || cr > 24) return -1;
|
||
return playerCombatRatings_[cr];
|
||
}
|
||
|
||
// Inventory
|
||
Inventory& getInventory() { return inventory; }
|
||
const Inventory& getInventory() const { return inventory; }
|
||
bool consumeOnlineEquipmentDirty() { bool d = onlineEquipDirty_; onlineEquipDirty_ = false; return d; }
|
||
void resetEquipmentDirtyTracking() { lastEquipDisplayIds_ = {}; onlineEquipDirty_ = true; }
|
||
void unequipToBackpack(EquipSlot equipSlot);
|
||
|
||
// Targeting
|
||
void setTarget(uint64_t guid);
|
||
void clearTarget();
|
||
uint64_t getTargetGuid() const { return targetGuid; }
|
||
std::shared_ptr<Entity> getTarget() const;
|
||
bool hasTarget() const { return targetGuid != 0; }
|
||
void tabTarget(float playerX, float playerY, float playerZ);
|
||
|
||
// Focus targeting
|
||
void setFocus(uint64_t guid);
|
||
void clearFocus();
|
||
uint64_t getFocusGuid() const { return focusGuid; }
|
||
std::shared_ptr<Entity> getFocus() const;
|
||
bool hasFocus() const { return focusGuid != 0; }
|
||
|
||
// Mouseover targeting — set each frame by the nameplate renderer
|
||
void setMouseoverGuid(uint64_t guid) { mouseoverGuid_ = guid; }
|
||
uint64_t getMouseoverGuid() const { return mouseoverGuid_; }
|
||
|
||
// Advanced targeting
|
||
void targetLastTarget();
|
||
void targetEnemy(bool reverse = false);
|
||
void targetFriend(bool reverse = false);
|
||
|
||
// 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)
|
||
};
|
||
const InspectResult* getInspectResult() const {
|
||
return inspectResult_.guid ? &inspectResult_ : nullptr;
|
||
}
|
||
|
||
// Server info commands
|
||
void queryServerTime();
|
||
void requestPlayedTime();
|
||
void queryWho(const std::string& playerName = "");
|
||
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;
|
||
};
|
||
const std::vector<WhoEntry>& getWhoResults() const { return whoResults_; }
|
||
uint32_t getWhoOnlineCount() const { return whoOnlineCount_; }
|
||
std::string getWhoAreaName(uint32_t zoneId) const { return getAreaName(zoneId); }
|
||
|
||
// 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);
|
||
const std::unordered_map<std::string, uint64_t>& getIgnoreCache() const { return ignoreCache; }
|
||
|
||
// 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{};
|
||
};
|
||
|
||
// 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;
|
||
};
|
||
|
||
// 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_; }
|
||
|
||
// 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
|
||
};
|
||
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
|
||
};
|
||
const std::vector<BgPlayerPosition>& getBgPlayerPositions() const { return bgPlayerPositions_; }
|
||
|
||
// Network latency (milliseconds, updated each PONG response)
|
||
uint32_t getLatencyMs() const { return lastLatency; }
|
||
|
||
// Logout commands
|
||
void requestLogout();
|
||
void cancelLogout();
|
||
bool isLoggingOut() const { return loggingOut_; }
|
||
float getLogoutCountdown() const { return logoutCountdown_; }
|
||
|
||
// Stand state
|
||
void setStandState(uint8_t state); // 0=stand, 1=sit, 2=sit_chair, 3=sleep, 4=sit_low_chair, 5=sit_medium_chair, 6=sit_high_chair, 7=dead, 8=kneel, 9=submerged
|
||
uint8_t getStandState() const { return standState_; }
|
||
bool isSitting() const { return standState_ >= 1 && standState_ <= 6; }
|
||
bool isDead() const { return standState_ == 7; }
|
||
bool isKneeling() const { return standState_ == 8; }
|
||
|
||
// Display toggles
|
||
void toggleHelm();
|
||
void toggleCloak();
|
||
bool isHelmVisible() const { return helmVisible_; }
|
||
bool isCloakVisible() const { return cloakVisible_; }
|
||
|
||
// Follow/Assist
|
||
void followTarget();
|
||
void cancelFollow(); // Stop following current target
|
||
void assistTarget();
|
||
|
||
// PvP
|
||
void togglePvp();
|
||
|
||
// Minimap ping (Ctrl+click on minimap; wowX/wowY in canonical WoW coords)
|
||
void sendMinimapPing(float wowX, float wowY);
|
||
|
||
// Guild commands
|
||
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();
|
||
|
||
// GM Ticket
|
||
void submitGmTicket(const std::string& text);
|
||
void deleteGmTicket();
|
||
void requestGmTicket(); ///< Send CMSG_GMTICKET_GETTICKET to query open ticket
|
||
|
||
// GM ticket status accessors
|
||
bool hasActiveGmTicket() const { return gmTicketActive_; }
|
||
const std::string& getGmTicketText() const { return gmTicketText_; }
|
||
bool isGmSupportAvailable() const { return gmSupportAvailable_; }
|
||
float getGmTicketWaitHours() const { return gmTicketWaitHours_; }
|
||
|
||
// Battlefield Manager (Wintergrasp)
|
||
bool hasBfMgrInvite() const { return bfMgrInvitePending_; }
|
||
bool isInBfMgrZone() const { return bfMgrActive_; }
|
||
uint32_t getBfMgrZoneId() const { return bfMgrZoneId_; }
|
||
void acceptBfMgrInvite();
|
||
void declineBfMgrInvite();
|
||
|
||
// WotLK Calendar
|
||
uint32_t getCalendarPendingInvites() const { return calendarPendingInvites_; }
|
||
void requestCalendar(); ///< Send CMSG_CALENDAR_GET_CALENDAR to the server
|
||
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 {
|
||
if (!guildName_.empty()) return true;
|
||
const Character* ch = getActiveCharacter();
|
||
return ch && ch->hasGuild();
|
||
}
|
||
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(); }
|
||
bool hasPetitionShowlist() const { return showPetitionDialog_; }
|
||
void clearPetitionDialog() { showPetitionDialog_ = false; }
|
||
uint32_t getPetitionCost() const { return petitionCost_; }
|
||
uint64_t getPetitionNpcGuid() const { return petitionNpcGuid_; }
|
||
|
||
// Guild name lookup for other players' nameplates
|
||
// Returns the guild name for a given guildId, or empty if unknown.
|
||
// Automatically queries the server for unknown guild IDs.
|
||
const std::string& lookupGuildName(uint32_t guildId);
|
||
// 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;
|
||
};
|
||
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 forfeitDuel();
|
||
|
||
// AFK/DND status
|
||
void toggleAfk(const std::string& message = "");
|
||
void toggleDnd(const std::string& message = "");
|
||
bool isAfk() const { return afkStatus_; }
|
||
bool isDnd() const { return dndStatus_; }
|
||
void replyToLastWhisper(const std::string& message);
|
||
std::string getLastWhisperSender() const { return lastWhisperSender_; }
|
||
void setLastWhisperSender(const std::string& name) { lastWhisperSender_ = name; }
|
||
|
||
// Party/Raid management
|
||
void uninvitePlayer(const std::string& playerName);
|
||
void leaveParty();
|
||
void setMainTank(uint64_t targetGuid);
|
||
void setMainAssist(uint64_t targetGuid);
|
||
void clearMainTank();
|
||
void clearMainAssist();
|
||
void requestRaidInfo();
|
||
|
||
// Combat and Trade
|
||
void proposeDuel(uint64_t targetGuid);
|
||
void initiateTrade(uint64_t targetGuid);
|
||
void stopCasting();
|
||
|
||
// ---- Phase 1: Name queries ----
|
||
void queryPlayerName(uint64_t guid);
|
||
void queryCreatureInfo(uint32_t entry, uint64_t guid);
|
||
void queryGameObjectInfo(uint32_t entry, uint64_t guid);
|
||
const GameObjectQueryResponseData* getCachedGameObjectInfo(uint32_t entry) const {
|
||
auto it = gameObjectInfoCache_.find(entry);
|
||
return (it != gameObjectInfoCache_.end()) ? &it->second : nullptr;
|
||
}
|
||
std::string getCachedPlayerName(uint64_t guid) const;
|
||
std::string getCachedCreatureName(uint32_t entry) const;
|
||
// Returns the creature subname/title (e.g. "<Warchief of the Horde>"), empty if not cached
|
||
std::string getCachedCreatureSubName(uint32_t entry) const {
|
||
auto it = creatureInfoCache.find(entry);
|
||
return (it != creatureInfoCache.end()) ? it->second.subName : "";
|
||
}
|
||
// Returns the creature rank (0=Normal,1=Elite,2=RareElite,3=Boss,4=Rare)
|
||
// or -1 if not cached yet
|
||
int getCreatureRank(uint32_t entry) const {
|
||
auto it = creatureInfoCache.find(entry);
|
||
return (it != creatureInfoCache.end()) ? static_cast<int>(it->second.rank) : -1;
|
||
}
|
||
// Returns creature type (1=Beast,2=Dragonkin,...,7=Humanoid,...) or 0 if not cached
|
||
uint32_t getCreatureType(uint32_t entry) const {
|
||
auto it = creatureInfoCache.find(entry);
|
||
return (it != creatureInfoCache.end()) ? it->second.creatureType : 0;
|
||
}
|
||
// Returns creature family (e.g. pet family for beasts) or 0
|
||
uint32_t getCreatureFamily(uint32_t entry) const {
|
||
auto it = creatureInfoCache.find(entry);
|
||
return (it != creatureInfoCache.end()) ? it->second.family : 0;
|
||
}
|
||
|
||
// ---- Phase 2: Combat ----
|
||
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; }
|
||
// 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; }
|
||
void updateCombatText(float deltaTime);
|
||
|
||
// Combat log (persistent rolling history, max MAX_COMBAT_LOG entries)
|
||
const std::deque<CombatLogEntry>& getCombatLog() const { return combatLog_; }
|
||
void clearCombatLog() { combatLog_.clear(); }
|
||
|
||
// Area trigger messages (SMSG_AREA_TRIGGER_MESSAGE) — drained by UI each frame
|
||
bool hasAreaTriggerMsg() const { return !areaTriggerMsgs_.empty(); }
|
||
std::string popAreaTriggerMsg() {
|
||
if (areaTriggerMsgs_.empty()) return {};
|
||
std::string msg = areaTriggerMsgs_.front();
|
||
areaTriggerMsgs_.pop_front();
|
||
return msg;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// ---- Phase 3: Spells ----
|
||
void castSpell(uint32_t spellId, uint64_t targetGuid = 0);
|
||
void cancelCast();
|
||
void cancelAura(uint32_t spellId);
|
||
void dismissPet();
|
||
void renamePet(const std::string& newName);
|
||
bool hasPet() const { return petGuid_ != 0; }
|
||
// Returns true once after SMSG_PET_RENAMEABLE; consuming the flag clears it.
|
||
bool consumePetRenameablePending() { bool v = petRenameablePending_; petRenameablePending_ = false; return v; }
|
||
uint64_t getPetGuid() const { return petGuid_; }
|
||
|
||
// ---- Pet state (populated by SMSG_PET_SPELLS / SMSG_PET_MODE) ----
|
||
// 10 action bar slots; each entry is a packed uint32:
|
||
// bits 0-23 = spell ID (or 0 for empty)
|
||
// bits 24-31 = action type (0x00=cast, 0xC0=autocast on, 0x40=autocast off)
|
||
static constexpr int PET_ACTION_BAR_SLOTS = 10;
|
||
uint32_t getPetActionSlot(int idx) const {
|
||
if (idx < 0 || idx >= PET_ACTION_BAR_SLOTS) return 0;
|
||
return petActionSlots_[idx];
|
||
}
|
||
// Pet command/react state from SMSG_PET_MODE or SMSG_PET_SPELLS
|
||
uint8_t getPetCommand() const { return petCommand_; } // 0=stay,1=follow,2=attack,3=dismiss
|
||
uint8_t getPetReact() const { return petReact_; } // 0=passive,1=defensive,2=aggressive
|
||
// Spells the pet knows (from SMSG_PET_SPELLS spell list)
|
||
const std::vector<uint32_t>& getPetSpells() const { return petSpellList_; }
|
||
// Pet autocast set (spellIds that have autocast enabled)
|
||
bool isPetSpellAutocast(uint32_t spellId) const {
|
||
return petAutocastSpells_.count(spellId) != 0;
|
||
}
|
||
// Send CMSG_PET_ACTION to issue a pet command
|
||
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; }
|
||
|
||
// ---- Pet Stable ----
|
||
struct StabledPet {
|
||
uint32_t petNumber = 0; // server-side pet number (used for unstable/swap)
|
||
uint32_t entry = 0; // creature entry ID
|
||
uint32_t level = 0;
|
||
std::string name;
|
||
uint32_t displayId = 0;
|
||
bool isActive = false; // true = currently summoned/active slot
|
||
};
|
||
bool isStableWindowOpen() const { return stableWindowOpen_; }
|
||
void closeStableWindow() { stableWindowOpen_ = false; }
|
||
uint64_t getStableMasterGuid() const { return stableMasterGuid_; }
|
||
uint8_t getStableSlots() const { return stableNumSlots_; }
|
||
const std::vector<StabledPet>& getStabledPets() const { return stabledPets_; }
|
||
void requestStabledPetList(); // CMSG MSG_LIST_STABLED_PETS
|
||
void stablePet(uint8_t slot); // CMSG_STABLE_PET (store active pet in slot)
|
||
void unstablePet(uint32_t petNumber); // CMSG_UNSTABLE_PET (retrieve to active)
|
||
|
||
// Player proficiency bitmasks (from SMSG_SET_PROFICIENCY)
|
||
// itemClass 2 = Weapon (subClassMask bits: 0=Axe1H,1=Axe2H,2=Bow,3=Gun,4=Mace1H,5=Mace2H,6=Polearm,7=Sword1H,8=Sword2H,10=Staff,13=Fist,14=Misc,15=Dagger,16=Thrown,17=Crossbow,18=Wand,19=Fishing)
|
||
// itemClass 4 = Armor (subClassMask bits: 1=Cloth,2=Leather,3=Mail,4=Plate,6=Shield)
|
||
uint32_t getWeaponProficiency() const { return weaponProficiency_; }
|
||
uint32_t getArmorProficiency() const { return armorProficiency_; }
|
||
bool canUseWeaponSubclass(uint32_t subClass) const { return (weaponProficiency_ >> subClass) & 1u; }
|
||
bool canUseArmorSubclass(uint32_t subClass) const { return (armorProficiency_ >> subClass) & 1u; }
|
||
|
||
// Minimap pings from party members
|
||
struct MinimapPing {
|
||
uint64_t senderGuid = 0;
|
||
float wowX = 0.0f; // canonical WoW X (north)
|
||
float wowY = 0.0f; // canonical WoW Y (west)
|
||
float age = 0.0f; // seconds since received
|
||
static constexpr float LIFETIME = 5.0f;
|
||
bool isExpired() const { return age >= LIFETIME; }
|
||
};
|
||
const std::vector<MinimapPing>& getMinimapPings() const { return minimapPings_; }
|
||
void tickMinimapPings(float dt) {
|
||
for (auto& p : minimapPings_) p.age += dt;
|
||
minimapPings_.erase(
|
||
std::remove_if(minimapPings_.begin(), minimapPings_.end(),
|
||
[](const MinimapPing& p){ return p.isExpired(); }),
|
||
minimapPings_.end());
|
||
}
|
||
|
||
bool isCasting() const { return casting; }
|
||
bool isChanneling() const { return casting && castIsChannel; }
|
||
bool isGameObjectInteractionCasting() const {
|
||
return casting && currentCastSpellId == 0 && pendingGameObjectInteractGuid_ != 0;
|
||
}
|
||
uint32_t getCurrentCastSpellId() const { return currentCastSpellId; }
|
||
float getCastProgress() const { return castTimeTotal > 0 ? (castTimeTotal - castTimeRemaining) / castTimeTotal : 0.0f; }
|
||
float getCastTimeRemaining() const { return castTimeRemaining; }
|
||
|
||
// Repeat-craft queue
|
||
void startCraftQueue(uint32_t spellId, int count);
|
||
void cancelCraftQueue();
|
||
int getCraftQueueRemaining() const { return craftQueueRemaining_; }
|
||
uint32_t getCraftQueueSpellId() const { return craftQueueSpellId_; }
|
||
|
||
// 400ms spell-queue window: next spell to cast when current finishes
|
||
uint32_t getQueuedSpellId() const { return queuedSpellId_; }
|
||
|
||
// Unit cast state (tracked per GUID for target frame + boss frames)
|
||
struct UnitCastState {
|
||
bool casting = false;
|
||
uint32_t spellId = 0;
|
||
float timeRemaining = 0.0f;
|
||
float timeTotal = 0.0f;
|
||
bool interruptible = true; ///< false when SPELL_ATTR_EX_NOT_INTERRUPTIBLE is set
|
||
};
|
||
// Returns cast state for any unit by GUID (empty/non-casting if not found)
|
||
const UnitCastState* getUnitCastState(uint64_t guid) const {
|
||
auto it = unitCastStates_.find(guid);
|
||
return (it != unitCastStates_.end() && it->second.casting) ? &it->second : 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;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Glyphs (WotLK): up to 6 glyph slots per spec (3 major + 3 minor)
|
||
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();
|
||
|
||
// Action bar — 4 bars × 12 slots = 48 total
|
||
// Bar 0 (slots 0-11): main bottom bar (1-0, -, =)
|
||
// Bar 1 (slots 12-23): second bar above main (Shift+1 ... Shift+=)
|
||
// Bar 2 (slots 24-35): right side vertical bar
|
||
// Bar 3 (slots 36-47): left side vertical bar
|
||
static constexpr int SLOTS_PER_BAR = 12;
|
||
static constexpr int ACTION_BARS = 4;
|
||
static constexpr int ACTION_BAR_SLOTS = SLOTS_PER_BAR * ACTION_BARS; // 48
|
||
std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() { return actionBar; }
|
||
const std::array<ActionBarSlot, ACTION_BAR_SLOTS>& getActionBar() const { return actionBar; }
|
||
void setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id);
|
||
|
||
// Client-side macro text storage (server sends only macro index; text is stored locally)
|
||
const std::string& getMacroText(uint32_t macroId) const;
|
||
void setMacroText(uint32_t macroId, const std::string& text);
|
||
|
||
void saveCharacterConfig();
|
||
void loadCharacterConfig();
|
||
static std::string getCharacterConfigDir();
|
||
|
||
// Auras
|
||
const std::vector<AuraSlot>& getPlayerAuras() const { return playerAuras; }
|
||
const std::vector<AuraSlot>& getTargetAuras() const { return targetAuras; }
|
||
// 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;
|
||
}
|
||
|
||
// Completed quests (populated from SMSG_QUERY_QUESTS_COMPLETED_RESPONSE)
|
||
bool isQuestCompleted(uint32_t questId) const { return completedQuests_.count(questId) > 0; }
|
||
const std::unordered_set<uint32_t>& getCompletedQuests() const { return completedQuests_; }
|
||
|
||
// NPC death callback (for animations)
|
||
using NpcDeathCallback = std::function<void(uint64_t guid)>;
|
||
void setNpcDeathCallback(NpcDeathCallback cb) { npcDeathCallback_ = std::move(cb); }
|
||
|
||
using NpcAggroCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
|
||
void setNpcAggroCallback(NpcAggroCallback cb) { npcAggroCallback_ = std::move(cb); }
|
||
|
||
// NPC respawn callback (health 0 → >0, resets animation to idle)
|
||
using NpcRespawnCallback = std::function<void(uint64_t guid)>;
|
||
void setNpcRespawnCallback(NpcRespawnCallback cb) { npcRespawnCallback_ = std::move(cb); }
|
||
|
||
// Stand state animation callback — fired when SMSG_STANDSTATE_UPDATE confirms a new state
|
||
// standState: 0=stand, 1-6=sit variants, 7=dead, 8=kneel
|
||
using StandStateCallback = std::function<void(uint8_t standState)>;
|
||
void setStandStateCallback(StandStateCallback cb) { standStateCallback_ = std::move(cb); }
|
||
|
||
// Ghost state callback — fired when player enters or leaves ghost (spirit) form
|
||
using GhostStateCallback = std::function<void(bool isGhost)>;
|
||
void setGhostStateCallback(GhostStateCallback cb) { ghostStateCallback_ = std::move(cb); }
|
||
|
||
// Melee swing callback (for driving animation/SFX)
|
||
using MeleeSwingCallback = std::function<void()>;
|
||
void setMeleeSwingCallback(MeleeSwingCallback cb) { meleeSwingCallback_ = std::move(cb); }
|
||
|
||
// Spell cast animation callbacks — true=start cast/channel, false=finish/cancel
|
||
// guid: caster (may be player or another unit), isChannel: channel vs regular cast
|
||
using SpellCastAnimCallback = std::function<void(uint64_t guid, bool start, bool isChannel)>;
|
||
void setSpellCastAnimCallback(SpellCastAnimCallback cb) { spellCastAnimCallback_ = std::move(cb); }
|
||
|
||
// Fired when the player's own spell cast fails (spellId of the failed spell).
|
||
using SpellCastFailedCallback = std::function<void(uint32_t spellId)>;
|
||
void setSpellCastFailedCallback(SpellCastFailedCallback cb) { spellCastFailedCallback_ = std::move(cb); }
|
||
|
||
// Unit animation hint: signal jump (animId=38) for other players/NPCs
|
||
using UnitAnimHintCallback = std::function<void(uint64_t guid, uint32_t animId)>;
|
||
void setUnitAnimHintCallback(UnitAnimHintCallback cb) { unitAnimHintCallback_ = std::move(cb); }
|
||
|
||
// Unit move-flags callback: fired on every MSG_MOVE_* for other players with the raw flags field.
|
||
// Drives Walk(4) vs Run(5) selection and swim state initialization from heartbeat packets.
|
||
using UnitMoveFlagsCallback = std::function<void(uint64_t guid, uint32_t moveFlags)>;
|
||
void setUnitMoveFlagsCallback(UnitMoveFlagsCallback cb) { unitMoveFlagsCallback_ = std::move(cb); }
|
||
|
||
// NPC swing callback (plays attack animation on NPC)
|
||
using NpcSwingCallback = std::function<void(uint64_t guid)>;
|
||
void setNpcSwingCallback(NpcSwingCallback cb) { npcSwingCallback_ = std::move(cb); }
|
||
|
||
// NPC greeting callback (plays voice line when NPC is clicked)
|
||
using NpcGreetingCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
|
||
void setNpcGreetingCallback(NpcGreetingCallback cb) { npcGreetingCallback_ = std::move(cb); }
|
||
|
||
using NpcFarewellCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
|
||
void setNpcFarewellCallback(NpcFarewellCallback cb) { npcFarewellCallback_ = std::move(cb); }
|
||
|
||
using NpcVendorCallback = std::function<void(uint64_t guid, const glm::vec3& position)>;
|
||
void setNpcVendorCallback(NpcVendorCallback cb) { npcVendorCallback_ = std::move(cb); }
|
||
|
||
// XP tracking
|
||
uint32_t getPlayerXp() const { return playerXp_; }
|
||
uint32_t getPlayerNextLevelXp() const { return playerNextLevelXp_; }
|
||
uint32_t getPlayerRestedXp() const { return playerRestedXp_; }
|
||
bool isPlayerResting() const { return isResting_; }
|
||
uint32_t getPlayerLevel() const { return serverPlayerLevel_; }
|
||
const std::vector<uint32_t>& getPlayerExploredZoneMasks() const { return playerExploredZones_; }
|
||
bool hasPlayerExploredZoneMasks() const { return hasPlayerExploredZones_; }
|
||
static uint32_t killXp(uint32_t playerLevel, uint32_t victimLevel);
|
||
|
||
// Server time (for deterministic moon phases, etc.)
|
||
float getGameTime() const { return gameTime_; }
|
||
float getTimeSpeed() const { return timeSpeed_; }
|
||
|
||
// Global Cooldown (GCD) — set when the server sends a spellId=0 cooldown entry
|
||
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; }
|
||
|
||
// Weather state (updated by SMSG_WEATHER)
|
||
// weatherType: 0=clear, 1=rain, 2=snow, 3=storm/fog
|
||
uint32_t getWeatherType() const { return weatherType_; }
|
||
float getWeatherIntensity() const { return weatherIntensity_; }
|
||
bool isRaining() const { return weatherType_ == 1 && weatherIntensity_ > 0.05f; }
|
||
bool isSnowing() const { return weatherType_ == 2 && weatherIntensity_ > 0.05f; }
|
||
uint32_t getOverrideLightId() const { return overrideLightId_; }
|
||
uint32_t getOverrideLightTransMs() const { return overrideLightTransMs_; }
|
||
|
||
// Player skills
|
||
const std::map<uint32_t, PlayerSkill>& getPlayerSkills() const { return playerSkills_; }
|
||
const std::string& getSkillName(uint32_t skillId) const;
|
||
uint32_t getSkillCategory(uint32_t skillId) const;
|
||
bool isProfessionSpell(uint32_t spellId) const;
|
||
|
||
// World entry callback (online mode - triggered when entering world)
|
||
// Parameters: mapId, x, y, z (canonical WoW coords), isInitialEntry=true on first login or reconnect
|
||
using WorldEntryCallback = std::function<void(uint32_t mapId, float x, float y, float z, bool isInitialEntry)>;
|
||
void setWorldEntryCallback(WorldEntryCallback cb) { worldEntryCallback_ = std::move(cb); }
|
||
|
||
// Knockback callback: called when server sends SMSG_MOVE_KNOCK_BACK for the player.
|
||
// Parameters: vcos, vsin (render-space direction), hspeed, vspeed (raw from packet).
|
||
using KnockBackCallback = std::function<void(float vcos, float vsin, float hspeed, float vspeed)>;
|
||
void setKnockBackCallback(KnockBackCallback cb) { knockBackCallback_ = std::move(cb); }
|
||
|
||
// Camera shake callback: called when server sends SMSG_CAMERA_SHAKE.
|
||
// Parameters: magnitude (world units), frequency (Hz), duration (seconds).
|
||
using CameraShakeCallback = std::function<void(float magnitude, float frequency, float duration)>;
|
||
void setCameraShakeCallback(CameraShakeCallback cb) { cameraShakeCallback_ = std::move(cb); }
|
||
|
||
// Unstuck callback (resets player Z to floor height)
|
||
using UnstuckCallback = std::function<void()>;
|
||
void setUnstuckCallback(UnstuckCallback cb) { unstuckCallback_ = std::move(cb); }
|
||
void unstuck();
|
||
void setUnstuckGyCallback(UnstuckCallback cb) { unstuckGyCallback_ = std::move(cb); }
|
||
void unstuckGy();
|
||
void setUnstuckHearthCallback(UnstuckCallback cb) { unstuckHearthCallback_ = std::move(cb); }
|
||
void unstuckHearth();
|
||
using BindPointCallback = std::function<void(uint32_t mapId, float x, float y, float z)>;
|
||
void setBindPointCallback(BindPointCallback cb) { bindPointCallback_ = std::move(cb); }
|
||
|
||
// Called when the player starts casting Hearthstone so terrain at the bind
|
||
// point can be pre-loaded during the cast time.
|
||
// Parameters: mapId and canonical (x, y, z) of the bind location.
|
||
using HearthstonePreloadCallback = std::function<void(uint32_t mapId, float x, float y, float z)>;
|
||
void setHearthstonePreloadCallback(HearthstonePreloadCallback cb) { hearthstonePreloadCallback_ = std::move(cb); }
|
||
|
||
// Creature spawn callback (online mode - triggered when creature enters view)
|
||
// Parameters: guid, displayId, x, y, z (canonical), orientation, scale (OBJECT_FIELD_SCALE_X)
|
||
using CreatureSpawnCallback = std::function<void(uint64_t guid, uint32_t displayId, float x, float y, float z, float orientation, float scale)>;
|
||
void setCreatureSpawnCallback(CreatureSpawnCallback cb) { creatureSpawnCallback_ = std::move(cb); }
|
||
|
||
// Creature despawn callback (online mode - triggered when creature leaves view)
|
||
using CreatureDespawnCallback = std::function<void(uint64_t guid)>;
|
||
void setCreatureDespawnCallback(CreatureDespawnCallback cb) { creatureDespawnCallback_ = std::move(cb); }
|
||
|
||
// Player spawn callback (online mode - triggered when a player enters view).
|
||
// Players need appearance data so the renderer can build the right body/hair textures.
|
||
using PlayerSpawnCallback = std::function<void(uint64_t guid,
|
||
uint32_t displayId,
|
||
uint8_t raceId,
|
||
uint8_t genderId,
|
||
uint32_t appearanceBytes,
|
||
uint8_t facialFeatures,
|
||
float x, float y, float z, float orientation)>;
|
||
void setPlayerSpawnCallback(PlayerSpawnCallback cb) { playerSpawnCallback_ = std::move(cb); }
|
||
|
||
using PlayerDespawnCallback = std::function<void(uint64_t guid)>;
|
||
void setPlayerDespawnCallback(PlayerDespawnCallback cb) { playerDespawnCallback_ = std::move(cb); }
|
||
|
||
// Online player equipment visuals callback.
|
||
// Sends a best-effort view of equipped items for players in view using ItemDisplayInfo IDs.
|
||
// Arrays are indexed by EquipSlot (0..18). Values are 0 when unknown/unavailable.
|
||
using PlayerEquipmentCallback = std::function<void(uint64_t guid,
|
||
const std::array<uint32_t, 19>& displayInfoIds,
|
||
const std::array<uint8_t, 19>& inventoryTypes)>;
|
||
void setPlayerEquipmentCallback(PlayerEquipmentCallback cb) { playerEquipmentCallback_ = std::move(cb); }
|
||
|
||
// GameObject spawn callback (online mode - triggered when gameobject enters view)
|
||
// Parameters: guid, entry, displayId, x, y, z (canonical), orientation, scale (OBJECT_FIELD_SCALE_X)
|
||
using GameObjectSpawnCallback = std::function<void(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation, float scale)>;
|
||
void setGameObjectSpawnCallback(GameObjectSpawnCallback cb) { gameObjectSpawnCallback_ = std::move(cb); }
|
||
|
||
// GameObject move callback (online mode - triggered when gameobject position updates)
|
||
// Parameters: guid, x, y, z (canonical), orientation
|
||
using GameObjectMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, float orientation)>;
|
||
void setGameObjectMoveCallback(GameObjectMoveCallback cb) { gameObjectMoveCallback_ = std::move(cb); }
|
||
|
||
// GameObject despawn callback (online mode - triggered when gameobject leaves view)
|
||
using GameObjectDespawnCallback = std::function<void(uint64_t guid)>;
|
||
void setGameObjectDespawnCallback(GameObjectDespawnCallback cb) { gameObjectDespawnCallback_ = std::move(cb); }
|
||
|
||
using GameObjectCustomAnimCallback = std::function<void(uint64_t guid, uint32_t animId)>;
|
||
void setGameObjectCustomAnimCallback(GameObjectCustomAnimCallback cb) { gameObjectCustomAnimCallback_ = std::move(cb); }
|
||
|
||
// Faction hostility map (populated from FactionTemplate.dbc by Application)
|
||
void setFactionHostileMap(std::unordered_map<uint32_t, bool> map) { factionHostileMap_ = std::move(map); }
|
||
|
||
// Creature move callback (online mode - triggered by SMSG_MONSTER_MOVE)
|
||
// Parameters: guid, x, y, z (canonical), duration_ms (0 = instant)
|
||
using CreatureMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, uint32_t durationMs)>;
|
||
void setCreatureMoveCallback(CreatureMoveCallback cb) { creatureMoveCallback_ = std::move(cb); }
|
||
|
||
// Transport move callback (online mode - triggered when transport position updates)
|
||
// Parameters: guid, x, y, z (canonical), orientation
|
||
using TransportMoveCallback = std::function<void(uint64_t guid, float x, float y, float z, float orientation)>;
|
||
void setTransportMoveCallback(TransportMoveCallback cb) { transportMoveCallback_ = std::move(cb); }
|
||
|
||
// Transport spawn callback (online mode - triggered when transport GameObject is first detected)
|
||
// Parameters: guid, entry, displayId, x, y, z (canonical), orientation
|
||
using TransportSpawnCallback = std::function<void(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation)>;
|
||
void setTransportSpawnCallback(TransportSpawnCallback cb) { transportSpawnCallback_ = std::move(cb); }
|
||
|
||
// Notify that a transport has been spawned (called after WMO instance creation)
|
||
void notifyTransportSpawned(uint64_t guid, uint32_t entry, uint32_t displayId, float x, float y, float z, float orientation) {
|
||
if (transportSpawnCallback_) {
|
||
transportSpawnCallback_(guid, entry, displayId, x, y, z, orientation);
|
||
}
|
||
}
|
||
|
||
// Transport state for player-on-transport
|
||
bool isOnTransport() const { return playerTransportGuid_ != 0; }
|
||
uint64_t getPlayerTransportGuid() const { return playerTransportGuid_; }
|
||
glm::vec3 getPlayerTransportOffset() const { return playerTransportOffset_; }
|
||
|
||
// Check if a GUID is a known transport
|
||
bool isTransportGuid(uint64_t guid) const { return transportGuids_.count(guid) > 0; }
|
||
bool hasServerTransportUpdate(uint64_t guid) const { return serverUpdatedTransportGuids_.count(guid) > 0; }
|
||
glm::vec3 getComposedWorldPosition(); // Compose transport transform * local offset
|
||
TransportManager* getTransportManager() { return transportManager_.get(); }
|
||
void setPlayerOnTransport(uint64_t transportGuid, const glm::vec3& localOffset) {
|
||
// Validate transport is registered before attaching player
|
||
// (defer if transport not yet registered to prevent desyncs)
|
||
if (transportGuid != 0 && !isTransportGuid(transportGuid)) {
|
||
return; // Transport not yet registered; skip attachment
|
||
}
|
||
playerTransportGuid_ = transportGuid;
|
||
playerTransportOffset_ = localOffset;
|
||
playerTransportStickyGuid_ = transportGuid;
|
||
playerTransportStickyTimer_ = 8.0f;
|
||
movementInfo.transportGuid = transportGuid;
|
||
}
|
||
void setPlayerTransportOffset(const glm::vec3& offset) {
|
||
playerTransportOffset_ = offset;
|
||
}
|
||
void clearPlayerTransport() {
|
||
if (playerTransportGuid_ != 0) {
|
||
playerTransportStickyGuid_ = playerTransportGuid_;
|
||
playerTransportStickyTimer_ = std::max(playerTransportStickyTimer_, 1.5f);
|
||
}
|
||
playerTransportGuid_ = 0;
|
||
playerTransportOffset_ = glm::vec3(0.0f);
|
||
movementInfo.transportGuid = 0;
|
||
}
|
||
|
||
// Cooldowns
|
||
float getSpellCooldown(uint32_t spellId) const;
|
||
const std::unordered_map<uint32_t, float>& getSpellCooldowns() const { return spellCooldowns; }
|
||
|
||
// Player GUID
|
||
uint64_t getPlayerGuid() const { return playerGuid; }
|
||
|
||
// Look up a display name for any guid: checks playerNameCache then entity manager.
|
||
// Returns empty string if unknown. Used by chat display to resolve names at render time.
|
||
const std::string& lookupName(uint64_t guid) const {
|
||
static const std::string kEmpty;
|
||
auto it = playerNameCache.find(guid);
|
||
if (it != playerNameCache.end()) return it->second;
|
||
auto entity = entityManager.getEntity(guid);
|
||
if (entity) {
|
||
if (auto* unit = dynamic_cast<const Unit*>(entity.get())) {
|
||
if (!unit->getName().empty()) return unit->getName();
|
||
}
|
||
}
|
||
return kEmpty;
|
||
}
|
||
|
||
uint8_t getPlayerClass() const {
|
||
const Character* ch = getActiveCharacter();
|
||
return ch ? static_cast<uint8_t>(ch->characterClass) : 0;
|
||
}
|
||
uint8_t getPlayerRace() const {
|
||
const Character* ch = getActiveCharacter();
|
||
return ch ? static_cast<uint8_t>(ch->race) : 0;
|
||
}
|
||
void setPlayerGuid(uint64_t guid) { playerGuid = guid; }
|
||
|
||
// Player death state
|
||
bool isPlayerDead() const { return playerDead_; }
|
||
bool isPlayerGhost() const { return releasedSpirit_; }
|
||
bool showDeathDialog() const { return playerDead_ && !releasedSpirit_; }
|
||
bool showResurrectDialog() const { return resurrectRequestPending_; }
|
||
/** True when SMSG_PRE_RESURRECT arrived — Reincarnation/Twisting Nether available. */
|
||
bool canSelfRes() const { return selfResAvailable_; }
|
||
/** Send CMSG_SELF_RES to use Reincarnation / Twisting Nether. */
|
||
void useSelfRes();
|
||
const std::string& getResurrectCasterName() const { return resurrectCasterName_; }
|
||
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; }
|
||
/** True when ghost is within 40 yards of corpse position (same map). */
|
||
bool canReclaimCorpse() const;
|
||
/** Seconds remaining on the PvP corpse-reclaim delay, or 0 if the reclaim is available now. */
|
||
float getCorpseReclaimDelaySec() const;
|
||
/** Distance (yards) from ghost to corpse, or -1 if no corpse data. */
|
||
float getCorpseDistance() const {
|
||
if (corpseMapId_ == 0 || currentMapId_ != corpseMapId_) return -1.0f;
|
||
// movementInfo is canonical (x=north=server_y, y=west=server_x);
|
||
// corpse coords are raw server (x=west, y=north) — swap to compare.
|
||
float dx = movementInfo.x - corpseY_;
|
||
float dy = movementInfo.y - corpseX_;
|
||
float dz = movementInfo.z - corpseZ_;
|
||
return std::sqrt(dx*dx + dy*dy + dz*dz);
|
||
}
|
||
/** Corpse position in canonical WoW coords (X=north, Y=west).
|
||
* Returns false if no corpse data or on a different map. */
|
||
bool getCorpseCanonicalPos(float& outX, float& outY) const {
|
||
if (corpseMapId_ == 0 || currentMapId_ != corpseMapId_) return false;
|
||
outX = corpseY_; // server Y = canonical X (north)
|
||
outY = corpseX_; // server X = canonical Y (west)
|
||
return true;
|
||
}
|
||
/** Send CMSG_RECLAIM_CORPSE; noop if not a ghost or not near corpse. */
|
||
void reclaimCorpse();
|
||
void releaseSpirit();
|
||
void acceptResurrect();
|
||
void declineResurrect();
|
||
|
||
// ---- Phase 4: Group ----
|
||
void inviteToGroup(const std::string& playerName);
|
||
void acceptGroupInvite();
|
||
void declineGroupInvite();
|
||
void leaveGroup();
|
||
bool isInGroup() const { return !partyData.isEmpty(); }
|
||
const GroupListData& getPartyData() const { return partyData; }
|
||
const std::vector<ContactEntry>& getContacts() const { return contacts_; }
|
||
bool hasPendingGroupInvite() const { return pendingGroupInvite; }
|
||
const std::string& getPendingInviterName() const { return pendingInviterName; }
|
||
|
||
// ---- 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);
|
||
|
||
// ---- 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();
|
||
|
||
// ---- Summon ----
|
||
bool hasPendingSummonRequest() const { return pendingSummonRequest_; }
|
||
const std::string& getSummonerName() const { return summonerName_; }
|
||
float getSummonTimeoutSec() const { return summonTimeoutSec_; }
|
||
void acceptSummon();
|
||
void declineSummon();
|
||
void tickSummonTimeout(float dt) {
|
||
if (!pendingSummonRequest_) return;
|
||
summonTimeoutSec_ -= dt;
|
||
if (summonTimeoutSec_ <= 0.0f) {
|
||
pendingSummonRequest_ = false;
|
||
summonTimeoutSec_ = 0.0f;
|
||
}
|
||
}
|
||
|
||
// ---- Trade ----
|
||
enum class TradeStatus : uint8_t {
|
||
None = 0, PendingIncoming, Open, Accepted, Complete
|
||
};
|
||
|
||
static constexpr int TRADE_SLOT_COUNT = 6; // WoW has 6 normal trade slots + slot 6 for non-trade item
|
||
|
||
struct TradeSlot {
|
||
uint32_t itemId = 0;
|
||
uint32_t displayId = 0;
|
||
uint32_t stackCount = 0;
|
||
uint64_t itemGuid = 0;
|
||
uint8_t bag = 0xFF; // 0xFF = not set
|
||
uint8_t bagSlot = 0xFF;
|
||
bool occupied = false;
|
||
};
|
||
|
||
TradeStatus getTradeStatus() const { return tradeStatus_; }
|
||
bool hasPendingTradeRequest() const { return tradeStatus_ == TradeStatus::PendingIncoming; }
|
||
bool isTradeOpen() const { return tradeStatus_ == TradeStatus::Open || tradeStatus_ == TradeStatus::Accepted; }
|
||
const std::string& getTradePeerName() const { return tradePeerName_; }
|
||
|
||
// My trade slots (what I'm offering)
|
||
const std::array<TradeSlot, TRADE_SLOT_COUNT>& getMyTradeSlots() const { return myTradeSlots_; }
|
||
// Peer's trade slots (what they're offering)
|
||
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(); // respond to incoming SMSG_TRADE_STATUS(1) with CMSG_BEGIN_TRADE
|
||
void declineTradeRequest(); // respond with CMSG_CANCEL_TRADE
|
||
void acceptTrade(); // lock in offer: CMSG_ACCEPT_TRADE
|
||
void cancelTrade(); // CMSG_CANCEL_TRADE
|
||
void setTradeItem(uint8_t tradeSlot, uint8_t bag, uint8_t bagSlot);
|
||
void clearTradeItem(uint8_t tradeSlot);
|
||
void setTradeGold(uint64_t copper);
|
||
|
||
// ---- Duel ----
|
||
bool hasPendingDuelRequest() const { return pendingDuelRequest_; }
|
||
const std::string& getDuelChallengerName() const { return duelChallengerName_; }
|
||
void acceptDuel();
|
||
// forfeitDuel() already declared at line ~399
|
||
// Returns remaining duel countdown seconds, or 0 if no active countdown
|
||
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;
|
||
}
|
||
|
||
// ---- 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;
|
||
};
|
||
const std::vector<InstanceLockout>& getInstanceLockouts() const { return instanceLockouts_; }
|
||
|
||
// Boss encounter unit tracking (SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
||
static constexpr uint32_t kMaxEncounterSlots = 5;
|
||
// Returns boss unit guid for the given encounter slot (0 if none)
|
||
uint64_t getEncounterUnitGuid(uint32_t slot) const {
|
||
return (slot < kMaxEncounterSlots) ? encounterUnitGuids_[slot] : 0;
|
||
}
|
||
|
||
// Raid target markers (MSG_RAID_TARGET_UPDATE)
|
||
// Icon indices 0-7: Star, Circle, Diamond, Triangle, Moon, Square, Cross, Skull
|
||
static constexpr uint32_t kRaidMarkCount = 8;
|
||
// Returns the GUID marked with the given icon (0 = no mark)
|
||
uint64_t getRaidMarkGuid(uint32_t icon) const {
|
||
return (icon < kRaidMarkCount) ? raidTargetGuids_[icon] : 0;
|
||
}
|
||
// Returns the raid mark icon for a given guid (0xFF = no mark)
|
||
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;
|
||
}
|
||
// Set or clear a raid mark on a guid (icon 0-7, or 0xFF to clear)
|
||
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,
|
||
};
|
||
|
||
// roles bitmask: 0x02=tank, 0x04=healer, 0x08=dps; pass LFGDungeonEntry ID
|
||
void lfgJoin(uint32_t dungeonId, uint8_t roles);
|
||
void lfgLeave();
|
||
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 { return getLfgDungeonName(lfgDungeonId_); }
|
||
std::string getMapName(uint32_t mapId) 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 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;
|
||
};
|
||
const std::vector<ArenaTeamStats>& getArenaTeamStats() const { return arenaTeamStats_; }
|
||
|
||
// ---- 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;
|
||
};
|
||
// Returns roster for the given teamId, or nullptr if not yet received
|
||
const ArenaTeamRoster* getArenaTeamRoster(uint32_t teamId) const {
|
||
for (const auto& r : arenaTeamRosters_) {
|
||
if (r.teamId == teamId) return &r;
|
||
}
|
||
return nullptr;
|
||
}
|
||
|
||
// ---- Phase 5: Loot ----
|
||
void lootTarget(uint64_t guid);
|
||
void lootItem(uint8_t slotIndex);
|
||
void closeLoot();
|
||
void activateSpiritHealer(uint64_t npcGuid);
|
||
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
|
||
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
|
||
};
|
||
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;
|
||
};
|
||
const std::vector<EquipmentSetInfo>& getEquipmentSets() const { return equipmentSetInfo_; }
|
||
void useEquipmentSet(uint32_t setId);
|
||
|
||
// NPC Gossip
|
||
void interactWithNpc(uint64_t guid);
|
||
void interactWithGameObject(uint64_t guid);
|
||
void selectGossipOption(uint32_t optionId);
|
||
void selectGossipQuest(uint32_t questId);
|
||
void acceptQuest();
|
||
void declineQuest();
|
||
void closeGossip();
|
||
// Quest-starting items: right-click triggers quest offer dialog via questgiver protocol
|
||
void offerQuestFromItem(uint64_t itemGuid, uint32_t questId);
|
||
uint64_t getBagItemGuid(int bagIndex, int slotIndex) const;
|
||
bool isGossipWindowOpen() const { return gossipWindowOpen; }
|
||
const GossipMessageData& getCurrentGossip() const { return currentGossip; }
|
||
bool isQuestDetailsOpen() {
|
||
// Check if delayed opening timer has expired
|
||
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 (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;
|
||
};
|
||
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(); // Send CMSG_QUESTGIVER_COMPLETE_QUEST
|
||
void closeQuestRequestItems();
|
||
|
||
bool isQuestOfferRewardOpen() const { return questOfferRewardOpen_; }
|
||
const QuestOfferRewardData& getQuestOfferReward() const { return currentQuestOfferReward_; }
|
||
void chooseQuestReward(uint32_t rewardIndex); // Send CMSG_QUESTGIVER_CHOOSE_REWARD
|
||
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
|
||
};
|
||
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
||
void abandonQuest(uint32_t questId);
|
||
void shareQuestWithParty(uint32_t questId); // CMSG_PUSHQUESTTOPARTY
|
||
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) {
|
||
if (tracked) trackedQuestIds_.insert(questId);
|
||
else trackedQuestIds_.erase(questId);
|
||
}
|
||
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); }
|
||
const std::unordered_map<uint32_t, uint32_t>& getWorldStates() const { return worldStates_; }
|
||
std::optional<uint32_t> getWorldState(uint32_t key) const {
|
||
auto it = worldStates_.find(key);
|
||
if (it == worldStates_.end()) return std::nullopt;
|
||
return it->second;
|
||
}
|
||
uint32_t getWorldStateMapId() const { return worldStateMapId_; }
|
||
uint32_t getWorldStateZoneId() const { return worldStateZoneId_; }
|
||
|
||
// Mirror timers (0=fatigue, 1=breath, 2=feigndeath)
|
||
struct MirrorTimer {
|
||
int32_t value = 0;
|
||
int32_t maxValue = 0;
|
||
int32_t scale = 0; // +1 = counting up, -1 = counting down
|
||
bool paused = false;
|
||
bool active = false;
|
||
};
|
||
const MirrorTimer& getMirrorTimer(int type) const {
|
||
static MirrorTimer empty;
|
||
return (type >= 0 && type < 3) ? mirrorTimers_[type] : empty;
|
||
}
|
||
|
||
// Combo points
|
||
uint8_t getComboPoints() const { return comboPoints_; }
|
||
uint64_t getComboTarget() const { return comboTarget_; }
|
||
|
||
// Death Knight rune state (6 runes: 0-1=Blood, 2-3=Unholy, 4-5=Frost; may become Death=3)
|
||
enum class RuneType : uint8_t { Blood = 0, Unholy = 1, Frost = 2, Death = 3 };
|
||
struct RuneSlot {
|
||
RuneType type = RuneType::Blood;
|
||
bool ready = true; // Server-confirmed ready state
|
||
float readyFraction = 1.0f; // 0.0=depleted → 1.0=full (from server sync)
|
||
};
|
||
const std::array<RuneSlot, 6>& getPlayerRunes() const { return playerRunes_; }
|
||
|
||
// Talent-driven spell modifiers (SMSG_SET_FLAT_SPELL_MODIFIER / SMSG_SET_PCT_SPELL_MODIFIER)
|
||
// SpellModOp matches WotLK SpellModOp enum (server-side).
|
||
enum class SpellModOp : uint8_t {
|
||
Damage = 0,
|
||
Duration = 1,
|
||
Threat = 2,
|
||
Effect1 = 3,
|
||
Charges = 4,
|
||
Range = 5,
|
||
Radius = 6,
|
||
CritChance = 7,
|
||
AllEffects = 8,
|
||
NotLoseCastingTime = 9,
|
||
CastingTime = 10,
|
||
Cooldown = 11,
|
||
Effect2 = 12,
|
||
IgnoreArmor = 13,
|
||
Cost = 14,
|
||
CritDamageBonus = 15,
|
||
ResistMissChance = 16,
|
||
JumpTargets = 17,
|
||
ChanceOfSuccess = 18,
|
||
ActivationTime = 19,
|
||
Efficiency = 20,
|
||
MultipleValue = 21,
|
||
ResistDispelChance = 22,
|
||
Effect3 = 23,
|
||
BonusMultiplier = 24,
|
||
ProcPerMinute = 25,
|
||
ValueMultiplier = 26,
|
||
ResistPushback = 27,
|
||
MechanicDuration = 28,
|
||
StartCooldown = 29,
|
||
PeriodicBonus = 30,
|
||
AttackPower = 31,
|
||
};
|
||
static constexpr int SPELL_MOD_OP_COUNT = 32;
|
||
|
||
// Key: (SpellModOp, groupIndex) — value: accumulated flat or pct modifier
|
||
// pct values are stored in integer percent (e.g. -20 means -20% reduction).
|
||
struct SpellModKey {
|
||
SpellModOp op;
|
||
uint8_t group;
|
||
bool operator==(const SpellModKey& o) const {
|
||
return op == o.op && group == o.group;
|
||
}
|
||
};
|
||
struct SpellModKeyHash {
|
||
std::size_t operator()(const SpellModKey& k) const {
|
||
return std::hash<uint32_t>()(
|
||
(static_cast<uint32_t>(static_cast<uint8_t>(k.op)) << 8) | k.group);
|
||
}
|
||
};
|
||
|
||
// Returns the sum of all flat modifiers for a given op across all groups.
|
||
// (Callers that need per-group resolution can use getSpellFlatMods() directly.)
|
||
int32_t getSpellFlatMod(SpellModOp op) const {
|
||
int32_t total = 0;
|
||
for (const auto& [k, v] : spellFlatMods_)
|
||
if (k.op == op) total += v;
|
||
return total;
|
||
}
|
||
// Returns the sum of all pct modifiers for a given op across all groups (in %).
|
||
int32_t getSpellPctMod(SpellModOp op) const {
|
||
int32_t total = 0;
|
||
for (const auto& [k, v] : spellPctMods_)
|
||
if (k.op == op) total += v;
|
||
return total;
|
||
}
|
||
|
||
// Convenience: apply flat+pct modifier to a base value.
|
||
// result = (base + flatMod) * (1.0 + pctMod/100.0), clamped to >= 0.
|
||
static int32_t applySpellMod(int32_t base, int32_t flat, int32_t pct) {
|
||
int64_t v = static_cast<int64_t>(base) + flat;
|
||
if (pct != 0) v = v + (v * pct + 50) / 100; // round half-up
|
||
return static_cast<int32_t>(v < 0 ? 0 : v);
|
||
}
|
||
|
||
struct FactionStandingInit {
|
||
uint8_t flags = 0;
|
||
int32_t standing = 0;
|
||
};
|
||
// Faction flag bitmask constants (from Faction.dbc ReputationFlags / SMSG_INITIALIZE_FACTIONS)
|
||
static constexpr uint8_t FACTION_FLAG_VISIBLE = 0x01; // shown in reputation list
|
||
static constexpr uint8_t FACTION_FLAG_AT_WAR = 0x02; // player is at war
|
||
static constexpr uint8_t FACTION_FLAG_HIDDEN = 0x04; // never shown
|
||
static constexpr uint8_t FACTION_FLAG_INVISIBLE_FORCED = 0x08;
|
||
static constexpr uint8_t FACTION_FLAG_PEACE_FORCED = 0x10;
|
||
|
||
const std::vector<FactionStandingInit>& getInitialFactions() const { return initialFactions_; }
|
||
const std::unordered_map<uint32_t, int32_t>& getFactionStandings() const { return factionStandings_; }
|
||
|
||
// Returns true if the player has "at war" toggled for the faction at repListId
|
||
bool isFactionAtWar(uint32_t repListId) const {
|
||
if (repListId >= initialFactions_.size()) return false;
|
||
return (initialFactions_[repListId].flags & FACTION_FLAG_AT_WAR) != 0;
|
||
}
|
||
// Returns true if the faction is visible in the reputation list
|
||
bool isFactionVisible(uint32_t repListId) const {
|
||
if (repListId >= initialFactions_.size()) return false;
|
||
const uint8_t f = initialFactions_[repListId].flags;
|
||
if (f & FACTION_FLAG_HIDDEN) return false;
|
||
if (f & FACTION_FLAG_INVISIBLE_FORCED) return false;
|
||
return (f & FACTION_FLAG_VISIBLE) != 0;
|
||
}
|
||
// Returns the faction ID for a given repListId (0 if unknown)
|
||
uint32_t getFactionIdByRepListId(uint32_t repListId) const;
|
||
// Returns the repListId for a given faction ID (0xFFFFFFFF if not found)
|
||
uint32_t getRepListIdByFactionId(uint32_t factionId) const;
|
||
// Shaman totems (4 slots: 0=Earth, 1=Fire, 2=Water, 3=Air)
|
||
struct TotemSlot {
|
||
uint32_t spellId = 0;
|
||
uint32_t durationMs = 0;
|
||
std::chrono::steady_clock::time_point placedAt{};
|
||
bool active() const { return spellId != 0 && remainingMs() > 0; }
|
||
float remainingMs() const {
|
||
if (spellId == 0 || durationMs == 0) return 0.0f;
|
||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||
std::chrono::steady_clock::now() - placedAt).count();
|
||
float rem = static_cast<float>(durationMs) - static_cast<float>(elapsed);
|
||
return rem > 0.0f ? rem : 0.0f;
|
||
}
|
||
};
|
||
static constexpr int NUM_TOTEM_SLOTS = 4;
|
||
const TotemSlot& getTotemSlot(int slot) const {
|
||
static TotemSlot empty;
|
||
return (slot >= 0 && slot < NUM_TOTEM_SLOTS) ? activeTotemSlots_[slot] : empty;
|
||
}
|
||
|
||
const std::string& getFactionNamePublic(uint32_t factionId) const;
|
||
uint32_t getWatchedFactionId() const { return watchedFactionId_; }
|
||
void setWatchedFactionId(uint32_t id) { watchedFactionId_ = id; }
|
||
uint32_t getLastContactListMask() const { return lastContactListMask_; }
|
||
uint32_t getLastContactListCount() const { return lastContactListCount_; }
|
||
bool isServerMovementAllowed() const { return serverMovementAllowed_; }
|
||
|
||
// Quest giver status (! and ? markers)
|
||
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const {
|
||
auto it = npcQuestStatus_.find(guid);
|
||
return (it != npcQuestStatus_.end()) ? it->second : QuestGiverStatus::NONE;
|
||
}
|
||
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const { return npcQuestStatus_; }
|
||
|
||
// Charge callback — fires when player casts a charge spell toward target
|
||
// Parameters: targetGuid, targetX, targetY, targetZ (canonical WoW coordinates)
|
||
using ChargeCallback = std::function<void(uint64_t targetGuid, float x, float y, float z)>;
|
||
void setChargeCallback(ChargeCallback cb) { chargeCallback_ = std::move(cb); }
|
||
|
||
// Level-up callback — fires when the player gains a level (newLevel > 1)
|
||
using LevelUpCallback = std::function<void(uint32_t newLevel)>;
|
||
void setLevelUpCallback(LevelUpCallback cb) { levelUpCallback_ = std::move(cb); }
|
||
|
||
// Stat deltas from the last SMSG_LEVELUP_INFO (valid until next level-up)
|
||
struct LevelUpDeltas {
|
||
uint32_t hp = 0;
|
||
uint32_t mana = 0;
|
||
uint32_t str = 0, agi = 0, sta = 0, intel = 0, spi = 0;
|
||
};
|
||
const LevelUpDeltas& getLastLevelUpDeltas() const { return lastLevelUpDeltas_; }
|
||
|
||
// Temporary weapon enchant timers (from SMSG_ITEM_ENCHANT_TIME_UPDATE)
|
||
// Slot: 0=main-hand, 1=off-hand, 2=ranged. Value: expire time (steady_clock ms).
|
||
struct TempEnchantTimer {
|
||
uint32_t slot = 0;
|
||
uint64_t expireMs = 0; // std::chrono::steady_clock ms timestamp when it expires
|
||
};
|
||
const std::vector<TempEnchantTimer>& getTempEnchantTimers() const { return tempEnchantTimers_; }
|
||
// Returns remaining ms for a given slot, or 0 if absent/expired.
|
||
uint32_t getTempEnchantRemainingMs(uint32_t slot) const;
|
||
static constexpr const char* kTempEnchantSlotNames[] = { "Main Hand", "Off Hand", "Ranged" };
|
||
|
||
// ---- Readable text (books / scrolls / notes) ----
|
||
// Populated by handlePageTextQueryResponse(); multi-page items chain via nextPageId.
|
||
struct BookPage { uint32_t pageId = 0; std::string text; };
|
||
const std::vector<BookPage>& getBookPages() const { return bookPages_; }
|
||
bool hasBookOpen() const { return !bookPages_.empty(); }
|
||
void clearBook() { bookPages_.clear(); }
|
||
|
||
// Other player level-up callback — fires when another player gains a level
|
||
using OtherPlayerLevelUpCallback = std::function<void(uint64_t guid, uint32_t newLevel)>;
|
||
void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); }
|
||
|
||
// Achievement earned callback — fires when SMSG_ACHIEVEMENT_EARNED is received
|
||
using AchievementEarnedCallback = std::function<void(uint32_t achievementId, const std::string& name)>;
|
||
void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); }
|
||
const std::unordered_set<uint32_t>& getEarnedAchievements() const { return earnedAchievements_; }
|
||
|
||
// Title system — earned title bits and the currently displayed title
|
||
const std::unordered_set<uint32_t>& getKnownTitleBits() const { return knownTitleBits_; }
|
||
int32_t getChosenTitleBit() const { return chosenTitleBit_; }
|
||
/// Returns the formatted title string for a given bit (replaces %s with player name), or empty.
|
||
std::string getFormattedTitle(uint32_t bit) const;
|
||
/// Send CMSG_SET_TITLE to activate a title (bit >= 0) or clear it (bit = -1).
|
||
void sendSetTitle(int32_t bit);
|
||
|
||
// Area discovery callback — fires when SMSG_EXPLORATION_EXPERIENCE is received
|
||
using AreaDiscoveryCallback = std::function<void(const std::string& areaName, uint32_t xpGained)>;
|
||
void setAreaDiscoveryCallback(AreaDiscoveryCallback cb) { areaDiscoveryCallback_ = std::move(cb); }
|
||
|
||
// Quest objective progress callback — fires on SMSG_QUESTUPDATE_ADD_KILL / ADD_ITEM
|
||
// questTitle: name of the quest; objectiveName: creature/item name; current/required counts
|
||
using QuestProgressCallback = std::function<void(const std::string& questTitle,
|
||
const std::string& objectiveName,
|
||
uint32_t current, uint32_t required)>;
|
||
void setQuestProgressCallback(QuestProgressCallback cb) { questProgressCallback_ = std::move(cb); }
|
||
const std::unordered_map<uint32_t, uint64_t>& getCriteriaProgress() const { return criteriaProgress_; }
|
||
/// Returns the WoW PackedTime earn date for an achievement, or 0 if unknown.
|
||
uint32_t getAchievementDate(uint32_t id) const {
|
||
auto it = achievementDates_.find(id);
|
||
return (it != achievementDates_.end()) ? it->second : 0u;
|
||
}
|
||
/// Returns the name of an achievement by ID, or empty string if unknown.
|
||
const std::string& getAchievementName(uint32_t id) const {
|
||
auto it = achievementNameCache_.find(id);
|
||
if (it != achievementNameCache_.end()) return it->second;
|
||
static const std::string kEmpty;
|
||
return kEmpty;
|
||
}
|
||
/// Returns the description of an achievement by ID, or empty string if unknown.
|
||
const std::string& getAchievementDescription(uint32_t id) const {
|
||
auto it = achievementDescCache_.find(id);
|
||
if (it != achievementDescCache_.end()) return it->second;
|
||
static const std::string kEmpty;
|
||
return kEmpty;
|
||
}
|
||
/// Returns the point value of an achievement by ID, or 0 if unknown.
|
||
uint32_t getAchievementPoints(uint32_t id) const {
|
||
auto it = achievementPointsCache_.find(id);
|
||
return (it != achievementPointsCache_.end()) ? it->second : 0u;
|
||
}
|
||
/// Returns the set of achievement IDs earned by an inspected player (via SMSG_RESPOND_INSPECT_ACHIEVEMENTS).
|
||
/// Returns nullptr if no inspect data is available for the given GUID.
|
||
const std::unordered_set<uint32_t>* getInspectedPlayerAchievements(uint64_t guid) const {
|
||
auto it = inspectedPlayerAchievements_.find(guid);
|
||
return (it != inspectedPlayerAchievements_.end()) ? &it->second : nullptr;
|
||
}
|
||
|
||
// Server-triggered music callback — fires when SMSG_PLAY_MUSIC is received.
|
||
// The soundId corresponds to a SoundEntries.dbc record. The receiver is
|
||
// responsible for looking up the file path and forwarding to MusicManager.
|
||
using PlayMusicCallback = std::function<void(uint32_t soundId)>;
|
||
void setPlayMusicCallback(PlayMusicCallback cb) { playMusicCallback_ = std::move(cb); }
|
||
|
||
// Server-triggered 2-D sound effect callback — fires when SMSG_PLAY_SOUND is received.
|
||
// The soundId corresponds to a SoundEntries.dbc record.
|
||
using PlaySoundCallback = std::function<void(uint32_t soundId)>;
|
||
void setPlaySoundCallback(PlaySoundCallback cb) { playSoundCallback_ = std::move(cb); }
|
||
|
||
// Server-triggered 3-D positional sound callback — fires for SMSG_PLAY_OBJECT_SOUND and
|
||
// SMSG_PLAY_SPELL_IMPACT. Includes sourceGuid so the receiver can look up world position.
|
||
using PlayPositionalSoundCallback = std::function<void(uint32_t soundId, uint64_t sourceGuid)>;
|
||
void setPlayPositionalSoundCallback(PlayPositionalSoundCallback cb) { playPositionalSoundCallback_ = std::move(cb); }
|
||
|
||
// UI error frame: prominent on-screen error messages (spell can't be cast, etc.)
|
||
using UIErrorCallback = std::function<void(const std::string& msg)>;
|
||
void setUIErrorCallback(UIErrorCallback cb) { uiErrorCallback_ = std::move(cb); }
|
||
void addUIError(const std::string& msg) { if (uiErrorCallback_) uiErrorCallback_(msg); }
|
||
|
||
// Reputation change toast: factionName, delta, new standing
|
||
using RepChangeCallback = std::function<void(const std::string& factionName, int32_t delta, int32_t standing)>;
|
||
void setRepChangeCallback(RepChangeCallback cb) { repChangeCallback_ = std::move(cb); }
|
||
|
||
// PvP honor credit callback (honorable kill or BG reward)
|
||
using PvpHonorCallback = std::function<void(uint32_t honorAmount, uint64_t victimGuid, uint32_t victimRank)>;
|
||
void setPvpHonorCallback(PvpHonorCallback cb) { pvpHonorCallback_ = std::move(cb); }
|
||
|
||
// Item looted / received callback (SMSG_ITEM_PUSH_RESULT when showInChat is set)
|
||
using ItemLootCallback = std::function<void(uint32_t itemId, uint32_t count, uint32_t quality, const std::string& name)>;
|
||
void setItemLootCallback(ItemLootCallback cb) { itemLootCallback_ = std::move(cb); }
|
||
|
||
// Quest turn-in completion callback
|
||
using QuestCompleteCallback = std::function<void(uint32_t questId, const std::string& questTitle)>;
|
||
void setQuestCompleteCallback(QuestCompleteCallback cb) { questCompleteCallback_ = std::move(cb); }
|
||
|
||
// Mount state
|
||
using MountCallback = std::function<void(uint32_t mountDisplayId)>; // 0 = dismount
|
||
void setMountCallback(MountCallback cb) { mountCallback_ = std::move(cb); }
|
||
|
||
// Taxi terrain precaching callback
|
||
using TaxiPrecacheCallback = std::function<void(const std::vector<glm::vec3>&)>;
|
||
void setTaxiPrecacheCallback(TaxiPrecacheCallback cb) { taxiPrecacheCallback_ = std::move(cb); }
|
||
|
||
// Taxi orientation callback (for mount rotation: yaw, pitch, roll in radians)
|
||
using TaxiOrientationCallback = std::function<void(float yaw, float pitch, float roll)>;
|
||
void setTaxiOrientationCallback(TaxiOrientationCallback cb) { taxiOrientationCallback_ = std::move(cb); }
|
||
|
||
// Callback for when taxi flight is about to start (after mounting delay, before movement begins)
|
||
using TaxiFlightStartCallback = std::function<void()>;
|
||
void setTaxiFlightStartCallback(TaxiFlightStartCallback cb) { taxiFlightStartCallback_ = std::move(cb); }
|
||
|
||
// Callback fired when server sends SMSG_OPEN_LFG_DUNGEON_FINDER (open dungeon finder UI)
|
||
using OpenLfgCallback = std::function<void()>;
|
||
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 isHostileFactionPublic(uint32_t factionTemplateId) const { return isHostileFaction(factionTemplateId); }
|
||
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_; }
|
||
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;
|
||
}
|
||
// Set the character pitch angle (radians) for movement packets (flight / swimming).
|
||
// Positive = nose up, negative = nose down.
|
||
void setMovementPitch(float radians) { movementInfo.pitch = radians; }
|
||
void dismount();
|
||
|
||
// 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;
|
||
}
|
||
|
||
// Vehicle (WotLK)
|
||
bool isInVehicle() const { return vehicleId_ != 0; }
|
||
uint32_t getVehicleId() const { return vehicleId_; }
|
||
void sendRequestVehicleExit();
|
||
|
||
// Vendor
|
||
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);
|
||
struct BuybackItem {
|
||
uint64_t itemGuid = 0;
|
||
ItemDef item;
|
||
uint32_t count = 1;
|
||
};
|
||
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);
|
||
// CMSG_OPEN_ITEM — for locked containers (lockboxes); server checks keyring automatically
|
||
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 useItemById(uint32_t itemId);
|
||
bool isVendorWindowOpen() const { return vendorWindowOpen; }
|
||
const ListInventoryData& getVendorItems() const { return currentVendorItems; }
|
||
void setVendorCanRepair(bool v) { currentVendorItems.canRepair = v; }
|
||
|
||
// Mail
|
||
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, uint32_t money, uint32_t cod = 0);
|
||
|
||
// Mail attachments (max 12 per WotLK)
|
||
static constexpr int MAIL_MAX_ATTACHMENTS = 12;
|
||
struct MailAttachSlot {
|
||
uint64_t itemGuid = 0;
|
||
game::ItemDef item;
|
||
uint8_t srcBag = 0xFF; // source container for return
|
||
uint8_t srcSlot = 0;
|
||
bool occupied() const { return itemGuid != 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
|
||
bool isTrainerWindowOpen() const { return trainerWindowOpen_; }
|
||
const TrainerListData& getTrainerSpells() const { return currentTrainerList_; }
|
||
void trainSpell(uint32_t spellId);
|
||
void closeTrainer();
|
||
const std::string& getSpellName(uint32_t spellId) const;
|
||
const std::string& getSpellRank(uint32_t spellId) const;
|
||
/// Returns the tooltip/description text from Spell.dbc (empty if unknown or has no text).
|
||
const std::string& getSpellDescription(uint32_t spellId) const;
|
||
const std::string& getSkillLineName(uint32_t spellId) const;
|
||
/// Returns the DispelType for a spell (0=none,1=magic,2=curse,3=disease,4=poison,5+=other)
|
||
uint8_t getSpellDispelType(uint32_t spellId) const;
|
||
/// Returns true if the spell can be interrupted by abilities like Kick/Counterspell.
|
||
/// False for spells with SPELL_ATTR_EX_NOT_INTERRUPTIBLE (attrEx bit 4 = 0x10).
|
||
bool isSpellInterruptible(uint32_t spellId) const;
|
||
/// Returns the school bitmask for the spell from Spell.dbc
|
||
/// (0x01=Physical, 0x02=Holy, 0x04=Fire, 0x08=Nature, 0x10=Frost, 0x20=Shadow, 0x40=Arcane).
|
||
/// Returns 0 if unknown.
|
||
uint32_t getSpellSchoolMask(uint32_t spellId) const;
|
||
|
||
struct TrainerTab {
|
||
std::string name;
|
||
std::vector<const TrainerSpell*> spells;
|
||
};
|
||
const std::vector<TrainerTab>& getTrainerTabs() const { return trainerTabs_; }
|
||
const ItemQueryResponseData* getItemInfo(uint32_t itemId) const {
|
||
auto it = itemInfoCache_.find(itemId);
|
||
return (it != itemInfoCache_.end()) ? &it->second : nullptr;
|
||
}
|
||
// Request item info from server if not already cached/pending
|
||
void ensureItemInfo(uint32_t entry) {
|
||
if (entry == 0 || itemInfoCache_.count(entry) || pendingItemQueries_.count(entry)) return;
|
||
queryItemInfo(entry, 0);
|
||
}
|
||
uint64_t getBackpackItemGuid(int index) const {
|
||
if (index < 0 || index >= static_cast<int>(backpackSlotGuids_.size())) return 0;
|
||
return backpackSlotGuids_[index];
|
||
}
|
||
uint64_t getEquipSlotGuid(int slot) const {
|
||
if (slot < 0 || slot >= static_cast<int>(equipSlotGuids_.size())) return 0;
|
||
return equipSlotGuids_[slot];
|
||
}
|
||
// Returns the permanent and temporary enchant IDs for an item by GUID (0 if unknown).
|
||
std::pair<uint32_t, uint32_t> getItemEnchantIds(uint64_t guid) const {
|
||
auto it = onlineItems_.find(guid);
|
||
if (it == onlineItems_.end()) return {0, 0};
|
||
return {it->second.permanentEnchantId, it->second.temporaryEnchantId};
|
||
}
|
||
// Returns the socket gem enchant IDs (3 slots; 0 = empty socket) for an item by GUID.
|
||
std::array<uint32_t, 3> getItemSocketEnchantIds(uint64_t guid) const {
|
||
auto it = onlineItems_.find(guid);
|
||
if (it == onlineItems_.end()) return {};
|
||
return it->second.socketEnchantIds;
|
||
}
|
||
uint64_t getVendorGuid() const { return currentVendorItems.vendorGuid; }
|
||
|
||
/**
|
||
* Set callbacks
|
||
*/
|
||
void setOnSuccess(WorldConnectSuccessCallback callback) { onSuccess = callback; }
|
||
void setOnFailure(WorldConnectFailureCallback callback) { onFailure = callback; }
|
||
|
||
/**
|
||
* Update - call regularly (e.g., each frame)
|
||
*
|
||
* @param deltaTime Time since last update in seconds
|
||
*/
|
||
void update(float deltaTime);
|
||
|
||
/**
|
||
* Reset DBC-backed caches so they reload from new expansion data.
|
||
* Called by Application when the expansion profile changes.
|
||
*/
|
||
void resetDbcCaches();
|
||
|
||
private:
|
||
void autoTargetAttacker(uint64_t attackerGuid);
|
||
|
||
/**
|
||
* Handle incoming packet from world server
|
||
*/
|
||
void handlePacket(network::Packet& packet);
|
||
void enqueueIncomingPacket(const network::Packet& packet);
|
||
void enqueueIncomingPacketFront(network::Packet&& packet);
|
||
void processQueuedIncomingPackets();
|
||
void enqueueUpdateObjectWork(UpdateObjectData&& data);
|
||
void processPendingUpdateObjectWork(const std::chrono::steady_clock::time_point& start,
|
||
float budgetMs);
|
||
void processOutOfRangeObjects(const std::vector<uint64_t>& guids);
|
||
void applyUpdateObjectBlock(const UpdateBlock& block, bool& newItemCreated);
|
||
void finalizeUpdateObjectBatch(bool newItemCreated);
|
||
|
||
/**
|
||
* Handle SMSG_AUTH_CHALLENGE from server
|
||
*/
|
||
void handleAuthChallenge(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_AUTH_RESPONSE from server
|
||
*/
|
||
void handleAuthResponse(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_CHAR_ENUM from server
|
||
*/
|
||
void handleCharEnum(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_CHARACTER_LOGIN_FAILED from server
|
||
*/
|
||
void handleCharLoginFailed(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_LOGIN_VERIFY_WORLD from server
|
||
*/
|
||
void handleLoginVerifyWorld(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_CLIENTCACHE_VERSION from server
|
||
*/
|
||
void handleClientCacheVersion(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_TUTORIAL_FLAGS from server
|
||
*/
|
||
void handleTutorialFlags(network::Packet& packet);
|
||
|
||
/**
|
||
* 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
|
||
*/
|
||
void handleAccountDataTimes(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_MOTD from server
|
||
*/
|
||
void handleMotd(network::Packet& packet);
|
||
|
||
/** Handle SMSG_NOTIFICATION (vanilla/classic server notification string) */
|
||
void handleNotification(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_PONG from server
|
||
*/
|
||
void handlePong(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_UPDATE_OBJECT from server
|
||
*/
|
||
void handleUpdateObject(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_COMPRESSED_UPDATE_OBJECT from server
|
||
*/
|
||
void handleCompressedUpdateObject(network::Packet& packet);
|
||
|
||
/**
|
||
* Handle SMSG_DESTROY_OBJECT from server
|
||
*/
|
||
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);
|
||
void handleGameObjectQueryResponse(network::Packet& packet);
|
||
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();
|
||
void updateOtherPlayerVisibleItems(uint64_t guid, const std::map<uint16_t, uint32_t>& fields);
|
||
void emitOtherPlayerEquipment(uint64_t guid);
|
||
void emitAllOtherPlayerEquipment();
|
||
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);
|
||
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);
|
||
|
||
// ---- Equipment set handler ----
|
||
void handleEquipmentSetList(network::Packet& packet);
|
||
void handleUpdateAuraDuration(uint8_t slot, uint32_t durationMs);
|
||
void handleSetForcedReactions(network::Packet& packet);
|
||
|
||
// ---- 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 handlePetSpells(network::Packet& packet);
|
||
void handleTurnInPetitionResults(network::Packet& packet);
|
||
|
||
// ---- Character creation handler ----
|
||
void handleCharCreateResponse(network::Packet& packet);
|
||
|
||
// ---- XP handler ----
|
||
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;
|
||
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);
|
||
bool shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId);
|
||
void addSystemChatMessage(const std::string& message);
|
||
|
||
/**
|
||
* Send CMSG_PING to server (heartbeat)
|
||
*/
|
||
void sendPing();
|
||
|
||
/**
|
||
* Send CMSG_AUTH_SESSION to server
|
||
*/
|
||
void sendAuthSession();
|
||
|
||
/**
|
||
* Generate random client seed
|
||
*/
|
||
uint32_t generateClientSeed();
|
||
|
||
/**
|
||
* Change state with logging
|
||
*/
|
||
void setState(WorldState newState);
|
||
|
||
/**
|
||
* Fail connection with reason
|
||
*/
|
||
void fail(const std::string& reason);
|
||
void updateAttachedTransportChildren(float deltaTime);
|
||
void setTransportAttachment(uint64_t childGuid, ObjectType type, uint64_t transportGuid,
|
||
const glm::vec3& localOffset, bool hasLocalOrientation,
|
||
float localOrientation);
|
||
void clearTransportAttachment(uint64_t childGuid);
|
||
|
||
// Opcode translation table (expansion-specific wire ↔ logical mapping)
|
||
OpcodeTable opcodeTable_;
|
||
|
||
// Update field table (expansion-specific field index mapping)
|
||
UpdateFieldTable updateFieldTable_;
|
||
|
||
// Packet parsers (expansion-specific binary format handling)
|
||
std::unique_ptr<PacketParsers> packetParsers_;
|
||
|
||
// Network
|
||
std::unique_ptr<network::WorldSocket> socket;
|
||
std::deque<network::Packet> pendingIncomingPackets_;
|
||
struct PendingUpdateObjectWork {
|
||
UpdateObjectData data;
|
||
size_t nextBlockIndex = 0;
|
||
bool outOfRangeProcessed = false;
|
||
bool newItemCreated = false;
|
||
};
|
||
std::deque<PendingUpdateObjectWork> pendingUpdateObjectWork_;
|
||
|
||
// State
|
||
WorldState state = WorldState::DISCONNECTED;
|
||
|
||
// Authentication data
|
||
std::vector<uint8_t> sessionKey; // 40-byte session key from auth server
|
||
std::string accountName; // Account name
|
||
uint32_t build = 12340; // Client build (3.3.5a)
|
||
uint32_t realmId_ = 0; // Realm ID from auth REALM_LIST (used in WotLK AUTH_SESSION)
|
||
uint32_t clientSeed = 0; // Random seed generated by client
|
||
uint32_t serverSeed = 0; // Seed from SMSG_AUTH_CHALLENGE
|
||
|
||
// Characters
|
||
std::vector<Character> characters; // Character list from SMSG_CHAR_ENUM
|
||
|
||
// Movement
|
||
MovementInfo movementInfo; // Current player movement state
|
||
uint32_t movementTime = 0; // Movement timestamp counter
|
||
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 for movement packet correctness.
|
||
// fallTime must be the elapsed ms since the FALLING flag was set; the server
|
||
// uses it for fall-damage calculations and anti-cheat validation.
|
||
bool isFalling_ = false;
|
||
uint32_t fallStartMs_ = 0; // movementInfo.time value when FALLING started
|
||
|
||
// Inventory
|
||
Inventory inventory;
|
||
|
||
// 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
|
||
ChatBubbleCallback chatBubbleCallback_;
|
||
EmoteAnimCallback emoteAnimCallback_;
|
||
|
||
// Targeting
|
||
uint64_t targetGuid = 0;
|
||
uint64_t focusGuid = 0; // Focus target
|
||
uint64_t lastTargetGuid = 0; // Previous target
|
||
uint64_t mouseoverGuid_ = 0; // Set each frame by nameplate renderer
|
||
std::vector<uint64_t> tabCycleList;
|
||
int tabCycleIndex = -1;
|
||
bool tabCycleStale = true;
|
||
|
||
// Heartbeat
|
||
uint32_t pingSequence = 0; // Ping sequence number (increments)
|
||
float timeSinceLastPing = 0.0f; // Time since last ping sent (seconds)
|
||
float pingInterval = 30.0f; // Ping interval (30 seconds)
|
||
float timeSinceLastMoveHeartbeat_ = 0.0f; // Periodic movement heartbeat to keep server position synced
|
||
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;
|
||
uint32_t lastLatency = 0; // Last measured latency (milliseconds)
|
||
std::chrono::steady_clock::time_point pingTimestamp_; // Time CMSG_PING was sent
|
||
|
||
// Player GUID and map
|
||
uint64_t playerGuid = 0;
|
||
uint32_t currentMapId_ = 0;
|
||
bool hasHomeBind_ = false;
|
||
uint32_t homeBindMapId_ = 0;
|
||
uint32_t homeBindZoneId_ = 0;
|
||
glm::vec3 homeBindPos_{0.0f};
|
||
|
||
// ---- Phase 1: Name caches ----
|
||
std::unordered_map<uint64_t, std::string> playerNameCache;
|
||
std::unordered_set<uint64_t> pendingNameQueries;
|
||
std::unordered_map<uint32_t, CreatureQueryResponseData> creatureInfoCache;
|
||
std::unordered_set<uint32_t> pendingCreatureQueries;
|
||
std::unordered_map<uint32_t, GameObjectQueryResponseData> gameObjectInfoCache_;
|
||
std::unordered_set<uint32_t> pendingGameObjectQueries_;
|
||
|
||
// ---- Friend/contact list cache ----
|
||
std::unordered_map<std::string, uint64_t> friendsCache; // name -> guid
|
||
std::unordered_set<uint64_t> friendGuids_; // all known friend GUIDs (for name backfill)
|
||
uint32_t lastContactListMask_ = 0;
|
||
uint32_t lastContactListCount_ = 0;
|
||
std::vector<ContactEntry> contacts_; // structured contact list (friends + ignores)
|
||
|
||
// ---- World state and faction initialization snapshots ----
|
||
uint32_t worldStateMapId_ = 0;
|
||
uint32_t worldStateZoneId_ = 0;
|
||
std::unordered_map<uint32_t, uint32_t> worldStates_;
|
||
std::vector<FactionStandingInit> initialFactions_;
|
||
|
||
// ---- Ignore list cache ----
|
||
std::unordered_map<std::string, uint64_t> ignoreCache; // name -> guid
|
||
|
||
// ---- Logout state ----
|
||
bool loggingOut_ = false;
|
||
float logoutCountdown_ = 0.0f; // seconds remaining before server logs us out (0 = instant/done)
|
||
|
||
// ---- Display state ----
|
||
bool helmVisible_ = true;
|
||
bool cloakVisible_ = true;
|
||
uint8_t standState_ = 0; // 0=stand, 1=sit, ..., 7=dead, 8=kneel (server-confirmed)
|
||
|
||
// ---- Follow state ----
|
||
uint64_t followTargetGuid_ = 0;
|
||
|
||
// ---- AFK/DND status ----
|
||
bool afkStatus_ = false;
|
||
bool dndStatus_ = false;
|
||
std::string afkMessage_;
|
||
std::string dndMessage_;
|
||
std::string lastWhisperSender_;
|
||
|
||
// ---- Online item tracking ----
|
||
struct OnlineItemInfo {
|
||
uint32_t entry = 0;
|
||
uint32_t stackCount = 1;
|
||
uint32_t curDurability = 0;
|
||
uint32_t maxDurability = 0;
|
||
uint32_t permanentEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 0 (enchanting)
|
||
uint32_t temporaryEnchantId = 0; // ITEM_ENCHANTMENT_SLOT 1 (sharpening stones, poisons)
|
||
std::array<uint32_t, 3> socketEnchantIds{}; // ITEM_ENCHANTMENT_SLOT 2-4 (gems)
|
||
};
|
||
std::unordered_map<uint64_t, OnlineItemInfo> onlineItems_;
|
||
std::unordered_map<uint32_t, ItemQueryResponseData> itemInfoCache_;
|
||
std::unordered_set<uint32_t> pendingItemQueries_;
|
||
|
||
// Deferred SMSG_ITEM_PUSH_RESULT notifications for items whose info wasn't
|
||
// cached at arrival time; emitted once the query response arrives.
|
||
struct PendingItemPushNotif {
|
||
uint32_t itemId = 0;
|
||
uint32_t count = 1;
|
||
};
|
||
std::vector<PendingItemPushNotif> pendingItemPushNotifs_;
|
||
std::array<uint64_t, 23> equipSlotGuids_{};
|
||
std::array<uint64_t, 16> backpackSlotGuids_{};
|
||
std::array<uint64_t, 32> keyringSlotGuids_{};
|
||
// Container (bag) contents: containerGuid -> array of item GUIDs per slot
|
||
struct ContainerInfo {
|
||
uint32_t numSlots = 0;
|
||
std::array<uint64_t, 36> slotGuids{}; // max 36 slots
|
||
};
|
||
std::unordered_map<uint64_t, ContainerInfo> containerContents_;
|
||
int invSlotBase_ = -1;
|
||
int packSlotBase_ = -1;
|
||
std::map<uint16_t, uint32_t> lastPlayerFields_;
|
||
bool onlineEquipDirty_ = false;
|
||
std::array<uint32_t, 19> lastEquipDisplayIds_{};
|
||
|
||
// Visible equipment for other players: detect the update-field layout (base + stride)
|
||
// using the local player's own equipped items, then decode other players by index.
|
||
// Default to known WotLK 3.3.5a layout: UNIT_END(148) + 0x0088 = 284, stride 2.
|
||
// The heuristic in maybeDetectVisibleItemLayout() can still override if needed.
|
||
int visibleItemEntryBase_ = 284;
|
||
int visibleItemStride_ = 2;
|
||
bool visibleItemLayoutVerified_ = false; // true once heuristic confirms/overrides default
|
||
std::unordered_map<uint64_t, std::array<uint32_t, 19>> otherPlayerVisibleItemEntries_;
|
||
std::unordered_set<uint64_t> otherPlayerVisibleDirty_;
|
||
std::unordered_map<uint64_t, uint32_t> otherPlayerMoveTimeMs_;
|
||
std::unordered_map<uint64_t, float> otherPlayerSmoothedIntervalMs_; // EMA of packet intervals
|
||
|
||
// Inspect fallback (when visible item fields are missing/unreliable)
|
||
std::unordered_map<uint64_t, std::array<uint32_t, 19>> inspectedPlayerItemEntries_;
|
||
InspectResult inspectResult_; // most-recently received inspect response
|
||
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_;
|
||
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_;
|
||
KnockBackCallback knockBackCallback_;
|
||
CameraShakeCallback cameraShakeCallback_;
|
||
UnstuckCallback unstuckCallback_;
|
||
UnstuckCallback unstuckGyCallback_;
|
||
UnstuckCallback unstuckHearthCallback_;
|
||
BindPointCallback bindPointCallback_;
|
||
HearthstonePreloadCallback hearthstonePreloadCallback_;
|
||
CreatureSpawnCallback creatureSpawnCallback_;
|
||
CreatureDespawnCallback creatureDespawnCallback_;
|
||
PlayerSpawnCallback playerSpawnCallback_;
|
||
PlayerDespawnCallback playerDespawnCallback_;
|
||
PlayerEquipmentCallback playerEquipmentCallback_;
|
||
CreatureMoveCallback creatureMoveCallback_;
|
||
TransportMoveCallback transportMoveCallback_;
|
||
TransportSpawnCallback transportSpawnCallback_;
|
||
GameObjectSpawnCallback gameObjectSpawnCallback_;
|
||
GameObjectMoveCallback gameObjectMoveCallback_;
|
||
GameObjectDespawnCallback gameObjectDespawnCallback_;
|
||
GameObjectCustomAnimCallback gameObjectCustomAnimCallback_;
|
||
|
||
// Transport tracking
|
||
struct TransportAttachment {
|
||
ObjectType type = ObjectType::OBJECT;
|
||
uint64_t transportGuid = 0;
|
||
glm::vec3 localOffset{0.0f};
|
||
float localOrientation = 0.0f;
|
||
bool hasLocalOrientation = false;
|
||
};
|
||
std::unordered_map<uint64_t, TransportAttachment> transportAttachments_;
|
||
std::unordered_set<uint64_t> transportGuids_; // GUIDs of known transport GameObjects
|
||
std::unordered_set<uint64_t> serverUpdatedTransportGuids_;
|
||
uint64_t playerTransportGuid_ = 0; // Transport the player is riding (0 = none)
|
||
glm::vec3 playerTransportOffset_ = glm::vec3(0.0f); // Player offset on transport
|
||
uint64_t playerTransportStickyGuid_ = 0; // Last transport player was on (temporary retention)
|
||
float playerTransportStickyTimer_ = 0.0f; // Seconds to keep sticky transport alive after transient clears
|
||
std::unique_ptr<TransportManager> transportManager_; // Transport movement manager
|
||
std::unordered_set<uint32_t> knownSpells;
|
||
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
|
||
uint32_t weaponProficiency_ = 0; // bitmask from SMSG_SET_PROFICIENCY itemClass=2
|
||
uint32_t armorProficiency_ = 0; // bitmask from SMSG_SET_PROFICIENCY itemClass=4
|
||
std::vector<MinimapPing> minimapPings_;
|
||
uint8_t castCount = 0;
|
||
bool casting = false;
|
||
bool castIsChannel = false;
|
||
uint32_t currentCastSpellId = 0;
|
||
float castTimeRemaining = 0.0f;
|
||
// Repeat-craft queue: re-cast the same profession spell N more times after current cast finishes
|
||
uint32_t craftQueueSpellId_ = 0;
|
||
int craftQueueRemaining_ = 0;
|
||
// Spell queue: next spell to cast within the 400ms window before current cast ends
|
||
uint32_t queuedSpellId_ = 0;
|
||
uint64_t queuedSpellTarget_ = 0;
|
||
// Per-unit cast state (keyed by GUID, populated from SMSG_SPELL_START)
|
||
std::unordered_map<uint64_t, UnitCastState> unitCastStates_;
|
||
uint64_t pendingGameObjectInteractGuid_ = 0;
|
||
|
||
// Talents (dual-spec support)
|
||
uint8_t activeTalentSpec_ = 0; // Currently active spec (0 or 1)
|
||
uint8_t unspentTalentPoints_[2] = {0, 0}; // Unspent points per spec
|
||
std::unordered_map<uint32_t, uint8_t> learnedTalents_[2]; // Learned talents per spec
|
||
std::array<std::array<uint16_t, MAX_GLYPH_SLOTS>, 2> learnedGlyphs_{}; // Glyphs per spec
|
||
std::unordered_map<uint32_t, TalentEntry> talentCache_; // talentId -> entry
|
||
std::unordered_map<uint32_t, TalentTabEntry> talentTabCache_; // tabId -> entry
|
||
bool talentDbcLoaded_ = false;
|
||
bool talentsInitialized_ = false; // Reset on world entry; guards first-spec selection
|
||
|
||
// ---- Area trigger detection ----
|
||
struct AreaTriggerEntry {
|
||
uint32_t id = 0;
|
||
uint32_t mapId = 0;
|
||
float x = 0, y = 0, z = 0; // canonical WoW coords (converted from DBC)
|
||
float radius = 0;
|
||
float boxLength = 0, boxWidth = 0, boxHeight = 0;
|
||
float boxYaw = 0;
|
||
};
|
||
bool areaTriggerDbcLoaded_ = false;
|
||
std::vector<AreaTriggerEntry> areaTriggers_;
|
||
std::unordered_set<uint32_t> activeAreaTriggers_; // triggers player is currently inside
|
||
float areaTriggerCheckTimer_ = 0.0f;
|
||
bool areaTriggerSuppressFirst_ = false; // suppress first check after map transfer
|
||
|
||
float castTimeTotal = 0.0f;
|
||
std::array<ActionBarSlot, ACTION_BAR_SLOTS> actionBar{};
|
||
std::unordered_map<uint32_t, std::string> macros_; // client-side macro text (persisted in char config)
|
||
std::vector<AuraSlot> playerAuras;
|
||
std::vector<AuraSlot> targetAuras;
|
||
std::unordered_map<uint64_t, std::vector<AuraSlot>> unitAurasCache_; // per-unit aura cache
|
||
uint64_t petGuid_ = 0;
|
||
uint32_t petActionSlots_[10] = {}; // SMSG_PET_SPELLS action bar (10 slots)
|
||
uint8_t petCommand_ = 1; // 0=stay,1=follow,2=attack,3=dismiss
|
||
uint8_t petReact_ = 1; // 0=passive,1=defensive,2=aggressive
|
||
bool petRenameablePending_ = false; // set by SMSG_PET_RENAMEABLE, consumed by UI
|
||
std::vector<uint32_t> petSpellList_; // known pet spells
|
||
std::unordered_set<uint32_t> petAutocastSpells_; // spells with autocast on
|
||
|
||
// ---- Pet Stable ----
|
||
bool stableWindowOpen_ = false;
|
||
uint64_t stableMasterGuid_ = 0;
|
||
uint8_t stableNumSlots_ = 0;
|
||
std::vector<StabledPet> stabledPets_;
|
||
void handleListStabledPets(network::Packet& packet);
|
||
|
||
// ---- Battleground queue state ----
|
||
std::array<BgQueueSlot, 3> bgQueues_{};
|
||
|
||
// ---- Available battleground list (SMSG_BATTLEFIELD_LIST) ----
|
||
std::vector<AvailableBgInfo> availableBgs_;
|
||
void handleBattlefieldList(network::Packet& packet);
|
||
|
||
// Instance difficulty
|
||
uint32_t instanceDifficulty_ = 0;
|
||
bool instanceIsHeroic_ = false;
|
||
|
||
// Raid target markers (icon 0-7 -> guid; 0 = empty slot)
|
||
std::array<uint64_t, kRaidMarkCount> raidTargetGuids_ = {};
|
||
|
||
// Mirror timers (0=fatigue, 1=breath, 2=feigndeath)
|
||
MirrorTimer mirrorTimers_[3];
|
||
|
||
// Combo points (rogues/druids)
|
||
uint8_t comboPoints_ = 0;
|
||
uint64_t comboTarget_ = 0;
|
||
|
||
// Instance / raid lockouts
|
||
std::vector<InstanceLockout> instanceLockouts_;
|
||
|
||
// Arena team stats (indexed by team slot, updated by SMSG_ARENA_TEAM_STATS)
|
||
std::vector<ArenaTeamStats> arenaTeamStats_;
|
||
// Arena team rosters (updated by SMSG_ARENA_TEAM_ROSTER)
|
||
std::vector<ArenaTeamRoster> arenaTeamRosters_;
|
||
|
||
// BG scoreboard (MSG_PVP_LOG_DATA)
|
||
BgScoreboardData bgScoreboard_;
|
||
|
||
// BG flag carrier / player positions (MSG_BATTLEGROUND_PLAYER_POSITIONS)
|
||
std::vector<BgPlayerPosition> bgPlayerPositions_;
|
||
|
||
// Instance encounter boss units (slots 0-4 from SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
||
std::array<uint64_t, kMaxEncounterSlots> encounterUnitGuids_ = {}; // 0 = empty slot
|
||
|
||
// LFG / Dungeon Finder state
|
||
LfgState lfgState_ = LfgState::None;
|
||
uint32_t lfgDungeonId_ = 0; // current dungeon entry
|
||
uint32_t lfgProposalId_ = 0; // pending proposal id (0 = none)
|
||
int32_t lfgAvgWaitSec_ = -1; // estimated wait, -1=unknown
|
||
uint32_t lfgTimeInQueueMs_= 0; // ms already in queue
|
||
uint32_t lfgBootVotes_ = 0; // current boot-yes votes
|
||
uint32_t lfgBootTotal_ = 0; // total votes cast
|
||
uint32_t lfgBootTimeLeft_ = 0; // seconds remaining
|
||
uint32_t lfgBootNeeded_ = 0; // votes needed to kick
|
||
std::string lfgBootTargetName_; // name of player being voted on
|
||
std::string lfgBootReason_; // reason given for kick
|
||
|
||
// Ready check state
|
||
bool pendingReadyCheck_ = false;
|
||
uint32_t readyCheckReadyCount_ = 0;
|
||
uint32_t readyCheckNotReadyCount_ = 0;
|
||
std::string readyCheckInitiator_;
|
||
std::vector<ReadyCheckResult> readyCheckResults_; // per-player status live during check
|
||
|
||
// Faction standings (factionId → absolute standing value)
|
||
std::unordered_map<uint32_t, int32_t> factionStandings_;
|
||
// Faction name cache (factionId → name), populated lazily from Faction.dbc
|
||
std::unordered_map<uint32_t, std::string> factionNameCache_;
|
||
// repListId → factionId mapping (populated with factionNameCache)
|
||
std::unordered_map<uint32_t, uint32_t> factionRepListToId_;
|
||
// factionId → repListId reverse mapping
|
||
std::unordered_map<uint32_t, uint32_t> factionIdToRepList_;
|
||
bool factionNameCacheLoaded_ = false;
|
||
void loadFactionNameCache();
|
||
std::string getFactionName(uint32_t factionId) const;
|
||
|
||
// ---- Phase 4: Group ----
|
||
GroupListData partyData;
|
||
bool pendingGroupInvite = false;
|
||
std::string pendingInviterName;
|
||
|
||
// Item text state
|
||
bool itemTextOpen_ = false;
|
||
std::string itemText_;
|
||
|
||
// Shared quest state
|
||
bool pendingSharedQuest_ = false;
|
||
uint32_t sharedQuestId_ = 0;
|
||
std::string sharedQuestTitle_;
|
||
std::string sharedQuestSharerName_;
|
||
uint64_t sharedQuestSharerGuid_ = 0;
|
||
|
||
// Summon state
|
||
bool pendingSummonRequest_ = false;
|
||
uint64_t summonerGuid_ = 0;
|
||
std::string summonerName_;
|
||
float summonTimeoutSec_ = 0.0f;
|
||
uint32_t totalTimePlayed_ = 0;
|
||
uint32_t levelTimePlayed_ = 0;
|
||
|
||
// Who results (last SMSG_WHO response)
|
||
std::vector<WhoEntry> whoResults_;
|
||
uint32_t whoOnlineCount_ = 0;
|
||
|
||
// 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;
|
||
|
||
// Shaman totem state
|
||
TotemSlot activeTotemSlots_[NUM_TOTEM_SLOTS];
|
||
|
||
// Duel state
|
||
bool pendingDuelRequest_ = false;
|
||
uint64_t duelChallengerGuid_= 0;
|
||
uint64_t duelFlagGuid_ = 0;
|
||
std::string duelChallengerName_;
|
||
uint32_t duelCountdownMs_ = 0; // 0 = no active countdown
|
||
std::chrono::steady_clock::time_point duelCountdownStartedAt_{};
|
||
|
||
// ---- Guild state ----
|
||
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_; // guildId → guild name
|
||
std::unordered_set<uint32_t> pendingGuildNameQueries_; // in-flight guild queries
|
||
bool pendingGuildInvite_ = false;
|
||
std::string pendingGuildInviterName_;
|
||
std::string pendingGuildInviteGuildName_;
|
||
bool showPetitionDialog_ = false;
|
||
uint32_t petitionCost_ = 0;
|
||
uint64_t petitionNpcGuid_ = 0;
|
||
|
||
uint64_t activeCharacterGuid_ = 0;
|
||
Race playerRace_ = Race::HUMAN;
|
||
|
||
// ---- Phase 5: Loot ----
|
||
bool lootWindowOpen = false;
|
||
bool autoLoot_ = false;
|
||
bool autoSellGrey_ = false;
|
||
bool autoRepair_ = false;
|
||
LootResponseData currentLoot;
|
||
std::vector<uint64_t> masterLootCandidates_; // from SMSG_LOOT_MASTER_LIST
|
||
|
||
// 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_;
|
||
// Tracks the last GO we sent CMSG_GAMEOBJ_USE to; used in handleSpellGo
|
||
// to send CMSG_LOOT after a gather cast (mining/herbalism) completes.
|
||
uint64_t lastInteractedGoGuid_ = 0;
|
||
uint64_t pendingLootMoneyGuid_ = 0;
|
||
uint32_t pendingLootMoneyAmount_ = 0;
|
||
float pendingLootMoneyNotifyTimer_ = 0.0f;
|
||
std::unordered_map<uint64_t, float> recentLootMoneyAnnounceCooldowns_;
|
||
uint64_t playerMoneyCopper_ = 0;
|
||
int32_t playerArmorRating_ = 0;
|
||
int32_t playerResistances_[6] = {}; // [0]=Holy,[1]=Fire,[2]=Nature,[3]=Frost,[4]=Shadow,[5]=Arcane
|
||
// Server-authoritative primary stats: [0]=STR [1]=AGI [2]=STA [3]=INT [4]=SPI; -1 = not received yet
|
||
int32_t playerStats_[5] = {-1, -1, -1, -1, -1};
|
||
// WotLK secondary combat stats (-1 = not yet received)
|
||
int32_t playerMeleeAP_ = -1;
|
||
int32_t playerRangedAP_ = -1;
|
||
int32_t playerSpellDmgBonus_[7] = {-1,-1,-1,-1,-1,-1,-1}; // per school 0-6
|
||
int32_t playerHealBonus_ = -1;
|
||
float playerDodgePct_ = -1.0f;
|
||
float playerParryPct_ = -1.0f;
|
||
float playerBlockPct_ = -1.0f;
|
||
float playerCritPct_ = -1.0f;
|
||
float playerRangedCritPct_ = -1.0f;
|
||
float playerSpellCritPct_[7] = {-1.0f,-1.0f,-1.0f,-1.0f,-1.0f,-1.0f,-1.0f};
|
||
int32_t playerCombatRatings_[25] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1};
|
||
// Some servers/custom clients shift update field indices. We can auto-detect coinage by correlating
|
||
// money-notify deltas with update-field diffs and then overriding UF::PLAYER_FIELD_COINAGE at runtime.
|
||
uint32_t pendingMoneyDelta_ = 0;
|
||
float pendingMoneyDeltaTimer_ = 0.0f;
|
||
|
||
// Gossip
|
||
bool gossipWindowOpen = false;
|
||
GossipMessageData currentGossip;
|
||
std::vector<GossipPoi> gossipPois_;
|
||
|
||
void performGameObjectInteractionNow(uint64_t guid);
|
||
|
||
// Quest details
|
||
bool questDetailsOpen = false;
|
||
std::chrono::steady_clock::time_point questDetailsOpenTime{}; // Delayed opening to allow item data to load
|
||
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_;
|
||
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_;
|
||
|
||
// Faction hostility lookup (populated from FactionTemplate.dbc)
|
||
std::unordered_map<uint32_t, bool> factionHostileMap_;
|
||
bool isHostileFaction(uint32_t factionTemplateId) const {
|
||
auto it = factionHostileMap_.find(factionTemplateId);
|
||
return it != factionHostileMap_.end() ? it->second : true; // default hostile if unknown
|
||
}
|
||
|
||
// Vehicle (WotLK): non-zero when player is seated in a vehicle
|
||
uint32_t vehicleId_ = 0;
|
||
|
||
// Taxi / Flight Paths
|
||
std::unordered_map<uint64_t, bool> taxiNpcHasRoutes_; // guid -> has new/available routes
|
||
std::unordered_map<uint32_t, TaxiNode> taxiNodes_;
|
||
std::vector<TaxiPathEdge> taxiPathEdges_;
|
||
std::unordered_map<uint32_t, std::vector<TaxiPathNode>> taxiPathNodes_; // pathId -> ordered waypoints
|
||
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; // Prevent re-entering taxi right after landing
|
||
float taxiStartGrace_ = 0.0f; // Ignore transient landing/dismount checks right after takeoff
|
||
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] = {}; // 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
|
||
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
|
||
bool bankOpen_ = false;
|
||
uint64_t bankerGuid_ = 0;
|
||
std::array<uint64_t, 28> bankSlotGuids_{};
|
||
std::array<uint64_t, 7> bankBagSlotGuids_{};
|
||
int effectiveBankSlots_ = 28; // 24 for Classic, 28 for TBC/WotLK
|
||
int effectiveBankBagSlots_ = 7; // 6 for Classic, 7 for TBC/WotLK
|
||
|
||
// Guild Bank
|
||
bool guildBankOpen_ = false;
|
||
uint64_t guildBankerGuid_ = 0;
|
||
GuildBankData guildBankData_;
|
||
uint8_t guildBankActiveTab_ = 0;
|
||
|
||
// Auction House
|
||
bool auctionOpen_ = false;
|
||
uint64_t auctioneerGuid_ = 0;
|
||
uint32_t auctionHouseId_ = 0;
|
||
AuctionListResult auctionBrowseResults_;
|
||
AuctionListResult auctionOwnerResults_;
|
||
AuctionListResult auctionBidderResults_;
|
||
int auctionActiveTab_ = 0; // 0=Browse, 1=Bids, 2=Auctions
|
||
float auctionSearchDelayTimer_ = 0.0f;
|
||
// Last search params for re-query (pagination, auto-refresh after bid/buyout)
|
||
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_;
|
||
// Routing: which result vector to populate from next SMSG_AUCTION_LIST_RESULT
|
||
enum class AuctionResultTarget { BROWSE, OWNER, BIDDER };
|
||
AuctionResultTarget pendingAuctionTarget_ = AuctionResultTarget::BROWSE;
|
||
|
||
// Vendor
|
||
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;
|
||
|
||
// Trainer
|
||
bool trainerWindowOpen_ = false;
|
||
TrainerListData currentTrainerList_;
|
||
struct SpellNameEntry { std::string name; std::string rank; std::string description; uint32_t schoolMask = 0; uint8_t dispelType = 0; uint32_t attrEx = 0; };
|
||
std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
||
bool spellNameCacheLoaded_ = false;
|
||
|
||
// Title cache: maps titleBit → title string (lazy-loaded from CharTitles.dbc)
|
||
// The strings use "%s" as a player-name placeholder (e.g. "Commander %s", "%s the Explorer").
|
||
std::unordered_map<uint32_t, std::string> titleNameCache_;
|
||
bool titleNameCacheLoaded_ = false;
|
||
void loadTitleNameCache();
|
||
// Set of title bit-indices known to the player (from SMSG_TITLE_EARNED).
|
||
std::unordered_set<uint32_t> knownTitleBits_;
|
||
// Currently selected title bit, or -1 for no title. Updated from PLAYER_CHOSEN_TITLE.
|
||
int32_t chosenTitleBit_ = -1;
|
||
|
||
// Achievement caches (lazy-loaded from Achievement.dbc on first earned event)
|
||
std::unordered_map<uint32_t, std::string> achievementNameCache_;
|
||
std::unordered_map<uint32_t, std::string> achievementDescCache_;
|
||
std::unordered_map<uint32_t, uint32_t> achievementPointsCache_;
|
||
bool achievementNameCacheLoaded_ = false;
|
||
void loadAchievementNameCache();
|
||
// Set of achievement IDs earned by the player (populated from SMSG_ALL_ACHIEVEMENT_DATA)
|
||
std::unordered_set<uint32_t> earnedAchievements_;
|
||
// Earn dates: achievementId → WoW PackedTime (from SMSG_ACHIEVEMENT_EARNED / SMSG_ALL_ACHIEVEMENT_DATA)
|
||
std::unordered_map<uint32_t, uint32_t> achievementDates_;
|
||
// Criteria progress: criteriaId → current value (from SMSG_CRITERIA_UPDATE)
|
||
std::unordered_map<uint32_t, uint64_t> criteriaProgress_;
|
||
void handleAllAchievementData(network::Packet& packet);
|
||
|
||
// 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)
|
||
std::unordered_map<uint32_t, std::string> areaNameCache_;
|
||
bool areaNameCacheLoaded_ = false;
|
||
void loadAreaNameCache();
|
||
std::string getAreaName(uint32_t areaId) const;
|
||
|
||
// Map name cache (lazy-loaded from Map.dbc; maps mapId → localized display name)
|
||
std::unordered_map<uint32_t, std::string> mapNameCache_;
|
||
bool mapNameCacheLoaded_ = false;
|
||
void loadMapNameCache();
|
||
|
||
// LFG dungeon name cache (lazy-loaded from LFGDungeons.dbc; WotLK only)
|
||
std::unordered_map<uint32_t, std::string> lfgDungeonNameCache_;
|
||
bool lfgDungeonNameCacheLoaded_ = false;
|
||
void loadLfgDungeonDbc();
|
||
std::string getLfgDungeonName(uint32_t dungeonId) const;
|
||
std::vector<TrainerTab> trainerTabs_;
|
||
void handleTrainerList(network::Packet& packet);
|
||
void loadSpellNameCache();
|
||
void categorizeTrainerSpells();
|
||
|
||
// Callbacks
|
||
WorldConnectSuccessCallback onSuccess;
|
||
WorldConnectFailureCallback onFailure;
|
||
CharCreateCallback charCreateCallback_;
|
||
CharDeleteCallback charDeleteCallback_;
|
||
CharLoginFailCallback charLoginFailCallback_;
|
||
uint8_t lastCharDeleteResult_ = 0xFF;
|
||
bool pendingCharCreateResult_ = false;
|
||
bool pendingCharCreateSuccess_ = false;
|
||
std::string pendingCharCreateMsg_;
|
||
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] = {};
|
||
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
|
||
bool wardenResponsePending_ = false;
|
||
|
||
// ---- RX silence detection ----
|
||
std::chrono::steady_clock::time_point lastRxTime_{};
|
||
bool rxSilenceLogged_ = false;
|
||
|
||
// ---- XP tracking ----
|
||
uint32_t playerXp_ = 0;
|
||
uint32_t playerNextLevelXp_ = 0;
|
||
uint32_t playerRestedXp_ = 0;
|
||
bool isResting_ = false;
|
||
uint32_t serverPlayerLevel_ = 1;
|
||
static uint32_t xpForLevel(uint32_t level);
|
||
|
||
// ---- Server time tracking (for deterministic celestial/sky systems) ----
|
||
float gameTime_ = 0.0f; // Server game time in seconds
|
||
float timeSpeed_ = 0.0166f; // Time scale (default: 1 game day = 1 real hour)
|
||
void handleLoginSetTimeSpeed(network::Packet& packet);
|
||
|
||
// ---- Global Cooldown (GCD) ----
|
||
float gcdTotal_ = 0.0f;
|
||
std::chrono::steady_clock::time_point gcdStartedAt_{};
|
||
|
||
// ---- Weather state (SMSG_WEATHER) ----
|
||
uint32_t weatherType_ = 0; // 0=clear, 1=rain, 2=snow, 3=storm
|
||
float weatherIntensity_ = 0.0f; // 0.0 to 1.0
|
||
|
||
// ---- Light override (SMSG_OVERRIDE_LIGHT) ----
|
||
uint32_t overrideLightId_ = 0; // 0 = no override
|
||
uint32_t overrideLightTransMs_ = 0;
|
||
|
||
// ---- Player skills ----
|
||
std::map<uint32_t, PlayerSkill> playerSkills_;
|
||
std::unordered_map<uint32_t, std::string> skillLineNames_;
|
||
std::unordered_map<uint32_t, uint32_t> skillLineCategories_;
|
||
std::unordered_map<uint32_t, uint32_t> spellToSkillLine_; // spellID -> skillLineID
|
||
bool skillLineDbcLoaded_ = false;
|
||
bool skillLineAbilityLoaded_ = false;
|
||
static constexpr size_t PLAYER_EXPLORED_ZONES_COUNT = 128;
|
||
std::vector<uint32_t> playerExploredZones_ =
|
||
std::vector<uint32_t>(PLAYER_EXPLORED_ZONES_COUNT, 0u);
|
||
bool hasPlayerExploredZones_ = false;
|
||
void loadSkillLineDbc();
|
||
void loadSkillLineAbilityDbc();
|
||
void extractSkillFields(const std::map<uint16_t, uint32_t>& fields);
|
||
void extractExploredZoneFields(const std::map<uint16_t, uint32_t>& fields);
|
||
void applyQuestStateFromFields(const std::map<uint16_t, uint32_t>& fields);
|
||
// Apply packed kill counts from player update fields to a quest entry that has
|
||
// already had its killObjectives populated from SMSG_QUEST_QUERY_RESPONSE.
|
||
void applyPackedKillCountsFromFields(QuestLogEntry& quest);
|
||
|
||
NpcDeathCallback npcDeathCallback_;
|
||
NpcAggroCallback npcAggroCallback_;
|
||
NpcRespawnCallback npcRespawnCallback_;
|
||
StandStateCallback standStateCallback_;
|
||
GhostStateCallback ghostStateCallback_;
|
||
MeleeSwingCallback meleeSwingCallback_;
|
||
uint64_t lastMeleeSwingMs_ = 0; // system_clock ms at last player auto-attack swing
|
||
SpellCastAnimCallback spellCastAnimCallback_;
|
||
SpellCastFailedCallback spellCastFailedCallback_;
|
||
UnitAnimHintCallback unitAnimHintCallback_;
|
||
UnitMoveFlagsCallback unitMoveFlagsCallback_;
|
||
NpcSwingCallback npcSwingCallback_;
|
||
NpcGreetingCallback npcGreetingCallback_;
|
||
NpcFarewellCallback npcFarewellCallback_;
|
||
NpcVendorCallback npcVendorCallback_;
|
||
ChargeCallback chargeCallback_;
|
||
LevelUpCallback levelUpCallback_;
|
||
LevelUpDeltas lastLevelUpDeltas_;
|
||
std::vector<TempEnchantTimer> tempEnchantTimers_;
|
||
std::vector<BookPage> bookPages_; // pages collected for the current readable item
|
||
OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_;
|
||
AchievementEarnedCallback achievementEarnedCallback_;
|
||
AreaDiscoveryCallback areaDiscoveryCallback_;
|
||
QuestProgressCallback questProgressCallback_;
|
||
MountCallback mountCallback_;
|
||
TaxiPrecacheCallback taxiPrecacheCallback_;
|
||
TaxiOrientationCallback taxiOrientationCallback_;
|
||
TaxiFlightStartCallback taxiFlightStartCallback_;
|
||
OpenLfgCallback openLfgCallback_;
|
||
uint32_t currentMountDisplayId_ = 0;
|
||
uint32_t mountAuraSpellId_ = 0; // Spell ID of the aura that caused mounting (for CMSG_CANCEL_AURA fallback)
|
||
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;
|
||
bool playerDead_ = false;
|
||
bool releasedSpirit_ = false;
|
||
uint32_t corpseMapId_ = 0;
|
||
float corpseX_ = 0.0f, corpseY_ = 0.0f, corpseZ_ = 0.0f;
|
||
uint64_t corpseGuid_ = 0;
|
||
// Absolute time (ms since epoch) when PvP corpse-reclaim delay expires.
|
||
// 0 means no active delay (reclaim allowed immediately upon proximity).
|
||
uint64_t corpseReclaimAvailableMs_ = 0;
|
||
// Death Knight runes (class 6): slots 0-1=Blood, 2-3=Unholy, 4-5=Frost initially
|
||
std::array<RuneSlot, 6> playerRunes_ = [] {
|
||
std::array<RuneSlot, 6> r{};
|
||
r[0].type = r[1].type = RuneType::Blood;
|
||
r[2].type = r[3].type = RuneType::Unholy;
|
||
r[4].type = r[5].type = RuneType::Frost;
|
||
return r;
|
||
}();
|
||
uint64_t pendingSpiritHealerGuid_ = 0;
|
||
bool resurrectPending_ = false;
|
||
bool resurrectRequestPending_ = false;
|
||
bool selfResAvailable_ = false; // SMSG_PRE_RESURRECT received — Reincarnation/Twisting Nether
|
||
// ---- 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;
|
||
bool resurrectIsSpiritHealer_ = false; // true = SMSG_SPIRIT_HEALER_CONFIRM, false = SMSG_RESURRECT_REQUEST
|
||
uint64_t resurrectCasterGuid_ = 0;
|
||
std::string resurrectCasterName_;
|
||
bool repopPending_ = false;
|
||
uint64_t lastRepopRequestMs_ = 0;
|
||
|
||
// ---- Completed quest IDs (SMSG_QUERY_QUESTS_COMPLETED_RESPONSE) ----
|
||
std::unordered_set<uint32_t> completedQuests_;
|
||
|
||
// ---- Equipment sets (SMSG_EQUIPMENT_SET_LIST) ----
|
||
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_; // public-facing copy
|
||
|
||
// ---- Forced faction reactions (SMSG_SET_FORCED_REACTIONS) ----
|
||
std::unordered_map<uint32_t, uint8_t> forcedReactions_; // factionId -> reaction tier
|
||
|
||
// ---- Server-triggered audio ----
|
||
PlayMusicCallback playMusicCallback_;
|
||
PlaySoundCallback playSoundCallback_;
|
||
PlayPositionalSoundCallback playPositionalSoundCallback_;
|
||
|
||
// ---- UI error frame callback ----
|
||
UIErrorCallback uiErrorCallback_;
|
||
|
||
// ---- Reputation change callback ----
|
||
RepChangeCallback repChangeCallback_;
|
||
uint32_t watchedFactionId_ = 0; // auto-set to most recently changed faction
|
||
|
||
// ---- PvP honor credit callback ----
|
||
PvpHonorCallback pvpHonorCallback_;
|
||
|
||
// ---- Item loot callback ----
|
||
ItemLootCallback itemLootCallback_;
|
||
|
||
// ---- Quest completion callback ----
|
||
QuestCompleteCallback questCompleteCallback_;
|
||
|
||
// ---- GM Ticket state (SMSG_GMTICKET_GETTICKET / SMSG_GMTICKET_SYSTEMSTATUS) ----
|
||
bool gmTicketActive_ = false; ///< True when an open ticket exists on the server
|
||
std::string gmTicketText_; ///< Text of the open ticket (from SMSG_GMTICKET_GETTICKET)
|
||
float gmTicketWaitHours_ = 0.0f; ///< Server-estimated wait time in hours
|
||
bool gmSupportAvailable_ = true; ///< GM support system online (SMSG_GMTICKET_SYSTEMSTATUS)
|
||
|
||
// ---- Battlefield Manager state (WotLK Wintergrasp / outdoor battlefields) ----
|
||
bool bfMgrInvitePending_ = false; ///< True when an entry/queue invite is pending acceptance
|
||
bool bfMgrActive_ = false; ///< True while the player is inside an outdoor battlefield
|
||
uint32_t bfMgrZoneId_ = 0; ///< Zone ID of the pending/active battlefield
|
||
|
||
// ---- WotLK Calendar: pending invite counter ----
|
||
uint32_t calendarPendingInvites_ = 0; ///< Unacknowledged calendar invites (SMSG_CALENDAR_SEND_NUM_PENDING)
|
||
|
||
// ---- Spell modifiers (SMSG_SET_FLAT_SPELL_MODIFIER / SMSG_SET_PCT_SPELL_MODIFIER) ----
|
||
// Keyed by (SpellModOp, groupIndex); cleared on logout/character change.
|
||
std::unordered_map<SpellModKey, int32_t, SpellModKeyHash> spellFlatMods_;
|
||
std::unordered_map<SpellModKey, int32_t, SpellModKeyHash> spellPctMods_;
|
||
};
|
||
|
||
} // namespace game
|
||
} // namespace wowee
|