diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index cf032f6b..f1e758e9 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1130,6 +1130,7 @@ private: void clearPendingQuestAccept(uint32_t questId); void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason); 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 handleListInventory(network::Packet& packet); diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 99d0623c..42ad5b5f 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -156,6 +156,12 @@ public: return QuestgiverQueryQuestPacket::build(npcGuid, questId); // includes unk1 } + /** Build CMSG_QUESTGIVER_ACCEPT_QUEST. + * WotLK/AzerothCore expects trailing unk1 uint32; older expansions may not. */ + virtual network::Packet buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) { + return QuestgiverAcceptQuestPacket::build(npcGuid, questId); + } + /** Parse SMSG_QUESTGIVER_QUEST_DETAILS. * WotLK has an extra informUnit GUID before questId; Vanilla/Classic do not. */ virtual bool parseQuestDetails(network::Packet& packet, QuestDetailsData& data) { @@ -279,6 +285,7 @@ public: bool parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll = false) override; bool parseNameQueryResponse(network::Packet& packet, NameQueryResponseData& data) override; bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override; + network::Packet buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) override; }; /** @@ -324,6 +331,7 @@ public: bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override; uint8_t readQuestGiverStatus(network::Packet& packet) override; network::Packet buildQueryQuestPacket(uint64_t npcGuid, uint32_t questId) override; + network::Packet buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) override; bool parseQuestDetails(network::Packet& packet, QuestDetailsData& data) override; uint8_t questLogStride() const override { return 3; } bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override { diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index a880ae72..cd86efbb 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -16,6 +16,9 @@ namespace wowee { namespace game { +// Normalize WoW in-text tokens (e.g. "$B", "|n") into plain text suitable for UI. +std::string normalizeWowTextTokens(std::string text); + /** * SMSG_AUTH_CHALLENGE data (from server) * diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 9c3ab76a..fb218c4e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -9957,6 +9957,20 @@ bool GameHandler::hasQuestInLog(uint32_t questId) const { return false; } +int GameHandler::findQuestLogSlotIndexFromServer(uint32_t questId) const { + if (questId == 0 || lastPlayerFields_.empty()) return -1; + const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START); + const uint8_t qStride = packetParsers_ ? packetParsers_->questLogStride() : 5; + for (uint16_t slot = 0; slot < 25; ++slot) { + const uint16_t idField = ufQuestStart + slot * qStride; + auto it = lastPlayerFields_.find(idField); + if (it != lastPlayerFields_.end() && it->second == questId) { + return static_cast(slot); + } + } + return -1; +} + void GameHandler::addQuestToLocalLogIfMissing(uint32_t questId, const std::string& title, const std::string& objectives) { if (questId == 0 || hasQuestInLog(questId)) return; QuestLogEntry entry; @@ -10039,18 +10053,23 @@ void GameHandler::acceptQuest() { currentQuestDetails = QuestDetailsData{}; return; } - if (hasQuestInLog(questId)) { - LOG_INFO("Ignoring duplicate quest accept already in local log: questId=", questId); + const bool inLocalLog = hasQuestInLog(questId); + const int serverSlot = findQuestLogSlotIndexFromServer(questId); + if (serverSlot >= 0) { + LOG_INFO("Ignoring duplicate quest accept already in server quest log: questId=", questId, + " slot=", serverSlot); questDetailsOpen = false; currentQuestDetails = QuestDetailsData{}; return; } + if (inLocalLog) { + LOG_WARNING("Quest accept local/server mismatch, allowing re-accept: questId=", questId); + std::erase_if(questLog_, [&](const QuestLogEntry& q) { return q.questId == questId; }); + } - // Keep quest accept payload minimal and expansion-safe: guid + questId. - // Some server cores reject trailing bytes and throw ByteBufferException. - network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST)); - packet.writeUInt64(npcGuid); - packet.writeUInt32(questId); + network::Packet packet = packetParsers_ + ? packetParsers_->buildAcceptQuestPacket(npcGuid, questId) + : QuestgiverAcceptQuestPacket::build(npcGuid, questId); socket->send(packet); pendingQuestAcceptTimeouts_[questId] = 5.0f; pendingQuestAcceptNpcGuids_[questId] = npcGuid; @@ -10073,20 +10092,34 @@ 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++) { + int localIndex = -1; + for (size_t i = 0; i < questLog_.size(); ++i) { if (questLog_[i].questId == questId) { - // Tell server to remove it (slot index in server quest log) - // We send the local index; server maps it via PLAYER_QUEST_LOG fields - if (state == WorldState::IN_WORLD && socket) { - network::Packet pkt(wireOpcode(Opcode::CMSG_QUESTLOG_REMOVE_QUEST)); - pkt.writeUInt8(static_cast(i)); - socket->send(pkt); - } - questLog_.erase(questLog_.begin() + static_cast(i)); - return; + localIndex = static_cast(i); + break; } } + + int slotIndex = findQuestLogSlotIndexFromServer(questId); + if (slotIndex < 0 && localIndex >= 0) { + // Best-effort fallback if update fields are stale/missing. + slotIndex = localIndex; + LOG_WARNING("Abandon quest using local slot fallback: questId=", questId, " slot=", slotIndex); + } + + if (slotIndex >= 0 && slotIndex < 25) { + if (state == WorldState::IN_WORLD && socket) { + network::Packet pkt(wireOpcode(Opcode::CMSG_QUESTLOG_REMOVE_QUEST)); + pkt.writeUInt8(static_cast(slotIndex)); + socket->send(pkt); + } + } else { + LOG_WARNING("Abandon quest failed: no quest-log slot found for questId=", questId); + } + + if (localIndex >= 0) { + questLog_.erase(questLog_.begin() + static_cast(localIndex)); + } } void GameHandler::handleQuestRequestItems(network::Packet& packet) { @@ -10770,17 +10803,17 @@ void GameHandler::handleQuestgiverQuestList(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() >= 5) { q.questFlags = packet.readUInt32(); q.isRepeatable = packet.readUInt8(); - q.title = packet.readString(); + q.title = normalizeWowTextTokens(packet.readString()); if (q.title.empty()) { packet.setReadPos(titlePos); q.questFlags = 0; q.isRepeatable = 0; - q.title = packet.readString(); + q.title = normalizeWowTextTokens(packet.readString()); } } else { q.questFlags = 0; q.isRepeatable = 0; - q.title = packet.readString(); + q.title = normalizeWowTextTokens(packet.readString()); } if (q.questId != 0) { data.quests.push_back(std::move(q)); diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 9878a533..59da759e 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -705,7 +705,7 @@ bool ClassicPacketParsers::parseGossipMessage(network::Packet& packet, GossipMes // Classic: NO questFlags, NO isRepeatable quest.questFlags = 0; quest.isRepeatable = 0; - quest.title = packet.readString(); + quest.title = normalizeWowTextTokens(packet.readString()); data.quests.push_back(quest); } @@ -1220,6 +1220,14 @@ network::Packet ClassicPacketParsers::buildQueryQuestPacket(uint64_t npcGuid, ui return packet; } +network::Packet ClassicPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) { + network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST)); + packet.writeUInt64(npcGuid); + packet.writeUInt32(questId); + // Classic/Turtle: no trailing unk1 uint32 + return packet; +} + // ============================================================================ // Classic SMSG_QUESTGIVER_QUEST_DETAILS — Vanilla 1.12 format // WotLK inserts an informUnit GUID (8 bytes) between npcGuid and questId. @@ -1231,9 +1239,9 @@ bool ClassicPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetai data.npcGuid = packet.readUInt64(); // Vanilla: questId follows immediately — no informUnit GUID data.questId = packet.readUInt32(); - data.title = packet.readString(); - data.details = packet.readString(); - data.objectives = packet.readString(); + data.title = normalizeWowTextTokens(packet.readString()); + data.details = normalizeWowTextTokens(packet.readString()); + data.objectives = normalizeWowTextTokens(packet.readString()); if (packet.getReadPos() + 5 > packet.getSize()) { LOG_INFO("Quest details classic (short): id=", data.questId, " title='", data.title, "'"); diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 755c67d7..1d30175e 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -450,6 +450,14 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa return true; } +network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) { + network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST)); + packet.writeUInt64(npcGuid); + packet.writeUInt32(questId); + // TBC servers generally expect guid + questId only. + return packet; +} + // ============================================================================ // TBC parseAuraUpdate - SMSG_AURA_UPDATE doesn't exist in TBC // TBC uses inline aura update fields + SMSG_INIT_EXTRA_AURA_INFO_OBSOLETE (0x3A3) / diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 77af66e2..cdbfa234 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -14,6 +14,35 @@ namespace wowee { namespace game { +std::string normalizeWowTextTokens(std::string text) { + if (text.empty()) return text; + + size_t pos = 0; + while ((pos = text.find('$', pos)) != std::string::npos) { + if (pos + 1 >= text.size()) break; + const char code = text[pos + 1]; + if (code == 'b' || code == 'B') { + text.replace(pos, 2, "\n"); + ++pos; + } else { + ++pos; + } + } + + pos = 0; + while ((pos = text.find("|n", pos)) != std::string::npos) { + text.replace(pos, 2, "\n"); + ++pos; + } + pos = 0; + while ((pos = text.find("|N", pos)) != std::string::npos) { + text.replace(pos, 2, "\n"); + ++pos; + } + + return text; +} + network::Packet AuthSessionPacket::build(uint32_t build, const std::string& accountName, uint32_t clientSeed, @@ -3068,6 +3097,7 @@ network::Packet QuestgiverAcceptQuestPacket::build(uint64_t npcGuid, uint32_t qu network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST)); packet.writeUInt64(npcGuid); packet.writeUInt32(questId); + packet.writeUInt32(0); // AzerothCore/WotLK expects trailing unk1 return packet; } @@ -3081,15 +3111,15 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data) size_t preInform = packet.getReadPos(); /*informUnit*/ packet.readUInt64(); data.questId = packet.readUInt32(); - data.title = packet.readString(); + data.title = normalizeWowTextTokens(packet.readString()); if (data.title.empty() || data.questId > 100000) { // Likely vanilla format — rewind past informUnit packet.setReadPos(preInform); data.questId = packet.readUInt32(); - data.title = packet.readString(); + data.title = normalizeWowTextTokens(packet.readString()); } - data.details = packet.readString(); - data.objectives = packet.readString(); + data.details = normalizeWowTextTokens(packet.readString()); + data.objectives = normalizeWowTextTokens(packet.readString()); if (packet.getReadPos() + 10 > packet.getSize()) { LOG_INFO("Quest details (short): id=", data.questId, " title='", data.title, "'"); @@ -3162,7 +3192,7 @@ bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data quest.questLevel = static_cast(packet.readUInt32()); quest.questFlags = packet.readUInt32(); quest.isRepeatable = packet.readUInt8(); - quest.title = packet.readString(); + quest.title = normalizeWowTextTokens(packet.readString()); data.quests.push_back(quest); } @@ -3194,8 +3224,8 @@ bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsDa if (packet.getSize() - packet.getReadPos() < 20) return false; data.npcGuid = packet.readUInt64(); data.questId = packet.readUInt32(); - data.title = packet.readString(); - data.completionText = packet.readString(); + data.title = normalizeWowTextTokens(packet.readString()); + data.completionText = normalizeWowTextTokens(packet.readString()); if (packet.getReadPos() + 9 > packet.getSize()) { LOG_INFO("Quest request items (short): id=", data.questId, " title='", data.title, "'"); @@ -3283,8 +3313,8 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData if (packet.getSize() - packet.getReadPos() < 20) return false; data.npcGuid = packet.readUInt64(); data.questId = packet.readUInt32(); - data.title = packet.readString(); - data.rewardText = packet.readString(); + data.title = normalizeWowTextTokens(packet.readString()); + data.rewardText = normalizeWowTextTokens(packet.readString()); if (packet.getReadPos() + 10 > packet.getSize()) { LOG_INFO("Quest offer reward (short): id=", data.questId, " title='", data.title, "'");