Compare commits
10 Commits
9da949dd66
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fe4b83a9 | |||
| 26a135a2ca | |||
| cf67ceb474 | |||
| de270a0e8e | |||
| c6019ea823 | |||
| 706fc0b8ad | |||
| c2b1b82b2f | |||
| 7ecc9016dd | |||
| 0103932b9b | |||
| 6aafd2c112 |
+43
-5
@@ -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
|
||||
|
||||
@@ -102,6 +102,20 @@ local trackerOptionsLocales = {
|
||||
["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"] = {
|
||||
["ptBR"] = "Minimizar missões concluídas",
|
||||
["ruRU"] = "Свернуть выполненные",
|
||||
|
||||
@@ -113,6 +113,8 @@ function QuestieOptionsDefaults:Load()
|
||||
autoTrackQuests = true,
|
||||
trackerShowCompleteQuests = true,
|
||||
trackerShowQuestLevel = true,
|
||||
trackerShowSuperTrackButton = true,
|
||||
trackerSuperTrackButtonSize = 25,
|
||||
collapseCompletedQuests = false,
|
||||
hideCompletedQuestObjectives = false,
|
||||
hideBlizzardCompletionText = false,
|
||||
|
||||
@@ -13,6 +13,8 @@ local TrackerBaseFrame = QuestieLoader:ImportModule("TrackerBaseFrame")
|
||||
local TrackerLinePool = QuestieLoader:ImportModule("TrackerLinePool")
|
||||
---@type TrackerQuestTimers
|
||||
local TrackerQuestTimers = QuestieLoader:ImportModule("TrackerQuestTimers")
|
||||
---@type TrackerUtils
|
||||
local TrackerUtils = QuestieLoader:ImportModule("TrackerUtils")
|
||||
---@type QuestieArrow
|
||||
local QuestieArrow = QuestieLoader:ImportModule("QuestieArrow")
|
||||
|
||||
@@ -175,6 +177,37 @@ function QuestieOptions.tabs.tracker:Initialize()
|
||||
QuestieTracker:Update()
|
||||
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 = {
|
||||
type = "toggle",
|
||||
order = 3,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -335,6 +351,10 @@ function QuestieTracker.Initialize()
|
||||
Questie.db.profile.trackerSetpoint = "TOPLEFT"
|
||||
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
|
||||
-- The Tracker is disabled, no need to continue
|
||||
return
|
||||
@@ -957,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")
|
||||
@@ -966,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
|
||||
|
||||
|
||||
@@ -1023,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
|
||||
@@ -1057,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
|
||||
@@ -1093,6 +1117,10 @@ function QuestieTracker:Update()
|
||||
-- Adds the AI_VoiceOver Play Buttons
|
||||
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 sourceItemId = QuestieDB.QueryQuestSingle(quest.Id, "sourceItemId")
|
||||
local isLiveSourceItem = false
|
||||
@@ -1167,7 +1195,8 @@ function QuestieTracker:Update()
|
||||
end
|
||||
|
||||
-- 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:Show()
|
||||
|
||||
@@ -1181,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.
|
||||
@@ -1275,7 +1308,7 @@ function QuestieTracker:Update()
|
||||
|
||||
-- Attach button to Quest Title linePool
|
||||
altButton:SetPoint("TOPLEFT", altButton.line, "TOPLEFT",
|
||||
2 + questItemButtonSize, 0)
|
||||
TrackerLinePool.GetItemButtonOffset() + 2 + questItemButtonSize, 0)
|
||||
altButton:SetParent(altButton.line)
|
||||
altButton:Show()
|
||||
|
||||
@@ -1576,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
|
||||
@@ -1713,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).
|
||||
|
||||
@@ -35,6 +35,67 @@ local l10n = QuestieLoader:ImportModule("l10n")
|
||||
local C_Timer = QuestieCompat.C_Timer
|
||||
local C_QuestLog = QuestieCompat.C_QuestLog
|
||||
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)
|
||||
if label.GetNumLines then
|
||||
return label:GetNumLines()
|
||||
@@ -54,6 +115,22 @@ local linePool = {}
|
||||
local buttonPool = {}
|
||||
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
|
||||
function TrackerLinePool.Initialize(questFrame)
|
||||
local trackerQuestFrame = questFrame
|
||||
@@ -404,6 +481,197 @@ function TrackerLinePool.Initialize(questFrame)
|
||||
|
||||
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
|
||||
local expandQuest = CreateFrame("Button", "linePool.expandQuest" .. i, line)
|
||||
expandQuest.texture = expandQuest:CreateTexture(nil, "OVERLAY", nil, 0)
|
||||
@@ -481,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
|
||||
@@ -731,11 +1002,29 @@ function TrackerLinePool.ResetLinesForChange()
|
||||
line.playButton:SetAlpha(0)
|
||||
line.playButton:Hide()
|
||||
end
|
||||
if line.superTrackButton then
|
||||
line.superTrackButton.questId = nil
|
||||
line.superTrackButton:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
lineIndex = 0
|
||||
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()
|
||||
if TrackerBaseFrame.isSizing == true or TrackerBaseFrame.isMoving == true then
|
||||
Questie:Debug(Questie.DEBUG_SPAM, "[TrackerLinePool:ResetButtonsForChange]")
|
||||
@@ -822,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
|
||||
@@ -871,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
|
||||
@@ -1098,6 +1434,12 @@ end
|
||||
TrackerLinePool.SetMode = function(self, mode)
|
||||
if mode ~= self.mode then
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1265,6 +1448,273 @@ function TrackerUtils:GetSortedQuestIds()
|
||||
return sortedQuestIds, questDetails
|
||||
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()
|
||||
-- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user