Merge pull request #23 from ldmonster/chore/split-game-handler

[chore] GameHandler: extract 8 domain handler classes
This commit is contained in:
Kelsi Rae Davis 2026-03-28 10:10:16 -07:00 committed by GitHub
commit 6a46e573bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 20991 additions and 17904 deletions

44
.clang-tidy Normal file
View file

@ -0,0 +1,44 @@
# clang-tidy configuration for WoWee
# Targets C++20. Checks are tuned for a Vulkan/game-engine codebase:
# - reinterpret_cast, pointer arithmetic, and magic numbers are frequent
# in low-level graphics/network code, so the most aggressive
# cppcoreguidelines and readability-magic-numbers checks are disabled.
---
Checks: >
bugprone-*,
clang-analyzer-*,
performance-*,
modernize-use-nullptr,
modernize-use-override,
modernize-use-default-member-init,
modernize-use-emplace,
modernize-loop-convert,
modernize-deprecated-headers,
modernize-make-unique,
modernize-make-shared,
readability-braces-around-statements,
readability-container-size-empty,
readability-delete-null-pointer,
readability-else-after-return,
readability-misplaced-array-index,
readability-non-const-parameter,
readability-redundant-control-flow,
readability-redundant-declaration,
readability-simplify-boolean-expr,
readability-string-compare,
-bugprone-easily-swappable-parameters,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
-performance-avoid-endl
WarningsAsErrors: ''
# Suppress the noise from GCC-only LTO flags in compile_commands.json.
# clang doesn't support -fno-fat-lto-objects; this silences the harmless warning.
ExtraArgs:
- -Wno-ignored-optimization-argument
HeaderFilterRegex: '^.*/include/.*\.hpp$'
CheckOptions:
- key: modernize-use-default-member-init.UseAssignment
value: true

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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

1532
src/game/combat_handler.cpp Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

1892
src/game/quest_handler.cpp Normal file

File diff suppressed because it is too large Load diff

2713
src/game/social_handler.cpp Normal file

File diff suppressed because it is too large Load diff

3211
src/game/spell_handler.cpp Normal file

File diff suppressed because it is too large Load diff

1369
src/game/warden_handler.cpp Normal file

File diff suppressed because it is too large Load diff

View file

@ -226,10 +226,8 @@ bool CharacterRenderer::initialize(VkContext* ctx, VkDescriptorSetLayout perFram
// --- Load shaders ---
rendering::VkShaderModule charVert, charFrag;
charVert.loadFromFile(device, "assets/shaders/character.vert.spv");
charFrag.loadFromFile(device, "assets/shaders/character.frag.spv");
if (!charVert.isValid() || !charFrag.isValid()) {
if (!charVert.loadFromFile(device, "assets/shaders/character.vert.spv") ||
!charFrag.loadFromFile(device, "assets/shaders/character.frag.spv")) {
LOG_ERROR("Character: Missing required shaders, cannot initialize");
return false;
}
@ -3287,10 +3285,8 @@ void CharacterRenderer::recreatePipelines() {
// --- Load shaders ---
rendering::VkShaderModule charVert, charFrag;
charVert.loadFromFile(device, "assets/shaders/character.vert.spv");
charFrag.loadFromFile(device, "assets/shaders/character.frag.spv");
if (!charVert.isValid() || !charFrag.isValid()) {
if (!charVert.loadFromFile(device, "assets/shaders/character.vert.spv") ||
!charFrag.loadFromFile(device, "assets/shaders/character.frag.spv")) {
LOG_ERROR("CharacterRenderer::recreatePipelines: missing required shaders");
return;
}

View file

@ -273,9 +273,12 @@ void ChargeEffect::recreatePipelines() {
// ---- Rebuild ribbon trail pipeline (TRIANGLE_STRIP) ----
{
VkShaderModule vertModule;
vertModule.loadFromFile(device, "assets/shaders/charge_ribbon.vert.spv");
VkShaderModule fragModule;
fragModule.loadFromFile(device, "assets/shaders/charge_ribbon.frag.spv");
if (!vertModule.loadFromFile(device, "assets/shaders/charge_ribbon.vert.spv") ||
!fragModule.loadFromFile(device, "assets/shaders/charge_ribbon.frag.spv")) {
LOG_ERROR("ChargeEffect::recreatePipelines: failed to load ribbon shader modules");
return;
}
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
@ -323,9 +326,12 @@ void ChargeEffect::recreatePipelines() {
// ---- Rebuild dust puff pipeline (POINT_LIST) ----
{
VkShaderModule vertModule;
vertModule.loadFromFile(device, "assets/shaders/charge_dust.vert.spv");
VkShaderModule fragModule;
fragModule.loadFromFile(device, "assets/shaders/charge_dust.frag.spv");
if (!vertModule.loadFromFile(device, "assets/shaders/charge_dust.vert.spv") ||
!fragModule.loadFromFile(device, "assets/shaders/charge_dust.frag.spv")) {
LOG_ERROR("ChargeEffect::recreatePipelines: failed to load dust shader modules");
return;
}
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);

View file

@ -158,9 +158,12 @@ void LensFlare::recreatePipelines() {
}
VkShaderModule vertModule;
vertModule.loadFromFile(device, "assets/shaders/lens_flare.vert.spv");
VkShaderModule fragModule;
fragModule.loadFromFile(device, "assets/shaders/lens_flare.frag.spv");
if (!vertModule.loadFromFile(device, "assets/shaders/lens_flare.vert.spv") ||
!fragModule.loadFromFile(device, "assets/shaders/lens_flare.frag.spv")) {
LOG_ERROR("LensFlare::recreatePipelines: failed to load shader modules");
return;
}
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);

View file

@ -277,9 +277,12 @@ void Lightning::recreatePipelines() {
// ---- Rebuild bolt pipeline (LINE_STRIP) ----
{
VkShaderModule vertModule;
vertModule.loadFromFile(device, "assets/shaders/lightning_bolt.vert.spv");
VkShaderModule fragModule;
fragModule.loadFromFile(device, "assets/shaders/lightning_bolt.frag.spv");
if (!vertModule.loadFromFile(device, "assets/shaders/lightning_bolt.vert.spv") ||
!fragModule.loadFromFile(device, "assets/shaders/lightning_bolt.frag.spv")) {
LOG_ERROR("Lightning::recreatePipelines: failed to load bolt shader modules");
return;
}
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);
@ -315,9 +318,12 @@ void Lightning::recreatePipelines() {
// ---- Rebuild flash pipeline (TRIANGLE_STRIP) ----
{
VkShaderModule vertModule;
vertModule.loadFromFile(device, "assets/shaders/lightning_flash.vert.spv");
VkShaderModule fragModule;
fragModule.loadFromFile(device, "assets/shaders/lightning_flash.frag.spv");
if (!vertModule.loadFromFile(device, "assets/shaders/lightning_flash.vert.spv") ||
!fragModule.loadFromFile(device, "assets/shaders/lightning_flash.frag.spv")) {
LOG_ERROR("Lightning::recreatePipelines: failed to load flash shader modules");
return;
}
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);

View file

@ -157,9 +157,12 @@ void MountDust::recreatePipelines() {
}
VkShaderModule vertModule;
vertModule.loadFromFile(device, "assets/shaders/mount_dust.vert.spv");
VkShaderModule fragModule;
fragModule.loadFromFile(device, "assets/shaders/mount_dust.frag.spv");
if (!vertModule.loadFromFile(device, "assets/shaders/mount_dust.vert.spv") ||
!fragModule.loadFromFile(device, "assets/shaders/mount_dust.frag.spv")) {
LOG_ERROR("MountDust::recreatePipelines: failed to load shader modules");
return;
}
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);

View file

@ -193,9 +193,12 @@ void QuestMarkerRenderer::recreatePipelines() {
}
VkShaderModule vertModule;
vertModule.loadFromFile(device, "assets/shaders/quest_marker.vert.spv");
VkShaderModule fragModule;
fragModule.loadFromFile(device, "assets/shaders/quest_marker.frag.spv");
if (!vertModule.loadFromFile(device, "assets/shaders/quest_marker.vert.spv") ||
!fragModule.loadFromFile(device, "assets/shaders/quest_marker.frag.spv")) {
LOG_ERROR("QuestMarkerRenderer::recreatePipelines: failed to load shader modules");
return;
}
VkPipelineShaderStageCreateInfo vertStage = vertModule.stageInfo(VK_SHADER_STAGE_VERTEX_BIT);
VkPipelineShaderStageCreateInfo fragStage = fragModule.stageInfo(VK_SHADER_STAGE_FRAGMENT_BIT);

View file

@ -3129,7 +3129,6 @@ bool WMORenderer::checkWallCollision(const glm::vec3& from, const glm::vec3& to,
glm::vec3 moveDir = to - from;
float moveDistSq = glm::dot(moveDir, moveDir);
if (moveDistSq < 1e-6f) return false;
float moveDist = std::sqrt(moveDistSq);
// Player collision parameters — WoW-style horizontal cylinder
// Tighter radius when inside for more responsive indoor collision

133
test.sh Executable file
View file

@ -0,0 +1,133 @@
#!/usr/bin/env bash
# test.sh — Run the C++ linter (clang-tidy) against all first-party sources.
#
# Usage:
# ./test.sh # lint src/ and include/ using build/compile_commands.json
# FIX=1 ./test.sh # apply suggested fixes automatically (use with care)
#
# Exit code is non-zero if any clang-tidy diagnostic is emitted.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# ---------------------------------------------------------------------------
# Dependency check
# ---------------------------------------------------------------------------
CLANG_TIDY=""
for candidate in clang-tidy clang-tidy-18 clang-tidy-17 clang-tidy-16 clang-tidy-15 clang-tidy-14; do
if command -v "$candidate" >/dev/null 2>&1; then
CLANG_TIDY="$candidate"
break
fi
done
if [[ -z "$CLANG_TIDY" ]]; then
echo "clang-tidy not found. Install it with:"
echo " sudo apt-get install clang-tidy"
exit 1
fi
echo "Using: $($CLANG_TIDY --version | head -1)"
# run-clang-tidy runs checks in parallel; fall back to sequential if absent.
RUN_CLANG_TIDY=""
for candidate in run-clang-tidy run-clang-tidy-18 run-clang-tidy-17 run-clang-tidy-16 run-clang-tidy-15 run-clang-tidy-14; do
if command -v "$candidate" >/dev/null 2>&1; then
RUN_CLANG_TIDY="$candidate"
break
fi
done
# ---------------------------------------------------------------------------
# Build database check
# ---------------------------------------------------------------------------
COMPILE_COMMANDS="$SCRIPT_DIR/build/compile_commands.json"
if [[ ! -f "$COMPILE_COMMANDS" ]]; then
echo "compile_commands.json not found at $COMPILE_COMMANDS"
echo "Run cmake first: cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
exit 1
fi
# ---------------------------------------------------------------------------
# Source files to check (first-party only)
# ---------------------------------------------------------------------------
mapfile -t SOURCE_FILES < <(
find "$SCRIPT_DIR/src" -type f \( -name '*.cpp' -o -name '*.cxx' -o -name '*.cc' \) | sort
)
if [[ ${#SOURCE_FILES[@]} -eq 0 ]]; then
echo "No source files found in src/"
exit 0
fi
echo "Linting ${#SOURCE_FILES[@]} source files..."
# ---------------------------------------------------------------------------
# Resolve GCC C++ stdlib include paths for clang-tidy
#
# compile_commands.json is generated by GCC. When clang-tidy processes those
# commands with its own clang driver it cannot locate GCC's libstdc++ headers.
# We query GCC for its include search list and forward each path as an
# -isystem extra argument so clang-tidy can find <vector>, <string>, etc.
# ---------------------------------------------------------------------------
EXTRA_TIDY_ARGS=() # for direct clang-tidy: --extra-arg=...
EXTRA_RUN_ARGS=() # for run-clang-tidy: -extra-arg=...
if command -v gcc >/dev/null 2>&1; then
while IFS= read -r inc_path; do
[[ -d "$inc_path" ]] || continue
EXTRA_TIDY_ARGS+=("--extra-arg=-isystem${inc_path}")
EXTRA_RUN_ARGS+=("-extra-arg=-isystem${inc_path}")
done < <(
gcc -E -x c++ - -v < /dev/null 2>&1 \
| sed -n '/#include <\.\.\.> search starts here:/,/End of search list\./p' \
| grep '^ ' \
| sed 's/^ //'
)
fi
# ---------------------------------------------------------------------------
# Run
# ---------------------------------------------------------------------------
FIX="${FIX:-0}"
FIX_FLAG=""
if [[ "$FIX" == "1" ]]; then
FIX_FLAG="-fix"
echo "Fix mode enabled — applying suggested fixes."
fi
FAILED=0
if [[ -n "$RUN_CLANG_TIDY" ]]; then
echo "Running via $RUN_CLANG_TIDY (parallel)..."
# run-clang-tidy takes a source-file regex; match our src/ tree
SRC_REGEX="$(echo "$SCRIPT_DIR/src" | sed 's|/|\\/|g')"
"$RUN_CLANG_TIDY" \
-clang-tidy-binary "$CLANG_TIDY" \
-p "$SCRIPT_DIR/build" \
$FIX_FLAG \
"${EXTRA_RUN_ARGS[@]}" \
"$SRC_REGEX" || FAILED=$?
else
echo "run-clang-tidy not found; running sequentially..."
for f in "${SOURCE_FILES[@]}"; do
"$CLANG_TIDY" \
-p "$SCRIPT_DIR/build" \
$FIX_FLAG \
"${EXTRA_TIDY_ARGS[@]}" \
"$f" || FAILED=$?
done
fi
# ---------------------------------------------------------------------------
# Result
# ---------------------------------------------------------------------------
if [[ $FAILED -ne 0 ]]; then
echo ""
echo "clang-tidy reported issues. Fix them or add suppressions in .clang-tidy."
exit 1
fi
echo ""
echo "Lint passed."