fix(quest/learner): draw turn-in '?' for in-log quests; learn turn-in NPC

Reverts the previous approach of stripping char.complete (which mutated
completed-quest data). Correct rule: a quest in the player's log is active
and should show its turn-in location regardless of the completed flag.

- AddFinisher now trusts the live quest log: if the quest is in
  currentQuestlog and not failed, the '?' finisher draws regardless of the
  (possibly stale, e.g. prior-prestige) char.complete flag. No completion
  data is modified. Reverts the AcceptQuest and QUEST_QUERY_COMPLETE strips.

- Learner: OnTargetChanged now learns quest-giver/turn-in NPC spawns
  (it previously only cached the GUID), so targeting a turn-in NPC records
  its location. OnQuestComplete/OnQuestTurnedIn fall back to the 'target'
  unit when the 'npc' gossip unit is already cleared, so the finisher NPC
  is reliably learned on turn-in.

selene 0 errors; busted 145 successes / same 7 pre-existing failures.
This commit is contained in:
Xurkon
2026-06-10 05:05:28 -05:00
parent f9ff77c088
commit dfdad2cbc3
4 changed files with 44 additions and 29 deletions
+2 -1
View File
@@ -32,7 +32,8 @@
### Bug Fixes ### Bug Fixes
- **[Quest - Re-accepted (Prestige) Quest Stuck "Complete", No Turn-In '?']** A quest completed in a previous Ascension prestige stays in `Questie.db.char.complete`; when re-accepted it is active again in the log, but the stale completion flag was never cleared. `AddFinisher` only draws the turn-in `?` when `not char.complete[questId]`, so the finisher was suppressed and the quest mislabelled "(Complete)" with stale objective text, even while standing at the turn-in NPC. An active in-log quest is now kept out of `char.complete` in two places: it is cleared in `QuestieQuest:AcceptQuest` on accept, and the asynchronous `QUEST_QUERY_COMPLETE` handler now also strips any quest currently in the player's log (so the server's completed list can't re-flag a re-accepted quest). The turn-in `?` and normal objective tracking are restored. - **[Quest - Turn-In '?' Missing For Quests In The Log]** A quest in the player's log is active and not yet turned in, so its turn-in `?` finisher should always be drawable — but `AddFinisher` also required `not char.complete[questId]`, so a quest completed in a previous Ascension prestige (still flagged in `char.complete`) and then re-accepted had its finisher suppressed while standing at the turn-in NPC. `AddFinisher` now trusts the live quest log: if the quest is in `currentQuestlog` and not failed, the finisher draws regardless of the (possibly stale) completed flag. No quest-completion data is modified.
- **[Learner - Turn-In/Quest NPC Not Learned When Targeted]** In learner mode the turn-in NPC's location wasn't always recorded, so its `?` finisher couldn't draw. `OnTargetChanged` only cached the GUID and never called `LearnNPC`; it now learns the spawn of quest-giver/turn-in NPCs (gated like `OnMouseoverUnit`), so simply targeting a turn-in NPC records its position. The `QUEST_COMPLETE`/`QUEST_TURNED_IN` handlers also fall back to the `target` unit when the `npc` gossip unit is already cleared, so the finisher NPC is reliably learned on turn-in.
- **[Map - Completed Quests Still Shown As Available]** (#7) The server's completed-quest list is delivered asynchronously (the `QUEST_QUERY_COMPLETE` event), often after available quests were first drawn — so quests that were actually already complete kept showing as available `!` until something forced a redraw (e.g. `/reload`). Questie now recalculates available quests once `char.complete` is populated by that event, removing the completed ones. This also clears the "already completed" subset of the false-available pins reported in #8. - **[Map - Completed Quests Still Shown As Available]** (#7) The server's completed-quest list is delivered asynchronously (the `QUEST_QUERY_COMPLETE` event), often after available quests were first drawn — so quests that were actually already complete kept showing as available `!` until something forced a redraw (e.g. `/reload`). Questie now recalculates available quests once `char.complete` is populated by that event, removing the completed ones. This also clears the "already completed" subset of the false-available pins reported in #8.
- **[Map - Available '!' Lingered On Minimap For Accepted Quests]** (#9) When a quest was accepted, its available `!` icon could remain on the minimap until a `/reload`. `QuestieMap:UnloadQuestFramesByDataType` unloaded the frame but left its name in the frame registry and `_G`, so the minimap icon was not fully torn down. It now removes the registry/global reference as well, so the available icon is cleared immediately on accept for both the map and minimap. - **[Map - Available '!' Lingered On Minimap For Accepted Quests]** (#9) When a quest was accepted, its available `!` icon could remain on the minimap until a `/reload`. `QuestieMap:UnloadQuestFramesByDataType` unloaded the frame but left its name in the frame registry and `_G`, so the minimap icon was not fully torn down. It now removes the registry/global reference as well, so the available icon is cleared immediately on accept for both the map and minimap.
+1 -7
View File
@@ -734,15 +734,9 @@ end
function QuestieCompat:QUEST_QUERY_COMPLETE(event) function QuestieCompat:QUEST_QUERY_COMPLETE(event)
GetQuestsCompleted(Questie.db.char.complete) GetQuestsCompleted(Questie.db.char.complete)
local currentQuestlog = QuestiePlayer and QuestiePlayer.currentQuestlog
local questId = next(Questie.db.char.complete) local questId = next(Questie.db.char.complete)
while questId do while questId do
-- Repeatable quests are never "complete" for availability purposes. if QuestieDB.IsRepeatable(questId) then
-- Also: a quest currently in the player's log is active, not turned in — the server's
-- completed list can still report a quest re-accepted after an Ascension prestige, and
-- that stale flag suppresses the turn-in '?' finisher and mislabels it "(Complete)".
-- Keep char.complete and the live quest log mutually exclusive.
if QuestieDB.IsRepeatable(questId) or (currentQuestlog and currentQuestlog[questId]) then
Questie.db.char.complete[questId] = nil Questie.db.char.complete[questId] = nil
end end
questId = next(Questie.db.char.complete, questId) questId = next(Questie.db.char.complete, questId)
+7 -10
View File
@@ -500,15 +500,6 @@ function QuestieQuest:AcceptQuest(questId)
-- shows again (covers re-doing the quest after abandon or an Ascension prestige). -- shows again (covers re-doing the quest after abandon or an Ascension prestige).
QuestieQuest:ClearLootedSpawns(questId) QuestieQuest:ClearLootedSpawns(questId)
-- An accepted quest is active, NOT completed-and-turned-in. On Ascension a quest
-- completed in a previous prestige stays in char.complete; when re-accepted the stale
-- flag made Questie treat it as done — the turn-in '?' finisher was suppressed
-- (AddFinisher requires `not char.complete[questId]`) and it showed "(Complete)".
-- Clear it so the quest tracks/finishes normally again.
if Questie.db.char.complete then
Questie.db.char.complete[questId] = nil
end
-- If any of these flags exist, this quest was previously accepted and may -- If any of these flags exist, this quest was previously accepted and may
-- have stale completion state (e.g. complete-then-abandon-then-reaccept leaves -- have stale completion state (e.g. complete-then-abandon-then-reaccept leaves
-- quest.isComplete=true, WasComplete=true). Only check quest-object flags -- quest.isComplete=true, WasComplete=true). Only check quest-object flags
@@ -1295,7 +1286,13 @@ function QuestieQuest:AddFinisher(quest)
local complete = QuestieDB.IsComplete(questId) local complete = QuestieDB.IsComplete(questId)
if (QuestiePlayer.currentQuestlog[questId] and (IsQuestFlaggedCompleted(questId) == false) and (complete == 1 or complete == 0) and (not Questie.db.char.complete[questId])) then -- A quest in the player's log is active and NOT yet turned in, so its turn-in finisher
-- should always be drawable regardless of the char.complete flag. On Ascension a quest
-- completed in a previous prestige stays flagged in char.complete; when re-accepted it is
-- active again, and gating the finisher on char.complete (the old `IsQuestFlaggedCompleted
-- == false and not char.complete` checks) wrongly suppressed the '?' at the turn-in NPC.
-- Trust the live quest log instead. (complete == -1 is a failed quest, so exclude it.)
if (QuestiePlayer.currentQuestlog[questId] and (complete == 1 or complete == 0)) then
local finisher, key local finisher, key
if quest.Finisher ~= nil then if quest.Finisher ~= nil then
+34 -11
View File
@@ -3417,6 +3417,23 @@ function QuestieLearner:OnTargetChanged()
_Learner.guidNpcCache = _Learner.guidNpcCache or {} _Learner.guidNpcCache = _Learner.guidNpcCache or {}
_Learner.guidNpcCache[guid] = { npcId = entityId, name = name, ts = time() } _Learner.guidNpcCache[guid] = { npcId = entityId, name = name, ts = time() }
-- Learn this NPC's spawn if it is a quest giver/turn-in NPC (mirrors OnMouseoverUnit).
-- Targeting a turn-in NPC should record its location so the finisher '?' can draw, even
-- when UPDATE_MOUSEOVER_UNIT didn't fire for it (e.g. it was click- or tab-targeted).
local npcFlags = UnitNPCFlags and UnitNPCFlags("target") or 0
local isQuestGiver = NpcFlagsHasQuestGiver(npcFlags)
if not isQuestGiver then
local rawNpc = QuestieDB and QuestieDB.npcData and QuestieDB.npcData[entityId]
if rawNpc and (rawNpc[10] or rawNpc[11]) then -- known quest starter (10) or ender (11)
isQuestGiver = true
end
end
if isQuestGiver then
local subName = UnitCreatureFamily and UnitCreatureFamily("target") or nil
-- nil coords -> LearnNPC falls back to the player's position (we're at the NPC).
self:LearnNPC(entityId, name, level, subName, npcFlags, nil, nil, nil, nil)
end
end end
-- Collects all available quest data from the quest detail/offer screen (before accepting) -- Collects all available quest data from the quest detail/offer screen (before accepting)
@@ -3479,19 +3496,22 @@ function QuestieLearner:OnQuestComplete()
end end
self:LearnQuest(questId, data) self:LearnQuest(questId, data)
-- Identify the quest turn-in NPC or object -- Identify the quest turn-in NPC or object. Prefer the "npc" gossip unit, but fall back
local npcGuid = UnitGUID("npc") -- to "target" since the player is targeting the turn-in NPC and the gossip unit isn't
-- always populated on this client.
local unit = (UnitGUID("npc") and "npc") or (UnitGUID("target") and "target") or nil
local npcGuid = unit and UnitGUID(unit)
if npcGuid then if npcGuid then
local entityId, unitType = self:ResolveNpcIdFromGuidAndName(npcGuid, UnitName("npc")) local entityId, unitType = self:ResolveNpcIdFromGuidAndName(npcGuid, UnitName(unit))
TraceLearnerEntity("quest_complete", npcGuid, unitType, entityId, UnitName("npc")) TraceLearnerEntity("quest_complete", npcGuid, unitType, entityId, UnitName(unit))
if entityId and entityId > 0 then if entityId and entityId > 0 then
local entityName = UnitName("npc") local entityName = UnitName(unit)
if unitType == "GameObject" then if unitType == "GameObject" then
self:LearnQuestGiver(questId, entityId, 2, false) self:LearnQuestGiver(questId, entityId, 2, false)
self:LearnObject(entityId, entityName, nil, nil, zoneId, true) self:LearnObject(entityId, entityName, nil, nil, zoneId, true)
elseif unitType == "Creature" or unitType == "Vehicle" then elseif unitType == "Creature" or unitType == "Vehicle" then
self:LearnQuestGiver(questId, entityId, 1, false) self:LearnQuestGiver(questId, entityId, 1, false)
local npcFlags = UnitNPCFlags and UnitNPCFlags("npc") or 2 local npcFlags = UnitNPCFlags and UnitNPCFlags(unit) or 2
self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil, nil, nil, zoneId) self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil, nil, nil, zoneId)
end end
end end
@@ -3800,18 +3820,21 @@ function QuestieLearner:OnQuestTurnedIn(questId, xpReward, moneyReward)
if not questId or questId <= 0 then return end if not questId or questId <= 0 then return end
local data = {} local data = {}
-- Capture turn-in NPC/object while the gossip unit is still set -- Capture the turn-in NPC/object. The "npc" gossip unit can already be cleared by the
local npcGuid = UnitGUID("npc") -- time QUEST_TURNED_IN fires, so fall back to "target" (the player almost always still
-- has the turn-in NPC targeted). This is what records the finisher's spawn location.
local unit = (UnitGUID("npc") and "npc") or (UnitGUID("target") and "target") or nil
local npcGuid = unit and UnitGUID(unit)
if npcGuid then if npcGuid then
local entityId, unitType = self:ResolveNpcIdFromGuidAndName(npcGuid, UnitName("npc")) local entityId, unitType = self:ResolveNpcIdFromGuidAndName(npcGuid, UnitName(unit))
if entityId and entityId > 0 then if entityId and entityId > 0 then
local entityName = UnitName("npc") local entityName = UnitName(unit)
if unitType == "GameObject" then if unitType == "GameObject" then
self:LearnQuestGiver(questId, entityId, 2, false) self:LearnQuestGiver(questId, entityId, 2, false)
self:LearnObject(entityId, entityName, nil, nil, GetZoneId(), true) self:LearnObject(entityId, entityName, nil, nil, GetZoneId(), true)
elseif unitType == "Creature" or unitType == "Vehicle" then elseif unitType == "Creature" or unitType == "Vehicle" then
self:LearnQuestGiver(questId, entityId, 1, false) self:LearnQuestGiver(questId, entityId, 1, false)
local npcFlags = UnitNPCFlags and UnitNPCFlags("npc") or 2 local npcFlags = UnitNPCFlags and UnitNPCFlags(unit) or 2
self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil, nil, nil, GetZoneId()) self:LearnNPC(entityId, entityName, nil, nil, npcFlags, nil, nil, nil, GetZoneId())
end end
end end