Files
Questie-X/Modules/Tracker/QuestieTracker.lua
T
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

2751 lines
136 KiB
Lua

---@class QuestieTracker
local QuestieTracker = QuestieLoader:CreateModule("QuestieTracker")
-------------------------
--Import QuestieTracker modules.
-------------------------
---@type TrackerBaseFrame
local TrackerBaseFrame = QuestieLoader:ImportModule("TrackerBaseFrame")
---@type TrackerHeaderFrame
local TrackerHeaderFrame = QuestieLoader:ImportModule("TrackerHeaderFrame")
---@type TrackerQuestFrame
local TrackerQuestFrame = QuestieLoader:ImportModule("TrackerQuestFrame")
---@type TrackerLinePool
local TrackerLinePool = QuestieLoader:ImportModule("TrackerLinePool")
---@type TrackerFadeTicker
local TrackerFadeTicker = QuestieLoader:ImportModule("TrackerFadeTicker")
---@type TrackerQuestTimers
local TrackerQuestTimers = QuestieLoader:ImportModule("TrackerQuestTimers")
---@type TrackerUtils
local TrackerUtils = QuestieLoader:ImportModule("TrackerUtils")
-------------------------
--Import Questie modules.
-------------------------
---@type QuestieQuest
local QuestieQuest = QuestieLoader:ImportModule("QuestieQuest")
---@type QuestieMap
local QuestieMap = QuestieLoader:ImportModule("QuestieMap")
---@type QuestieTooltips
local QuestieTooltips = QuestieLoader:ImportModule("QuestieTooltips")
---@type QuestieLib
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
---@type QuestiePlayer
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
---@type QuestieCombatQueue
local QuestieCombatQueue = QuestieLoader:ImportModule("QuestieCombatQueue")
---@type QuestEventHandler
local QuestEventHandler = QuestieLoader:ImportModule("QuestEventHandler")
local _QuestEventHandler = QuestEventHandler.private
---@type QuestieDB
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
---@type l10n
local l10n = QuestieLoader:ImportModule("l10n")
---@type QuestieDebugOffer
local QuestieDebugOffer = QuestieLoader:ImportModule("QuestieDebugOffer")
--- COMPATIBILITY ---
local C_Timer = QuestieCompat.C_Timer
local GetQuestLogTitle = QuestieCompat.GetQuestLogTitle
local GetQuestLogIndexByID = QuestieCompat.GetQuestLogIndexByID
local GetQuestLogSpecialItemInfo = QuestieCompat.GetQuestLogSpecialItemInfo
local GetItemInfo = QuestieCompat.GetItemInfo
local function GetNumLines(label)
if label.GetNumLines then
return label:GetNumLines()
end
return QuestieCompat.GetNumLines(label)
end
local function GetWrappedWidth(label)
if label.GetWrappedWidth then
return label:GetWrappedWidth()
end
return label:GetWidth()
end
local LSM30 = LibStub and LibStub("LibSharedMedia-3.0", true)
-- Local Vars
local trackerLineWidth = 0
local trackerMinLineWidth = 260
local trackerMarginRight = 30
local trackerMarginLeft = 14
local lastAQW = GetTime()
local lastTrackerUpdate = GetTime()
local lastAchieveId = GetTime()
local durabilityInitialPosition = { DurabilityFrame:GetPoint() }
local voiceOverInitialPosition
if VoiceOverFrame then
voiceOverInitialPosition = { VoiceOverFrame:GetPoint() }
end
local questsWatched = GetNumQuestWatches()
local trackedAchievements
local trackedAchievementIds
if Questie.IsWotlk or QuestieCompat.Is335 then
trackedAchievements = { GetTrackedAchievements() }
trackedAchievementIds = {}
end
local isFirstRun = true
local allowFormattingUpdate = false
local trackerBaseFrame, trackerHeaderFrame, trackerQuestFrame
local QuestLogFrame = QuestLogExFrame or ClassicQuestLog or QuestLogFrame
-- ============================================================
-- Ghost quest cleanup (quests that auto-complete / disappear)
-- Fixes:
-- • Tracker not updating when the quest vanishes from the quest log
-- • Warning spam: "QuestId doesn't exist in Game's quest log: <id>"
-- ============================================================
local function _RemoveQuestIdFromCharTables(questId)
if not questId then return end
local c = Questie and Questie.db and Questie.db.char
if not c then return end
if c.TrackedQuests then c.TrackedQuests[questId] = nil end
if c.AutoUntrackedQuests then c.AutoUntrackedQuests[questId] = nil end
if c.collapsedQuests then c.collapsedQuests[questId] = nil end
if c.TrackerHiddenQuests then c.TrackerHiddenQuests[questId] = nil end
if c.minAllQuestsInZone then c.minAllQuestsInZone[questId] = nil end
-- Remove hidden objectives for this quest (keys are "questId objectiveIndex")
if c.TrackerHiddenObjectives then
local prefix = tostring(questId) .. " "
for k in pairs(c.TrackerHiddenObjectives) do
if type(k) == "string" and strfind(k, prefix, 1, true) == 1 then
c.TrackerHiddenObjectives[k] = nil
end
end
end
end
function QuestieTracker:PruneGhostQuests()
if not QuestiePlayer or type(QuestiePlayer.currentQuestlog) ~= "table" then
return false
end
local removedAny = false
-- Fix #8: Collect stale keys first, then delete AFTER the pairs() loop to
-- avoid iterator invalidation (undefined behaviour in all WoW Lua runtimes).
local toRemove = {}
for key, q in pairs(QuestiePlayer.currentQuestlog) do
local keyId = tonumber(key)
local questId = keyId or (type(q) == "table" and q.Id) or (type(q) == "number" and q) or nil
-- Normalize bad entries: sometimes the value is the questId instead of the quest table.
if questId and type(q) ~= "table" then
local quest = QuestieDB and QuestieDB.GetQuest and QuestieDB.GetQuest(questId) or nil
if quest then
-- Schedule the repair as a removal + reinsertion by key; done below.
toRemove[#toRemove + 1] = { key = key, questId = questId, quest = quest, repair = true }
q = quest
keyId = questId
end
end
if questId then
local idx = GetQuestLogIndexByID and GetQuestLogIndexByID(questId)
if (not idx) or idx <= 0 then
-- Fix #1: Guard the questId before accessing so we never generate
-- the "doesn't exist in Game's quest log" warning at all.
toRemove[#toRemove + 1] = { key = key, questId = questId, clean = true }
end
end
end
-- Apply removals / repairs safely outside the iteration.
local i = 1
while i <= #toRemove do
local entry = toRemove[i]
if entry.repair then
if entry.key ~= entry.questId then
QuestiePlayer.currentQuestlog[entry.key] = nil
end
QuestiePlayer.currentQuestlog[entry.questId] = entry.quest
elseif entry.clean then
QuestiePlayer.currentQuestlog[entry.key] = nil
if entry.key ~= entry.questId then
QuestiePlayer.currentQuestlog[entry.questId] = nil
end
_RemoveQuestIdFromCharTables(entry.questId)
if QuestieTooltips and QuestieTooltips.RemoveQuest then
QuestieTooltips:RemoveQuest(entry.questId)
end
removedAny = true
end
i = i + 1
end
return removedAny
end
local KeybindFrame
local function _CreateSecureKeybindFrame()
if KeybindFrame then return end
KeybindFrame = CreateFrame("Button", "Questie_KeybindFrame", UIParent, "SecureActionButtonTemplate")
KeybindFrame:SetAttribute("type", "item")
KeybindFrame:RegisterForClicks("AnyDown")
KeybindFrame:Hide()
KeybindFrame:SetScript("PostClick", function(self, button)
if button == "OptionsButton" then
TrackerUtils:ToggleOptions()
elseif button == "TrackerButton" then
TrackerUtils:ToggleTracker()
elseif button == "JourneyButton" then
TrackerUtils:ToggleJourney()
end
end)
local lastProximityUpdate = 0
local function UpdateNearestQuestItemButton()
if InCombatLockdown() then return end
local now = GetTime()
if now - lastProximityUpdate > 5 then
lastProximityUpdate = now
local itemId = TrackerUtils:GetNearestQuestItemId()
if itemId then
KeybindFrame:SetAttribute("item", "item:" .. itemId)
else
KeybindFrame:SetAttribute("item", nil)
end
end
end
local f = CreateFrame("Frame")
f:RegisterEvent("PLAYER_REGEN_ENABLED")
f:SetScript("OnEvent", function()
UpdateNearestQuestItemButton()
end)
f:SetScript("OnUpdate", function()
UpdateNearestQuestItemButton()
end)
end
function QuestieTracker_UpdateQuestItemKeybind()
if not KeybindFrame then return end
if InCombatLockdown() then
return QuestieCombatQueue:Queue(QuestieTracker_UpdateQuestItemKeybind)
end
ClearOverrideBindings(KeybindFrame)
-- 1. Use Quest Item (Nearest)
local useItemKey = Questie.db.profile.useQuestItemKeybind
if useItemKey and useItemKey ~= "" then
SetOverrideBindingClick(KeybindFrame, true, useItemKey, "Questie_KeybindFrame", "LeftButton")
end
-- 2. Toggle Options
local optionsKey = Questie.db.profile.toggleOptionsKeybind
if optionsKey and optionsKey ~= "" then
SetOverrideBindingClick(KeybindFrame, true, optionsKey, "Questie_KeybindFrame", "OptionsButton")
end
-- 3. Toggle Tracker
local trackerKey = Questie.db.profile.toggleTrackerKeybind
if trackerKey and trackerKey ~= "" then
SetOverrideBindingClick(KeybindFrame, true, trackerKey, "Questie_KeybindFrame", "TrackerButton")
end
-- 4. Toggle My Journey
local journeyKey = Questie.db.profile.toggleMyJourneyKeybind
if journeyKey and journeyKey ~= "" then
SetOverrideBindingClick(KeybindFrame, true, journeyKey, "Questie_KeybindFrame", "JourneyButton")
end
end
local function _InstallQuestLogUpdateListener()
if QuestieTracker._questLogUpdateListenerInstalled then return end
local f = CreateFrame("Frame")
f:RegisterEvent("QUEST_LOG_UPDATE")
f:RegisterEvent("PLAYER_ENTERING_WORLD")
f:SetScript("OnEvent", function()
-- Queue to avoid taint/combat issues + collapse multiple updates
QuestieCombatQueue:Queue(function()
if not QuestieTracker.started then return end
QuestieTracker:PruneGhostQuests()
QuestieTracker:Update()
end)
end)
QuestieTracker._questLogUpdateListenerInstalled = true
end
-- Fix #1: _InstallMissingQuestLogWarningFilter is REMOVED.
-- The old implementation did `function Questie:Warning(...)` which is a raw
-- method replacement on the globally-registered Questie object, tainting it.
-- The warnings it suppressed are now prevented upstream in PruneGhostQuests
-- via the two-phase (collect then delete) pattern, so ghost-quest log index
-- warnings are never generated in the first place.
-- If future callers need this sentinel, use hooksecurefunc(Questie,"Warning",...)
-- which is safe on all clients that support it.
function QuestieTracker.Initialize()
if QuestieTracker.started then
-- The Tracker was already initialized, so we don't need to do it again.
return
end
-- These values might also be accessed by other modules, so we need to make sure they exist. Even when the Tracker is disabled
if (not Questie.db.char.TrackerHiddenQuests) then
Questie.db.char.TrackerHiddenQuests = {}
end
if (not Questie.db.char.TrackerHiddenObjectives) then
Questie.db.char.TrackerHiddenObjectives = {}
end
if (not Questie.db.char.TrackedQuests) then
Questie.db.char.TrackedQuests = {}
end
if (not Questie.db.char.AutoUntrackedQuests) then
Questie.db.char.AutoUntrackedQuests = {}
end
if (not Questie.db.char.collapsedZones) then
Questie.db.char.collapsedZones = {}
end
if (not Questie.db.char.minAllQuestsInZone) then
Questie.db.char.minAllQuestsInZone = {}
end
if (not Questie.db.char.collapsedQuests) then
Questie.db.char.collapsedQuests = {}
end
if (not Questie.db.char.trackedAchievementIds) then
Questie.db.char.trackedAchievementIds = {}
end
if (not Questie.db.profile.TrackerWidth) then
Questie.db.profile.TrackerWidth = 0
end
if (not Questie.db.profile.TrackerHeight) then
Questie.db.profile.TrackerHeight = 0
end
if (not Questie.db.profile.trackerSetpoint) then
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
end
-- Initialize tracker frames
trackerBaseFrame = TrackerBaseFrame.Initialize()
trackerHeaderFrame = TrackerHeaderFrame.Initialize(trackerBaseFrame)
trackerQuestFrame = TrackerQuestFrame.Initialize(trackerBaseFrame, trackerHeaderFrame)
-- Initialize tracker functions
TrackerLinePool.Initialize(trackerQuestFrame)
-- Keep tracker in sync for quests that auto-complete/disappear
_InstallQuestLogUpdateListener()
-- Note: _InstallMissingQuestLogWarningFilter was removed (fix #1).
-- Ghost quest warnings are now prevented upstream in PruneGhostQuests.
TrackerFadeTicker.Initialize(trackerBaseFrame, trackerHeaderFrame)
QuestieTracker.started = true
-- Initialize the secure keybind frame
_CreateSecureKeybindFrame()
QuestieTracker_UpdateQuestItemKeybind()
-- Initialize hooks
QuestieTracker:HookBaseTracker()
-- Insures all other data we're getting from other addons and WoW is loaded. There are edge
-- cases where Questie loads too fast before everything else is available.
C_Timer.After(1.0, function()
-- Hide frames during startup
if QuestieTracker.alreadyHooked then
if Questie.db.profile.stickyDurabilityFrame then DurabilityFrame:Hide() end
if TrackerUtils:IsVoiceOverLoaded() then VoiceOverFrame:Hide() end
end
-- Flip some Dugi Guides options to prevent weird behavior
if IsAddOnLoaded("DugisGuideViewerZ") then
-- Turns off "Show Quest Objectives - Display quest objectives in small/anchored frame instead of the watch frame"
DugisGuideViewer:SetDB(false, 39) -- DGV_OBJECTIVECOUNTER
-- Turns off "Auto Quest Tracking - Automatically add quest to the Objective Tracker on accept or objective update"
DugisGuideViewer:SetDB(false, 78) -- DGV_AUTO_QUEST_TRACK
-- Turns on "Clear Final Waypoint - Always clear the last waypoint on reach"
DugisGuideViewer:SetDB(true, 1006) -- DGV_CLEAR_FINAL_WAYPOINT
end
-- Quest Focus Feature
if Questie.db.char.TrackerFocus then
local focusType = type(Questie.db.char.TrackerFocus)
if focusType == "number" then
TrackerUtils:FocusQuest(Questie.db.char.TrackerFocus)
QuestieQuest:ToggleNotes(false)
elseif focusType == "string" then
local questId, objectiveIndex = string.match(Questie.db.char.TrackerFocus, "(%d+) (%d+)")
TrackerUtils:FocusObjective(questId, objectiveIndex)
QuestieQuest:ToggleNotes(false)
end
end
QuestieCombatQueue:Queue(function()
-- Hides tracker during a login or reloadUI
if Questie.db.profile.hideTrackerInDungeons and IsInInstance() then
QuestieTracker:Collapse()
end
-- Sync and populate the QuestieTracker - this should only run when a player has loaded
-- Questie for the first time or when Re-enabling the QuestieTracker after it's disabled.
-- The questsWatched variable is populated by the Unhooked GetNumQuestWatches(). If Questie
-- is enabled, this is always 0 unless it's run with a true var RE:GetNumQuestWatches(true).
if questsWatched > 0 then
-- When a quest is removed from the Watch Frame, the questIndex can change so we need to snag
-- the entire list and build a temp table with QuestIDs instead to ensure we remove them all.
local tempQuestIDs = {}
for i = 1, questsWatched do
local questIndex = GetQuestIndexForWatch(i)
if questIndex then
local _, _, _, _, _, _, _, questId = GetQuestLogTitle(questIndex)
if questId then
tempQuestIDs[i] = questId
end
end
end
-- Remove quest from the Blizzard Quest Watch and populate the tracker.
for _, questId in pairs(tempQuestIDs) do
local questIndex = GetQuestLogIndexByID(questId)
if questIndex then
QuestieTracker:AQW_Insert(questIndex, QUEST_WATCH_NO_EXPIRE)
end
end
end
-- Look for any QuestID's that don't belong in the Questie.db.char.TrackedQuests or
-- the Questie.db.char.AutoUntrackedQuests tables. They can get out of sync.
if Questie.db.profile.autoTrackQuests and Questie.db.char.AutoUntrackedQuests then
for untrackedQuestId in pairs(Questie.db.char.AutoUntrackedQuests) do
if not QuestiePlayer.currentQuestlog[untrackedQuestId] then
Questie.db.char.AutoUntrackedQuests[untrackedQuestId] = nil
end
end
elseif Questie.db.char.TrackedQuests then
for trackedQuestId in pairs(Questie.db.char.TrackedQuests) do
if not QuestiePlayer.currentQuestlog[trackedQuestId] then
Questie.db.char.TrackedQuests[trackedQuestId] = nil
end
end
end
-- The trackedAchievements variable is populated by GetTrackedAchievements(). If Questie
-- is enabled, this will always return nil so we need to save it before we enable Questie.
if Questie.IsWotlk or QuestieCompat.Is335 then
if #trackedAchievements > 0 then
local tempAchieves = trackedAchievements
-- Remove achievement from the Blizzard Quest Watch and populate the tracker.
for _, achieveId in pairs(tempAchieves) do
if achieveId then
RemoveTrackedAchievement(achieveId)
Questie.db.char.trackedAchievementIds[achieveId] = true
if (not AchievementFrame) then
AchievementFrame_LoadUI()
end
AchievementFrameAchievements_ForceUpdate()
end
end
end
trackedAchievements = { GetTrackedAchievements() }
WatchFrame_Update()
-- Sync and populate QuestieTrackers achievement cache
if Questie.db.char.trackedAchievementIds ~= trackedAchievementIds then
for achieveId in pairs(Questie.db.char.trackedAchievementIds) do
if Questie.db.char.trackedAchievementIds[achieveId] == true then
trackedAchievementIds[achieveId] = true
end
end
end
else
QuestWatch_Update()
end
if QuestLogFrame:IsShown() then QuestLog_Update() end
QuestieTracker:Update()
trackerBaseFrame:Hide()
end)
end)
end
function QuestieTracker:ResetLocation()
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:ResetLocation]")
trackerHeaderFrame.trackedQuests:SetMode(1) -- maximized
Questie.db.char.isTrackerExpanded = true
Questie.db.char.AutoUntrackedQuests = {}
Questie.db.profile.TrackerLocation = nil
Questie.db.char.collapsedQuests = {}
Questie.db.char.collapsedZones = {}
Questie.db.profile.TrackerWidth = 0
Questie.db.profile.TrackerHeight = 0
trackerBaseFrame:SetSize(25, 25)
TrackerBaseFrame:SetSafePoint()
QuestieTracker:Update()
end
function QuestieTracker:ResetDurabilityFrame()
if durabilityInitialPosition then
-- Only reset if it's been moved from it's default position set by Blizzard
if durabilityInitialPosition ~= { DurabilityFrame:GetPoint() } then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:ResetDurabilityFrame]")
-- Resets Durability Frame back to it's default position
DurabilityFrame:ClearAllPoints()
DurabilityFrame:SetPoint(unpack(durabilityInitialPosition))
local numAlerts = 0
for i = 1, #INVENTORY_ALERT_STATUS_SLOTS do
if GetInventoryAlertStatus(i) > 0 then
numAlerts = numAlerts + 1
end
end
-- Check the alert status and reset visibility
if numAlerts > 0 then
DurabilityFrame:Show()
else
if DurabilityFrame:IsShown() then
DurabilityFrame:Hide()
end
end
end
end
end
function QuestieTracker:UpdateDurabilityFrame()
if QuestieTracker.started and Questie.db.profile.trackerEnabled and Questie.db.profile.stickyDurabilityFrame then
if Questie.db.char.isTrackerExpanded and QuestieTracker:HasQuest() then
local numAlerts = 0
for i = 1, #INVENTORY_ALERT_STATUS_SLOTS do
if GetInventoryAlertStatus(i) > 0 then
numAlerts = numAlerts + 1
end
end
if numAlerts > 0 then
-- screen width accounting for scale
local screenWidth = GetScreenWidth() * UIParent:GetEffectiveScale()
-- middle of the frame, first return is x value, second return is the y value
local trackerFrameX = trackerBaseFrame:GetCenter()
DurabilityFrame:ClearAllPoints()
DurabilityFrame:SetClampedToScreen(true)
DurabilityFrame:SetFrameStrata("MEDIUM")
DurabilityFrame:SetFrameLevel(0)
if trackerFrameX <= (screenWidth / 2) then
DurabilityFrame:SetPoint("LEFT", trackerBaseFrame, "TOPRIGHT", 0, -40)
else
DurabilityFrame:SetPoint("RIGHT", trackerBaseFrame, "TOPLEFT", 0, -40)
end
DurabilityFrame:Show()
else
if DurabilityFrame:IsShown() then
DurabilityFrame:Hide()
end
end
if TrackerBaseFrame.isSizing == true or TrackerBaseFrame.isMoving == true then
Questie:Debug(Questie.DEBUG_SPAM, "[QuestieTracker:UpdateDurabilityFrame]")
else
Questie:Debug(Questie.DEBUG_INFO, "[QuestieTracker:UpdateDurabilityFrame]")
end
else
QuestieTracker:ResetDurabilityFrame()
end
end
end
function QuestieTracker:ResetVoiceOverFrame()
if voiceOverInitialPosition then
if voiceOverInitialPosition ~= { VoiceOverFrame:GetPoint() } then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:ResetVoiceOverFrame]")
VoiceOverFrame:ClearAllPoints()
VoiceOverFrame:SetPoint(unpack(voiceOverInitialPosition))
VoiceOverFrame:SetClampedToScreen(true)
VoiceOverFrame:SetFrameStrata("MEDIUM")
VoiceOverFrame:SetFrameLevel(0)
VoiceOver.Addon.db.profile.SoundQueueUI.LockFrame = false
VoiceOver.SoundQueueUI:RefreshConfig()
if VoiceOverFrame:IsShown() then
VoiceOver.SoundQueue:RemoveAllSoundsFromQueue()
VoiceOverFrame:Hide()
end
end
end
end
function QuestieTracker:UpdateVoiceOverFrame()
if TrackerUtils:IsVoiceOverLoaded() then
if QuestieTracker.started and Questie.db.profile.trackerEnabled and Questie.db.profile.stickyVoiceOverFrame then
if Questie.db.char.isTrackerExpanded and QuestieTracker:HasQuest() then
-- screen width accounting for scale
local screenWidth = GetScreenWidth() * UIParent:GetEffectiveScale()
-- middle of the frame, first return is x value, second return is the y value
local trackerFrameX = trackerBaseFrame:GetCenter()
VoiceOverFrame:SetClampedToScreen(true)
VoiceOverFrame:SetFrameStrata("MEDIUM")
VoiceOverFrame:SetFrameLevel(0)
local verticalOffSet
if Questie.db.profile.stickyDurabilityFrame then
if DurabilityFrame:IsVisible() then
verticalOffSet = -125
else
verticalOffSet = -7
end
end
if trackerFrameX <= (screenWidth / 2) then
VoiceOverFrame:ClearAllPoints()
VoiceOverFrame:SetPoint("TOPLEFT", trackerBaseFrame, "TOPRIGHT", 15, verticalOffSet)
else
VoiceOverFrame:ClearAllPoints()
VoiceOverFrame:SetPoint("TOPRIGHT", trackerBaseFrame, "TOPLEFT", -15, verticalOffSet)
end
VoiceOverFrame:SetWidth(500)
VoiceOverFrame:SetHeight(120)
VoiceOver.Addon.db.profile.SoundQueueUI.LockFrame = true
VoiceOver.SoundQueueUI:RefreshConfig()
VoiceOver.SoundQueueUI:UpdateSoundQueueDisplay()
if TrackerBaseFrame.isSizing == true or TrackerBaseFrame.isMoving == true then
Questie:Debug(Questie.DEBUG_SPAM, "[QuestieTracker:UpdateVoiceOverFrame]")
else
Questie:Debug(Questie.DEBUG_INFO, "[QuestieTracker:UpdateVoiceOverFrame]")
end
else
QuestieTracker:ResetVoiceOverFrame()
end
end
end
end
-- If the player loots a "Quest Item" then this triggers a Tracker Update so the
-- Quest Item Button can be switched on and appear in the tracker.
---@param text string
function QuestieTracker:QuestItemLooted(text)
local playerLoot = strmatch(text, "You receive ") or strmatch(text, "You create")
local itemId = tonumber(string.match(text, "item:(%d+)"))
if playerLoot and itemId then
local _, _, _, _, _, itemType, _, _, _, _, _, classID = GetItemInfo(itemId)
local usableItem = TrackerUtils:IsQuestItemUsable(itemId)
if (itemType == "Quest" or classID == 12 or QuestieDB.QueryItemSingle(itemId, "class") == 12) and usableItem then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker] - Quest Item Detected (itemId) - ", itemId)
C_Timer.After(0.25, function()
_QuestEventHandler:UpdateAllQuests()
Questie:Debug(Questie.DEBUG_DEVELOP,
"[QuestieTracker] - Callback --> QuestEventHandler:UpdateAllQuests()")
end)
QuestieCombatQueue:Queue(function()
C_Timer.After(0.5, function()
QuestieTracker:Update()
end)
end)
end
end
end
function QuestieTracker:HasQuest()
local hasQuest
if (GetNumQuestWatches(true) == 0) then
if Questie.IsWotlk or QuestieCompat.Is335 then
if (GetNumTrackedAchievements(true) == 0) then
hasQuest = false
else
hasQuest = true
end
else
hasQuest = false
end
else
if not Questie.db.profile.trackerShowCompleteQuests then
local completedQuests = 0
-- Keep track of the number of completed quests
for questId, quest in pairs(QuestiePlayer.currentQuestlog) do
if not quest then break end
if type(quest) ~= "table" then
quest = QuestieDB and QuestieDB.GetQuest and QuestieDB.GetQuest(questId) or nil
if quest then
QuestiePlayer.currentQuestlog[questId] = quest
end
end
if quest and quest.IsComplete and quest:IsComplete() == 1 then
completedQuests = completedQuests + 1
end
end
-- This hides the Tracker when all tracked Quests are complete
if completedQuests == GetNumQuestWatches(true) then
hasQuest = false
else
hasQuest = true
end
else
hasQuest = true
end
end
Questie:Debug(Questie.DEBUG_SPAM, "[QuestieTracker:HasQuest] - ", hasQuest)
return hasQuest
end
function QuestieTracker:Enable()
-- Update the questsWatched var before we re-enable
if questsWatched == 0 then
questsWatched = GetNumQuestWatches()
end
Questie.db.profile.trackerEnabled = true
QuestieTracker.started = false
QuestieTracker.Initialize()
ReloadUI()
end
function QuestieTracker:Disable()
Questie.db.profile.trackerEnabled = false
QuestieTracker:ResetDurabilityFrame()
QuestieTracker:ResetVoiceOverFrame()
Questie.db.char.TrackedQuests = {}
Questie.db.char.AutoUntrackedQuests = {}
if Questie.IsWotlk or QuestieCompat.Is335 then
Questie.db.char.trackedAchievementIds = {}
trackedAchievementIds = {}
end
QuestieTracker:Unhook()
QuestieTracker:Update()
ReloadUI()
end
-- Function for the Slash handler
function QuestieTracker:Toggle()
if Questie.db.profile.trackerEnabled then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:Toggle] - Tracker Disabled.")
Questie.db.profile.trackerEnabled = false
else
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:Toggle] - Tracker Enabled.")
Questie.db.profile.trackerEnabled = true
end
QuestieTracker:Update()
end
-- Minimizes the QuestieTracker
function QuestieTracker:Collapse()
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:Collapse]")
if trackerHeaderFrame and trackerHeaderFrame.trackedQuests and Questie.db.char.isTrackerExpanded then
trackerHeaderFrame.trackedQuests:Click()
QuestieTracker:Update()
end
end
-- Maximizes the QuestieTracker
function QuestieTracker:Expand()
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:Expand]")
if trackerHeaderFrame and trackerHeaderFrame.trackedQuests and (not Questie.db.char.isTrackerExpanded) then
trackerHeaderFrame.trackedQuests:Click()
QuestieTracker:Update()
end
end
function QuestieTracker:Update()
-- Prevents calling the tracker too often, especially when the QuestieCombatQueue empties after combat ends
local now = GetTime()
if (not QuestieTracker.started) then
return
end
-- Auto-complete / vanish quests can leave ghost entries around.
-- Always prune them first to prevent stale lines + warning spam.
QuestieTracker:PruneGhostQuests()
if InCombatLockdown() or (now - lastTrackerUpdate) < 0.1 then
return
end
lastTrackerUpdate = now
-- Tracker has started but not enabled, hide the frames
if (not Questie.db.profile.trackerEnabled or QuestieTracker.disableHooks == true) then
if trackerBaseFrame and trackerBaseFrame:IsShown() then
QuestieCombatQueue:Queue(function()
if Questie.db.profile.stickyDurabilityFrame then
QuestieTracker:ResetDurabilityFrame()
end
if Questie.db.profile.stickyVoiceOverFrame then
QuestieTracker:ResetVoiceOverFrame()
end
trackerBaseFrame:Hide()
end)
end
return
end
if TrackerBaseFrame.isSizing == true or TrackerBaseFrame.isMoving == true or TrackerUtils.FilterProximityTimer == true then
Questie:Debug(Questie.DEBUG_SPAM, "[QuestieTracker:Update]")
else
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:Update]")
end
TrackerHeaderFrame:Update()
TrackerQuestFrame:Update()
TrackerBaseFrame:Update()
TrackerLinePool.ResetLinesForChange()
TrackerLinePool.ResetButtonsForChange()
-- This is needed so the Tracker can also decrease its width
trackerLineWidth = 0
-- Setup local QuestieTracker:Update vars
local trackerFontSizeZone = Questie.db.profile.trackerFontSizeZone
local trackerFontSizeQuest = Questie.db.profile.trackerFontSizeQuest
local questMarginLeft = (trackerMarginLeft + trackerMarginRight) - (18 - trackerFontSizeQuest)
local objectiveMarginLeft = questMarginLeft + trackerFontSizeQuest
local questItemButtonSize = 12 + trackerFontSizeQuest
local objectiveColor = Questie.db.profile.trackerColorObjectives
local line
local sortedQuestIds, questDetails = TrackerUtils:GetSortedQuestIds()
local firstQuestInZone = false
local zoneCheck
local primaryButton = false
local secondaryButton = false
local secondaryButtonAlpha
-- Begin populating the Tracker with Quests
local _UpdateQuests = function()
for _, questId in pairs(sortedQuestIds) do
local status, result = pcall(function()
if not questId then return "BREAK" end
local quest = questDetails[questId].quest
local complete = quest:IsComplete()
local zoneName = questDetails[questId].zoneName
local remainingSeconds = TrackerQuestTimers:GetRemainingTime(quest, nil, true)
local timedQuest = (quest.trackTimedQuest or quest.timedBlizzardQuest)
if (complete ~= 1 or Questie.db.profile.trackerShowCompleteQuests or timedQuest)
and (Questie.db.profile.autoTrackQuests and not Questie.db.char.AutoUntrackedQuests[questId])
or (not Questie.db.profile.autoTrackQuests and Questie.db.char.TrackedQuests[questId]) then
-- Add Quest Zones
if zoneCheck ~= zoneName then
firstQuestInZone = true
end
if firstQuestInZone then
-- Get first line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then return "BREAK" end
-- Set Line Mode, Types, Clickers
line:SetMode("zone")
line:SetZone(zoneName)
line.expandQuest:Hide()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Setup Zone Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", 0, 0)
-- Set Zone Title and default Min/Max states
if Questie.db.char.collapsedZones[zoneName] then
line.expandZone:SetMode(0)
line.label:SetText("|cFFC0C0C0" .. l10n(zoneName) .. " +|r")
else
line.expandZone:SetMode(1)
line.label:SetText("|cFFC0C0C0" .. l10n(zoneName) .. "|r")
end
-- Checks the minAllQuestsInZone[zone] table and if empty, zero out the table.
if Questie.db.char.minAllQuestsInZone[zoneName] ~= nil and not Questie.db.char.minAllQuestsInZone[zoneName].isTrue and not Questie.db.char.collapsedZones[zoneName] then
local minQuestIdCount = 0
for minQuestId, _ in pairs(Questie.db.char.minAllQuestsInZone[zoneName]) do
if type(minQuestId) == "number" then
minQuestIdCount = minQuestIdCount + 1
end
end
if minQuestIdCount == 0 then
Questie.db.char.minAllQuestsInZone[zoneName] = nil
end
end
-- Check and measure Zone Label text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() + trackerMarginLeft +
trackerMarginRight)
-- Set Zone Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - trackerMarginLeft - trackerMarginRight)
line:SetWidth(line.label:GetWidth())
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + trackerMarginLeft)
-- Setup Min/Max Button
line.expandZone:ClearAllPoints()
line.expandZone:SetPoint("TOPLEFT", line, "TOPLEFT", 0, 0)
line.expandZone:SetWidth(line.label:GetWidth())
line.expandZone:SetHeight(line.label:GetHeight())
line.expandZone:Show()
-- Adds 4 pixels between Zone and first Quest Title
line:SetHeight(line.label:GetHeight() + 4)
-- Set Zone states
line:Show()
line.label:Show()
line.Quest = nil
line.Objective = nil
firstQuestInZone = false
zoneCheck = zoneName
end
-- Add quest
if (not Questie.db.char.collapsedZones[zoneName]) then
-- Get next line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then return "BREAK" end
-- Set Line Mode, Types, Clickers
line:SetMode("quest")
line:SetOnClick("quest")
line:SetQuest(quest)
line:SetObjective(nil)
line.expandZone:Hide()
line.criteriaMark:Hide()
-- Set Min/Max Button and default states
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 8, 1)
line.expandQuest.zoneId = zoneName
-- Set Completion Text
local completionText = TrackerUtils:GetCompletionText(quest)
-- Clear Blizzard Completion Text
if ((Questie.db.profile.hideBlizzardCompletionText or objectiveColor == "minimal") and not timedQuest) or complete == -1 then
completionText = nil
end
-- This removes any blank lines from Completion Text
if completionText ~= nil then
if strfind(completionText, "\r\n") then
completionText = completionText:gsub("\r\n", "")
else
completionText = completionText:gsub("(.\r?\n?)\r?\n?", "%1")
end
-- Completion Text should always be green
completionText = "|cFF4CFF4C" .. completionText
end
-- Set minimizable quest flag
local isMinimizable = (complete == 1 or (#quest.Objectives == 0 and quest.isComplete == true)) and
completionText == nil
-- Handles the collapseCompletedQuests option from the Questie Config --> Tracker options.
if Questie.db.profile.collapseCompletedQuests and isMinimizable and not timedQuest then
if not Questie.db.char.collapsedQuests[quest.Id] then
Questie.db.char.collapsedQuests[quest.Id] = true
end
else
-- The minAllQuestsInZone table is always blank until a player Shift+Clicks the Zone header (MouseDown).
-- QuestieTracker:Update is triggered and the table is then filled with all Quest ID's in the same Zone.
if Questie.db.char.minAllQuestsInZone[zoneName] ~= nil and Questie.db.char.minAllQuestsInZone[zoneName].isTrue then
Questie.db.char.minAllQuestsInZone[zoneName][quest.Id] = true
end
-- Handles all the Min/Max behavior individually for each quest.
if Questie.db.char.collapsedQuests[quest.Id] then
line.expandQuest:SetMode(0)
else
line.expandQuest:SetMode(1)
end
end
-- Setup Quest Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", questMarginLeft, 0)
-- Set Quest Title - This handles the "Auto Minimize Completed Quests" option but we don't auto-minimize timed quests.
local coloredQuestName
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)
if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
questName = "[" .. quest.level .. "] " .. questName
end
if Questie.db.profile.enableTooltipsQuestID then
questName = questName .. " (" .. quest.Id .. ")"
end
coloredQuestName = "|cFFFFFF00" .. questName .. "|r"
-- Fallback quests (e.g. custom server quests not in QuestieDB) skip
-- GetColoredQuestName entirely since it needs a DB name lookup, so they
-- never got the (Complete) suffix DB quests get under the same setting.
if Questie.db.profile.collapseCompletedQuests and isMinimizable then
coloredQuestName = coloredQuestName .. " " ..
Questie:Colorize("(" .. l10n("Complete") .. ")", "green")
end
elseif timedQuest then
coloredQuestName = QuestieLib:GetColoredQuestName(quest.Id,
Questie.db.profile.trackerShowQuestLevel, false, false)
else
coloredQuestName = QuestieLib:GetColoredQuestName(quest.Id,
Questie.db.profile.trackerShowQuestLevel,
(Questie.db.profile.collapseCompletedQuests and isMinimizable), false)
end
if not coloredQuestName then
-- QuestieDB has no "name" entry for this quest (e.g. a custom
-- server quest not yet in the static DB), so GetColoredQuestName
-- returned nil -- fall back instead of SetText(nil) blanking the
-- title line while its objectives still render normally below it.
-- Use the live quest object's own level (kept in sync by
-- QuestieQuest:PopulateQuestLogInfo) instead of QuestieLib.GetTbcLevel,
-- 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)
if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
fallbackName = QuestieLib:GetQuestString(quest.Id, fallbackName, quest.level, false)
end
if Questie.db.profile.enableTooltipsQuestID then
fallbackName = fallbackName .. " (" .. quest.Id .. ")"
end
coloredQuestName = "|cFFFFFF00" .. fallbackName .. "|r"
-- Same suffix rule as the normal (DB) branch -- GetColoredQuestName
-- never got a chance to run since it bailed out on the missing name.
if Questie.db.profile.collapseCompletedQuests and isMinimizable then
coloredQuestName = coloredQuestName .. " " ..
Questie:Colorize("(" .. l10n("Complete") .. ")", "green")
end
end
line.label:SetText(coloredQuestName)
-- Check and measure Quest Label text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() + questMarginLeft +
trackerMarginRight)
-- Set Quest Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - questMarginLeft - trackerMarginRight)
line:SetWidth(line.label:GetWidth() + questMarginLeft)
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + questMarginLeft)
-- Adds 4 pixels between Quest Title and first Objective
line:SetHeight(line.label:GetHeight() + 4)
-- 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
if not sourceItemId or sourceItemId == 0 then
-- QuestieDB has no data for this quest (e.g. a custom server quest).
-- Ask the client directly which item, if any, it considers this quest's
-- usable "special item" instead of relying on static DB data that
-- simply doesn't exist for quests Questie doesn't know about.
local questLogIndex = GetQuestLogIndexByID and GetQuestLogIndexByID(quest.Id)
if questLogIndex and questLogIndex > 0 and GetQuestLogSpecialItemInfo then
local link = GetQuestLogSpecialItemInfo(questLogIndex)
local liveItemId = link and tonumber(link:match("item:(%d+)"))
if liveItemId then
sourceItemId = liveItemId
isLiveSourceItem = true
quest.sourceItemId = liveItemId
end
end
end
quest._liveSourceItem = isLiveSourceItem
local sourceItem = sourceItemId and TrackerUtils:IsQuestItemUsable(sourceItemId)
local requiredItems = quest.requiredSourceItems
local requiredItem = requiredItems and TrackerUtils:IsQuestItemUsable(requiredItems[1])
local isComplete = (quest.isComplete ~= true and #quest.Objectives == 0) or
quest.isComplete == true
-- Occasionally a quest will be in a complete state and still have a usable Quest Item. Sometimes these usable
-- items spawn an NPC that is needed to finish the quest. Or an item that teleports you to the quest finisher.
if complete == 1 and isComplete and (sourceItem or requiredItem) then
-- This shows QIB's for Quest Itmes that are needed after a quest is complete with objectives
if sourceItemId > 1 and requiredItem and sourceItemId ~= requiredItems[1] then
quest.sourceItemId = 0
usableQIB = true
end
-- This shows QIB's for Quest Items that are needed after a quest is complete without objectives
if sourceItemId > 1 and not requiredItem and quest.isComplete ~= true then
usableQIB = true
end
end
-- Adds the primary Quest Item button
if complete ~= 1 and (sourceItem or (requiredItems and #requiredItems == 1 and requiredItem)) or usableQIB then
-- Get button from buttonPool
local button = TrackerLinePool.GetNextItemButton()
if not button then return "BREAK" end -- stop populating the tracker
-- Get and save Quest Title linePool to buttonPool
button.line = line
-- Setup button and set attributes
if button:SetItem(quest, "primary", questItemButtonSize) then
local height = 0
local frame = button.line
while frame and frame ~= trackerQuestFrame do
local _, parent, _, _, yOff = frame:GetPoint()
height = height - (frame:GetHeight() - yOff)
frame = parent
end
-- If the Quest is minimized show the Expand Quest button
if Questie.db.char.collapsedQuests[quest.Id] then
if Questie.db.profile.collapseCompletedQuests and isMinimizable and not timedQuest then
button.line.expandQuest:Hide()
else
button.line.expandQuest:Show()
end
else
button.line.expandQuest:Hide()
end
-- Attach button to Quest Title linePool
button:SetPoint("TOPLEFT", button.line, "TOPLEFT", 0, 0)
button:SetParent(button.line)
button:Show()
-- Set flag to allow secondary Quest Item Buttons
primaryButton = true
-- If the Quest Zone or Quest is minimized then set UIParent and hide buttons since the buttons are normally attached to the Quest frame.
-- If buttons are left attached to the Quest frame and if the Tracker frame is hidden in combat, then it would also try and hide the
-- buttons which you can't do in combat. This helps avoid violating the Blizzard SecureActionButtonTemplate restrictions relating to combat.
if Questie.db.char.collapsedZones[zoneName] or Questie.db.char.collapsedQuests[quest.Id] then
button:SetParent(UIParent)
button:Hide()
end
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.
button:SetParent(UIParent)
if (Questie.db.profile.collapseCompletedQuests and isMinimizable and not timedQuest) then
line.expandQuest:Hide()
else
line.expandQuest:Show()
end
button:Hide()
end
-- Save button to linePool
line.button = button
-- Hide button if quest complete or failed
elseif (Questie.db.profile.collapseCompletedQuests and isMinimizable and not timedQuest) then
line.expandQuest:Hide()
else
line.expandQuest:Show()
end
-- Adds the Secondary Quest Item Button (only if Primary is present)
if (complete ~= 1 and primaryButton and requiredItems and #requiredItems > 1 and next(quest.Objectives)) then
if type(requiredItems) == "table" then
-- Make sure it's a "secondary" button and if a quest item is "usable".
for _, itemId in pairs(requiredItems) do
-- GetItemSpell(itemId) is a bit of a work around for not having a Blizzard API for checking an items IsUsable state.
if itemId and itemId ~= sourceItemId and QuestieDB.QueryItemSingle(itemId, "class") == 12 and TrackerUtils:IsQuestItemUsable(itemId) then
-- Get button from buttonPool
local altButton = TrackerLinePool.GetNextItemButton()
if not altButton then return "BREAK" end -- stop populating the tracker
-- Set itemID
altButton.itemID = itemId
-- Get and save Quest Title linePool to buttonPool
altButton.line = line
-- Setup button and set attributes
if altButton:SetItem(quest, "secondary", questItemButtonSize) then
local height = 0
local frame = altButton.line
while frame and frame ~= trackerQuestFrame do
local _, parent, _, _, yOff = frame:GetPoint()
height = height - (frame:GetHeight() - yOff)
frame = parent
end
if not Questie.db.char.collapsedQuests[quest.Id] and altButton:GetAlpha() > 0 then
-- Set and indent Quest Title linePool
altButton.line.label:ClearAllPoints()
altButton.line.label:SetPoint("TOPLEFT", altButton.line, "TOPLEFT",
questMarginLeft + 2 + questItemButtonSize, 0)
-- Recheck and Remeasure Quest Label text width and update tracker width
QuestieTracker:UpdateWidth(altButton.line.label:GetStringWidth() +
questMarginLeft + trackerMarginRight + questItemButtonSize)
-- Reset Quest Title Label and linePool widths
altButton.line.label:SetWidth(trackerBaseFrame:GetWidth() -
questMarginLeft - trackerMarginRight - questItemButtonSize)
altButton.line:SetWidth(altButton.line.label:GetWidth() + questMarginLeft +
questItemButtonSize)
-- Re-compare largest text Label in the tracker with Secondary Button/Quest and current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
altButton.line.label:GetStringWidth() + questMarginLeft +
questItemButtonSize)
elseif altButton:GetAlpha() == 0 then
-- Set Quest Title linePool
altButton.line.label:ClearAllPoints()
altButton.line.label:SetPoint("TOPLEFT", altButton.line, "TOPLEFT",
questMarginLeft, 0)
-- Recheck and Remeasure Quest Label text width and update tracker width
QuestieTracker:UpdateWidth(altButton.line.label:GetStringWidth() +
questMarginLeft + trackerMarginRight)
-- Reset Quest Title Label and linePool widths
altButton.line.label:SetWidth(trackerBaseFrame:GetWidth() -
questMarginLeft - trackerMarginRight)
altButton.line:SetWidth(altButton.line.label:GetWidth() + questMarginLeft)
-- Re-compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
altButton.line.label:GetStringWidth() + questMarginLeft)
end
-- Attach button to Quest Title linePool
altButton:SetPoint("TOPLEFT", altButton.line, "TOPLEFT",
2 + questItemButtonSize, 0)
altButton:SetParent(altButton.line)
altButton:Show()
-- Set flag to shift objective lines
secondaryButton = true
secondaryButtonAlpha = altButton:GetAlpha()
-- If the Quest Zone or Quest is minimized then set UIParent and hide buttons since the buttons are normally attached to the Quest frame.
-- If buttons are left attached to the Quest frame and if the Tracker frame is hidden in combat, then it would also try and hide the
-- buttons which you can't do in combat. This helps avoid violating the Blizzard SecureActionButtonTemplate restrictions relating to combat.
if Questie.db.char.collapsedZones[zoneName] or Questie.db.char.collapsedQuests[quest.Id] then
altButton:SetParent(UIParent)
altButton:Hide()
end
else
-- See previous comment for details on why we're setting this button to UIParent.
altButton:SetParent(UIParent)
altButton:Hide()
end
-- Save button to linePool
line.altButton = altButton
end
end
end
end
-- Set Secondary Quest Item Button Margins (QBC - Quest Button Check)
local lineLabelWidthQBC, lineLabelBaseFrameQBC, lineWidthQBC
if secondaryButton and secondaryButtonAlpha ~= 0 then
lineLabelWidthQBC = objectiveMarginLeft + trackerMarginRight + questItemButtonSize
lineLabelBaseFrameQBC = objectiveMarginLeft + trackerMarginRight + questItemButtonSize
lineWidthQBC = objectiveMarginLeft + questItemButtonSize
else
lineLabelWidthQBC = objectiveMarginLeft + trackerMarginRight
lineLabelBaseFrameQBC = objectiveMarginLeft + trackerMarginRight
lineWidthQBC = objectiveMarginLeft
end
-- Set Quest Line states
line:Show()
line.label:Show()
-- Add quest Objectives (if applicable)
if (not Questie.db.char.collapsedQuests[quest.Id]) then
-- Add Quest Timers (if applicable)
if timedQuest then
-- Get next line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then return "BREAK" end
-- Set Line Mode, Types, Clickers
line:SetMode("objective")
line:SetOnClick("quest")
line:SetQuest(quest)
line.expandZone:Hide()
line.expandQuest:Hide()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Setup Timer Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", lineWidthQBC, 0)
-- Set Timer font
line.label:SetFont((LSM30 and LSM30.Fetch and LSM30:Fetch("font", Questie.db.profile.trackerFontObjective)) or Questie.db.profile.trackerFontObjective,
Questie.db.profile.trackerFontSizeObjective, Questie.db.profile.trackerFontOutline)
-- Set Timer Title based on states
line.label.activeTimer = false
if quest.timedBlizzardQuest then
line.label:SetText(Questie:Colorize(l10n("Blizzard Timer Active") .. "!", "blue"))
else
local timeRemainingString, timeRemaining = TrackerQuestTimers:GetRemainingTime(quest,
line, false)
if timeRemaining then
if timeRemaining <= 1 then
line.label:SetText(Questie:Colorize("0 Seconds", "blue"))
line.label.activeTimer = false
else
line.label:SetText(Questie:Colorize(timeRemainingString, "blue"))
line.label.activeTimer = true
end
end
end
-- Check and measure Timer text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() + lineLabelWidthQBC)
-- Set Timer Label and Line widthsl
line.label:SetWidth(trackerBaseFrame:GetWidth() - lineLabelBaseFrameQBC)
line:SetWidth(line.label:GetWidth() + lineWidthQBC)
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + lineWidthQBC)
-- Set Timer states
line:Show()
line.label:Show()
end
-- Add incomplete Quest Objectives
if complete == 0 and quest.isComplete ~= true then
for _, objective in pairs(quest.Objectives) do
if objective and objective.Description and (not Questie.db.profile.hideCompletedQuestObjectives or (Questie.db.profile.hideCompletedQuestObjectives and objective.Needed ~= objective.Collected)) then
-- Get next line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then return "BREAK" end
-- Set Line Mode, Types, Clickers
line:SetMode("objective")
line:SetOnClick("quest")
line:SetQuest(quest)
line:SetObjective(objective)
line.expandZone:Hide()
line.expandQuest:Hide()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Setup Objective Label based on states.
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", lineWidthQBC, 0)
-- Set Objective based on states
local objDesc = objective.Description:gsub("%.", "")
if (objective.Completed ~= true or (objective.Completed == true and #quest.Objectives > 1)) then
local lineEnding
lineEnding = tostring(objective.Collected) ..
"/" .. tostring(objective.Needed)
-- Set Objective text
line.label:SetText(QuestieLib:GetRGBForObjective(objective) ..
objDesc .. ": " .. lineEnding)
-- Check and measure Objective text and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() +
lineLabelWidthQBC)
-- Set Objective label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - lineLabelBaseFrameQBC)
line:SetWidth(line.label:GetWidth() + lineWidthQBC)
-- Compare current text label and the largest text label in the Tracker, then save the widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + lineWidthQBC)
-- Edge case where the quest is still flagged incomplete for single objectives and yet the objective itself is flagged complete
elseif (objective.Completed == true and completionText ~= nil and #quest.Objectives == 1) and objectiveColor ~= "minimal" then
-- Set Blizzard Completion text for single objectives
line.label:SetText(completionText)
-- If the line width is less than the minimum Tracker width then don't wrap text
if line.label:GetStringWidth() + objectiveMarginLeft < trackerMinLineWidth then
-- Check and measure Blizzard Completion text and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() +
objectiveMarginLeft + trackerMarginRight)
-- Set Blizzard Completion label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
line:SetWidth(line.label:GetWidth() + objectiveMarginLeft)
-- Compare current text label and the largest text label in the Tracker, then save the widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + objectiveMarginLeft)
else
-- Set Blizzard Completion Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
line:SetWidth(GetWrappedWidth(line.label) + objectiveMarginLeft)
-- Blizzard Completion Text tends to be rather verbose. Allow text wrapping.
line.label:SetHeight(line.label:GetStringHeight() *
GetNumLines(line.label))
line:SetHeight(line.label:GetHeight())
-- Compare trackerLineWidth, trackerMinLineWidth and the current label, then save the widest width
trackerLineWidth = math.max(trackerLineWidth, trackerMinLineWidth,
GetWrappedWidth(line.label) + objectiveMarginLeft)
end
-- Update Quest has a check for this edge case. Should reset the Quest Icons and show the Quest Finisher
QuestieQuest:UpdateQuest(quest.Id)
-- Hide the Secondary Quest Item Button
if secondaryButton and secondaryButtonAlpha ~= 0 then
line.altButton:SetParent(UIParent)
line.altButton:Hide()
end
end
-- Adds 1 pixel between multiple Objectives
line:SetHeight(line.label:GetHeight() + 1)
-- Set Objective state
line:Show()
line.label:Show()
end
end
-- Add complete/failed Quest Objectives and tag them as either complete or failed so as to always have at least one objective.
-- Some quests have "Blizzard Completion Text" that is displayed to show where to go next or where to turn in the quest.
elseif complete == 1 or complete == -1 or quest.isComplete == true then
-- Get next line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then return "BREAK" end
-- Set Line Mode, Types, Clickers
line:SetMode("objective")
line:SetOnClick("quest")
line:SetQuest(quest)
line.expandZone:Hide()
line.expandQuest:Hide()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Setup Objective Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", lineWidthQBC, 0)
-- Set Objective label based on states
if ((complete == 1 and completionText ~= nil and #quest.Objectives == 0) or (quest.isComplete == true and completionText ~= nil)) and objectiveColor ~= "minimal" then
-- Set Blizzard Completion text for single objectives
line.label:SetText(completionText)
-- If the line width is less than the minimum Tracker width then don't wrap text
if line.label:GetStringWidth() + objectiveMarginLeft < trackerMinLineWidth then
-- Check and measure Blizzard Completion text and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() +
objectiveMarginLeft + trackerMarginRight)
-- Set Blizzard Completion label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
line:SetWidth(line.label:GetWidth() + objectiveMarginLeft)
-- Compare current text label and the largest text label in the Tracker, then save the widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + objectiveMarginLeft)
else
-- Set Blizzard Completion Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
line:SetWidth(GetWrappedWidth(line.label) + objectiveMarginLeft)
-- Blizzard Completion Text tends to be rather verbose. Allow text wrapping.
line.label:SetHeight(line.label:GetStringHeight() * GetNumLines(line.label))
line:SetHeight(line.label:GetHeight())
-- Compare trackerLineWidth, trackerMinLineWidth and the current label, then save the widest width
trackerLineWidth = math.max(trackerLineWidth, trackerMinLineWidth,
GetWrappedWidth(line.label) + objectiveMarginLeft)
end
-- Hide the Secondary Quest Item Button. There are some quests with usable items after a quest is completed I
-- have yet to encounter a completed quest where both a Primary and Secondary "usable" Quest Item was needed.
if secondaryButton and secondaryButtonAlpha ~= 0 then
line.altButton:SetParent(UIParent)
line.altButton:Hide()
end
else
if complete == 1 or (#quest.Objectives == 0 and quest.isComplete == true and completionText == nil and complete ~= -1) then
line.label:SetText(Questie:Colorize(l10n("Quest Complete") .. "!", "green"))
elseif complete == -1 then
line.label:SetText(Questie:Colorize(l10n("Quest Failed") .. "!", "red"))
end
-- Check and measure Objective text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() + lineLabelWidthQBC)
-- Set Objective Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - lineLabelBaseFrameQBC)
line:SetWidth(line.label:GetWidth() + lineWidthQBC)
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + lineWidthQBC)
end
-- Set Objective state
line:Show()
line.label:Show()
end
end
-- Safety check in case we hit the linePool limit
if not line then
line = TrackerLinePool.GetLastLine()
end
-- Adds 2 pixels and "Padding Between Quests" setting in Tracker Options
line:SetHeight(line.label:GetHeight() + (Questie.db.profile.trackerQuestPadding + 2))
end
primaryButton = false
secondaryButton = false
end
end)
if not status then
Questie:Error("Tracker Error for quest " .. tostring(questId) .. ": " .. tostring(result))
elseif result == "BREAK" then
break
end
end
end
-- Begin populating the tracker with achievements
local _UpdateAchievements = function()
-- Begin populating the tracker with achievements
if Questie.IsWotlk or QuestieCompat.Is335 then
-- Begin populating the tracker with tracked achievements - Note: We're limited to tracking only 10 Achievements at a time.
-- For all intents and purposes at a code level we're going to treat each tracked Achievement the same way we treat and add Quests. This loop is
-- necessary to keep separate from the above tracked Quests loop so we can place all tracked Achievements into it's own "Zone" called Achievements.
-- This will force Achievements to always appear at the bottom of the tracker. Obviously it'll show at the top if there are no quests being tracked.
local firstAchieveInZone = false
local achieveId, achieveName, achieveDescription, achieveComplete, numCriteria, zoneName, achieve
for trackedId, _ in pairs(trackedAchievementIds) do
achieveId, achieveName, _, _, _, _, _, achieveDescription, _, _, _, _, achieveComplete, _, _ =
GetAchievementInfo(trackedId)
numCriteria = GetAchievementNumCriteria(trackedId)
zoneName = "Achievements"
achieve = {
Id = achieveId,
Name = achieveName,
Description = achieveDescription
}
if achieveId and (not achieveComplete) and trackedAchievementIds[achieveId] == true then
-- Add Achievement Zone
if zoneCheck ~= zoneName then
firstAchieveInZone = true
end
if firstAchieveInZone then
-- Get first line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then break end
-- Set Line Mode, Types, Clickers
line:SetMode("zone")
line:SetZone(zoneName)
line.expandQuest:Hide()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Setup Zone Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", 0, 0)
-- Set Zone Title and Min/Max states
if Questie.db.char.collapsedZones[zoneName] then
line.expandZone:SetMode(0)
local text = zoneName == "Achievements" and l10n("Achievements") or zoneName
line.label:SetText("|cFFC0C0C0" .. text .. " +|r")
else
line.expandZone:SetMode(1)
local text = zoneName == "Achievements" and l10n("Achievements") or zoneName
line.label:SetText("|cFFC0C0C0" .. text .. ": " .. GetNumTrackedAchievements(true) .. "/10|r")
end
-- Checks the minAllQuestsInZone[zone] table and if empty, zero out the table.
if Questie.db.char.minAllQuestsInZone[zoneName] ~= nil and not Questie.db.char.minAllQuestsInZone[zoneName].isTrue and not Questie.db.char.collapsedZones[zoneName] then
local minQuestIdCount = 0
for minQuestId, _ in pairs(Questie.db.char.minAllQuestsInZone[zoneName]) do
if type(minQuestId) == "number" then
minQuestIdCount = minQuestIdCount + 1
end
end
if minQuestIdCount == 0 then
Questie.db.char.minAllQuestsInZone[zoneName] = nil
end
end
-- Check and measure Zone Label text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() + trackerMarginLeft +
trackerMarginRight)
-- Set Zone Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - trackerMarginLeft - trackerMarginRight)
line:SetWidth(line.label:GetWidth())
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + trackerMarginLeft)
-- Setup Min/Max Button
line.expandZone:ClearAllPoints()
line.expandZone:SetPoint("TOPLEFT", line.label, "TOPLEFT", 0, 0)
line.expandZone:SetWidth(line.label:GetWidth())
line.expandZone:SetHeight(line.label:GetHeight())
line.expandZone:Show()
-- Adds 4 pixels between Zone and first Achievement Title
line:SetHeight(line.label:GetHeight() + 4)
-- Set Zone states
line:Show()
line.label:Show()
line.Quest = nil
line.Objective = nil
firstAchieveInZone = false
zoneCheck = zoneName
end
-- Add Achievements
if (not Questie.db.char.collapsedZones[zoneName]) then
-- Get next line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then break end
-- Set Line Mode, Types, Clickers
line:SetMode("achieve")
line:SetOnClick("achieve")
line:SetQuest(achieve)
line:SetObjective(nil)
line.expandZone:Hide()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Set Min/Max Button and default states
line.expandQuest:Show()
line.expandQuest:SetPoint("TOPRIGHT", line, "TOPLEFT", questMarginLeft - 8, 1)
line.expandQuest.zoneId = zoneName
-- The minAllQuestsInZone table is always blank until a player Shift+Clicks the Zone header (MouseDown).
-- QuestieTracker:Update is triggered and the table is then filled with all Achievement ID's in the same Zone.
if Questie.db.char.minAllQuestsInZone[zoneName] ~= nil and Questie.db.char.minAllQuestsInZone[zoneName].isTrue then
Questie.db.char.minAllQuestsInZone[zoneName][achieve.Id] = true
end
-- Handles all the Min/Max behavior individually for each Achievement.
if Questie.db.char.collapsedQuests[achieve.Id] then
line.expandQuest:SetMode(0)
else
line.expandQuest:SetMode(1)
end
-- Setup Achievement Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", questMarginLeft, 0)
-- Set Achievement Title
if Questie.db.profile.enableTooltipsQuestID then
line.label:SetText("|cFFFFFF00" .. achieve.Name .. " (" .. achieve.Id .. ")|r")
else
line.label:SetText("|cFFFFFF00" .. achieve.Name .. "|r")
end
-- Check and measure Achievement Label text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() + questMarginLeft +
trackerMarginRight)
-- Set Achievement Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - questMarginLeft - trackerMarginRight)
line:SetWidth(line.label:GetWidth() + questMarginLeft)
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + questMarginLeft)
-- Adds 4 pixels between Achievement Title and first Objective
line:SetHeight(line.label:GetHeight() + 4)
-- Set Achievement states
line:Show()
line.label:Show()
-- Add achievement Objective (if applicable)
if (not Questie.db.char.collapsedQuests[achieve.Id]) then
-- Achievements with no number criteria
if numCriteria == 0 then
-- Get next line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then break end
-- Set Line Mode, Types, Clickers
line:SetMode("objective")
line:SetOnClick("achieve")
line:SetQuest(achieve)
line:SetObjective("objective")
line.expandZone:Hide()
line.expandQuest:Hide()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Setup Objective Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", objectiveMarginLeft, 0)
-- Set Objective text
local objDesc = achieve.Description:gsub("%.", "")
line.label:SetText(QuestieLib:GetRGBForObjective({ Collected = 0, Needed = 1 }) ..
objDesc)
-- If the line width is less than the minimum Tracker width then don't wrap text
if line.label:GetStringWidth() + objectiveMarginLeft < trackerMinLineWidth then
-- Check and measure Objective text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() + objectiveMarginLeft +
trackerMarginRight)
-- Set Objective Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
line:SetWidth(line.label:GetWidth() + objectiveMarginLeft)
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + objectiveMarginLeft)
else
-- Set Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
line:SetWidth(GetWrappedWidth(line.label) + objectiveMarginLeft)
-- TextWrap Objective and set height
line.label:SetHeight(line.label:GetStringHeight() * GetNumLines(line.label))
line:SetHeight(line.label:GetHeight())
-- Compare trackerLineWidth, trackerMinLineWidth and the current label, then save the widest width
trackerLineWidth = math.max(trackerLineWidth, trackerMinLineWidth,
GetWrappedWidth(line.label) + objectiveMarginLeft)
end
-- Set Objective state
line:Show()
line.label:Show()
end
-- Achievements with number criteria
for objCriteria = 1, numCriteria do
local criteriaString, _, completed, quantityProgress, quantityNeeded, _, _, refId, quantityString =
GetAchievementCriteriaInfo(achieve.Id, objCriteria)
if ((Questie.db.profile.hideCompletedAchieveObjectives) and (not completed)) or (not Questie.db.profile.hideCompletedAchieveObjectives) then
-- Get next line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then break end
-- Set Line Mode, Types, Clickers
line:SetMode("objective")
line:SetOnClick("achieve")
-- Set correct Objective ID. Sometimes stand alone trackable Achievements are part of a group of Achievements under a parent Achievement.
local objId
if refId and select(2, GetAchievementInfo(refId)) == criteriaString and ((GetAchievementInfo(refId) and refId ~= 0) or (refId > 0 and (not QuestieDB.GetQuest(refId)))) then
objId = refId
else
objId = achieve
end
line:SetQuest(objId)
line:SetObjective("objective")
line.expandZone:Hide()
line.expandQuest:Hide()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Setup Objective Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", objectiveMarginLeft, 0)
-- Set Objective label based on state
if (criteriaString == "") then
criteriaString = achieve.Description
end
local objDesc = criteriaString:gsub("%.", "")
-- Set Objectives with more than one Objective number criteria
if not (completed or quantityNeeded == 1 or quantityProgress == quantityNeeded) then
if string.find(quantityString, "|") then
quantityString = quantityString:gsub("/%s?", "/")
else
quantityString = quantityProgress .. "/" .. quantityNeeded
end
local lineEnding = tostring(quantityString)
-- Set Objective text
line.label:SetText(QuestieLib:GetRGBForObjective({
Collected = quantityProgress,
Needed =
quantityNeeded
}) .. objDesc .. ": " .. lineEnding)
-- Check and measure Objective text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() +
objectiveMarginLeft + trackerMarginRight)
-- Set Label width
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
-- Split Objective description and Progress/Needed into seperate lines
if (trackerLineWidth < line.label:GetStringWidth() + objectiveMarginLeft) and (line.label:GetWidth() < line.label:GetStringWidth() + 5) then
-- Set Objective text
line.label:SetText(QuestieLib:GetRGBForObjective({
Collected =
quantityProgress,
Needed = quantityNeeded
}) .. objDesc .. ": ")
-- Check and measure Objective text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() +
objectiveMarginLeft + trackerMarginRight)
-- Set Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
line:SetWidth(line.label:GetWidth() + objectiveMarginLeft)
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + objectiveMarginLeft)
-- Adds 1 pixel between split Objectives
line:SetHeight(line.label:GetHeight() + 1)
-- Set Objective state
line:Show()
line.label:Show()
-- Get next line in linePool
line = TrackerLinePool.GetNextLine()
-- Safety check - make sure we didn't run over our linePool limit.
if not line then break end
-- Set Line Mode, Types, Clickers
line:SetMode("objective")
line:SetOnClick("achieve")
line:SetQuest(objId)
line:SetObjective("objective")
line.expandZone:Hide()
line.expandQuest:Hide()
line.criteriaMark:Hide()
line.playButton:Hide()
-- Set Objective Label
line.label:ClearAllPoints()
line.label:SetPoint("TOPLEFT", line, "TOPLEFT", objectiveMarginLeft, 0)
-- Set Objective text
line.label:SetText(QuestieLib:GetRGBForObjective({
Collected =
quantityProgress,
Needed = quantityNeeded
}) .. " > " .. lineEnding)
-- Check and measure Objective text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() +
objectiveMarginLeft + trackerMarginRight)
-- Set Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
line:SetWidth(line.label:GetWidth() + objectiveMarginLeft)
else
-- Set Line widths
line:SetWidth(line.label:GetWidth() + objectiveMarginLeft)
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + objectiveMarginLeft)
end
-- Set Objectives with a single Objective number criteria
else
-- Set Objective text
if completed then
line.label:SetText(QuestieLib:GetRGBForObjective({ Collected = 1, Needed = 1 }) ..
objDesc)
else
line.label:SetText(QuestieLib:GetRGBForObjective({ Collected = 0, Needed = 1 }) ..
objDesc)
end
-- Set Objective criteria mark
if not Questie.db.profile.hideCompletedAchieveObjectives and (not objectiveColor or objectiveColor == "white") then
line.criteriaMark:SetCriteria(completed)
if line.criteriaMark.mode == true then
line.criteriaMark:Show()
end
end
-- Check and measure Objective text width and update tracker width
QuestieTracker:UpdateWidth(line.label:GetStringWidth() +
objectiveMarginLeft + trackerMarginRight)
-- Set Objective Label and Line widths
line.label:SetWidth(trackerBaseFrame:GetWidth() - objectiveMarginLeft -
trackerMarginRight)
line:SetWidth(line.label:GetWidth() + objectiveMarginLeft)
-- Compare largest text Label in the tracker with current Label, then save widest width
trackerLineWidth = math.max(trackerLineWidth,
line.label:GetStringWidth() + objectiveMarginLeft)
end
-- Adds 1 pixel between multiple Objectives
line:SetHeight(line.label:GetHeight() + 1)
-- Set Objective state
line:Show()
line.label:Show()
end
end
end
-- Safety check in case we hit the linePool limit
if not line then
line = TrackerLinePool.GetLastLine()
end
-- Adds 2 pixels and "Padding Between Quests" setting in Tracker Options
line:SetHeight(line.label:GetHeight() + (Questie.db.profile.trackerQuestPadding + 2))
end
end
end
end
end
-- Populate Achievements first then Quests
if Questie.db.profile.listAchievementsFirst and (Questie.IsWotlk or QuestieCompat.Is335) then
_UpdateAchievements()
_UpdateQuests()
else
_UpdateQuests()
_UpdateAchievements()
end
-- Safety check in case we hit the linePool limit
if not line then
line = TrackerLinePool.GetLastLine()
end
-- Update tracker formatting
if line then
QuestieTracker:UpdateFormatting()
end
-- First run clean up
if isFirstRun then
trackerBaseFrame:Hide()
for questId, quest in pairs(QuestiePlayer.currentQuestlog) do
if quest then
if type(quest) ~= "table" then
quest = QuestieDB and QuestieDB.GetQuest and QuestieDB.GetQuest(questId) or nil
if quest then
QuestiePlayer.currentQuestlog[questId] = quest
end
end
if type(quest) == "table" then
if Questie.db.char.TrackerHiddenQuests[questId] then
quest.HideIcons = true
end
if Questie.db.char.TrackerFocus and type(Questie.db.char.TrackerFocus) == "number" and quest.Id and Questie.db.char.TrackerFocus == quest.Id then -- quest focus
TrackerUtils:FocusQuest(quest.Id)
end
if quest.Objectives then
for _, objective in pairs(quest.Objectives) do
if objective and objective.Index then
if Questie.db.char.TrackerHiddenObjectives[tostring(questId) .. " " .. tostring(objective.Index)] then
objective.HideIcons = true
end
if Questie.db.char.TrackerFocus and type(Questie.db.char.TrackerFocus) == "string" and quest.Id and Questie.db.char.TrackerFocus == tostring(quest.Id) .. " " .. tostring(objective.Index) then
TrackerUtils:FocusObjective(quest.Id, objective.Index)
end
end
end
end
if quest.SpecialObjectives then
for _, objective in pairs(quest.SpecialObjectives) do
if objective and objective.Index then
if Questie.db.char.TrackerHiddenObjectives[tostring(questId) .. " " .. tostring(objective.Index)] then
objective.HideIcons = true
end
if Questie.db.char.TrackerFocus and type(Questie.db.char.TrackerFocus) == "string" and quest.Id and Questie.db.char.TrackerFocus == tostring(quest.Id) .. " " .. tostring(objective.Index) then
TrackerUtils:FocusObjective(quest.Id, objective.Index)
end
end
end
end
end
end
end
isFirstRun = false
C_Timer.After(1.0, function()
QuestieCombatQueue:Queue(function()
allowFormattingUpdate = true
QuestieTracker:Update()
end)
end)
end
end
function QuestieTracker:UpdateFormatting()
if not allowFormattingUpdate then
return
end
if TrackerBaseFrame.isSizing == true or TrackerBaseFrame.isMoving == true or TrackerUtils.FilterProximityTimer == true then
Questie:Debug(Questie.DEBUG_SPAM, "[QuestieTracker:UpdateFormatting]")
else
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:UpdateFormatting]")
end
-- The Proximity Timer only pulses every 5 secs while running.
-- Flip back to false so we're not hiding other valid updates.
TrackerUtils.FilterProximityTimer = nil
-- Hide unused lines
TrackerLinePool.HideUnusedLines()
-- Hide unused item buttons
QuestieCombatQueue:Queue(function()
TrackerLinePool.HideUnusedButtons()
end)
-- This is responsible for handling the visibility of the Tracker
-- when nothing is tracked or when alwaysShowTracker is being used.
if (not QuestieTracker:HasQuest()) then
if Questie.db.profile.alwaysShowTracker then
trackerBaseFrame:Show()
else
trackerBaseFrame:Hide()
end
else
trackerBaseFrame:Show()
end
TrackerHeaderFrame:Update()
if TrackerLinePool.GetCurrentLine() and trackerLineWidth > 1 then
local trackerVarsCombined = trackerLineWidth + trackerMarginRight
TrackerLinePool.UpdateWrappedLineWidths(trackerLineWidth)
QuestieTracker:UpdateWidth(trackerVarsCombined)
QuestieTracker:UpdateHeight()
TrackerQuestFrame:Update()
end
TrackerBaseFrame:Update()
if Questie.db.profile.trackerHeaderEnabled or (Questie.db.profile.alwaysShowTracker and not QuestieTracker:HasQuest()) then
QuestieCompat.SetResizeBounds(trackerBaseFrame,
trackerHeaderFrame:GetWidth() + Questie.db.profile.trackerFontSizeHeader + 10,
trackerHeaderFrame:GetHeight() + Questie.db.profile.trackerFontSizeZone + 23)
else
QuestieCompat.SetResizeBounds(trackerBaseFrame,
(TrackerLinePool.GetFirstLine().label:GetStringWidth() + 40),
Questie.db.profile.trackerFontSizeZone + 22)
end
TrackerUtils:ShowVoiceOverPlayButtons()
TrackerUtils:UpdateVoiceOverPlayButtons()
end
function QuestieTracker:UpdateWidth(trackerVarsCombined)
local trackerWidthByManual = Questie.db.profile.TrackerWidth
local trackerHeaderFrameWidth = (trackerHeaderFrame:GetWidth() + Questie.db.profile.trackerFontSizeHeader + 10)
local trackerHeaderlessWidth = (TrackerLinePool.GetFirstLine().label:GetStringWidth() + 30)
if Questie.db.char.isTrackerExpanded then
if trackerWidthByManual > 0 then
-- Tracker Sizer is in Manual Mode
if (not TrackerBaseFrame.isSizing) then
-- Tracker is not being Sized | Manual width based on the width set by the Tracker Sizer
if trackerWidthByManual < trackerHeaderFrameWidth and (Questie.db.profile.trackerHeaderEnabled or (Questie.db.profile.alwaysShowTracker and not QuestieTracker:HasQuest())) then
trackerBaseFrame:SetWidth(trackerHeaderFrameWidth)
elseif trackerWidthByManual < trackerHeaderlessWidth then
trackerBaseFrame:SetWidth(trackerHeaderlessWidth)
else
trackerBaseFrame:SetWidth(trackerWidthByManual)
end
else
-- Tracker is being Sized | This will update the Tracker width while the Sizer is being used
trackerBaseFrame:SetWidth(trackerWidthByManual)
end
else
-- Tracker Sizer is in Auto Mode
if (trackerVarsCombined < trackerHeaderFrameWidth and (Questie.db.profile.trackerHeaderEnabled or (Questie.db.profile.alwaysShowTracker and not QuestieTracker:HasQuest()))) then
-- Apply headerFrameWidth
trackerBaseFrame:SetWidth(trackerHeaderFrameWidth)
else
-- Apply trackerVarsCombined width based on the maximum size of the largest line in the Tracker
trackerBaseFrame:SetWidth(trackerVarsCombined)
end
end
trackerQuestFrame:SetWidth(trackerBaseFrame:GetWidth())
trackerQuestFrame.ScrollChildFrame:SetWidth(trackerBaseFrame:GetWidth())
else
if Questie.db.profile.trackerHeaderEnabled or (Questie.db.profile.alwaysShowTracker and not QuestieTracker:HasQuest()) then
trackerBaseFrame:SetWidth(trackerHeaderFrameWidth)
trackerQuestFrame:SetWidth(trackerHeaderFrameWidth)
trackerQuestFrame.ScrollChildFrame:SetWidth(trackerHeaderFrameWidth)
end
end
end
function QuestieTracker:UpdateHeight()
local trackerHeaderFrameHeight = trackerHeaderFrame:GetHeight() + Questie.db.profile.trackerFontSizeZone + 23
local trackerHeightByRatio = GetScreenHeight() * Questie.db.profile.trackerHeightRatio
local trackerHeightByManual = Questie.db.profile.TrackerHeight
local trackerHeightCheck = trackerHeightByManual > 0 and trackerHeightByManual or trackerHeightByRatio
local trackerHeaderlessHeight = Questie.db.profile.trackerFontSizeZone + 22
if Questie.db.char.isTrackerExpanded then
-- Removes any padding from the last line in the tracker
TrackerLinePool.GetCurrentLine():SetHeight(TrackerLinePool.GetCurrentLine().label:GetStringHeight())
if TrackerLinePool.GetCurrentLine().mode == "zone" then
-- If a single zone is the only line in the tracker then don't add pixel padding
trackerQuestFrame.ScrollChildFrame:SetHeight((TrackerLinePool.GetFirstLine():GetTop() - TrackerLinePool.GetCurrentLine():GetBottom()))
else
-- Add 3 pixels to bottom of tracker to account for text that traverses beyond the GetStringHeight() function such as lower case "g".
trackerQuestFrame.ScrollChildFrame:SetHeight((TrackerLinePool.GetFirstLine():GetTop() - TrackerLinePool.GetCurrentLine():GetBottom() + 3))
end
-- Set the baseFrame to full height so we can measure it
trackerQuestFrame:SetHeight(trackerQuestFrame.ScrollChildFrame:GetHeight())
if Questie.db.profile.trackerHeaderEnabled or (Questie.db.profile.alwaysShowTracker and not QuestieTracker:HasQuest()) then
trackerBaseFrame:SetHeight(trackerQuestFrame:GetHeight() + trackerHeaderFrame:GetHeight() + 20)
else
trackerBaseFrame:SetHeight(trackerQuestFrame:GetHeight() + 20)
end
-- Use trackerHeightCheck (Sizer Manual or Auto) and set the heights
if (not TrackerBaseFrame.isSizing) then
-- Tracker is not being re-sized
if trackerBaseFrame:GetHeight() > trackerHeightCheck then
if trackerHeightCheck < trackerHeaderFrameHeight + 10 and (Questie.db.profile.trackerHeaderEnabled or (Questie.db.profile.alwaysShowTracker and not QuestieTracker:HasQuest())) then
trackerBaseFrame:SetHeight(trackerHeaderFrameHeight)
elseif trackerHeightCheck < trackerHeaderlessHeight then
trackerBaseFrame:SetHeight(trackerHeaderlessHeight)
else
trackerBaseFrame:SetHeight(trackerHeightCheck)
end
end
else
trackerBaseFrame:SetHeight(trackerHeightCheck)
end
-- Resize the questFrame to match the baseFrame after the trackerHeightCheck is applied
if Questie.db.profile.trackerHeaderEnabled or (Questie.db.profile.alwaysShowTracker and not QuestieTracker:HasQuest()) then
-- With Header Frame
trackerQuestFrame:SetHeight(trackerBaseFrame:GetHeight() - trackerHeaderFrame:GetHeight() - 20)
else
-- Without Header Frame
trackerQuestFrame:SetHeight(trackerBaseFrame:GetHeight() - 20)
end
else
trackerBaseFrame:SetHeight(trackerHeaderFrameHeight - 20)
trackerQuestFrame:SetHeight(trackerHeaderFrameHeight - 20)
trackerQuestFrame.ScrollChildFrame:SetHeight(trackerHeaderFrameHeight - 20)
end
end
function QuestieTracker:Unhook()
if (not QuestieTracker.alreadyHooked) then
return
end
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:Unhook]")
QuestieTracker.disableHooks = true
TrackerQuestTimers:ShowBlizzardTimer()
-- Quest Hooks
if QuestieTracker.IsQuestWatched then
IsQuestWatched = QuestieTracker.IsQuestWatched
GetNumQuestWatches = QuestieTracker.GetNumQuestWatches
end
-- Achievement Hooks
if Questie.IsWotlk or QuestieCompat.Is335 then
if QuestieTracker.IsTrackedAchievement then
IsTrackedAchievement = QuestieTracker.IsTrackedAchievement
GetNumTrackedAchievements = QuestieTracker.GetNumTrackedAchievements
end
end
QuestieTracker.alreadyHooked = nil
end
function QuestieTracker:HookBaseTracker()
if QuestieTracker.alreadyHooked then
return
end
QuestieTracker.disableHooks = nil
if not QuestieTracker.alreadyHookedSecure then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:HookBaseTracker] - Secure hooks")
-- Durability Frame hook
hooksecurefunc("UIParent_ManageFramePositions", QuestieTracker.UpdateDurabilityFrame)
-- QuestWatch secure hook
if AutoQuestWatch_Insert then
hooksecurefunc("AutoQuestWatch_Insert",
function(index, watchTimer) QuestieTracker:AQW_Insert(index, watchTimer) end)
end
hooksecurefunc("AddQuestWatch", function(index, watchTimer) QuestieTracker:AQW_Insert(index, watchTimer) end)
hooksecurefunc("RemoveQuestWatch", QuestieTracker.RemoveQuestWatch)
-- Achievement secure hooks
if Questie.IsWotlk or QuestieCompat.Is335 then
hooksecurefunc("AddTrackedAchievement", function(achieveId) QuestieTracker:TrackAchieve(achieveId) end)
hooksecurefunc("RemoveTrackedAchievement", QuestieTracker.RemoveTrackedAchievement)
end
QuestieTracker.alreadyHookedSecure = true
end
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:HookBaseTracker] - Non-secure hooks")
-- Quest Hooks
if not QuestieTracker.IsQuestWatched then
QuestieTracker.IsQuestWatched = IsQuestWatched
QuestieTracker.GetNumQuestWatches = GetNumQuestWatches
end
-- Intercept and return a Questie boolean value
IsQuestWatched = function(index)
local _, _, _, _, _, _, _, questId = GetQuestLogTitle(index)
if questId == 0 then
-- When an objective progresses in TBC "index" is the questId, but when a quest is manually added to the quest watch
-- (e.g. shift clicking it in the quest log) "index" is the questLogIndex.
questId = index
end
if not Questie.db.profile.autoTrackQuests then
return questId and Questie.db.char.TrackedQuests[questId]
else
return questId and QuestiePlayer.currentQuestlog[questId] and (not Questie.db.char.AutoUntrackedQuests[questId])
end
end
-- Intercept and return only what Questie is tracking
GetNumQuestWatches = function(isQuestie)
local activeQuests = 0
if isQuestie and Questie.db.profile.autoTrackQuests and Questie.db.char.AutoUntrackedQuests then
local autoUnTrackedQuests = 0
for _ in pairs(Questie.db.char.AutoUntrackedQuests) do
autoUnTrackedQuests = autoUnTrackedQuests + 1
end
return select(2, GetNumQuestLogEntries()) - autoUnTrackedQuests
elseif isQuestie and Questie.db.char.TrackedQuests then
local autoTrackedQuests = 0
for _ in pairs(Questie.db.char.TrackedQuests) do
autoTrackedQuests = autoTrackedQuests + 1
end
return autoTrackedQuests
else
return 0
end
end
-- Achievement Hooks
if Questie.IsWotlk or QuestieCompat.Is335 then
if not QuestieTracker.IsTrackedAchievement then
QuestieTracker.IsTrackedAchievement = IsTrackedAchievement
QuestieTracker.GetNumTrackedAchievements = GetNumTrackedAchievements
end
-- Intercept and return a Questie boolean value
IsTrackedAchievement = function(achieveId)
if Questie.db.char.trackedAchievementIds[achieveId] then
return achieveId and Questie.db.char.trackedAchievementIds[achieveId]
else
return false
end
end
-- Intercept and return only what Questie is tracking
GetNumTrackedAchievements = function(isQuestie)
if isQuestie and Questie.db.char.trackedAchievementIds then
local numTrackedAchievements = 0
for _ in pairs(Questie.db.char.trackedAchievementIds) do
numTrackedAchievements = numTrackedAchievements + 1
end
return numTrackedAchievements
else
return 0
end
end
end
if Questie.db.profile.showBlizzardQuestTimer then
TrackerQuestTimers:ShowBlizzardTimer()
else
TrackerQuestTimers:HideBlizzardTimer()
end
QuestieTracker.alreadyHooked = true
QuestieCombatQueue:Queue(function()
QuestieTracker:Update()
end)
end
function QuestieTracker:RemoveQuest(questId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:RemoveQuest] - ", questId)
-- Clean up any fallback quest object we built for this quest
if TrackerUtils._fallbackQuests then
TrackerUtils._fallbackQuests[questId] = nil
end
if Questie.db.char.collapsedQuests then
Questie.db.char.collapsedQuests[questId] = nil
end
-- Let's remove the Quest from the Tracker tables just in case...
if Questie.db.char.AutoUntrackedQuests[questId] then
Questie.db.char.AutoUntrackedQuests[questId] = nil
elseif Questie.db.char.TrackedQuests[questId] then
Questie.db.char.TrackedQuests[questId] = nil
end
if Questie.db.char.TrackerFocus then
if (type(Questie.db.char.TrackerFocus) == "number" and Questie.db.char.TrackerFocus == questId)
or (type(Questie.db.char.TrackerFocus) == "string" and Questie.db.char.TrackerFocus:sub(1, #tostring(questId)) == tostring(questId)) then
TrackerUtils:UnFocus()
QuestieQuest:ToggleNotes(true)
end
end
end
function QuestieTracker.RemoveQuestWatch(index, isQuestie)
if QuestieTracker.disableHooks then
return
end
if not isQuestie then
if index then
local _, _, _, isHeader, _, _, _, questId = GetQuestLogTitle(index)
if isHeader then
return
end
if (not questId) or questId == 0 then
-- Maybe index was already a questId (Ascension/Ebonhold extension)
if index > 0 and (GetQuestLogIndexByID and GetQuestLogIndexByID(index) > 0) then
questId = index
else
return
end
end
if questId and questId > 0 then
local stack = debugstack(2, 5, 0)
local isUserAction = stack:find("QuestLog") or stack:find("OnClick") or stack:find("QuestWatch") or stack:find("TrackerLinePool") or stack:find("TrackerMenu") or IsShiftKeyDown()
if not isUserAction then
-- Internal update: Only untrack if we are NOT in auto-track mode
if not Questie.db.profile.autoTrackQuests then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker.RemoveQuestWatch] - Internal update (untrack):", questId)
QuestieTracker:UntrackQuestId(questId)
end
else
-- User triggered untrack: Explicitly follow it.
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker.RemoveQuestWatch] - User action (untrack):", questId)
QuestieTracker:UntrackQuestId(questId)
end
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker.RemoveQuestWatch] - by Blizzard", questId)
end
end
else
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker.RemoveQuestWatch] - by Questie")
end
end
function QuestieTracker:UntrackQuestId(questId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:UntrackQuestId] - ", questId)
if Questie.db.profile.autoTrackQuests then
-- In auto-track mode, mark the quest as explicitly hidden
Questie.db.char.AutoUntrackedQuests[questId] = true
Questie.db.char.TrackedQuests[questId] = nil
else
-- In manual-track mode, remove from tracked list
Questie.db.char.TrackedQuests[questId] = nil
Questie.db.char.AutoUntrackedQuests[questId] = nil
end
if Questie.db.profile.hideUntrackedQuestsMapIcons then
-- Hides objective icons for untracked quests.
QuestieQuest:ToggleNotes(false)
-- Removes objective tooltips for untracked quests.
QuestieTooltips:RemoveQuest(questId)
end
QuestieCombatQueue:Queue(function()
QuestieTracker:Update()
end)
end
function QuestieTracker:AQW_Insert(index, expire)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:AQW_Insert]")
if (not Questie.db.profile.trackerEnabled) or (index == 0) or (index == nil) then
return
end
-- This prevents double calling this function
local now = GetTime()
if index and index == QuestieTracker.last_aqw and (now - lastAQW) < 0.05 then
return
end
lastAQW = now
QuestieTracker.last_aqw = index
-- This removes quests from the Blizzard QuestWatchFrame so when the option "Show Blizzard Timer" is enabled,
-- that is all the player will see. This also prevents hitting the Blizzard Quest Watch Limit.
RemoveQuestWatch(index, true)
local _, _, _, isHeader, _, _, _, questId = GetQuestLogTitle(index)
if isHeader then return end
if (not questId) or questId == 0 then
-- When an objective progresses in TBC "index" is the questId, but when a quest is manually added to the quest watch
-- (e.g. shift clicking it in the quest log) "index" is the questLogIndex.
if index and index > 50 and GetQuestLogIndexByID and GetQuestLogIndexByID(index) > 0 then
questId = index
else
return
end
end
if questId and questId > 0 then
-- Check if this was a manual user action (Shift-Click in Quest Log or similar)
local stack = debugstack(2, 8, 0)
local isUserAction = stack:find("QuestLog") or stack:find("OnClick") or stack:find("QuestWatch") or stack:find("Toggle") or stack:find("TrackerLinePool") or stack:find("TrackerMenu") or IsShiftKeyDown()
-- These checks makes sure the only way to track a quest is through the Blizzard Quest Log
-- or another Addon hooked into the Blizzard Quest Log that replaces the default Quest Log.
if not Questie.db.profile.autoTrackQuests then
if isUserAction then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:AQW_Insert] - Manual mode, user re-track:", questId)
Questie.db.char.TrackedQuests[questId] = true
Questie.db.char.AutoUntrackedQuests[questId] = nil
else
-- In manual mode, we normally only track on user action, but if Blizzard calls it,
-- it might be a re-sync of something the user HAD tracked.
if Questie.db.char.TrackedQuests[questId] then
-- Keep it tracked
else
-- Blizzard auto-tracked something we didn't ask for? Ignore it in Manual mode.
return
end
end
else
if isUserAction then
-- Auto track mode: User manually tracked it (un-hiding)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:AQW_Insert] - Auto mode, user un-hide:", questId)
Questie.db.char.AutoUntrackedQuests[questId] = nil
elseif Questie.db.char.AutoUntrackedQuests[questId] then
-- Auto track mode: Internal update for a HIDDEN quest — respect the hidden state!
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:AQW_Insert] - Auto mode, skipping hidden quest:", questId)
return
end
end
local quest = QuestieDB.GetQuest(questId)
if quest then
-- Make sure quests or zones (re)added to the tracker isn't in a minimized state
local zoneId = quest.zoneOrSort
if Questie.db.char.collapsedQuests[questId] == true then
Questie.db.char.collapsedQuests[questId] = nil
end
if Questie.db.char.collapsedZones[zoneId] == true then
Questie.db.char.collapsedZones[zoneId] = nil
end
-- Unhide quest icons when retracking quests.
if Questie.db.profile.hideUntrackedQuestsMapIcons then
-- Shows objective icons for tracked quests.
QuestieQuest:ToggleNotes(true)
-- Readd objective tooltips for tracked quests.
QuestieQuest:PopulateObjectiveNotes(quest)
end
else
if Questie.IsSoD then
QuestieDebugOffer.QuestTracking(questId)
else
-- Quest not in DB — try building a fallback object from the quest log.
-- Store in TrackerUtils._fallbackQuests only, NOT currentQuestlog,
-- so QuestieArrow/QuestieQuest don't call DB-only methods on it.
local fallback = TrackerUtils:BuildFallbackQuest(questId)
if fallback then
TrackerUtils._fallbackQuests[questId] = fallback
QuestieCombatQueue:Queue(function()
QuestieTracker:Update()
end)
else
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker] Quest " .. tostring(questId) .. " not in DB and not in quest log, skipping")
end
end
end
end
QuestieCombatQueue:Queue(function()
QuestieTracker:Update()
end)
end
QuestieTracker.RemoveTrackedAchievement = function(achieveId, isQuestie)
if QuestieTracker.disableHooks then
return
end
if not isQuestie then
if achieveId then
QuestieTracker:UntrackAchieveId(achieveId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker.RemoveTrackedAchievement] - by Blizzard")
end
else
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker.RemoveTrackedAchievement] - by Questie")
end
end
function QuestieTracker:UpdateAchieveTrackerCache(achieveId)
-- Since we're essentially adding & force removing an achievement from the QuestWatch frame while we add an achievement to the Questie Tracker, the event this
-- function is called from, TRACKED_ACHIEVEMENT_LIST_CHANGED, fires twice. When we remove an achievement from the Questie Tracker the event still fires twice
-- because the Blizzard function responsible for this is essentially a "toggle". It quickly re-adds the achievement to the QuestWatch frame and then removes it.
-- So, again this event again fires twice. We only need to allow this to run once and it often fires before the Questie.db.char.trackedAchievementIds table is
-- updated so we're going to throttle this 1/10th of a second.
if Questie.db.profile.trackerEnabled then
if achieveId then
C_Timer.After(0.1, function()
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:UpdateAchieveTrackerCache] - ", achieveId)
if (not Questie.db.profile.trackerEnabled) or (achieveId == 0) then
return
end
-- Look for changes in the Saved VAR and update the achievement cache
if Questie.db.char.trackedAchievementIds[achieveId] ~= trackedAchievementIds[achieveId] then
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:UpdateAchieveTrackerCache] - Change Detected!")
trackedAchievementIds[achieveId] = Questie.db.char.trackedAchievementIds[achieveId]
QuestieCombatQueue:Queue(function()
C_Timer.After(0.1, function()
QuestieTracker:Update()
end)
end)
else
Questie:Debug(Questie.DEBUG_DEVELOP,
"[QuestieTracker:UpdateAchieveTrackerCache] - No Change Detected!")
end
end)
end
end
end
function QuestieTracker:UntrackAchieveId(achieveId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:UntrackAchieve] - ", achieveId)
if Questie.db.char.trackedAchievementIds[achieveId] then
Questie.db.char.trackedAchievementIds[achieveId] = nil
end
end
function QuestieTracker:TrackAchieve(achieveId)
Questie:Debug(Questie.DEBUG_DEVELOP, "[QuestieTracker:TrackAchieve] - ", achieveId)
if (not Questie.db.profile.trackerEnabled) or (achieveId == 0) then
return
end
-- If an achievement is already tracked in the Achievement UI then untrack it (Mimicks a Toggle effect).
if Questie.db.char.trackedAchievementIds[achieveId] then
QuestieTracker:UntrackAchieveId(achieveId)
RemoveTrackedAchievement(achieveId, true)
return
end
-- Prevents tracking more than 10 Achievements
if (GetNumTrackedAchievements(true) == 10) then
RemoveTrackedAchievement(achieveId, true)
UIErrorsFrame:AddMessage(format(l10n("You may only track 10 achievements at a time."), 10), 1.0, 0.1, 0.1, 1.0)
return
end
-- This prevents double calling this function
local now = GetTime()
if achieveId and achieveId == QuestieTracker.last_achieveId and (now - lastAchieveId) < 0.1 then
return
end
lastAchieveId = now
QuestieTracker.last_achieveId = achieveId
-- This removes achievements from the Blizzard QuestWatchFrame so when the
-- option "Show Blizzard Timer" is enabled, that is all the player will see.
RemoveTrackedAchievement(achieveId, true)
if achieveId > 0 then
-- This handles the Track check box in the Achievement UI
local mouseFocus
local frameMatch
-- Krowi isn't using this check box for their Achievement frame
if not IsAddOnLoaded("Krowi_AchievementFilter") then
mouseFocus = GetMouseFocus():GetName()
frameMatch = strmatch(mouseFocus, "(AchievementFrameAchievementsContainerButton%dTracked.*)")
end
-- Upon first login or reloadui, this frame isn't loaded
if (not AchievementFrame) then
AchievementFrame_LoadUI()
end
-- This check makes sure the only way to track an achieve is through the Blizzard Achievement UI
if Questie.db.char.trackedAchievementIds[achieveId] then
Questie.db.char.trackedAchievementIds[achieveId] = nil
elseif IsShiftKeyDown() and AchievementFrame:IsShown() then
Questie.db.char.trackedAchievementIds[achieveId] = true
elseif AchievementFrame:IsShown() and (mouseFocus == frameMatch) then
Questie.db.char.trackedAchievementIds[achieveId] = true
end
-- Forces the achievement out of a minimized state
if Questie.db.char.collapsedQuests[achieveId] == true then
Questie.db.char.collapsedQuests[achieveId] = nil
end
-- Forces the 'Achievement Zone' out of a minimized state
if Questie.db.char.collapsedZones["Achievements"] == true then
Questie.db.char.collapsedZones["Achievements"] = nil
end
end
end