Compare commits
2 Commits
cf67ceb474
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fe4b83a9 | |||
| 26a135a2ca |
+43
-5
@@ -1785,16 +1785,36 @@ function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
-- Build a minimal live-fallback quest from the quest log so the tracker still works
|
||||
-- Build a minimal live-fallback quest from the quest log so the tracker still works.
|
||||
-- The override that got us here is partial by nature -- a Learner record of a single
|
||||
-- field, a correction -- but whatever it does carry beats guessing, and the quest object
|
||||
-- built here is cached for the session, so anything left blank stays blank.
|
||||
local logEntry = QuestLogCache.GetQuest(questId)
|
||||
if not logEntry then return nil end
|
||||
local function NonEmpty(text)
|
||||
if text and text ~= "" then return text end
|
||||
return nil
|
||||
end
|
||||
local overrideName = NonEmpty(overrideData[QuestieDB.questKeys.name])
|
||||
local overrideLevel = overrideData[QuestieDB.questKeys.questLevel]
|
||||
local overrideZone = overrideData[QuestieDB.questKeys.zoneOrSort]
|
||||
local cachedTitle = NonEmpty(logEntry.title)
|
||||
local liveTitle
|
||||
if (not overrideName) and (not cachedTitle) and GetQuestLogIndexByID and GetQuestLogTitle then
|
||||
-- QuestLogCache is a snapshot and can be missing the title of a quest the client
|
||||
-- has since filled in, which is what leaves a quest showing as its own id.
|
||||
local questLogIndex = GetQuestLogIndexByID(questId)
|
||||
if questLogIndex and questLogIndex > 0 then
|
||||
liveTitle = NonEmpty(GetQuestLogTitle(questLogIndex))
|
||||
end
|
||||
end
|
||||
local fallback = {
|
||||
Id = questId,
|
||||
name = logEntry.title or tostring(questId),
|
||||
level = logEntry.level or 0,
|
||||
questLevel = logEntry.level or 0,
|
||||
name = overrideName or cachedTitle or liveTitle or tostring(questId),
|
||||
level = overrideLevel or logEntry.level or 0,
|
||||
questLevel = overrideLevel or logEntry.level or 0,
|
||||
requiredLevel = 0,
|
||||
zoneOrSort = 0,
|
||||
zoneOrSort = overrideZone or 0,
|
||||
questFlags = 0,
|
||||
specialFlags = 0,
|
||||
Starts = { CreatureStarts = {}, ObjectStarts = {}, ItemStarts = {} },
|
||||
@@ -1916,6 +1936,24 @@ function QuestieDB.GetQuest(questId, ...) -- /dump QuestieDB.GetQuest(867)
|
||||
if learnerRecord.objIndex and not QO.objIndex then QO.objIndex = learnerRecord.objIndex end
|
||||
end
|
||||
|
||||
-- A record that never captured a name -- a Learner entry written from an objective update, an
|
||||
-- override carrying a single key -- leaves QO.name nil, and this object is cached for the rest
|
||||
-- of the session, so everything downstream ends up printing the quest id. Ask the client.
|
||||
if ((not QO.name) or QO.name == "") and GetQuestLogIndexByID and GetQuestLogTitle then
|
||||
local questLogIndex = GetQuestLogIndexByID(questId)
|
||||
if questLogIndex and questLogIndex > 0 then
|
||||
local logTitle = GetQuestLogTitle(questLogIndex)
|
||||
if logTitle and logTitle ~= "" then
|
||||
QO.name = logTitle
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Same story for the zone: partial records leave it nil, and callers all treat it as a number
|
||||
-- (`quest.zoneOrSort > 0` errors outright on nil). 0 is the value everything already reads as
|
||||
-- "no zone on file", which sends the tracker to the quest log for one.
|
||||
QO.zoneOrSort = QO.zoneOrSort or 0
|
||||
|
||||
local questLevel, requiredLevel = QuestieLib.GetTbcLevel(questId)
|
||||
QO.level = questLevel
|
||||
QO.requiredLevel = requiredLevel
|
||||
|
||||
@@ -63,6 +63,22 @@ local function GetWrappedWidth(label)
|
||||
return label:GetWidth()
|
||||
end
|
||||
|
||||
-- A quest built from partial data can reach the tracker with no name, or with the id standing in
|
||||
-- for one, and it is cached that way for the session. Ask the quest log before printing a number.
|
||||
local function GetDisplayableQuestName(quest)
|
||||
local questName = quest.name
|
||||
|
||||
if (not questName) or questName == "" or questName == tostring(quest.Id) then
|
||||
questName = TrackerUtils:GetQuestLogTitleById(quest.Id) or questName
|
||||
end
|
||||
|
||||
if (not questName) or questName == "" then
|
||||
questName = tostring(quest.Id)
|
||||
end
|
||||
|
||||
return questName
|
||||
end
|
||||
|
||||
local LSM30 = LibStub and LibStub("LibSharedMedia-3.0", true)
|
||||
|
||||
-- Local Vars
|
||||
@@ -1031,7 +1047,7 @@ function QuestieTracker:Update()
|
||||
|
||||
if quest.isFallback or quest._isLogFallback then
|
||||
-- Quest not in DB: use the name stored on the fallback object
|
||||
local questName = quest.name or tostring(quest.Id)
|
||||
local questName = GetDisplayableQuestName(quest)
|
||||
if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
|
||||
questName = "[" .. quest.level .. "] " .. questName
|
||||
end
|
||||
@@ -1065,7 +1081,7 @@ function QuestieTracker:Update()
|
||||
-- which queries the static DB only: for a quest with no DB entry it
|
||||
-- silently defaults to level 1, and Ascension's scaling then scales
|
||||
-- that wrong base instead of the quest's real level.
|
||||
local fallbackName = quest.name or tostring(quest.Id)
|
||||
local fallbackName = GetDisplayableQuestName(quest)
|
||||
if Questie.db.profile.trackerShowQuestLevel and quest.level and quest.level > 0 then
|
||||
fallbackName = QuestieLib:GetQuestString(quest.Id, fallbackName, quest.level, false)
|
||||
end
|
||||
|
||||
@@ -685,9 +685,14 @@ local questLogHeaders = {}
|
||||
-- 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
|
||||
@@ -698,6 +703,7 @@ local function BuildQuestLogHeaders()
|
||||
end
|
||||
elseif logId then
|
||||
questIds[logId] = true
|
||||
indexes[logId] = i
|
||||
if header then
|
||||
headers[logId] = header
|
||||
end
|
||||
@@ -706,6 +712,87 @@ local function BuildQuestLogHeaders()
|
||||
|
||||
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.
|
||||
@@ -722,7 +809,13 @@ local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
|
||||
if zoneNameOverride and zoneNameOverride ~= "" then
|
||||
return zoneNameOverride
|
||||
end
|
||||
if not zoneOrSort then return "Unknown Zone" end
|
||||
|
||||
-- A quest assembled from a partial record -- a Learner entry that never captured the field,
|
||||
-- an override carrying a single key -- reaches here with no zoneOrSort at all. That is the
|
||||
-- same "nothing to look up" case as 0, and the quest log below still knows where the client
|
||||
-- files the quest, so it must not short-circuit to Unknown Zone ahead of that.
|
||||
zoneOrSort = zoneOrSort or 0
|
||||
|
||||
local zoneName
|
||||
local sortObj = Questie.db.profile.trackerSortObjectives
|
||||
if sortObj == "byZone" or sortObj == "byZoneComplete" or sortObj == "byZoneCompleteReversed" or sortObj == "byZonePlayerProximity" or sortObj == "byZonePlayerProximityReversed" then
|
||||
@@ -778,67 +871,102 @@ local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
|
||||
return zoneName
|
||||
end
|
||||
|
||||
-- The client's own title for a quest, or nil if the player does not have it. Quest objects can
|
||||
-- reach the tracker without a usable name -- a questDataOverrides entry that carries no name
|
||||
-- field, a QuestLogCache row read before the client had filled the title in -- and since those
|
||||
-- objects are cached for the session, the name never repairs itself. The log always knows.
|
||||
function TrackerUtils:GetQuestLogTitleById(questId)
|
||||
local questLogIndex = GetQuestLogIndexForQuest(questId)
|
||||
if not questLogIndex then return nil end
|
||||
|
||||
local title = GetQuestLogTitle(questLogIndex)
|
||||
if title and title ~= "" then
|
||||
return title
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
-- IsComplete must be a method (called as quest:IsComplete()), and it reads the state the last
|
||||
-- refresh stored rather than closing over the state at build time -- these quests outlive many
|
||||
-- redraws, and a captured value would still claim the quest is unfinished after it is turned in.
|
||||
local function FallbackQuestIsComplete(self)
|
||||
if self.logIsComplete == 1 or self.logIsComplete == true then
|
||||
return 1
|
||||
end
|
||||
|
||||
if QuestiePlayer.currentQuestlog[self.Id] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(self.Id) then
|
||||
return 1
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Re-reads everything the quest log owns: title, level, completion and the objectives. The
|
||||
-- tracker keeps fallback quests between draws, so without this they stay frozen at whatever
|
||||
-- the log said when the quest was first seen.
|
||||
---@return boolean @false if the player no longer has the quest
|
||||
function TrackerUtils:RefreshFallbackQuest(quest)
|
||||
if not quest then return false end
|
||||
|
||||
local questLogIndex = GetQuestLogIndexForQuest(quest.Id)
|
||||
if not questLogIndex then return false end
|
||||
|
||||
local title, level, _, _, _, isComplete = GetQuestLogTitle(questLogIndex)
|
||||
if title and title ~= "" then
|
||||
quest.name = title
|
||||
end
|
||||
if level and level > 0 then
|
||||
quest.level = level
|
||||
end
|
||||
|
||||
quest.logIsComplete = isComplete
|
||||
quest.isComplete = (isComplete == 1)
|
||||
|
||||
ReadFallbackObjectives(questLogIndex, quest.Id, quest.Objectives)
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- Returns nil if the quest is not currently in the quest log.
|
||||
function TrackerUtils:BuildFallbackQuest(questId)
|
||||
for i = 1, GetNumQuestLogEntries() do
|
||||
local title, level, _, isHeader, _, isComplete, _, logQuestId = GetQuestLogTitle(i)
|
||||
if not isHeader and logQuestId == questId then
|
||||
-- Parse objectives from the leaderboard
|
||||
local objectives = {}
|
||||
local numObj = GetNumQuestLeaderBoards and GetNumQuestLeaderBoards(i) or 0
|
||||
for j = 1, numObj do
|
||||
local text, _, finished = GetQuestLogLeaderBoard(j, i)
|
||||
if text then
|
||||
-- Parse "Description: X/Y" or just "Description"
|
||||
local collected, needed = string.match(text, ":.-(%d+)/(%d+)%s*$")
|
||||
collected = tonumber(collected) or (finished and 1 or 0)
|
||||
needed = tonumber(needed) or 1
|
||||
objectives[j] = {
|
||||
text = text,
|
||||
Needed = needed,
|
||||
Collected = collected,
|
||||
Finished = finished or (collected >= needed),
|
||||
Type = "fallback",
|
||||
}
|
||||
end
|
||||
end
|
||||
local questLogIndex = GetQuestLogIndexForQuest(questId)
|
||||
if not questLogIndex then return nil end
|
||||
|
||||
-- Walk backwards from i in the quest log to find the zone header.
|
||||
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
|
||||
local zoneText = nil
|
||||
for h = i, 1, -1 do
|
||||
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
|
||||
if hIsHeader and hTitle and hTitle ~= "" then
|
||||
zoneText = hTitle
|
||||
break
|
||||
end
|
||||
end
|
||||
local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0
|
||||
local zoneNameOverride = nil
|
||||
if zoneText and (not zoneId or zoneId == 0) then
|
||||
zoneNameOverride = zoneText
|
||||
end
|
||||
|
||||
local quest = {
|
||||
Id = questId,
|
||||
name = title or ("Quest " .. questId),
|
||||
level = level or 0,
|
||||
zoneOrSort = zoneId,
|
||||
zoneName = zoneText,
|
||||
zoneNameOverride = zoneNameOverride,
|
||||
Objectives = objectives,
|
||||
SpecialObjectives = {},
|
||||
isFallback = true,
|
||||
}
|
||||
-- IsComplete must be a method (called as quest:IsComplete())
|
||||
quest.IsComplete = function(self)
|
||||
return (isComplete == 1 or (QuestiePlayer.currentQuestlog[questId] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(questId))) and 1 or 0
|
||||
end
|
||||
|
||||
return quest
|
||||
-- Walk backwards from the quest in the log to find the zone header.
|
||||
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
|
||||
local zoneText = nil
|
||||
for h = questLogIndex, 1, -1 do
|
||||
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
|
||||
if hIsHeader and hTitle and hTitle ~= "" then
|
||||
zoneText = hTitle
|
||||
break
|
||||
end
|
||||
end
|
||||
return nil
|
||||
local zoneId = (zoneText and GetAreaIdByZoneName(zoneText)) or 0
|
||||
local zoneNameOverride = nil
|
||||
if zoneText and (not zoneId or zoneId == 0) then
|
||||
zoneNameOverride = zoneText
|
||||
end
|
||||
|
||||
local quest = {
|
||||
Id = questId,
|
||||
name = "Quest " .. questId,
|
||||
level = 0,
|
||||
zoneOrSort = zoneId,
|
||||
zoneName = zoneText,
|
||||
zoneNameOverride = zoneNameOverride,
|
||||
Objectives = {},
|
||||
SpecialObjectives = {},
|
||||
isFallback = true,
|
||||
IsComplete = FallbackQuestIsComplete,
|
||||
}
|
||||
|
||||
-- Same read the tracker does on every later draw, so a quest built here and one refreshed
|
||||
-- from the cache carry exactly the same fields.
|
||||
TrackerUtils:RefreshFallbackQuest(quest)
|
||||
|
||||
return quest
|
||||
end
|
||||
|
||||
function TrackerUtils:GetSortedQuestIds()
|
||||
@@ -921,6 +1049,11 @@ function TrackerUtils:GetSortedQuestIds()
|
||||
if fallback then
|
||||
TrackerUtils._fallbackQuests[qid] = fallback
|
||||
end
|
||||
else
|
||||
-- Cached by an earlier draw, so its objectives and completion state are as
|
||||
-- old as the cache. Nothing else updates them -- these quests have no DB
|
||||
-- entry, so QuestieQuest's populate path skips them entirely.
|
||||
TrackerUtils:RefreshFallbackQuest(fallback)
|
||||
end
|
||||
if fallback then
|
||||
quest = fallback
|
||||
|
||||
Reference in New Issue
Block a user