mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Harden quest accept state and resync quest log on login
- add idempotent pending-accept tracking with timeout and per-quest cleanup hooks - stop optimistic local quest insertion on accept; rely on server-authoritative updates - handle QUEST_INVALID already-on/completed as reconciliation paths for pending accepts - trigger quest metadata + status resync when accept state drifts - add login-time quest log rebuild from PLAYER_QUEST_LOG server slots - query quest metadata for server-slot quests to hydrate titles/objectives - clear stale pending quest accept/query state on fresh world entry - improve gossip quest selection fallback by trusting server quest slots when local cache is missing This reduces duplicate accept/state mismatch loops (notably reason=13) and stabilizes WotLK quest behavior after relogs.
This commit is contained in:
parent
ebaeea43cb
commit
954edc91b8
2 changed files with 189 additions and 15 deletions
|
|
@ -1127,6 +1127,11 @@ private:
|
|||
void handleQuestDetails(network::Packet& packet);
|
||||
void handleQuestRequestItems(network::Packet& packet);
|
||||
void handleQuestOfferReward(network::Packet& packet);
|
||||
void clearPendingQuestAccept(uint32_t questId);
|
||||
void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason);
|
||||
bool hasQuestInLog(uint32_t questId) const;
|
||||
void addQuestToLocalLogIfMissing(uint32_t questId, const std::string& title, const std::string& objectives);
|
||||
bool resyncQuestLogFromServerSlots(bool forceQueryMetadata);
|
||||
void handleListInventory(network::Packet& packet);
|
||||
void addMoneyCopper(uint32_t amount);
|
||||
|
||||
|
|
@ -1487,12 +1492,16 @@ private:
|
|||
uint32_t pendingTurnInQuestId_ = 0;
|
||||
uint64_t pendingTurnInNpcGuid_ = 0;
|
||||
bool pendingTurnInRewardRequest_ = false;
|
||||
std::unordered_map<uint32_t, float> pendingQuestAcceptTimeouts_;
|
||||
std::unordered_map<uint32_t, uint64_t> pendingQuestAcceptNpcGuids_;
|
||||
bool questOfferRewardOpen_ = false;
|
||||
QuestOfferRewardData currentQuestOfferReward_;
|
||||
|
||||
// Quest log
|
||||
std::vector<QuestLogEntry> questLog_;
|
||||
std::unordered_set<uint32_t> pendingQuestQueryIds_;
|
||||
bool pendingLoginQuestResync_ = false;
|
||||
float pendingLoginQuestResyncTimeout_ = 0.0f;
|
||||
|
||||
// Quest giver status per NPC
|
||||
std::unordered_map<uint64_t, QuestGiverStatus> npcQuestStatus_;
|
||||
|
|
|
|||
|
|
@ -575,6 +575,20 @@ void GameHandler::update(float deltaTime) {
|
|||
if (auctionSearchDelayTimer_ < 0.0f) auctionSearchDelayTimer_ = 0.0f;
|
||||
}
|
||||
|
||||
for (auto it = pendingQuestAcceptTimeouts_.begin(); it != pendingQuestAcceptTimeouts_.end();) {
|
||||
it->second -= deltaTime;
|
||||
if (it->second <= 0.0f) {
|
||||
const uint32_t questId = it->first;
|
||||
const uint64_t npcGuid = pendingQuestAcceptNpcGuids_.count(questId) != 0
|
||||
? pendingQuestAcceptNpcGuids_[questId] : 0;
|
||||
triggerQuestAcceptResync(questId, npcGuid, "timeout");
|
||||
it = pendingQuestAcceptTimeouts_.erase(it);
|
||||
pendingQuestAcceptNpcGuids_.erase(questId);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingMoneyDeltaTimer_ > 0.0f) {
|
||||
pendingMoneyDeltaTimer_ -= deltaTime;
|
||||
if (pendingMoneyDeltaTimer_ <= 0.0f) {
|
||||
|
|
@ -583,6 +597,18 @@ void GameHandler::update(float deltaTime) {
|
|||
}
|
||||
}
|
||||
|
||||
if (pendingLoginQuestResync_) {
|
||||
pendingLoginQuestResyncTimeout_ -= deltaTime;
|
||||
if (resyncQuestLogFromServerSlots(true)) {
|
||||
pendingLoginQuestResync_ = false;
|
||||
pendingLoginQuestResyncTimeout_ = 0.0f;
|
||||
} else if (pendingLoginQuestResyncTimeout_ <= 0.0f) {
|
||||
pendingLoginQuestResync_ = false;
|
||||
pendingLoginQuestResyncTimeout_ = 0.0f;
|
||||
LOG_WARNING("Quest login resync timed out waiting for player quest slot fields");
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = pendingGameObjectLootRetries_.begin(); it != pendingGameObjectLootRetries_.end();) {
|
||||
it->timer -= deltaTime;
|
||||
if (it->timer <= 0.0f) {
|
||||
|
|
@ -2223,6 +2249,30 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
case 19: reasonStr = "Can't take any more quests"; break;
|
||||
}
|
||||
LOG_WARNING("Quest invalid: reason=", failReason, " (", reasonStr, ")");
|
||||
if (!pendingQuestAcceptTimeouts_.empty()) {
|
||||
std::vector<uint32_t> pendingQuestIds;
|
||||
pendingQuestIds.reserve(pendingQuestAcceptTimeouts_.size());
|
||||
for (const auto& pending : pendingQuestAcceptTimeouts_) {
|
||||
pendingQuestIds.push_back(pending.first);
|
||||
}
|
||||
for (uint32_t questId : pendingQuestIds) {
|
||||
const uint64_t npcGuid = pendingQuestAcceptNpcGuids_.count(questId) != 0
|
||||
? pendingQuestAcceptNpcGuids_[questId] : 0;
|
||||
if (failReason == 13) {
|
||||
std::string fallbackTitle = "Quest #" + std::to_string(questId);
|
||||
std::string fallbackObjectives;
|
||||
if (currentQuestDetails.questId == questId) {
|
||||
if (!currentQuestDetails.title.empty()) fallbackTitle = currentQuestDetails.title;
|
||||
fallbackObjectives = currentQuestDetails.objectives;
|
||||
}
|
||||
addQuestToLocalLogIfMissing(questId, fallbackTitle, fallbackObjectives);
|
||||
triggerQuestAcceptResync(questId, npcGuid, "already-on-quest");
|
||||
} else if (failReason == 18) {
|
||||
triggerQuestAcceptResync(questId, npcGuid, "already-completed");
|
||||
}
|
||||
clearPendingQuestAccept(questId);
|
||||
}
|
||||
}
|
||||
// Only show error to user for real errors (not informational messages)
|
||||
if (failReason != 13 && failReason != 18) { // Don't spam "already on/completed"
|
||||
addSystemChatMessage(std::string("Quest unavailable: ") + reasonStr);
|
||||
|
|
@ -2268,6 +2318,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
size_t rem = packet.getSize() - packet.getReadPos();
|
||||
if (rem >= 12) {
|
||||
uint32_t questId = packet.readUInt32();
|
||||
clearPendingQuestAccept(questId);
|
||||
uint32_t entry = packet.readUInt32(); // Creature entry
|
||||
uint32_t count = packet.readUInt32(); // Current kills
|
||||
uint32_t reqCount = 0;
|
||||
|
|
@ -2302,6 +2353,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
} else if (rem >= 4) {
|
||||
// Swapped mapping fallback: treat as QUESTUPDATE_COMPLETE packet.
|
||||
uint32_t questId = packet.readUInt32();
|
||||
clearPendingQuestAccept(questId);
|
||||
LOG_INFO("Quest objectives completed (compat via ADD_KILL): questId=", questId);
|
||||
for (auto& quest : questLog_) {
|
||||
if (quest.questId == questId) {
|
||||
|
|
@ -2347,6 +2399,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
size_t rem = packet.getSize() - packet.getReadPos();
|
||||
if (rem >= 12) {
|
||||
uint32_t questId = packet.readUInt32();
|
||||
clearPendingQuestAccept(questId);
|
||||
uint32_t entry = packet.readUInt32();
|
||||
uint32_t count = packet.readUInt32();
|
||||
uint32_t reqCount = 0;
|
||||
|
|
@ -2364,6 +2417,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
} else if (rem >= 4) {
|
||||
uint32_t questId = packet.readUInt32();
|
||||
clearPendingQuestAccept(questId);
|
||||
LOG_INFO("Quest objectives completed: questId=", questId);
|
||||
|
||||
for (auto& quest : questLog_) {
|
||||
|
|
@ -2384,6 +2438,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
break;
|
||||
}
|
||||
uint32_t questId = packet.readUInt32();
|
||||
clearPendingQuestAccept(questId);
|
||||
pendingQuestQueryIds_.erase(questId);
|
||||
if (questId == 0) {
|
||||
// Some servers emit a zero-id variant during world bootstrap.
|
||||
|
|
@ -3092,6 +3147,10 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
|
|||
playerSkills_.clear();
|
||||
questLog_.clear();
|
||||
pendingQuestQueryIds_.clear();
|
||||
pendingLoginQuestResync_ = false;
|
||||
pendingLoginQuestResyncTimeout_ = 0.0f;
|
||||
pendingQuestAcceptTimeouts_.clear();
|
||||
pendingQuestAcceptNpcGuids_.clear();
|
||||
npcQuestStatus_.clear();
|
||||
hostileAttackers_.clear();
|
||||
combatText.clear();
|
||||
|
|
@ -3144,6 +3203,7 @@ void GameHandler::handleLoginSetTimeSpeed(network::Packet& packet) {
|
|||
|
||||
void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
||||
LOG_INFO("Handling SMSG_LOGIN_VERIFY_WORLD");
|
||||
const bool initialWorldEntry = (state == WorldState::ENTERING_WORLD);
|
||||
|
||||
LoginVerifyWorldData data;
|
||||
if (!LoginVerifyWorldParser::parse(packet, data)) {
|
||||
|
|
@ -3232,6 +3292,15 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
|
|||
taxiRecoverPending_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (initialWorldEntry) {
|
||||
pendingQuestAcceptTimeouts_.clear();
|
||||
pendingQuestAcceptNpcGuids_.clear();
|
||||
pendingQuestQueryIds_.clear();
|
||||
pendingLoginQuestResync_ = true;
|
||||
pendingLoginQuestResyncTimeout_ = 10.0f;
|
||||
LOG_INFO("Queued quest log resync for login (from server quest slots)");
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleClientCacheVersion(network::Packet& packet) {
|
||||
|
|
@ -9675,7 +9744,18 @@ void GameHandler::selectGossipQuest(uint32_t questId) {
|
|||
}
|
||||
return false;
|
||||
};
|
||||
const bool activeQuestConfirmedByServer = activeQuest && questInServerLogSlots(questId);
|
||||
const bool questInServerLog = questInServerLogSlots(questId);
|
||||
if (questInServerLog && !activeQuest) {
|
||||
addQuestToLocalLogIfMissing(questId, "Quest #" + std::to_string(questId), "");
|
||||
requestQuestQuery(questId, false);
|
||||
for (const auto& q : questLog_) {
|
||||
if (q.questId == questId) {
|
||||
activeQuest = &q;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const bool activeQuestConfirmedByServer = questInServerLog;
|
||||
// Only trust server quest-log slots for deciding "already accepted" flow.
|
||||
// Gossip icon values can differ across cores/expansions and misclassify
|
||||
// available quests as active, which blocks acceptance.
|
||||
|
|
@ -9733,31 +9813,113 @@ void GameHandler::handleQuestDetails(network::Packet& packet) {
|
|||
gossipWindowOpen = false;
|
||||
}
|
||||
|
||||
bool GameHandler::hasQuestInLog(uint32_t questId) const {
|
||||
for (const auto& q : questLog_) {
|
||||
if (q.questId == questId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void GameHandler::addQuestToLocalLogIfMissing(uint32_t questId, const std::string& title, const std::string& objectives) {
|
||||
if (questId == 0 || hasQuestInLog(questId)) return;
|
||||
QuestLogEntry entry;
|
||||
entry.questId = questId;
|
||||
entry.title = title.empty() ? ("Quest #" + std::to_string(questId)) : title;
|
||||
entry.objectives = objectives;
|
||||
questLog_.push_back(std::move(entry));
|
||||
}
|
||||
|
||||
bool GameHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) {
|
||||
if (lastPlayerFields_.empty()) return false;
|
||||
|
||||
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
|
||||
const uint8_t qStride = packetParsers_ ? packetParsers_->questLogStride() : 5;
|
||||
std::unordered_set<uint32_t> serverQuestIds;
|
||||
serverQuestIds.reserve(25);
|
||||
for (uint16_t slot = 0; slot < 25; ++slot) {
|
||||
const uint16_t idField = ufQuestStart + slot * qStride;
|
||||
auto it = lastPlayerFields_.find(idField);
|
||||
if (it == lastPlayerFields_.end()) continue;
|
||||
if (it->second != 0) serverQuestIds.insert(it->second);
|
||||
}
|
||||
|
||||
const size_t localBefore = questLog_.size();
|
||||
std::erase_if(questLog_, [&](const QuestLogEntry& q) {
|
||||
return q.questId == 0 || serverQuestIds.count(q.questId) == 0;
|
||||
});
|
||||
const size_t removed = localBefore - questLog_.size();
|
||||
|
||||
size_t added = 0;
|
||||
for (uint32_t questId : serverQuestIds) {
|
||||
if (hasQuestInLog(questId)) continue;
|
||||
addQuestToLocalLogIfMissing(questId, "Quest #" + std::to_string(questId), "");
|
||||
++added;
|
||||
}
|
||||
|
||||
if (forceQueryMetadata) {
|
||||
for (uint32_t questId : serverQuestIds) {
|
||||
requestQuestQuery(questId, false);
|
||||
}
|
||||
}
|
||||
|
||||
LOG_INFO("Quest log resync from server slots: server=", serverQuestIds.size(),
|
||||
" localBefore=", localBefore, " removed=", removed, " added=", added);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameHandler::clearPendingQuestAccept(uint32_t questId) {
|
||||
pendingQuestAcceptTimeouts_.erase(questId);
|
||||
pendingQuestAcceptNpcGuids_.erase(questId);
|
||||
}
|
||||
|
||||
void GameHandler::triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason) {
|
||||
if (questId == 0 || !socket || state != WorldState::IN_WORLD) return;
|
||||
|
||||
LOG_INFO("Quest accept resync: questId=", questId, " reason=", reason ? reason : "unknown");
|
||||
requestQuestQuery(questId, true);
|
||||
|
||||
if (npcGuid != 0) {
|
||||
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
|
||||
qsPkt.writeUInt64(npcGuid);
|
||||
socket->send(qsPkt);
|
||||
|
||||
auto queryPkt = packetParsers_
|
||||
? packetParsers_->buildQueryQuestPacket(npcGuid, questId)
|
||||
: QuestgiverQueryQuestPacket::build(npcGuid, questId);
|
||||
socket->send(queryPkt);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::acceptQuest() {
|
||||
if (!questDetailsOpen || state != WorldState::IN_WORLD || !socket) return;
|
||||
const uint32_t questId = currentQuestDetails.questId;
|
||||
if (questId == 0) return;
|
||||
uint64_t npcGuid = currentQuestDetails.npcGuid;
|
||||
if (pendingQuestAcceptTimeouts_.count(questId) != 0) {
|
||||
LOG_DEBUG("Ignoring duplicate quest accept while pending: questId=", questId);
|
||||
triggerQuestAcceptResync(questId, npcGuid, "duplicate-accept");
|
||||
questDetailsOpen = false;
|
||||
currentQuestDetails = QuestDetailsData{};
|
||||
return;
|
||||
}
|
||||
if (hasQuestInLog(questId)) {
|
||||
LOG_INFO("Ignoring duplicate quest accept already in local log: questId=", questId);
|
||||
questDetailsOpen = false;
|
||||
currentQuestDetails = QuestDetailsData{};
|
||||
return;
|
||||
}
|
||||
|
||||
// WotLK/TBC expect an additional trailing flag on CMSG_QUESTGIVER_ACCEPT_QUEST.
|
||||
// Classic/Turtle use the short form (guid + questId only).
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(currentQuestDetails.questId);
|
||||
packet.writeUInt32(questId);
|
||||
if (!isActiveExpansion("classic") && !isActiveExpansion("turtle")) {
|
||||
packet.writeUInt8(1); // from-gossip / auto-accept continuation flag
|
||||
}
|
||||
socket->send(packet);
|
||||
|
||||
// Add to quest log
|
||||
bool alreadyInLog = false;
|
||||
for (const auto& q : questLog_) {
|
||||
if (q.questId == currentQuestDetails.questId) { alreadyInLog = true; break; }
|
||||
}
|
||||
if (!alreadyInLog) {
|
||||
QuestLogEntry entry;
|
||||
entry.questId = currentQuestDetails.questId;
|
||||
entry.title = currentQuestDetails.title;
|
||||
entry.objectives = currentQuestDetails.objectives;
|
||||
questLog_.push_back(entry);
|
||||
}
|
||||
pendingQuestAcceptTimeouts_[questId] = 5.0f;
|
||||
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
|
||||
|
||||
questDetailsOpen = false;
|
||||
currentQuestDetails = QuestDetailsData{};
|
||||
|
|
@ -9776,6 +9938,7 @@ void GameHandler::declineQuest() {
|
|||
}
|
||||
|
||||
void GameHandler::abandonQuest(uint32_t questId) {
|
||||
clearPendingQuestAccept(questId);
|
||||
// Find the quest's index in our local log
|
||||
for (size_t i = 0; i < questLog_.size(); i++) {
|
||||
if (questLog_[i].questId == questId) {
|
||||
|
|
@ -9798,6 +9961,7 @@ void GameHandler::handleQuestRequestItems(network::Packet& packet) {
|
|||
LOG_WARNING("Failed to parse SMSG_QUESTGIVER_REQUEST_ITEMS");
|
||||
return;
|
||||
}
|
||||
clearPendingQuestAccept(data.questId);
|
||||
|
||||
// Expansion-safe fallback: COMPLETE_QUEST is the default flow.
|
||||
// If a server echoes REQUEST_ITEMS again while still completable,
|
||||
|
|
@ -9860,6 +10024,7 @@ void GameHandler::handleQuestOfferReward(network::Packet& packet) {
|
|||
LOG_WARNING("Failed to parse SMSG_QUESTGIVER_OFFER_REWARD");
|
||||
return;
|
||||
}
|
||||
clearPendingQuestAccept(data.questId);
|
||||
LOG_INFO("Quest offer reward: questId=", data.questId, " title=\"", data.title, "\"");
|
||||
if (pendingTurnInQuestId_ == data.questId) {
|
||||
pendingTurnInQuestId_ = 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue