From 3a293e6a3ca75520e3ec8ec5fbd3a6fabbf95d1e Mon Sep 17 00:00:00 2001 From: Xurkon Date: Thu, 11 Jun 2026 11:51:20 -0500 Subject: [PATCH] fix(map): harden quest completion pin cleanup --- Modules/Quest/QuestieQuest.lua | 49 ++++++++++++++++++-- Tests/QuestieQuestCompletionCleanup_spec.lua | 36 ++++++++++++++ 2 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 Tests/QuestieQuestCompletionCleanup_spec.lua diff --git a/Modules/Quest/QuestieQuest.lua b/Modules/Quest/QuestieQuest.lua index 7c2e0dd..98a7ba9 100644 --- a/Modules/Quest/QuestieQuest.lua +++ b/Modules/Quest/QuestieQuest.lua @@ -618,6 +618,36 @@ end local allianceChampionMarkerQuests = { [13699] = true, [13713] = true, [13723] = true, [13724] = true, [13725] = true } local hordeChampionMarkerQuests = { [13726] = true, [13727] = true, [13728] = true, [13729] = true, [13731] = true } +local function _CleanupCompletedQuestObjectivePins(quest) + if (not quest) or (not quest.Id) then + return + end + + -- Full quest completion has to drain objective-owned frame refs before the + -- cached objective table is cleared. Some kill pins can be absent from the + -- map registry after data-source refreshes, but still live in AlreadySpawned. + if type(quest.Objectives) == "table" then + for objectiveIndex, objective in pairs(quest.Objectives) do + if type(objective) == "table" then + _UnloadAlreadySpawnedIcons(objective) + if type(objectiveIndex) == "number" then + QuestieMap:UnloadQuestFramesForObjective(quest.Id, objectiveIndex) + end + end + end + end + + if type(quest.SpecialObjectives) == "table" then + for _, objective in pairs(quest.SpecialObjectives) do + if type(objective) == "table" then + _UnloadAlreadySpawnedIcons(objective) + end + end + end + + QuestieMap:UnloadQuestFrames(quest.Id) +end + ---@param questId number function QuestieQuest:CompleteQuest(questId) -- Skip quests which are turn in only and are not added to the quest log in the first place @@ -651,11 +681,15 @@ function QuestieQuest:CompleteQuest(questId) -- Cached Completed=true / isUpdated=true flags that would cause -- PopulateObjectiveNotes to skip drawing map pins (bug: complete-abandon-reaccept). local quest = QuestieDB.GetQuest(questId) - if quest and type(quest.Objectives) == "table" then - quest.Objectives = {} - end + if quest then + _CleanupCompletedQuestObjectivePins(quest) - QuestieMap:UnloadQuestFrames(questId) + if type(quest.Objectives) == "table" then + quest.Objectives = {} + end + else + QuestieMap:UnloadQuestFrames(questId) + end -- Clear the pending-complete guard now that frames are unloaded if QuestiePlayer.pendingCompleteQuestIds then @@ -669,6 +703,13 @@ function QuestieQuest:CompleteQuest(questId) -- Delayed verification to ensure all objective icons are removed -- This handles race conditions where AvailableQuests might redraw icons or UnloadQuestFrames misses some C_Timer.After(0.5, function() + local delayedQuest = QuestieDB.GetQuest(questId) + if delayedQuest then + _CleanupCompletedQuestObjectivePins(delayedQuest) + elseif QuestieMap.questIdFrames[questId] then + QuestieMap:UnloadQuestFrames(questId) + end + if QuestieMap.questIdFrames[questId] then Questie:Debug(Questie.DEBUG_INFO, "[QuestieQuest:CompleteQuest] Lingering frames detected for quest:", questId, "- forcing cleanup") diff --git a/Tests/QuestieQuestCompletionCleanup_spec.lua b/Tests/QuestieQuestCompletionCleanup_spec.lua new file mode 100644 index 0000000..c1f99fc --- /dev/null +++ b/Tests/QuestieQuestCompletionCleanup_spec.lua @@ -0,0 +1,36 @@ +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") + + 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:UnloadQuestFrames(quest.Id)")) + end) +end)