diff --git a/Compat/Compat.lua b/Compat/Compat.lua index 0d26ebc..0266a96 100644 --- a/Compat/Compat.lua +++ b/Compat/Compat.lua @@ -474,6 +474,12 @@ function QuestieCompat.GetServerTime() end local questObjectivesCache = {} +-- Chat-parsed objective progress (see QuestieCompat.UiInfoMessage below) is only valid for a +-- short window: it exists to get ahead of GetQuestLogLeaderBoard's text, which can lag behind +-- the chat message on this server. If it isn't consumed quickly, GetQuestLogLeaderBoard has +-- almost certainly caught up on its own, so an old entry is more likely a stale/mismatched +-- leftover (e.g. two quests sharing the same objective text) than a still-valid override. +local QUEST_OBJECTIVE_CACHE_TTL = 3 local function parseQuestObjective(text) local name, fulfilled, required = string.match(string.gsub(text, "\239\188\154", ":"), "(.*):%s*([%d]+)%s*/%s*([%d]+)") @@ -499,10 +505,14 @@ cLog.GetQuestObjectives = function(questID, questLogIndex) if objectiveType ~= "log" and description then local objectiveName, numFulfilled, numRequired = parseQuestObjective(description) if objectiveName then - local fulfilled = questObjectivesCache[objectiveName] - if fulfilled then - numFulfilled = fulfilled + local cachedOverride = questObjectivesCache[objectiveName] + if cachedOverride then + -- Always drop it on read: a hit is single-use whether or not it's stale, + -- otherwise a leftover entry could keep getting reapplied to unrelated reads. questObjectivesCache[objectiveName] = nil + if (GetTime() - cachedOverride.time) <= QUEST_OBJECTIVE_CACHE_TTL then + numFulfilled = cachedOverride.value + end end end @@ -1738,7 +1748,7 @@ function QuestieCompat.UiInfoMessage(event, message) if string.find(message, pattern) then local objectiveName, numFulfilled = parseQuestObjective(message) if objectiveName and numFulfilled then - questObjectivesCache[objectiveName] = numFulfilled + questObjectivesCache[objectiveName] = { value = numFulfilled, time = GetTime() } end MinimapIcon:UpdateText(message) end diff --git a/Compat/HBD.lua b/Compat/HBD.lua index 66116e9..cf5b9de 100644 --- a/Compat/HBD.lua +++ b/Compat/HBD.lua @@ -869,8 +869,36 @@ local function HandleWorldMapPin(icon, data) if x and y then icon:ClearAllPoints() - icon:SetPoint("CENTER", WorldMapButton, "TOPLEFT", x * worldmapWidth, -y * worldmapHeight) + -- SetPoint offsets are in units of the icon's own effective scale, not + -- WorldMapButton's. worldmapWidth/Height are pre-multiplied by + -- WorldMapButton's effective scale (see UpdateWorldMap), so divide back + -- out by the icon's effective scale to get the correct local offset. + -- Needed because zoom addons (e.g. Magnify, bundled with LootCollector) + -- rescale an ancestor frame instead of WorldMapButton itself. + local iconEffScale = icon:GetEffectiveScale() + icon:SetPoint("CENTER", WorldMapButton, "TOPLEFT", (x * worldmapWidth) / iconEffScale, -(y * worldmapHeight) / iconEffScale) icon:Show() + + -- Questie fix: when zoomed in (e.g. via Magnify/LootCollector), WorldMapButton is + -- drawn far larger than the visible window and only WorldMapScrollFrame clips it. + -- Pins positioned past the visible edge still render on top of the surrounding UI + -- instead of being clipped, so hide them manually once they're outside the + -- scroll frame's actual on-screen bounds. GetLeft/Right/Top/Bottom/Center are all + -- in a shared scale-independent coordinate space, so no extra scale math needed. + -- Only do this when the map is actually drawn larger than its viewport (the zoom + -- case) -- at normal size (e.g. just the "show objective" panel active, no zoom) + -- the whole map already fits on-screen, so this check should never fire there; + -- skip it entirely rather than risk false positives hiding valid icons. + if WorldMapScrollFrame and WorldMapScrollFrame:IsVisible() then + local scrollWidth, scrollHeight = WorldMapScrollFrame:GetWidth(), WorldMapScrollFrame:GetHeight() + if scrollWidth and scrollHeight and (worldmapWidth > scrollWidth + 1 or worldmapHeight > scrollHeight + 1) then + local iconCenterX, iconCenterY = icon:GetCenter() + local vLeft, vRight, vTop, vBottom = WorldMapScrollFrame:GetLeft(), WorldMapScrollFrame:GetRight(), WorldMapScrollFrame:GetTop(), WorldMapScrollFrame:GetBottom() + if iconCenterX and vLeft and (iconCenterX < vLeft or iconCenterX > vRight or iconCenterY > vTop or iconCenterY < vBottom) then + icon:Hide() + end + end + end else icon:Hide() end @@ -885,7 +913,11 @@ end local function UpdateWorldMap() if not WorldMapFrame:IsVisible() then return end - local scale = WorldMapButton:GetScale() + -- Use effective (inherited) scale, not WorldMapButton's own scale: zoom addons + -- like Magnify (bundled with LootCollector) hardcode WorldMapButton's own scale + -- to 1 and instead rescale an ancestor frame (WorldMapDetailFrame), so only + -- GetEffectiveScale() reflects the actual on-screen size in all cases. + local scale = WorldMapButton:GetEffectiveScale() worldmapWidth = WorldMapButton:GetWidth()*scale worldmapHeight = WorldMapButton:GetHeight()*scale @@ -901,8 +933,27 @@ local function UpdateWorldMap() end pins.UpdateWorldMap = UpdateWorldMap +-- Questie fix: WorldMapButton can be resized/rescaled by things we have no event for +-- (this client's "show objective" side panel, third-party map-zoom addons, etc). +-- Poll its size/effective-scale each frame like we already do for the minimap, and only +-- pay for a full reposition when it actually changed. Effective scale (not own scale) is +-- required because zoom addons like Magnify rescale an ancestor frame instead of +-- WorldMapButton itself, leaving WorldMapButton:GetScale() frozen at 1. +local lastWorldMapRawWidth, lastWorldMapRawHeight, lastWorldMapEffScale +local function CheckWorldMapSizeChanged() + if not WorldMapFrame:IsVisible() then return end + + local w, h, s = WorldMapButton:GetWidth(), WorldMapButton:GetHeight(), WorldMapButton:GetEffectiveScale() + if w ~= lastWorldMapRawWidth or h ~= lastWorldMapRawHeight or s ~= lastWorldMapEffScale then + lastWorldMapRawWidth, lastWorldMapRawHeight, lastWorldMapEffScale = w, h, s + UpdateWorldMap() + end +end + local last_update = 0 local function OnUpdateHandler(frame, elapsed) + CheckWorldMapSizeChanged() + last_update = last_update + elapsed if last_update > 1 or queueFullUpdate then UpdateMinimapPins(queueFullUpdate) diff --git a/Modules/Quest/QuestEventHandler.lua b/Modules/Quest/QuestEventHandler.lua index 9a4cf88..dc3b5a6 100644 --- a/Modules/Quest/QuestEventHandler.lua +++ b/Modules/Quest/QuestEventHandler.lua @@ -104,6 +104,21 @@ function QuestEventHandler:RegisterEvents() end) end + -- Force a full reconciliation whenever the player opens the native quest log. + -- QuestLogCache only updates its stored objective counts when it detects a change, so if a + -- scan ever ran while the game's own quest log text was still lagging behind the server, the + -- Tracker can get stuck showing stale progress with nothing left to trigger a retry. Looking + -- at the quest log is the moment the player is most likely to notice the mismatch, so treat it + -- as a cue to force a fresh comparison against the live quest log state. + local questLogFrame = QuestLogExFrame or ClassicQuestLog or QuestLogFrame + if questLogFrame then + questLogFrame:HookScript("OnShow", function() + Questie:Debug(Questie.DEBUG_DEVELOP, "[Quest Event] Quest log opened: forcing full quest log scan") + doFullQuestLogScan = true + _QuestEventHandler:QuestLogUpdate() + end) + end + -- StaticPopup dialog hooks. Deleteing Quest items do not always trigger a Quest Log Update. hooksecurefunc("StaticPopup_Show", function(...) -- Hook StaticPopup_Show. If we find the "DELETE_ITEM" dialog, check for Quest Items and notify the player. diff --git a/Modules/QuestieLearner.lua b/Modules/QuestieLearner.lua index 0c40793..151344b 100644 --- a/Modules/QuestieLearner.lua +++ b/Modules/QuestieLearner.lua @@ -795,6 +795,19 @@ end -- so the second debounce stage would only add redundant latency. local _deferPinRefreshScheduling = false +-- Clearing QuestieDB.private.questCache[questId] makes the NEXT QuestieDB.GetQuest(questId) +-- rebuild a brand new quest table so the just-learned override data takes effect. But +-- QuestiePlayer.currentQuestlog[questId] (what the Tracker actually reads every redraw) holds a +-- direct reference to the OLD table and is never told about the swap, so it freezes forever on +-- whatever the old table last held while every future update lands on the new, unreferenced one. +-- Rebuild immediately and re-point currentQuestlog at the new table so nothing is left orphaned. +local function _InvalidateQuestCache(questId) + QuestieDB.private.questCache[questId] = nil + if QuestiePlayer.currentQuestlog[questId] then + QuestiePlayer.currentQuestlog[questId] = QuestieDB.GetQuest(questId) + end +end + local function _RefreshActiveQuestPins(questIdSet) -- Skip scheduling when the set is empty (avoids no-op timer callbacks) if not next(questIdSet) then return end @@ -1995,7 +2008,7 @@ function QuestieLearner:LearnQuest(questId, data) end end if QuestieDB.private and QuestieDB.private.questCache then - QuestieDB.private.questCache[questId] = nil + _InvalidateQuestCache(questId) end end @@ -2044,7 +2057,7 @@ function QuestieLearner:LearnQuestGiver(questId, entityId, entityType, isStart) end if not found then table.insert(ovrList, entityId) end if QuestieDB.private and QuestieDB.private.questCache then - QuestieDB.private.questCache[questId] = nil + _InvalidateQuestCache(questId) end end @@ -2120,7 +2133,7 @@ function QuestieLearner:LearnQuestObjectiveNPC(questId, npcId, objText, objectiv ovr.objIndex[objectiveIndex] = existing.objIndex[objectiveIndex] end if QuestieDB.private and QuestieDB.private.questCache then - QuestieDB.private.questCache[questId] = nil + _InvalidateQuestCache(questId) end end @@ -2209,7 +2222,7 @@ function QuestieLearner:LearnQuestObjectiveObject(questId, objectId, objText, ob ovr.objIndex[objectiveIndex] = existing.objIndex[objectiveIndex] end if QuestieDB.private and QuestieDB.private.questCache then - QuestieDB.private.questCache[questId] = nil + _InvalidateQuestCache(questId) end end diff --git a/Modules/Tracker/QuestieTracker.lua b/Modules/Tracker/QuestieTracker.lua index f456ef1..3e9cd19 100644 --- a/Modules/Tracker/QuestieTracker.lua +++ b/Modules/Tracker/QuestieTracker.lua @@ -1039,6 +1039,25 @@ function QuestieTracker:Update() (Questie.db.profile.collapseCompletedQuests and isMinimizable), false) end + if not coloredQuestName then + -- QuestieDB has no "name" entry for this quest (e.g. a custom + -- server quest not yet in the static DB), so GetColoredQuestName + -- returned nil -- fall back instead of SetText(nil) blanking the + -- title line while its objectives still render normally below it. + -- Level/tag lookups don't require a DB "name" entry, so reuse the + -- same formatting helper as the normal path to keep the "[level]" + -- prefix consistent with quests that do have a DB name. + local fallbackName = quest.name or tostring(quest.Id) + if Questie.db.profile.trackerShowQuestLevel then + local level = QuestieLib.GetTbcLevel(quest.Id) + fallbackName = QuestieLib:GetQuestString(quest.Id, fallbackName, level, false) + end + if Questie.db.profile.enableTooltipsQuestID then + fallbackName = fallbackName .. " (" .. quest.Id .. ")" + end + coloredQuestName = "|cFFFFFF00" .. fallbackName .. "|r" + end + line.label:SetText(coloredQuestName) -- Check and measure Quest Label text width and update tracker width diff --git a/Modules/Tracker/TrackerBaseFrame.lua b/Modules/Tracker/TrackerBaseFrame.lua index a95a4aa..5f1e99a 100644 --- a/Modules/Tracker/TrackerBaseFrame.lua +++ b/Modules/Tracker/TrackerBaseFrame.lua @@ -317,16 +317,17 @@ function TrackerBaseFrame.OnDragStart(frame, button) if TrackerBaseFrame.isMoving ~= true and TrackerBaseFrame.isSizing ~= true then if IsMouseButtonDown(button) and button ~= "MiddleButton" then if (IsControlKeyDown() and Questie.db.profile.trackerLocked and not ChatEdit_GetActiveWindow()) or not Questie.db.profile.trackerLocked then - if baseFrame:IsMovable() then - Questie:Debug(Questie.DEBUG_DEVELOP, "[TrackerBaseFrame:OnDragStart] - Dragging Started.") - TrackerBaseFrame.isMoving = true - TrackerBaseFrame.baseFrame.isMoving = true + -- Force movability here instead of trusting baseFrame:IsMovable(), which is + -- only refreshed asynchronously by TrackerBaseFrame:Update() on unrelated + -- tracker events and is almost never in sync with the live Ctrl/lock state + -- already checked above at the moment of an actual drag attempt. + baseFrame:SetMovable(true) + Questie:Debug(Questie.DEBUG_DEVELOP, "[TrackerBaseFrame:OnDragStart] - Dragging Started.") + TrackerBaseFrame.isMoving = true + TrackerBaseFrame.baseFrame.isMoving = true - baseFrame:StartMoving() - TrackerBaseFrame:Update() - else - Questie:Debug(Questie.DEBUG_DEVELOP, "[TrackerBaseFrame:OnDragStart] - Frame is not movable!") - end + baseFrame:StartMoving() + TrackerBaseFrame:Update() end end end diff --git a/Modules/Tracker/TrackerHeaderFrame.lua b/Modules/Tracker/TrackerHeaderFrame.lua index 4301d0d..fa141d7 100644 --- a/Modules/Tracker/TrackerHeaderFrame.lua +++ b/Modules/Tracker/TrackerHeaderFrame.lua @@ -51,6 +51,9 @@ function TrackerHeaderFrame.Initialize(baseFrame) questieIcon:EnableMouse(true) questieIcon:RegisterForClicks("LeftButtonUp", "RightButtonUp") + questieIcon:RegisterForDrag("LeftButton") + questieIcon:SetScript("OnDragStart", TrackerBaseFrame.OnDragStart) + questieIcon:SetScript("OnDragStop", TrackerBaseFrame.OnDragStop) questieIcon:SetScript("OnClick", function(_, button) if button == "LeftButton" then