Fix quest turn-in flow and WoW quest text placeholders

This commit is contained in:
Kelsi 2026-02-19 01:12:14 -08:00
parent a37004db03
commit 8efc21115f
6 changed files with 170 additions and 56 deletions

View file

@ -1771,6 +1771,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
// Quest query failed - parse failure reason
if (packet.getSize() - packet.getReadPos() >= 4) {
uint32_t failReason = packet.readUInt32();
pendingTurnInRewardRequest_ = false;
const char* reasonStr = "Unknown";
switch (failReason) {
case 0: reasonStr = "Don't have quest"; break;
@ -1794,6 +1795,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() >= 4) {
uint32_t questId = packet.readUInt32();
LOG_INFO("Quest completed: questId=", questId);
if (pendingTurnInQuestId_ == questId) {
pendingTurnInQuestId_ = 0;
pendingTurnInNpcGuid_ = 0;
pendingTurnInRewardRequest_ = false;
}
for (auto it = questLog_.begin(); it != questLog_.end(); ++it) {
if (it->questId == questId) {
questLog_.erase(it);
@ -8765,6 +8771,20 @@ void GameHandler::handleQuestRequestItems(network::Packet& packet) {
LOG_WARNING("Failed to parse SMSG_QUESTGIVER_REQUEST_ITEMS");
return;
}
// Expansion-safe fallback: COMPLETE_QUEST is the default flow.
// If a server echoes REQUEST_ITEMS again while still completable,
// request the reward explicitly once.
if (pendingTurnInRewardRequest_ &&
data.questId == pendingTurnInQuestId_ &&
data.npcGuid == pendingTurnInNpcGuid_ &&
data.isCompletable() &&
socket) {
auto rewardReq = QuestgiverRequestRewardPacket::build(data.npcGuid, data.questId);
socket->send(rewardReq);
pendingTurnInRewardRequest_ = false;
}
currentQuestRequestItems_ = data;
questRequestItemsOpen_ = true;
gossipWindowOpen = false;
@ -8814,6 +8834,11 @@ void GameHandler::handleQuestOfferReward(network::Packet& packet) {
return;
}
LOG_INFO("Quest offer reward: questId=", data.questId, " title=\"", data.title, "\"");
if (pendingTurnInQuestId_ == data.questId) {
pendingTurnInQuestId_ = 0;
pendingTurnInNpcGuid_ = 0;
pendingTurnInRewardRequest_ = false;
}
currentQuestOfferReward_ = data;
questOfferRewardOpen_ = true;
questRequestItemsOpen_ = false;
@ -8829,6 +8854,11 @@ void GameHandler::handleQuestOfferReward(network::Packet& packet) {
void GameHandler::completeQuest() {
if (!questRequestItemsOpen_ || state != WorldState::IN_WORLD || !socket) return;
pendingTurnInQuestId_ = currentQuestRequestItems_.questId;
pendingTurnInNpcGuid_ = currentQuestRequestItems_.npcGuid;
pendingTurnInRewardRequest_ = currentQuestRequestItems_.isCompletable();
// Default quest turn-in flow used by all branches.
auto packet = QuestgiverCompleteQuestPacket::build(
currentQuestRequestItems_.npcGuid, currentQuestRequestItems_.questId);
socket->send(packet);
@ -8837,6 +8867,7 @@ void GameHandler::completeQuest() {
}
void GameHandler::closeQuestRequestItems() {
pendingTurnInRewardRequest_ = false;
questRequestItemsOpen_ = false;
currentQuestRequestItems_ = QuestRequestItemsData{};
}
@ -8849,6 +8880,9 @@ void GameHandler::chooseQuestReward(uint32_t rewardIndex) {
auto packet = QuestgiverChooseRewardPacket::build(
npcGuid, currentQuestOfferReward_.questId, rewardIndex);
socket->send(packet);
pendingTurnInQuestId_ = 0;
pendingTurnInNpcGuid_ = 0;
pendingTurnInRewardRequest_ = false;
questOfferRewardOpen_ = false;
currentQuestOfferReward_ = QuestOfferRewardData{};
@ -8861,6 +8895,7 @@ void GameHandler::chooseQuestReward(uint32_t rewardIndex) {
}
void GameHandler::closeQuestOfferReward() {
pendingTurnInRewardRequest_ = false;
questOfferRewardOpen_ = false;
currentQuestOfferReward_ = QuestOfferRewardData{};
}

View file

@ -3110,30 +3110,75 @@ bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsDa
return true;
}
// emoteDelay (uint32) + emoteId (uint32) + autoFinish (uint8) = 9 bytes
// (same format in vanilla 1.12 and WotLK 3.3.5a)
/*uint32_t emoteDelay =*/ packet.readUInt32();
/*uint32_t emoteId =*/ packet.readUInt32();
/*uint8_t autoFinish =*/ packet.readUInt8();
struct ParsedTail {
uint32_t requiredMoney = 0;
uint32_t completableFlags = 0;
std::vector<QuestRewardItem> requiredItems;
bool ok = false;
int score = -1;
};
if (packet.getReadPos() + 4 > packet.getSize()) return true;
data.requiredMoney = packet.readUInt32();
auto parseTail = [&](size_t startPos, bool closeFlagIsU32) -> ParsedTail {
ParsedTail out;
packet.setReadPos(startPos);
if (packet.getReadPos() + 4 > packet.getSize()) return true;
uint32_t requiredItemCount = packet.readUInt32();
if (packet.getReadPos() + 8 > packet.getSize()) return out;
/*uint32_t emoteDelay =*/ packet.readUInt32();
/*uint32_t emoteId =*/ packet.readUInt32();
for (uint32_t i = 0; i < requiredItemCount && requiredItemCount < 20; ++i) {
if (packet.getReadPos() + 12 > packet.getSize()) break;
QuestRewardItem item;
item.itemId = packet.readUInt32();
item.count = packet.readUInt32();
item.displayInfoId = packet.readUInt32();
if (item.itemId > 0)
data.requiredItems.push_back(item);
if (closeFlagIsU32) {
if (packet.getReadPos() + 4 > packet.getSize()) return out;
/*uint32_t closeOnCancel =*/ packet.readUInt32();
} else {
if (packet.getReadPos() + 1 > packet.getSize()) return out;
/*uint8_t autoFinish =*/ packet.readUInt8();
}
if (packet.getReadPos() + 8 > packet.getSize()) return out;
out.requiredMoney = packet.readUInt32();
uint32_t requiredItemCount = packet.readUInt32();
if (requiredItemCount > 64) return out; // sanity guard against misalignment
out.requiredItems.reserve(requiredItemCount);
for (uint32_t i = 0; i < requiredItemCount; ++i) {
if (packet.getReadPos() + 12 > packet.getSize()) return out;
QuestRewardItem item;
item.itemId = packet.readUInt32();
item.count = packet.readUInt32();
item.displayInfoId = packet.readUInt32();
if (item.itemId != 0) out.requiredItems.push_back(item);
}
if (packet.getReadPos() + 4 > packet.getSize()) return out;
out.completableFlags = packet.readUInt32();
out.ok = true;
// Prefer layouts that produce plausible quest-requirement shapes.
out.score = 0;
if (requiredItemCount <= 6) out.score += 4;
if (out.requiredItems.size() == requiredItemCount) out.score += 3;
if ((out.completableFlags & ~0x3u) == 0) out.score += 2;
if (closeFlagIsU32) out.score += 1; // classic cores often use 32-bit here
return out;
};
size_t tailStart = packet.getReadPos();
ParsedTail parseU8 = parseTail(tailStart, false);
ParsedTail parseU32 = parseTail(tailStart, true);
const ParsedTail* chosen = nullptr;
if (parseU8.ok && parseU32.ok) {
chosen = (parseU32.score >= parseU8.score) ? &parseU32 : &parseU8;
} else if (parseU32.ok) {
chosen = &parseU32;
} else if (parseU8.ok) {
chosen = &parseU8;
} else {
return true;
}
if (packet.getReadPos() + 4 > packet.getSize()) return true;
data.completableFlags = packet.readUInt32();
data.requiredMoney = chosen->requiredMoney;
data.completableFlags = chosen->completableFlags;
data.requiredItems = chosen->requiredItems;
LOG_INFO("Quest request items: id=", data.questId, " title='", data.title,
"' items=", data.requiredItems.size(), " completable=", data.isCompletable());
@ -3209,6 +3254,13 @@ network::Packet QuestgiverCompleteQuestPacket::build(uint64_t npcGuid, uint32_t
return packet;
}
network::Packet QuestgiverRequestRewardPacket::build(uint64_t npcGuid, uint32_t questId) {
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_REQUEST_REWARD));
packet.writeUInt64(npcGuid);
packet.writeUInt32(questId);
return packet;
}
network::Packet QuestgiverChooseRewardPacket::build(uint64_t npcGuid, uint32_t questId, uint32_t rewardIndex) {
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_CHOOSE_REWARD));
packet.writeUInt64(npcGuid);