mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix quest accept/abandon flow and expansion-specific accept packet format
Normalize WoW quest text tokens during parsing so quest titles/details no longer leak raw markup like and |n into UI. Apply to WotLK and Classic parser paths, including quest list parsing in GameHandler. Harden quest state handling by mapping abandon requests to authoritative server quest-log slots (PLAYER_QUEST_LOG_START) instead of local vector order, with a guarded fallback when update fields are unavailable. Improve accept de-duplication by trusting server slot state over stale local cache; allow re-accept when local/server state diverges and trigger resync semantics. Add expansion-aware CMSG_QUESTGIVER_ACCEPT_QUEST builders: WotLK sends guid+questId+unk1(uint32), while TBC/Classic/Turtle send guid+questId only. Wire GameHandler through PacketParsers for compatibility across expansions and cores.
This commit is contained in:
parent
73273a6ab5
commit
ace24e8ccc
7 changed files with 125 additions and 34 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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<int>(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<uint8_t>(i));
|
||||
socket->send(pkt);
|
||||
}
|
||||
questLog_.erase(questLog_.begin() + static_cast<ptrdiff_t>(i));
|
||||
return;
|
||||
localIndex = static_cast<int>(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<uint8_t>(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<ptrdiff_t>(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));
|
||||
|
|
|
|||
|
|
@ -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, "'");
|
||||
|
|
|
|||
|
|
@ -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) /
|
||||
|
|
|
|||
|
|
@ -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<int32_t>(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, "'");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue