Apply local fixes on top of upstream: tracker desync/drag/title, map icon zoom offset

- Compat/HBD.lua: fix world map pin offset math to account for zoom addons
  (e.g. Magnify) rescaling an ancestor frame instead of WorldMapButton itself;
  cull pins that fall outside the scroll frame's viewport when zoomed
- Compat/Compat.lua: add a short TTL to the chat-parsed objective progress
  cache so a stale entry can't be reapplied to an unrelated quest
- Modules/QuestieLearner.lua: re-point QuestiePlayer.currentQuestlog at the
  rebuilt quest table after invalidating QuestieDB's quest cache, fixing
  Tracker freezing on stale objective progress
- Modules/Tracker/QuestieTracker.lua: fall back to a formatted quest name
  when QuestieDB has no title for a quest (e.g. custom server quests)
- Modules/Tracker/TrackerBaseFrame.lua: force baseFrame movability on drag
  start instead of trusting the async-refreshed IsMovable() state
- Modules/Tracker/TrackerHeaderFrame.lua: wire up drag on the tracker icon
- Modules/Quest/QuestEventHandler.lua: force a full quest log reconciliation
  when the native quest log is opened

Squashed from prior commit-by-commit history to rebuild this working copy
on a clean fork of aron-w/Questie-X (restores the native GitHub fork link).
Per-fix rationale is preserved in project memory
(project_tracker_fixes.md, project_map_icon_offset.md).
This commit is contained in:
2026-07-15 01:02:15 +02:00
parent 9df64114df
commit c09d30b832
7 changed files with 131 additions and 19 deletions
+14 -4
View File
@@ -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
+53 -2
View File
@@ -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)
+15
View File
@@ -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.
+17 -4
View File
@@ -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
+19
View File
@@ -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
+10 -9
View File
@@ -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
+3
View File
@@ -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