fix(tracker): show and update objectives on quests missing from the DB
Quests with no QuestieDB entry get a fallback object built from the quest log, and three things were wrong with it. Its objectives carried the leaderboard line in a `text` field, but the tracker's objective loop skips any objective without a `Description`, so those quests drew a title and nothing under it. Split the line into a description and its counts instead, and set the same `Index`/`Completed` fields a DB objective carries. The object was then cached for the session and only rebuilt when it had no zone info, so its counts stayed at whatever the log said when the quest was first seen. `IsComplete` had the same problem one level down: it closed over the completion flag read at build time, so the quest never went (Complete). Both now re-read the log on every draw, in place, so the objective tables the drawn lines hold stay valid. Refreshing per draw needs the quest's log index, so the walk that already builds the header map records it too.
This commit is contained in:
@@ -685,9 +685,14 @@ local questLogHeaders = {}
|
|||||||
-- and this is what says whether the player still has a given quest.
|
-- and this is what says whether the player still has a given quest.
|
||||||
local questLogQuestIds = {}
|
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 function BuildQuestLogHeaders()
|
||||||
local headers = {}
|
local headers = {}
|
||||||
local questIds = {}
|
local questIds = {}
|
||||||
|
local indexes = {}
|
||||||
local header
|
local header
|
||||||
|
|
||||||
for i = 1, (GetNumQuestLogEntries and GetNumQuestLogEntries() or 0) do
|
for i = 1, (GetNumQuestLogEntries and GetNumQuestLogEntries() or 0) do
|
||||||
@@ -698,6 +703,7 @@ local function BuildQuestLogHeaders()
|
|||||||
end
|
end
|
||||||
elseif logId then
|
elseif logId then
|
||||||
questIds[logId] = true
|
questIds[logId] = true
|
||||||
|
indexes[logId] = i
|
||||||
if header then
|
if header then
|
||||||
headers[logId] = header
|
headers[logId] = header
|
||||||
end
|
end
|
||||||
@@ -706,6 +712,87 @@ local function BuildQuestLogHeaders()
|
|||||||
|
|
||||||
questLogHeaders = headers
|
questLogHeaders = headers
|
||||||
questLogQuestIds = questIds
|
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
|
end
|
||||||
|
|
||||||
-- Returns the header title string, or nil if the quest is not in the log under one.
|
-- Returns the header title string, or nil if the quest is not in the log under one.
|
||||||
@@ -778,35 +865,56 @@ local function _GetZoneName(zoneOrSort, questId, zoneNameOverride)
|
|||||||
return zoneName
|
return zoneName
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Returns nil if the quest is not currently in the quest log.
|
-- IsComplete must be a method (called as quest:IsComplete()), and it reads the state the last
|
||||||
function TrackerUtils:BuildFallbackQuest(questId)
|
-- refresh stored rather than closing over the state at build time -- these quests outlive many
|
||||||
for i = 1, GetNumQuestLogEntries() do
|
-- redraws, and a captured value would still claim the quest is unfinished after it is turned in.
|
||||||
local title, level, _, isHeader, _, isComplete, _, logQuestId = GetQuestLogTitle(i)
|
local function FallbackQuestIsComplete(self)
|
||||||
if not isHeader and logQuestId == questId then
|
if self.logIsComplete == 1 or self.logIsComplete == true then
|
||||||
-- Parse objectives from the leaderboard
|
return 1
|
||||||
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
|
end
|
||||||
|
|
||||||
-- Walk backwards from i in the quest log to find the zone header.
|
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.
|
-- This is the canonical 3.3.5 method: zone headers sit above their quests.
|
||||||
local zoneText = nil
|
local zoneText = nil
|
||||||
for h = i, 1, -1 do
|
for h = questLogIndex, 1, -1 do
|
||||||
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
|
local hTitle, _, _, hIsHeader = GetQuestLogTitle(h)
|
||||||
if hIsHeader and hTitle and hTitle ~= "" then
|
if hIsHeader and hTitle and hTitle ~= "" then
|
||||||
zoneText = hTitle
|
zoneText = hTitle
|
||||||
@@ -821,24 +929,22 @@ function TrackerUtils:BuildFallbackQuest(questId)
|
|||||||
|
|
||||||
local quest = {
|
local quest = {
|
||||||
Id = questId,
|
Id = questId,
|
||||||
name = title or ("Quest " .. questId),
|
name = "Quest " .. questId,
|
||||||
level = level or 0,
|
level = 0,
|
||||||
zoneOrSort = zoneId,
|
zoneOrSort = zoneId,
|
||||||
zoneName = zoneText,
|
zoneName = zoneText,
|
||||||
zoneNameOverride = zoneNameOverride,
|
zoneNameOverride = zoneNameOverride,
|
||||||
Objectives = objectives,
|
Objectives = {},
|
||||||
SpecialObjectives = {},
|
SpecialObjectives = {},
|
||||||
isFallback = true,
|
isFallback = true,
|
||||||
|
IsComplete = FallbackQuestIsComplete,
|
||||||
}
|
}
|
||||||
-- IsComplete must be a method (called as quest:IsComplete())
|
|
||||||
quest.IsComplete = function(self)
|
-- Same read the tracker does on every later draw, so a quest built here and one refreshed
|
||||||
return (isComplete == 1 or (QuestiePlayer.currentQuestlog[questId] and IsQuestFlaggedCompleted and IsQuestFlaggedCompleted(questId))) and 1 or 0
|
-- from the cache carry exactly the same fields.
|
||||||
end
|
TrackerUtils:RefreshFallbackQuest(quest)
|
||||||
|
|
||||||
return quest
|
return quest
|
||||||
end
|
|
||||||
end
|
|
||||||
return nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function TrackerUtils:GetSortedQuestIds()
|
function TrackerUtils:GetSortedQuestIds()
|
||||||
@@ -921,6 +1027,11 @@ function TrackerUtils:GetSortedQuestIds()
|
|||||||
if fallback then
|
if fallback then
|
||||||
TrackerUtils._fallbackQuests[qid] = fallback
|
TrackerUtils._fallbackQuests[qid] = fallback
|
||||||
end
|
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
|
end
|
||||||
if fallback then
|
if fallback then
|
||||||
quest = fallback
|
quest = fallback
|
||||||
|
|||||||
Reference in New Issue
Block a user