fix(quest): quest log population, NPC marker updates on accept/abandon

- Delegate GameHandler::getQuestGiverStatus() to QuestHandler instead of
  reading from GameHandler's own empty npcQuestStatus_ map
- Immediately add quest to local log in acceptQuest() instead of waiting
  for field updates, fixing quests not appearing after accept
- Handle duplicate accept path (server already has quest) by also adding
  to local log
- Remove early return on empty questLog_ in applyQuestStateFromFields()
- Re-query nearby quest giver NPC statuses on abandon so markers refresh

Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
Pavel Okhlopkov 2026-04-10 19:50:56 +03:00
parent 9c1ffae140
commit 759d6046bb
3 changed files with 49 additions and 7 deletions

View file

@ -1699,10 +1699,7 @@ public:
bool isServerMovementAllowed() const;
// Quest giver status (! and ? markers)
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const {
auto it = npcQuestStatus_.find(guid);
return (it != npcQuestStatus_.end()) ? it->second : QuestGiverStatus::NONE;
}
QuestGiverStatus getQuestGiverStatus(uint64_t guid) const;
const std::unordered_map<uint64_t, QuestGiverStatus>& getNpcQuestStatuses() const;
// Charge callback — fires when player casts a charge spell toward target

View file

@ -2261,6 +2261,10 @@ const std::unordered_map<uint64_t, QuestGiverStatus>& GameHandler::getNpcQuestSt
static const std::unordered_map<uint64_t, QuestGiverStatus> empty;
return empty;
}
QuestGiverStatus GameHandler::getQuestGiverStatus(uint64_t guid) const {
if (questHandler_) return questHandler_->getQuestGiverStatus(guid);
return QuestGiverStatus::NONE;
}
const std::vector<GameHandler::QuestLogEntry>& GameHandler::getQuestLog() const {
if (questHandler_) return questHandler_->getQuestLog();
static const std::vector<QuestLogEntry> empty;

View file

@ -339,6 +339,8 @@ void QuestHandler::registerOpcodes(DispatchTable& table) {
if (packet.hasRemaining(9)) {
uint64_t npcGuid = packet.readUInt64();
uint8_t status = owner_.getPacketParsers()->readQuestGiverStatus(packet);
LOG_INFO("SMSG_QUESTGIVER_STATUS: npcGuid=0x", std::hex, npcGuid, std::dec,
" status=", static_cast<int>(status));
npcQuestStatus_[npcGuid] = static_cast<QuestGiverStatus>(status);
}
};
@ -1075,8 +1077,17 @@ void QuestHandler::acceptQuest() {
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);
LOG_INFO("Quest already in server quest log: questId=", questId,
" slot=", serverSlot, " inLocalLog=", inLocalLog);
// Ensure it's in our local log even if server already has it
addQuestToLocalLogIfMissing(questId, currentQuestDetails_.title, currentQuestDetails_.objectives);
requestQuestQuery(questId, false);
// Re-query NPC status from server
if (npcGuid && owner_.getSocket()) {
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(npcGuid);
owner_.getSocket()->send(qsPkt);
}
questDetailsOpen_ = false;
questDetailsOpenTime_ = std::chrono::steady_clock::time_point{};
currentQuestDetails_ = QuestDetailsData{};
@ -1094,6 +1105,9 @@ void QuestHandler::acceptQuest() {
pendingQuestAcceptTimeouts_[questId] = 5.0f;
pendingQuestAcceptNpcGuids_[questId] = npcGuid;
// Immediately add to local quest log using available details
addQuestToLocalLogIfMissing(questId, currentQuestDetails_.title, currentQuestDetails_.objectives);
// Play quest-accept sound
if (auto* ac = owner_.services().audioCoordinator) {
if (auto* sfx = ac->getUiSoundManager())
@ -1223,6 +1237,19 @@ void QuestHandler::abandonQuest(uint32_t questId) {
}
}
// Re-query nearby quest giver NPCs so markers refresh (e.g. "?" → "!")
if (owner_.getSocket()) {
for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) {
if (entity->getType() != ObjectType::UNIT) continue;
auto unit = std::static_pointer_cast<Unit>(entity);
if (unit->getNpcFlags() & 0x02) {
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(guid);
owner_.getSocket()->send(qsPkt);
}
}
}
// Remove any quest POI minimap markers for this quest.
gossipPois_.erase(
std::remove_if(gossipPois_.begin(), gossipPois_.end(),
@ -1376,7 +1403,7 @@ bool QuestHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) {
void QuestHandler::applyQuestStateFromFields(const std::map<uint16_t, uint32_t>& fields) {
const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START);
if (ufQuestStart == 0xFFFF || questLog_.empty()) return;
if (ufQuestStart == 0xFFFF) return;
const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5;
if (qStride < 2) return;
@ -1391,6 +1418,20 @@ void QuestHandler::applyQuestStateFromFields(const std::map<uint16_t, uint32_t>&
uint32_t questId = idIt->second;
if (questId == 0) continue;
// Add quest to local log only if we have a pending accept for it
if (!hasQuestInLog(questId) && pendingQuestAcceptTimeouts_.count(questId) != 0) {
addQuestToLocalLogIfMissing(questId, "Quest #" + std::to_string(questId), "");
requestQuestQuery(questId, false);
// Re-query quest giver status for the NPC that gave us this quest
auto pendingIt = pendingQuestAcceptNpcGuids_.find(questId);
if (pendingIt != pendingQuestAcceptNpcGuids_.end() && pendingIt->second != 0 && owner_.getSocket()) {
network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY));
qsPkt.writeUInt64(pendingIt->second);
owner_.getSocket()->send(qsPkt);
}
clearPendingQuestAccept(questId);
}
auto stateIt = fields.find(stateField);
if (stateIt == fields.end()) continue;
bool serverComplete = ((stateIt->second & 0xFF) == kQuestStatusComplete);