Compare commits

...

10 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
Narcasung 7ecc9016dd feat(tracker): match the client's pin interactions on the marker button
Hover used a Blizzard minimize-button highlight, which read as a red
tint over the pin art and looked nothing like the map. The pins draw
their hover and pressed states from the same UI-QuestPoi-NumberIcons
atlas they draw everything else from, additively blended, so mirror
those cells alongside the ones already being copied.

Add the pressed offset the pins have, nudging the digit or the "?" a
pixel down and right while held, reset on redraw since the pool recycles
buttons and a line can be rebuilt with the mouse still down. Play the
sound the pins play as well: we call WorldMapFrame_SelectQuestFrame
directly, and the sound lives a level above it in WorldMapQuestPOI_OnClick.
2026-07-25 16:31:20 +02:00
Narcasung 0103932b9b fix(tracker): make the objective marker button usable at larger sizes
The button shared the quest gutter with the collapse button, the quest
item buttons and the zone header text, so anything past the default size
was clipped by the tracker edge or drawn on top of its neighbours.

Give it a gutter of its own instead. GetSuperTrackMarginReserve feeds
questMarginLeft, which every layout and width calculation already builds
on, and the quest item buttons and zone labels are shifted by the same
amount. The button then anchors flush left of the line and the tracker
widens to match, so nothing overlaps at any size. Raise the size cap to
70 now that it fits, and drop the hover tooltip.

Also stop relying on the SetSuperTrackedQuestID hook as the only source
of truth. It never fires while the player is a ghost -- the corpse arrow
takes over the marker -- and it has not fired yet on a login or reload,
which left the tracker with no idea what was tracked in both cases. The
client is now asked directly: the watch frame POI buttons flag their own
selection, the map pins say it through their art (the selected variant
sits half a texture above the normal one), and the quest log selection
answers for the login window when no pin is styled yet. Selection calls
are hooked for the same reason, so clicks repaint while dead, and the
requests are coalesced because callers select a quest log entry and
restore the previous one a line later.
2026-07-25 16:19:56 +02:00
Narcasung 6aafd2c112 feat(tracker): add objective marker button to quest lines
Ascension's client backports retail's floating objective marker. This adds
a button to each tracker quest line that points the marker at that quest,
mirroring the quest pin the world map draws for it.

Supertracking is a slave of the map's quest selection: both the map and the
Blizzard watch frame funnel through SelectQuestLogEntry, and calling
C_SuperTrack.SetSuperTrackedQuestID directly only moves the marker until the
next map interaction stomps it. So the button clicks the same POI frame the
client clicks -- the watch frame button when one exists, otherwise the map's
quest frame.

The client dropped GetSuperTrackedQuestID, so the current quest is read by
hooking SetSuperTrackedQuestID instead. Every path ends up there, including
the automatic re-pick on zone change, so the highlight cannot fall out of
sync with tracking changed outside the addon. Caching what we last set would
have gone stale the moment the player used the map.

The button mirrors the pin's own textures rather than picking atlas cells, so
the digit, the "?" completed quests use and the selected variant all follow
whatever the client draws. Quests with no pin get no button, and the map's
POI frames are built on demand so buttons appear without opening the map.

Adds trackerShowSuperTrackButton and trackerSuperTrackButtonSize.
2026-07-25 01:50:03 +02:00
9 changed files with 1025 additions and 89 deletions
+43 -5
View File
@@ -1785,16 +1785,36 @@ function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
end end
return nil return nil
end 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) local logEntry = QuestLogCache.GetQuest(questId)
if not logEntry then return nil end 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 = { local fallback = {
Id = questId, Id = questId,
name = logEntry.title or tostring(questId), name = overrideName or cachedTitle or liveTitle or tostring(questId),
level = logEntry.level or 0, level = overrideLevel or logEntry.level or 0,
questLevel = logEntry.level or 0, questLevel = overrideLevel or logEntry.level or 0,
requiredLevel = 0, requiredLevel = 0,
zoneOrSort = 0, zoneOrSort = overrideZone or 0,
questFlags = 0, questFlags = 0,
specialFlags = 0, specialFlags = 0,
Starts = { CreatureStarts = {}, ObjectStarts = {}, ItemStarts = {} }, 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 if learnerRecord.objIndex and not QO.objIndex then QO.objIndex = learnerRecord.objIndex end
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) local questLevel, requiredLevel = QuestieLib.GetTbcLevel(questId)
QO.level = questLevel QO.level = questLevel
QO.requiredLevel = requiredLevel QO.requiredLevel = requiredLevel
@@ -102,6 +102,20 @@ local trackerOptionsLocales = {
["frFR"] = "Affiche le niveau des quêtes avec le titre des quêtes.", ["frFR"] = "Affiche le niveau des quêtes avec le titre des quêtes.",
}, },
--------------------------------------------------------- ---------------------------------------------------------
["Show Objective Marker Button"] = {
["enUS"] = true,
},
["When this is checked, quests that can be reached by the floating objective marker get a button in the Questie Tracker that points the marker at them."] = {
["enUS"] = true,
},
---------------------------------------------------------
["Objective Marker Button Size"] = {
["enUS"] = true,
},
["The size of the objective marker button shown next to each quest in the Questie Tracker."] = {
["enUS"] = true,
},
---------------------------------------------------------
["Auto Minimize Completed Quests"] = { ["Auto Minimize Completed Quests"] = {
["ptBR"] = "Minimizar missões concluídas", ["ptBR"] = "Minimizar missões concluídas",
["ruRU"] = "Свернуть выполненные", ["ruRU"] = "Свернуть выполненные",
@@ -113,6 +113,8 @@ function QuestieOptionsDefaults:Load()
autoTrackQuests = true, autoTrackQuests = true,
trackerShowCompleteQuests = true, trackerShowCompleteQuests = true,
trackerShowQuestLevel = true, trackerShowQuestLevel = true,
trackerShowSuperTrackButton = true,
trackerSuperTrackButtonSize = 25,
collapseCompletedQuests = false, collapseCompletedQuests = false,
hideCompletedQuestObjectives = false, hideCompletedQuestObjectives = false,
hideBlizzardCompletionText = false, hideBlizzardCompletionText = false,
@@ -13,6 +13,8 @@ local TrackerBaseFrame = QuestieLoader:ImportModule("TrackerBaseFrame")
local TrackerLinePool = QuestieLoader:ImportModule("TrackerLinePool") local TrackerLinePool = QuestieLoader:ImportModule("TrackerLinePool")
---@type TrackerQuestTimers ---@type TrackerQuestTimers
local TrackerQuestTimers = QuestieLoader:ImportModule("TrackerQuestTimers") local TrackerQuestTimers = QuestieLoader:ImportModule("TrackerQuestTimers")
---@type TrackerUtils
local TrackerUtils = QuestieLoader:ImportModule("TrackerUtils")
---@type QuestieArrow ---@type QuestieArrow
local QuestieArrow = QuestieLoader:ImportModule("QuestieArrow") local QuestieArrow = QuestieLoader:ImportModule("QuestieArrow")
@@ -175,6 +177,37 @@ function QuestieOptions.tabs.tracker:Initialize()
QuestieTracker:Update() QuestieTracker:Update()
end end
}, },
showSuperTrackButton = {
type = "toggle",
order = 5,
width = 1.5,
name = function() return l10n('Show Objective Marker Button') end,
desc = function() return l10n('When this is checked, quests that can be reached by the floating objective marker get a button in the Questie Tracker that points the marker at them.') end,
hidden = function() return not TrackerUtils:IsSuperTrackAvailable() end,
disabled = function() return not Questie.db.profile.trackerEnabled end,
get = function() return Questie.db.profile.trackerShowSuperTrackButton end,
set = function(_, value)
Questie.db.profile.trackerShowSuperTrackButton = value
QuestieTracker:Update()
end
},
superTrackButtonSize = {
type = "range",
order = 6,
width = 1.5,
name = function() return l10n('Objective Marker Button Size') end,
desc = function() return l10n('The size of the objective marker button shown next to each quest in the Questie Tracker.') end,
hidden = function() return not TrackerUtils:IsSuperTrackAvailable() end,
disabled = function() return (not Questie.db.profile.trackerEnabled) or (not Questie.db.profile.trackerShowSuperTrackButton) end,
min = 8,
max = 70,
step = 1,
get = function() return Questie.db.profile.trackerSuperTrackButtonSize end,
set = function(_, value)
Questie.db.profile.trackerSuperTrackButtonSize = value
QuestieTracker:Update()
end
},
showQuestTimer = { showQuestTimer = {
type = "toggle", type = "toggle",
order = 3, order = 3,
+13 -2
View File
@@ -553,6 +553,10 @@ function _QuestEventHandler:QuestLogUpdate()
doFullQuestLogScan = false doFullQuestLogScan = false
-- Function call updates doFullQuestLogScan. Order matters. -- Function call updates doFullQuestLogScan. Order matters.
_QuestEventHandler:UpdateAllQuests() _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 else
_QuestEventHandler:CleanupRemovedQuestsFallback() _QuestEventHandler:CleanupRemovedQuestsFallback()
QuestieCombatQueue:Queue(function() QuestieCombatQueue:Queue(function()
@@ -666,7 +670,9 @@ function _QuestEventHandler:CleanupRemovedQuestsFallback()
if QuestiePlayer and QuestiePlayer.currentQuestlog then if QuestiePlayer and QuestiePlayer.currentQuestlog then
local removedQuestIds = {} local removedQuestIds = {}
for questId in pairs(QuestiePlayer.currentQuestlog) do 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 removedQuestIds[#removedQuestIds + 1] = questId
end end
end end
@@ -679,7 +685,12 @@ function _QuestEventHandler:CleanupRemovedQuestsFallback()
local wasTurnedIn = questLog[questId] and questLog[questId].state == QUEST_LOG_STATES.QUEST_TURNED_IN 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 wasAlreadyComplete = Questie.db.char.complete and Questie.db.char.complete[questId]
local completeAtRemoval = QuestieDB.IsComplete(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) QuestLogCache.RemoveQuest(questId)
QuestieQuest:SetObjectivesDirty(questId) QuestieQuest:SetObjectivesDirty(questId)
+49 -6
View File
@@ -63,6 +63,22 @@ local function GetWrappedWidth(label)
return label:GetWidth() return label:GetWidth()
end 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 LSM30 = LibStub and LibStub("LibSharedMedia-3.0", true)
-- Local Vars -- Local Vars
@@ -335,6 +351,10 @@ function QuestieTracker.Initialize()
Questie.db.profile.trackerSetpoint = "TOPLEFT" Questie.db.profile.trackerSetpoint = "TOPLEFT"
end end
-- Tracks what the client considers supertracked. Permanent by design: hooksecurefunc cannot be
-- undone, and the value has to stay correct even while the Questie tracker is disabled.
TrackerUtils:InitSuperTrackHook()
if (not Questie.db.profile.trackerEnabled) then if (not Questie.db.profile.trackerEnabled) then
-- The Tracker is disabled, no need to continue -- The Tracker is disabled, no need to continue
return return
@@ -957,6 +977,10 @@ function QuestieTracker:Update()
-- Safety check - make sure we didn't run over our linePool limit. -- Safety check - make sure we didn't run over our linePool limit.
if not line then return "BREAK" end 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 -- Set Line Mode, Types, Clickers
line:SetMode("quest") line:SetMode("quest")
line:SetOnClick("quest") line:SetOnClick("quest")
@@ -966,7 +990,7 @@ function QuestieTracker:Update()
line.criteriaMark:Hide() line.criteriaMark:Hide()
-- Set Min/Max Button and default states -- 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 line.expandQuest.zoneId = zoneName
@@ -1023,7 +1047,7 @@ function QuestieTracker:Update()
if quest.isFallback or quest._isLogFallback then if quest.isFallback or quest._isLogFallback then
-- Quest not in DB: use the name stored on the fallback object -- 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 if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
questName = "[" .. quest.level .. "] " .. questName questName = "[" .. quest.level .. "] " .. questName
end end
@@ -1057,7 +1081,7 @@ function QuestieTracker:Update()
-- which queries the static DB only: for a quest with no DB entry it -- 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 -- silently defaults to level 1, and Ascension's scaling then scales
-- that wrong base instead of the quest's real level. -- 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 if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
fallbackName = QuestieLib:GetQuestString(quest.Id, fallbackName, quest.level, false) fallbackName = QuestieLib:GetQuestString(quest.Id, fallbackName, quest.level, false)
end end
@@ -1093,6 +1117,10 @@ function QuestieTracker:Update()
-- Adds the AI_VoiceOver Play Buttons -- Adds the AI_VoiceOver Play Buttons
line.playButton:SetPlayButton(questId) line.playButton:SetPlayButton(questId)
-- Adds the button that points the floating objective marker at this quest.
-- Must run after SetPlayButton, since it anchors around the play button.
line.superTrackButton:SetSuperTrackButton(questId)
local usableQIB = false local usableQIB = false
local sourceItemId = QuestieDB.QueryQuestSingle(quest.Id, "sourceItemId") local sourceItemId = QuestieDB.QueryQuestSingle(quest.Id, "sourceItemId")
local isLiveSourceItem = false local isLiveSourceItem = false
@@ -1167,7 +1195,8 @@ function QuestieTracker:Update()
end end
-- Attach button to Quest Title linePool -- Attach button to Quest Title linePool
button:SetPoint("TOPLEFT", button.line, "TOPLEFT", 0, 0) button:SetPoint("TOPLEFT", button.line, "TOPLEFT",
TrackerLinePool.GetItemButtonOffset(), 0)
button:SetParent(button.line) button:SetParent(button.line)
button:Show() button:Show()
@@ -1181,6 +1210,10 @@ function QuestieTracker:Update()
button:SetParent(UIParent) button:SetParent(UIParent)
button:Hide() button:Hide()
end 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 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. -- 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. -- See previous comment for details on why we're setting this button to UIParent.
@@ -1275,7 +1308,7 @@ function QuestieTracker:Update()
-- Attach button to Quest Title linePool -- Attach button to Quest Title linePool
altButton:SetPoint("TOPLEFT", altButton.line, "TOPLEFT", altButton:SetPoint("TOPLEFT", altButton.line, "TOPLEFT",
2 + questItemButtonSize, 0) TrackerLinePool.GetItemButtonOffset() + 2 + questItemButtonSize, 0)
altButton:SetParent(altButton.line) altButton:SetParent(altButton.line)
altButton:Show() altButton:Show()
@@ -1576,6 +1609,16 @@ function QuestieTracker:Update()
-- Adds 2 pixels and "Padding Between Quests" setting in Tracker Options -- Adds 2 pixels and "Padding Between Quests" setting in Tracker Options
line:SetHeight(line.label:GetHeight() + (Questie.db.profile.trackerQuestPadding + 2)) 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 end
primaryButton = false primaryButton = false
@@ -1713,7 +1756,7 @@ function QuestieTracker:Update()
-- Set Min/Max Button and default states -- Set Min/Max Button and default states
line.expandQuest:Show() 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 line.expandQuest.zoneId = zoneName
-- The minAllQuestsInZone table is always blank until a player Shift+Clicks the Zone header (MouseDown). -- The minAllQuestsInZone table is always blank until a player Shift+Clicks the Zone header (MouseDown).
+342
View File
@@ -35,6 +35,67 @@ local l10n = QuestieLoader:ImportModule("l10n")
local C_Timer = QuestieCompat.C_Timer local C_Timer = QuestieCompat.C_Timer
local C_QuestLog = QuestieCompat.C_QuestLog local C_QuestLog = QuestieCompat.C_QuestLog
local GetQuestLogIndexByID = QuestieCompat.GetQuestLogIndexByID local GetQuestLogIndexByID = QuestieCompat.GetQuestLogIndexByID
-- Copies one texture region of a quest pin onto our button. The texture file travels with the
-- coordinates because the client swaps files between states, and the region is only shown when the
-- pin itself shows it -- the pin uses "number" for in-progress quests and "turnin" for completed
-- ones, never both.
local function MirrorPinRegion(destination, source, size, pinSize)
if (not source) or (not source:IsShown()) or (not source:GetTexture()) then
destination:Hide()
return
end
-- Regions are not all the size of the pin -- the "?" is drawn larger than a digit -- so scale
-- them by how the pin itself was scaled instead of stretching each one to the button.
local scale = (pinSize and pinSize > 0) and (size / pinSize) or 1
local width = (source:GetWidth() or 0) * scale
local height = (source:GetHeight() or 0) * scale
if width <= 0 or height <= 0 then
width = size
height = size
end
destination:SetWidth(width)
destination:SetHeight(height)
destination:SetTexture(source:GetTexture())
destination:SetTexCoord(source:GetTexCoord())
destination:Show()
end
-- The pin atlases stack the selected (yellow circle, black digits) variant of every cell exactly
-- half a texture above the normal one. The client only restyles its pins during the world map's own
-- selection pass, which has not run yet right after a login or a reload, so the variant is forced
-- here rather than taken on trust from the pin.
local function ApplySelectedVariant(texture, selected)
local topLeftX, topLeftY, bottomLeftX, bottomLeftY, topRightX, topRightY, bottomRightX, bottomRightY = texture:GetTexCoord()
if (not topLeftY) or (not bottomLeftY) or (bottomLeftY - topLeftY) > 0.5 then
return
end
local offset
if selected and topLeftY >= 0.5 then
offset = -0.5
elseif (not selected) and bottomLeftY <= 0.5 then
offset = 0.5
else
return
end
texture:SetTexCoord(topLeftX, topLeftY + offset, bottomLeftX, bottomLeftY + offset,
topRightX, topRightY + offset, bottomRightX, bottomRightY + offset)
end
-- Whole-button states (hover, pressed) rather than the regions drawn inside them, so these fill the
-- button on their own and only need the atlas cell copied across.
local function MirrorPinButtonTexture(destination, source)
if (not destination) or (not source) or (not source.GetTexture) or (not source:GetTexture()) then
return
end
destination:SetTexture(source:GetTexture())
destination:SetTexCoord(source:GetTexCoord())
end
local function GetNumLines(label) local function GetNumLines(label)
if label.GetNumLines then if label.GetNumLines then
return label:GetNumLines() return label:GetNumLines()
@@ -54,6 +115,22 @@ local linePool = {}
local buttonPool = {} local buttonPool = {}
local lineMarginLeft = 10 local lineMarginLeft = 10
-- Gap kept between the supertrack button and whatever it is tucked in next to.
local superTrackButtonGap = 1
-- 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 ---@param questFrame Frame
function TrackerLinePool.Initialize(questFrame) function TrackerLinePool.Initialize(questFrame)
local trackerQuestFrame = questFrame local trackerQuestFrame = questFrame
@@ -404,6 +481,197 @@ function TrackerLinePool.Initialize(questFrame)
line.playButton = playButton line.playButton = playButton
-- create supertrack buttons for the Ascension floating objective marker
local superTrackButton = CreateFrame("Button", "linePool.superTrackButton" .. i, line)
superTrackButton:SetWidth(25)
superTrackButton:SetHeight(25)
superTrackButton:SetHitRectInsets(1, 1, 1, 1)
-- Hover and pressed states come from the pin atlas too, additively blended, exactly as the
-- client's own pins do it. RefreshSuperTrackButton mirrors the pin's cells over these.
superTrackButton:SetHighlightTexture("Interface\\WorldMap\\UI-QuestPoi-NumberIcons", "ADD")
superTrackButton:GetHighlightTexture():SetTexCoord(0.625, 0.75, 0.375, 0.5)
superTrackButton:SetPushedTexture("Interface\\WorldMap\\UI-QuestPoi-NumberIcons")
-- Same texture and atlas sub-rect the client's own quest POI pins use, so the tracker button
-- reads as native rather than as an addon icon.
superTrackButton:SetNormalTexture("Interface\\WorldMap\\UI-QuestPoi-NumberIcons")
superTrackButton:GetNormalTexture():SetTexCoord(0.5, 0.625, 0.875, 1)
-- The client marks the selected pin with this glow rather than by swapping the icon, so the
-- tracker button highlights the same way the map pin does.
superTrackButton.glow = superTrackButton:CreateTexture(nil, "BACKGROUND")
superTrackButton.glow:SetTexture("Interface\\WorldMap\\UI-QuestPoi-IconGlow")
superTrackButton.glow:SetBlendMode("ADD")
superTrackButton.glow:SetPoint("CENTER", superTrackButton, "CENTER", 0, 0)
superTrackButton.glow:Hide()
-- The digit printed inside the pin is another cell of the same atlas, drawn over the icon.
superTrackButton.number = superTrackButton:CreateTexture(nil, "OVERLAY")
superTrackButton.number:SetPoint("CENTER", superTrackButton, "CENTER", 0, 0)
superTrackButton.number:Hide()
-- Completed quests draw a "?" from this separate region instead of a digit.
superTrackButton.turnin = superTrackButton:CreateTexture(nil, "OVERLAY")
superTrackButton.turnin:SetPoint("CENTER", superTrackButton, "CENTER", 0, 0)
superTrackButton.turnin:Hide()
-- The quest id is remembered even while the button is hidden. POI frames only exist once the
-- world map has built them, so a quest that looks unreachable while the tracker is drawing
-- can become reachable later -- without the id we would have nothing left to re-check.
-- The pin's pressed state nudges what is drawn inside it down and to the right; the pushed
-- texture covers the circle, this covers the digit and the "?".
superTrackButton.SetPressedOffset = function(self, pressed)
local offset = pressed and 1 or 0
self.number:ClearAllPoints()
self.number:SetPoint("CENTER", self, "CENTER", offset, -offset)
self.turnin:ClearAllPoints()
self.turnin:SetPoint("CENTER", self, "CENTER", offset, -offset)
end
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.
local pin = self.questId and TrackerUtils:GetSuperTrackPin(self.questId)
if (not pin) or (not Questie.db.profile.trackerShowSuperTrackButton) then
self:Hide()
return
end
local buttonSize = Questie.db.profile.trackerSuperTrackButtonSize or 25
self:SetWidth(buttonSize)
self:SetHeight(buttonSize)
self.glow:SetWidth(buttonSize * 1.4)
self.glow:SetHeight(buttonSize * 1.4)
-- Mirror the pin rather than picking atlas cells ourselves, so whatever the client
-- decides to draw -- digit, "?", selected variant -- shows up here unchanged. The
-- texture file has to be copied along with the coordinates: completed quests swap in a
-- different file for these slots, and coordinates from one file applied to another
-- sample nonsense.
local isSuperTracked = TrackerUtils:GetSuperTrackedQuestId() == self.questId
local pinTexture = pin.GetNormalTexture and pin:GetNormalTexture()
if pinTexture then
local ownTexture = self:GetNormalTexture()
ownTexture:SetTexture(pinTexture:GetTexture())
ownTexture:SetTexCoord(pinTexture:GetTexCoord())
ApplySelectedVariant(ownTexture, isSuperTracked)
end
MirrorPinButtonTexture(self:GetHighlightTexture(), pin.GetHighlightTexture and pin:GetHighlightTexture())
local ownPushed = self:GetPushedTexture()
MirrorPinButtonTexture(ownPushed, pin.GetPushedTexture and pin:GetPushedTexture())
if ownPushed then
ApplySelectedVariant(ownPushed, isSuperTracked)
end
local pinSize = pin:GetWidth()
MirrorPinRegion(self.number, pin.number, buttonSize, pinSize)
MirrorPinRegion(self.turnin, pin.turnin, buttonSize, pinSize)
if self.number:IsShown() then
ApplySelectedVariant(self.number, isSuperTracked)
end
-- Undo any leftover pressed offset: the pool recycles buttons, and a line can be redrawn
-- while the mouse is still held down.
self:SetPressedOffset(false)
self:AnchorSuperTrackButton()
-- The icon carries the selected variant itself; the glow is the one part the pin draws
-- as a separate texture.
if isSuperTracked then
self.glow:Show()
else
self.glow:Hide()
end
self:Show()
end
superTrackButton:EnableMouse(true)
superTrackButton:RegisterForClicks("LeftButtonUp")
superTrackButton:SetScript("OnMouseDown", function(self)
self:SetPressedOffset(true)
end)
superTrackButton:SetScript("OnMouseUp", function(self)
self:SetPressedOffset(false)
end)
superTrackButton:SetScript("OnClick", function(self)
if self.questId then
-- Same sound the client plays for its own quest pins.
PlaySound("igMainMenuOptionCheckBoxOn")
TrackerUtils:SetSuperTrackedQuest(self.questId)
end
end)
superTrackButton:Hide()
line.superTrackButton = superTrackButton
-- create expanding buttons for quests with objectives -- create expanding buttons for quests with objectives
local expandQuest = CreateFrame("Button", "linePool.expandQuest" .. i, line) local expandQuest = CreateFrame("Button", "linePool.expandQuest" .. i, line)
expandQuest.texture = expandQuest:CreateTexture(nil, "OVERLAY", nil, 0) expandQuest.texture = expandQuest:CreateTexture(nil, "OVERLAY", nil, 0)
@@ -481,6 +749,9 @@ function TrackerLinePool.Initialize(questFrame)
line.expandQuest = expandQuest 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 linePool[i] = line
nextFrame = line nextFrame = line
end end
@@ -731,11 +1002,29 @@ function TrackerLinePool.ResetLinesForChange()
line.playButton:SetAlpha(0) line.playButton:SetAlpha(0)
line.playButton:Hide() line.playButton:Hide()
end end
if line.superTrackButton then
line.superTrackButton.questId = nil
line.superTrackButton:Hide()
end
end end
lineIndex = 0 lineIndex = 0
end end
-- Re-evaluates the supertrack buttons without rebuilding the tracker. Driven by the
-- SetSuperTrackedQuestID hook, which also fires on map open/close -- that is what makes buttons
-- appear once the world map has built its POI frames, since the tracker itself does not redraw then.
function TrackerLinePool.UpdateSuperTrackButtons()
-- Rebuild the map's POI frames first so a zone change is picked up even with the map closed.
TrackerUtils:PrimeSuperTrackFrames()
for _, line in pairs(linePool) do
if line.superTrackButton and line.superTrackButton.questId then
line.superTrackButton:RefreshSuperTrackButton()
end
end
end
function TrackerLinePool.ResetButtonsForChange() function TrackerLinePool.ResetButtonsForChange()
if TrackerBaseFrame.isSizing == true or TrackerBaseFrame.isMoving == true then if TrackerBaseFrame.isSizing == true or TrackerBaseFrame.isMoving == true then
Questie:Debug(Questie.DEBUG_SPAM, "[TrackerLinePool:ResetButtonsForChange]") Questie:Debug(Questie.DEBUG_SPAM, "[TrackerLinePool:ResetButtonsForChange]")
@@ -822,6 +1111,49 @@ function TrackerLinePool.GetCurrentButton()
return buttonPool[buttonIndex] return buttonPool[buttonIndex]
end 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] ---@return table|nil lineIndex linePool[lineIndex - 1]
function TrackerLinePool.GetPreviousLine() function TrackerLinePool.GetPreviousLine()
lineIndex = lineIndex - 1 lineIndex = lineIndex - 1
@@ -871,6 +1203,10 @@ function TrackerLinePool.HideUnusedLines()
line.expandZone.zoneId = nil line.expandZone.zoneId = nil
line.criteriaMark.mode = nil line.criteriaMark.mode = nil
line.playButton.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 end
end end
@@ -1098,6 +1434,12 @@ end
TrackerLinePool.SetMode = function(self, mode) TrackerLinePool.SetMode = function(self, mode)
if mode ~= self.mode then if mode ~= self.mode then
self.mode = mode self.mode = mode
-- Lines are recycled between zone headers, quest titles and objectives. Only quest title
-- lines own a supertrack button, so drop it whenever a line takes on another role.
if mode ~= "quest" and self.superTrackButton then
self.superTrackButton.questId = nil
self.superTrackButton:Hide()
end
if mode == "zone" then if mode == "zone" then
local trackerFontSizeZone = Questie.db.profile.trackerFontSizeZone local trackerFontSizeZone = Questie.db.profile.trackerFontSizeZone
self.label:SetFont((LSM30 and LSM30.Fetch and LSM30:Fetch("font", Questie.db.profile.trackerFontZone)) or Questie.db.profile.trackerFontZone, trackerFontSizeZone, Questie.db.profile.trackerFontOutline) self.label:SetFont((LSM30 and LSM30.Fetch and LSM30:Fetch("font", Questie.db.profile.trackerFontZone)) or Questie.db.profile.trackerFontZone, trackerFontSizeZone, Questie.db.profile.trackerFontOutline)
+504 -54
View File
@@ -676,37 +676,162 @@ local function GetAreaIdByZoneName(zoneName)
return l10n:GetAreaIdByLocalName(zoneName) return l10n:GetAreaIdByLocalName(zoneName)
end end
-- Walk the quest log to find the zone header for a given questId. -- questId -> the header the client filed it under in the quest log. In 3.3.5 those headers are
-- In 3.3.5, zone names appear as isHeader=true entries above their quests. -- isHeader=true entries sitting above the quests they cover. Built in one pass and kept, because
-- Returns the header title string, or nil if not found. -- the tracker asks per quest and redraws often -- walking the log once per quest is quadratic.
local function GetQuestLogZoneName(questId) local questLogHeaders = {}
local targetIndex = nil
local total = GetNumQuestLogEntries and GetNumQuestLogEntries() or 0 -- Every questId the log currently holds, headers aside. The tracker draws from currentQuestlog,
for i = 1, total do -- 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) local _, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(i)
if not isHeader and logId == questId then if (not isHeader) and logId == questId then
targetIndex = i questLogIndexes[questId] = i
break return i
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
end end
end end
return nil return nil
end 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) local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
if zoneNameOverride and zoneNameOverride ~= "" then if zoneNameOverride and zoneNameOverride ~= "" then
return zoneNameOverride return zoneNameOverride
end 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 zoneName
local sortObj = Questie.db.profile.trackerSortObjectives local sortObj = Questie.db.profile.trackerSortObjectives
if sortObj == "byZone" or sortObj == "byZoneComplete" or sortObj == "byZoneCompleteReversed" or sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then 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 if (zoneOrSort) > 0 then
zoneName = TrackerUtils:GetZoneNameByID(zoneOrSort) zoneName = TrackerUtils:GetZoneNameByID(zoneOrSort)
if not zoneName or zoneName == "Unknown Zone" then if not zoneName or zoneName == "Unknown Zone" then
@@ -746,35 +871,72 @@ local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
return zoneName return zoneName
end end
-- Returns nil if the quest is not currently in the quest log. -- The client's own title for a quest, or nil if the player does not have it. Quest objects can
function TrackerUtils:BuildFallbackQuest(questId) -- reach the tracker without a usable name -- a questDataOverrides entry that carries no name
for i = 1, GetNumQuestLogEntries() do -- field, a QuestLogCache row read before the client had filled the title in -- and since those
local title, level, _, isHeader, _, isComplete, _, logQuestId = GetQuestLogTitle(i) -- objects are cached for the session, the name never repairs itself. The log always knows.
if not isHeader and logQuestId == questId then function TrackerUtils:GetQuestLogTitleById(questId)
-- Parse objectives from the leaderboard local questLogIndex = GetQuestLogIndexForQuest(questId)
local objectives = {} if not questLogIndex then return nil end
local numObj = GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(i) or 0
for j = 1, numObj do local title = GetQuestLogTitle(questLogIndex)
local text, _, finished = GetQuestLogLeaderBoard(j, i) if title and title ~= "" then
if text then return title
-- 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 end
-- Walk backwards from i in the quest log to find the zone header. 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)
local questLogIndex = GetQuestLogIndexForQuest(questId)
if not questLogIndex then return nil end
-- 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. -- This is the canonical 3.3.5 method: zone headers sit above their quests.
local zoneText = nil local zoneText = nil
for h = i, 1, -1 do for h = questLogIndex, 1, -1 do
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h) local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
if hIsHeader and hTitle and hTitle ~= "" then if hIsHeader and hTitle and hTitle ~= "" then
zoneText = hTitle zoneText = hTitle
@@ -789,30 +951,39 @@ function TrackerUtils:BuildFallbackQuest(questId)
local quest = { local quest = {
Id = questId, Id = questId,
name = title or ("Quest " .. questId), name = "Quest " .. questId,
level = level or 0, level = 0,
zoneOrSort = zoneId, zoneOrSort = zoneId,
zoneName = zoneText, zoneName = zoneText,
zoneNameOverride = zoneNameOverride, zoneNameOverride = zoneNameOverride,
Objectives = objectives, Objectives = {},
SpecialObjectives = {}, SpecialObjectives = {},
isFallback = true, isFallback = true,
IsComplete = FallbackQuestIsComplete,
} }
-- IsComplete must be a method (called as quest:IsComplete())
quest.IsComplete = function(self) -- Same read the tracker does on every later draw, so a quest built here and one refreshed
return (isComplete == 1 or (QuestiePlayer.currentQuestlog[questId] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(questId))) and 1 or 0 -- from the cache carry exactly the same fields.
end TrackerUtils:RefreshFallbackQuest(quest)
return quest return quest
end end
end
return nil
end
function TrackerUtils:GetSortedQuestIds() function TrackerUtils:GetSortedQuestIds()
local sortedQuestIds = {} local sortedQuestIds = {}
local questDetails = {} local questDetails = {}
local sortObj = Questie.db.profile.trackerSortObjectives 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 -- Update quest objectives
for questId, quest in pairs(QuestiePlayer.currentQuestlog) do for questId, quest in pairs(QuestiePlayer.currentQuestlog) do
@@ -878,6 +1049,11 @@ function TrackerUtils:GetSortedQuestIds()
if fallback then if fallback then
TrackerUtils._fallbackQuests[qid] = fallback TrackerUtils._fallbackQuests[qid] = fallback
end 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 end
if fallback then if fallback then
quest = fallback quest = fallback
@@ -885,7 +1061,14 @@ function TrackerUtils:GetSortedQuestIds()
end end
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 -- Insert Quest Ids into sortedQuestIds table
tinsert(sortedQuestIds, qid) tinsert(sortedQuestIds, qid)
@@ -1265,6 +1448,273 @@ function TrackerUtils:GetSortedQuestIds()
return sortedQuestIds, questDetails return sortedQuestIds, questDetails
end end
-- Ascension's 3.3.5 client backports the retail floating objective marker ("SuperTracker").
-- Supertracking is a slave of the quest log selection: the world map and the Blizzard watch frame
-- both funnel through SelectQuestLogEntry -> SuperTrackerUtil.SetToBestSuperTrackingType. Calling
-- C_SuperTrack.SetSuperTrackedQuestID directly only moves the marker until the next map interaction
-- stomps it, so we click the same POI frames the client itself clicks.
local superTrackedQuestId
local superTrackHooked
local superTrackRefreshing
local superTrackRefreshPending
local superTrackEventFrame
-- Refreshing rebuilds the map's POI frames, which can land back in the very hooks that asked for the
-- refresh, so this is the only way the buttons are ever repainted. Requests are also coalesced:
-- callers like TrackerQuestTimers select a quest log entry and immediately restore the previous one,
-- so reading the selection on the first of those calls would catch a state the client is about to
-- undo. Waiting a tick means the burst has settled.
local function RefreshSuperTrackButtons()
if superTrackRefreshing or superTrackRefreshPending then
return
end
superTrackRefreshPending = true
C_Timer.After(0.05, function()
superTrackRefreshPending = false
superTrackRefreshing = true
TrackerLinePool.UpdateSuperTrackButtons()
superTrackRefreshing = false
end)
end
function TrackerUtils:IsSuperTrackAvailable()
return (C_SuperTrack ~= nil) and ((WorldMapFrame_SelectQuestFrame ~= nil) or (WatchFrameQuestPOI_OnClick ~= nil))
end
-- Most paths that change the supertracked quest end up in SetSuperTrackedQuestID -- our own button,
-- world map pins, the map quest list, the Blizzard tracker, and the automatic re-pick that happens
-- when the map switches zone -- so hooking it stands in for the GetSuperTrackedQuestID getter this
-- client dropped. It goes quiet while the player is a ghost, though: the corpse arrow takes the
-- marker over, so no quest is ever handed to it even though the map keeps selecting one. The quest
-- selection itself is therefore hooked as well, and that is what keeps the buttons honest while
-- dead. Caching what we last set would go stale the moment the player changed it by other means.
function TrackerUtils:InitSuperTrackHook()
if superTrackHooked or (not C_SuperTrack) then
return
end
superTrackHooked = true
hooksecurefunc(C_SuperTrack, "SetSuperTrackedQuestID", function(questId)
superTrackedQuestId = questId
RefreshSuperTrackButtons()
end)
if C_SuperTrack.ClearSuperTracker then
hooksecurefunc(C_SuperTrack, "ClearSuperTracker", function()
superTrackedQuestId = nil
RefreshSuperTrackButtons()
end)
end
if WorldMapFrame_SelectQuestFrame then
hooksecurefunc("WorldMapFrame_SelectQuestFrame", RefreshSuperTrackButtons)
end
if WatchFrameQuestPOI_OnClick then
hooksecurefunc("WatchFrameQuestPOI_OnClick", RefreshSuperTrackButtons)
end
-- Everything that supertracks a quest goes through the quest log selection, map or no map, alive
-- or dead -- including opening a quest in the quest log window, which no other hook here sees.
if SelectQuestLogEntry then
hooksecurefunc("SelectQuestLogEntry", RefreshSuperTrackButtons)
end
-- Nothing at all fires on a login or a reload: the map has not been touched, so the hooks above
-- stay silent and the tracker draws before the client has styled its pins. These events are the
-- only prompt to go back and look.
superTrackEventFrame = CreateFrame("Frame")
superTrackEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
superTrackEventFrame:RegisterEvent("PLAYER_UNGHOST")
superTrackEventFrame:RegisterEvent("PLAYER_ALIVE")
superTrackEventFrame:RegisterEvent("PLAYER_DEAD")
superTrackEventFrame:SetScript("OnEvent", function()
RefreshSuperTrackButtons()
-- The client fills in its POI frames a moment after entering the world, so the immediate
-- pass above can still come up empty.
C_Timer.After(2, RefreshSuperTrackButtons)
end)
end
-- The hook only hears about the changes the client itself makes, and while the player is a ghost the
-- floating marker is disabled: selecting a quest then never reaches SetSuperTrackedQuestID, so the
-- hook reports nothing after a login or a reload in that state and goes stale after any click. The
-- client's own frames still know. Watch frame POI buttons say so outright, and the world map's quest
-- frames say it through their art: the atlases stack the selected (yellow) variant of a cell half a
-- texture above the normal one, so a pin drawn from the upper half is the supertracked one.
local function FindSelectedQuestId()
for i = 1, 30 do
local firstInRow = _G["poiWatchFrameLines" .. i .. "_1"]
if not firstInRow then
break
end
for j = 1, 5 do
local poiButton = (j == 1) and firstInRow or _G["poiWatchFrameLines" .. i .. "_" .. j]
if not poiButton then
break
end
if poiButton.isSelected and poiButton.questId then
return poiButton.questId
end
end
end
for i = 1, 25 do
local questFrame = _G["WorldMapQuestFrame" .. i]
if not questFrame then
break
end
local pin = questFrame.ownPOI or questFrame.poiIcon
local pinTexture = pin and pin.GetNormalTexture and pin:GetNormalTexture()
if pinTexture and questFrame.questId then
local _, topY = pinTexture:GetTexCoord()
if topY and topY < 0.5 then
return questFrame.questId
end
end
end
-- Right after a login or a reload no pin is styled at all: the client marks them the first time
-- the world map is opened. The quest log selection is what it reads when it gets there, so it
-- answers for the gap in between.
local selection = GetQuestLogSelection and GetQuestLogSelection()
if selection and selection > 0 then
-- GetQuestIDFromLogIndex, not the raw API: GetQuestLogTitle is the compat wrapper here,
-- which normalises the client's 9 return values down to 8.
local questId = QuestieCompat.GetQuestIDFromLogIndex(selection)
if questId and questId ~= 0 then
return questId
end
end
return nil
end
-- The client is asked before the hook: the hook cannot see a ghost's selection changes at all, so
-- its value is the fallback for when no POI frame exists to read (another zone, or frames not built
-- yet), not the source of truth.
function TrackerUtils:GetSuperTrackedQuestId()
return FindSelectedQuestId() or superTrackedQuestId
end
-- The world map builds its quest POI frames lazily, so right after login -- or after a zone change
-- with the map still closed -- there is nothing to match a quest against and every button would
-- hide itself. The client can build them without the map being shown, and doing so does not disturb
-- which quest is currently supertracked. Skipped while the map is open so we never fight the player.
function TrackerUtils:PrimeSuperTrackFrames()
if WorldMapFrame and WorldMapFrame:IsShown() then
return
end
if WorldMapFrame_UpdateQuests then
WorldMapFrame_UpdateQuests()
end
end
---@return table|nil frame, function|nil clickHandler
local function GetSuperTrackFrame(questId)
if (not questId) or questId == 0 then
return nil
end
-- Blizzard watch frame POI buttons carry the quest id directly. Questie empties the Blizzard
-- watch list (see QuestieTracker:AQW_Insert) so only a handful of quests ever have one. Both
-- scans below stop at the first gap: these frames are created in order, so a missing index
-- means there are no further ones and this runs once per quest line per redraw.
if WatchFrameQuestPOI_OnClick then
for i = 1, 30 do
local firstInRow = _G["poiWatchFrameLines" .. i .. "_1"]
if not firstInRow then
break
end
for j = 1, 5 do
local poiButton = (j == 1) and firstInRow or _G["poiWatchFrameLines" .. i .. "_" .. j]
if not poiButton then
break
end
if poiButton.questId == questId then
return poiButton, WatchFrameQuestPOI_OnClick
end
end
end
end
-- World map quest frames cover every quest with a POI on the currently viewed map, which is the
-- wider net of the two. Pass the WorldMapQuestFrame itself and never its poiIcon --
-- WorldMapFrame_SelectQuestFrame indexes questFrame.poiIcon and errors on the POI frame.
if WorldMapFrame_SelectQuestFrame then
if not _G["WorldMapQuestFrame1"] then
TrackerUtils:PrimeSuperTrackFrames()
end
for i = 1, 25 do
local questFrame = _G["WorldMapQuestFrame" .. i]
if not questFrame then
break
end
if questFrame.questId == questId then
return questFrame, WorldMapFrame_SelectQuestFrame
end
end
end
return nil
end
-- A quest can only be supertracked while it has a POI frame, so quests in another zone or without
-- map coordinates simply have no button.
function TrackerUtils:CanSuperTrackQuest(questId)
return GetSuperTrackFrame(questId) ~= nil
end
-- The map pin the client draws for this quest. Copying its texture coordinates is what keeps the
-- tracker button identical to the pin: the number, the "?" shown for completed quests and the
-- black-on-yellow selected variant all follow along without us mapping atlas cells by hand. The
-- number is also the pin's position among quests that actually have a POI, which is not the same as
-- the quest frame index -- another reason to read it from the client instead of deriving it.
---@return table|nil
function TrackerUtils:GetSuperTrackPin(questId)
if (not questId) or questId == 0 or (not WorldMapFrame_SelectQuestFrame) then
return nil
end
if not _G["WorldMapQuestFrame1"] then
TrackerUtils:PrimeSuperTrackFrames()
end
for i = 1, 25 do
local questFrame = _G["WorldMapQuestFrame" .. i]
if not questFrame then
break
end
if questFrame.questId == questId then
-- ownPOI is the pin drawn in the map's quest list, poiIcon the one on the map itself.
-- The list version is the one we mirror: it keeps the circular background on completed
-- quests, where the map version draws a bare "?".
return questFrame.ownPOI or questFrame.poiIcon
end
end
return nil
end
function TrackerUtils:SetSuperTrackedQuest(questId)
local frame, clickHandler = GetSuperTrackFrame(questId)
if not frame then
return false
end
clickHandler(frame)
-- Refresh here rather than leaning on the SetSuperTrackedQuestID hook: it stays silent while the
-- player is a ghost, which would leave the button we just clicked looking untouched.
RefreshSuperTrackButtons()
return true
end
function TrackerUtils:IsVoiceOverLoaded() function TrackerUtils:IsVoiceOverLoaded()
-- Require not just that the VoiceOver addons are loaded, but that the runtime -- Require not just that the VoiceOver addons are loaded, but that the runtime
-- structure we index actually exists. Some VoiceOver builds (e.g. on Elune) expose -- structure we index actually exists. Some VoiceOver builds (e.g. on Elune) expose
+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 having no name in the tracker.
- fixed custom quests not showing (complete) in the tracker. - fixed custom quests not showing (complete) in the tracker.
- fixed custom quest items not showing. - 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 ## 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 ## Credits