Files
Questie-X/Modules/QuestieValidateGameCache.lua
T

185 lines
6.3 KiB
Lua

--[[
Flow:
-> QuestieValidateGameCache.StartCheck()
--> Wait for PLAYER_ENTERING_WORLD
---> Wait For QUEST_LOG_UPDATE (2x at login, 1x at reload)
----> Game cache should have all quests in it, data of each quest may be invalid.
If data is invalid, Wait for next QUEST_LOG_UPDATE and check again.
-----> Game Cache ok. Call possible callback functions.
]] --
---@class QuestieValidateGameCache
local QuestieValidateGameCache = QuestieLoader:CreateModule("QuestieValidateGameCache")
local QuestieLib = QuestieLoader:ImportModule("QuestieLib")
local QuestieCompat = QuestieLoader:ImportModule("QuestieCompat")
-- Defer API assignments to runtime to avoid load-order nil errors
local stringByte, tremove = string.byte, table.remove
local tpack = QuestieLib.tpack
local tunpack = QuestieLib.tunpack
-- 3 * (Max possible number of quests in game quest log)
-- This is a safe value, even smaller would be enough. Too large won't effect performance
local numberOfQuestLogUpdatesToSkip = 0
local checkStarted = false
local eventFrame = nil
local callbacks = {}
local isCacheGood = false
local function DestroyEventFrame()
if eventFrame then
eventFrame:UnregisterAllEvents()
eventFrame:SetScript("OnEvent", nil)
eventFrame = nil
end
end
local function OnQuestLogUpdate()
-- Fetch APIs at runtime with extreme defensive checks
local qCompat = QuestieCompat or _G.QuestieCompat
local rawCLog = qCompat and rawget(qCompat, "C_QuestLog")
local cLog = (qCompat and qCompat.C_QuestLog) or {}
local GetNumQuestLogEntries = cLog.GetNumQuestLogEntries or _G.GetNumQuestLogEntries
local GetQuestLogTitle = cLog.GetQuestLogTitle or _G.GetQuestLogTitle
local GetNumQuestLeaderBoards = _G.GetNumQuestLeaderBoards
local GetQuestObjectives = cLog.GetQuestObjectives or function() return {} end
if isCacheGood then
DestroyEventFrame()
return
end
if numberOfQuestLogUpdatesToSkip > 0 then
numberOfQuestLogUpdatesToSkip = numberOfQuestLogUpdatesToSkip - 1
return
end
local isQuestLogGood = true
local numQuests = select(1, GetNumQuestLogEntries()) or 0
local goodQuestsCount = 0
for i = 1, numQuests do
local status, err = pcall(function()
local title, _, _, _, isHeader, _, _, _, questId = GetQuestLogTitle(i)
if title and (not isHeader) and questId and questId > 0 then
local numObjectives = GetNumQuestLeaderBoards(i) or 0
if numObjectives > 0 then
local objectiveList = GetQuestObjectives(questId, i)
if objectiveList and objectiveList[1] then
local hasInvalidObjective = false
for _, objective in pairs(objectiveList) do
-- Fix: Only fail if text is nil or empty.
-- Leading spaces (ASCII 32) are common on some servers/quests and shouldn't block initialization.
-- Ghost quests (removed from DB but still in log) may have no text - skip those silently.
if (not objective.text) or (objective.text == "") then
hasInvalidObjective = true
break
end
end
if not hasInvalidObjective then
goodQuestsCount = goodQuestsCount + 1
end
-- Don't fail validation for ghost quests with empty text, just skip them
else
-- Quest has objectives according to game but GetQuestObjectives returns nothing
-- This is likely a ghost quest, skip it
end
else
goodQuestsCount = goodQuestsCount + 1
end
end
end)
end
if not isQuestLogGood then
Questie:Debug(Questie.DEBUG_INFO, "[QuestieValidateGameCache] Quest log is NOT yet okey. Good quest:",
goodQuestsCount .. "/" .. numQuests)
return
end
DestroyEventFrame()
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieValidateGameCache] Quest log is ok. Good quest:",
goodQuestsCount .. "/" .. numQuests)
isCacheGood = true
while (#callbacks > 0) do
local callback = tremove(callbacks, 1)
local func, args = callback[1], callback[2]
func(tunpack(args))
end
end
local function OnPlayerEnteringWorld(_, _, isInitialLogin, isReloadingUi)
if isInitialLogin == nil then isInitialLogin = true end
if isReloadingUi == nil then isReloadingUi = false end
if QuestieCompat.Is335 then
isInitialLogin, isReloadingUi = false, true
end
numberOfQuestLogUpdatesToSkip = isInitialLogin and 1 or 0
if not eventFrame then eventFrame = CreateFrame("Frame") end
eventFrame:UnregisterAllEvents()
eventFrame:SetScript("OnEvent", OnQuestLogUpdate)
eventFrame:RegisterEvent("QUEST_LOG_UPDATE")
end
function QuestieValidateGameCache.StartCheck()
if checkStarted then return end
checkStarted = true
if not eventFrame then eventFrame = CreateFrame("Frame") end
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:SetScript("OnEvent", OnPlayerEnteringWorld)
if IsLoggedIn() then
OnPlayerEnteringWorld(eventFrame, "PLAYER_ENTERING_WORLD", true, false)
end
local function backupCheck()
if isCacheGood then return end
OnQuestLogUpdate()
if not isCacheGood then
local qCompat = QuestieCompat or _G.QuestieCompat
local timer = (qCompat and qCompat.C_QuestLog and qCompat.C_Timer) or _G.C_Timer
if timer and timer.After then
timer.After(2.0, backupCheck)
end
end
end
local qCompat = QuestieCompat or _G.QuestieCompat
local timer = (qCompat and qCompat.C_QuestLog and qCompat.C_Timer) or _G.C_Timer
if timer and timer.After then
timer.After(2.0, backupCheck)
end
end
function QuestieValidateGameCache.IsCacheGood()
return isCacheGood
end
function QuestieValidateGameCache.RegisterCallback(func, ...)
if isCacheGood then
func(...)
else
table.insert(callbacks, { func, tpack(...) })
end
end