perf: batch learner merges and guard debug scans

This commit is contained in:
Xurkon
2026-06-04 07:00:38 -05:00
parent 51561b97d8
commit 45357263c1
2 changed files with 143 additions and 41 deletions
+6
View File
@@ -128,14 +128,18 @@ local function loadFullDatabase()
print("\124cFF4DDBFF [1/9] " .. l10n("Loading database") .. "...") print("\124cFF4DDBFF [1/9] " .. l10n("Loading database") .. "...")
QuestieInit:LoadBaseDB() QuestieInit:LoadBaseDB()
if Questie.db and Questie.db.profile and Questie.db.profile.debugEnabled then
Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] After LoadBaseDB - quest:" .. _dbStats(QuestieDB.questData) .. " npc:" .. _dbStats(QuestieDB.npcData)) Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] After LoadBaseDB - quest:" .. _dbStats(QuestieDB.questData) .. " npc:" .. _dbStats(QuestieDB.npcData))
Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] obj:" .. _dbStats(QuestieDB.objectData) .. " item:" .. _dbStats(QuestieDB.itemData)) Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] obj:" .. _dbStats(QuestieDB.objectData) .. " item:" .. _dbStats(QuestieDB.itemData))
end
print("\124cFF4DDBFF [2/9] " .. l10n("Applying database corrections") .. "...") print("\124cFF4DDBFF [2/9] " .. l10n("Applying database corrections") .. "...")
coYield() coYield()
QuestieCorrections:Initialize() QuestieCorrections:Initialize()
if Questie.db and Questie.db.profile and Questie.db.profile.debugEnabled then
Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] After Corrections - quest:" .. _dbStats(QuestieDB.questData) .. " npc:" .. _dbStats(QuestieDB.npcData)) Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] After Corrections - quest:" .. _dbStats(QuestieDB.questData) .. " npc:" .. _dbStats(QuestieDB.npcData))
end
print("\124cFF4DDBFF [3/9] " .. l10n("Initializing townfolks") .. "...") print("\124cFF4DDBFF [3/9] " .. l10n("Initializing townfolks") .. "...")
coYield() coYield()
@@ -289,7 +293,9 @@ QuestieInit.Stages[1] = function() -- run as a coroutine
l10n("Questie DB has updated!") .. l10n("Questie DB has updated!") ..
"|r|cFFFF6F22 " .. l10n("Data is being processed, this may take a few moments and cause some lag...")) "|r|cFFFF6F22 " .. l10n("Data is being processed, this may take a few moments and cause some lag..."))
loadFullDatabase() loadFullDatabase()
if Questie.db and Questie.db.profile and Questie.db.profile.debugEnabled then
Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] Before Compile - quest:" .. _dbStats(QuestieDB.questData) .. " npc:" .. _dbStats(QuestieDB.npcData)) Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] Before Compile - quest:" .. _dbStats(QuestieDB.questData) .. " npc:" .. _dbStats(QuestieDB.npcData))
end
QuestieDBCompiler:Compile() QuestieDBCompiler:Compile()
Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] After Compile - quest:type=" .. type(QuestieDB.questData) .. " npc:type=" .. type(QuestieDB.npcData)) Questie:Debug(Questie.DEBUG_DEVELOP, "[DBDiag] After Compile - quest:type=" .. type(QuestieDB.questData) .. " npc:type=" .. type(QuestieDB.npcData))
dbCompiled = true dbCompiled = true
+127 -31
View File
@@ -123,7 +123,10 @@ _Learner.pendingNpcs = {}
_Learner.pendingQuests = {} _Learner.pendingQuests = {}
_Learner.pendingItems = {} _Learner.pendingItems = {}
_Learner.pendingObjects = {} _Learner.pendingObjects = {}
_Learner.pendingNetworkMerges = {}
_Learner.pendingItemLinks = {} -- queue for async GetItemInfo retries _Learner.pendingItemLinks = {} -- queue for async GetItemInfo retries
_Learner.npcNameIndex = nil
_Learner.npcNameIndexDirty = true
-- Direct reference to learnedData, set on Initialize -- Direct reference to learnedData, set on Initialize
QuestieLearner.data = nil QuestieLearner.data = nil
@@ -658,6 +661,51 @@ local function _QueueNpcLiveUpdate(npcId)
end end
end end
local function _MarkNpcNameIndexDirty()
_Learner.npcNameIndexDirty = true
end
local function _RebuildNpcNameIndex()
local overrideIndex = {}
local baseIndex = {}
if QuestieDB and QuestieDB.npcDataOverrides then
for npcId, data in pairs(QuestieDB.npcDataOverrides) do
local name = data and data[1]
if type(name) == "string" and name ~= "" then
overrideIndex[string.lower(name)] = npcId
end
end
end
if QuestieDB and QuestieDB.npcData then
for npcId, data in pairs(QuestieDB.npcData) do
local name = data and data[1]
if type(name) == "string" and name ~= "" then
local lowerName = string.lower(name)
if overrideIndex[lowerName] == nil and baseIndex[lowerName] == nil then
baseIndex[lowerName] = npcId
end
end
end
end
_Learner.npcNameIndex = {
override = overrideIndex,
base = baseIndex,
}
_Learner.npcNameIndexDirty = false
end
local function _EnsureNpcNameIndex()
if _Learner.npcNameIndex and not _Learner.npcNameIndexDirty then
return _Learner.npcNameIndex
end
_RebuildNpcNameIndex()
return _Learner.npcNameIndex
end
------------------------------------------------------------------------ ------------------------------------------------------------------------
-- CrossLinkAfterNPC: called when a new NPC is first learned. -- CrossLinkAfterNPC: called when a new NPC is first learned.
-- Scans all learned quests for any reference to this npcId and stitches -- Scans all learned quests for any reference to this npcId and stitches
@@ -963,7 +1011,10 @@ function QuestieLearner:LearnNPC(npcId, name, level, subName, npcFlags, factionS
Questie.dbLearner.global.npcs[npcId] = existing Questie.dbLearner.global.npcs[npcId] = existing
end end
if name and name ~= "" and (not existing[1] or existing[1] == "") then existing[1] = name end if name and name ~= "" and (not existing[1] or existing[1] == "") then
existing[1] = name
_MarkNpcNameIndexDirty()
end
if level then if level then
if not existing[4] or level < existing[4] then existing[4] = level end if not existing[4] or level < existing[4] then existing[4] = level end
if not existing[5] or level > existing[5] then existing[5] = level end if not existing[5] or level > existing[5] then existing[5] = level end
@@ -1997,6 +2048,7 @@ function QuestieLearner:InjectLearnedData()
-- 1. NPCs -- 1. NPCs
local npcIdsToFix = {} local npcIdsToFix = {}
local npcNameIndexNeedsRebuild = false
for npcId, data in pairs(learned.npcs) do for npcId, data in pairs(learned.npcs) do
local nid = tonumber(npcId) local nid = tonumber(npcId)
if type(npcId) == "string" and nid then if type(npcId) == "string" and nid then
@@ -2009,12 +2061,14 @@ function QuestieLearner:InjectLearnedData()
-- and can pollute curated plugin spawn tables. -- and can pollute curated plugin spawn tables.
QuestieDB.npcDataOverrides[nid or npcId] = CopyWithoutField(data, 7) QuestieDB.npcDataOverrides[nid or npcId] = CopyWithoutField(data, 7)
npcCount = npcCount + 1 npcCount = npcCount + 1
if data[1] then npcNameIndexNeedsRebuild = true end
else else
local existing = QuestieDB.npcDataOverrides[nid or npcId] local existing = QuestieDB.npcDataOverrides[nid or npcId]
-- Adopt other fields if missing -- Adopt other fields if missing
for k, v in pairs(data) do for k, v in pairs(data) do
if k ~= "mc" and k ~= 7 and existing[k] == nil and not IsAscensionProtected("NPC", nid or npcId, k) then if k ~= "mc" and k ~= 7 and existing[k] == nil and not IsAscensionProtected("NPC", nid or npcId, k) then
existing[k] = v existing[k] = v
if k == 1 then npcNameIndexNeedsRebuild = true end
end end
end end
end end
@@ -2024,6 +2078,7 @@ function QuestieLearner:InjectLearnedData()
learned.npcs[old] = nil learned.npcs[old] = nil
end end
if Questie.db and Questie.db.profile and Questie.db.profile.debugEnabled then
-- Diagnostic: log how many NPCs were injected with spawn overrides -- Diagnostic: log how many NPCs were injected with spawn overrides
local spawnOverrideCount = 0 local spawnOverrideCount = 0
for nid, ovr in pairs(QuestieDB.npcDataOverrides) do for nid, ovr in pairs(QuestieDB.npcDataOverrides) do
@@ -2032,6 +2087,10 @@ function QuestieLearner:InjectLearnedData()
end end
end end
Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieLearner] InjectLearnedData: injected", npcCount, "NPCs (", spawnOverrideCount, "with spawn overrides)") Questie:Debug(Questie.DEBUG_CRITICAL, "[QuestieLearner] InjectLearnedData: injected", npcCount, "NPCs (", spawnOverrideCount, "with spawn overrides)")
end
if npcNameIndexNeedsRebuild then
_MarkNpcNameIndexDirty()
end
-- Purge garbage quest entries: quests with no name [1] and only mc/ls metadata. -- Purge garbage quest entries: quests with no name [1] and only mc/ls metadata.
-- These are Ascension internal tracking artifacts (hash-like IDs) with no real quest data. -- These are Ascension internal tracking artifacts (hash-like IDs) with no real quest data.
@@ -2523,30 +2582,16 @@ function QuestieLearner:OnQuestComplete()
end end
end end
-- Helper to find an NPC ID by name (case-insensitive substring match) -- Helper to find an NPC ID by name (case-insensitive exact match)
-- Used for proactive objective mapping when a quest is first accepted. -- Used for proactive objective mapping when a quest is first accepted.
function QuestieLearner:GetNPCIdByName(npcName) function QuestieLearner:GetNPCIdByName(npcName)
if not npcName or npcName == "" then return nil end if not npcName or npcName == "" then return nil end
local lowerName = string.lower(npcName) local lowerName = string.lower(npcName)
-- Check overrides first (most likely for custom servers) local index = _EnsureNpcNameIndex()
if QuestieDB.npcDataOverrides then local overrideId = index.override[lowerName]
for id, data in pairs(QuestieDB.npcDataOverrides) do if overrideId then return overrideId end
if data and data[1] and string.lower(data[1]) == lowerName then return index.base[lowerName]
return id
end
end
end
-- Check base NPC database
-- Note: QueryNPCSingle is a dummy until initialization, but we can fallback to raw access
local npcData = QuestieDB.npcData or {}
for id, data in pairs(npcData) do
if data and data[1] and string.lower(data[1]) == lowerName then
return id
end
end
return nil
end end
-- Fires when a quest is accepted. -- Fires when a quest is accepted.
@@ -3664,42 +3709,89 @@ function _Learner:BroadcastIfCommsAvailable(typ, id, data)
end end
end end
local NETWORK_MERGE_DELAY = 0.5
local function _QueueIncomingNetworkMerge(typ, id, data, op)
_Learner.pendingNetworkMerges = _Learner.pendingNetworkMerges or {}
local key = typ .. ":" .. tostring(id)
_Learner.pendingNetworkMerges[key] = {
typ = typ,
id = id,
data = data,
op = op,
}
if _Learner.pendingNetworkMergeTimer then return end
local timer = QuestieCompat and QuestieCompat.C_Timer
local function FlushNetworkMerges()
local pending = _Learner.pendingNetworkMerges
_Learner.pendingNetworkMerges = {}
_Learner.pendingNetworkMergeTimer = nil
local anyChanged = false
for _, entry in pairs(pending) do
local changed = QuestieLearner:_ApplyIncomingNetworkMerge(entry.typ, entry.id, entry.data, entry.op)
anyChanged = anyChanged or changed
end
if anyChanged then
QuestieLearner:InjectLearnedData()
QuestieLearner.data = Questie.dbLearner.global
end
end
if timer and timer.After then
_Learner.pendingNetworkMergeTimer = true
timer.After(NETWORK_MERGE_DELAY, FlushNetworkMerges)
else
FlushNetworkMerges()
end
end
-- Receives validated, decoded data from QuestieLearnerComms or QuestieLearnerExport:MergeImport -- Receives validated, decoded data from QuestieLearnerComms or QuestieLearnerExport:MergeImport
function QuestieLearner:HandleNetworkData(typ, id, d, op) function QuestieLearner:HandleNetworkData(typ, id, d, op)
if not self:IsEnabled() then return end if not self:IsEnabled() then return end
if not EnsureLearnedData() then return end if not EnsureLearnedData() then return end
if not typ or not id or not d then return end if not typ or not id or not d then return end
_QueueIncomingNetworkMerge(typ, id, d, op)
end
function QuestieLearner:_ApplyIncomingNetworkMerge(typ, id, d, op)
if not typ or not id or not d then return false end
local store local store
if typ == "NPC" then if typ == "NPC" then
if not Questie.dbLearner.global.settings.learnNpcs then return end if not Questie.dbLearner.global.settings.learnNpcs then return false end
store = Questie.dbLearner.global.npcs store = Questie.dbLearner.global.npcs
elseif typ == "QUEST" then elseif typ == "QUEST" then
if not Questie.dbLearner.global.settings.learnQuests then return end if not Questie.dbLearner.global.settings.learnQuests then return false end
store = Questie.dbLearner.global.quests store = Questie.dbLearner.global.quests
elseif typ == "ITEM" then elseif typ == "ITEM" then
if not Questie.dbLearner.global.settings.learnItems then return end if not Questie.dbLearner.global.settings.learnItems then return false end
store = Questie.dbLearner.global.items store = Questie.dbLearner.global.items
elseif typ == "OBJECT" then elseif typ == "OBJECT" then
if not Questie.dbLearner.global.settings.learnObjects then return end if not Questie.dbLearner.global.settings.learnObjects then return false end
store = Questie.dbLearner.global.objects store = Questie.dbLearner.global.objects
else else
return return false
end end
-- Validate external data before merging to prevent crash on malformed input -- Validate external data before merging to prevent crash on malformed input
if not _ValidateLearnedSpawnData(d) then if not _ValidateLearnedSpawnData(d) then
Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Rejected malformed network data", typ, id) Questie:Debug(Questie.DEBUG_LEARNER, "[QuestieLearner] Rejected malformed network data", typ, id)
return return false
end end
local existing = store[id] local existing = store[id]
if not existing then if not existing then
store[id] = d store[id] = d
store[id].mc = 1 store[id].mc = 1
self:InjectLearnedData() if typ == "NPC" and type(d[1]) == "string" and d[1] ~= "" then
QuestieLearner.data = Questie.dbLearner.global _MarkNpcNameIndexDirty()
return end
return true
end end
local changed = false local changed = false
@@ -3708,6 +3800,9 @@ function QuestieLearner:HandleNetworkData(typ, id, d, op)
if k ~= "mc" and existing[k] == nil then if k ~= "mc" and existing[k] == nil then
existing[k] = v existing[k] = v
changed = true changed = true
if typ == "NPC" and k == 1 and type(v) == "string" and v ~= "" then
_MarkNpcNameIndexDirty()
end
end end
end end
@@ -3744,9 +3839,10 @@ function QuestieLearner:HandleNetworkData(typ, id, d, op)
if changed or (op == "NEW" or op == "UPDATE") then if changed or (op == "NEW" or op == "UPDATE") then
existing.ls = time() -- Refresh timestamp on network confirmation existing.ls = time() -- Refresh timestamp on network confirmation
existing.mc = (existing.mc or 0) + 1 existing.mc = (existing.mc or 0) + 1
QuestieLearner.data = Questie.dbLearner.global return true
self:InjectLearnedData()
end end
return false
end end
return QuestieLearner return QuestieLearner