mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-25 00:20:16 +00:00
feat: restore quest kill counts from update fields using parsed objectives
Parse kill/item objectives from SMSG_QUEST_QUERY_RESPONSE binary data: - extractQuestQueryObjectives() scans past the fixed integer header and variable-length strings to reach the 4 entity + 6 item objective entries (using known offsets: 40 fields for Classic/TBC, 55 for WotLK) - Objectives stored in QuestLogEntry.killObjectives / itemObjectives arrays - After storing, applyPackedKillCountsFromFields() reads 6-bit packed counts from update-field slots (stride+2 / stride+3) and populates killCounts using the parsed creature/GO entry IDs as keys This means on login, quests that were in progress show correct kill count progress (e.g. "2/5 Defias Bandits killed") without waiting for the first server SMSG_QUESTUPDATE_ADD_KILL notification.
This commit is contained in:
parent
7e55d21cdd
commit
73439a4457
2 changed files with 174 additions and 1 deletions
|
|
@ -357,6 +357,70 @@ QuestQueryTextCandidate pickBestQuestQueryTexts(const std::vector<uint8_t>& data
|
|||
|
||||
return best;
|
||||
}
|
||||
|
||||
// Parse kill/item objectives from SMSG_QUEST_QUERY_RESPONSE raw data.
|
||||
// Returns true if the objective block was found and at least one entry read.
|
||||
//
|
||||
// Format after the fixed integer header (40*4 Classic or 55*4 WotLK bytes post questId+questMethod):
|
||||
// N strings (title, objectives, details, endText; + completedText for WotLK)
|
||||
// 4x { int32 npcOrGoId, uint32 count } -- entity (kill/interact) objectives
|
||||
// 6x { uint32 itemId, uint32 count } -- item collect objectives
|
||||
// 4x cstring -- per-objective display text
|
||||
//
|
||||
// We use the same fixed-offset heuristic as pickBestQuestQueryTexts and then scan past
|
||||
// the string section to reach the objective data.
|
||||
struct QuestQueryObjectives {
|
||||
struct Kill { int32_t npcOrGoId; uint32_t required; };
|
||||
struct Item { uint32_t itemId; uint32_t required; };
|
||||
std::array<Kill, 4> kills{};
|
||||
std::array<Item, 6> items{};
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
static uint32_t readU32At(const std::vector<uint8_t>& d, size_t pos) {
|
||||
return static_cast<uint32_t>(d[pos])
|
||||
| (static_cast<uint32_t>(d[pos + 1]) << 8)
|
||||
| (static_cast<uint32_t>(d[pos + 2]) << 16)
|
||||
| (static_cast<uint32_t>(d[pos + 3]) << 24);
|
||||
}
|
||||
|
||||
QuestQueryObjectives extractQuestQueryObjectives(const std::vector<uint8_t>& data, bool classicHint) {
|
||||
QuestQueryObjectives out;
|
||||
if (data.size() < 16) return out;
|
||||
|
||||
const size_t base = 8; // questId(4) + questMethod(4) already at start
|
||||
// Number of fixed uint32 fields before the first string (title).
|
||||
const size_t fixedFields = classicHint ? 40u : 55u;
|
||||
size_t pos = base + fixedFields * 4;
|
||||
|
||||
// Number of strings before the objective data.
|
||||
const int nStrings = classicHint ? 4 : 5;
|
||||
|
||||
// Scan past each string (null-terminated).
|
||||
for (int si = 0; si < nStrings; ++si) {
|
||||
while (pos < data.size() && data[pos] != 0) ++pos;
|
||||
if (pos >= data.size()) return out;
|
||||
++pos; // consume null terminator
|
||||
}
|
||||
|
||||
// Read 4 entity objectives: int32 npcOrGoId + uint32 count each.
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
if (pos + 8 > data.size()) return out;
|
||||
out.kills[i].npcOrGoId = static_cast<int32_t>(readU32At(data, pos)); pos += 4;
|
||||
out.kills[i].required = readU32At(data, pos); pos += 4;
|
||||
}
|
||||
|
||||
// Read 6 item objectives: uint32 itemId + uint32 count each.
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
if (pos + 8 > data.size()) break;
|
||||
out.items[i].itemId = readU32At(data, pos); pos += 4;
|
||||
out.items[i].required = readU32At(data, pos); pos += 4;
|
||||
}
|
||||
|
||||
out.valid = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
|
|
@ -4253,6 +4317,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
|
||||
const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() == 3;
|
||||
const QuestQueryTextCandidate parsed = pickBestQuestQueryTexts(packet.getData(), isClassicLayout);
|
||||
const QuestQueryObjectives objs = extractQuestQueryObjectives(packet.getData(), isClassicLayout);
|
||||
|
||||
for (auto& q : questLog_) {
|
||||
if (q.questId != questId) continue;
|
||||
|
|
@ -4276,6 +4341,26 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
(q.objectives.empty() || q.objectives.size() < 16)) {
|
||||
q.objectives = parsed.objectives;
|
||||
}
|
||||
|
||||
// Store structured kill/item objectives for later kill-count restoration.
|
||||
if (objs.valid) {
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
q.killObjectives[i].npcOrGoId = objs.kills[i].npcOrGoId;
|
||||
q.killObjectives[i].required = objs.kills[i].required;
|
||||
}
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
q.itemObjectives[i].itemId = objs.items[i].itemId;
|
||||
q.itemObjectives[i].required = objs.items[i].required;
|
||||
}
|
||||
// Now that we have the objective creature IDs, apply any packed kill
|
||||
// counts from the player update fields that arrived at login.
|
||||
applyPackedKillCountsFromFields(q);
|
||||
LOG_DEBUG("Quest ", questId, " objectives parsed: kills=[",
|
||||
objs.kills[0].npcOrGoId, "/", objs.kills[0].required, ", ",
|
||||
objs.kills[1].npcOrGoId, "/", objs.kills[1].required, ", ",
|
||||
objs.kills[2].npcOrGoId, "/", objs.kills[2].required, ", ",
|
||||
objs.kills[3].npcOrGoId, "/", objs.kills[3].required, "]");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -14913,6 +14998,76 @@ void GameHandler::applyQuestStateFromFields(const std::map<uint16_t, uint32_t>&
|
|||
}
|
||||
}
|
||||
|
||||
// Extract packed 6-bit kill/objective counts from WotLK/TBC/Classic quest-log update fields
|
||||
// and populate quest.killCounts + quest.itemCounts using the structured objectives obtained
|
||||
// from a prior SMSG_QUEST_QUERY_RESPONSE. Silently does nothing if objectives are absent.
|
||||
void GameHandler::applyPackedKillCountsFromFields(QuestLogEntry& quest) {
|
||||
if (lastPlayerFields_.empty()) return;
|
||||
|
||||
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
|
||||
if (ufQuestStart == 0xFFFF) return;
|
||||
|
||||
const uint8_t qStride = packetParsers_ ? packetParsers_->questLogStride() : 5;
|
||||
if (qStride < 3) return; // Need at least id + state + packed-counts field
|
||||
|
||||
// Find which server slot this quest occupies.
|
||||
int slot = findQuestLogSlotIndexFromServer(quest.questId);
|
||||
if (slot < 0) return;
|
||||
|
||||
// Packed count fields: stride+2 (all expansions), stride+3 (WotLK only, stride==5)
|
||||
const uint16_t countField1 = ufQuestStart + static_cast<uint16_t>(slot) * qStride + 2;
|
||||
const uint16_t countField2 = (qStride >= 5)
|
||||
? static_cast<uint16_t>(countField1 + 1)
|
||||
: static_cast<uint16_t>(0xFFFF);
|
||||
|
||||
auto f1It = lastPlayerFields_.find(countField1);
|
||||
if (f1It == lastPlayerFields_.end()) return;
|
||||
const uint32_t packed1 = f1It->second;
|
||||
|
||||
uint32_t packed2 = 0;
|
||||
if (countField2 != 0xFFFF) {
|
||||
auto f2It = lastPlayerFields_.find(countField2);
|
||||
if (f2It != lastPlayerFields_.end()) packed2 = f2It->second;
|
||||
}
|
||||
|
||||
// Unpack six 6-bit counts (bit fields 0-5, 6-11, 12-17, 18-23 in packed1;
|
||||
// bits 0-5, 6-11 in packed2 for objectives 4 and 5).
|
||||
auto unpack6 = [](uint32_t word, int idx) -> uint8_t {
|
||||
return static_cast<uint8_t>((word >> (idx * 6)) & 0x3F);
|
||||
};
|
||||
const uint8_t counts[6] = {
|
||||
unpack6(packed1, 0), unpack6(packed1, 1),
|
||||
unpack6(packed1, 2), unpack6(packed1, 3),
|
||||
unpack6(packed2, 0), unpack6(packed2, 1),
|
||||
};
|
||||
|
||||
// Apply kill objective counts (indices 0-3).
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const auto& obj = quest.killObjectives[i];
|
||||
if (obj.npcOrGoId == 0 || obj.required == 0) continue;
|
||||
const uint32_t entryKey = static_cast<uint32_t>(obj.npcOrGoId);
|
||||
// Don't overwrite live kill count with stale packed data if already non-zero.
|
||||
if (counts[i] == 0 && quest.killCounts.count(entryKey)) continue;
|
||||
quest.killCounts[entryKey] = {counts[i], obj.required};
|
||||
LOG_DEBUG("Quest ", quest.questId, " objective[", i, "]: npcOrGo=",
|
||||
obj.npcOrGoId, " count=", (int)counts[i], "/", obj.required);
|
||||
}
|
||||
|
||||
// Apply item objective counts (only available in WotLK stride+3 positions 4-5).
|
||||
// Item counts also arrive live via SMSG_QUESTUPDATE_ADD_ITEM; just initialise here.
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
const auto& obj = quest.itemObjectives[i];
|
||||
if (obj.itemId == 0 || obj.required == 0) continue;
|
||||
if (i < 2 && qStride >= 5) {
|
||||
uint8_t cnt = counts[4 + i];
|
||||
if (cnt > 0) {
|
||||
quest.itemCounts[obj.itemId] = std::max(quest.itemCounts[obj.itemId], static_cast<uint32_t>(cnt));
|
||||
}
|
||||
}
|
||||
quest.requiredItemCounts.emplace(obj.itemId, obj.required);
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::clearPendingQuestAccept(uint32_t questId) {
|
||||
pendingQuestAcceptTimeouts_.erase(questId);
|
||||
pendingQuestAcceptNpcGuids_.erase(questId);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue