fix: correct quest offer reward parser and trade slot trail size

- QuestOfferRewardParser: replace 4-variant heuristic with 0..16 byte
  prefix scan × fixed/variable arrays (34 candidates total).  AzerothCore
  WotLK 3.3.5a sends uint32 autoFinish + uint32 suggestedPlayers = 8 bytes
  before emoteCount; old uint8 read caused 3-byte misalignment, producing
  wrong item IDs and missing icons on quest reward windows.  Scoring now
  strongly favours the 8-byte prefix and exact byte consumption.
- Quest reward tooltip: delegate to InventoryScreen::renderItemTooltip()
  for full stats (armor, DPS, stats, bind type, etc.); show "Loading…"
  while item data is still fetching instead of showing nothing.
- SMSG_TRADE_STATUS_EXTENDED: fix SLOT_TRAIL 49→52 bytes.  AC 3.3.5a
  sends giftCreatorGuid(8) + 6 enchant slots(24) + randPropId(4) +
  suffixFactor(4) + durability(4) + maxDurability(4) + createPlayedTime(4)
  = 52 bytes after isWrapped; wrong skip misaligned all subsequent slots.
This commit is contained in:
Kelsi 2026-03-11 01:00:08 -07:00
parent 170ff1597c
commit 568c566e1a
3 changed files with 70 additions and 49 deletions

View file

@ -19167,16 +19167,16 @@ void GameHandler::handleTradeStatusExtended(network::Packet& packet) {
uint32_t displayId = packet.readUInt32();
uint32_t stackCount = packet.readUInt32();
// isWrapped + giftCreatorGuid + several enchant fields — skip them all
// We need at least 1+8+4*5 = 29 bytes for the rest of this slot entry
bool isWrapped = false;
if (packet.getSize() - packet.getReadPos() >= 1) {
isWrapped = (packet.readUInt8() != 0);
}
// Skip giftCreatorGuid (8) + enchantId*5 (20) + suffixFactor (4) + randPropId (4) + lockId (4)
// + maxDurability (4) + durability (4) = 49 bytes
// Plus if wrapped: giftCreatorGuid already consumed; additional guid = 0
constexpr size_t SLOT_TRAIL = 49;
// AzerothCore 3.3.5a SendUpdateTrade() field order after isWrapped:
// giftCreatorGuid (8) + PERM enchant (4) + SOCK enchants×3 (12)
// + BONUS enchant (4) + TEMP enchant (4) [total enchants: 24]
// + randomPropertyId (4) + suffixFactor (4)
// + durability (4) + maxDurability (4) + createPlayedTime (4) = 52 bytes
constexpr size_t SLOT_TRAIL = 52;
if (packet.getSize() - packet.getReadPos() >= SLOT_TRAIL) {
packet.setReadPos(packet.getReadPos() + SLOT_TRAIL);
} else {

View file

@ -3666,11 +3666,19 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
data.title = normalizeWowTextTokens(packet.readString());
data.rewardText = normalizeWowTextTokens(packet.readString());
if (packet.getReadPos() + 10 > packet.getSize()) {
if (packet.getReadPos() + 8 > packet.getSize()) {
LOG_DEBUG("Quest offer reward (short): id=", data.questId, " title='", data.title, "'");
return true;
}
// After the two strings the packet contains a variable prefix (autoFinish + optional fields)
// before the emoteCount. Different expansions and server emulator versions differ:
// Classic 1.12 : uint8 autoFinish + uint32 suggestedPlayers = 5 bytes
// TBC 2.4.3 : uint32 autoFinish + uint32 suggestedPlayers = 8 bytes (variable arrays)
// WotLK 3.3.5a : uint32 autoFinish + uint32 suggestedPlayers = 8 bytes (fixed 6/4 arrays)
// Some vanilla-family servers omit autoFinish entirely (0 bytes of prefix).
// We scan prefix sizes 0..16 bytes with both fixed and variable array layouts, scoring each.
struct ParsedTail {
uint32_t rewardMoney = 0;
uint32_t rewardXp = 0;
@ -3678,28 +3686,27 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
std::vector<QuestRewardItem> fixedRewards;
bool ok = false;
int score = -1000;
size_t prefixSkip = 0;
bool fixedArrays = false;
};
auto parseTail = [&](size_t startPos, bool hasFlags, bool fixedArrays) -> ParsedTail {
auto parseTail = [&](size_t startPos, size_t prefixSkip, bool fixedArrays) -> ParsedTail {
ParsedTail out;
out.prefixSkip = prefixSkip;
out.fixedArrays = fixedArrays;
packet.setReadPos(startPos);
if (packet.getReadPos() + 1 > packet.getSize()) return out;
/*autoFinish*/ packet.readUInt8();
if (hasFlags) {
if (packet.getReadPos() + 4 > packet.getSize()) return out;
/*flags*/ packet.readUInt32();
}
if (packet.getReadPos() + 4 > packet.getSize()) return out;
/*suggestedPlayers*/ packet.readUInt32();
// Skip the prefix bytes (autoFinish + optional suggestedPlayers before emoteCount)
if (packet.getReadPos() + prefixSkip > packet.getSize()) return out;
packet.setReadPos(packet.getReadPos() + prefixSkip);
if (packet.getReadPos() + 4 > packet.getSize()) return out;
uint32_t emoteCount = packet.readUInt32();
if (emoteCount > 64) return out; // guard against misalignment
if (emoteCount > 32) return out; // guard against misalignment
for (uint32_t i = 0; i < emoteCount; ++i) {
if (packet.getReadPos() + 8 > packet.getSize()) return out;
packet.readUInt32(); // delay
packet.readUInt32(); // emote
packet.readUInt32(); // emote type
}
if (packet.getReadPos() + 4 > packet.getSize()) return out;
@ -3717,7 +3724,7 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
item.choiceSlot = i;
if (item.itemId > 0) {
out.choiceRewards.push_back(item);
nonZeroChoice++;
++nonZeroChoice;
}
}
@ -3735,7 +3742,7 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
item.displayInfoId = packet.readUInt32();
if (item.itemId > 0) {
out.fixedRewards.push_back(item);
nonZeroFixed++;
++nonZeroFixed;
}
}
@ -3746,43 +3753,56 @@ bool QuestOfferRewardParser::parse(network::Packet& packet, QuestOfferRewardData
out.ok = true;
out.score = 0;
if (hasFlags) out.score += 1;
if (fixedArrays) out.score += 1;
// Prefer the standard WotLK/TBC 8-byte prefix (uint32 autoFinish + uint32 suggestedPlayers)
if (prefixSkip == 8) out.score += 3;
else if (prefixSkip == 5) out.score += 1; // Classic uint8 autoFinish + uint32 suggestedPlayers
// Prefer fixed arrays (WotLK/TBC servers always send 6+4 slots)
if (fixedArrays) out.score += 2;
// Valid counts
if (choiceCount <= 6) out.score += 3;
if (rewardCount <= 4) out.score += 3;
if (fixedArrays) {
if (nonZeroChoice <= choiceCount) out.score += 3;
if (nonZeroFixed <= rewardCount) out.score += 3;
} else {
out.score += 3; // variable arrays align naturally with count
}
if (packet.getReadPos() <= packet.getSize()) out.score += 2;
// All non-zero items are within declared counts
if (nonZeroChoice <= choiceCount) out.score += 2;
if (nonZeroFixed <= rewardCount) out.score += 2;
// No bytes left over (or only a few)
size_t remaining = packet.getSize() - packet.getReadPos();
if (remaining <= 32) out.score += 2;
if (remaining == 0) out.score += 5;
else if (remaining <= 4) out.score += 3;
else if (remaining <= 8) out.score += 2;
else if (remaining <= 16) out.score += 1;
else out.score -= static_cast<int>(remaining / 4);
// Plausible money/XP values
if (out.rewardMoney < 5000000u) out.score += 1; // < 500g
if (out.rewardXp < 200000u) out.score += 1; // < 200k XP
return out;
};
size_t tailStart = packet.getReadPos();
ParsedTail a = parseTail(tailStart, true, true); // WotLK-like (flags + fixed 6/4 arrays)
ParsedTail b = parseTail(tailStart, false, true); // no flags + fixed 6/4 arrays
ParsedTail c = parseTail(tailStart, true, false); // flags + variable arrays
ParsedTail d = parseTail(tailStart, false, false); // classic-like variable arrays
// Try prefix sizes 0..16 bytes with both fixed and variable array layouts
std::vector<ParsedTail> candidates;
candidates.reserve(34);
for (size_t skip = 0; skip <= 16; ++skip) {
candidates.push_back(parseTail(tailStart, skip, true)); // fixed arrays
candidates.push_back(parseTail(tailStart, skip, false)); // variable arrays
}
const ParsedTail* best = nullptr;
for (const ParsedTail* cand : {&a, &b, &c, &d}) {
if (!cand->ok) continue;
if (!best || cand->score > best->score) best = cand;
for (const auto& cand : candidates) {
if (!cand.ok) continue;
if (!best || cand.score > best->score) best = &cand;
}
if (best) {
data.choiceRewards = best->choiceRewards;
data.fixedRewards = best->fixedRewards;
data.rewardMoney = best->rewardMoney;
data.rewardXp = best->rewardXp;
data.fixedRewards = best->fixedRewards;
data.rewardMoney = best->rewardMoney;
data.rewardXp = best->rewardXp;
}
LOG_DEBUG("Quest offer reward: id=", data.questId, " title='", data.title,
"' choices=", data.choiceRewards.size(), " fixed=", data.fixedRewards.size());
"' choices=", data.choiceRewards.size(), " fixed=", data.fixedRewards.size(),
" prefix=", (best ? best->prefixSkip : size_t(0)),
(best && best->fixedArrays ? " fixed" : " var"));
return true;
}

View file

@ -7530,15 +7530,16 @@ void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
return {iconTex, col};
};
// Helper: show item tooltip
auto rewardItemTooltip = [&](const game::QuestRewardItem& ri, ImVec4 nameCol) {
// Helper: show full item tooltip (reuses InventoryScreen's rich tooltip)
auto rewardItemTooltip = [&](const game::QuestRewardItem& ri, ImVec4 /*nameCol*/) {
auto* info = gameHandler.getItemInfo(ri.itemId);
if (!info || !info->valid) return;
ImGui::BeginTooltip();
ImGui::TextColored(nameCol, "%s", info->name.c_str());
if (!info->description.empty())
ImGui::TextWrapped("%s", info->description.c_str());
ImGui::EndTooltip();
if (!info || !info->valid) {
ImGui::BeginTooltip();
ImGui::TextDisabled("Loading item data...");
ImGui::EndTooltip();
return;
}
inventoryScreen.renderItemTooltip(*info);
};
if (!quest.choiceRewards.empty()) {