diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 127a168a..b64e9478 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1437,6 +1437,9 @@ private: // Quest turn-in bool questRequestItemsOpen_ = false; QuestRequestItemsData currentQuestRequestItems_; + uint32_t pendingTurnInQuestId_ = 0; + uint64_t pendingTurnInNpcGuid_ = 0; + bool pendingTurnInRewardRequest_ = false; bool questOfferRewardOpen_ = false; QuestOfferRewardData currentQuestOfferReward_; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 10aa9430..1e0a0593 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2006,6 +2006,12 @@ public: static network::Packet build(uint64_t npcGuid, uint32_t questId); }; +/** CMSG_QUESTGIVER_REQUEST_REWARD packet builder */ +class QuestgiverRequestRewardPacket { +public: + static network::Packet build(uint64_t npcGuid, uint32_t questId); +}; + /** CMSG_QUESTGIVER_CHOOSE_REWARD packet builder */ class QuestgiverChooseRewardPacket { public: diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a0c2d2df..bc22b4c3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -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{}; } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 736b98a4..958912e0 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -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 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); diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 33a90cd7..c8ffb22a 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -6161,49 +6161,22 @@ std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game: // Helper to trim whitespace auto trim = [](std::string& s) { - s.erase(0, s.find_first_not_of(" \t\n\r")); - s.erase(s.find_last_not_of(" \t\n\r") + 1); + const char* ws = " \t\n\r"; + size_t start = s.find_first_not_of(ws); + if (start == std::string::npos) { s.clear(); return; } + size_t end = s.find_last_not_of(ws); + s = s.substr(start, end - start + 1); }; - // Replace simple placeholders first - // $n = player name - // $p = subject pronoun (he/she/they) - // $o = object pronoun (him/her/them) - // $s = possessive adjective (his/her/their) - // $S = possessive pronoun (his/hers/theirs) + // Replace $g/$G placeholders first. size_t pos = 0; while ((pos = result.find('$', pos)) != std::string::npos) { if (pos + 1 >= result.length()) break; + char marker = result[pos + 1]; + if (marker != 'g' && marker != 'G') { pos++; continue; } - char code = result[pos + 1]; - std::string replacement; - - switch (code) { - case 'n': case 'N': replacement = playerName; break; - case 'p': replacement = pronouns.subject; break; - case 'o': replacement = pronouns.object; break; - case 's': replacement = pronouns.possessive; break; - case 'S': replacement = pronouns.possessiveP; break; - case 'g': - // Handle $g separately below - pos++; - continue; - default: - pos++; - continue; - } - - // Replace the placeholder - result.replace(pos, 2, replacement); - pos += replacement.length(); - } - - // Find and replace all $g placeholders (gender-specific text) - // Format: $g:; or $g::; - pos = 0; - while ((pos = result.find("$g", pos)) != std::string::npos) { size_t endPos = result.find(';', pos); - if (endPos == std::string::npos) break; + if (endPos == std::string::npos) { pos += 2; continue; } std::string placeholder = result.substr(pos + 2, endPos - pos - 2); @@ -6261,6 +6234,46 @@ std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game: pos += replacement.length(); } + // Replace simple placeholders. + // $n = player name + // $p = subject pronoun (he/she/they) + // $o = object pronoun (him/her/them) + // $s = possessive adjective (his/her/their) + // $S = possessive pronoun (his/hers/theirs) + // $b/$B = line break + pos = 0; + while ((pos = result.find('$', pos)) != std::string::npos) { + if (pos + 1 >= result.length()) break; + + char code = result[pos + 1]; + std::string replacement; + switch (code) { + case 'n': case 'N': replacement = playerName; break; + case 'p': replacement = pronouns.subject; break; + case 'o': replacement = pronouns.object; break; + case 's': replacement = pronouns.possessive; break; + case 'S': replacement = pronouns.possessiveP; break; + case 'b': case 'B': replacement = "\n"; break; + case 'g': case 'G': pos++; continue; + default: pos++; continue; + } + + result.replace(pos, 2, replacement); + pos += replacement.length(); + } + + // WoW markup linebreak token. + pos = 0; + while ((pos = result.find("|n", pos)) != std::string::npos) { + result.replace(pos, 2, "\n"); + pos += 1; + } + pos = 0; + while ((pos = result.find("|N", pos)) != std::string::npos) { + result.replace(pos, 2, "\n"); + pos += 1; + } + return result; } diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index efc1dc57..ad7df081 100644 --- a/src/ui/quest_log_screen.cpp +++ b/src/ui/quest_log_screen.cpp @@ -95,7 +95,7 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler case 's': replacement = pronouns.possessive; break; case 'S': replacement = pronouns.possessiveP; break; case 'r': replacement = pronouns.object; break; - case 'b': replacement = "\n"; break; + case 'b': case 'B': replacement = "\n"; break; case 'g': case 'G': pos++; continue; default: pos++; continue; } @@ -110,6 +110,11 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler result.replace(pos, 2, "\n"); pos += 1; } + pos = 0; + while ((pos = result.find("|N", pos)) != std::string::npos) { + result.replace(pos, 2, "\n"); + pos += 1; + } return result; }