30fe4b83a9
Quests assembled from a Learner entry or a questDataOverrides entry that never captured every field reach the tracker with nil fields, since GetQuest copies rawdata key by key. Two of those show: a nil name printed as the quest id, and a nil zoneOrSort that sent _GetZoneName down its very first line, `if not zoneOrSort then return "Unknown Zone" end`, before it could consult the quest log header the earlier fix added. Those objects are cached for the session, so neither repaired itself. GetQuest now fills a missing name from the quest log and defaults zoneOrSort to 0, the value every caller already reads as "no zone on file" -- and which some of them require, `quest.zoneOrSort > 0` erroring outright on nil. _GetZoneName treats nil the same way rather than short-circuiting, which also stops a nil quest from labelling its group Unknown Zone under the sort modes that do not group by zone at all. The tracker asks the quest log for a title before printing an id, so quests already cached without a name come out right too, and the live-fallback builder stops discarding the override data it just looked up.
1882 lines
74 KiB
Lua
1882 lines
74 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 = {}
|
|
|
|
-- questId -> its row in the quest log, so re-reading a quest's leaderboard costs a lookup
|
|
-- instead of another walk. Verified before use, since the log renumbers on every change.
|
|
local questLogIndexes = {}
|
|
|
|
local function BuildQuestLogHeaders()
|
|
local headers = {}
|
|
local questIds = {}
|
|
local indexes = {}
|
|
local header
|
|
|
|
for i = 1, (GetNumQuestLogEntries and GetNumQuestLogEntries() or 0) do
|
|
local title, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(i)
|
|
if isHeader then
|
|
if title and title ~= "" then
|
|
header = title
|
|
end
|
|
elseif logId then
|
|
questIds[logId] = true
|
|
indexes[logId] = i
|
|
if header then
|
|
headers[logId] = header
|
|
end
|
|
end
|
|
end
|
|
|
|
questLogHeaders = headers
|
|
questLogQuestIds = questIds
|
|
questLogIndexes = indexes
|
|
end
|
|
|
|
-- Returns the quest's current row in the quest log, or nil if the player no longer has it.
|
|
local function GetQuestLogIndexForQuest(questId)
|
|
local index = questLogIndexes[questId]
|
|
if index then
|
|
local _, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(index)
|
|
if (not isHeader) and logId == questId then
|
|
return index
|
|
end
|
|
end
|
|
|
|
for i = 1, (GetNumQuestLogEntries and GetNumQuestLogEntries() or 0) do
|
|
local _, _, _, isHeader, _, _, _, logId = GetQuestLogTitle(i)
|
|
if (not isHeader) and logId == questId then
|
|
questLogIndexes[questId] = i
|
|
return i
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- 3.3.5 hands over a leaderboard line as one string -- "Icefang slain: 3/8" -- while the tracker
|
|
-- prints the description and the counts separately, so they have to come back apart here.
|
|
-- Objectives with nothing to count arrive as bare text and stand in as a single 0/1 step.
|
|
local function ParseLeaderBoardText(text, finished)
|
|
-- Greedy on purpose: a description of its own may hold a colon, and the counter is the last one.
|
|
local description, collected, needed = string.match(text, "^(.*):%s*(%d+)%s*/%s*(%d+)%s*$")
|
|
if not description then
|
|
-- Servers writing their own objective strings do not always keep the colon.
|
|
description, collected, needed = string.match(text, "^(.-)%s*(%d+)%s*/%s*(%d+)%s*$")
|
|
end
|
|
|
|
if (not description) or description == "" then
|
|
return text, (finished and 1 or 0), 1
|
|
end
|
|
|
|
collected = tonumber(collected) or 0
|
|
needed = tonumber(needed) or 1
|
|
if needed <= 0 then
|
|
needed = 1
|
|
end
|
|
|
|
return description, collected, needed
|
|
end
|
|
|
|
-- Fills `objectives` from the quest's leaderboard, in place: the tracker hands these very tables
|
|
-- to the lines it draws, so a redraw has to update them rather than swap in new ones.
|
|
local function ReadFallbackObjectives(questLogIndex, questId, objectives)
|
|
local numObjectives = (GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(questLogIndex)) or 0
|
|
|
|
for j = 1, numObjectives do
|
|
local text, objectiveType, finished = GetQuestLogLeaderBoard(j, questLogIndex)
|
|
if text then
|
|
local description, collected, needed = ParseLeaderBoardText(text, finished)
|
|
local objective = objectives[j]
|
|
if not objective then
|
|
objective = {}
|
|
objectives[j] = objective
|
|
end
|
|
|
|
objective.questId = questId
|
|
objective.Index = j
|
|
objective.Description = description
|
|
objective.text = text
|
|
objective.Collected = collected
|
|
objective.Needed = needed
|
|
objective.Completed = (finished or collected >= needed) and true or false
|
|
objective.baseType = objectiveType
|
|
-- Nothing here came from the DB, so map/tooltip code has to leave it alone.
|
|
objective.Type = "fallback"
|
|
end
|
|
end
|
|
|
|
for j = #objectives, numObjectives + 1, -1 do
|
|
objectives[j] = nil
|
|
end
|
|
|
|
return objectives
|
|
end
|
|
|
|
-- Returns the header title string, or nil if the quest is not in the log under one.
|
|
local function GetQuestLogZoneName(questId)
|
|
if not questLogQuestIds[questId] then
|
|
-- Asked about a quest the last pass did not see, so the log has moved on since.
|
|
BuildQuestLogHeaders()
|
|
end
|
|
|
|
return questLogHeaders[questId]
|
|
end
|
|
|
|
local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
|
|
if zoneNameOverride and zoneNameOverride ~= "" then
|
|
return zoneNameOverride
|
|
end
|
|
|
|
-- A quest assembled from a partial record -- a Learner entry that never captured the field,
|
|
-- an override carrying a single key -- reaches here with no zoneOrSort at all. That is the
|
|
-- same "nothing to look up" case as 0, and the quest log below still knows where the client
|
|
-- files the quest, so it must not short-circuit to Unknown Zone ahead of that.
|
|
zoneOrSort = zoneOrSort or 0
|
|
|
|
local zoneName
|
|
local sortObj = Questie.db.profile.trackerSortObjectives
|
|
if sortObj == "byZone" or sortObj == "byZoneComplete" or sortObj == "byZoneCompleteReversed" or sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then
|
|
-- A server can file quests under categories of its own -- Ascension's "Ascension Main
|
|
-- Quest" -- and those exist nowhere in the zone tables, so the quest data points at a zone
|
|
-- instead: for anything the Learner recorded, whichever zone it was picked up in. A header
|
|
-- that does not resolve to an area is one of those categories, and the client's own
|
|
-- grouping is the only thing that knows about it.
|
|
local logHeader = GetQuestLogZoneName(questId)
|
|
if logHeader and logHeader ~= "" then
|
|
local headerAreaId = GetAreaIdByZoneName(logHeader)
|
|
if (not headerAreaId) or headerAreaId == 0 then
|
|
return logHeader
|
|
end
|
|
end
|
|
|
|
if (zoneOrSort) > 0 then
|
|
zoneName = TrackerUtils:GetZoneNameByID(zoneOrSort)
|
|
if not zoneName or zoneName == "Unknown Zone" then
|
|
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
|
|
|
|
-- The client's own title for a quest, or nil if the player does not have it. Quest objects can
|
|
-- reach the tracker without a usable name -- a questDataOverrides entry that carries no name
|
|
-- field, a QuestLogCache row read before the client had filled the title in -- and since those
|
|
-- objects are cached for the session, the name never repairs itself. The log always knows.
|
|
function TrackerUtils:GetQuestLogTitleById(questId)
|
|
local questLogIndex = GetQuestLogIndexForQuest(questId)
|
|
if not questLogIndex then return nil end
|
|
|
|
local title = GetQuestLogTitle(questLogIndex)
|
|
if title and title ~= "" then
|
|
return title
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- IsComplete must be a method (called as quest:IsComplete()), and it reads the state the last
|
|
-- refresh stored rather than closing over the state at build time -- these quests outlive many
|
|
-- redraws, and a captured value would still claim the quest is unfinished after it is turned in.
|
|
local function FallbackQuestIsComplete(self)
|
|
if self.logIsComplete == 1 or self.logIsComplete == true then
|
|
return 1
|
|
end
|
|
|
|
if QuestiePlayer.currentQuestlog[self.Id] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(self.Id) then
|
|
return 1
|
|
end
|
|
|
|
return 0
|
|
end
|
|
|
|
-- Re-reads everything the quest log owns: title, level, completion and the objectives. The
|
|
-- tracker keeps fallback quests between draws, so without this they stay frozen at whatever
|
|
-- the log said when the quest was first seen.
|
|
---@return boolean @false if the player no longer has the quest
|
|
function TrackerUtils:RefreshFallbackQuest(quest)
|
|
if not quest then return false end
|
|
|
|
local questLogIndex = GetQuestLogIndexForQuest(quest.Id)
|
|
if not questLogIndex then return false end
|
|
|
|
local title, level, _, _, _, isComplete = GetQuestLogTitle(questLogIndex)
|
|
if title and title ~= "" then
|
|
quest.name = title
|
|
end
|
|
if level and level > 0 then
|
|
quest.level = level
|
|
end
|
|
|
|
quest.logIsComplete = isComplete
|
|
quest.isComplete = (isComplete == 1)
|
|
|
|
ReadFallbackObjectives(questLogIndex, quest.Id, quest.Objectives)
|
|
|
|
return true
|
|
end
|
|
|
|
-- Returns nil if the quest is not currently in the quest log.
|
|
function TrackerUtils:BuildFallbackQuest(questId)
|
|
local questLogIndex = GetQuestLogIndexForQuest(questId)
|
|
if not questLogIndex then return nil end
|
|
|
|
-- Walk backwards from the quest in the log to find the zone header.
|
|
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
|
|
local zoneText = nil
|
|
for h = questLogIndex, 1, -1 do
|
|
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
|
|
if hIsHeader and hTitle and hTitle ~= "" then
|
|
zoneText = hTitle
|
|
break
|
|
end
|
|
end
|
|
local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0
|
|
local zoneNameOverride = nil
|
|
if zoneText and (not zoneId or zoneId == 0) then
|
|
zoneNameOverride = zoneText
|
|
end
|
|
|
|
local quest = {
|
|
Id = questId,
|
|
name = "Quest " .. questId,
|
|
level = 0,
|
|
zoneOrSort = zoneId,
|
|
zoneName = zoneText,
|
|
zoneNameOverride = zoneNameOverride,
|
|
Objectives = {},
|
|
SpecialObjectives = {},
|
|
isFallback = true,
|
|
IsComplete = FallbackQuestIsComplete,
|
|
}
|
|
|
|
-- Same read the tracker does on every later draw, so a quest built here and one refreshed
|
|
-- from the cache carry exactly the same fields.
|
|
TrackerUtils:RefreshFallbackQuest(quest)
|
|
|
|
return quest
|
|
end
|
|
|
|
function TrackerUtils:GetSortedQuestIds()
|
|
local sortedQuestIds = {}
|
|
local questDetails = {}
|
|
local sortObj = Questie.db.profile.trackerSortObjectives
|
|
|
|
-- One walk of the quest log for the whole draw, so the per-quest lookups below are reads.
|
|
BuildQuestLogHeaders()
|
|
|
|
-- currentQuestlog is only as good as the removal events that maintain it, and a quest the
|
|
-- server finishes on its own -- Ascension's auto-complete quests -- can leave the log without
|
|
-- any of them landing, which strands the quest in the tracker for the rest of the session. The
|
|
-- log is the authority on what the player still has, so anything missing from it is skipped.
|
|
-- Skipped rather than pruned: a redraw that catches the log mid-refresh would otherwise throw
|
|
-- away state Questie is about to want back.
|
|
local questLogIsReadable = next(questLogQuestIds) ~= nil
|
|
-- Update quest objectives
|
|
|
|
for questId, quest in pairs(QuestiePlayer.currentQuestlog) do
|
|
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
|
|
else
|
|
-- Cached by an earlier draw, so its objectives and completion state are as
|
|
-- old as the cache. Nothing else updates them -- these quests have no DB
|
|
-- entry, so QuestieQuest's populate path skips them entirely.
|
|
TrackerUtils:RefreshFallbackQuest(fallback)
|
|
end
|
|
if fallback then
|
|
quest = fallback
|
|
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
|
|
|
|
|