fix: harden quest poi filtering and objective refresh

This commit is contained in:
Xurkon
2026-06-13 08:30:41 -05:00
parent 12b30cd566
commit 8898dcf96a
5 changed files with 165 additions and 2 deletions
+2
View File
@@ -53,6 +53,8 @@
### 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.
- **[Map - Native POIs Respect Hidden Available-Quest Filters]** (#10, #11) Blizzard/server POI suppression now also hides native available-quest buttons when Questie's own available-quest filters intentionally hide that quest, such as Callboard/repeatable quests below 60 or dungeon quests while dungeon quests are disabled. This covers the case where Questie correctly hides its own `!`, leaving no visible duplicate frame for the older suppression logic to detect. Active quest-log entries are excluded so native turn-in/objective POIs are not suppressed just because Questie lacks a visible frame.
- **[Quest Events - Completed Objective Refresh Hardened]** (#21) `QUEST_WATCH_UPDATE` now dirties the updated quest and schedules debounced quest-log scans itself instead of relying on a later `QUEST_LOG_UPDATE` that Ascension may not send. A second delayed pass catches server-side objective-counter lag, so fully completed objective pins are removed and the finisher pin can replace them without waiting for the periodic refresh.
- **[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.
+97 -1
View File
@@ -610,6 +610,15 @@ local BLIZZARD_POI_QUEST_LOG_INDEX_FIELDS = {
"logIndex",
}
local BLIZZARD_POI_QUEST_TAG_FIELDS = {
"questTagID",
"questTagId",
"questTag",
"questType",
"tagID",
"tagId",
}
local function _ToPositiveNumber(value)
if type(value) == "number" or type(value) == "string" then
local number = tonumber(value)
@@ -621,6 +630,19 @@ local function _ToPositiveNumber(value)
return nil
end
local function _GetPlayerLevel()
if UnitLevel then
return UnitLevel("player") or 0
end
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
if QuestiePlayer and QuestiePlayer.GetPlayerLevel then
return QuestiePlayer.GetPlayerLevel() or 0
end
return 0
end
---Enables Blizzard/server objective POIs without globally disabling them when Questie objectives are on.
function QuestieCompat.EnableBlizzardObjectivePOIs()
if GetCVar and GetCVar("questPOI") ~= nil and SetCVar then
@@ -695,10 +717,84 @@ function QuestieCompat.GetQuestIDFromBlizzardPOIButton(poiButton)
return nil
end
---@param poiButton table
---@return number|nil
function QuestieCompat.GetQuestTagIDFromBlizzardPOIButton(poiButton)
if not poiButton then
return nil
end
for _, field in ipairs(BLIZZARD_POI_QUEST_TAG_FIELDS) do
local questTagId = _ToPositiveNumber(poiButton[field])
if questTagId then
return questTagId
end
end
return nil
end
---@param questId number
---@param poiButton table|nil
---@return boolean
function QuestieCompat.ShouldSuppressHiddenQuestieAvailablePOI(questId, poiButton)
if (not questId) or questId <= 0 or (not Questie) or (not Questie.db) or (not Questie.db.profile) then
return false
end
local profile = Questie.db.profile
if not profile.enabled then
return false
end
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
if QuestiePlayer and QuestiePlayer.currentQuestlog and QuestiePlayer.currentQuestlog[questId] then
return false
end
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
if not QuestieDB then
return false
end
if Questie.db.char and Questie.db.char.complete and Questie.db.char.complete[questId] then
return true
end
if Questie.db.char and Questie.db.char.hidden and Questie.db.char.hidden[questId] then
return true
end
local repeatable = QuestieDB.IsRepeatable and QuestieDB.IsRepeatable(questId)
local event = QuestieDB.IsActiveEventQuest and QuestieDB.IsActiveEventQuest(questId)
local dungeon = QuestieDB.IsDungeonQuest and QuestieDB.IsDungeonQuest(questId)
local raid = QuestieDB.IsRaidQuest and QuestieDB.IsRaidQuest(questId)
local pvp = QuestieDB.IsPvPQuest and QuestieDB.IsPvPQuest(questId)
local questTagId = QuestieCompat.GetQuestTagIDFromBlizzardPOIButton(poiButton)
if questTagId == 81 then
dungeon = true
elseif questTagId == 62 then
raid = true
elseif questTagId == 41 then
pvp = true
elseif questTagId == 82 then
event = true
end
local normal = not (repeatable or event or dungeon or raid or pvp)
return ((not profile.enableAvailable) and normal)
or ((not profile.showRepeatableQuests) and repeatable)
or (profile.hideRepeatableBelowMaxLevel and (repeatable or (QuestieDB.IsBoardQuest and QuestieDB.IsBoardQuest(questId))) and _GetPlayerLevel() < 60)
or ((not profile.showEventQuests) and event)
or ((not profile.showDungeonQuests) and dungeon)
or ((not profile.showRaidQuests) and raid)
or ((not profile.showPvPQuests) and pvp)
end
---@param poiButton table
function QuestieCompat.SuppressDuplicateBlizzardPOIButton(poiButton)
local questId = QuestieCompat.GetQuestIDFromBlizzardPOIButton(poiButton)
if questId and QuestieCompat.HasVisibleQuestiePOIForQuest(questId) then
if questId and (QuestieCompat.HasVisibleQuestiePOIForQuest(questId) or QuestieCompat.ShouldSuppressHiddenQuestieAvailablePOI(questId, poiButton)) then
poiButton.questieDuplicateSuppressed = true
poiButton:Hide()
end
+31
View File
@@ -57,6 +57,11 @@ local deletedQuestItem = false
-- Also schedules a follow-up scan to catch server-side quest-log counter lag (loot bot batch loots).
local _bagUpdateDebounceTimer = nil
local _bagUpdateFollowUpTimer = nil
-- Debounce QUEST_WATCH_UPDATE scans too. Ascension can send objective progress
-- without a follow-up QUEST_LOG_UPDATE, so waiting for the next QLU can leave
-- completed objective pins visible until the periodic refresh catches them.
local _questWatchUpdateDebounceTimer = nil
local _questWatchUpdateFollowUpTimer = nil
-- Periodic quest state verification timer.
-- Ascension server events (QUEST_LOG_UPDATE, UNIT_QUEST_LOG_CHANGED) can be
@@ -548,6 +553,32 @@ function _QuestEventHandler:QuestWatchUpdate(questId)
-- a QUEST_LOG_UPDATE. Also not every QUEST_WATCH_UPDATE gets a single QUEST_LOG_UPDATE and doing a full
-- scan is less error prone
doFullQuestLogScan = true
if questId and questId > 0 then
questLog[questId] = questLog[questId] or {
state = QUEST_LOG_STATES.QUEST_ACCEPTED
}
QuestieQuest:SetObjectivesDirty(questId)
end
if _questWatchUpdateDebounceTimer then
_questWatchUpdateDebounceTimer:Cancel()
_questWatchUpdateDebounceTimer = nil
end
_questWatchUpdateDebounceTimer = C_Timer.NewTimer(0.2, function()
_questWatchUpdateDebounceTimer = nil
doFullQuestLogScan = true
_QuestEventHandler:QuestLogUpdate()
end)
if _questWatchUpdateFollowUpTimer then
_questWatchUpdateFollowUpTimer:Cancel()
_questWatchUpdateFollowUpTimer = nil
end
_questWatchUpdateFollowUpTimer = C_Timer.NewTimer(1.0, function()
_questWatchUpdateFollowUpTimer = nil
doFullQuestLogScan = true
_QuestEventHandler:QuestLogUpdate()
end)
end
local _UnitQuestLogChangedCallback = function()
@@ -0,0 +1,28 @@
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("Quest objective refresh events", function()
local handler = read("Modules/Quest/QuestEventHandler.lua")
it("does not depend on a later QUEST_LOG_UPDATE after QUEST_WATCH_UPDATE", function()
local start = assert(handler:find("function _QuestEventHandler:QuestWatchUpdate", 1, true))
local finish = assert(handler:find("local _UnitQuestLogChangedCallback", start, true))
local body = handler:sub(start, finish)
assert.is_true(has(handler, "_questWatchUpdateDebounceTimer"))
assert.is_true(has(handler, "_questWatchUpdateFollowUpTimer"))
assert.is_true(has(body, "QuestieQuest:SetObjectivesDirty(questId)"))
assert.is_true(has(body, "state = QUEST_LOG_STATES.QUEST_ACCEPTED"))
assert.is_true(has(body, "C_Timer.NewTimer(0.2"))
assert.is_true(has(body, "C_Timer.NewTimer(1.0"))
assert.is_true(has(body, "_QuestEventHandler:QuestLogUpdate()"))
end)
end)
@@ -15,7 +15,7 @@ describe("Blizzard objective POI suppression", function()
local iconOptions = read("Modules/Options/IconsTab/QuestieOptionsIcons.lua")
local questieMenu = read("Modules/QuestieMenu/QuestieMenu.lua")
it("suppresses only native POI buttons that duplicate visible Questie quest icons", function()
it("suppresses native POI buttons that duplicate visible or intentionally hidden Questie quest icons", function()
local helperStart = assert(compat:find("function QuestieCompat.HasVisibleQuestiePOIForQuest", 1, true))
local helperEnd = assert(compat:find("-- https://wowpedia.fandom.com/wiki/API_GetQuestLink", helperStart, true))
local helper = compat:sub(helperStart, helperEnd)
@@ -28,6 +28,12 @@ describe("Blizzard objective POI suppression", function()
assert.is_true(has(compat, "frame:ShouldBeHidden()"))
assert.is_true(has(helper, "QuestieMap.questIdFrames and QuestieMap.questIdFrames[questId]"))
assert.is_true(has(helper, "poiButton:Hide()"))
assert.is_true(has(helper, "function QuestieCompat.ShouldSuppressHiddenQuestieAvailablePOI"))
assert.is_true(has(helper, "profile.hideRepeatableBelowMaxLevel"))
assert.is_true(has(helper, "QuestieDB.IsBoardQuest"))
assert.is_true(has(helper, "profile.showDungeonQuests"))
assert.is_true(has(helper, "questTagId == 81"))
assert.is_true(has(helper, "QuestieCompat.HasVisibleQuestiePOIForQuest(questId) or QuestieCompat.ShouldSuppressHiddenQuestieAvailablePOI(questId, poiButton)"))
end)
it("hooks Blizzard POI display without globally disabling native POIs", function()