From 70a99346e6dd1d293f521a6b6197ac720f763513 Mon Sep 17 00:00:00 2001 From: Xurkon Date: Fri, 12 Jun 2026 22:32:24 -0500 Subject: [PATCH] fix: suppress duplicate native quest POIs --- Compat/Compat.lua | 150 +++++++++++++++++- .../Options/IconsTab/QuestieOptionsIcons.lua | 5 +- Modules/QuestieInit.lua | 2 +- Modules/QuestieMenu/QuestieMenu.lua | 1 - .../QuestieBlizzardObjectivePOISync_spec.lua | 38 +++-- 5 files changed, 174 insertions(+), 22 deletions(-) diff --git a/Compat/Compat.lua b/Compat/Compat.lua index ad07c7b..7d89417 100644 --- a/Compat/Compat.lua +++ b/Compat/Compat.lua @@ -587,18 +587,158 @@ function QuestieCompat.GetQuestIDFromLogIndex(questLogIndex) return select(8, QuestieCompat.GetQuestLogTitle(questLogIndex)) end ----Keeps Blizzard/server objective POIs from drawing over Questie's objective icons. ----@param useQuestieObjectives boolean -function QuestieCompat.SyncBlizzardObjectivePOIs(useQuestieObjectives) +local QUESTIE_DUPLICATE_POI_ICON_TYPES = { + complete = true, + monster = true, + object = true, + item = true, + event = true, +} + +local BLIZZARD_POI_QUEST_ID_FIELDS = { + "questID", + "questId", + "questIDNumber", + "questIDNum", + "questLogID", + "id", +} + +local BLIZZARD_POI_QUEST_LOG_INDEX_FIELDS = { + "questLogIndex", + "questIndex", + "logIndex", +} + +local function _ToPositiveNumber(value) + if type(value) == "number" or type(value) == "string" then + local number = tonumber(value) + if number and number > 0 then + return number + end + end + + return nil +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 - SetCVar("questPOI", useQuestieObjectives and "0" or "1") + SetCVar("questPOI", "1") end if WorldMapQuestShowObjectives and WorldMapQuestShowObjectives.SetChecked then - WorldMapQuestShowObjectives:SetChecked(not useQuestieObjectives) + WorldMapQuestShowObjectives:SetChecked(true) end end +---@param frame table +---@return boolean +local function _IsVisibleQuestiePOIFrame(frame) + if (not frame) or (not frame.data) or (not QUESTIE_DUPLICATE_POI_ICON_TYPES[frame.data.Type]) then + return false + end + + if frame.ShouldBeHidden then + return not frame:ShouldBeHidden() + end + + return not frame.hidden +end + +---@param questId number +---@return boolean +function QuestieCompat.HasVisibleQuestiePOIForQuest(questId) + if (not questId) or questId <= 0 then + return false + end + + local QuestieMap = QuestieLoader:ImportModule("QuestieMap") + local questFrames = QuestieMap.questIdFrames and QuestieMap.questIdFrames[questId] + if not questFrames then + return false + end + + for _, frameName in pairs(questFrames) do + if _IsVisibleQuestiePOIFrame(_G[frameName]) then + return true + end + end + + return false +end + +---@param poiButton table +---@return number|nil +function QuestieCompat.GetQuestIDFromBlizzardPOIButton(poiButton) + if not poiButton then + return nil + end + + for _, field in ipairs(BLIZZARD_POI_QUEST_ID_FIELDS) do + local questId = _ToPositiveNumber(poiButton[field]) + if questId then + return questId + end + end + + for _, field in ipairs(BLIZZARD_POI_QUEST_LOG_INDEX_FIELDS) do + local questLogIndex = _ToPositiveNumber(poiButton[field]) + if questLogIndex then + local questId = QuestieCompat.GetQuestIDFromLogIndex(questLogIndex) + if questId and questId > 0 then + return questId + end + end + end + + return nil +end + +---@param poiButton table +function QuestieCompat.SuppressDuplicateBlizzardPOIButton(poiButton) + local questId = QuestieCompat.GetQuestIDFromBlizzardPOIButton(poiButton) + if questId and QuestieCompat.HasVisibleQuestiePOIForQuest(questId) then + poiButton.questieDuplicateSuppressed = true + poiButton:Hide() + end +end + +function QuestieCompat.SuppressDuplicateBlizzardPOI(parentName, buttonType, buttonIndex) + local poiButton = _G[string.format( + "poi%s%s_%d", + tostring(parentName or ""), + tostring(buttonType or ""), + _ToPositiveNumber(buttonIndex) or 0 + )] + if poiButton then + QuestieCompat.SuppressDuplicateBlizzardPOIButton(poiButton) + end +end + +function QuestieCompat.InitializeBlizzardPOISuppression() + if QuestieCompat._blizzardPOISuppressionHooked then + return + end + + local hooked = false + if QuestPOI_DisplayButton then + hooksecurefunc("QuestPOI_DisplayButton", function(parentName, buttonType, buttonIndex) + QuestieCompat.SuppressDuplicateBlizzardPOI(parentName, buttonType, buttonIndex) + end) + hooked = true + end + + if QuestPOI_SelectButton then + hooksecurefunc("QuestPOI_SelectButton", function(poiButton) + QuestieCompat.SuppressDuplicateBlizzardPOIButton(poiButton) + end) + hooked = true + end + + QuestieCompat._blizzardPOISuppressionHooked = hooked +end + -- https://wowpedia.fandom.com/wiki/API_GetQuestLink -- Returns a QuestLink for a quest. -- Between patches 6.2 and 7.3.2 argument was changed to take a QuestID instead of a quest log index. diff --git a/Modules/Options/IconsTab/QuestieOptionsIcons.lua b/Modules/Options/IconsTab/QuestieOptionsIcons.lua index 5a41553..d7a87c2 100644 --- a/Modules/Options/IconsTab/QuestieOptionsIcons.lua +++ b/Modules/Options/IconsTab/QuestieOptionsIcons.lua @@ -266,7 +266,6 @@ function QuestieOptions.tabs.icons:Initialize() get = function() return Questie.db.profile.enableObjectives; end, set = function(info, value) Questie.db.profile.enableObjectives = value - QuestieCompat.SyncBlizzardObjectivePOIs(value) QuestieQuest:ToggleNotes(value) QuestieOptionsUtils.DetermineTheme() end, @@ -1335,7 +1334,6 @@ end function QuestieOptionsUtils.ExecuteTheme(info, value) Questie.db.profile.iconTheme = value if value == 'questie' then - QuestieCompat.SyncBlizzardObjectivePOIs(true) Questie.db.profile.enableObjectives = true Questie.db.profile.ICON_SLAY = Questie.icons["slay"] Questie.db.profile.ICON_LOOT = Questie.icons["loot"] @@ -1348,7 +1346,6 @@ function QuestieOptionsUtils.ExecuteTheme(info, value) Questie.db.profile.alwaysGlowMinimap = optionsDefaults.profile.alwaysGlowMinimap Questie.db.profile.clusterLevelHotzone = optionsDefaults.profile.clusterLevelHotzone elseif value == 'pfquest' then - QuestieCompat.SyncBlizzardObjectivePOIs(true) Questie.db.profile.enableObjectives = true Questie.db.profile.ICON_SLAY = Questie.icons["node"] Questie.db.profile.ICON_LOOT = Questie.icons["node"] @@ -1361,7 +1358,7 @@ function QuestieOptionsUtils.ExecuteTheme(info, value) Questie.db.profile.alwaysGlowMinimap = false Questie.db.profile.clusterLevelHotzone = 1 elseif value == 'blizzard' then - QuestieCompat.SyncBlizzardObjectivePOIs(false) + QuestieCompat.EnableBlizzardObjectivePOIs() Questie.db.profile.enableObjectives = false Questie.db.profile.ICON_SLAY = Questie.icons["slay"] Questie.db.profile.ICON_LOOT = Questie.icons["loot"] diff --git a/Modules/QuestieInit.lua b/Modules/QuestieInit.lua index 2ab3566..41f2a29 100644 --- a/Modules/QuestieInit.lua +++ b/Modules/QuestieInit.lua @@ -226,7 +226,7 @@ QuestieInit.Stages[1] = function() -- run as a coroutine end Questie:SetIcons() - QuestieCompat.SyncBlizzardObjectivePOIs(Questie.db.profile.enableObjectives) + QuestieCompat.InitializeBlizzardPOISuppression() if QUESTIE_LOCALES_OVERRIDE ~= nil then l10n:InitializeLocaleOverride() diff --git a/Modules/QuestieMenu/QuestieMenu.lua b/Modules/QuestieMenu/QuestieMenu.lua index ecf9932..158ffe9 100644 --- a/Modules/QuestieMenu/QuestieMenu.lua +++ b/Modules/QuestieMenu/QuestieMenu.lua @@ -302,7 +302,6 @@ function QuestieMenu:Show(hideDelay) tinsert(menuTable, { text= l10n("Objective"), func = function() local value = not Questie.db.profile.enableObjectives Questie.db.profile.enableObjectives = value - QuestieCompat.SyncBlizzardObjectivePOIs(value) QuestieQuest:ToggleNotes(value) QuestieQuest:SmoothReset() end, icon=QuestieLib.AddonPath.."Icons\\event.blp", notCheckable=false, checked=Questie.db.profile.enableObjectives, isNotRadio=true, keepShownOnClick=true}) diff --git a/Tests/QuestieBlizzardObjectivePOISync_spec.lua b/Tests/QuestieBlizzardObjectivePOISync_spec.lua index b194fa7..95ca8d4 100644 --- a/Tests/QuestieBlizzardObjectivePOISync_spec.lua +++ b/Tests/QuestieBlizzardObjectivePOISync_spec.lua @@ -9,26 +9,42 @@ local function has(content, needle) return content:find(needle, 1, true) ~= nil end -describe("Blizzard objective POI sync", function() +describe("Blizzard objective POI suppression", function() local compat = read("Compat/Compat.lua") local init = read("Modules/QuestieInit.lua") local iconOptions = read("Modules/Options/IconsTab/QuestieOptionsIcons.lua") local questieMenu = read("Modules/QuestieMenu/QuestieMenu.lua") - it("disables native quest POIs when Questie objective icons are enabled", function() - local helperStart = assert(compat:find("function QuestieCompat.SyncBlizzardObjectivePOIs", 1, true)) + it("suppresses only native POI buttons that duplicate visible 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) - assert.is_true(has(helper, "SetCVar(\"questPOI\", useQuestieObjectives and \"0\" or \"1\")")) - assert.is_true(has(helper, "WorldMapQuestShowObjectives:SetChecked(not useQuestieObjectives)")) + assert.is_true(has(compat, "complete = true")) + assert.is_true(has(compat, "monster = true")) + assert.is_true(has(compat, "object = true")) + assert.is_true(has(compat, "item = true")) + assert.is_true(has(compat, "event = true")) + 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()")) end) - it("syncs native POIs on startup and objective ownership changes", function() - assert.is_true(has(init, "QuestieCompat.SyncBlizzardObjectivePOIs(Questie.db.profile.enableObjectives)")) - assert.is_true(has(iconOptions, "QuestieCompat.SyncBlizzardObjectivePOIs(value)")) - assert.is_true(has(iconOptions, "QuestieCompat.SyncBlizzardObjectivePOIs(true)")) - assert.is_true(has(iconOptions, "QuestieCompat.SyncBlizzardObjectivePOIs(false)")) - assert.is_true(has(questieMenu, "QuestieCompat.SyncBlizzardObjectivePOIs(value)")) + it("hooks Blizzard POI display without globally disabling native POIs", function() + local helperStart = assert(compat:find("function QuestieCompat.InitializeBlizzardPOISuppression", 1, true)) + local helperEnd = assert(compat:find("-- https://wowpedia.fandom.com/wiki/API_GetQuestLink", helperStart, true)) + local helper = compat:sub(helperStart, helperEnd) + + assert.is_true(has(helper, "hooksecurefunc(\"QuestPOI_DisplayButton\"")) + assert.is_true(has(helper, "hooksecurefunc(\"QuestPOI_SelectButton\"")) + assert.is_true(has(compat, "\"poi%s%s_%d\"")) + assert.is_false(has(compat, "SetCVar(\"questPOI\", useQuestieObjectives and \"0\" or \"1\")")) + end) + + it("initializes duplicate suppression but leaves objective toggles in control of Questie icons only", function() + assert.is_true(has(init, "QuestieCompat.InitializeBlizzardPOISuppression()")) + assert.is_false(has(iconOptions, "QuestieCompat.SyncBlizzardObjectivePOIs")) + assert.is_true(has(iconOptions, "QuestieCompat.EnableBlizzardObjectivePOIs()")) + assert.is_false(has(questieMenu, "QuestieCompat.SyncBlizzardObjectivePOIs")) end) end)