game: fix TBC SMSG_QUESTGIVER_QUEST_DETAILS parsing by promoting Classic override to TbcPacketParsers

TBC 2.4.3 and Classic 1.12 share the same SMSG_QUESTGIVER_QUEST_DETAILS
format.  WotLK 3.3.5a adds three extra fields (informUnit u64, flags u32,
isFinished u8) that the base QuestDetailsParser::parse handles.  TBC had no
override, so it fell through to the WotLK heuristic which read flags+isFinished
as if they were TBC fields, misaligning choiceCount, rewardMoney, and rewardXp.

Fix: move parseQuestDetails from ClassicPacketParsers to TbcPacketParsers.
Classic inherits it unchanged (formats are identical).  Both expansions now
correctly parse: no informUnit, activateAccept(u8), suggestedPlayers(u32),
emote section, variable choice/reward item counts, rewardMoney, and rewardXp.
This commit is contained in:
Kelsi 2026-03-10 04:37:03 -07:00
parent 475e0c213c
commit 31ae689b2c
3 changed files with 73 additions and 64 deletions

View file

@ -353,6 +353,9 @@ public:
// TBC 2.4.3 CMSG_QUESTGIVER_QUERY_QUEST: guid(8) + questId(4) — no trailing
// isDialogContinued byte that WotLK added
network::Packet buildQueryQuestPacket(uint64_t npcGuid, uint32_t questId) override;
// TBC/Classic SMSG_QUESTGIVER_QUEST_DETAILS lacks informUnit(u64), flags(u32),
// isFinished(u8) that WotLK added; uses variable item counts + emote section.
bool parseQuestDetails(network::Packet& packet, QuestDetailsData& data) override;
};
/**
@ -402,7 +405,7 @@ public:
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;
// parseQuestDetails inherited from TbcPacketParsers (same format as TBC 2.4.3)
uint8_t questLogStride() const override { return 3; }
bool parseMonsterMove(network::Packet& packet, MonsterMoveData& data) override {
return MonsterMoveParser::parseVanilla(packet, data);

View file

@ -1557,69 +1557,6 @@ network::Packet ClassicPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, u
return packet;
}
// ============================================================================
// Classic SMSG_QUESTGIVER_QUEST_DETAILS — Vanilla 1.12 format
// WotLK inserts an informUnit GUID (8 bytes) between npcGuid and questId.
// Vanilla has: npcGuid(8) + questId(4) + title + details + objectives + ...
// ============================================================================
bool ClassicPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsData& data) {
if (packet.getSize() < 16) return false;
data.npcGuid = packet.readUInt64();
// Vanilla: questId follows immediately — no informUnit GUID
data.questId = packet.readUInt32();
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, "'");
return !data.title.empty() || data.questId != 0;
}
/*activateAccept*/ packet.readUInt8();
data.suggestedPlayers = packet.readUInt32();
// Vanilla 1.12: emote section before reward items
// Format: emoteCount(u32) + [delay(u32) + type(u32)] × emoteCount
if (packet.getReadPos() + 4 <= packet.getSize()) {
uint32_t emoteCount = packet.readUInt32();
for (uint32_t i = 0; i < emoteCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) {
packet.readUInt32(); // delay
packet.readUInt32(); // type
}
}
// Choice reward items: variable count + 3 uint32s each
if (packet.getReadPos() + 4 <= packet.getSize()) {
uint32_t choiceCount = packet.readUInt32();
for (uint32_t i = 0; i < choiceCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
packet.readUInt32(); // itemId
packet.readUInt32(); // count
packet.readUInt32(); // displayInfo
}
}
// Fixed reward items: variable count + 3 uint32s each
if (packet.getReadPos() + 4 <= packet.getSize()) {
uint32_t rewardCount = packet.readUInt32();
for (uint32_t i = 0; i < rewardCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
packet.readUInt32(); // itemId
packet.readUInt32(); // count
packet.readUInt32(); // displayInfo
}
}
if (packet.getReadPos() + 4 <= packet.getSize())
data.rewardMoney = packet.readUInt32();
// Vanilla 1.12 includes rewardXp after rewardMoney (same as WotLK)
if (packet.getReadPos() + 4 <= packet.getSize())
data.rewardXp = packet.readUInt32();
LOG_INFO("Quest details classic: id=", data.questId, " title='", data.title, "'");
return true;
}
// ============================================================================
// ClassicPacketParsers::parseCreatureQueryResponse
//

View file

@ -694,6 +694,75 @@ network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint3
return packet;
}
// ============================================================================
// TBC 2.4.3 SMSG_QUESTGIVER_QUEST_DETAILS
//
// TBC and Classic share the same format — neither has the WotLK-specific fields
// (informUnit GUID, flags uint32, isFinished uint8) that were added in 3.x.
//
// Format:
// npcGuid(8) + questId(4) + title + details + objectives
// + activateAccept(1) + suggestedPlayers(4)
// + emoteCount(4) + [delay(4)+type(4)] × emoteCount
// + choiceCount(4) + [itemId(4)+count(4)+displayInfo(4)] × choiceCount
// + rewardCount(4) + [itemId(4)+count(4)+displayInfo(4)] × rewardCount
// + rewardMoney(4) + rewardXp(4)
// ============================================================================
bool TbcPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsData& data) {
if (packet.getSize() < 16) return false;
data.npcGuid = packet.readUInt64();
data.questId = packet.readUInt32();
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 tbc/classic (short): id=", data.questId, " title='", data.title, "'");
return !data.title.empty() || data.questId != 0;
}
/*activateAccept*/ packet.readUInt8();
data.suggestedPlayers = packet.readUInt32();
// TBC/Classic: emote section before reward items
if (packet.getReadPos() + 4 <= packet.getSize()) {
uint32_t emoteCount = packet.readUInt32();
for (uint32_t i = 0; i < emoteCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) {
packet.readUInt32(); // delay
packet.readUInt32(); // type
}
}
// Choice reward items (variable count, up to QUEST_REWARD_CHOICES_COUNT)
if (packet.getReadPos() + 4 <= packet.getSize()) {
uint32_t choiceCount = packet.readUInt32();
for (uint32_t i = 0; i < choiceCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
packet.readUInt32(); // itemId
packet.readUInt32(); // count
packet.readUInt32(); // displayInfo
}
}
// Fixed reward items (variable count, up to QUEST_REWARDS_COUNT)
if (packet.getReadPos() + 4 <= packet.getSize()) {
uint32_t rewardCount = packet.readUInt32();
for (uint32_t i = 0; i < rewardCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
packet.readUInt32(); // itemId
packet.readUInt32(); // count
packet.readUInt32(); // displayInfo
}
}
if (packet.getReadPos() + 4 <= packet.getSize())
data.rewardMoney = packet.readUInt32();
if (packet.getReadPos() + 4 <= packet.getSize())
data.rewardXp = packet.readUInt32();
LOG_INFO("Quest details tbc/classic: id=", data.questId, " title='", data.title, "'");
return true;
}
// ============================================================================
// TBC 2.4.3 CMSG_QUESTGIVER_QUERY_QUEST
//