Compare commits

..

7 Commits

Author SHA1 Message Date
Narcasung 30fe4b83a9 fix(tracker): name and group quests built from partial records
Quests assembled from a Learner entry or a questDataOverrides entry that
never captured every field reach the tracker with nil fields, since GetQuest
copies rawdata key by key. Two of those show: a nil name printed as the
quest id, and a nil zoneOrSort that sent _GetZoneName down its very first
line, `if not zoneOrSort then return "Unknown Zone" end`, before it could
consult the quest log header the earlier fix added. Those objects are cached
for the session, so neither repaired itself.

GetQuest now fills a missing name from the quest log and defaults zoneOrSort
to 0, the value every caller already reads as "no zone on file" -- and which
some of them require, `quest.zoneOrSort > 0` erroring outright on nil.
_GetZoneName treats nil the same way rather than short-circuiting, which
also stops a nil quest from labelling its group Unknown Zone under the sort
modes that do not group by zone at all.

The tracker asks the quest log for a title before printing an id, so quests
already cached without a name come out right too, and the live-fallback
builder stops discarding the override data it just looked up.
2026-08-17 15:57:17 +02:00
Narcasung 26a135a2ca fix(tracker): show and update objectives on quests missing from the DB
Quests with no QuestieDB entry get a fallback object built from the quest
log, and three things were wrong with it.

Its objectives carried the leaderboard line in a `text` field, but the
tracker's objective loop skips any objective without a `Description`, so
those quests drew a title and nothing under it. Split the line into a
description and its counts instead, and set the same `Index`/`Completed`
fields a DB objective carries.

The object was then cached for the session and only rebuilt when it had no
zone info, so its counts stayed at whatever the log said when the quest was
first seen. `IsComplete` had the same problem one level down: it closed over
the completion flag read at build time, so the quest never went (Complete).
Both now re-read the log on every draw, in place, so the objective tables
the drawn lines hold stay valid.

Refreshing per draw needs the quest's log index, so the walk that already
builds the header map records it too.
2026-08-17 14:11:49 +02:00
Narcasung cf67ceb474 docs: cover the supertrack buttons, main quest and quest tag fixes 2026-07-30 18:33:33 +02:00
Narcasung de270a0e8e fix(tracker): drop quests the player no longer has
Ascension's special quests are finished by the server the moment their
objectives are met, and one of them stayed in the tracker for the rest of
the session: currentQuestlog is only as good as the removal events that
maintain it, and nothing landed for that quest.

The tracker now checks the quest log itself, which it already walks once
per draw for the zone headers, and skips anything that is no longer in
it, dropping the fallback object built for it as well. Skipped rather
than pruned, so a redraw that catches the log mid-refresh does not throw
away state Questie is about to want back.

Three things on the event side that let it get that far:

- CleanupRemovedQuestsFallback now also runs after a full quest log scan.
  UpdateAllQuests only inspects quests still in the log, so for as long
  as something kept asking for full scans, nothing looked for removals.
- A removed quest counts as completed when the server has it flagged
  complete. QuestieDB.IsComplete cannot answer for a quest the database
  has never heard of, so these were filed as abandoned instead.
- The removal scan type-checks the quest id before comparing it, so a
  stray string key cannot error out the pass for every other quest.
2026-07-30 18:26:42 +02:00
Narcasung c6019ea823 style(tracker): tighten the gutter ahead of a quest title
The collapse button moves to 4px from the quest text, the marker to 1px
from the collapse button, and the quest item buttons 6px in from the edge
of the line. Everything ahead of the title is that much closer to it, so
the marker, which tucks in to the left of whichever of those it finds,
hangs less far over the tracker's edge -- at the default sizes it no
longer reaches past it at all when the quest has no item button.

The item button offset lives in TrackerLinePool now: the marker is placed
against it, so the two cannot drift apart.
2026-07-30 18:26:42 +02:00
Narcasung 706fc0b8ad fix(tracker): group quests under their quest log header for server categories
Ascension files its main quest chain under a header of its own,
"Ascension Main Quest", which exists in no zone table. The tracker fell
back to the quest's zoneOrSort, and for anything the Learner recorded
that is the zone the quest was picked up in -- so the whole chain showed
up under "Red Cloud Mesa".

A header that resolves to no area is a category the server invented, and
the client's own grouping is the only thing that knows about it, so use
it. Headers are now mapped in a single pass per draw instead of a walk of
the quest log per quest.
2026-07-30 18:15:10 +02:00
Narcasung c2b1b82b2f feat(tracker): tuck the objective marker in beside the quest line
Drops the left-margin reserve the marker used to get: the quest list goes
back to its original indent and the marker tucks in to the left of
whatever owns the head of the line -- the quest item button where there
is one, the collapse button where there is not -- hanging over the
tracker's left edge, and off it entirely at larger sizes. A marker that
reaches past the edge is reparented above the scroll frame, which would
otherwise clip it.

It is also centred on the quest's whole text block rather than on the
title alone. The block is added up from the line heights the tracker
itself sets: measuring the frames with GetTop/GetBottom reads positions
from a half-built tracker on login and flings the marker off it.
2026-07-30 18:15:04 +02:00
6 changed files with 490 additions and 128 deletions
+43 -5
View File
@@ -1785,16 +1785,36 @@ function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
end
return nil
end
-- Build a minimal live-fallback quest from the quest log so the tracker still works
-- Build a minimal live-fallback quest from the quest log so the tracker still works.
-- The override that got us here is partial by nature -- a Learner record of a single
-- field, a correction -- but whatever it does carry beats guessing, and the quest object
-- built here is cached for the session, so anything left blank stays blank.
local logEntry = QuestLogCache.GetQuest(questId)
if not logEntry then return nil end
local function NonEmpty(text)
if text and text ~= "" then return text end
return nil
end
local overrideName = NonEmpty(overrideData[QuestieDB.questKeys.name])
local overrideLevel = overrideData[QuestieDB.questKeys.questLevel]
local overrideZone = overrideData[QuestieDB.questKeys.zoneOrSort]
local cachedTitle = NonEmpty(logEntry.title)
local liveTitle
if (not overrideName) and (not cachedTitle) and GetQuestLogIndexByID and GetQuestLogTitle then
-- QuestLogCache is a snapshot and can be missing the title of a quest the client
-- has since filled in, which is what leaves a quest showing as its own id.
local questLogIndex = GetQuestLogIndexByID(questId)
if questLogIndex and questLogIndex > 0 then
liveTitle = NonEmpty(GetQuestLogTitle(questLogIndex))
end
end
local fallback = {
Id = questId,
name = logEntry.title or tostring(questId),
level = logEntry.level or 0,
questLevel = logEntry.level or 0,
name = overrideName or cachedTitle or liveTitle or tostring(questId),
level = overrideLevel or logEntry.level or 0,
questLevel = overrideLevel or logEntry.level or 0,
requiredLevel = 0,
zoneOrSort = 0,
zoneOrSort = overrideZone or 0,
questFlags = 0,
specialFlags = 0,
Starts = { CreatureStarts = {}, ObjectStarts = {}, ItemStarts = {} },
@@ -1916,6 +1936,24 @@ function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
if learnerRecord.objIndex and not QO.objIndex then QO.objIndex = learnerRecord.objIndex end
end
-- A record that never captured a name -- a Learner entry written from an objective update, an
-- override carrying a single key -- leaves QO.name nil, and this object is cached for the rest
-- of the session, so everything downstream ends up printing the quest id. Ask the client.
if ((not QO.name) or QO.name == "") and GetQuestLogIndexByID and GetQuestLogTitle then
local questLogIndex = GetQuestLogIndexByID(questId)
if questLogIndex and questLogIndex > 0 then
local logTitle = GetQuestLogTitle(questLogIndex)
if logTitle and logTitle ~= "" then
QO.name = logTitle
end
end
end
-- Same story for the zone: partial records leave it nil, and callers all treat it as a number
-- (`quest.zoneOrSort > 0` errors outright on nil). 0 is the value everything already reads as
-- "no zone on file", which sends the tracker to the quest log for one.
QO.zoneOrSort = QO.zoneOrSort or 0
local questLevel, requiredLevel = QuestieLib.GetTbcLevel(questId)
QO.level = questLevel
QO.requiredLevel = requiredLevel
+13 -2
View File
@@ -553,6 +553,10 @@ function _QuestEventHandler:QuestLogUpdate()
doFullQuestLogScan = false
-- Function call updates doFullQuestLogScan. Order matters.
_QuestEventHandler:UpdateAllQuests()
-- Also on this path: UpdateAllQuests only looks at quests still in the log, so a removal
-- that fired no event of its own would sit there unnoticed for as long as full scans keep
-- being asked for.
_QuestEventHandler:CleanupRemovedQuestsFallback()
else
_QuestEventHandler:CleanupRemovedQuestsFallback()
QuestieCombatQueue:Queue(function()
@@ -666,7 +670,9 @@ function _QuestEventHandler:CleanupRemovedQuestsFallback()
if QuestiePlayer and QuestiePlayer.currentQuestlog then
local removedQuestIds = {}
for questId in pairs(QuestiePlayer.currentQuestlog) do
if questId and questId > 0 and (not gameQuestIds[questId]) then
-- Typed check: a stray string key (saved variables have produced them) would other-
-- wise error on the comparison and take the whole pass down with it.
if type(questId) == "number" and questId > 0 and (not gameQuestIds[questId]) then
removedQuestIds[#removedQuestIds + 1] = questId
end
end
@@ -679,7 +685,12 @@ function _QuestEventHandler:CleanupRemovedQuestsFallback()
local wasTurnedIn = questLog[questId] and questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN
local wasAlreadyComplete = Questie.db.char.complete and Questie.db.char.complete[questId]
local completeAtRemoval = QuestieDB.IsComplete(questId)
local shouldComplete = wasTurnedIn or wasAlreadyComplete or completeAtRemoval == 1
-- The server's own record, and the only one that knows anything about a quest the
-- database has never heard of: QuestieDB.IsComplete cannot answer for those, so an
-- Ascension quest the server finished by itself would otherwise be filed as abandoned.
local serverFlaggedComplete = IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(questId)
local shouldComplete = wasTurnedIn or wasAlreadyComplete or completeAtRemoval == 1 or
serverFlaggedComplete
QuestLogCache.RemoveQuest(questId)
QuestieQuest:SetObjectivesDirty(questId)
+55 -26
View File
@@ -63,6 +63,22 @@ local function GetWrappedWidth(label)
return label:GetWidth()
end
-- A quest built from partial data can reach the tracker with no name, or with the id standing in
-- for one, and it is cached that way for the session. Ask the quest log before printing a number.
local function GetDisplayableQuestName(quest)
local questName = quest.name
if (not questName) or questName == "" or questName == tostring(quest.Id) then
questName = TrackerUtils:GetQuestLogTitleById(quest.Id) or questName
end
if (not questName) or questName == "" then
questName = tostring(quest.Id)
end
return questName
end
local LSM30 = LibStub and LibStub("LibSharedMedia-3.0", true)
-- Local Vars
@@ -845,11 +861,7 @@ function QuestieTracker:Update()
-- Setup local QuestieTracker:Update vars
local trackerFontSizeZone = Questie.db.profile.trackerFontSizeZone
local trackerFontSizeQuest = Questie.db.profile.trackerFontSizeQuest
-- The supertrack button sits at the very left of a quest line, so the whole quest list is
-- indented past it: otherwise it is clipped by the tracker's left edge and collides with the
-- quest item buttons, which share that gutter.
local superTrackMarginLeft = TrackerLinePool.GetSuperTrackMarginReserve()
local questMarginLeft = (trackerMarginLeft + trackerMarginRight) - (18 - trackerFontSizeQuest) + superTrackMarginLeft
local questMarginLeft = (trackerMarginLeft + trackerMarginRight) - (18 - trackerFontSizeQuest)
local objectiveMarginLeft = questMarginLeft + trackerFontSizeQuest
local questItemButtonSize = 12 + trackerFontSizeQuest
local objectiveColor = Questie.db.profile.trackerColorObjectives
@@ -899,11 +911,9 @@ function QuestieTracker:Update()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Setup Zone Label. Indented like the quest lines below it: the supertrack
-- buttons overflow their own line at larger sizes, and the zone label is the
-- only text that would otherwise share that gutter with them.
-- Setup Zone Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", superTrackMarginLeft, 0)
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", 0, 0)
-- Set Zone Title and default Min/Max states
if Questie.db.char.collapsedZones[zoneName] then
@@ -930,19 +940,19 @@ function QuestieTracker:Update()
-- Check and measure Zone Label text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() + trackerMarginLeft +
superTrackMarginLeft + trackerMarginRight)
trackerMarginRight)
-- Set Zone Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - trackerMarginLeft - superTrackMarginLeft - trackerMarginRight)
line:SetWidth(line.label:GetWidth() + superTrackMarginLeft)
line.label:SetWidth(trackerBaseFrame:GetWidth() - trackerMarginLeft - trackerMarginRight)
line:SetWidth(line.label:GetWidth())
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + trackerMarginLeft + superTrackMarginLeft)
line.label:GetStringWidth() + trackerMarginLeft)
-- Setup Min/Max Button
line.expandZone:ClearAllPoints()
line.expandZone:SetPoint("TOPLEFT", line, "TOPLEFT", superTrackMarginLeft, 0)
line.expandZone:SetPoint("TOPLEFT", line, "TOPLEFT", 0, 0)
line.expandZone:SetWidth(line.label:GetWidth())
line.expandZone:SetHeight(line.label:GetHeight())
line.expandZone:Show()
@@ -967,6 +977,10 @@ function QuestieTracker:Update()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then return "BREAK" end
-- Kept so the supertrack button, which lives on the title line, can be
-- re-centred over the finished quest block further down.
local questTitleLine = line
-- Set Line Mode, Types, Clickers
line:SetMode("quest")
line:SetOnClick("quest")
@@ -976,7 +990,7 @@ function QuestieTracker:Update()
line.criteriaMark:Hide()
-- Set Min/Max Button and default states
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 8, 1)
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 4, 1)
line.expandQuest.zoneId = zoneName
@@ -1033,7 +1047,7 @@ function QuestieTracker:Update()
if quest.isFallback or quest._isLogFallback then
-- Quest not in DB: use the name stored on the fallback object
local questName = quest.name or tostring(quest.Id)
local questName = GetDisplayableQuestName(quest)
if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
questName = "[" .. quest.level .. "] " .. questName
end
@@ -1067,7 +1081,7 @@ function QuestieTracker:Update()
-- which queries the static DB only: for a quest with no DB entry it
-- silently defaults to level 1, and Ascension's scaling then scales
-- that wrong base instead of the quest's real level.
local fallbackName = quest.name or tostring(quest.Id)
local fallbackName = GetDisplayableQuestName(quest)
if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
fallbackName = QuestieLib:GetQuestString(quest.Id, fallbackName, quest.level, false)
end
@@ -1181,7 +1195,8 @@ function QuestieTracker:Update()
end
-- Attach button to Quest Title linePool
button:SetPoint("TOPLEFT", button.line, "TOPLEFT", superTrackMarginLeft, 0)
button:SetPoint("TOPLEFT", button.line, "TOPLEFT",
TrackerLinePool.GetItemButtonOffset(), 0)
button:SetParent(button.line)
button:Show()
@@ -1195,6 +1210,10 @@ function QuestieTracker:Update()
button:SetParent(UIParent)
button:Hide()
end
-- The quest item button owns this slot, so the marker steps out to
-- the left of the line for as long as it is there.
button.line.superTrackButton:SetItemButtonShown(button:IsShown())
else
-- Button failed to get setup for some reason or the quest item is now gone. Hide it and enable the Quest Min/Max button.
-- See previous comment for details on why we're setting this button to UIParent.
@@ -1289,7 +1308,7 @@ function QuestieTracker:Update()
-- Attach button to Quest Title linePool
altButton:SetPoint("TOPLEFT", altButton.line, "TOPLEFT",
superTrackMarginLeft + 2 + questItemButtonSize, 0)
TrackerLinePool.GetItemButtonOffset() + 2 + questItemButtonSize, 0)
altButton:SetParent(altButton.line)
altButton:Show()
@@ -1590,6 +1609,16 @@ function QuestieTracker:Update()
-- Adds 2 pixels and "Padding Between Quests" setting in Tracker Options
line:SetHeight(line.label:GetHeight() + (Questie.db.profile.trackerQuestPadding + 2))
-- Centre the supertrack button on the quest's whole text block now that its
-- objective lines are drawn and their heights are final.
if questTitleLine.superTrackButton.questId then
local blockHeight = TrackerLinePool.GetQuestBlockHeight(questTitleLine, line)
if blockHeight then
questTitleLine.superTrackButton:SetBlockHeight(blockHeight)
end
end
end
primaryButton = false
@@ -1648,9 +1677,9 @@ function QuestieTracker:Update()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Setup Zone Label (indented past the supertrack button gutter, as above)
-- Setup Zone Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", superTrackMarginLeft, 0)
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", 0, 0)
-- Set Zone Title and Min/Max states
if Questie.db.char.collapsedZones[zoneName] then
@@ -1679,15 +1708,15 @@ function QuestieTracker:Update()
-- Check and measure Zone Label text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() + trackerMarginLeft +
superTrackMarginLeft + trackerMarginRight)
trackerMarginRight)
-- Set Zone Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - trackerMarginLeft - superTrackMarginLeft - trackerMarginRight)
line:SetWidth(line.label:GetWidth() + superTrackMarginLeft)
line.label:SetWidth(trackerBaseFrame:GetWidth() - trackerMarginLeft - trackerMarginRight)
line:SetWidth(line.label:GetWidth())
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + trackerMarginLeft + superTrackMarginLeft)
line.label:GetStringWidth() + trackerMarginLeft)
-- Setup Min/Max Button
line.expandZone:ClearAllPoints()
@@ -1727,7 +1756,7 @@ function QuestieTracker:Update()
-- Set Min/Max Button and default states
line.expandQuest:Show()
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 8, 1)
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 4, 1)
line.expandQuest.zoneId = zoneName
-- The minAllQuestsInZone table is always blank until a player Shift+Clicks the Zone header (MouseDown).
+117 -19
View File
@@ -115,18 +115,20 @@ local linePool = {}
local buttonPool = {}
local lineMarginLeft = 10
-- Gutter given to the supertrack button, which sits flush with the left edge of a quest line. The
-- collapse button, the quest item buttons and the labels are all shifted right by this much, so
-- nothing lands on top of it and nothing hangs over the tracker's left edge. Fed into
-- questMarginLeft, which every width calculation already builds on, so the tracker widens to match.
function TrackerLinePool.GetSuperTrackMarginReserve()
if (not Questie.db.profile.trackerShowSuperTrackButton) or (not TrackerUtils:IsSuperTrackAvailable()) then
return 0
end
-- Gap kept between the supertrack button and whatever it is tucked in next to.
local superTrackButtonGap = 1
-- Reserved even for quests whose button is hidden, so the list does not shift around as quests
-- come in and out of the supertrackable set.
return (Questie.db.profile.trackerSuperTrackButtonSize or 25) + 4
-- Left edge of a quest line's collapse button. QuestieTracker anchors it at questMarginLeft - 4
-- with a width of trackerFontSizeQuest, and questMarginLeft carries a matching + trackerFontSizeQuest,
-- so it lands on a flat 22 whatever the font size is.
local superTrackCollapseButtonLeft = 22
-- Left edge of the quest item buttons. QuestieTracker anchors them there and the marker tucks in
-- to the left of them, so the two have to agree on it.
local questItemButtonLeft = 6
function TrackerLinePool.GetItemButtonOffset()
return questItemButtonLeft
end
---@param questFrame Frame
@@ -529,9 +531,62 @@ function TrackerLinePool.Initialize(questFrame)
superTrackButton.SetSuperTrackButton = function(self, questId)
self.questId = questId
-- Back to the title line on its own: the objective lines below have not been laid out
-- yet, so the tracker measures the block and calls SetBlockHeight once they are. Same
-- for the quest item button, which is set up further down and owns this slot.
self.blockHeight = nil
self.itemButtonShown = nil
self:RefreshSuperTrackButton()
end
-- Horizontally: tucked in to the left of whatever else owns the head of the line -- the
-- quest item button where there is one, the collapse button where there is not. It hangs
-- over the line's left edge, and off the tracker entirely at larger sizes; giving the
-- marker a column of its own would mean indenting every quest in the tracker for it.
-- Vertically: centred on the quest's whole text block, title plus objectives, the height
-- the tracker hands us. Without one, centred on the quest title alone, which is all that
-- exists at the point the button is first set up.
superTrackButton.AnchorSuperTrackButton = function(self)
local buttonSize = Questie.db.profile.trackerSuperTrackButtonSize or 25
local blockHeight = self.blockHeight or Questie.db.profile.trackerFontSizeQuest
local slotLeft = self.itemButtonShown and questItemButtonLeft or superTrackCollapseButtonLeft
local offsetX = slotLeft - superTrackButtonGap - buttonSize
-- The lines live inside the tracker's scroll frame, which clips anything hanging over
-- its edge, so a marker that reaches past it is reparented above the clip. It stays
-- anchored to its line either way, and every redraw hides it by hand
-- (ResetLinesForChange) before deciding whether to show it again.
self:SetParent(((offsetX + lineMarginLeft) < 0) and trackerQuestFrame or line)
self:ClearAllPoints()
self:SetPoint("TOPLEFT", line, "TOPLEFT", offsetX, (buttonSize - blockHeight) / 2 + 1)
-- Has to sit above the tracker backdrop, which is what swallows a frame left at level 0.
-- Strata comes from the line rather than from whichever parent it ended up with, so the
-- two cases draw the same.
self:SetFrameStrata(line:GetFrameStrata())
self:SetFrameLevel(line:GetFrameLevel() + 10)
end
superTrackButton.SetBlockHeight = function(self, blockHeight)
if self.blockHeight == blockHeight then
return
end
self.blockHeight = blockHeight
self:AnchorSuperTrackButton()
end
superTrackButton.SetItemButtonShown = function(self, shown)
shown = shown and true or false
if (self.itemButtonShown or false) == shown then
return
end
self.itemButtonShown = shown
self:AnchorSuperTrackButton()
end
superTrackButton.RefreshSuperTrackButton = function(self)
-- No map pin means the client has nothing to point the marker at (quest in another zone,
-- or a quest without map coordinates), so there is nothing to offer.
@@ -581,12 +636,7 @@ function TrackerLinePool.Initialize(questFrame)
-- while the mouse is still held down.
self:SetPressedOffset(false)
-- Flush with the left edge of the line, in the gutter GetSuperTrackMarginReserve keeps
-- clear. Anchored to the top rather than centred on the line so the button stays level
-- with the first row of a quest title that wraps, matching the collapse button.
local fontSizeQuest = Questie.db.profile.trackerFontSizeQuest
self:ClearAllPoints()
self:SetPoint("TOPLEFT", line, "TOPLEFT", 0, (buttonSize - fontSizeQuest) / 2 + 1)
self:AnchorSuperTrackButton()
-- The icon carries the selected variant itself; the glow is the one part the pin draws
-- as a separate texture.
@@ -596,8 +646,6 @@ function TrackerLinePool.Initialize(questFrame)
self.glow:Hide()
end
-- Has to sit above the tracker backdrop, which is what swallows a frame left at level 0.
self:SetFrameLevel(line:GetFrameLevel() + 10)
self:Show()
end
@@ -701,6 +749,9 @@ function TrackerLinePool.Initialize(questFrame)
line.expandQuest = expandQuest
-- Its own slot in the pool, so a run of lines drawn for one quest can be walked back over.
line.lineIndex = i
linePool[i] = line
nextFrame = line
end
@@ -1060,6 +1111,49 @@ function TrackerLinePool.GetCurrentButton()
return buttonPool[buttonIndex]
end
-- Height of the block of lines a single quest was drawn into, title line through last objective.
-- Added up from the heights the tracker itself set rather than measured off the frames: on login
-- the tracker has not been laid out yet, and GetTop/GetBottom then report positions from a
-- half-built frame, which is enough to fling a marker centred on the result off the tracker.
---@return number|nil blockHeight
function TrackerLinePool.GetQuestBlockHeight(firstLine, lastLine)
if (not firstLine) or (not lastLine) or (not firstLine.lineIndex) or (not lastLine.lineIndex) then
return nil
end
if lastLine.lineIndex < firstLine.lineIndex then
return nil
end
local blockHeight = 0
for i = firstLine.lineIndex, lastLine.lineIndex do
local line = linePool[i]
if not line then
return nil
end
blockHeight = blockHeight + line:GetHeight()
end
-- The last line of a quest carries the padding to the next one. That is empty space below the
-- text, so it plays no part in where the block's centre is. A quest collapsed down to its title
-- is the exception: that single line is the whole quest, and neither reference reads right on
-- its own -- against the text alone the marker rides high over the row's empty half, against
-- the whole row it sits low under the title it belongs to -- so it splits the difference.
local trailingPadding = Questie.db.profile.trackerQuestPadding + 2
if lastLine.lineIndex == firstLine.lineIndex then
trailingPadding = trailingPadding / 2
end
blockHeight = blockHeight - trailingPadding
if blockHeight <= 0 then
return nil
end
return blockHeight
end
---@return table|nil lineIndex linePool[lineIndex - 1]
function TrackerLinePool.GetPreviousLine()
lineIndex = lineIndex - 1
@@ -1109,6 +1203,10 @@ function TrackerLinePool.HideUnusedLines()
line.expandZone.zoneId = nil
line.criteriaMark.mode = nil
line.playButton.mode = nil
-- Hidden by hand: a marker that overflows to the left of its line is parented above the
-- scroll frame's clip, so hiding the line no longer hides it.
line.superTrackButton.questId = nil
line.superTrackButton:Hide()
end
end
end
+258 -75
View File
@@ -676,37 +676,162 @@ local function GetAreaIdByZoneName(zoneName)
return l10n:GetAreaIdByLocalName(zoneName)
end
-- Walk the quest log to find the zone header for a given questId.
-- In 3.3.5, zone names appear as isHeader=true entries above their quests.
-- Returns the header title string, or nil if not found.
local function GetQuestLogZoneName(questId)
local targetIndex = nil
local total = GetNumQuestLogEntries and GetNumQuestLogEntries() or 0
for i = 1, total do
-- questId -> the header the client filed it under in the quest log. In 3.3.5 those headers are
-- isHeader=true entries sitting above the quests they cover. Built in one pass and kept, because
-- the tracker asks per quest and redraws often -- walking the log once per quest is quadratic.
local questLogHeaders = {}
-- Every questId the log currently holds, headers aside. The tracker draws from currentQuestlog,
-- and this is what says whether the player still has a given quest.
local questLogQuestIds = {}
-- questId -> its row in the quest log, so re-reading a quest's leaderboard costs a lookup
-- instead of another walk. Verified before use, since the log renumbers on every change.
local questLogIndexes = {}
local function BuildQuestLogHeaders()
local headers = {}
local questIds = {}
local indexes = {}
local header
for i = 1, (GetNumQuestLogEntries and GetNumQuestLogEntries() or 0) do
local title, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(i)
if isHeader then
if title and title ~= "" then
header = title
end
elseif logId then
questIds[logId] = true
indexes[logId] = i
if header then
headers[logId] = header
end
end
end
questLogHeaders = headers
questLogQuestIds = questIds
questLogIndexes = indexes
end
-- Returns the quest's current row in the quest log, or nil if the player no longer has it.
local function GetQuestLogIndexForQuest(questId)
local index = questLogIndexes[questId]
if index then
local _, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(index)
if (not isHeader) and logId == questId then
return index
end
end
for i = 1, (GetNumQuestLogEntries and GetNumQuestLogEntries() or 0) do
local _, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(i)
if not isHeader and logId == questId then
targetIndex = i
break
end
end
if not targetIndex then return nil end
for i = targetIndex, 1, -1 do
local title, _, _, isHeader = GetQuestLogTitle(i)
if isHeader and title and title ~= "" then
return title
if (not isHeader) and logId == questId then
questLogIndexes[questId] = i
return i
end
end
return nil
end
-- 3.3.5 hands over a leaderboard line as one string -- "Icefang slain: 3/8" -- while the tracker
-- prints the description and the counts separately, so they have to come back apart here.
-- Objectives with nothing to count arrive as bare text and stand in as a single 0/1 step.
local function ParseLeaderBoardText(text, finished)
-- Greedy on purpose: a description of its own may hold a colon, and the counter is the last one.
local description, collected, needed = string.match(text, "^(.*):%s*(%d+)%s*/%s*(%d+)%s*$")
if not description then
-- Servers writing their own objective strings do not always keep the colon.
description, collected, needed = string.match(text, "^(.-)%s*(%d+)%s*/%s*(%d+)%s*$")
end
if (not description) or description == "" then
return text, (finished and 1 or 0), 1
end
collected = tonumber(collected) or 0
needed = tonumber(needed) or 1
if needed <= 0 then
needed = 1
end
return description, collected, needed
end
-- Fills `objectives` from the quest's leaderboard, in place: the tracker hands these very tables
-- to the lines it draws, so a redraw has to update them rather than swap in new ones.
local function ReadFallbackObjectives(questLogIndex, questId, objectives)
local numObjectives = (GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(questLogIndex)) or 0
for j = 1, numObjectives do
local text, objectiveType, finished = GetQuestLogLeaderBoard(j, questLogIndex)
if text then
local description, collected, needed = ParseLeaderBoardText(text, finished)
local objective = objectives[j]
if not objective then
objective = {}
objectives[j] = objective
end
objective.questId = questId
objective.Index = j
objective.Description = description
objective.text = text
objective.Collected = collected
objective.Needed = needed
objective.Completed = (finished or collected >= needed) and true or false
objective.baseType = objectiveType
-- Nothing here came from the DB, so map/tooltip code has to leave it alone.
objective.Type = "fallback"
end
end
for j = #objectives, numObjectives + 1, -1 do
objectives[j] = nil
end
return objectives
end
-- Returns the header title string, or nil if the quest is not in the log under one.
local function GetQuestLogZoneName(questId)
if not questLogQuestIds[questId] then
-- Asked about a quest the last pass did not see, so the log has moved on since.
BuildQuestLogHeaders()
end
return questLogHeaders[questId]
end
local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
if zoneNameOverride and zoneNameOverride ~= "" then
return zoneNameOverride
end
if not zoneOrSort then return "Unknown Zone" end
-- A quest assembled from a partial record -- a Learner entry that never captured the field,
-- an override carrying a single key -- reaches here with no zoneOrSort at all. That is the
-- same "nothing to look up" case as 0, and the quest log below still knows where the client
-- files the quest, so it must not short-circuit to Unknown Zone ahead of that.
zoneOrSort = zoneOrSort or 0
local zoneName
local sortObj = Questie.db.profile.trackerSortObjectives
if sortObj == "byZone" or sortObj == "byZoneComplete" or sortObj == "byZoneCompleteReversed" or sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then
-- A server can file quests under categories of its own -- Ascension's "Ascension Main
-- Quest" -- and those exist nowhere in the zone tables, so the quest data points at a zone
-- instead: for anything the Learner recorded, whichever zone it was picked up in. A header
-- that does not resolve to an area is one of those categories, and the client's own
-- grouping is the only thing that knows about it.
local logHeader = GetQuestLogZoneName(questId)
if logHeader and logHeader ~= "" then
local headerAreaId = GetAreaIdByZoneName(logHeader)
if (not headerAreaId) or headerAreaId == 0 then
return logHeader
end
end
if (zoneOrSort) > 0 then
zoneName = TrackerUtils:GetZoneNameByID(zoneOrSort)
if not zoneName or zoneName == "Unknown Zone" then
@@ -746,73 +871,119 @@ local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
return zoneName
end
-- The client's own title for a quest, or nil if the player does not have it. Quest objects can
-- reach the tracker without a usable name -- a questDataOverrides entry that carries no name
-- field, a QuestLogCache row read before the client had filled the title in -- and since those
-- objects are cached for the session, the name never repairs itself. The log always knows.
function TrackerUtils:GetQuestLogTitleById(questId)
local questLogIndex = GetQuestLogIndexForQuest(questId)
if not questLogIndex then return nil end
local title = GetQuestLogTitle(questLogIndex)
if title and title ~= "" then
return title
end
return nil
end
-- IsComplete must be a method (called as quest:IsComplete()), and it reads the state the last
-- refresh stored rather than closing over the state at build time -- these quests outlive many
-- redraws, and a captured value would still claim the quest is unfinished after it is turned in.
local function FallbackQuestIsComplete(self)
if self.logIsComplete == 1 or self.logIsComplete == true then
return 1
end
if QuestiePlayer.currentQuestlog[self.Id] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(self.Id) then
return 1
end
return 0
end
-- Re-reads everything the quest log owns: title, level, completion and the objectives. The
-- tracker keeps fallback quests between draws, so without this they stay frozen at whatever
-- the log said when the quest was first seen.
---@return boolean @false if the player no longer has the quest
function TrackerUtils:RefreshFallbackQuest(quest)
if not quest then return false end
local questLogIndex = GetQuestLogIndexForQuest(quest.Id)
if not questLogIndex then return false end
local title, level, _, _, _, isComplete = GetQuestLogTitle(questLogIndex)
if title and title ~= "" then
quest.name = title
end
if level and level > 0 then
quest.level = level
end
quest.logIsComplete = isComplete
quest.isComplete = (isComplete == 1)
ReadFallbackObjectives(questLogIndex, quest.Id, quest.Objectives)
return true
end
-- Returns nil if the quest is not currently in the quest log.
function TrackerUtils:BuildFallbackQuest(questId)
for i = 1, GetNumQuestLogEntries() do
local title, level, _, isHeader, _, isComplete, _, logQuestId = GetQuestLogTitle(i)
if not isHeader and logQuestId == questId then
-- Parse objectives from the leaderboard
local objectives = {}
local numObj = GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(i) or 0
for j = 1, numObj do
local text, _, finished = GetQuestLogLeaderBoard(j, i)
if text then
-- Parse "Description: X/Y" or just "Description"
local collected, needed = string.match(text, ":.-(%d+)/(%d+)%s*$")
collected = tonumber(collected) or (finished and 1 or 0)
needed = tonumber(needed) or 1
objectives[j] = {
text = text,
Needed = needed,
Collected = collected,
Finished = finished or (collected >= needed),
Type = "fallback",
}
end
end
local questLogIndex = GetQuestLogIndexForQuest(questId)
if not questLogIndex then return nil end
-- Walk backwards from i in the quest log to find the zone header.
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
local zoneText = nil
for h = i, 1, -1 do
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
if hIsHeader and hTitle and hTitle ~= "" then
zoneText = hTitle
break
end
end
local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0
local zoneNameOverride = nil
if zoneText and (not zoneId or zoneId == 0) then
zoneNameOverride = zoneText
end
local quest = {
Id = questId,
name = title or ("Quest " .. questId),
level = level or 0,
zoneOrSort = zoneId,
zoneName = zoneText,
zoneNameOverride = zoneNameOverride,
Objectives = objectives,
SpecialObjectives = {},
isFallback = true,
}
-- IsComplete must be a method (called as quest:IsComplete())
quest.IsComplete = function(self)
return (isComplete == 1 or (QuestiePlayer.currentQuestlog[questId] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(questId))) and 1 or 0
end
return quest
-- Walk backwards from the quest in the log to find the zone header.
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
local zoneText = nil
for h = questLogIndex, 1, -1 do
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
if hIsHeader and hTitle and hTitle ~= "" then
zoneText = hTitle
break
end
end
return nil
local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0
local zoneNameOverride = nil
if zoneText and (not zoneId or zoneId == 0) then
zoneNameOverride = zoneText
end
local quest = {
Id = questId,
name = "Quest " .. questId,
level = 0,
zoneOrSort = zoneId,
zoneName = zoneText,
zoneNameOverride = zoneNameOverride,
Objectives = {},
SpecialObjectives = {},
isFallback = true,
IsComplete = FallbackQuestIsComplete,
}
-- Same read the tracker does on every later draw, so a quest built here and one refreshed
-- from the cache carry exactly the same fields.
TrackerUtils:RefreshFallbackQuest(quest)
return quest
end
function TrackerUtils:GetSortedQuestIds()
local sortedQuestIds = {}
local questDetails = {}
local sortObj = Questie.db.profile.trackerSortObjectives
-- One walk of the quest log for the whole draw, so the per-quest lookups below are reads.
BuildQuestLogHeaders()
-- currentQuestlog is only as good as the removal events that maintain it, and a quest the
-- server finishes on its own -- Ascension's auto-complete quests -- can leave the log without
-- any of them landing, which strands the quest in the tracker for the rest of the session. The
-- log is the authority on what the player still has, so anything missing from it is skipped.
-- Skipped rather than pruned: a redraw that catches the log mid-refresh would otherwise throw
-- away state Questie is about to want back.
local questLogIsReadable = next(questLogQuestIds) ~= nil
-- Update quest objectives
for questId, quest in pairs(QuestiePlayer.currentQuestlog) do
@@ -878,6 +1049,11 @@ function TrackerUtils:GetSortedQuestIds()
if fallback then
TrackerUtils._fallbackQuests[qid] = fallback
end
else
-- Cached by an earlier draw, so its objectives and completion state are as
-- old as the cache. Nothing else updates them -- these quests have no DB
-- entry, so QuestieQuest's populate path skips them entirely.
TrackerUtils:RefreshFallbackQuest(fallback)
end
if fallback then
quest = fallback
@@ -885,7 +1061,14 @@ function TrackerUtils:GetSortedQuestIds()
end
end
if type(quest) == "table" and quest.IsComplete and quest.Objectives then
local isInQuestLog = (not questLogIsReadable) or (questLogQuestIds[qid] == true)
if not isInQuestLog then
-- Left over from a removal nothing told the tracker about, so the object built for it
-- goes too -- otherwise it would still be here to serve the next draw.
TrackerUtils._fallbackQuests[qid] = nil
end
if isInQuestLog and type(quest) == "table" and quest.IsComplete and quest.Objectives then
-- Insert Quest Ids into sortedQuestIds table
tinsert(sortedQuestIds, qid)
+4 -1
View File
@@ -11,10 +11,13 @@ Made for and tested on CoA, but it should also work on other 3.3.5 clients.
- fixed custom quests having no name in the tracker.
- fixed custom quests not showing (complete) in the tracker.
- fixed custom quest items not showing.
- fixed quest tags (group, elite, dungeon...) being wrong on quest ids Ascension reuses for its own content.
- fixed Ascension "main quests" not having the correct category and not clearing on complete.
## Changes
- added "By Zone + %% Completed" and "By Zone + %% Completed (Reversed)" tracker sorting options.
- implemented Ascension's backported supertracker buttons to the tracker.
- added "By Zone + % Completed" and "By Zone + % Completed (Reversed)" tracker sorting options.
## Credits