fix: purge lingering completed quest icons

This commit is contained in:
Xurkon
2026-06-13 07:49:04 -05:00
parent 3ebcedb362
commit 12b30cd566
5 changed files with 133 additions and 13 deletions
+1
View File
@@ -52,6 +52,7 @@
### Bug Fixes
- **[Map - Completed Quest Icon Cleanup Hardened]** (#9) Completed/removed quest cleanup now purges quest-owned frames from Questie's registry, pending map/minimap draw queues, and HBD's active map/minimap pin tables. The removed-quest fallback also snapshots the last known completion state before clearing the quest cache, so Ascension quest-log removals without clean turn-in events are completed instead of misclassified as abandoned.
- **[Options - Instant Quest Text Toggle]** Fixed the General tab "Enable Instant Quest Text" checkbox so it can be toggled even when the client reports the backing `instantQuestText` CVar as unset before the first write.
- **[Learner - Secondary Tooltip Without Spawn Coordinates]** Unit-hover learner tooltips now still open the secondary learner tooltip when the learner has confidence data but no recorded spawn coordinates yet. The normal unit tooltip suppression path also suppresses the old inline learner confidence line, so secondary mode no longer leaks learner-only lines back into the main tooltip.
- **[Map - Suppress Duplicate Native Quest POIs]** Rather than globally disabling the server/Blizzard objective POIs, Questie now keeps them enabled and hides only the individual Blizzard POI buttons for quests that already have a visible Questie POI (per-quest duplicate-POI suppression in `QuestieCompat`, hooked at init). Blizzard POIs still appear for quests Questie does not cover, but no longer stack on top of Questie's own objective icons.
+71 -11
View File
@@ -127,19 +127,18 @@ function QuestieMap:UnloadQuestFramesForObjective(questId, objectiveIndex)
end
function QuestieMap:UnloadQuestFrames(questId, iconType)
if not iconType then
QuestieMap:PurgeQuestFrames(questId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieMap] Unloading quest frames for questid:", questId)
return
end
if QuestieMap.questIdFrames[questId] then
if not iconType then
for _, frame in pairs(QuestieMap:GetFramesForQuest(questId)) do
for name, frame in pairs(QuestieMap:GetFramesForQuest(questId)) do
if frame and frame.data and frame.data.Icon == iconType then
frame:Unload();
end
QuestieMap.questIdFrames[questId] = nil;
else
for name, frame in pairs(QuestieMap:GetFramesForQuest(questId)) do
if frame and frame.data and frame.data.Icon == iconType then
frame:Unload();
QuestieMap.questIdFrames[questId][name] = nil
_G[name] = nil
end
QuestieMap.questIdFrames[questId][name] = nil
_G[name] = nil
end
end
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieMap] Unloading quest frames for questid:", questId)
@@ -257,6 +256,67 @@ function QuestieMap:DequeueFrameDrawCalls(frame)
end
end
function QuestieMap:PurgeQuestFrames(questId)
if not questId then return end
local frames = {}
for _, frame in pairs(QuestieMap:GetFramesForQuest(questId)) do
if frame then
frames[frame] = true
end
end
local function collectQueuedFrames(queue)
if type(queue) ~= "table" then return end
for i = #queue, 1, -1 do
local frame = queue[i] and queue[i][2]
if frame and frame.data and frame.data.Id == questId then
frames[frame] = true
tremove(queue, i)
end
end
end
collectQueuedFrames(QuestieMap._mapDrawQueue)
collectQueuedFrames(QuestieMap._minimapDrawQueue)
local function collectHbdFrames(pinTable)
if type(pinTable) ~= "table" then return end
for frame in pairs(pinTable) do
if frame and frame.data and frame.data.Id == questId then
frames[frame] = true
end
end
end
if HBDPins then
collectHbdFrames(HBDPins.activeMinimapPins)
collectHbdFrames(HBDPins.worldmapPins)
end
for frame in pairs(frames) do
if frame.Unload then
frame:Unload()
else
if HBDPins then
HBDPins:RemoveMinimapIcon(Questie, frame)
HBDPins:RemoveWorldMapIcon(Questie, frame)
end
if frame.Hide then frame:Hide() end
if frame.ClearAllPoints then frame:ClearAllPoints() end
end
if frame.GetName then
local frameName = frame:GetName()
if frameName then
_G[frameName] = nil
end
end
end
QuestieMap.questIdFrames[questId] = nil
end
--- Called at startup (Stage 3) and on PLAYER_ENTERING_WORLD to reset the draw queue.
function QuestieMap:InitializeQueue()
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieMap] Starting draw queue timer!")
+3 -1
View File
@@ -607,13 +607,15 @@ function _QuestEventHandler:CleanupRemovedQuestsFallback()
-- Check if this quest was confirmed as turned in (not just objectives complete)
local wasTurnedIn = questLog[questId] and questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN
local wasAlreadyComplete = Questie.db.char.complete and Questie.db.char.complete[questId]
local completeAtRemoval = QuestieDB.IsComplete(questId)
local shouldComplete = wasTurnedIn or wasAlreadyComplete or completeAtRemoval == 1
QuestLogCache.RemoveQuest(questId)
QuestieQuest:SetObjectivesDirty(questId)
-- Only mark as complete if it was actually turned in OR already marked complete from previous session
-- Don't use quest.WasComplete because that's set when objectives complete, not when quest is turned in
if wasTurnedIn or wasAlreadyComplete then
if shouldComplete then
QuestieQuest:CompleteQuest(questId)
else
QuestieQuest:AbandonedQuest(questId)
+1 -1
View File
@@ -652,7 +652,7 @@ local function _CleanupCompletedQuestObjectivePins(quest)
end
end
QuestieMap:UnloadQuestFrames(quest.Id)
QuestieMap:PurgeQuestFrames(quest.Id)
end
---@param questId number
@@ -0,0 +1,57 @@
local function read(path)
local f = assert(io.open(path, "r"), "cannot open " .. path)
local content = f:read("*a")
f:close()
return content
end
local function has(content, needle)
return content:find(needle, 1, true) ~= nil
end
describe("QuestieQuest completion objective pin cleanup", function()
local questieQuest = read("Modules/Quest/QuestieQuest.lua")
local questieMap = read("Modules/Map/QuestieMap.lua")
local questEventHandler = read("Modules/Quest/QuestEventHandler.lua")
it("unloads objective-owned spawned pins before clearing cached objectives", function()
local completeStart = assert(questieQuest:find("function QuestieQuest:CompleteQuest(questId)", 1, true))
local completeEnd = assert(questieQuest:find("---@param questId number\nfunction QuestieQuest:AbandonedQuest", completeStart, true))
local completeQuest = questieQuest:sub(completeStart, completeEnd)
local cleanupCall = assert(completeQuest:find("_CleanupCompletedQuestObjectivePins(quest)", 1, true))
local clearObjectives = assert(completeQuest:find("quest.Objectives = {}", 1, true))
assert.is_true(cleanupCall < clearObjectives)
end)
it("cleans standard objectives, special objectives, and registry frames", function()
local helperStart = assert(questieQuest:find("local function _CleanupCompletedQuestObjectivePins(quest)", 1, true))
local helperEnd = assert(questieQuest:find("---@param questId number\nfunction QuestieQuest:CompleteQuest", helperStart, true))
local helper = questieQuest:sub(helperStart, helperEnd)
assert.is_true(has(helper, "for objectiveIndex, objective in pairs(quest.Objectives) do"))
assert.is_true(has(helper, "_UnloadAlreadySpawnedIcons(objective)"))
assert.is_true(has(helper, "QuestieMap:UnloadQuestFramesForObjective(quest.Id, objectiveIndex)"))
assert.is_true(has(helper, "for _, objective in pairs(quest.SpecialObjectives) do"))
assert.is_true(has(helper, "QuestieMap:PurgeQuestFrames(quest.Id)"))
end)
it("purges quest-owned frames from HBD registries and pending draw queues", function()
assert.is_true(has(questieMap, "function QuestieMap:PurgeQuestFrames(questId)"))
assert.is_true(has(questieMap, "if not iconType then\n QuestieMap:PurgeQuestFrames(questId)"))
assert.is_true(has(questieMap, "QuestieMap._mapDrawQueue"))
assert.is_true(has(questieMap, "QuestieMap._minimapDrawQueue"))
assert.is_true(has(questieMap, "HBDPins.activeMinimapPins"))
assert.is_true(has(questieMap, "HBDPins.worldmapPins"))
assert.is_true(has(questieMap, "frame.data.Id == questId"))
end)
it("uses the last known complete state when fallback detects a removed quest", function()
local fallbackStart = assert(questEventHandler:find("function _QuestEventHandler:CleanupRemovedQuestsFallback()", 1, true))
local fallback = questEventHandler:sub(fallbackStart)
assert.is_true(has(fallback, "local completeAtRemoval = QuestieDB.IsComplete(questId)"))
assert.is_true(has(fallback, "local shouldComplete = wasTurnedIn or wasAlreadyComplete or completeAtRemoval == 1"))
assert.is_true(has(fallback, "if shouldComplete then"))
end)
end)