de270a0e8e
Ascension's special quests are finished by the server the moment their objectives are met, and one of them stayed in the tracker for the rest of the session: currentQuestlog is only as good as the removal events that maintain it, and nothing landed for that quest. The tracker now checks the quest log itself, which it already walks once per draw for the zone headers, and skips anything that is no longer in it, dropping the fallback object built for it as well. Skipped rather than pruned, so a redraw that catches the log mid-refresh does not throw away state Questie is about to want back. Three things on the event side that let it get that far: - CleanupRemovedQuestsFallback now also runs after a full quest log scan. UpdateAllQuests only inspects quests still in the log, so for as long as something kept asking for full scans, nothing looked for removals. - A removed quest counts as completed when the server has it flagged complete. QuestieDB.IsComplete cannot answer for a quest the database has never heard of, so these were filed as abandoned instead. - The removal scan type-checks the quest id before comparing it, so a stray string key cannot error out the pass for every other quest.
1749 lines
69 KiB
Lua
1749 lines
69 KiB
Lua
---@class TrackerUtils
|
|
local TrackerUtils = QuestieLoader:ImportModule("TrackerUtils")
|
|
-------------------------
|
|
--Import QuestieTracker modules.
|
|
-------------------------
|
|
---@type QuestieTracker
|
|
local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker")
|
|
---@type TrackerLinePool
|
|
local TrackerLinePool = QuestieLoader:ImportModule("TrackerLinePool")
|
|
---@type TrackerFadeTicker
|
|
local TrackerFadeTicker = QuestieLoader:ImportModule("TrackerFadeTicker")
|
|
---@type QuestieCombatQueue
|
|
local QuestieCombatQueue = QuestieLoader:ImportModule("QuestieCombatQueue")
|
|
-------------------------
|
|
--Import Questie modules.
|
|
-------------------------
|
|
---@type QuestiePlayer
|
|
local QuestiePlayer = QuestieLoader:ImportModule("QuestiePlayer")
|
|
---@type QuestieDB
|
|
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
|
|
---@type QuestieMap
|
|
local QuestieMap = QuestieLoader:ImportModule("QuestieMap")
|
|
---@type QuestieCoords
|
|
local QuestieCoords = QuestieLoader:ImportModule("QuestieCoords")
|
|
---@type ZoneDB
|
|
local ZoneDB = QuestieLoader:ImportModule("ZoneDB")
|
|
---@type QuestieArrow
|
|
local QuestieArrow = QuestieLoader:ImportModule("QuestieArrow")
|
|
---@type l10n
|
|
local l10n = QuestieLoader:ImportModule("l10n")
|
|
|
|
--- COMPATIBILITY ---
|
|
local C_Timer = QuestieCompat.C_Timer
|
|
local C_Map = QuestieCompat.C_Map
|
|
local GetQuestLogTitle = QuestieCompat.GetQuestLogTitle
|
|
local GetQuestLogIndexByID = QuestieCompat.GetQuestLogIndexByID
|
|
local WorldMapFrame = QuestieCompat.WorldMapFrame
|
|
|
|
local tinsert = table.insert
|
|
|
|
local objectiveFlashTicker
|
|
local zoneCache = {}
|
|
local questProximityTimer
|
|
local questZoneProximityTimer
|
|
local bindTruthTable = {
|
|
['left'] = function(button)
|
|
return "LeftButton" == button
|
|
end,
|
|
['right'] = function(button)
|
|
return "RightButton" == button
|
|
end,
|
|
['shiftleft'] = function(button)
|
|
return "LeftButton" == button and IsShiftKeyDown()
|
|
end,
|
|
['shiftright'] = function(button)
|
|
return "RightButton" == button and IsShiftKeyDown()
|
|
end,
|
|
['ctrlleft'] = function(button)
|
|
return "LeftButton" == button and IsControlKeyDown()
|
|
end,
|
|
['ctrlright'] = function(button)
|
|
return "RightButton" == button and IsControlKeyDown()
|
|
end,
|
|
['altleft'] = function(button)
|
|
return "LeftButton" == button and IsAltKeyDown()
|
|
end,
|
|
['altright'] = function(button)
|
|
return "RightButton" == button and IsAltKeyDown()
|
|
end,
|
|
['disabled'] = function() return false end,
|
|
}
|
|
|
|
local _QuestLogScrollBar = QuestLogScrollFrameScrollBar or QuestLogListScrollFrame.ScrollBar or
|
|
QuestLogListScrollFrameScrollBar
|
|
|
|
---@param quest table The table provided by QuestieDB.GetQuest(questId)
|
|
function TrackerUtils:ShowQuestLog(quest)
|
|
local questFrame = QuestLogExFrame or ClassicQuestLog or QuestLogFrame
|
|
|
|
local questLogIndex = (GetQuestLogIndexByID and GetQuestLogIndexByID(quest.Id)) or 0
|
|
if not questLogIndex or questLogIndex <= 0 then
|
|
-- Quest is not in the Quest Log (completed/auto-completed and then disappeared)
|
|
return
|
|
end
|
|
|
|
SelectQuestLogEntry(questLogIndex)
|
|
|
|
-- Scroll to the quest in the quest log (only if scrolling is available)
|
|
if _QuestLogScrollBar and _QuestLogScrollBar.GetValueStep then
|
|
local scrollSteps = _QuestLogScrollBar:GetValueStep() or 0
|
|
if scrollSteps > 0 then
|
|
_QuestLogScrollBar:SetValue(questLogIndex * scrollSteps - scrollSteps * 3)
|
|
end
|
|
end
|
|
|
|
if not questFrame:IsShown() then
|
|
if not InCombatLockdown() then
|
|
ShowUIPanel(questFrame)
|
|
if (QuestLogEx) then
|
|
QuestLogEx:Maximize()
|
|
end
|
|
else
|
|
Questie:Print(l10n("Can't open Quest Log while in combat. Open it manually."))
|
|
end
|
|
end
|
|
|
|
QuestLog_UpdateQuestDetails()
|
|
QuestLog_Update()
|
|
end
|
|
|
|
---@param title string The name of the WayPoint
|
|
---@param zone number The zone ID number
|
|
---@param x number X coordinate
|
|
---@param y number Y coordinate
|
|
function TrackerUtils:SetTomTomTarget(title, zone, x, y)
|
|
if QuestieArrow and QuestieArrow.SetTarget and title and zone and x and y then
|
|
QuestieArrow:SetTarget(title, zone, x, y)
|
|
end
|
|
|
|
if TomTom and TomTom.AddWaypoint then
|
|
if Questie.db.char._tom_waypoint and TomTom.RemoveWaypoint then -- remove old waypoint
|
|
TomTom:RemoveWaypoint(Questie.db.char._tom_waypoint)
|
|
end
|
|
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
|
|
|
|
if QuestieCompat.Is335 then
|
|
Questie.db.char._tom_waypoint = QuestieCompat.TomTom_AddWaypoint(title, uiMapId, x, y)
|
|
else
|
|
Questie.db.char._tom_waypoint = TomTom:AddWaypoint(uiMapId, x / 100, y / 100, { title = title, crazy = true })
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param objective table The table provided by QuestieDB.GetQuest(questId).Objectives[objective]
|
|
function TrackerUtils:ShowObjectiveOnMap(objective)
|
|
local spawn, zone = QuestieMap:GetNearestSpawn(objective)
|
|
if spawn then
|
|
WorldMapFrame:Show()
|
|
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
|
|
WorldMapFrame:SetMapID(uiMapId)
|
|
TrackerUtils:FlashObjective(objective)
|
|
end
|
|
end
|
|
|
|
---@param quest table The table provided by QuestieDB.GetQuest(questId)
|
|
function TrackerUtils:ShowFinisherOnMap(quest)
|
|
local spawn, zone = QuestieMap:GetNearestQuestSpawn(quest)
|
|
if spawn then
|
|
WorldMapFrame:Show()
|
|
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
|
|
WorldMapFrame:SetMapID(uiMapId)
|
|
TrackerUtils:FlashFinisher(quest)
|
|
end
|
|
end
|
|
|
|
---@param objective table The table provided by QuestieDB.GetQuest(questId).Objectives[objective]
|
|
function TrackerUtils:FlashObjective(objective)
|
|
if next(objective.AlreadySpawned) then
|
|
local toFlash = {}
|
|
-- ugly code
|
|
for _, framelist in pairs(QuestieMap.questIdFrames) do
|
|
for _, frameName in pairs(framelist) do
|
|
local icon = _G[frameName]
|
|
if not icon.miniMapIcon then
|
|
-- todo: move into frame.session
|
|
if icon:IsShown() then
|
|
icon._hidden_by_flash = true
|
|
icon:Hide()
|
|
if icon.data.lineFrames then
|
|
for _, line in pairs(icon.data.lineFrames) do
|
|
if line:IsShown() then
|
|
line._hidden_by_flash = true
|
|
line:Hide()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
for _, spawn in pairs(objective.AlreadySpawned) do
|
|
if spawn.mapRefs then
|
|
for _, frame in pairs(spawn.mapRefs) do
|
|
tinsert(toFlash, frame)
|
|
if frame._hidden_by_flash then
|
|
frame:Show()
|
|
end
|
|
|
|
-- todo: move into frame.session
|
|
frame._hidden_by_flash = nil
|
|
frame._size = frame:GetWidth()
|
|
end
|
|
end
|
|
end
|
|
local flashW = 1
|
|
local flashB = true
|
|
local flashDone = 0
|
|
objectiveFlashTicker = C_Timer.NewTicker(0.1, function()
|
|
for _, frame in pairs(toFlash) do
|
|
frame:SetWidth(frame._size + flashW)
|
|
frame:SetHeight(frame._size + flashW)
|
|
end
|
|
if flashB then
|
|
if flashW < 10 then
|
|
flashW = flashW + (16 - flashW) / 2 + 0.06
|
|
if flashW >= 9.5 then
|
|
flashB = false
|
|
end
|
|
end
|
|
else
|
|
if flashW > 0 then
|
|
flashW = flashW - 2
|
|
--flashW = (flashW + (-flashW) / 3) - 0.06
|
|
if flashW < 1 then
|
|
--flashW = 0
|
|
flashB = true
|
|
-- ugly code
|
|
if flashDone > 0 then
|
|
C_Timer.After(0.1, function()
|
|
objectiveFlashTicker:Cancel()
|
|
for _, frame in pairs(toFlash) do
|
|
frame:SetWidth(frame._size)
|
|
frame:SetHeight(frame._size)
|
|
frame._size = nil
|
|
end
|
|
end)
|
|
C_Timer.After(0.5, function()
|
|
for _, framelist in pairs(QuestieMap.questIdFrames) do
|
|
for _, frameName in pairs(framelist) do
|
|
local icon = _G[frameName]
|
|
if icon._hidden_by_flash then
|
|
icon._hidden_by_flash = nil
|
|
icon:Show()
|
|
if icon.data.lineFrames then
|
|
for _, line in pairs(icon.data.lineFrames) do
|
|
if line._hidden_by_flash then
|
|
line._hidden_by_flash = nil
|
|
line:Show()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
flashDone = flashDone + 1
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
---@param quest table The table provided by QuestieDB.GetQuest(questId)
|
|
function TrackerUtils:FlashFinisher(quest)
|
|
local toFlash = {}
|
|
-- ugly code
|
|
for questId, framelist in pairs(QuestieMap.questIdFrames) do
|
|
if questId ~= quest.Id then
|
|
for _, frameName in pairs(framelist) do
|
|
local icon = _G[frameName]
|
|
if not icon.miniMapIcon then
|
|
-- todo: move into frame.session
|
|
if icon:IsShown() then
|
|
icon._hidden_by_flash = true
|
|
icon:Hide()
|
|
if icon.data.lineFrames then
|
|
for _, line in pairs(icon.data.lineFrames) do
|
|
if line:IsShown() then
|
|
line._hidden_by_flash = true
|
|
line:Hide()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
else
|
|
for _, frameName in pairs(framelist) do
|
|
local icon = _G[frameName]
|
|
if not icon.miniMapIcon then
|
|
icon._size = icon:GetWidth()
|
|
tinsert(toFlash, icon)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local flashW = 1
|
|
local flashB = true
|
|
local flashDone = 0
|
|
objectiveFlashTicker = C_Timer.NewTicker(0.1, function()
|
|
for _, frame in pairs(toFlash) do
|
|
frame:SetWidth(frame._size + flashW)
|
|
frame:SetHeight(frame._size + flashW)
|
|
end
|
|
if flashB then
|
|
if flashW < 10 then
|
|
flashW = flashW + (16 - flashW) / 2 + 0.06
|
|
if flashW >= 9.5 then
|
|
flashB = false
|
|
end
|
|
end
|
|
else
|
|
if flashW > 0 then
|
|
flashW = flashW - 2
|
|
--flashW = (flashW + (-flashW) / 3) - 0.06
|
|
if flashW < 1 then
|
|
--flashW = 0
|
|
flashB = true
|
|
-- ugly code
|
|
if flashDone > 0 then
|
|
C_Timer.After(0.1, function()
|
|
objectiveFlashTicker:Cancel()
|
|
for _, frame in pairs(toFlash) do
|
|
frame:SetWidth(frame._size)
|
|
frame:SetHeight(frame._size)
|
|
frame._size = nil
|
|
end
|
|
end)
|
|
C_Timer.After(0.5, function()
|
|
for _, framelist in pairs(QuestieMap.questIdFrames) do
|
|
for _, frameName in pairs(framelist) do
|
|
local icon = _G[frameName]
|
|
if icon._hidden_by_flash then
|
|
icon._hidden_by_flash = nil
|
|
icon:Show()
|
|
if icon.data.lineFrames then
|
|
for _, line in pairs(icon.data.lineFrames) do
|
|
if line._hidden_by_flash then
|
|
line._hidden_by_flash = nil
|
|
line:Show()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
flashDone = flashDone + 1
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
---@param bind string
|
|
---@param button string
|
|
---@return string bind The input keybind string
|
|
---@return string button The input button string
|
|
---@return string bindTruthTable.bind Returns matched bind string
|
|
---@return function|boolean bindTruthTable.bind.button Returns button function or false if there is no keybind set
|
|
function TrackerUtils:IsBindTrue(bind, button)
|
|
return bind and button and bindTruthTable[bind] and bindTruthTable[bind](button)
|
|
end
|
|
|
|
---@param itemId number
|
|
---@return boolean
|
|
function TrackerUtils:IsQuestItemUsable(itemId)
|
|
if itemId and (GetItemSpell(itemId) or IsEquippableItem(itemId)) then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
---@param quest table Quest Table
|
|
---@return string|nil completionText Quest Completion text string or nil
|
|
function TrackerUtils:GetCompletionText(quest)
|
|
local completionText
|
|
if GetQuestLogCompletionText then
|
|
local questIndex = GetQuestLogIndexByID(quest.Id)
|
|
completionText = GetQuestLogCompletionText(questIndex)
|
|
end
|
|
|
|
if completionText then
|
|
return completionText
|
|
elseif quest.Description and quest.Description[1] then
|
|
return quest.Description[1]:gsub("%.", "")
|
|
else
|
|
-- Fallback/custom quests (e.g. server quests not in QuestieDB) have no
|
|
-- top-level Description array. Without this, indexing quest.Description[1]
|
|
-- throws and aborts the whole tracker render for that quest via the
|
|
-- caller's pcall, freezing its line on the last successful render.
|
|
return nil
|
|
end
|
|
end
|
|
|
|
---@param zoneId number Zone ID number
|
|
---@return string @Zone Name (Localized) or "Unknown Zone"
|
|
local function GetZoneNameByIDFallback(zoneId)
|
|
if zoneCache[zoneId] then
|
|
return zoneCache[zoneId]
|
|
end
|
|
|
|
if zoneId <= 0 or type(zoneId) ~= "number" then
|
|
return "Unknown Zone"
|
|
end
|
|
|
|
for _, zone in pairs(l10n.zoneLookup) do
|
|
if zone[zoneId] then
|
|
zoneCache[zoneId] = zone[zoneId]
|
|
return zoneCache[zoneId]
|
|
end
|
|
end
|
|
|
|
-- Ascension can use custom UiMapIds for zones/sub-zones (e.g. 1238 Northshire Valley).
|
|
-- Those won't exist in l10n.zoneLookup (which is AreaId-based), so fallback to UiMapData / mapInfo.
|
|
local uiMapData = QuestieCompat and QuestieCompat.UiMapData and QuestieCompat.UiMapData[zoneId]
|
|
if uiMapData and uiMapData.name then
|
|
zoneCache[zoneId] = uiMapData.name
|
|
return zoneCache[zoneId]
|
|
end
|
|
if C_Map and C_Map.GetMapInfo then
|
|
local mapInfo = C_Map.GetMapInfo(zoneId)
|
|
if mapInfo and mapInfo.name then
|
|
zoneCache[zoneId] = mapInfo.name
|
|
return zoneCache[zoneId]
|
|
end
|
|
end
|
|
|
|
Questie:Debug(Questie.DEBUG_CRITICAL, "[GetZoneNameByIDFallback]: Unable to find a zone name for zoneId", zoneId)
|
|
|
|
return "Unknown Zone"
|
|
end
|
|
|
|
---@param zoneId number Zone ID number
|
|
---@return string @Zone Name (Localized)
|
|
function TrackerUtils:GetZoneNameByID(zoneId)
|
|
if zoneCache[zoneId] then
|
|
return zoneCache[zoneId]
|
|
end
|
|
|
|
if C_Map and C_Map.GetAreaInfo(zoneId) then
|
|
zoneCache[zoneId] = C_Map.GetAreaInfo(zoneId)
|
|
else
|
|
zoneCache[zoneId] = GetZoneNameByIDFallback(zoneId)
|
|
end
|
|
|
|
return zoneCache[zoneId]
|
|
end
|
|
|
|
---@param catId number Catagory ID number
|
|
---@return string CatagoryName Catagory Name (Localized) or "Unknown Category"
|
|
function TrackerUtils:GetCategoryNameByID(catId)
|
|
if zoneCache[catId] then
|
|
return zoneCache[catId]
|
|
end
|
|
|
|
if type(catId) == "number" and catId < 0 and type(l10n.questCategoryLookup[catId]) == "string" then
|
|
zoneCache[catId] = l10n.questCategoryLookup[catId]
|
|
return zoneCache[catId]
|
|
end
|
|
|
|
return "Unknown Category"
|
|
end
|
|
|
|
function TrackerUtils:UnFocus()
|
|
-- reset HideIcons to match savedvariable state
|
|
if (not Questie.db.char.TrackerFocus) then
|
|
return
|
|
end
|
|
for questId in pairs(QuestiePlayer.currentQuestlog) do
|
|
local quest = QuestieDB.GetQuest(questId)
|
|
|
|
if quest then
|
|
quest.FadeIcons = nil
|
|
if next(quest.Objectives) then
|
|
if Questie.db.char.TrackerHiddenQuests[quest.Id] then
|
|
quest.HideIcons = true
|
|
quest.FadeIcons = nil
|
|
else
|
|
quest.HideIcons = nil
|
|
quest.FadeIcons = nil
|
|
end
|
|
|
|
for _, objective in pairs(quest.Objectives) do
|
|
if Questie.db.char.TrackerHiddenObjectives[tostring(questId) .. " " .. tostring(objective.Index)] then
|
|
objective.HideIcons = true
|
|
objective.FadeIcons = nil
|
|
else
|
|
objective.HideIcons = nil
|
|
objective.FadeIcons = nil
|
|
end
|
|
end
|
|
|
|
for _, objective in pairs(quest.SpecialObjectives) do
|
|
if Questie.db.char.TrackerHiddenObjectives[tostring(questId) .. " " .. tostring(objective.Index)] then
|
|
objective.HideIcons = true
|
|
objective.FadeIcons = nil
|
|
else
|
|
objective.HideIcons = nil
|
|
objective.FadeIcons = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
Questie.db.char.TrackerFocus = nil
|
|
end
|
|
|
|
---@param questId number Quest ID number
|
|
---@param objectiveIndex number Objective Index number
|
|
function TrackerUtils:FocusObjective(questId, objectiveIndex)
|
|
if Questie.db.char.TrackerFocus and (type(Questie.db.char.TrackerFocus) ~= "string" or Questie.db.char.TrackerFocus ~= tostring(questId) .. " " .. tostring(objectiveIndex)) then
|
|
TrackerUtils:UnFocus()
|
|
end
|
|
|
|
Questie.db.char.TrackerFocus = tostring(questId) .. " " .. tostring(objectiveIndex)
|
|
for questLogQuestId in pairs(QuestiePlayer.currentQuestlog) do
|
|
local quest = QuestieDB.GetQuest(questLogQuestId)
|
|
if quest and next(quest.Objectives) then
|
|
if questLogQuestId == questId then
|
|
quest.HideIcons = nil
|
|
quest.FadeIcons = nil
|
|
|
|
for _, objective in pairs(quest.Objectives) do
|
|
if objective.Index == objectiveIndex then
|
|
objective.HideIcons = nil
|
|
objective.FadeIcons = nil
|
|
else
|
|
objective.FadeIcons = true
|
|
end
|
|
end
|
|
|
|
for _, objective in pairs(quest.SpecialObjectives) do
|
|
if objective.Index == objectiveIndex then
|
|
objective.HideIcons = nil
|
|
objective.FadeIcons = nil
|
|
else
|
|
objective.FadeIcons = true
|
|
end
|
|
end
|
|
else
|
|
quest.FadeIcons = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@param questId number Quest ID number
|
|
function TrackerUtils:FocusQuest(questId)
|
|
if Questie.db.char.TrackerFocus and (type(Questie.db.char.TrackerFocus) ~= "number" or Questie.db.char.TrackerFocus ~= questId) then
|
|
TrackerUtils:UnFocus()
|
|
end
|
|
|
|
Questie.db.char.TrackerFocus = questId
|
|
for questLogQuestId in pairs(QuestiePlayer.currentQuestlog) do
|
|
local quest = QuestieDB.GetQuest(questLogQuestId)
|
|
if quest then
|
|
if questLogQuestId == questId then
|
|
quest.HideIcons = nil
|
|
quest.FadeIcons = nil
|
|
else
|
|
quest.FadeIcons = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@return table|nil position Returns Players current X/Y coordinates or nil if a Players postion can't be determined
|
|
local function _GetWorldPlayerPosition()
|
|
-- Turns coords into 'world' coords so it can be compared with any coords in another zone
|
|
local mapPosition, mapID = QuestieCoords.GetPlayerMapPosition()
|
|
if (not mapPosition) or (not mapPosition.x) then
|
|
return nil
|
|
end
|
|
|
|
local worldPosition = select(2, C_Map.GetWorldPosFromMapPos(mapID, mapPosition))
|
|
local position = {
|
|
x = worldPosition.x,
|
|
y = worldPosition.y
|
|
}
|
|
|
|
return position
|
|
end
|
|
|
|
---@param x1 number Current Position X
|
|
---@param y1 number Current Position Y
|
|
---@param x2 number Previous Position X
|
|
---@param y2 number Previous Position Y
|
|
---@return number Distance @Distance between Current and Previous X/Y coordinates
|
|
local function _GetDistance(x1, y1, x2, y2)
|
|
return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2)
|
|
end
|
|
|
|
---@param questId number Quest ID number
|
|
---@return number|nil closestDistance Returns X/Y coordinates to closest Objective or nil if nothing is found
|
|
local function _GetDistanceToClosestObjective(questId)
|
|
-- main function for proximity sorting
|
|
local player = _GetWorldPlayerPosition()
|
|
|
|
if (not player) then
|
|
return nil
|
|
end
|
|
|
|
local coordinates = {}
|
|
local quest = QuestieDB.GetQuest(questId)
|
|
|
|
if (not quest) then
|
|
return nil
|
|
end
|
|
|
|
local spawn, zone, name = QuestieMap:GetNearestQuestSpawn(quest)
|
|
|
|
if (not spawn) or (not zone) or (not name) then
|
|
return nil
|
|
end
|
|
|
|
local uiMapId = ZoneDB:GetUiMapIdByAreaId(zone)
|
|
if not uiMapId then
|
|
return nil
|
|
end
|
|
local _, worldPosition = C_Map.GetWorldPosFromMapPos(uiMapId, {
|
|
x = spawn[1] / 100,
|
|
y = spawn[2] / 100
|
|
})
|
|
|
|
tinsert(coordinates, {
|
|
x = worldPosition.x,
|
|
y = worldPosition.y
|
|
})
|
|
|
|
if (not coordinates) then
|
|
return nil
|
|
end
|
|
|
|
local closestDistance
|
|
for _, _ in pairs(coordinates) do
|
|
local distance = _GetDistance(player.x, player.y, worldPosition.x, worldPosition.y)
|
|
if (not closestDistance) or (distance < closestDistance) then
|
|
closestDistance = distance
|
|
end
|
|
end
|
|
|
|
return closestDistance
|
|
end
|
|
|
|
---@param uiMapId number Continent ID number
|
|
---@return string Continent Returns Continent Name or "UNKNOW"
|
|
local function _GetContinent(uiMapId)
|
|
if (not uiMapId) then
|
|
return
|
|
end
|
|
|
|
local useUiMapId = uiMapId
|
|
local mapInfo = C_Map.GetMapInfo(useUiMapId)
|
|
while mapInfo and mapInfo.mapType ~= 2 and mapInfo.parentMapID ~= useUiMapId do
|
|
useUiMapId = mapInfo.parentMapID
|
|
mapInfo = C_Map.GetMapInfo(useUiMapId)
|
|
end
|
|
|
|
if mapInfo ~= nil then
|
|
return mapInfo.name
|
|
else
|
|
return "UNKNOWN"
|
|
end
|
|
end
|
|
|
|
|
|
|
|
---@return table sortedQuestIds Table with sorted Quest ID's by Sort Type
|
|
---@return table questDetails Table with raw quest table from QuestiePlayer.currentQuestLog, percentage completed value per quest, and a "translated" zoneName
|
|
-- Private cache of fallback quest objects for quests not in QuestieDB.
|
|
-- Intentionally NOT stored in QuestiePlayer.currentQuestlog so arrow/map/other modules
|
|
-- don't try to call DB-only methods on them.
|
|
TrackerUtils._fallbackQuests = TrackerUtils._fallbackQuests or {}
|
|
|
|
-- Reverse-lookup: given a localized zone name string, find the area ID from l10n.zoneLookup.
|
|
local function GetAreaIdByZoneName(zoneName)
|
|
return l10n:GetAreaIdByLocalName(zoneName)
|
|
end
|
|
|
|
-- questId -> the header the client filed it under in the quest log. In 3.3.5 those headers are
|
|
-- isHeader=true entries sitting above the quests they cover. Built in one pass and kept, because
|
|
-- the tracker asks per quest and redraws often -- walking the log once per quest is quadratic.
|
|
local questLogHeaders = {}
|
|
|
|
-- Every questId the log currently holds, headers aside. The tracker draws from currentQuestlog,
|
|
-- and this is what says whether the player still has a given quest.
|
|
local questLogQuestIds = {}
|
|
|
|
local function BuildQuestLogHeaders()
|
|
local headers = {}
|
|
local questIds = {}
|
|
local header
|
|
|
|
for i = 1, (GetNumQuestLogEntries and GetNumQuestLogEntries() or 0) do
|
|
local title, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(i)
|
|
if isHeader then
|
|
if title and title ~= "" then
|
|
header = title
|
|
end
|
|
elseif logId then
|
|
questIds[logId] = true
|
|
if header then
|
|
headers[logId] = header
|
|
end
|
|
end
|
|
end
|
|
|
|
questLogHeaders = headers
|
|
questLogQuestIds = questIds
|
|
end
|
|
|
|
-- Returns the header title string, or nil if the quest is not in the log under one.
|
|
local function GetQuestLogZoneName(questId)
|
|
if not questLogQuestIds[questId] then
|
|
-- Asked about a quest the last pass did not see, so the log has moved on since.
|
|
BuildQuestLogHeaders()
|
|
end
|
|
|
|
return questLogHeaders[questId]
|
|
end
|
|
|
|
local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
|
|
if zoneNameOverride and zoneNameOverride ~= "" then
|
|
return zoneNameOverride
|
|
end
|
|
if not zoneOrSort then return "Unknown Zone" end
|
|
local zoneName
|
|
local sortObj = Questie.db.profile.trackerSortObjectives
|
|
if sortObj == "byZone" or sortObj == "byZoneComplete" or sortObj == "byZoneCompleteReversed" or sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then
|
|
-- A server can file quests under categories of its own -- Ascension's "Ascension Main
|
|
-- Quest" -- and those exist nowhere in the zone tables, so the quest data points at a zone
|
|
-- instead: for anything the Learner recorded, whichever zone it was picked up in. A header
|
|
-- that does not resolve to an area is one of those categories, and the client's own
|
|
-- grouping is the only thing that knows about it.
|
|
local logHeader = GetQuestLogZoneName(questId)
|
|
if logHeader and logHeader ~= "" then
|
|
local headerAreaId = GetAreaIdByZoneName(logHeader)
|
|
if (not headerAreaId) or headerAreaId == 0 then
|
|
return logHeader
|
|
end
|
|
end
|
|
|
|
if (zoneOrSort) > 0 then
|
|
zoneName = TrackerUtils:GetZoneNameByID(zoneOrSort)
|
|
if not zoneName or zoneName == "Unknown Zone" then
|
|
local logZone = GetQuestLogZoneName(questId)
|
|
if logZone then
|
|
zoneName = logZone
|
|
end
|
|
end
|
|
elseif (zoneOrSort) < 0 then
|
|
zoneName = TrackerUtils:GetCategoryNameByID(zoneOrSort)
|
|
else
|
|
-- zoneOrSort == 0: try quest log header as last resort before "Unknown Zone"
|
|
local logZone = GetQuestLogZoneName(questId)
|
|
if logZone then
|
|
zoneName = logZone
|
|
else
|
|
zoneName = "Unknown Zone"
|
|
Questie:Debug(Questie.DEBUG_CRITICAL, "[TrackerUtils:_GetZoneName] zoneOrSort", zoneOrSort, "of quest",
|
|
questId, "is not in the Database!")
|
|
end
|
|
end
|
|
else
|
|
if sortObj == "byComplete" then
|
|
zoneName = "Quests (By %% Complete)"
|
|
elseif sortObj == "byCompleteReversed" then
|
|
zoneName = "Quests (By %% Complete Reversed)"
|
|
elseif sortObj == "byLevel" then
|
|
zoneName = "Quests (By Level)"
|
|
elseif sortObj == "byLevelReversed" then
|
|
zoneName = "Quests (By Level Reversed)"
|
|
elseif sortObj == "byProximity" then
|
|
zoneName = "Quests (By Proximity)"
|
|
elseif sortObj == "byProximityReversed" then
|
|
zoneName = "Quests (By Proximity Reversed)"
|
|
end
|
|
end
|
|
return zoneName
|
|
end
|
|
|
|
-- Returns nil if the quest is not currently in the quest log.
|
|
function TrackerUtils:BuildFallbackQuest(questId)
|
|
for i = 1, GetNumQuestLogEntries() do
|
|
local title, level, _, isHeader, _, isComplete, _, logQuestId = GetQuestLogTitle(i)
|
|
if not isHeader and logQuestId == questId then
|
|
-- Parse objectives from the leaderboard
|
|
local objectives = {}
|
|
local numObj = GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(i) or 0
|
|
for j = 1, numObj do
|
|
local text, _, finished = GetQuestLogLeaderBoard(j, i)
|
|
if text then
|
|
-- Parse "Description: X/Y" or just "Description"
|
|
local collected, needed = string.match(text, ":.-(%d+)/(%d+)%s*$")
|
|
collected = tonumber(collected) or (finished and 1 or 0)
|
|
needed = tonumber(needed) or 1
|
|
objectives[j] = {
|
|
text = text,
|
|
Needed = needed,
|
|
Collected = collected,
|
|
Finished = finished or (collected >= needed),
|
|
Type = "fallback",
|
|
}
|
|
end
|
|
end
|
|
|
|
-- Walk backwards from i in the quest log to find the zone header.
|
|
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
|
|
local zoneText = nil
|
|
for h = i, 1, -1 do
|
|
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
|
|
if hIsHeader and hTitle and hTitle ~= "" then
|
|
zoneText = hTitle
|
|
break
|
|
end
|
|
end
|
|
local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0
|
|
local zoneNameOverride = nil
|
|
if zoneText and (not zoneId or zoneId == 0) then
|
|
zoneNameOverride = zoneText
|
|
end
|
|
|
|
local quest = {
|
|
Id = questId,
|
|
name = title or ("Quest " .. questId),
|
|
level = level or 0,
|
|
zoneOrSort = zoneId,
|
|
zoneName = zoneText,
|
|
zoneNameOverride = zoneNameOverride,
|
|
Objectives = objectives,
|
|
SpecialObjectives = {},
|
|
isFallback = true,
|
|
}
|
|
-- IsComplete must be a method (called as quest:IsComplete())
|
|
quest.IsComplete = function(self)
|
|
return (isComplete == 1 or (QuestiePlayer.currentQuestlog[questId] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(questId))) and 1 or 0
|
|
end
|
|
|
|
return quest
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function TrackerUtils:GetSortedQuestIds()
|
|
local sortedQuestIds = {}
|
|
local questDetails = {}
|
|
local sortObj = Questie.db.profile.trackerSortObjectives
|
|
|
|
-- One walk of the quest log for the whole draw, so the per-quest lookups below are reads.
|
|
BuildQuestLogHeaders()
|
|
|
|
-- currentQuestlog is only as good as the removal events that maintain it, and a quest the
|
|
-- server finishes on its own -- Ascension's auto-complete quests -- can leave the log without
|
|
-- any of them landing, which strands the quest in the tracker for the rest of the session. The
|
|
-- log is the authority on what the player still has, so anything missing from it is skipped.
|
|
-- Skipped rather than pruned: a redraw that catches the log mid-refresh would otherwise throw
|
|
-- away state Questie is about to want back.
|
|
local questLogIsReadable = next(questLogQuestIds) ~= nil
|
|
-- Update quest objectives
|
|
|
|
for questId, quest in pairs(QuestiePlayer.currentQuestlog) do
|
|
local qid = tonumber(questId) or questId
|
|
|
|
-- Defensive: sometimes other code (or saved vars) leaves the questId as the value instead of a quest table.
|
|
if type(quest) ~= "table" then
|
|
quest = QuestieDB.GetQuest(qid)
|
|
if quest then
|
|
QuestiePlayer.currentQuestlog[qid] = quest
|
|
end
|
|
end
|
|
|
|
-- Fallback for quests not in QuestieDB (e.g. custom server quests).
|
|
-- Two cases: QuestLogCache already put a partial object in currentQuestlog
|
|
-- (has _isLogFallback=true, isComplete=bool, but NO IsComplete method),
|
|
-- or the slot is missing entirely. Both need a working IsComplete method.
|
|
if type(quest) ~= "table" or type(quest.IsComplete) ~= "function" then
|
|
if type(quest) == "table" and quest._isLogFallback then
|
|
-- Patch IsComplete method onto the existing QuestLogCache object so
|
|
-- QuestieMap and other modules that read currentQuestlog also get it.
|
|
-- Also ensure fields QuestieMap iterates exist to avoid pairs(nil) crashes.
|
|
local capturedId = qid
|
|
quest.IsComplete = function(self)
|
|
for i = 1, GetNumQuestLogEntries() do
|
|
local _, _, _, isHeader, _, isCompleteFlag, _, logId = GetQuestLogTitle(i)
|
|
if not isHeader and logId == capturedId then
|
|
return (isCompleteFlag == 1 or isCompleteFlag == true) and 1 or 0
|
|
end
|
|
end
|
|
return 0
|
|
end
|
|
if not quest.Objectives then quest.Objectives = {} end
|
|
if not quest.SpecialObjectives then quest.SpecialObjectives = {} end
|
|
if not quest.ExtraObjectives then quest.ExtraObjectives = {} end
|
|
-- Use the quest log header walk (canonical 3.3.5 zone resolution)
|
|
-- Also run if zoneName is set but zoneOrSort is still 0 (e.g. GetAreaIdByZoneName
|
|
-- returned 0 for a valid zone name like "Sunstrider Isle", leaving zoneOrSort wrong).
|
|
if not quest.zoneName or quest.zoneName == "" or quest.zoneOrSort == 0 then
|
|
local logZone = GetQuestLogZoneName(capturedId)
|
|
if logZone then
|
|
quest.zoneName = logZone
|
|
local areaId = GetAreaIdByZoneName(logZone)
|
|
if areaId and areaId > 0 then
|
|
quest.zoneOrSort = areaId
|
|
else
|
|
quest.zoneOrSort = 0
|
|
quest.zoneNameOverride = logZone
|
|
end
|
|
end
|
|
end
|
|
QuestiePlayer.currentQuestlog[qid] = quest
|
|
else
|
|
-- No object at all — build one from the log
|
|
local fallback = TrackerUtils._fallbackQuests[qid]
|
|
-- Re-build if cached without zone info (e.g. was built before log was ready)
|
|
if fallback and not fallback.zoneName then
|
|
TrackerUtils._fallbackQuests[qid] = nil
|
|
fallback = nil
|
|
end
|
|
if not fallback then
|
|
fallback = TrackerUtils:BuildFallbackQuest(qid)
|
|
if fallback then
|
|
TrackerUtils._fallbackQuests[qid] = fallback
|
|
end
|
|
end
|
|
if fallback then
|
|
quest = fallback
|
|
end
|
|
end
|
|
end
|
|
|
|
local isInQuestLog = (not questLogIsReadable) or (questLogQuestIds[qid] == true)
|
|
if not isInQuestLog then
|
|
-- Left over from a removal nothing told the tracker about, so the object built for it
|
|
-- goes too -- otherwise it would still be here to serve the next draw.
|
|
TrackerUtils._fallbackQuests[qid] = nil
|
|
end
|
|
|
|
if isInQuestLog and type(quest) == "table" and quest.IsComplete and quest.Objectives then
|
|
-- Insert Quest Ids into sortedQuestIds table
|
|
tinsert(sortedQuestIds, qid)
|
|
|
|
-- Create questDetails table keys and insert values
|
|
questDetails[qid] = {}
|
|
questDetails[qid].quest = quest
|
|
questDetails[qid].zoneName = _GetZoneName(quest.zoneOrSort, qid, quest.zoneNameOverride)
|
|
|
|
if quest:IsComplete() == 1 or (not next(quest.Objectives)) then
|
|
questDetails[qid].questCompletePercent = 1
|
|
else
|
|
local percent = 0
|
|
local count = 0
|
|
for _, Objective in pairs(quest.Objectives) do
|
|
local needed = Objective and Objective.Needed
|
|
local collected = Objective and Objective.Collected
|
|
if type(needed) == "number" and needed > 0 and type(collected) == "number" then
|
|
percent = percent + (collected / needed)
|
|
count = count + 1
|
|
end
|
|
end
|
|
|
|
if count > 0 then
|
|
percent = percent / count
|
|
else
|
|
percent = 0
|
|
end
|
|
|
|
questDetails[qid].questCompletePercent = percent
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
-- Quests and objectives sort
|
|
if sortObj == "byComplete" or sortObj == "byCompleteReversed" then
|
|
table.sort(sortedQuestIds, function(a, b)
|
|
local vA, vB = questDetails[a].questCompletePercent, questDetails[b].questCompletePercent
|
|
if vA == vB then
|
|
local qA = questDetails[a].quest
|
|
local qB = questDetails[b].quest
|
|
return qA and qB and qA.level < qB.level
|
|
end
|
|
|
|
if sortObj == "byComplete" then
|
|
return vB < vA
|
|
else
|
|
return vB > vA
|
|
end
|
|
end)
|
|
elseif sortObj == "byLevel" or sortObj == "byLevelReversed" then
|
|
table.sort(sortedQuestIds, function(a, b)
|
|
local qA = questDetails[a].quest
|
|
local qB = questDetails[b].quest
|
|
if sortObj == "byLevel" then
|
|
return qA and qB and qA.level < qB.level
|
|
else
|
|
return qA and qB and qA.level > qB.level
|
|
end
|
|
end)
|
|
elseif sortObj == "byZone" then
|
|
table.sort(sortedQuestIds, function(a, b)
|
|
local qA = questDetails[a].quest
|
|
local qB = questDetails[b].quest
|
|
local qAZone = questDetails[a].zoneName
|
|
local qBZone = questDetails[b].zoneName
|
|
|
|
-- Sort by Zone then by Level to mimic QuestLog sorting
|
|
if qAZone == qBZone then
|
|
return qA.level < qB.level
|
|
else
|
|
if qAZone ~= nil and qBZone ~= nil then
|
|
return qAZone < qBZone
|
|
else
|
|
return qAZone and qBZone
|
|
end
|
|
end
|
|
end)
|
|
elseif sortObj == "byZoneComplete" or sortObj == "byZoneCompleteReversed" then
|
|
table.sort(sortedQuestIds, function(a, b)
|
|
local qAZone = questDetails[a].zoneName
|
|
local qBZone = questDetails[b].zoneName
|
|
|
|
-- Sort by Zone first, then by % Complete within each zone
|
|
-- (byZoneComplete: completes at top, 0% at bottom; reversed: opposite)
|
|
if qAZone == qBZone then
|
|
local vA, vB = questDetails[a].questCompletePercent, questDetails[b].questCompletePercent
|
|
if vA == vB then
|
|
local qA = questDetails[a].quest
|
|
local qB = questDetails[b].quest
|
|
return qA and qB and qA.level < qB.level
|
|
end
|
|
|
|
if sortObj == "byZoneComplete" then
|
|
return vA > vB
|
|
else
|
|
return vA < vB
|
|
end
|
|
else
|
|
if qAZone ~= nil and qBZone ~= nil then
|
|
return qAZone < qBZone
|
|
else
|
|
return qAZone and qBZone
|
|
end
|
|
end
|
|
end)
|
|
elseif sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then
|
|
local toSort = {}
|
|
local continent = _GetContinent(C_Map.GetBestMapForUnit("player"))
|
|
|
|
for _, questId in pairs(sortedQuestIds) do
|
|
local sortData = {}
|
|
sortData.questId = questId
|
|
sortData.distance = _GetDistanceToClosestObjective(questId)
|
|
sortData.q = questDetails[questId].quest
|
|
|
|
local _, zone, _ = QuestieMap:GetNearestQuestSpawn(sortData.q)
|
|
sortData.zone = zone
|
|
sortData.continent = _GetContinent(ZoneDB:GetUiMapIdByAreaId(zone))
|
|
toSort[questId] = sortData
|
|
end
|
|
|
|
local sorter = function(a, b)
|
|
local qAZone = questDetails[a].zoneName
|
|
local qBZone = questDetails[b].zoneName
|
|
|
|
-- If same Zone as Player then sort by Proximity
|
|
if qAZone == qBZone then
|
|
a = toSort[a]
|
|
b = toSort[b]
|
|
if ((continent == a.continent) and (continent == b.continent)) or ((continent ~= a.continent) and (continent ~= b.continent)) then
|
|
if a.distance == b.distance then
|
|
-- Same distance then sort by Level
|
|
return a.q and b.q and a.q.level < b.q.level
|
|
end
|
|
|
|
if not a.distance and b.distance then
|
|
return false
|
|
elseif a.distance and not b.distance then
|
|
return true
|
|
end
|
|
|
|
return a.distance < b.distance
|
|
elseif (continent == a.continent) and (continent ~= b.continent) then
|
|
return true
|
|
elseif (continent ~= a.continent) and (continent == b.continent) then
|
|
return false
|
|
end
|
|
else
|
|
-- Sort by Zone
|
|
if qAZone ~= nil and qBZone ~= nil then
|
|
return qAZone < qBZone
|
|
else
|
|
return qAZone and qBZone
|
|
end
|
|
end
|
|
end
|
|
|
|
local sorterReversed = function(a, b)
|
|
local qAZone = questDetails[a].zoneName
|
|
local qBZone = questDetails[b].zoneName
|
|
|
|
-- If same Zone as Player then sort by Proximity
|
|
if qAZone == qBZone then
|
|
a = toSort[a]
|
|
b = toSort[b]
|
|
if ((continent == a.continent) and (continent == b.continent)) or ((continent ~= a.continent) and (continent ~= b.continent)) then
|
|
if a.distance == b.distance then
|
|
-- Same distance then sort by Level
|
|
return a.q and b.q and a.q.level > b.q.level
|
|
end
|
|
|
|
if not a.distance and b.distance then
|
|
return true
|
|
elseif a.distance and not b.distance then
|
|
return false
|
|
end
|
|
|
|
return a.distance > b.distance
|
|
elseif (continent == a.continent) and (continent ~= b.continent) then
|
|
return false
|
|
elseif (continent ~= a.continent) and (continent == b.continent) then
|
|
return true
|
|
end
|
|
else
|
|
-- Sort by Zone
|
|
if qAZone ~= nil and qBZone ~= nil then
|
|
return qAZone < qBZone
|
|
else
|
|
return qAZone and qBZone
|
|
end
|
|
end
|
|
end
|
|
|
|
if sortObj == "byZonePlayerProximity" then
|
|
table.sort(sortedQuestIds, sorter)
|
|
else
|
|
table.sort(sortedQuestIds, sorterReversed)
|
|
end
|
|
|
|
if not questZoneProximityTimer and not IsInInstance() then
|
|
-- Check location often and update if you've moved
|
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[TrackerUtils:GetSortedQuestIds] - Zone Proximity Timer Started!")
|
|
|
|
local playerPosition
|
|
questZoneProximityTimer = C_Timer.NewTicker(5.0, function()
|
|
if IsInInstance() and questZoneProximityTimer then
|
|
Questie:Debug(Questie.DEBUG_DEVELOP,
|
|
"[TrackerUtils:GetSortedQuestIds] - Zone Proximity Timer Stoped!")
|
|
questZoneProximityTimer:Cancel()
|
|
questZoneProximityTimer = nil
|
|
else
|
|
local position = _GetWorldPlayerPosition()
|
|
if position then
|
|
local distance = playerPosition and
|
|
_GetDistance(position.x, position.y, playerPosition.x, playerPosition.y)
|
|
if not distance or distance > 0.01 then -- Position has changed
|
|
Questie:Debug(Questie.DEBUG_SPAM,
|
|
"[TrackerUtils:GetSortedQuestIds] - Zone Proximity Timer Updated!")
|
|
playerPosition = position
|
|
local orderCopy = {}
|
|
|
|
for index, val in pairs(sortedQuestIds) do
|
|
orderCopy[index] = val
|
|
end
|
|
|
|
if sortObj == "byZonePlayerProximity" then
|
|
table.sort(sortedQuestIds, sorter)
|
|
else
|
|
table.sort(sortedQuestIds, sorterReversed)
|
|
end
|
|
|
|
for index, val in pairs(sortedQuestIds) do
|
|
if orderCopy[index] ~= val then -- The order has changed
|
|
break
|
|
end
|
|
end
|
|
|
|
QuestieCombatQueue:Queue(function()
|
|
TrackerUtils.FilterProximityTimer = true
|
|
QuestieTracker:Update()
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
elseif sortObj == "byProximity" or sortObj == "byProximityReversed" then
|
|
local toSort = {}
|
|
local continent = _GetContinent(C_Map.GetBestMapForUnit("player"))
|
|
|
|
for _, questId in pairs(sortedQuestIds) do
|
|
local sortData = {}
|
|
sortData.questId = questId
|
|
sortData.distance = _GetDistanceToClosestObjective(questId)
|
|
sortData.q = questDetails[questId].quest
|
|
|
|
local _, zone, _ = QuestieMap:GetNearestQuestSpawn(sortData.q)
|
|
sortData.zone = zone
|
|
sortData.continent = _GetContinent(ZoneDB:GetUiMapIdByAreaId(zone))
|
|
toSort[questId] = sortData
|
|
end
|
|
|
|
local sorter = function(a, b)
|
|
a = toSort[a]
|
|
b = toSort[b]
|
|
if ((continent == a.continent) and (continent == b.continent)) or ((continent ~= a.continent) and (continent ~= b.continent)) then
|
|
if a.distance == b.distance then
|
|
return a.q and b.q and a.q.level < b.q.level
|
|
end
|
|
|
|
if not a.distance and b.distance then
|
|
return false
|
|
elseif a.distance and not b.distance then
|
|
return true
|
|
end
|
|
|
|
return a.distance < b.distance
|
|
elseif (continent == a.continent) and (continent ~= b.continent) then
|
|
return true
|
|
elseif (continent ~= a.continent) and (continent == b.continent) then
|
|
return false
|
|
end
|
|
end
|
|
|
|
local sorterReversed = function(a, b)
|
|
a = toSort[a]
|
|
b = toSort[b]
|
|
if ((continent == a.continent) and (continent == b.continent)) or ((continent ~= a.continent) and (continent ~= b.continent)) then
|
|
if a.distance == b.distance then
|
|
return a.q and b.q and a.q.level > b.q.level
|
|
end
|
|
|
|
if not a.distance and b.distance then
|
|
return true
|
|
elseif a.distance and not b.distance then
|
|
return false
|
|
end
|
|
|
|
return a.distance > b.distance
|
|
elseif (continent == a.continent) and (continent ~= b.continent) then
|
|
return false
|
|
elseif (continent ~= a.continent) and (continent == b.continent) then
|
|
return true
|
|
end
|
|
end
|
|
|
|
if sortObj == "byProximity" then
|
|
table.sort(sortedQuestIds, sorter)
|
|
else
|
|
table.sort(sortedQuestIds, sorterReversed)
|
|
end
|
|
|
|
if not questProximityTimer and not IsInInstance() then
|
|
-- Check location often and update if you've moved
|
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[TrackerUtils:GetSortedQuestIds] - Proximity Timer Started!")
|
|
|
|
local playerPosition
|
|
questProximityTimer = C_Timer.NewTicker(5.0, function()
|
|
if IsInInstance() and questProximityTimer then
|
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[TrackerUtils:GetSortedQuestIds] - Proximity Timer Stoped!")
|
|
questProximityTimer:Cancel()
|
|
questProximityTimer = nil
|
|
else
|
|
local position = _GetWorldPlayerPosition()
|
|
if position then
|
|
local distance = playerPosition and
|
|
_GetDistance(position.x, position.y, playerPosition.x, playerPosition.y)
|
|
if not distance or distance > 0.01 then -- Position has changed
|
|
Questie:Debug(Questie.DEBUG_SPAM,
|
|
"[TrackerUtils:GetSortedQuestIds] - Proximity Timer Updated!")
|
|
playerPosition = position
|
|
local orderCopy = {}
|
|
|
|
for index, val in pairs(sortedQuestIds) do
|
|
orderCopy[index] = val
|
|
end
|
|
|
|
if sortObj == "byProximity" then
|
|
table.sort(sortedQuestIds, sorter)
|
|
else
|
|
table.sort(sortedQuestIds, sorterReversed)
|
|
end
|
|
|
|
for index, val in pairs(sortedQuestIds) do
|
|
if orderCopy[index] ~= val then -- The order has changed
|
|
break
|
|
end
|
|
end
|
|
|
|
QuestieCombatQueue:Queue(function()
|
|
TrackerUtils.FilterProximityTimer = true
|
|
QuestieTracker:Update()
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
|
|
if (sortObj ~= strmatch(sortObj, "byProximity.*")) and questProximityTimer and questProximityTimer ~= nil then
|
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[TrackerUtils:GetSortedQuestIds] - Proximity Timer Stoped!")
|
|
questProximityTimer:Cancel()
|
|
TrackerUtils.FilterProximityTimer = nil
|
|
questProximityTimer = nil
|
|
end
|
|
|
|
if (sortObj ~= strmatch(sortObj, "byZonePlayerProximity.*")) and questZoneProximityTimer and questZoneProximityTimer ~= nil then
|
|
Questie:Debug(Questie.DEBUG_DEVELOP, "[TrackerUtils:GetSortedQuestIds] - Zone Proximity Timer Stoped!")
|
|
questZoneProximityTimer:Cancel()
|
|
TrackerUtils.FilterProximityTimer = nil
|
|
questZoneProximityTimer = nil
|
|
end
|
|
|
|
return sortedQuestIds, questDetails
|
|
end
|
|
|
|
-- Ascension's 3.3.5 client backports the retail floating objective marker ("SuperTracker").
|
|
-- Supertracking is a slave of the quest log selection: the world map and the Blizzard watch frame
|
|
-- both funnel through SelectQuestLogEntry -> SuperTrackerUtil.SetToBestSuperTrackingType. Calling
|
|
-- C_SuperTrack.SetSuperTrackedQuestID directly only moves the marker until the next map interaction
|
|
-- stomps it, so we click the same POI frames the client itself clicks.
|
|
local superTrackedQuestId
|
|
local superTrackHooked
|
|
local superTrackRefreshing
|
|
local superTrackRefreshPending
|
|
local superTrackEventFrame
|
|
|
|
-- Refreshing rebuilds the map's POI frames, which can land back in the very hooks that asked for the
|
|
-- refresh, so this is the only way the buttons are ever repainted. Requests are also coalesced:
|
|
-- callers like TrackerQuestTimers select a quest log entry and immediately restore the previous one,
|
|
-- so reading the selection on the first of those calls would catch a state the client is about to
|
|
-- undo. Waiting a tick means the burst has settled.
|
|
local function RefreshSuperTrackButtons()
|
|
if superTrackRefreshing or superTrackRefreshPending then
|
|
return
|
|
end
|
|
|
|
superTrackRefreshPending = true
|
|
C_Timer.After(0.05, function()
|
|
superTrackRefreshPending = false
|
|
superTrackRefreshing = true
|
|
TrackerLinePool.UpdateSuperTrackButtons()
|
|
superTrackRefreshing = false
|
|
end)
|
|
end
|
|
|
|
function TrackerUtils:IsSuperTrackAvailable()
|
|
return (C_SuperTrack ~= nil) and ((WorldMapFrame_SelectQuestFrame ~= nil) or (WatchFrameQuestPOI_OnClick ~= nil))
|
|
end
|
|
|
|
-- Most paths that change the supertracked quest end up in SetSuperTrackedQuestID -- our own button,
|
|
-- world map pins, the map quest list, the Blizzard tracker, and the automatic re-pick that happens
|
|
-- when the map switches zone -- so hooking it stands in for the GetSuperTrackedQuestID getter this
|
|
-- client dropped. It goes quiet while the player is a ghost, though: the corpse arrow takes the
|
|
-- marker over, so no quest is ever handed to it even though the map keeps selecting one. The quest
|
|
-- selection itself is therefore hooked as well, and that is what keeps the buttons honest while
|
|
-- dead. Caching what we last set would go stale the moment the player changed it by other means.
|
|
function TrackerUtils:InitSuperTrackHook()
|
|
if superTrackHooked or (not C_SuperTrack) then
|
|
return
|
|
end
|
|
|
|
superTrackHooked = true
|
|
|
|
hooksecurefunc(C_SuperTrack, "SetSuperTrackedQuestID", function(questId)
|
|
superTrackedQuestId = questId
|
|
RefreshSuperTrackButtons()
|
|
end)
|
|
|
|
if C_SuperTrack.ClearSuperTracker then
|
|
hooksecurefunc(C_SuperTrack, "ClearSuperTracker", function()
|
|
superTrackedQuestId = nil
|
|
RefreshSuperTrackButtons()
|
|
end)
|
|
end
|
|
|
|
if WorldMapFrame_SelectQuestFrame then
|
|
hooksecurefunc("WorldMapFrame_SelectQuestFrame", RefreshSuperTrackButtons)
|
|
end
|
|
|
|
if WatchFrameQuestPOI_OnClick then
|
|
hooksecurefunc("WatchFrameQuestPOI_OnClick", RefreshSuperTrackButtons)
|
|
end
|
|
|
|
-- Everything that supertracks a quest goes through the quest log selection, map or no map, alive
|
|
-- or dead -- including opening a quest in the quest log window, which no other hook here sees.
|
|
if SelectQuestLogEntry then
|
|
hooksecurefunc("SelectQuestLogEntry", RefreshSuperTrackButtons)
|
|
end
|
|
|
|
-- Nothing at all fires on a login or a reload: the map has not been touched, so the hooks above
|
|
-- stay silent and the tracker draws before the client has styled its pins. These events are the
|
|
-- only prompt to go back and look.
|
|
superTrackEventFrame = CreateFrame("Frame")
|
|
superTrackEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
|
|
superTrackEventFrame:RegisterEvent("PLAYER_UNGHOST")
|
|
superTrackEventFrame:RegisterEvent("PLAYER_ALIVE")
|
|
superTrackEventFrame:RegisterEvent("PLAYER_DEAD")
|
|
superTrackEventFrame:SetScript("OnEvent", function()
|
|
RefreshSuperTrackButtons()
|
|
-- The client fills in its POI frames a moment after entering the world, so the immediate
|
|
-- pass above can still come up empty.
|
|
C_Timer.After(2, RefreshSuperTrackButtons)
|
|
end)
|
|
end
|
|
|
|
-- The hook only hears about the changes the client itself makes, and while the player is a ghost the
|
|
-- floating marker is disabled: selecting a quest then never reaches SetSuperTrackedQuestID, so the
|
|
-- hook reports nothing after a login or a reload in that state and goes stale after any click. The
|
|
-- client's own frames still know. Watch frame POI buttons say so outright, and the world map's quest
|
|
-- frames say it through their art: the atlases stack the selected (yellow) variant of a cell half a
|
|
-- texture above the normal one, so a pin drawn from the upper half is the supertracked one.
|
|
local function FindSelectedQuestId()
|
|
for i = 1, 30 do
|
|
local firstInRow = _G["poiWatchFrameLines" .. i .. "_1"]
|
|
if not firstInRow then
|
|
break
|
|
end
|
|
for j = 1, 5 do
|
|
local poiButton = (j == 1) and firstInRow or _G["poiWatchFrameLines" .. i .. "_" .. j]
|
|
if not poiButton then
|
|
break
|
|
end
|
|
if poiButton.isSelected and poiButton.questId then
|
|
return poiButton.questId
|
|
end
|
|
end
|
|
end
|
|
|
|
for i = 1, 25 do
|
|
local questFrame = _G["WorldMapQuestFrame" .. i]
|
|
if not questFrame then
|
|
break
|
|
end
|
|
|
|
local pin = questFrame.ownPOI or questFrame.poiIcon
|
|
local pinTexture = pin and pin.GetNormalTexture and pin:GetNormalTexture()
|
|
if pinTexture and questFrame.questId then
|
|
local _, topY = pinTexture:GetTexCoord()
|
|
if topY and topY < 0.5 then
|
|
return questFrame.questId
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Right after a login or a reload no pin is styled at all: the client marks them the first time
|
|
-- the world map is opened. The quest log selection is what it reads when it gets there, so it
|
|
-- answers for the gap in between.
|
|
local selection = GetQuestLogSelection and GetQuestLogSelection()
|
|
if selection and selection > 0 then
|
|
-- GetQuestIDFromLogIndex, not the raw API: GetQuestLogTitle is the compat wrapper here,
|
|
-- which normalises the client's 9 return values down to 8.
|
|
local questId = QuestieCompat.GetQuestIDFromLogIndex(selection)
|
|
if questId and questId ~= 0 then
|
|
return questId
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- The client is asked before the hook: the hook cannot see a ghost's selection changes at all, so
|
|
-- its value is the fallback for when no POI frame exists to read (another zone, or frames not built
|
|
-- yet), not the source of truth.
|
|
function TrackerUtils:GetSuperTrackedQuestId()
|
|
return FindSelectedQuestId() or superTrackedQuestId
|
|
end
|
|
|
|
-- The world map builds its quest POI frames lazily, so right after login -- or after a zone change
|
|
-- with the map still closed -- there is nothing to match a quest against and every button would
|
|
-- hide itself. The client can build them without the map being shown, and doing so does not disturb
|
|
-- which quest is currently supertracked. Skipped while the map is open so we never fight the player.
|
|
function TrackerUtils:PrimeSuperTrackFrames()
|
|
if WorldMapFrame and WorldMapFrame:IsShown() then
|
|
return
|
|
end
|
|
|
|
if WorldMapFrame_UpdateQuests then
|
|
WorldMapFrame_UpdateQuests()
|
|
end
|
|
end
|
|
|
|
---@return table|nil frame, function|nil clickHandler
|
|
local function GetSuperTrackFrame(questId)
|
|
if (not questId) or questId == 0 then
|
|
return nil
|
|
end
|
|
|
|
-- Blizzard watch frame POI buttons carry the quest id directly. Questie empties the Blizzard
|
|
-- watch list (see QuestieTracker:AQW_Insert) so only a handful of quests ever have one. Both
|
|
-- scans below stop at the first gap: these frames are created in order, so a missing index
|
|
-- means there are no further ones and this runs once per quest line per redraw.
|
|
if WatchFrameQuestPOI_OnClick then
|
|
for i = 1, 30 do
|
|
local firstInRow = _G["poiWatchFrameLines" .. i .. "_1"]
|
|
if not firstInRow then
|
|
break
|
|
end
|
|
for j = 1, 5 do
|
|
local poiButton = (j == 1) and firstInRow or _G["poiWatchFrameLines" .. i .. "_" .. j]
|
|
if not poiButton then
|
|
break
|
|
end
|
|
if poiButton.questId == questId then
|
|
return poiButton, WatchFrameQuestPOI_OnClick
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- World map quest frames cover every quest with a POI on the currently viewed map, which is the
|
|
-- wider net of the two. Pass the WorldMapQuestFrame itself and never its poiIcon --
|
|
-- WorldMapFrame_SelectQuestFrame indexes questFrame.poiIcon and errors on the POI frame.
|
|
if WorldMapFrame_SelectQuestFrame then
|
|
if not _G["WorldMapQuestFrame1"] then
|
|
TrackerUtils:PrimeSuperTrackFrames()
|
|
end
|
|
|
|
for i = 1, 25 do
|
|
local questFrame = _G["WorldMapQuestFrame" .. i]
|
|
if not questFrame then
|
|
break
|
|
end
|
|
if questFrame.questId == questId then
|
|
return questFrame, WorldMapFrame_SelectQuestFrame
|
|
end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- A quest can only be supertracked while it has a POI frame, so quests in another zone or without
|
|
-- map coordinates simply have no button.
|
|
function TrackerUtils:CanSuperTrackQuest(questId)
|
|
return GetSuperTrackFrame(questId) ~= nil
|
|
end
|
|
|
|
-- The map pin the client draws for this quest. Copying its texture coordinates is what keeps the
|
|
-- tracker button identical to the pin: the number, the "?" shown for completed quests and the
|
|
-- black-on-yellow selected variant all follow along without us mapping atlas cells by hand. The
|
|
-- number is also the pin's position among quests that actually have a POI, which is not the same as
|
|
-- the quest frame index -- another reason to read it from the client instead of deriving it.
|
|
---@return table|nil
|
|
function TrackerUtils:GetSuperTrackPin(questId)
|
|
if (not questId) or questId == 0 or (not WorldMapFrame_SelectQuestFrame) then
|
|
return nil
|
|
end
|
|
|
|
if not _G["WorldMapQuestFrame1"] then
|
|
TrackerUtils:PrimeSuperTrackFrames()
|
|
end
|
|
|
|
for i = 1, 25 do
|
|
local questFrame = _G["WorldMapQuestFrame" .. i]
|
|
if not questFrame then
|
|
break
|
|
end
|
|
if questFrame.questId == questId then
|
|
-- ownPOI is the pin drawn in the map's quest list, poiIcon the one on the map itself.
|
|
-- The list version is the one we mirror: it keeps the circular background on completed
|
|
-- quests, where the map version draws a bare "?".
|
|
return questFrame.ownPOI or questFrame.poiIcon
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function TrackerUtils:SetSuperTrackedQuest(questId)
|
|
local frame, clickHandler = GetSuperTrackFrame(questId)
|
|
if not frame then
|
|
return false
|
|
end
|
|
|
|
clickHandler(frame)
|
|
|
|
-- Refresh here rather than leaning on the SetSuperTrackedQuestID hook: it stays silent while the
|
|
-- player is a ghost, which would leave the button we just clicked looking untouched.
|
|
RefreshSuperTrackButtons()
|
|
return true
|
|
end
|
|
|
|
function TrackerUtils:IsVoiceOverLoaded()
|
|
-- Require not just that the VoiceOver addons are loaded, but that the runtime
|
|
-- structure we index actually exists. Some VoiceOver builds (e.g. on Elune) expose
|
|
-- a different QuestOverlayUI shape where questPlayButtons is absent, which previously
|
|
-- crashed UpdateVoiceOverPlayButtons / SetAllPlayButtonAlpha with "attempt to index
|
|
-- field 'questPlayButtons' (a nil value)". All play-button call sites gate on this
|
|
-- function, so verifying the table here makes them all safe.
|
|
if IsAddOnLoaded("AI_VoiceOver") and IsAddOnLoaded("AI_VoiceOverData_Vanilla")
|
|
and VoiceOver and VoiceOver.QuestOverlayUI and VoiceOver.QuestOverlayUI.questPlayButtons then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function TrackerUtils:ShowVoiceOverPlayButtons()
|
|
if self:IsVoiceOverLoaded() then
|
|
if Questie.db.char.isTrackerExpanded then
|
|
if IsShiftKeyDown() and MouseIsOver(Questie_BaseFrame) then
|
|
if Questie_BaseFrame.isSizing == true or Questie_BaseFrame.isMoving == true then
|
|
Questie:Debug(Questie.DEBUG_SPAM, "[TrackerUtils:ShowVoiceOverPlayButtons]")
|
|
else
|
|
Questie:Debug(Questie.DEBUG_INFO, "[TrackerUtils:ShowVoiceOverPlayButtons]")
|
|
end
|
|
end
|
|
|
|
if IsShiftKeyDown() then
|
|
if MouseIsOver(Questie_BaseFrame) then
|
|
TrackerLinePool.SetAllPlayButtonAlpha(1)
|
|
TrackerFadeTicker.Fade()
|
|
|
|
if not Questie.db.profile.trackerFadeMinMaxButtons then
|
|
TrackerLinePool.SetAllExpandQuestAlpha(0)
|
|
end
|
|
|
|
if not Questie.db.profile.trackerFadeQuestItemButtons then
|
|
TrackerLinePool.SetAllItemButtonAlpha(0)
|
|
end
|
|
end
|
|
else
|
|
if MouseIsOver(Questie_BaseFrame) then
|
|
TrackerLinePool.SetAllPlayButtonAlpha(0)
|
|
TrackerFadeTicker.Unfade()
|
|
else
|
|
TrackerLinePool.SetAllPlayButtonAlpha(0)
|
|
TrackerFadeTicker.Fade()
|
|
end
|
|
|
|
if not Questie.db.profile.trackerFadeMinMaxButtons then
|
|
TrackerLinePool.SetAllExpandQuestAlpha(1)
|
|
end
|
|
|
|
if not Questie.db.profile.trackerFadeQuestItemButtons then
|
|
TrackerLinePool.SetAllItemButtonAlpha(1)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function TrackerUtils:UpdateVoiceOverPlayButtons()
|
|
if self:IsVoiceOverLoaded() then
|
|
if Questie_BaseFrame.isSizing == true or Questie_BaseFrame.isMoving == true then
|
|
Questie:Debug(Questie.DEBUG_SPAM, "[TrackerUtils:UpdateVoiceOverPlayButtons]")
|
|
else
|
|
Questie:Debug(Questie.DEBUG_INFO, "[TrackerUtils:UpdateVoiceOverPlayButtons]")
|
|
end
|
|
|
|
for i = 1, 75 do
|
|
local title, level, questTag, isHeader, isCollapsed, isComplete, isDaily, questId = GetQuestLogTitle(i)
|
|
|
|
if title and questId and (not isHeader) then
|
|
if not VoiceOver.QuestOverlayUI.questPlayButtons[questId] then
|
|
VoiceOver.QuestOverlayUI:CreatePlayButton(questId)
|
|
table.insert(VoiceOver.QuestOverlayUI.displayedButtons,
|
|
VoiceOver.QuestOverlayUI.questPlayButtons[questId])
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---@return number|nil itemId The ID of the nearest usable quest item
|
|
function TrackerUtils:GetNearestQuestItemId()
|
|
local questIds = QuestiePlayer.currentQuestlog
|
|
local bestItemId = nil
|
|
local minDistance = 999999
|
|
|
|
local playerPos = _GetWorldPlayerPosition()
|
|
if not playerPos then return nil end
|
|
|
|
for questId in pairs(questIds) do
|
|
local quest = QuestieDB.GetQuest(questId)
|
|
if quest then
|
|
local items = {}
|
|
if quest.sourceItemId and quest.sourceItemId ~= 0 then
|
|
table.insert(items, quest.sourceItemId)
|
|
end
|
|
if type(quest.requiredSourceItems) == "table" then
|
|
for _, itemId in pairs(quest.requiredSourceItems) do
|
|
if itemId and itemId ~= 0 then
|
|
table.insert(items, itemId)
|
|
end
|
|
end
|
|
end
|
|
|
|
local foundItemId = nil
|
|
for i = 1, table.getn(items) do
|
|
local itemId = items[i]
|
|
-- Check if item is in bags and is a quest item (class 12)
|
|
if GetItemCount(itemId) > 0 and QuestieDB.QueryItemSingle(itemId, "class") == 12 then
|
|
foundItemId = itemId
|
|
break
|
|
end
|
|
end
|
|
|
|
if foundItemId then
|
|
local distance = _GetDistanceToClosestObjective(questId)
|
|
if distance and distance < minDistance then
|
|
minDistance = distance
|
|
bestItemId = foundItemId
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return bestItemId
|
|
end
|
|
|
|
--- Logic to use the nearest quest item (Fallback for non-secure usage or manual calls)
|
|
function TrackerUtils:UseNearestQuestItem()
|
|
local itemId = self:GetNearestQuestItemId()
|
|
if itemId then
|
|
local itemName = GetItemInfo(itemId)
|
|
if itemName then
|
|
UseItemByName(itemName)
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Toggle the Questie Options window
|
|
function TrackerUtils:ToggleOptions()
|
|
local QuestieOptions = QuestieLoader:ImportModule("QuestieOptions")
|
|
if QuestieOptions and QuestieOptions.OpenConfigWindow then
|
|
QuestieOptions:OpenConfigWindow()
|
|
end
|
|
end
|
|
|
|
--- Toggle the Questie Tracker
|
|
function TrackerUtils:ToggleTracker()
|
|
local QuestieTracker = QuestieLoader:ImportModule("QuestieTracker")
|
|
if QuestieTracker and QuestieTracker.Toggle then
|
|
QuestieTracker:Toggle()
|
|
end
|
|
end
|
|
|
|
--- Toggle the Questie Journey window
|
|
function TrackerUtils:ToggleJourney()
|
|
local QuestieJourney = QuestieLoader:ImportModule("QuestieJourney")
|
|
if QuestieJourney and QuestieJourney.ToggleJourneyWindow then
|
|
QuestieJourney:ToggleJourneyWindow()
|
|
end
|
|
end
|
|
|
|
|