Kelsidavis-WoWee/include/game/quest_handler.hpp
Kelsi 961af04b36 fix: gossip banker sent CMSG_BANKER_ACTIVATE twice; deduplicate quest icons
Icon==6 and text=="GOSSIP_OPTION_BANKER" both sent BANKER_ACTIVATE
independently. Banking NPCs match both, so the packet was sent twice —
some servers toggle the bank window open then closed. Added sentBanker
guard so only one packet is sent.

Also extracts classifyGossipQuests() from two identical 30-line blocks
in handleGossipMessage and handleQuestgiverQuestList. The icon→status
mapping (5/6/10=completable, 3/4=incomplete, 2/7/8=available) is now
in one place with a why-comment explaining these are protocol-defined.
2026-03-29 18:53:30 -07:00

200 lines
8 KiB
C++

#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 classifyGossipQuests(bool updateQuestLog);
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